Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70,214 changes: 35,108 additions & 35,106 deletions mobile/.tamagui/tamagui.config.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import { router, useLocalSearchParams } from "expo-router";
import { Screen } from "../../../../../../../components/Screen";
import Header from "../../../../../../../components/Header";
import { Icon } from "../../../../../../../components/Icon";
import { useUserData } from "../../../../../../../contexts/user/UserContext.provider";
import { Typography } from "../../../../../../../components/Typography";
import { ScrollView, Spinner, useWindowDimensions, YStack } from "tamagui";
import { useMemo, useState } from "react";
import { ListView } from "../../../../../../../components/ListView";
import OptionsSheet from "../../../../../../../components/OptionsSheet";
import ChangeLanguageDialog from "../../../../../../../components/ChangeLanguageDialog";
import { useTranslation } from "react-i18next";
import { RefreshControl } from "react-native";
import { ScrollView, Spinner, useWindowDimensions, YStack } from "tamagui";
import { setFormLanguagePreference } from "../../../../../../../common/language.preferences";
import { useFormById } from "../../../../../../../services/queries/forms.query";
import { useFormSubmissionByFormId } from "../../../../../../../services/queries/form-submissions.query";
import ChangeLanguageDialog from "../../../../../../../components/ChangeLanguageDialog";
import FormOverview from "../../../../../../../components/FormOverview";
import FormQuestionListItem, {
FormQuestionListItemProps,
QuestionStatus,
} from "../../../../../../../components/FormQuestionListItem";
import FormOverview from "../../../../../../../components/FormOverview";
import { useTranslation } from "react-i18next";
import Header from "../../../../../../../components/Header";
import { Icon } from "../../../../../../../components/Icon";
import { ListView } from "../../../../../../../components/ListView";
import OptionsSheet from "../../../../../../../components/OptionsSheet";
import { Screen } from "../../../../../../../components/Screen";
import SearchInput from "../../../../../../../components/SearchInput";
import { Typography } from "../../../../../../../components/Typography";
import WarningDialog from "../../../../../../../components/WarningDialog";
import { useNetInfoContext } from "../../../../../../../contexts/net-info-banner/NetInfoContext";
import { useUserData } from "../../../../../../../contexts/user/UserContext.provider";
import { shouldDisplayQuestion } from "../../../../../../../services/form.parser";
import {
useFormSubmissionMutation,
useMarkFormSubmissionCompletionStatusMutation,
} from "../../../../../../../services/mutations/form-submission.mutation";
import { shouldDisplayQuestion } from "../../../../../../../services/form.parser";
import WarningDialog from "../../../../../../../components/WarningDialog";
import { useAttachments } from "../../../../../../../services/queries/attachments.query";
import { useFormSubmissionByFormId } from "../../../../../../../services/queries/form-submissions.query";
import { useFormById } from "../../../../../../../services/queries/forms.query";
import { useNotesForFormId } from "../../../../../../../services/queries/notes.query";
import { RefreshControl } from "react-native";
import { useNetInfoContext } from "../../../../../../../contexts/net-info-banner/NetInfoContext";

const ESTIMATED_ITEM_SIZE = 100;

Expand All @@ -50,6 +51,8 @@ const FormDetails = () => {
const [optionSheetOpen, setOptionSheetOpen] = useState(false);
const [clearingForm, setClearingForm] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [search, setSearch] = useState("");

const { width } = useWindowDimensions();

const { mutate: updateSubmission } = useFormSubmissionMutation({
Expand Down Expand Up @@ -238,6 +241,12 @@ const FormDetails = () => {
}
};

const filteredQuestions = useMemo(() => {
return questions?.filter((q) =>
q.question.toLowerCase().includes(search.trim().toLocaleLowerCase()),
);
}, [questions, search]);

if (isLoadingCurrentForm || isLoadingAnswers) {
return (
<Screen preset="fixed" contentContainerStyle={{ flexGrow: 1 }}>
Expand Down Expand Up @@ -299,18 +308,16 @@ const FormDetails = () => {
numberOfNotes: number;
}
>
data={questions}
data={filteredQuestions}
ListHeaderComponent={
<YStack gap="$xl" paddingBottom="$xxs">
<YStack marginBottom="$xs" gap="$md">
<FormOverview
completedAnswers={Object.keys(answers || {}).length}
numberOfQuestions={numberOfQuestions}
onFormActionClick={onFormOverviewActionClick}
isCompleted={isCompleted}
/>
<Typography preset="body1" fontWeight="700" gap="$xxs">
{t("questions.title")}
</Typography>
<SearchInput onSearch={setSearch} placeholder={t("search")} />
</YStack>
}
showsVerticalScrollIndicator={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,7 @@ const Index = () => {
{activeElectionRound && selectedPollingStation?.pollingStationId && psiFormQuestions && (
<FormList
ListHeaderComponent={
<YStack>
<PollingStationGeneral psiData={psiData} psiFormQuestions={psiFormQuestions} />
<Typography preset="body1" fontWeight="700" marginTop="$lg" marginBottom="$xxs">
{t("forms.heading")}
</Typography>
</YStack>
<PollingStationGeneral psiData={psiData} psiFormQuestions={psiFormQuestions} />
}
/>
)}
Expand Down
114 changes: 65 additions & 49 deletions mobile/app/(observer)/(app)/(drawer)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React, { useMemo } from "react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { DrawerContentScrollView, DrawerItem } from "@react-navigation/drawer";
import { Drawer } from "expo-router/drawer";
import React, { useMemo } from "react";
import { ScrollViewProps } from "react-native";
import { DrawerContentScrollView, DrawerItem } from "@react-navigation/drawer";
import { useTheme, XStack } from "tamagui";
import { useUserData } from "../../../../contexts/user/UserContext.provider";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { useTheme, YStack } from "tamagui";
import { AppMode } from "../../../../contexts/app-mode/AppModeContext.provider";
import { useUserData } from "../../../../contexts/user/UserContext.provider";

import { useTranslation } from "react-i18next";
import { AppModeSwitchButton } from "../../../../components/AppModeSwitchButton";
import { Icon } from "../../../../components/Icon";
import { PastElectionsNavigateButton } from "../../../../components/PastElectionsNavigateButton";
import { Typography } from "../../../../components/Typography";
import { electionRoundSorter } from "../../../../helpers/election-rounds";

type DrawerContentProps = ScrollViewProps & {
children?: React.ReactNode;
Expand All @@ -19,59 +21,73 @@ type DrawerContentProps = ScrollViewProps & {

export const DrawerContent = (props: DrawerContentProps) => {
const { electionRounds, activeElectionRound, setSelectedElectionRoundId } = useUserData();
const { t } = useTranslation("drawer");

const startedElectionRounds = useMemo(
() => electionRounds?.filter((electionRound) => electionRound.status === "Started"),
() =>
electionRounds
?.filter((electionRound) => electionRound.status === "Started")
.sort(electionRoundSorter),
[electionRounds],
);

const theme = useTheme();
const insets = useSafeAreaInsets();

return (
<DrawerContentScrollView
contentContainerStyle={{ flexGrow: 1, paddingBottom: insets.bottom + 32 }}
bounces={false}
stickyHeaderIndices={[0]}
{...props}
>
<XStack paddingTop={16} paddingLeft="$md" paddingBottom="$xl">
<Icon icon="vmObserverLogo" width={211} height={65} />
</XStack>
{startedElectionRounds?.map((round, index) => {
return (
<DrawerItem
key={index}
// use a custom component for the label, as the default one only displays one line of text
label={({ color }) => (
<Typography preset="body2" color={color}>
{`${round.status} - ${round.title}`}
</Typography>
)}
focused={activeElectionRound?.id === round.id}
activeTintColor={theme.purple5?.val}
activeBackgroundColor={theme.yellow5?.val}
inactiveTintColor="white"
onPress={() => {
if (activeElectionRound?.id !== round.id) {
setSelectedElectionRoundId(round.id);
}
}}
style={{
paddingVertical: 4,
paddingHorizontal: 16,
marginVertical: 0,
marginHorizontal: 0,
borderRadius: 0,
}}
allowFontScaling={false}
/>
);
})}
<YStack flex={1}>
<DrawerContentScrollView
contentContainerStyle={{ flexGrow: 1 }}
bounces={false}
stickyHeaderIndices={[0]}
{...props}
>
<YStack paddingLeft="$md" backgroundColor={theme.purple5?.val}>
<Icon icon="vmObserverLogo" width={211} height={65} />
<Typography preset="subheading" color="white">
{t("active_elections")}
</Typography>
</YStack>
{startedElectionRounds?.map((round, index) => {
return (
<DrawerItem
key={index}
// use a custom component for the label, as the default one only displays one line of text
label={({ color }) => (
<Typography preset="body2" color={color}>
{`${round.status} - ${round.title}`}
</Typography>
)}
focused={activeElectionRound?.id === round.id}
activeTintColor={theme.purple5?.val}
activeBackgroundColor={theme.yellow5?.val}
inactiveTintColor="white"
onPress={() => {
if (activeElectionRound?.id !== round.id) {
setSelectedElectionRoundId(round.id);
}
}}
style={{
paddingVertical: 4,
paddingHorizontal: 16,
marginVertical: 0,
marginHorizontal: 0,
borderRadius: 0,
}}
allowFontScaling={false}
/>
);
})}
</DrawerContentScrollView>

{/* past elections */}
<YStack padding="$md" backgroundColor={theme.purple5?.val}>
<PastElectionsNavigateButton />
</YStack>
{/* app mode switch */}
<AppModeSwitchButton switchToMode={AppMode.CITIZEN} />
</DrawerContentScrollView>
<YStack padding="$md" backgroundColor={theme.purple5?.val}>
<AppModeSwitchButton switchToMode={AppMode.CITIZEN} />
</YStack>
</YStack>
);
};

Expand Down
2 changes: 2 additions & 0 deletions mobile/app/(observer)/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const AppLayout = () => {
<Stack.Screen name="change-password" options={{ headerShown: false }} />
<Stack.Screen name="about-votemonitor" options={{ headerShown: false }} />
<Stack.Screen name="guide/[guideId]" options={{ headerShown: false }} />
<Stack.Screen name="past-elections" options={{ headerShown: false }} />
<Stack.Screen name="er-statistics/[electionRoundId]" options={{ headerShown: false }} />
</Stack>
</NotificationContextProvider>
</UserContextProvider>
Expand Down
126 changes: 126 additions & 0 deletions mobile/app/(observer)/(app)/er-statistics/[electionRoundId].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useLocalSearchParams, useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { RefreshControl } from "react-native";
import { ScrollView, YStack } from "tamagui";
import Header from "../../../../components/Header";
import { Icon } from "../../../../components/Icon";
import { Screen } from "../../../../components/Screen";
import { Typography } from "../../../../components/Typography";
import { useElectionRoundsQuery } from "../../../../services/queries.service";
import { useElectionRoundStatistics } from "../../../../services/queries/er-statistics.query";

type SearchParamsType = {
electionRoundId: string;
};

const ElectionRoundStatistics = () => {
const { electionRoundId } = useLocalSearchParams<SearchParamsType>();
const { data: electionRound, isLoading: isLoadingElectionRound } = useElectionRoundsQuery(
(elections) => elections.find((e) => e.id === electionRoundId),
);
const { t } = useTranslation(["er_statistics", "common"]);
const router = useRouter();

if (!electionRoundId) {
return <Typography>Incorrect page params</Typography>;
}

const {
data: erStatistics,
isLoading: isLoadingERStatistics,
error: erStatisticsError,
refetch: refetchERStatistics,
isRefetching: isRefetchingERStatistics,
} = useElectionRoundStatistics(electionRoundId);

if (isLoadingElectionRound || isLoadingERStatistics) {
return <Typography>{t("loading", { ns: "common" })}</Typography>;
}

if (erStatisticsError) {
return (
<Screen preset="fixed" contentContainerStyle={{ flexGrow: 1 }}>
<Header
title={t("header")}
leftIcon={<Icon icon="chevronLeft" color="white" />}
onLeftPress={() => router.back()}
/>
<YStack flex={1}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ flex: 1, alignItems: "center", flexGrow: 1 }}
paddingVertical="$xxl"
refreshControl={
<RefreshControl
refreshing={isRefetchingERStatistics}
onRefresh={refetchERStatistics}
/>
}
>
<Typography>{t("error")}</Typography>
</ScrollView>
</YStack>
</Screen>
);
}

return (
<Screen
preset="fixed"
contentContainerStyle={{
flexGrow: 1,
}}
backgroundColor="white"
>
<Header
title={t("header")}
leftIcon={<Icon icon="chevronLeft" color="white" />}
onLeftPress={() => router.back()}
/>

<YStack flex={1}>
<ScrollView
contentContainerStyle={{
paddingVertical: 32,
gap: 32,
flexGrow: 1,
paddingHorizontal: 16,
}}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={isRefetchingERStatistics} onRefresh={refetchERStatistics} />
}
>
<YStack gap={16}>
<Typography preset="heading" fontWeight="700">
{t("title", {
title: electionRound?.title || "",
englishTitle: electionRound?.englishTitle || "",
})}
</Typography>
<Typography preset="body1">
{t("forms_submitted", { value: erStatistics?.numberOfFormsSubmitted || 0 })}
</Typography>
<Typography preset="body1">
{t("questions_answered", { value: erStatistics?.numberOfQuestionsAnswered || 0 })}
</Typography>
<Typography preset="body1">
{t("quick_reports", { value: erStatistics?.numberOfQuickReports || 0 })}
</Typography>
<Typography preset="body1">
{t("notes_taken", { value: erStatistics?.numberOfNotes || 0 })}
</Typography>
<Typography preset="body1">
{t("attachments_sent", { value: erStatistics?.numberOfAttachments || 0 })}
</Typography>
<Typography preset="body1">
{t("visited_ps", { value: erStatistics?.numberOfPollingStationsVisited || 0 })}
</Typography>
</YStack>
</ScrollView>
</YStack>
</Screen>
);
};

export default ElectionRoundStatistics;
Loading
Loading