Skip to content
Open
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
9 changes: 5 additions & 4 deletions mobile/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "VoteMonitor",
"slug": "vote-monitor",
"version": "2.3.2",
"version": "2.5.2",
"scheme": "mobile",
"orientation": "portrait",
"icon": "./assets/icons/icon.png",
Expand All @@ -18,7 +18,7 @@
"ios": {
"supportsTablet": true,
"bundleIdentifier": "org.commitglobal.votemonitor",
"buildNumber": "111",
"buildNumber": "133",
"config": {
"usesNonExemptEncryption": false
},
Expand All @@ -31,7 +31,7 @@
},
"android": {
"softwareKeyboardLayoutMode": "pan",
"versionCode": 115,
"versionCode": 131,
"adaptiveIcon": {
"foregroundImage": "./assets/icons/adaptive-icon.png",
"backgroundColor": "#FDD20C"
Expand Down Expand Up @@ -126,7 +126,8 @@
"project": "votemonitor-mobile"
}
],
"react-native-compressor"
"react-native-compressor",
"react-native-map-link"
]
}
}
125 changes: 113 additions & 12 deletions mobile/app/(observer)/(app)/(drawer)/(tabs)/(observation)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { DrawerActions } from "@react-navigation/native";
import * as Clipboard from "expo-clipboard";
import { router, useNavigation } from "expo-router";
import React, { useState } from "react";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Popup } from "react-native-map-link";
import Toast from "react-native-toast-message";
import { YStack } from "tamagui";
import SingleSubmissionFormList from "../../../../../../components/SingleSubmissionFormList";
import MultiSubmissionFormList from "../../../../../../components/MultiSubmissionFormList";
import Header from "../../../../../../components/Header";
import { Icon } from "../../../../../../components/Icon";
import MultiSubmissionFormList from "../../../../../../components/MultiSubmissionFormList";
import NoElectionRounds from "../../../../../../components/NoElectionRounds";
import NoVisitsExist from "../../../../../../components/NoVisitsExist";
import OptionsSheet from "../../../../../../components/OptionsSheet";
import { PollingStationGeneral } from "../../../../../../components/PollingStationGeneral";
import { Screen } from "../../../../../../components/Screen";
import SelectPollingStation from "../../../../../../components/SelectPollingStation";
import SingleSubmissionFormList from "../../../../../../components/SingleSubmissionFormList";
import ObservationSkeleton from "../../../../../../components/SkeletonLoaders/ObservationSkeleton";
import { Typography } from "../../../../../../components/Typography";
import { useUserData } from "../../../../../../contexts/user/UserContext.provider";
Expand All @@ -25,6 +28,7 @@ const Index = () => {
const { t } = useTranslation("observation");
const navigation = useNavigation();
const [openContextualMenu, setOpenContextualMenu] = useState(false);
const [openSelectNavigationAppSheet, setOpenSelectNavigationAppSheet] = useState(false);

const { isLoading, visits, selectedPollingStation, activeElectionRound } = useUserData();

Expand All @@ -36,6 +40,75 @@ const Index = () => {
const { data: psiFormQuestions, isLoading: isLoadingPsiFormQuestions } =
usePollingStationInformationForm(activeElectionRound?.id);

const options = useMemo(() => {
if (!selectedPollingStation) {
return;
}

// Find the matching visit to get address and level information
const matchingVisit = visits?.find(
(visit) => visit.pollingStationId === selectedPollingStation.pollingStationId,
);

const fullAddress = [
matchingVisit?.level1,
matchingVisit?.level2,
matchingVisit?.level3,
matchingVisit?.level4,
matchingVisit?.level5,
matchingVisit?.address,
]
.filter(Boolean)
.join(" ");

return {
address: fullAddress,
latitude: selectedPollingStation.latitude,
longitude: selectedPollingStation.longitude,
dialogTitle: t("navigate_to_polling_station.title"),
dialogMessage: t("navigate_to_polling_station.description"),
cancelText: t("navigate_to_polling_station.actions.cancel"),
};
}, [selectedPollingStation, visits]);

const handleCopyPollingStationInfo = async () => {
if (!selectedPollingStation) {
return;
}

// Find the matching visit to get address and level information
const matchingVisit = visits?.find(
(visit) => visit.pollingStationId === selectedPollingStation.pollingStationId,
);

const infoText = [
matchingVisit?.level1,
matchingVisit?.level2,
matchingVisit?.level3,
matchingVisit?.level4,
matchingVisit?.level5,
matchingVisit?.number,
matchingVisit?.address,
]
.filter(Boolean)
.join(" / ");

try {
await Clipboard.setStringAsync(infoText);
Toast.show({
type: "success",
text2: t("options_menu.copy_success_toast"),
});
setOpenContextualMenu(false);
} catch (error) {
console.error("Failed to copy polling station information:", error);
Toast.show({
type: "error",
text2: t("options_menu.copy_error_toast"),
});
}
};

if (!isLoading && !activeElectionRound) {
return <NoElectionRounds />;
}
Expand All @@ -61,21 +134,49 @@ const Index = () => {

{openContextualMenu && (
<OptionsSheet open setOpen={setOpenContextualMenu}>
<YStack
paddingVertical="$xxs"
paddingHorizontal="$sm"
onPress={() => {
setOpenContextualMenu(false);
router.push("/manage-polling-stations");
}}
>
<Typography preset="body1" color="$gray7" lineHeight={24}>
<YStack paddingHorizontal="$sm" gap="$xxs">
<Typography
preset="body1"
color="$gray7"
paddingVertical="$xs"
lineHeight={24}
onPress={() => {
setOpenContextualMenu(false);
router.push("/manage-polling-stations");
}}
>
{t("options_menu.manage_my_polling_stations")}
</Typography>
<Typography
preset="body1"
color="$gray7"
paddingVertical="$xs"
lineHeight={24}
onPress={handleCopyPollingStationInfo}
>
{t("options_menu.copy_polling_station_information")}
</Typography>
<Typography
preset="body1"
color="$gray7"
paddingVertical="$xs"
lineHeight={24}
onPress={() => setOpenSelectNavigationAppSheet(true)}
>
{t("options_menu.navigate_to_polling_station")}
</Typography>
</YStack>
</OptionsSheet>
)}

<Popup
isVisible={openSelectNavigationAppSheet}
setIsVisible={setOpenSelectNavigationAppSheet}
onCancelPressed={() => setOpenSelectNavigationAppSheet(false)}
onAppPressed={() => setOpenSelectNavigationAppSheet(false)}
options={options ?? {}}
/>

<YStack paddingHorizontal="$md" flex={1}>
{(isLoading || isLoadingPsiData || isLoadingPsiFormQuestions) && <ObservationSkeleton />}
{activeElectionRound &&
Expand Down
54 changes: 24 additions & 30 deletions mobile/app/(observer)/(app)/polling-station-questionnaire.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,10 @@ const PollingStationQuestionnaire = () => {
});

const currentLanguage = useMemo(() => {
const activeLanguage = language ? language.toLocaleUpperCase() : i18n.language.toLocaleUpperCase();

const activeLanguage = language
? language.toLocaleUpperCase()
: i18n.language.toLocaleUpperCase();

if (
formStructure &&
formStructure?.defaultLanguage &&
Expand Down Expand Up @@ -169,10 +171,10 @@ const PollingStationQuestionnaire = () => {
}));
return mappedSelections?.length
? ({
$answerType: "multiSelectAnswer",
questionId,
selection: mappedSelections,
} as ApiFormAnswer)
$answerType: "multiSelectAnswer",
questionId,
selection: mappedSelections,
} as ApiFormAnswer)
: undefined;
}
default:
Expand Down Expand Up @@ -296,9 +298,7 @@ const PollingStationQuestionnaire = () => {
const onConfirmFormLanguage = (formId: string, language: string) => {
setFormLanguagePreference({ formId, language });

router.replace(
`/polling-station-questionnaire?language=${language}`,
);
router.replace(`/polling-station-questionnaire?language=${language}`);
setIsChangeLanguageModalOpen(false);
};

Expand Down Expand Up @@ -607,15 +607,15 @@ const PollingStationQuestionnaire = () => {
/>
)}

{isChangeLanguageModalOpen && formStructure?.languages && (
<ChangeLanguageDialog
currentLanguage={currentLanguage}
formId={formStructure?.id as string}
languages={formStructure.languages}
onCancel={setIsChangeLanguageModalOpen.bind(null, false)}
onSelectLanguage={onConfirmFormLanguage}
/>
)}
{isChangeLanguageModalOpen && formStructure?.languages && (
<ChangeLanguageDialog
currentLanguage={currentLanguage}
formId={formStructure?.id as string}
languages={formStructure.languages}
onCancel={setIsChangeLanguageModalOpen.bind(null, false)}
onSelectLanguage={onConfirmFormLanguage}
/>
)}
</Screen>

<XStack
Expand Down Expand Up @@ -655,14 +655,11 @@ const OptionSheetContent = ({

return (
<View paddingVertical="$xxs" paddingHorizontal="$sm" gap="$xxs">
<Typography preset="body1" paddingVertical="$xs" onPress={onChangeLanguagePress}>
{t("menu.change_language")}
</Typography>
<Typography
preset="body1"
color={disableMarkAsDone ? "$gray3" : "$gray7"}
lineHeight={24}
color={disableMarkAsDone ? "$gray3" : "$gray9"}
paddingVertical="$xs"
lineHeight={24}
onPress={() => {
onSetCompletion(!isCompleted);
}}
Expand All @@ -672,13 +669,10 @@ const OptionSheetContent = ({
? t("forms.mark_as_done", { ns: "common" })
: t("forms.mark_as_in_progress", { ns: "common" })}
</Typography>
<Typography
preset="body1"
color="$gray7"
lineHeight={24}
paddingVertical="$xs"
onPress={onClear}
>
<Typography preset="body1" paddingVertical="$xs" onPress={onChangeLanguagePress}>
{t("menu.change_language")}
</Typography>
<Typography preset="body1" color="$red10" paddingVertical="$xs" onPress={onClear}>
{t("menu.clear")}
</Typography>
</View>
Expand Down
4 changes: 3 additions & 1 deletion mobile/app/(observer)/(app)/polling-station-wizzard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,15 @@ const PollingStationWizzardContent = ({
pollingStationId: pollingStation.pollingStationId,
visitedAt: new Date().toISOString(),
address: pollingStation.name,
number: pollingStation?.number || "",
number: pollingStation.number ?? "",
isNotSynced: true,
level1: steps[0].name,
level2: steps[1]?.name,
level3: steps[2]?.name,
level4: steps[3]?.name,
level5: steps[4]?.name,
latitude: pollingStation.latitude,
longitude: pollingStation.longitude,
},
],
);
Expand Down
13 changes: 11 additions & 2 deletions mobile/assets/locales/az/translations_AZ.json
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,16 @@
}
},
"options_menu": {
"manage_my_polling_stations": "Mənim seçki məntəqələrimin idarə edilməsi"
"manage_my_polling_stations": "Mənim seçki məntəqələrimin idarə edilməsi",
"copy_polling_station_information": "Seçki məntəqəsi məlumatlarını mübadilə buferinə kopyala",
"copy_success_toast": "Seçki məntəqəsi məlumatları mübadilə buferinə kopyalandı",
"copy_error_toast": "Seçki məntəqəsi məlumatlarını kopyalamaq mümkün olmadı",
"navigate_to_polling_station": "Seçki məntəqəsinə keçid et"
},
"navigate_to_polling_station": {
"title": "Seçki məntəqəsinə keçid et",
"description": "Seçki məntəqəsinə istiqamətləri almaq üçün tətbiq seçin.",
"cancel": "Ləğv etmək"
}
},
"polling_station_information_form": {
Expand Down Expand Up @@ -890,4 +899,4 @@
"attachments": "Əlavələr:",
"notes": "Qeydlər:"
}
}
}
15 changes: 13 additions & 2 deletions mobile/assets/locales/bg/translations_BG.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@
"info_modal": {
"p1": "Докладите, подадени чрез Vote Monitor за граждани, ще бъдат анализирани от неправителствените организации, които наблюдават изборите, и могат да бъдат включени в техните официални доклади. В определени случаи вашите доклади могат да бъдат предадени на институциите, отговорни за организирането на изборите, с цел подобряване на изборния процес.",
"p2": "Моля, имайте предвид, че подаването на сигнал чрез това приложение не представлява официална жалба, подадена до институциите, отговорни за организирането на изборите.",
"p3": "В определени случаи вашите сигнали могат да бъдат споделени с институциите, отговорни за организацията на изборите. Това може да се направи с цел подобряване на изборния процес, което обаче не е гарантирано.",
"p4": "Вие имате пълен контрол върху личните си данни. Формулярите за подаване на сигнали ви позволяват да изберете дали да подадете сигнал анонимно или да дадете съгласие организацията да се свърже с вас, за да се уточни информацията по Вашия сигнал.",
"ok": "OK"
}
},
Expand Down Expand Up @@ -361,6 +363,11 @@
"form_details_button_label": "Информация за изборната секция",
"number_of_questions": "{{value}} въпроса",
"answer_questions": "Отговорете на въпросите"
},
"navigate_to_polling_station": {
"title": "Преминаване до избирателната секция",
"description": "Изберете приложение, за да получите упътвания до избирателната секция",
"cancel": "Откажете"
}
},
"forms": {
Expand All @@ -380,7 +387,11 @@
}
},
"options_menu": {
"manage_my_polling_stations": "Управление на моите изборни секции"
"manage_my_polling_stations": "Управление на моите изборни секции",
"copy_polling_station_information": "Копиране на данните за изборната секция в клипборда",
"copy_success_toast": "Данните за изборната секция бяха копирани в клипборда",
"copy_error_toast": "Неуспешно копиране на данните за изборната секция",
"navigate_to_polling_station": "Преминаване към избирателната секция"
}
},
"polling_station_information_form": {
Expand Down Expand Up @@ -890,4 +901,4 @@
"attachments": "Прикачени файлове:",
"notes": "Бележки:"
}
}
}
Loading
Loading