diff --git a/.env.example b/.env.example
index 5532a71b5c..d3d0d9cff0 100644
--- a/.env.example
+++ b/.env.example
@@ -2,6 +2,4 @@ VITE_FIREBASE_APIKEY=
VITE_FIREBASE_APPID=
VITE_FIREBASE_AUTHDOMAIN=
VITE_FIREBASE_AUTH_EMULATOR_HOST=http://127.0.0.1:9099
-VITE_FIREBASE_MEASUREMENTID=
-VITE_FIREBASE_MESSAGINGSENDERID=
VITE_FIREBASE_PROJECTID=
diff --git a/LOCAL_ENVIRONMENT_SETUP.md b/LOCAL_ENVIRONMENT_SETUP.md
index d643e3f518..845e005e7a 100644
--- a/LOCAL_ENVIRONMENT_SETUP.md
+++ b/LOCAL_ENVIRONMENT_SETUP.md
@@ -132,16 +132,14 @@ Firebase Emulators require a storage rule file to set up Firebase Storage locall
Now, let’s assign values to these variables:
-1. Starting with the **USER_PARSER_API_KEY,** go to [userparser.com](https://www.userparser.com). This API helps us better authorize users by collecting the user agent data of their devices. Log in to your account if you already have one, or create a new one if you don’t. It’s completely free.
-2. After completing the authentication, navigate to **Dashboard,** where you should see the API key at the top of the page. Copy that API key to the **USER_PARSER_API_KEY** variable.
-3. Next, we have three environment variables: **GMAIL_ADDRESS, GMAIL_APP_PASSWORD,** and **GMAIL_SENDER_NAME.** We won’t use these during local development, so we can skip them for now.
-4. Let’s move on to the Firebase environment variables. For **FIREBASE_APP_NAME,** use the project id assigned to your firebase project. You can get it from the URL (ie: `organized-app-47c7u` from `https://console.firebase.google.com/u/1/project/organized-app-47c7u/overview`). Alternatively, you can go to **Settings,** then **General,** and find the `Project ID`.
-5. For the **GOOGLE_CONFIG_BASE64,** there are many approaches to get this base64 string of the private key. We’re just showing one way of getting it.
-6. In this example, we’ll use Node directly in the Terminal window by typing `node`.
-7. Let’s create a variable to store our private key JSON contents. Open the JSON file we downloaded earlier and copy its contents. Then, type `const firebaseConfig =` and right after this paste all the JSON content into the Terminal. Press Enter. Remember, it’s just the JSON data saved in this newly defined variable.
-8. To convert it to a base64 string, we use the command `Buffer.from(JSON.stringify(firebaseConfig)).toString('base64')`. Please, note that we recommend using the local converting command rather than online base64 converter tools, because of security reasons. Then, press **Enter.**
-9. You should now have the base64 encoded string of your Firebase private key. Copy that text to the **GOOGLE_CONFIG_BASE64** variable.
-10. Additionally, create .firebaserc file with `cp .firebaserc.example .firebaserc` command and update the default field **your-organized-project-id** with your Firebase project ID.
+1. Let’s move on to the Firebase environment variables. For **FIREBASE_APP_NAME,** use the project id assigned to your firebase project. You can get it from the URL (ie: `organized-app-47c7u` from `https://console.firebase.google.com/u/1/project/organized-app-47c7u/overview`). Alternatively, you can go to **Settings,** then **General,** and find the `Project ID`.
+2. For the **GOOGLE_CONFIG_BASE64,** there are many approaches to get this base64 string of the private key. We’re just showing one way of getting it.
+3. In this example, we’ll use Node directly in the Terminal window by typing `node`.
+4. Let’s create a variable to store our private key JSON contents. Open the JSON file we downloaded earlier and copy its contents. Then, type `const firebaseConfig =` and right after this paste all the JSON content into the Terminal. Press Enter. Remember, it’s just the JSON data saved in this newly defined variable.
+5. To convert it to a base64 string, we use the command `Buffer.from(JSON.stringify(firebaseConfig)).toString('base64')`. Please, note that we recommend using the local converting command rather than online base64 converter tools, because of security reasons. Then, press **Enter.**
+6. You should now have the base64 encoded string of your Firebase private key. Copy that text to the **GOOGLE_CONFIG_BASE64** variable.
+7. The three environment variables: **MAIL_ADDRESS, MAIL_PASSWORD,** and **MAIL_SENDER_NAME.** are not used during local development, so we can skip them for now.
+8. Additionally, create .firebaserc file with `cp .firebaserc.example .firebaserc` command and update the default field **your-organized-project-id** with your Firebase project ID.
#### Setup the Firebase emulators
@@ -174,12 +172,11 @@ _That completes the setup of the backend project for the local environment. The
_Now, let’s add the required environment variables for the frontend application._
1. Create an `.env` file for this frontend project. You can do it starting from the example file `cp .env.example .env`.
-2. Write all the required variables. We need the **VITE_FIREBASE_APIKEY, VITE_FIREBASE_AUTHDOMAIN, VITE_FIREBASE_PROJECTID, VITE_FIREBASE_APPID,**, **VITE_FIREBASE_MEASUREMENTID.**, and **VITE_FIREBASE_MESSAGINGSENDERID**.
-3. To get these values, go back to the Firebase Console and open your project.
-4. Navigate to **Project Settings.** Find “Your apps” or “Add an app” area and hit the “Web” button. Then create and register a new Web App.
-5. Give a nickname for the web app. For example, ‘Organized web app’.
-6. We don’t need to set up Firebase Hosting for this app, so continue.
-7. In this section, we get all the required values for our environment variables like **apiKey, authDomain, projectId, appId,** and **measurementId.** Copy these values from the Firebase console to our `.env` file.
+2. Write all the required variables. We need the **VITE_FIREBASE_APIKEY, VITE_FIREBASE_AUTHDOMAIN, VITE_FIREBASE_PROJECTID**, and **VITE_FIREBASE_APPID.** To get these values, go back to the Firebase Console and open your project.
+3. Navigate to **Project Settings.** Find “Your apps” or “Add an app” area and hit the “Web” button. Then create and register a new Web App.
+4. Give a nickname for the web app. For example, ‘Organized web app’.
+5. We don’t need to set up Firebase Hosting for this app, so continue.
+6. In this section, we get all the required values for our environment variables like **apiKey, authDomain, projectId,** and **appId.** Copy these values from the Firebase console to our `.env` file.
_All the dependencies were installed, and the environment variables are all ready. We can now start the frontend application._
diff --git a/package-lock.json b/package-lock.json
index 3955c349df..35046d3dc4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10196,15 +10196,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/@eslint/js": {
- "version": "9.16.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz",
- "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==",
- "dev": true,
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
"node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
diff --git a/src/assets/img/netlify_darkmode.png b/src/assets/img/netlify_darkmode.png
new file mode 100644
index 0000000000..d6ed07eb74
Binary files /dev/null and b/src/assets/img/netlify_darkmode.png differ
diff --git a/src/assets/img/netlify_lightmode.png b/src/assets/img/netlify_lightmode.png
new file mode 100644
index 0000000000..7a4a0c65e9
Binary files /dev/null and b/src/assets/img/netlify_lightmode.png differ
diff --git a/src/definition/app.ts b/src/definition/app.ts
index d526d9320d..bf0207e031 100644
--- a/src/definition/app.ts
+++ b/src/definition/app.ts
@@ -68,3 +68,5 @@ export type ReleaseNoteType = {
export type UpdateStatusType = {
[version: string]: boolean;
};
+
+export type BackupFileType = 'CPE' | 'Organized' | '';
diff --git a/src/definition/backup.ts b/src/definition/backup.ts
new file mode 100644
index 0000000000..c77d42293a
--- /dev/null
+++ b/src/definition/backup.ts
@@ -0,0 +1,226 @@
+export type BackupOrganizedType = {
+ name: string;
+ exported: string;
+ version: string;
+ data: {
+ [table: string]: object[];
+ };
+};
+
+export type BackupCPEType = {
+ persons: {
+ person_uid: string;
+ person_name: string;
+ person_displayName: string;
+ isMale: boolean;
+ isFemale: boolean;
+ isUnavailable: boolean;
+ assignments: { code: number }[];
+ isMoved: boolean;
+ isDisqualified: boolean;
+ birthDate: string;
+ isAnointed: boolean;
+ isOtherSheep: boolean;
+ isBaptized: boolean;
+ immersedDate: string;
+ email: string;
+ address: string;
+ phone: string;
+ spiritualStatus: {
+ statusId: string;
+ status: string;
+ startDate: string;
+ endDate: string;
+ }[];
+ otherService: {
+ serviceId: string;
+ service: string;
+ startDate: string;
+ endDate: string;
+ }[];
+ firstMonthReport: string;
+ }[];
+ fieldServiceGroup: {
+ fieldServiceGroup_uid: string;
+ isCurrent: boolean;
+ groups: {
+ group_uid: string;
+ persons: {
+ person_uid: string;
+ isOverseer: boolean;
+ isAssistant: boolean;
+ }[];
+ }[];
+ deleted: boolean;
+ }[];
+ visiting_speakers: {
+ cong_name: string;
+ cong_number: number;
+ cong_speakers: {
+ person_uid: string;
+ person_name: string;
+ person_displayName: string;
+ is_elder: boolean;
+ is_ms: boolean;
+ talks: number[];
+ is_unavailable: boolean;
+ is_deleted: boolean;
+ email: string;
+ phone: string;
+ }[];
+ is_local: boolean;
+ is_deleted: boolean;
+ request_status: string;
+ }[];
+ branchReports: {
+ report_uid: string;
+ report: string;
+ service_year: string;
+ month: string;
+ details: {
+ isFinalized: boolean;
+ isSubmitted: boolean;
+ activePublishers: number;
+ weekendMeetingAttendanceAvg: number;
+ totalReports: number;
+ totalPlacements: number;
+ totalVideos: number;
+ totalHours: number;
+ totalReturnVisits: number;
+ totalBibleStudies: number;
+ publishersReports: number;
+ publishersPlacements: number;
+ publishersVideos: number;
+ publishersHours: number;
+ publishersReturnVisits: number;
+ publishersBibleStudies: number;
+ auxPioneersReports: number;
+ auxPioneersPlacements: number;
+ auxPioneersVideos: number;
+ auxPioneersHours: number;
+ auxPioneersReturnVisits: number;
+ auxPioneersBibleStudies: number;
+ FRReports: number;
+ FRPlacements: number;
+ FRVideos: number;
+ FRHours: number;
+ FRReturnVisits: number;
+ FRBibleStudies: number;
+ };
+ }[];
+ fieldServiceReports: {
+ uid: string;
+ service_year: string;
+ person_uid: string;
+ months: {
+ uid: string;
+ month_value: string;
+ hours: string;
+ hourCredit: string;
+ bibleStudies: number;
+ minutes: string;
+ placements: string;
+ returnVisits: string;
+ videos: string;
+ comments: string;
+ }[];
+ }[];
+ meetingAttendance: {
+ uid: string;
+ service_year: string;
+ month_value: string;
+ midweek_meeting: { index: number; count: string }[];
+ weekend_meeting: { index: number; count: string }[];
+ }[];
+ sources: {
+ weekOf: string;
+ mwb_week_date_locale: { [lang: string]: string };
+ mwb_weekly_bible_reading: { [lang: string]: string };
+ mwb_song_first: number;
+ mwb_tgw_talk: { [lang: string]: string };
+ mwb_tgw_bread: { [lang: string]: string };
+ mwb_ayf_count: number;
+ mwb_ayf_part1_type: { [lang: string]: number };
+ mwb_ayf_part1_time: number;
+ mwb_ayf_part1: { [lang: string]: string };
+ mwb_ayf_part2_type: { [lang: string]: number };
+ mwb_ayf_part2_time: number;
+ mwb_ayf_part2: { [lang: string]: string };
+ mwb_ayf_part3_type: { [lang: string]: number };
+ mwb_ayf_part3_time: number;
+ mwb_ayf_part3: { [lang: string]: string };
+ mwb_ayf_part4_type: { [lang: string]: number };
+ mwb_ayf_part4_time: number;
+ mwb_ayf_part4: { [lang: string]: string };
+ mwb_song_middle: number;
+ mwb_lc_count: number;
+ mwb_lc_count_override: number;
+ mwb_lc_part1_time: number;
+ mwb_lc_part1: { [lang: string]: string };
+ mwb_lc_part1_content: { [lang: string]: string };
+ mwb_lc_part1_time_override: number;
+ mwb_lc_part1_override: { [lang: string]: string };
+ mwb_lc_part1_content_override: { [lang: string]: string };
+ mwb_lc_part2_time: number;
+ mwb_lc_part2: { [lang: string]: string };
+ mwb_lc_part2_content: { [lang: string]: string };
+ mwb_lc_part2_time_override: number;
+ mwb_lc_part2_override: { [lang: string]: string };
+ mwb_lc_part2_content_override: { [lang: string]: string };
+ mwb_lc_cbs: { [lang: string]: string };
+ mwb_lc_cbs_time_override: string;
+ mwb_co_talk_title: string;
+ mwb_song_conclude: number;
+ mwb_song_conclude_override: string;
+ w_study_date_locale: { [lang: string]: string };
+ w_co_talk_title: string;
+ w_talk_title_override: string;
+ w_study_title: { [lang: string]: string };
+ w_study_opening_song: string;
+ w_study_concluding_song: string;
+ }[];
+ sched: {
+ weekOf: string;
+ week_type: number;
+ noMMeeting: boolean;
+ noWMeeting: boolean;
+ chairmanMM_A: string;
+ chairmanMM_B: string;
+ cbs_conductor: string;
+ tgw_talk: string;
+ tgw_gems: string;
+ lc_part1: string;
+ lc_part2: string;
+ lc_part3: string;
+ cbs_reader: string;
+ opening_prayerMM: string;
+ closing_prayerMM: string;
+ bRead_stu_A: string;
+ bRead_stu_B: string;
+ ass1_stu_A: string;
+ ass1_stu_B: string;
+ ass1_ass_A: string;
+ ass1_ass_B: string;
+ ass2_stu_A: string;
+ ass2_stu_B: string;
+ ass2_ass_A: string;
+ ass2_ass_B: string;
+ ass3_stu_A: string;
+ ass3_stu_B: string;
+ ass3_ass_A: string;
+ ass3_ass_B: string;
+ ass4_stu_A: string;
+ ass4_stu_B: string;
+ ass4_ass_A: string;
+ ass4_ass_B: string;
+ public_talk: number;
+ public_talk_title: string;
+ speaker_1: string;
+ speaker_2: string;
+ substitute_speaker: string;
+ chairman_WM: string;
+ opening_prayerWM: string;
+ wtstudy_reader: string;
+ is_visiting_speaker: boolean;
+ }[];
+};
diff --git a/src/definition/meeting_attendance.ts b/src/definition/meeting_attendance.ts
index 273746e00e..0a79d0ab7c 100644
--- a/src/definition/meeting_attendance.ts
+++ b/src/definition/meeting_attendance.ts
@@ -11,6 +11,7 @@ export type WeeklyAttendance = {
};
export type MeetingAttendanceType = {
+ _deleted: { value: boolean; updatedAt: string };
month_date: string;
week_1: WeeklyAttendance;
week_2: WeeklyAttendance;
diff --git a/src/features/congregation/settings/import_export/confirm_import/index.tsx b/src/features/congregation/settings/import_export/confirm_import/index.tsx
new file mode 100644
index 0000000000..e221f81b0f
--- /dev/null
+++ b/src/features/congregation/settings/import_export/confirm_import/index.tsx
@@ -0,0 +1,224 @@
+import { Box, Grid2 as Grid, Stack } from '@mui/material';
+import { IconImportJson, IconLoading } from '@components/icons';
+import { ConfirmImportProps } from './index.types';
+import { useAppTranslation, useBreakpoints } from '@hooks/index';
+import useConfirmImport from './useConfirmImport';
+import Button from '@components/button';
+import Divider from '@components/divider';
+import Typography from '@components/typography';
+import Checkbox from '@components/checkbox';
+
+const ConfirmImport = (props: ConfirmImportProps) => {
+ const { t } = useAppTranslation();
+
+ const { tabletDown } = useBreakpoints();
+
+ const {
+ filename,
+ isProcessing,
+ handleImportData,
+ persons,
+ handleSelectData,
+ selected,
+ field_service_groups,
+ visiting_speakers,
+ user_field_service_reports,
+ cong_field_service_reports,
+ meeting_attendance,
+ schedules,
+ selectedAll,
+ inderterminate,
+ handleSelectAll,
+ } = useConfirmImport();
+
+ return (
+
+ {t('tr_importDataConfirm')}
+
+
+ {t('tr_importDataConfirmDesc')}
+
+
+ }
+ >
+
+
+
+ {filename}
+
+
+
+
+
+ {t('tr_selectAll')}
+
+ }
+ />
+
+
+
+ handleSelectData('persons', checked)}
+ label={
+
+ {t('tr_persons')}
+
+ }
+ />
+
+
+ handleSelectData('field_service_groups', checked)
+ }
+ label={
+
+ {t('tr_fieldServiceGroups')}
+
+ }
+ />
+
+
+ handleSelectData('visiting_speakers', checked)
+ }
+ label={
+
+ {t('tr_visitingSpeakers')}
+
+ }
+ />
+
+
+ handleSelectData('user_field_service_reports', checked)
+ }
+ label={
+
+ {t('tr_ministryReports')}
+
+ }
+ />
+
+
+
+
+ handleSelectData('cong_field_service_reports', checked)
+ }
+ label={
+
+ {t('tr_congregationReports')}
+
+ }
+ />
+
+ handleSelectData('meeting_attendance', checked)
+ }
+ label={
+
+ {t('tr_recordAttendance')}
+
+ }
+ />
+
+
+ handleSelectData('midweek_history', checked)
+ }
+ label={
+
+ {t('tr_midweekMeetingHistory')}
+
+ }
+ />
+
+
+ handleSelectData('weekend_history', checked)
+ }
+ label={
+
+ {t('tr_weekendMeetingHistory')}
+
+ }
+ />
+
+
+
+
+
+
+ }
+ >
+ {t('tr_import')}
+
+
+
+
+ );
+};
+
+export default ConfirmImport;
diff --git a/src/features/congregation/settings/import_export/confirm_import/index.types.ts b/src/features/congregation/settings/import_export/confirm_import/index.types.ts
new file mode 100644
index 0000000000..0d1b8ef4ea
--- /dev/null
+++ b/src/features/congregation/settings/import_export/confirm_import/index.types.ts
@@ -0,0 +1,43 @@
+import { BranchCongAnalysisType } from '@definition/branch_cong_analysis';
+import { BranchFieldServiceReportType } from '@definition/branch_field_service_reports';
+import { CongFieldServiceReportType } from '@definition/cong_field_service_reports';
+import { FieldServiceGroupType } from '@definition/field_service_groups';
+import { MeetingAttendanceType } from '@definition/meeting_attendance';
+import { PersonType } from '@definition/person';
+import { SchedWeekType } from '@definition/schedules';
+import { SourceWeekType } from '@definition/sources';
+import { SpeakersCongregationsType } from '@definition/speakers_congregations';
+import { UserBibleStudyType } from '@definition/user_bible_studies';
+import { UserFieldServiceReportType } from '@definition/user_field_service_reports';
+import { VisitingSpeakerType } from '@definition/visiting_speakers';
+
+export type ConfirmImportProps = {
+ onBack: VoidFunction;
+};
+
+export type ImportFieldType =
+ | 'persons'
+ | 'field_service_groups'
+ | 'visiting_speakers'
+ | 'user_field_service_reports'
+ | 'cong_field_service_reports'
+ | 'meeting_attendance'
+ | 'midweek_history'
+ | 'weekend_history';
+
+export type ImportChoiceType = Record;
+
+export type ImportDbType = {
+ persons?: PersonType[];
+ field_service_groups?: FieldServiceGroupType[];
+ visiting_speakers?: VisitingSpeakerType[];
+ speakers_congregations?: SpeakersCongregationsType[];
+ user_field_service_reports?: UserFieldServiceReportType[];
+ user_bible_studies?: UserBibleStudyType[];
+ branch_cong_analysis?: BranchCongAnalysisType[];
+ branch_field_service_reports?: BranchFieldServiceReportType[];
+ cong_field_service_reports?: CongFieldServiceReportType[];
+ meeting_attendance?: MeetingAttendanceType[];
+ sources?: SourceWeekType[];
+ sched?: SchedWeekType[];
+};
diff --git a/src/features/congregation/settings/import_export/confirm_import/useAttendanceImport.tsx b/src/features/congregation/settings/import_export/confirm_import/useAttendanceImport.tsx
new file mode 100644
index 0000000000..3c7024ce17
--- /dev/null
+++ b/src/features/congregation/settings/import_export/confirm_import/useAttendanceImport.tsx
@@ -0,0 +1,37 @@
+import { MeetingAttendanceType } from '@definition/meeting_attendance';
+import { updatedAtOverride } from '@utils/common';
+import appDb from '@db/appDb';
+
+const useAttendanceImport = () => {
+ const getAttendances = async (attendances: MeetingAttendanceType[]) => {
+ const result: MeetingAttendanceType[] = [];
+
+ result.push(...attendances);
+
+ const oldAttendances = await appDb.meeting_attendance.toArray();
+
+ for (const oldAttendance of oldAttendances) {
+ const newAttendance = attendances.find(
+ (record) => record.month_date === oldAttendance.month_date
+ );
+
+ if (!newAttendance) {
+ oldAttendance._deleted = {
+ value: true,
+ updatedAt: new Date().toISOString(),
+ };
+
+ result.push(oldAttendance);
+ }
+ }
+
+ return result.map((record) => {
+ const data = updatedAtOverride(record);
+ return data;
+ });
+ };
+
+ return { getAttendances };
+};
+
+export default useAttendanceImport;
diff --git a/src/features/congregation/settings/import_export/confirm_import/useConfirmImport.tsx b/src/features/congregation/settings/import_export/confirm_import/useConfirmImport.tsx
new file mode 100644
index 0000000000..b98355ea6e
--- /dev/null
+++ b/src/features/congregation/settings/import_export/confirm_import/useConfirmImport.tsx
@@ -0,0 +1,576 @@
+import { useMemo, useState } from 'react';
+import { useRecoilValue } from 'recoil';
+import { useAppTranslation } from '@hooks/index';
+import { ImportChoiceType, ImportDbType, ImportFieldType } from './index.types';
+import {
+ backupFileContentsState,
+ backupFileNameState,
+ backupFileTypeState,
+} from '@states/app';
+import { BackupCPEType, BackupOrganizedType } from '@definition/backup';
+import { PersonType } from '@definition/person';
+import { displaySnackNotification } from '@services/recoil/app';
+import { getMessageByCode } from '@services/i18n/translation';
+import { FieldServiceGroupType } from '@definition/field_service_groups';
+import { VisitingSpeakerType } from '@definition/visiting_speakers';
+import { UserFieldServiceReportType } from '@definition/user_field_service_reports';
+import { CongFieldServiceReportType } from '@definition/cong_field_service_reports';
+import { MeetingAttendanceType } from '@definition/meeting_attendance';
+import { SchedWeekType } from '@definition/schedules';
+import { SpeakersCongregationsType } from '@definition/speakers_congregations';
+import { UserBibleStudyType } from '@definition/user_bible_studies';
+import { BranchCongAnalysisType } from '@definition/branch_cong_analysis';
+import { BranchFieldServiceReportType } from '@definition/branch_field_service_reports';
+import { SourceWeekType } from '@definition/sources';
+import useCongReportsImport from './useCongReportsImport';
+import useMinistryReportsImport from './useMinistryReportsImport';
+import usePersonsImport from './usePersonsImport';
+import useServiceGroups from './useServiceGroups';
+import useVisitingSpeakersImport from './useVisitingSpeakersImport';
+import useAttendanceImport from './useAttendanceImport';
+import useMeetingImport from './useMeetingImport';
+import useImportCPE from './useImportCPE';
+import appDb from '@db/appDb';
+
+const useConfirmImport = () => {
+ const { t } = useAppTranslation();
+
+ const { getPersons } = usePersonsImport();
+ const { getServiceGroups } = useServiceGroups();
+ const { getSpeakersCongregations, getVisitingSpeakers } =
+ useVisitingSpeakersImport();
+ const { getUserBibleStudies, getUserReports } = useMinistryReportsImport();
+ const { getBranchCongAnalysis, getBranchFieldReports, getCongFieldReports } =
+ useCongReportsImport();
+ const { getAttendances } = useAttendanceImport();
+ const { getSchedules, getSources } = useMeetingImport();
+ const {
+ migratePersons,
+ migrateServiceGroups,
+ migrateSpeakers,
+ migrateCongReports,
+ migrateBranchReports,
+ migrateAttendances,
+ migrateSchedules,
+ migrateSources,
+ } = useImportCPE();
+
+ const filename = useRecoilValue(backupFileNameState);
+ const backupFileType = useRecoilValue(backupFileTypeState);
+ const backupFileContents = useRecoilValue(backupFileContentsState);
+
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [selected, setSelected] = useState({
+ persons: false,
+ field_service_groups: false,
+ visiting_speakers: false,
+ user_field_service_reports: false,
+ cong_field_service_reports: false,
+ meeting_attendance: false,
+ midweek_history: false,
+ weekend_history: false,
+ });
+
+ const backupContents = useMemo(() => {
+ return JSON.parse(backupFileContents);
+ }, [backupFileContents]);
+
+ const persons = useMemo(() => {
+ if (backupFileType === 'Organized') {
+ const backup = backupContents as BackupOrganizedType;
+ const backupData = backup.data['persons'] as PersonType[];
+ return backupData.filter((record) => !record._deleted.value);
+ }
+
+ if (backupFileType === 'CPE') {
+ const backup = backupContents as BackupCPEType;
+ const oldPersons = backup.persons;
+
+ return migratePersons(oldPersons);
+ }
+
+ return [];
+ }, [backupFileType, backupContents, migratePersons]);
+
+ const field_service_groups = useMemo(() => {
+ if (backupFileType === 'Organized') {
+ const backup = backupContents as BackupOrganizedType;
+ const backupData = backup.data[
+ 'field_service_groups'
+ ] as FieldServiceGroupType[];
+ return backupData.filter((record) => !record.group_data._deleted);
+ }
+
+ if (backupFileType === 'CPE') {
+ const backup = backupContents as BackupCPEType;
+ const oldGroups = backup.fieldServiceGroup;
+
+ return migrateServiceGroups(oldGroups);
+ }
+
+ return [];
+ }, [backupFileType, backupContents, migrateServiceGroups]);
+
+ const visiting_speakers = useMemo(() => {
+ if (backupFileType === 'Organized') {
+ const backup = backupContents as BackupOrganizedType;
+ const backupData = backup.data[
+ 'visiting_speakers'
+ ] as VisitingSpeakerType[];
+ return backupData.filter((record) => !record._deleted.value);
+ }
+
+ if (backupFileType === 'CPE') {
+ const backup = backupContents as BackupCPEType;
+ const oldCongregations = backup.visiting_speakers;
+
+ const result = migrateSpeakers(oldCongregations);
+ return result.speakers;
+ }
+
+ return [];
+ }, [backupFileType, backupContents, migrateSpeakers]);
+
+ const user_field_service_reports = useMemo(() => {
+ if (backupFileType === 'Organized') {
+ const backup = backupContents as BackupOrganizedType;
+ const backupData = backup.data[
+ 'user_field_service_reports'
+ ] as UserFieldServiceReportType[];
+ return backupData.filter((record) => !record.report_data._deleted);
+ }
+
+ return [];
+ }, [backupFileType, backupContents]);
+
+ const cong_field_service_reports = useMemo(() => {
+ if (backupFileType === 'Organized') {
+ const backup = backupContents as BackupOrganizedType;
+ const backupData = backup.data[
+ 'cong_field_service_reports'
+ ] as CongFieldServiceReportType[];
+ return backupData.filter((record) => !record.report_data._deleted);
+ }
+
+ if (backupFileType === 'CPE') {
+ const backup = backupContents as BackupCPEType;
+ const oldReports = backup.fieldServiceReports;
+
+ return migrateCongReports(oldReports);
+ }
+
+ return [];
+ }, [backupFileType, backupContents, migrateCongReports]);
+
+ const meeting_attendance = useMemo(() => {
+ if (backupFileType === 'Organized') {
+ const backup = backupContents as BackupOrganizedType;
+ const backupData = backup.data[
+ 'meeting_attendance'
+ ] as MeetingAttendanceType[];
+ return backupData;
+ }
+
+ if (backupFileType === 'CPE') {
+ const backup = backupContents as BackupCPEType;
+ const oldAttendances = backup.meetingAttendance;
+
+ return migrateAttendances(oldAttendances);
+ }
+
+ return [];
+ }, [backupFileType, backupContents, migrateAttendances]);
+
+ const schedules = useMemo(() => {
+ if (backupFileType === 'Organized') {
+ const backup = backupContents as BackupOrganizedType;
+ const backupData = backup.data['sched'] as SchedWeekType[];
+ return backupData;
+ }
+
+ if (backupFileType === 'CPE') {
+ const backup = backupContents as BackupCPEType;
+ const oldSchedules = backup.sched;
+
+ return migrateSchedules(oldSchedules);
+ }
+
+ return [];
+ }, [backupFileType, backupContents, migrateSchedules]);
+
+ const selectedAll = useMemo(() => {
+ if (persons.length > 0 && !selected.persons) {
+ return false;
+ }
+
+ if (
+ cong_field_service_reports.length > 0 &&
+ !selected.cong_field_service_reports
+ ) {
+ return false;
+ }
+
+ if (field_service_groups.length > 0 && !selected.field_service_groups) {
+ return false;
+ }
+
+ if (meeting_attendance.length > 0 && !selected.meeting_attendance) {
+ return false;
+ }
+
+ if (schedules.length > 0 && !selected.midweek_history) {
+ return false;
+ }
+
+ if (schedules.length > 0 && !selected.weekend_history) {
+ return false;
+ }
+
+ if (
+ user_field_service_reports.length > 0 &&
+ !selected.user_field_service_reports
+ ) {
+ return false;
+ }
+
+ if (visiting_speakers.length > 0 && !selected.visiting_speakers) {
+ return false;
+ }
+
+ if (Object.values(selected).every((value) => !value)) return false;
+
+ return true;
+ }, [
+ selected,
+ persons,
+ cong_field_service_reports,
+ field_service_groups,
+ meeting_attendance,
+ schedules,
+ user_field_service_reports,
+ visiting_speakers,
+ ]);
+
+ const inderterminate = useMemo(() => {
+ if (selectedAll) return false;
+
+ return Object.values(selected).some((value) => value);
+ }, [selectedAll, selected]);
+
+ const handleSelectAll = () => {
+ if (inderterminate || selectedAll) {
+ setSelected((prev) => {
+ const data = structuredClone(prev);
+
+ for (const key of Object.keys(data)) {
+ data[key] = false;
+ }
+
+ return data;
+ });
+ }
+
+ if (!inderterminate && !selectedAll) {
+ setSelected((prev) => {
+ const data = structuredClone(prev);
+
+ if (persons.length > 0) {
+ data.persons = true;
+ }
+
+ if (cong_field_service_reports.length > 0) {
+ data.cong_field_service_reports = true;
+ }
+
+ if (field_service_groups.length > 0) {
+ data.field_service_groups = true;
+ }
+
+ if (meeting_attendance.length > 0) {
+ data.meeting_attendance = true;
+ }
+
+ if (schedules.length > 0) {
+ data.midweek_history = true;
+ }
+
+ if (schedules.length > 0) {
+ data.weekend_history = true;
+ }
+
+ if (user_field_service_reports.length > 0) {
+ data.user_field_service_reports = true;
+ }
+
+ if (visiting_speakers.length > 0) {
+ data.visiting_speakers = true;
+ }
+
+ return data;
+ });
+ }
+ };
+
+ const handleSelectData = (field: ImportFieldType, checked: boolean) => {
+ setSelected((prev) => {
+ const data = structuredClone(prev);
+ data[field] = checked;
+
+ if (field === 'persons' && !checked) {
+ data.cong_field_service_reports = false;
+ data.field_service_groups = false;
+ data.midweek_history = false;
+ data.visiting_speakers = false;
+ data.weekend_history = false;
+ }
+
+ const needsPerson: ImportFieldType[] = [
+ 'cong_field_service_reports',
+ 'field_service_groups',
+ 'midweek_history',
+ 'visiting_speakers',
+ 'weekend_history',
+ ];
+
+ if (needsPerson.includes(field) && checked) {
+ data.persons = true;
+ }
+
+ return data;
+ });
+ };
+
+ const handleImportData = async () => {
+ if (isProcessing) return;
+
+ if (Object.values(selected).every((value) => !value)) return;
+
+ try {
+ setIsProcessing(true);
+
+ const backup = backupContents as BackupOrganizedType;
+
+ const data = {} as ImportDbType;
+
+ if (selected.persons) {
+ data.persons = await getPersons(persons);
+ }
+
+ if (selected.field_service_groups) {
+ data.field_service_groups =
+ await getServiceGroups(field_service_groups);
+ }
+
+ if (selected.visiting_speakers) {
+ if (backupFileType === 'Organized') {
+ const backupData = backup.data[
+ 'speakers_congregations'
+ ] as SpeakersCongregationsType[];
+
+ const speakers_congregations = backupData.filter(
+ (record) => !record._deleted.value
+ );
+
+ data.visiting_speakers = await getVisitingSpeakers(visiting_speakers);
+
+ data.speakers_congregations = await getSpeakersCongregations(
+ speakers_congregations
+ );
+ }
+
+ if (backupFileType === 'CPE') {
+ const backup = backupContents as BackupCPEType;
+ const oldCongregations = backup.visiting_speakers;
+
+ const { congregations, speakers } = migrateSpeakers(oldCongregations);
+
+ data.visiting_speakers = speakers;
+ data.speakers_congregations = congregations;
+ }
+ }
+
+ if (selected.user_field_service_reports) {
+ const backupData = backup.data[
+ 'user_bible_studies'
+ ] as UserBibleStudyType[];
+
+ const user_bible_studies = backupData.filter(
+ (record) => !record.person_data._deleted
+ );
+
+ data.user_field_service_reports = await getUserReports(
+ user_field_service_reports
+ );
+ data.user_bible_studies = await getUserBibleStudies(user_bible_studies);
+ }
+
+ if (selected.cong_field_service_reports) {
+ data.cong_field_service_reports = await getCongFieldReports(
+ cong_field_service_reports
+ );
+
+ if (backupFileType === 'Organized') {
+ const backupAnalysisData = backup.data[
+ 'branch_cong_analysis'
+ ] as BranchCongAnalysisType[];
+
+ const branch_cong_analysis = backupAnalysisData.filter(
+ (record) => !record.report_data._deleted
+ );
+
+ const backupBranchReportsData = backup.data[
+ 'branch_field_service_reports'
+ ] as BranchFieldServiceReportType[];
+
+ const branch_field_service_reports = backupBranchReportsData.filter(
+ (record) => !record.report_data._deleted
+ );
+
+ data.branch_cong_analysis =
+ await getBranchCongAnalysis(branch_cong_analysis);
+
+ data.branch_field_service_reports = await getBranchFieldReports(
+ branch_field_service_reports
+ );
+ }
+
+ if (backupFileType === 'CPE') {
+ const backup = backupContents as BackupCPEType;
+ const oldReports = backup.branchReports;
+ data.branch_field_service_reports = migrateBranchReports(oldReports);
+ }
+ }
+
+ if (selected.meeting_attendance) {
+ data.meeting_attendance = await getAttendances(meeting_attendance);
+ }
+
+ const isMidweek = selected.midweek_history;
+ const isWeekend = selected.weekend_history;
+
+ if (isMidweek || isWeekend) {
+ data.sched = getSchedules(schedules, isMidweek, isWeekend);
+
+ if (backupFileType === 'Organized') {
+ const sources = backup.data['sources'] as SourceWeekType[];
+ data.sources = getSources(sources);
+ }
+
+ if (backupFileType === 'CPE') {
+ const backup = backupContents as BackupCPEType;
+ const oldSources = backup.sources;
+ const oldSchedules = backup.sched;
+ data.sources = migrateSources(oldSources, oldSchedules);
+ }
+ }
+
+ if (data.branch_cong_analysis) {
+ await appDb.branch_cong_analysis.clear();
+ await appDb.branch_cong_analysis.bulkPut(data.branch_cong_analysis);
+ }
+
+ if (data.branch_field_service_reports) {
+ await appDb.branch_field_service_reports.clear();
+ await appDb.branch_field_service_reports.bulkPut(
+ data.branch_field_service_reports
+ );
+ }
+
+ if (data.cong_field_service_reports) {
+ await appDb.cong_field_service_reports.clear();
+ await appDb.cong_field_service_reports.bulkPut(
+ data.cong_field_service_reports
+ );
+ }
+
+ if (data.field_service_groups) {
+ await appDb.field_service_groups.clear();
+ await appDb.field_service_groups.bulkPut(data.field_service_groups);
+ }
+
+ if (data.meeting_attendance) {
+ await appDb.meeting_attendance.clear();
+ await appDb.meeting_attendance.bulkPut(data.meeting_attendance);
+ }
+
+ if (data.persons) {
+ await appDb.persons.clear();
+ await appDb.persons.bulkPut(data.persons);
+ }
+
+ if (data.sched) {
+ await appDb.sched.clear();
+ await appDb.sched.bulkPut(data.sched);
+ }
+
+ if (data.sources) {
+ await appDb.sources.clear();
+ await appDb.sources.bulkPut(data.sources);
+ }
+
+ if (data.speakers_congregations) {
+ await appDb.speakers_congregations.clear();
+ await appDb.speakers_congregations.bulkPut(data.speakers_congregations);
+ }
+
+ if (data.user_bible_studies) {
+ await appDb.user_bible_studies.clear();
+ await appDb.user_bible_studies.bulkPut(data.user_bible_studies);
+ }
+
+ if (data.user_field_service_reports) {
+ await appDb.user_field_service_reports.clear();
+ await appDb.user_field_service_reports.bulkPut(
+ data.user_field_service_reports
+ );
+ }
+
+ if (data.visiting_speakers) {
+ await appDb.visiting_speakers.clear();
+ await appDb.visiting_speakers.bulkPut(data.visiting_speakers);
+ }
+
+ await displaySnackNotification({
+ severity: 'success',
+ header: t('tr_importDataCompleted'),
+ message: t('tr_importDataCompletedDesc'),
+ });
+
+ setTimeout(() => {
+ window.location.href = './';
+ }, 2000);
+
+ setIsProcessing(false);
+ } catch (error) {
+ setIsProcessing(false);
+
+ console.error(error);
+
+ displaySnackNotification({
+ severity: 'error',
+ header: getMessageByCode('error_app_generic-title'),
+ message: getMessageByCode(error.message),
+ });
+ }
+ };
+
+ return {
+ filename,
+ persons,
+ isProcessing,
+ handleImportData,
+ handleSelectData,
+ selected,
+ field_service_groups,
+ visiting_speakers,
+ user_field_service_reports,
+ cong_field_service_reports,
+ meeting_attendance,
+ schedules,
+ selectedAll,
+ inderterminate,
+ handleSelectAll,
+ };
+};
+
+export default useConfirmImport;
diff --git a/src/features/congregation/settings/import_export/confirm_import/useCongReportsImport.tsx b/src/features/congregation/settings/import_export/confirm_import/useCongReportsImport.tsx
new file mode 100644
index 0000000000..9fcd6620e1
--- /dev/null
+++ b/src/features/congregation/settings/import_export/confirm_import/useCongReportsImport.tsx
@@ -0,0 +1,91 @@
+import { updatedAtOverride } from '@utils/common';
+import { BranchCongAnalysisType } from '@definition/branch_cong_analysis';
+import { BranchFieldServiceReportType } from '@definition/branch_field_service_reports';
+import { CongFieldServiceReportType } from '@definition/cong_field_service_reports';
+import appDb from '@db/appDb';
+
+const useCongReportsImport = () => {
+ const getBranchCongAnalysis = async (reports: BranchCongAnalysisType[]) => {
+ const result: BranchCongAnalysisType[] = [];
+
+ result.push(...reports);
+
+ const oldReports = await appDb.branch_cong_analysis.toArray();
+
+ for (const oldReport of oldReports) {
+ const newReport = reports.find(
+ (record) => record.report_date === oldReport.report_date
+ );
+
+ if (!newReport) {
+ oldReport.report_data._deleted = true;
+ oldReport.report_data.updatedAt = new Date().toISOString();
+
+ result.push(oldReport);
+ }
+ }
+
+ return result.map((record) => {
+ const data = updatedAtOverride(record);
+ return data;
+ });
+ };
+
+ const getBranchFieldReports = async (
+ reports: BranchFieldServiceReportType[]
+ ) => {
+ const result: BranchFieldServiceReportType[] = [];
+
+ result.push(...reports);
+
+ const oldReports = await appDb.branch_field_service_reports.toArray();
+
+ for (const oldReport of oldReports) {
+ const newReport = reports.find(
+ (record) => record.report_date === oldReport.report_date
+ );
+
+ if (!newReport) {
+ oldReport.report_data._deleted = true;
+ oldReport.report_data.updatedAt = new Date().toISOString();
+
+ result.push(oldReport);
+ }
+ }
+
+ return result.map((record) => {
+ const data = updatedAtOverride(record);
+ return data;
+ });
+ };
+
+ const getCongFieldReports = async (reports: CongFieldServiceReportType[]) => {
+ const result: CongFieldServiceReportType[] = [];
+
+ result.push(...reports);
+
+ const oldReports = await appDb.cong_field_service_reports.toArray();
+
+ for (const oldReport of oldReports) {
+ const newReport = reports.find(
+ (record) => record.report_id === oldReport.report_id
+ );
+
+ if (!newReport) {
+ oldReport.report_data._deleted = true;
+ oldReport.report_data.updatedAt = new Date().toISOString();
+
+ result.push(oldReport);
+ }
+ }
+
+ return result.map((record) => {
+ const data = updatedAtOverride(record);
+ return data;
+ });
+ };
+
+ return { getBranchCongAnalysis, getBranchFieldReports, getCongFieldReports };
+};
+
+export default useCongReportsImport;
diff --git a/src/features/congregation/settings/import_export/confirm_import/useImportCPE.tsx b/src/features/congregation/settings/import_export/confirm_import/useImportCPE.tsx
new file mode 100644
index 0000000000..d22227bc79
--- /dev/null
+++ b/src/features/congregation/settings/import_export/confirm_import/useImportCPE.tsx
@@ -0,0 +1,1258 @@
+import { PersonType } from '@definition/person';
+import { BackupCPEType } from '@definition/backup';
+import { formatDate } from '@services/dateformat';
+import { FieldServiceGroupType } from '@definition/field_service_groups';
+import {
+ scheduleSchema,
+ sourceSchema,
+ speakersCongregationSchema,
+ vistingSpeakerSchema,
+} from '@services/dexie/schema';
+import { SpeakersCongregationsType } from '@definition/speakers_congregations';
+import { VisitingSpeakerType } from '@definition/visiting_speakers';
+import { CongFieldServiceReportType } from '@definition/cong_field_service_reports';
+import { BranchFieldServiceReportType } from '@definition/branch_field_service_reports';
+import { MeetingAttendanceType } from '@definition/meeting_attendance';
+import { LANGUAGE_LIST } from '@constants/index';
+import { SourceWeekType } from '@definition/sources';
+import { SchedWeekType } from '@definition/schedules';
+
+const isMondayDate = (date) => {
+ const inputDate = new Date(date);
+ const dayOfWeek = inputDate.getDay();
+
+ return dayOfWeek === 1;
+};
+
+const useImportCPE = () => {
+ const migratePersons = (oldPersons: BackupCPEType['persons']) => {
+ const newPersons: PersonType[] = [];
+
+ for (const oldPerson of oldPersons) {
+ const names = oldPerson.person_name.split(' ');
+ const lastname = names.shift();
+ const firstname = names.join(' ');
+
+ const firstReport = oldPerson.firstMonthReport
+ ? new Date(oldPerson.firstMonthReport).toISOString()
+ : null;
+
+ const baptismDate = oldPerson.immersedDate
+ ? new Date(oldPerson.immersedDate).toISOString()
+ : null;
+
+ const validStatus = [];
+
+ if (firstReport) {
+ if (oldPerson.spiritualStatus) {
+ const isActive = oldPerson.spiritualStatus.some(
+ (record) => record.endDate === null
+ );
+
+ const records = oldPerson.spiritualStatus
+ .filter(
+ (record) =>
+ record.startDate &&
+ new Date(record.startDate).toISOString() >= firstReport
+ )
+ .sort((a, b) => a.startDate.localeCompare(b.startDate))
+ .map((record) => {
+ return {
+ id: record.statusId,
+ _deleted: false,
+ updatedAt: new Date().toISOString(),
+ start_date: formatDate(
+ new Date(record.startDate),
+ 'yyyy/MM/dd'
+ ),
+ end_date: record.endDate
+ ? formatDate(new Date(record.endDate), 'yyyy/MM/dd')
+ : null,
+ };
+ });
+
+ if (records.length > 0) {
+ const firstRecord = records.at(0);
+ firstRecord.start_date = formatDate(
+ new Date(firstReport),
+ 'yyyy/MM/dd'
+ );
+ }
+
+ if (isActive && records.length === 0) {
+ records.push({
+ id: crypto.randomUUID(),
+ _deleted: false,
+ updatedAt: new Date().toISOString(),
+ start_date: formatDate(new Date(firstReport), 'yyyy/MM/dd'),
+ end_date: null,
+ });
+ }
+
+ validStatus.push(...records);
+ }
+ }
+
+ const unbaptizedStatus = [];
+ const baptizedStatus = [];
+
+ if (!baptismDate) {
+ unbaptizedStatus.push(...validStatus);
+ }
+
+ if (baptismDate && baptismDate <= firstReport) {
+ baptizedStatus.push(...validStatus);
+ }
+
+ if (baptismDate && baptismDate > firstReport) {
+ const baptism = new Date(baptismDate);
+ const splitDate = formatDate(baptism, 'yyyy/MM/01');
+
+ const baptismYear = baptism.getFullYear();
+ const baptismMonth = baptism.getMonth();
+
+ const prevDate = new Date(baptismYear, baptismMonth, 0);
+
+ const splitDateObj = new Date(splitDate);
+
+ validStatus.forEach((dateRange) => {
+ const startDate = new Date(dateRange.start_date);
+ const endDate = dateRange.end_date
+ ? new Date(dateRange.end_date)
+ : splitDateObj;
+
+ if (splitDateObj > startDate && splitDateObj <= endDate) {
+ unbaptizedStatus.push({
+ start_date: dateRange.start_date,
+ end_date: formatDate(prevDate, 'yyyy/MM/dd'),
+ });
+ baptizedStatus.push({
+ start_date: splitDate,
+ end_date: dateRange.end_date,
+ });
+ } else if (startDate >= splitDateObj) {
+ baptizedStatus.push(dateRange);
+ } else {
+ unbaptizedStatus.push(dateRange);
+ }
+ });
+ }
+
+ const isMidweekStudent =
+ unbaptizedStatus.length === 0 && baptizedStatus.length === 0;
+
+ const assignments = oldPerson.assignments.map((assignment) => {
+ return {
+ code: assignment.code,
+ updatedAt: new Date().toISOString(),
+ _deleted: false,
+ };
+ });
+
+ const privileges = oldPerson.spiritualStatus
+ ? oldPerson.spiritualStatus
+ .filter(
+ (record) =>
+ record.startDate &&
+ (record.status === 'elder' || record.status === 'ms')
+ )
+ .map((record) => {
+ return {
+ id: record.statusId,
+ _deleted: false,
+ updatedAt: new Date().toISOString(),
+ privilege: record.status,
+ start_date: formatDate(
+ new Date(record.startDate),
+ 'yyyy/MM/dd'
+ ),
+ end_date: record.endDate
+ ? formatDate(new Date(record.endDate), 'yyyy/MM/dd')
+ : null,
+ };
+ })
+ .sort((a, b) => a.start_date.localeCompare(b.start_date))
+ : [];
+
+ const enrollments = oldPerson.otherService
+ ? oldPerson.otherService
+ .filter((record) => record.startDate)
+ .map((record) => {
+ const enrollment =
+ record.service === 'specialPioneer'
+ ? 'FS'
+ : record.service === 'regularPioneer'
+ ? 'FR'
+ : 'AP';
+
+ return {
+ id: record.serviceId,
+ _deleted: false,
+ updatedAt: new Date().toISOString(),
+ enrollment: enrollment,
+ start_date: formatDate(
+ new Date(record.startDate),
+ 'yyyy/MM/dd'
+ ),
+ end_date: record.endDate
+ ? formatDate(new Date(record.endDate), 'yyyy/MM/dd')
+ : null,
+ };
+ })
+ : [];
+
+ const newPerson = {
+ _deleted: { value: false, updatedAt: '' },
+ person_uid: oldPerson.person_uid,
+ person_data: {
+ disqualified: {
+ value: oldPerson.isDisqualified,
+ updatedAt: new Date().toISOString(),
+ },
+ female: {
+ value: oldPerson.isFemale,
+ updatedAt: new Date().toISOString(),
+ },
+ male: {
+ value: oldPerson.isMale,
+ updatedAt: new Date().toISOString(),
+ },
+ archived: {
+ value: false,
+ updatedAt: new Date().toISOString(),
+ },
+ person_firstname: {
+ value: firstname,
+ updatedAt: new Date().toISOString(),
+ },
+ person_lastname: {
+ value: lastname,
+ updatedAt: new Date().toISOString(),
+ },
+ person_display_name: {
+ value: oldPerson.person_displayName,
+ updatedAt: new Date().toISOString(),
+ },
+ birth_date: {
+ value: oldPerson.birthDate
+ ? new Date(oldPerson.birthDate).toISOString()
+ : null,
+ updatedAt: new Date().toISOString(),
+ },
+ address: {
+ value: oldPerson.address,
+ updatedAt: new Date().toISOString(),
+ },
+ email: {
+ value: oldPerson.email,
+ updatedAt: new Date().toISOString(),
+ },
+ phone: {
+ value: oldPerson.phone,
+ updatedAt: new Date().toISOString(),
+ },
+ first_report: {
+ value: firstReport,
+ updatedAt: new Date().toISOString(),
+ },
+ publisher_baptized: {
+ active: {
+ value: oldPerson.isBaptized,
+ updatedAt: new Date().toISOString(),
+ },
+ baptism_date: {
+ value: baptismDate ? new Date(baptismDate).toISOString() : null,
+ updatedAt: new Date().toISOString(),
+ },
+ other_sheep: {
+ value: oldPerson.isOtherSheep,
+ updatedAt: new Date().toISOString(),
+ },
+ anointed: {
+ value: oldPerson.isAnointed,
+ updatedAt: new Date().toISOString(),
+ },
+ history: baptizedStatus,
+ },
+ publisher_unbaptized: {
+ active: {
+ value: !isMidweekStudent && !oldPerson.isBaptized,
+ updatedAt: new Date().toISOString(),
+ },
+ history: unbaptizedStatus,
+ },
+ midweek_meeting_student: {
+ active: {
+ value: isMidweekStudent,
+ updatedAt: new Date().toISOString(),
+ },
+ history: isMidweekStudent
+ ? [
+ {
+ id: crypto.randomUUID(),
+ _deleted: false,
+ updatedAt: new Date().toISOString(),
+ start_date: '2023/09/01',
+ end_date: null,
+ },
+ ]
+ : [],
+ },
+ timeAway: [],
+ assignments,
+ privileges,
+ enrollments,
+ emergency_contacts: [],
+ categories: ['main'],
+ },
+ };
+
+ newPersons.push(newPerson as PersonType);
+ }
+
+ return newPersons;
+ };
+
+ const migrateServiceGroups = (
+ oldLists: BackupCPEType['fieldServiceGroup']
+ ) => {
+ const currentList = oldLists.find(
+ (record) => record.isCurrent && !record.deleted
+ );
+
+ if (!currentList) return [];
+
+ const groups: FieldServiceGroupType[] = [];
+
+ let index = 0;
+ for (const group of currentList.groups) {
+ const sortedPersons = group.persons.sort((a, b) => {
+ if (a.isOverseer === b.isOverseer) {
+ return Number(b.isAssistant) - Number(a.isAssistant);
+ }
+
+ return Number(b.isOverseer) - Number(a.isOverseer);
+ });
+
+ groups.push({
+ group_id: group.group_uid,
+ group_data: {
+ _deleted: false,
+ updatedAt: new Date().toISOString(),
+ name: '',
+ sort_index: index,
+ members: sortedPersons.map((person, sort_index) => {
+ return { ...person, sort_index };
+ }),
+ },
+ });
+
+ index++;
+ }
+
+ return groups;
+ };
+
+ const migrateSpeakers = (
+ oldCongregations: BackupCPEType['visiting_speakers']
+ ) => {
+ oldCongregations = oldCongregations.filter(
+ (record) => !record.is_deleted && record.is_local
+ );
+
+ const congregations: SpeakersCongregationsType[] = [];
+ const speakers: VisitingSpeakerType[] = [];
+
+ for (const congregation of oldCongregations) {
+ const objCong = structuredClone(speakersCongregationSchema);
+
+ objCong.id = crypto.randomUUID();
+
+ objCong.cong_data.cong_number = {
+ value: String(congregation.cong_number),
+ updatedAt: new Date().toISOString(),
+ };
+
+ objCong.cong_data.cong_name = {
+ value: congregation.cong_name,
+ updatedAt: new Date().toISOString(),
+ };
+
+ objCong.cong_data.request_status = 'approved';
+
+ congregations.push(objCong);
+
+ for (const speaker of congregation.cong_speakers) {
+ const objSpeaker = structuredClone(vistingSpeakerSchema);
+
+ const names = speaker.person_name.split(' ');
+ const lastname = names.shift();
+ const firstname = names.join(' ');
+
+ objSpeaker.person_uid = speaker.person_uid;
+ objSpeaker.speaker_data.cong_id = objCong.id;
+ objSpeaker.speaker_data.person_firstname = {
+ value: firstname,
+ updatedAt: new Date().toISOString(),
+ };
+ objSpeaker.speaker_data.person_lastname = {
+ value: lastname,
+ updatedAt: new Date().toISOString(),
+ };
+ objSpeaker.speaker_data.person_display_name = {
+ value: speaker.person_displayName,
+ updatedAt: new Date().toISOString(),
+ };
+ objSpeaker.speaker_data.person_email = {
+ value: speaker.email,
+ updatedAt: new Date().toISOString(),
+ };
+ objSpeaker.speaker_data.person_phone = {
+ value: speaker.phone,
+ updatedAt: new Date().toISOString(),
+ };
+ objSpeaker.speaker_data.elder = {
+ value: speaker.is_elder,
+ updatedAt: new Date().toISOString(),
+ };
+ objSpeaker.speaker_data.ministerial_servant = {
+ value: speaker.is_ms,
+ updatedAt: new Date().toISOString(),
+ };
+ objSpeaker.speaker_data.talks = speaker.talks.map((talk) => {
+ return {
+ _deleted: false,
+ updatedAt: new Date().toISOString(),
+ talk_number: talk,
+ talk_songs: [],
+ };
+ });
+
+ speakers.push(objSpeaker);
+ }
+ }
+
+ return { congregations, speakers };
+ };
+
+ const migrateCongReports = (
+ oldReports: BackupCPEType['fieldServiceReports']
+ ) => {
+ const reports: CongFieldServiceReportType[] = [];
+
+ for (const report of oldReports) {
+ for (const record of report.months) {
+ const shared =
+ +record.bibleStudies > 0 ||
+ +record.hourCredit > 0 ||
+ +record.hours > 0 ||
+ +record.minutes > 0 ||
+ +record.placements > 0 ||
+ +record.returnVisits > 0 ||
+ +record.videos > 0;
+
+ reports.push({
+ report_id: record.uid,
+ report_data: {
+ _deleted: false,
+ updatedAt: new Date().toISOString(),
+ report_date: formatDate(new Date(record.month_value), 'yyyy/MM'),
+ person_uid: report.person_uid,
+ shared_ministry: shared,
+ hours: {
+ field_service: +record.hours,
+ credit: { value: 0, approved: +record.hourCredit },
+ },
+ bible_studies: +record.bibleStudies,
+ comments: record.comments,
+ late: { value: false, submitted: '' },
+ status: 'confirmed',
+ },
+ });
+ }
+ }
+
+ return reports;
+ };
+
+ const migrateBranchReports = (oldReports: BackupCPEType['branchReports']) => {
+ const reports: BranchFieldServiceReportType[] = oldReports.map((record) => {
+ return {
+ report_date: formatDate(new Date(record.month), 'yyyy/MM'),
+ report_data: {
+ _deleted: false,
+ updatedAt: new Date().toISOString(),
+ publishers_active: record.details.activePublishers,
+ weekend_meeting_average: record.details.weekendMeetingAttendanceAvg,
+ submitted: record.details.isSubmitted,
+ publishers: {
+ report_count: record.details.publishersReports,
+ bible_studies: record.details.totalBibleStudies,
+ },
+ APs: {
+ report_count: record.details.auxPioneersReports,
+ hours: record.details.auxPioneersHours,
+ bible_studies: record.details.auxPioneersBibleStudies,
+ },
+ FRs: {
+ report_count: record.details.FRReports,
+ hours: record.details.FRHours,
+ bible_studies: record.details.FRBibleStudies,
+ },
+ },
+ };
+ });
+
+ return reports;
+ };
+
+ const migrateAttendances = (
+ oldAttendances: BackupCPEType['meetingAttendance']
+ ) => {
+ const attendances: MeetingAttendanceType[] = oldAttendances.map(
+ (record) => {
+ const midweek1 = record.midweek_meeting.find(
+ (meet) => meet.index === 1
+ );
+ const midweek2 = record.midweek_meeting.find(
+ (meet) => meet.index === 2
+ );
+ const midweek3 = record.midweek_meeting.find(
+ (meet) => meet.index === 3
+ );
+ const midweek4 = record.midweek_meeting.find(
+ (meet) => meet.index === 4
+ );
+ const midweek5 = record.midweek_meeting.find(
+ (meet) => meet.index === 5
+ );
+
+ const weekend1 = record.weekend_meeting.find(
+ (meet) => meet.index === 1
+ );
+ const weekend2 = record.weekend_meeting.find(
+ (meet) => meet.index === 2
+ );
+ const weekend3 = record.weekend_meeting.find(
+ (meet) => meet.index === 3
+ );
+ const weekend4 = record.weekend_meeting.find(
+ (meet) => meet.index === 4
+ );
+ const weekend5 = record.weekend_meeting.find(
+ (meet) => meet.index === 5
+ );
+
+ return {
+ month_date: formatDate(new Date(record.month_value), 'yyyy/MM'),
+ week_1: {
+ midweek: [
+ {
+ present: midweek1?.count ? +midweek1.count : undefined,
+ online: undefined,
+ type: 'main',
+ updatedAt: midweek1?.count ? new Date().toISOString() : '',
+ },
+ ],
+ weekend: [
+ {
+ present: weekend1?.count ? +weekend1.count : undefined,
+ online: undefined,
+ type: 'main',
+ updatedAt: weekend1?.count ? new Date().toISOString() : '',
+ },
+ ],
+ },
+ week_2: {
+ midweek: [
+ {
+ present: midweek2?.count ? +midweek2.count : undefined,
+ online: undefined,
+ type: 'main',
+ updatedAt: midweek2?.count ? new Date().toISOString() : '',
+ },
+ ],
+ weekend: [
+ {
+ present: weekend2?.count ? +weekend2.count : undefined,
+ online: undefined,
+ type: 'main',
+ updatedAt: weekend2?.count ? new Date().toISOString() : '',
+ },
+ ],
+ },
+ week_3: {
+ midweek: [
+ {
+ present: midweek3?.count ? +midweek3.count : undefined,
+ online: undefined,
+ type: 'main',
+ updatedAt: midweek3?.count ? new Date().toISOString() : '',
+ },
+ ],
+ weekend: [
+ {
+ present: weekend3?.count ? +weekend3.count : undefined,
+ online: undefined,
+ type: 'main',
+ updatedAt: weekend3?.count ? new Date().toISOString() : '',
+ },
+ ],
+ },
+ week_4: {
+ midweek: [
+ {
+ present: midweek4?.count ? +midweek4.count : undefined,
+ online: undefined,
+ type: 'main',
+ updatedAt: midweek4?.count ? new Date().toISOString() : '',
+ },
+ ],
+ weekend: [
+ {
+ present: weekend4?.count ? +weekend4.count : undefined,
+ online: undefined,
+ type: 'main',
+ updatedAt: weekend4?.count ? new Date().toISOString() : '',
+ },
+ ],
+ },
+ week_5: {
+ midweek: [
+ {
+ present: midweek5?.count ? +midweek5.count : undefined,
+ online: undefined,
+ type: 'main',
+ updatedAt: midweek5?.count ? new Date().toISOString() : '',
+ },
+ ],
+ weekend: [
+ {
+ present: weekend5?.count ? +weekend5.count : undefined,
+ online: undefined,
+ type: 'main',
+ updatedAt: weekend5?.count ? new Date().toISOString() : '',
+ },
+ ],
+ },
+ _deleted: { value: false, updatedAt: new Date().toISOString() },
+ };
+ }
+ );
+
+ return attendances;
+ };
+
+ const migrateSources = (
+ oldSources: BackupCPEType['sources'],
+ schedules: BackupCPEType['sched']
+ ) => {
+ const uiLang = localStorage.getItem('ui_lang') || 'en';
+ const lang =
+ LANGUAGE_LIST.find((record) => record.locale === uiLang)?.code || 'E';
+
+ const sources: SourceWeekType[] = [];
+
+ for (const record of oldSources) {
+ const isMonday = isMondayDate(record.weekOf);
+
+ if (!isMonday) continue;
+
+ if (!record.mwb_week_date_locale) continue;
+
+ const obj = structuredClone(sourceSchema);
+
+ const schedule = schedules.find((s) => s.weekOf === record.weekOf);
+
+ obj.weekOf = record.weekOf;
+
+ obj.midweek_meeting.week_date_locale = record.mwb_week_date_locale;
+
+ obj.midweek_meeting.weekly_bible_reading =
+ record.mwb_weekly_bible_reading;
+
+ obj.midweek_meeting.song_first = {
+ [lang]: String(record.mwb_song_first),
+ };
+
+ obj.midweek_meeting.tgw_talk = {
+ src: record.mwb_tgw_talk,
+ time: { default: 10, override: [] },
+ };
+
+ obj.midweek_meeting.tgw_gems = {
+ title: { [lang]: '' },
+ time: { default: 10, override: [] },
+ };
+
+ obj.midweek_meeting.tgw_bible_reading = {
+ src: record.mwb_tgw_bread,
+ title: { [lang]: '' },
+ };
+
+ obj.midweek_meeting.ayf_count = { [lang]: record.mwb_ayf_count };
+
+ obj.midweek_meeting.ayf_part1 = {
+ type: record.mwb_ayf_part1_type,
+ time: { [lang]: record.mwb_ayf_part1_time },
+ src: record.mwb_ayf_part1,
+ title: { [lang]: '' },
+ };
+
+ obj.midweek_meeting.ayf_part2 = {
+ type: record.mwb_ayf_part2_type,
+ time: { [lang]: record.mwb_ayf_part2_time },
+ src: record.mwb_ayf_part2,
+ title: { [lang]: '' },
+ };
+
+ obj.midweek_meeting.ayf_part3 = {
+ type: record.mwb_ayf_part3_type,
+ time: { [lang]: record.mwb_ayf_part3_time },
+ src: record.mwb_ayf_part3,
+ title: { [lang]: '' },
+ };
+
+ obj.midweek_meeting.ayf_part4 = {
+ type: record.mwb_ayf_part4_type,
+ time: { [lang]: record.mwb_ayf_part4_time },
+ src: record.mwb_ayf_part4,
+ title: { [lang]: '' },
+ };
+
+ obj.midweek_meeting.song_middle = {
+ [lang]: String(record.mwb_song_middle),
+ };
+
+ obj.midweek_meeting.lc_count = {
+ default: { [lang]: record.mwb_lc_count },
+ override: [
+ {
+ type: 'main',
+ value: record.mwb_lc_count_override,
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ };
+
+ obj.midweek_meeting.lc_part1 = {
+ time: {
+ default: { [lang]: record.mwb_lc_part1_time },
+ override: [
+ {
+ type: 'main',
+ value: record.mwb_lc_part1_time_override,
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ },
+ title: {
+ default: record.mwb_lc_part1,
+ override: [
+ {
+ type: 'main',
+ value: record.mwb_lc_part1_override?.[lang] ?? '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ },
+ desc: {
+ default: record.mwb_lc_part1_content,
+ override: [
+ {
+ type: 'main',
+ value: record.mwb_lc_part1_content_override?.[lang] ?? '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ },
+ };
+
+ obj.midweek_meeting.lc_part2 = {
+ time: {
+ default: { [lang]: record.mwb_lc_part2_time },
+ override: [
+ {
+ type: 'main',
+ value: record.mwb_lc_part2_time_override,
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ },
+ title: {
+ default: record.mwb_lc_part2,
+ override: [
+ {
+ type: 'main',
+ value: record.mwb_lc_part2_override?.[lang] ?? '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ },
+ desc: {
+ default: record.mwb_lc_part2_content,
+ override: [
+ {
+ type: 'main',
+ value: record.mwb_lc_part2_content_override?.[lang] ?? '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ },
+ };
+
+ obj.midweek_meeting.lc_cbs = {
+ title: { default: { [lang]: '' }, override: [] },
+ time: {
+ default: 30,
+ override: [
+ {
+ type: 'main',
+ value: +(record.mwb_lc_cbs_time_override || ''),
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ },
+ src: record.mwb_lc_cbs,
+ };
+
+ obj.midweek_meeting.co_talk_title = {
+ src: record.mwb_co_talk_title || '',
+ updatedAt: new Date().toISOString(),
+ };
+
+ obj.midweek_meeting.song_conclude = {
+ default: { [lang]: String(record.mwb_song_conclude) },
+ override: [
+ {
+ type: 'main',
+ value: String(record.mwb_song_conclude_override || ''),
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ };
+
+ const talk = schedule.public_talk || record.w_talk_title_override || '';
+
+ obj.weekend_meeting.public_talk = [
+ {
+ type: 'main',
+ value: talk,
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.weekend_meeting.co_talk_title.public = {
+ src: record.w_co_talk_title || '',
+ updatedAt: new Date().toISOString(),
+ };
+
+ obj.weekend_meeting.song_middle = {
+ [lang]: String(record.w_study_opening_song),
+ };
+
+ obj.weekend_meeting.w_study = record.w_study_title;
+
+ obj.weekend_meeting.song_conclude = {
+ default: { [lang]: String(record.w_study_concluding_song) },
+ override: [],
+ };
+
+ sources.push(obj);
+ }
+
+ return sources;
+ };
+
+ const migrateSchedules = (oldSchedules: BackupCPEType['sched']) => {
+ const schedules: SchedWeekType[] = [];
+
+ for (const record of oldSchedules) {
+ const isMonday = isMondayDate(record.weekOf);
+
+ if (!isMonday) continue;
+
+ const obj = structuredClone(scheduleSchema);
+
+ obj.weekOf = record.weekOf;
+
+ obj.midweek_meeting.chairman = {
+ main_hall: [
+ {
+ type: 'main',
+ value: record?.chairmanMM_A || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ aux_class_1: {
+ type: 'main',
+ value: record?.chairmanMM_B || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ };
+
+ obj.midweek_meeting.opening_prayer = [
+ {
+ type: 'main',
+ value: record?.opening_prayerMM || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.midweek_meeting.tgw_talk = [
+ {
+ type: 'main',
+ value: record?.tgw_talk || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.midweek_meeting.tgw_gems = [
+ {
+ type: 'main',
+ value: record?.tgw_gems || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.midweek_meeting.tgw_bible_reading = {
+ main_hall: [
+ {
+ type: 'main',
+ value: record?.bRead_stu_A || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ aux_class_1: {
+ type: 'main',
+ value: record?.bRead_stu_B || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ aux_class_2: {
+ type: 'main',
+ value: '',
+ name: '',
+ updatedAt: '',
+ },
+ };
+
+ obj.midweek_meeting.ayf_part1 = {
+ main_hall: {
+ student: [
+ {
+ type: 'main',
+ value: record?.ass1_stu_A || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ assistant: [
+ {
+ type: 'main',
+ value: record?.ass1_ass_A || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ },
+ aux_class_1: {
+ student: {
+ type: 'main',
+ value: record?.ass1_stu_B || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ assistant: {
+ type: 'main',
+ value: record?.ass1_ass_B || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ },
+ aux_class_2: {
+ student: { type: 'main', value: '', name: '', updatedAt: '' },
+ assistant: { type: 'main', value: '', name: '', updatedAt: '' },
+ },
+ };
+
+ obj.midweek_meeting.ayf_part2 = {
+ main_hall: {
+ student: [
+ {
+ type: 'main',
+ value: record?.ass2_stu_A || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ assistant: [
+ {
+ type: 'main',
+ value: record?.ass2_ass_A || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ },
+ aux_class_1: {
+ student: {
+ type: 'main',
+ value: record?.ass2_stu_B || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ assistant: {
+ type: 'main',
+ value: record?.ass2_ass_B || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ },
+ aux_class_2: {
+ student: { type: 'main', value: '', name: '', updatedAt: '' },
+ assistant: { type: 'main', value: '', name: '', updatedAt: '' },
+ },
+ };
+
+ obj.midweek_meeting.ayf_part3 = {
+ main_hall: {
+ student: [
+ {
+ type: 'main',
+ value: record?.ass3_stu_A || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ assistant: [
+ {
+ type: 'main',
+ value: record?.ass3_ass_A || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ },
+ aux_class_1: {
+ student: {
+ type: 'main',
+ value: record?.ass3_stu_B || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ assistant: {
+ type: 'main',
+ value: record?.ass3_ass_B || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ },
+ aux_class_2: {
+ student: { type: 'main', value: '', name: '', updatedAt: '' },
+ assistant: { type: 'main', value: '', name: '', updatedAt: '' },
+ },
+ };
+
+ obj.midweek_meeting.ayf_part4 = {
+ main_hall: {
+ student: [
+ {
+ type: 'main',
+ value: record?.ass4_stu_A || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ assistant: [
+ {
+ type: 'main',
+ value: record?.ass4_ass_A || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ },
+ aux_class_1: {
+ student: {
+ type: 'main',
+ value: record?.ass4_stu_B || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ assistant: {
+ type: 'main',
+ value: record?.ass4_ass_B || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ },
+ aux_class_2: {
+ student: { type: 'main', value: '', name: '', updatedAt: '' },
+ assistant: { type: 'main', value: '', name: '', updatedAt: '' },
+ },
+ };
+
+ obj.midweek_meeting.lc_part1 = [
+ {
+ type: 'main',
+ value: record?.lc_part1 || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.midweek_meeting.lc_part2 = [
+ {
+ type: 'main',
+ value: record?.lc_part2 || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.midweek_meeting.lc_part3 = [
+ {
+ type: 'main',
+ value: record?.lc_part3 || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.midweek_meeting.lc_cbs = {
+ conductor: [
+ {
+ type: 'main',
+ value: record?.cbs_conductor || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ reader: [
+ {
+ type: 'main',
+ value: record?.cbs_reader || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ };
+
+ obj.midweek_meeting.closing_prayer = [
+ {
+ type: 'main',
+ value: record?.closing_prayerMM || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.midweek_meeting.week_type = [
+ {
+ type: 'main',
+ value: record.week_type,
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.weekend_meeting.chairman = [
+ {
+ type: 'main',
+ value: record?.chairman_WM || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.weekend_meeting.opening_prayer = [
+ {
+ type: 'main',
+ value: record?.opening_prayerWM || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.weekend_meeting.public_talk_type = [
+ {
+ type: 'main',
+ value: record.is_visiting_speaker
+ ? 'visitingSpeaker'
+ : 'localSpeaker',
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ obj.weekend_meeting.speaker = {
+ part_1: [
+ {
+ type: 'main',
+ value: record.speaker_1 || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ part_2: [
+ {
+ type: 'main',
+ value: record.speaker_2 || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ substitute: [
+ {
+ type: 'main',
+ value: record.substitute_speaker || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ };
+
+ obj.weekend_meeting.wt_study = {
+ conductor: [{ type: 'main', value: '', name: '', updatedAt: '' }],
+ reader: [
+ {
+ type: 'main',
+ value: record.wtstudy_reader || '',
+ name: '',
+ updatedAt: new Date().toISOString(),
+ },
+ ],
+ };
+
+ obj.weekend_meeting.week_type = [
+ {
+ type: 'main',
+ value: record.week_type,
+ updatedAt: new Date().toISOString(),
+ },
+ ];
+
+ schedules.push(obj);
+ }
+
+ return schedules;
+ };
+
+ return {
+ migratePersons,
+ migrateServiceGroups,
+ migrateSpeakers,
+ migrateCongReports,
+ migrateBranchReports,
+ migrateAttendances,
+ migrateSources,
+ migrateSchedules,
+ };
+};
+
+export default useImportCPE;
diff --git a/src/features/congregation/settings/import_export/confirm_import/useMeetingImport.tsx b/src/features/congregation/settings/import_export/confirm_import/useMeetingImport.tsx
new file mode 100644
index 0000000000..5433e12b80
--- /dev/null
+++ b/src/features/congregation/settings/import_export/confirm_import/useMeetingImport.tsx
@@ -0,0 +1,40 @@
+import { SchedWeekType } from '@definition/schedules';
+import { SourceWeekType } from '@definition/sources';
+import { scheduleSchema } from '@services/dexie/schema';
+import { updatedAtOverride } from '@utils/common';
+
+const useMeetingImport = () => {
+ const getSources = (sources: SourceWeekType[]) => {
+ return sources.map((record) => {
+ const data = updatedAtOverride(record);
+ return data;
+ });
+ };
+
+ const getSchedules = (
+ schedules: SchedWeekType[],
+ midweek: boolean,
+ weekend: boolean
+ ) => {
+ return schedules.map((record) => {
+ const midweekData = midweek
+ ? record.midweek_meeting
+ : scheduleSchema.midweek_meeting;
+
+ const weekendData = weekend
+ ? record.weekend_meeting
+ : scheduleSchema.weekend_meeting;
+
+ record.midweek_meeting = midweekData;
+ record.weekend_meeting = weekendData;
+
+ updatedAtOverride(record);
+
+ return record;
+ });
+ };
+
+ return { getSources, getSchedules };
+};
+
+export default useMeetingImport;
diff --git a/src/features/congregation/settings/import_export/confirm_import/useMinistryReportsImport.tsx b/src/features/congregation/settings/import_export/confirm_import/useMinistryReportsImport.tsx
new file mode 100644
index 0000000000..f0d08012d0
--- /dev/null
+++ b/src/features/congregation/settings/import_export/confirm_import/useMinistryReportsImport.tsx
@@ -0,0 +1,62 @@
+import { updatedAtOverride } from '@utils/common';
+import { UserFieldServiceReportType } from '@definition/user_field_service_reports';
+import { UserBibleStudyType } from '@definition/user_bible_studies';
+import appDb from '@db/appDb';
+
+const useMinistryReportsImport = () => {
+ const getUserReports = async (reports: UserFieldServiceReportType[]) => {
+ const result: UserFieldServiceReportType[] = [];
+
+ result.push(...reports);
+
+ const oldReports = await appDb.user_field_service_reports.toArray();
+
+ for (const oldReport of oldReports) {
+ const newReport = reports.find(
+ (record) => record.report_date === oldReport.report_date
+ );
+
+ if (!newReport) {
+ oldReport.report_data._deleted = true;
+ oldReport.report_data.updatedAt = new Date().toISOString();
+
+ result.push(oldReport);
+ }
+ }
+
+ return result.map((record) => {
+ const data = updatedAtOverride(record);
+ return data;
+ });
+ };
+
+ const getUserBibleStudies = async (studies: UserBibleStudyType[]) => {
+ const result: UserBibleStudyType[] = [];
+
+ result.push(...studies);
+
+ const oldStudies = await appDb.user_bible_studies.toArray();
+
+ for (const oldStudy of oldStudies) {
+ const newStudy = studies.find(
+ (record) => record.person_uid === oldStudy.person_uid
+ );
+
+ if (!newStudy) {
+ oldStudy.person_data._deleted = true;
+ oldStudy.person_data.updatedAt = new Date().toISOString();
+
+ result.push(oldStudy);
+ }
+ }
+
+ return result.map((record) => {
+ const data = updatedAtOverride(record);
+ return data;
+ });
+ };
+
+ return { getUserReports, getUserBibleStudies };
+};
+
+export default useMinistryReportsImport;
diff --git a/src/features/congregation/settings/import_export/confirm_import/usePersonsImport.tsx b/src/features/congregation/settings/import_export/confirm_import/usePersonsImport.tsx
new file mode 100644
index 0000000000..af449bfc85
--- /dev/null
+++ b/src/features/congregation/settings/import_export/confirm_import/usePersonsImport.tsx
@@ -0,0 +1,35 @@
+import { PersonType } from '@definition/person';
+import { updatedAtOverride } from '@utils/common';
+import appDb from '@db/appDb';
+
+const usePersonsImport = () => {
+ const getPersons = async (persons: PersonType[]) => {
+ const result: PersonType[] = [];
+
+ result.push(...persons);
+
+ const oldPersons = await appDb.persons.toArray();
+
+ for (const oldPerson of oldPersons) {
+ const newPerson = persons.find(
+ (record) => record.person_uid === oldPerson.person_uid
+ );
+
+ if (!newPerson) {
+ oldPerson._deleted = {
+ value: true,
+ updatedAt: new Date().toISOString(),
+ };
+ result.push(oldPerson);
+ }
+ }
+
+ return result.map((record) => {
+ const data = updatedAtOverride(record);
+ return data;
+ });
+ };
+ return { getPersons };
+};
+
+export default usePersonsImport;
diff --git a/src/features/congregation/settings/import_export/confirm_import/useServiceGroups.tsx b/src/features/congregation/settings/import_export/confirm_import/useServiceGroups.tsx
new file mode 100644
index 0000000000..4964efed4c
--- /dev/null
+++ b/src/features/congregation/settings/import_export/confirm_import/useServiceGroups.tsx
@@ -0,0 +1,35 @@
+import { FieldServiceGroupType } from '@definition/field_service_groups';
+import { updatedAtOverride } from '@utils/common';
+import appDb from '@db/appDb';
+
+const useServiceGroups = () => {
+ const getServiceGroups = async (groups: FieldServiceGroupType[]) => {
+ const result: FieldServiceGroupType[] = [];
+
+ result.push(...groups);
+
+ const oldGroups = await appDb.field_service_groups.toArray();
+
+ for (const oldGroup of oldGroups) {
+ const newGroup = groups.find(
+ (record) => record.group_id === oldGroup.group_id
+ );
+
+ if (!newGroup) {
+ oldGroup.group_data._deleted = true;
+ oldGroup.group_data.updatedAt = new Date().toISOString();
+
+ result.push(oldGroup);
+ }
+ }
+
+ return result.map((record) => {
+ const data = updatedAtOverride(record);
+ return data;
+ });
+ };
+
+ return { getServiceGroups };
+};
+
+export default useServiceGroups;
diff --git a/src/features/congregation/settings/import_export/confirm_import/useVisitingSpeakersImport.tsx b/src/features/congregation/settings/import_export/confirm_import/useVisitingSpeakersImport.tsx
new file mode 100644
index 0000000000..85df56e4d9
--- /dev/null
+++ b/src/features/congregation/settings/import_export/confirm_import/useVisitingSpeakersImport.tsx
@@ -0,0 +1,68 @@
+import { VisitingSpeakerType } from '@definition/visiting_speakers';
+import { updatedAtOverride } from '@utils/common';
+import { SpeakersCongregationsType } from '@definition/speakers_congregations';
+import appDb from '@db/appDb';
+
+const useVisitingSpeakersImport = () => {
+ const getVisitingSpeakers = async (speakers: VisitingSpeakerType[]) => {
+ const result: VisitingSpeakerType[] = [];
+
+ result.push(...speakers);
+
+ const oldSpeakers = await appDb.visiting_speakers.toArray();
+
+ for (const oldSpeaker of oldSpeakers) {
+ const newSpeaker = speakers.find(
+ (record) => record.person_uid === oldSpeaker.person_uid
+ );
+
+ if (!newSpeaker) {
+ oldSpeaker._deleted = {
+ value: true,
+ updatedAt: new Date().toISOString(),
+ };
+
+ result.push(oldSpeaker);
+ }
+ }
+
+ return result.map((record) => {
+ const data = updatedAtOverride(record);
+ return data;
+ });
+ };
+
+ const getSpeakersCongregations = async (
+ congregations: SpeakersCongregationsType[]
+ ) => {
+ const result: SpeakersCongregationsType[] = [];
+
+ result.push(...congregations);
+
+ const oldCongregations = await appDb.speakers_congregations.toArray();
+
+ for (const oldCongregation of oldCongregations) {
+ const newCongregation = congregations.find(
+ (record) => record.id === oldCongregation.id
+ );
+
+ if (!newCongregation) {
+ oldCongregation._deleted = {
+ value: true,
+ updatedAt: new Date().toISOString(),
+ };
+
+ result.push(oldCongregation);
+ }
+ }
+
+ return result.map((record) => {
+ const data = updatedAtOverride(record);
+ return data;
+ });
+ };
+
+ return { getVisitingSpeakers, getSpeakersCongregations };
+};
+
+export default useVisitingSpeakersImport;
diff --git a/src/features/congregation/settings/import_export/export/useExport.tsx b/src/features/congregation/settings/import_export/export/useExport.tsx
index 4a31bc576f..45f7ab0751 100644
--- a/src/features/congregation/settings/import_export/export/useExport.tsx
+++ b/src/features/congregation/settings/import_export/export/useExport.tsx
@@ -17,6 +17,8 @@ import { speakersCongregationsActiveState } from '@states/speakers_congregations
import { visitingSpeakersActiveState } from '@states/visiting_speakers';
import { assignmentState } from '@states/assignment';
import { weekTypeState } from '@states/weekType';
+import { userFieldServiceReportsState } from '@states/user_field_service_reports';
+import { userBibleStudiesState } from '@states/user_bible_studies';
const useExport = () => {
const persons = useRecoilValue(personsState);
@@ -34,6 +36,8 @@ const useExport = () => {
const speakersCongregations = useRecoilValue(
speakersCongregationsActiveState
);
+ const userFieldReports = useRecoilValue(userFieldServiceReportsState);
+ const userBibleStudies = useRecoilValue(userBibleStudiesState);
const [isProcessing, setIsProcessing] = useState(false);
@@ -96,6 +100,12 @@ const useExport = () => {
sched: schedules,
sources,
speakers_congregations: speakersCongregations,
+ user_field_service_reports: userFieldReports.filter(
+ (record) => !record.report_data._deleted
+ ),
+ user_bible_studies: userBibleStudies.filter(
+ (record) => !record.person_data._deleted
+ ),
visiting_speakers: visitingSpeakers,
week_type: handleGetWeekTypes(),
},
diff --git a/src/features/congregation/settings/import_export/import/index.tsx b/src/features/congregation/settings/import_export/import/index.tsx
index fc09735ff5..fd69f4247b 100644
--- a/src/features/congregation/settings/import_export/import/index.tsx
+++ b/src/features/congregation/settings/import_export/import/index.tsx
@@ -5,58 +5,60 @@ import { ImportType } from './index.types';
import useImport from './useImport';
import Button from '@components/button';
import Typography from '@components/typography';
+import WaitingLoader from '@components/waiting_loader';
const Import = (props: ImportType) => {
const { t } = useAppTranslation();
- const { getInputProps, getRootProps } = useImport();
+ const { getInputProps, getRootProps, isProcessing } = useImport(props);
return (
-
-
+ {!isProcessing && (
+
+
-
-
-
-
- {t('tr_dragOrClick')}
+
+
+
+
+ {t('tr_dragOrClick')}
+
+
+
+ {t('tr_uploadJsonFile')}
-
-
- {t('tr_uploadJsonFile')}
-
-
-
+
+
+ )}
-
-
-
-
+ {isProcessing && }
+
+
);
};
diff --git a/src/features/congregation/settings/import_export/import/index.types.ts b/src/features/congregation/settings/import_export/import/index.types.ts
index 68e7043985..bf691b77d3 100644
--- a/src/features/congregation/settings/import_export/import/index.types.ts
+++ b/src/features/congregation/settings/import_export/import/index.types.ts
@@ -1,3 +1,4 @@
export type ImportType = {
onClose: VoidFunction;
+ onNext: VoidFunction;
};
diff --git a/src/features/congregation/settings/import_export/import/useImport.tsx b/src/features/congregation/settings/import_export/import/useImport.tsx
index b14dba3b7f..48d032ceba 100644
--- a/src/features/congregation/settings/import_export/import/useImport.tsx
+++ b/src/features/congregation/settings/import_export/import/useImport.tsx
@@ -1,37 +1,73 @@
-import { useCallback } from 'react';
+import { useCallback, useState } from 'react';
+import { useSetRecoilState } from 'recoil';
import { FileWithPath, useDropzone } from 'react-dropzone';
+import { ImportType } from './index.types';
import { displaySnackNotification } from '@services/recoil/app';
import { getMessageByCode } from '@services/i18n/translation';
+import {
+ backupFileContentsState,
+ backupFileNameState,
+ backupFileTypeState,
+} from '@states/app';
-const useImport = () => {
- const onDrop = useCallback(async (acceptedFiles: FileWithPath[]) => {
- try {
- if (acceptedFiles.length !== 1) {
- displaySnackNotification({
- severity: 'error',
- header: getMessageByCode('error_app_generic-title'),
- message: getMessageByCode('error_app_data_invalid-file'),
- });
+const useImport = ({ onNext }: ImportType) => {
+ const setBackupFileName = useSetRecoilState(backupFileNameState);
+ const setBackupFileContents = useSetRecoilState(backupFileContentsState);
+ const setBackupFileType = useSetRecoilState(backupFileTypeState);
- return;
- }
+ const [isProcessing, setIsProcessing] = useState(false);
+
+ const onDrop = useCallback(
+ async (acceptedFiles: FileWithPath[]) => {
+ try {
+ setIsProcessing(true);
+
+ if (acceptedFiles.length !== 1) {
+ throw new Error('error_app_data_invalid-file');
+ }
+
+ const file = acceptedFiles.at(0);
+ const rawData = await file.text();
+ const data = JSON.parse(rawData);
+
+ const keys = Object.keys(data);
+
+ const isCPE =
+ keys.includes('app_settings') && keys.includes('fieldServiceReports');
- const file = acceptedFiles.at(0);
+ const isOrganized =
+ keys.includes('name') && data['name'] === 'Organized';
- const rawData = await file.text();
- const data = JSON.parse(rawData);
+ if (isCPE) {
+ setBackupFileType('CPE');
+ }
- console.log(data);
- } catch (error) {
- console.error(error);
+ if (isOrganized) {
+ setBackupFileType('Organized');
+ }
- displaySnackNotification({
- severity: 'error',
- header: getMessageByCode('error_app_generic-title'),
- message: getMessageByCode(error.message),
- });
- }
- }, []);
+ if (!isCPE && !isOrganized) {
+ throw new Error('error_app_data_invalid-file');
+ }
+
+ setBackupFileName(file.name);
+ setBackupFileContents(JSON.stringify(data));
+
+ onNext();
+ } catch (error) {
+ setIsProcessing(false);
+
+ console.error(error);
+
+ displaySnackNotification({
+ severity: 'error',
+ header: getMessageByCode('error_app_generic-title'),
+ message: getMessageByCode(error.message),
+ });
+ }
+ },
+ [onNext, setBackupFileName, setBackupFileContents, setBackupFileType]
+ );
const { getRootProps, getInputProps } = useDropzone({
onDrop,
@@ -41,7 +77,7 @@ const useImport = () => {
multiple: false,
});
- return { getRootProps, getInputProps };
+ return { getRootProps, getInputProps, isProcessing };
};
export default useImport;
diff --git a/src/features/congregation/settings/import_export/index.tsx b/src/features/congregation/settings/import_export/index.tsx
index ab5aa67253..874683e9c5 100644
--- a/src/features/congregation/settings/import_export/index.tsx
+++ b/src/features/congregation/settings/import_export/index.tsx
@@ -2,6 +2,7 @@ import { Box, Stack } from '@mui/material';
import { useAppTranslation } from '@hooks/index';
import { ImportExportType } from './index.types';
import useImportExport from './useImportExport';
+import ConfirmImport from './confirm_import';
import Dialog from '@components/dialog';
import Typography from '@components/typography';
import Tabs from '@components/tabs';
@@ -9,21 +10,28 @@ import Tabs from '@components/tabs';
const ImportExport = (props: ImportExportType) => {
const { t } = useAppTranslation();
- const { tabs, handleTabChange, value } = useImportExport(props);
+ const { tabs, handleTabChange, value, state, handleOpenImportExport } =
+ useImportExport(props);
return (
);
};
diff --git a/src/features/congregation/settings/import_export/index.types.ts b/src/features/congregation/settings/import_export/index.types.ts
index 62fbcd40da..6c4df9f79d 100644
--- a/src/features/congregation/settings/import_export/index.types.ts
+++ b/src/features/congregation/settings/import_export/index.types.ts
@@ -2,3 +2,5 @@ export type ImportExportType = {
open: boolean;
onClose: VoidFunction;
};
+
+export type DialogType = 'import/export' | 'import/confirm';
diff --git a/src/features/congregation/settings/import_export/useImportExport.tsx b/src/features/congregation/settings/import_export/useImportExport.tsx
index 522014e4a1..1f285cdec1 100644
--- a/src/features/congregation/settings/import_export/useImportExport.tsx
+++ b/src/features/congregation/settings/import_export/useImportExport.tsx
@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react';
import { useAppTranslation } from '@hooks/index';
-import { ImportExportType } from './index.types';
+import { DialogType, ImportExportType } from './index.types';
import Import from './import';
import Export from './export';
@@ -8,6 +8,11 @@ const useImportExport = ({ onClose }: ImportExportType) => {
const { t } = useAppTranslation();
const [value, setValue] = useState(0);
+ const [state, setState] = useState('import/export');
+
+ const handleOpenImportExport = () => setState('import/export');
+
+ const handleOpenConfirm = () => setState('import/confirm');
const tabs = useMemo(() => {
return [
@@ -17,14 +22,14 @@ const useImportExport = ({ onClose }: ImportExportType) => {
},
{
label: t('tr_import'),
- Component: ,
+ Component: ,
},
];
}, [t, onClose]);
const handleTabChange = (tab: number) => setValue(tab);
- return { tabs, value, handleTabChange };
+ return { tabs, value, handleTabChange, state, handleOpenImportExport };
};
export default useImportExport;
diff --git a/src/features/reports/meeting_attendance/shared_styles/index.tsx b/src/features/reports/meeting_attendance/shared_styles/index.tsx
index f1c6404695..cc9f961ace 100644
--- a/src/features/reports/meeting_attendance/shared_styles/index.tsx
+++ b/src/features/reports/meeting_attendance/shared_styles/index.tsx
@@ -1,5 +1,4 @@
-import { styled } from '@mui/system';
-import { Box } from '@mui/material';
+import { Box, styled } from '@mui/material';
export const CardContainer = styled(Box)({
backgroundColor: 'var(--white)',
@@ -9,4 +8,4 @@ export const CardContainer = styled(Box)({
display: 'flex',
gap: '24px',
flexDirection: 'column',
-});
+}) as unknown as typeof Box;
diff --git a/src/layouts/root_layout/index.tsx b/src/layouts/root_layout/index.tsx
index 3321b5c304..19f748444a 100644
--- a/src/layouts/root_layout/index.tsx
+++ b/src/layouts/root_layout/index.tsx
@@ -1,6 +1,6 @@
import { Suspense } from 'react';
import { Outlet } from 'react-router-dom';
-import { Box, Container, Toolbar } from '@mui/material';
+import { Box, Container, Link, Toolbar } from '@mui/material';
import { IconClose } from '@components/icons';
import { AppModalWrapper } from '@wrapper/index';
import { Startup } from '@features/app_start';
@@ -26,6 +26,9 @@ import Support from '@features/support';
import UnsupportedBrowser from '@features/app_start/shared/unsupported_browser';
import WaitingLoader from '@components/waiting_loader';
import WhatsNew from '@features/whats_new';
+import Typography from '@components/typography';
+import NetlifyLight from '@assets/img/netlify_lightmode.png';
+import NetlifyDark from '@assets/img/netlify_darkmode.png';
const RootLayout = ({ updatePwa }: { updatePwa: VoidFunction }) => {
const { isSupported } = useGlobal();
@@ -41,6 +44,7 @@ const RootLayout = ({ updatePwa }: { updatePwa: VoidFunction }) => {
isDemoNoticeOpen,
migrationOpen,
initialSetupOpen,
+ isDarkTheme,
} = useRootLayout();
return (
@@ -68,6 +72,7 @@ const RootLayout = ({ updatePwa }: { updatePwa: VoidFunction }) => {
paddingLeft: { mobile: '16px', tablet: '24px', desktop: '32px' },
paddingRight: { mobile: '16px', tablet: '24px', desktop: '32px' },
marginTop: '24px',
+ marginBottom: import.meta.env.VITE_APP_ON_NETLIFY ? '70px' : 'unset',
}}
>
{!isSupported && }
@@ -112,6 +117,42 @@ const RootLayout = ({ updatePwa }: { updatePwa: VoidFunction }) => {
>
)}
+
+ {import.meta.env.VITE_APP_ON_NETLIFY && (
+
+
+
+
+ This site is powered by Netlify
+
+
+
+ )}
);
};
diff --git a/src/layouts/root_layout/useRootLayout.tsx b/src/layouts/root_layout/useRootLayout.tsx
index 1e17ed72f4..e0f40f8a80 100644
--- a/src/layouts/root_layout/useRootLayout.tsx
+++ b/src/layouts/root_layout/useRootLayout.tsx
@@ -7,6 +7,7 @@ import {
isAboutOpenState,
isAppLoadState,
isContactOpenState,
+ isDarkThemeState,
isOnlineState,
isSupportOpenState,
restoreDbOpenState,
@@ -38,6 +39,7 @@ const useRootLayout = () => {
const isOnline = useRecoilValue(isOnlineState);
const isDemoNoticeOpen = useRecoilValue(demoNoticeOpenState);
const settings = useRecoilValue(settingsState);
+ const isDarkTheme = useRecoilValue(isDarkThemeState);
const isDashboard = location.pathname === '/';
@@ -75,6 +77,7 @@ const useRootLayout = () => {
isDemoNoticeOpen,
migrationOpen,
initialSetupOpen,
+ isDarkTheme,
};
};
diff --git a/src/locales/en/congregation.json b/src/locales/en/congregation.json
index 01c0d75a8f..211546d6a7 100644
--- a/src/locales/en/congregation.json
+++ b/src/locales/en/congregation.json
@@ -444,10 +444,14 @@
"tr_importExportTitle": "Import or export congregation data",
"tr_exportDesc": "Export your congregation data to create a local backup or transfer it to another app.",
"tr_importDesc": "Upload a file to import. Consider exporting your current data first to keep a backup for potential restoration later.",
- "tr_ministryReports": "Ministry reports",
+ "tr_ministryReports": "My ministry reports",
"tr_midweekMeetingHistory": "Midweek meeting history",
"tr_weekendMeetingHistory": "Weekend meeting history",
"tr_visitingSpeakers": "Visiting speakers",
"tr_dragOrClick": "Drag or click to upload",
- "tr_uploadJsonFile": "Upload a file in JSON format"
+ "tr_uploadJsonFile": "Upload a file in JSON format",
+ "tr_importDataConfirm": "Confirm importing new data",
+ "tr_importDataConfirmDesc": "Select the data you want to import to replace your records in Organized",
+ "tr_importDataCompleted": "Import completed",
+ "tr_importDataCompletedDesc": "Your data has been imported successfully. Organized app will now reload."
}
diff --git a/src/locales/en/ministry.json b/src/locales/en/ministry.json
index 4253a789ac..cffd6d3996 100644
--- a/src/locales/en/ministry.json
+++ b/src/locales/en/ministry.json
@@ -131,5 +131,6 @@
"tr_fieldReportEdit": "Edit report",
"tr_undoBranchReportSubmissionDesc": "Are you sure to undo the submission of this report? Make any necessary adjustments and submit the updated report as soon as possible.",
"tr_reportExcluded": "The first month of report value does not include this report",
- "tr_reportReadOnly": "This publisher report can no longer be edited since the branch report was already submitted"
+ "tr_reportReadOnly": "This publisher report can no longer be edited since the branch report was already submitted",
+ "tr_congregationReports": "Congregation reports"
}
diff --git a/src/migration/action_steps/action_migrate/useSpeakersMigrate.jsx b/src/migration/action_steps/action_migrate/useSpeakersMigrate.jsx
index ea93d4fa02..5e7fa84b39 100644
--- a/src/migration/action_steps/action_migrate/useSpeakersMigrate.jsx
+++ b/src/migration/action_steps/action_migrate/useSpeakersMigrate.jsx
@@ -13,385 +13,6 @@ const useSpeakersMigrate = () => {
(record) => !record.is_deleted && record.is_local
);
- oldCongregations = [
- {
- cong_name: 'Tsiadana',
- cong_number: -1,
- cong_speakers: [
- {
- person_uid: '251d87f0-2872-4a9d-aa8c-c00efad6c318',
- person_name: 'Markland Kaniel',
- person_displayName: 'M. Kaniel',
- is_elder: true,
- is_ms: false,
- talks: [25, 105],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-08-25T10:40:32.882Z',
- field: 'talks',
- value: [25, 105],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'Maibahoaka',
- cong_number: 2,
- cong_speakers: [
- {
- person_uid: 'c2215c19-8dbe-4997-93d5-6bb3a0739229',
- person_name: 'Andriantsoa Marco',
- person_displayName: 'Andriantsoa Marco',
- is_elder: true,
- is_ms: false,
- talks: [182],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-03-26T17:56:50.119Z',
- field: 'person_displayName',
- value: 'Andriantsoa Marco',
- },
- {
- date: '2024-03-26T17:57:12.038Z',
- field: 'talks',
- value: [182],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'Ankadindravola',
- cong_number: 3,
- cong_speakers: [
- {
- person_uid: '6feff238-ef52-4249-8ef4-6bffcad83f28',
- person_name: 'Helly Fabrice',
- person_displayName: 'Helly Fabrice',
- is_elder: true,
- is_ms: false,
- talks: [164],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-03-26T18:23:09.692Z',
- field: 'talks',
- value: [164],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'Anjomahakely',
- cong_number: 4,
- cong_speakers: [
- {
- person_uid: 'f3a42952-0a88-4231-b112-1369570a6c3f',
- person_name: 'Andriamasy ',
- person_displayName: 'Titosy ',
- talks: [],
- is_deleted: false,
- is_unavailable: false,
- is_elder: true,
- is_ms: false,
- email: '',
- phone: '',
- changes: [],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'ivandry',
- cong_number: 5,
- cong_speakers: [
- {
- person_uid: '30c14bfc-f29c-4983-a78e-d254a0f5b460',
- person_name: 'Pierre Petignat',
- person_displayName: 'P. Petignat',
- is_elder: true,
- is_ms: false,
- talks: [93],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-03-26T17:55:20.970Z',
- field: 'talks',
- value: [93],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'Nigeria',
- cong_number: 6,
- cong_speakers: [
- {
- person_uid: 'a19542c5-f051-43c2-b5e2-285bba030ffb',
- person_name: 'Ohwofosa Mudiaga',
- person_displayName: 'O. Mudiaga',
- is_elder: true,
- is_ms: false,
- talks: [41],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-07-12T20:59:51.815Z',
- field: 'talks',
- value: [41],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'Tana 1',
- cong_number: 7,
- cong_speakers: [
- {
- person_uid: 'b0a9bac8-9e02-4622-b55a-eab734823ee5',
- person_name: 'Moss Jordan',
- person_displayName: 'M. Jordan',
- is_elder: true,
- is_ms: false,
- talks: [189],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-07-12T21:02:14.311Z',
- field: 'talks',
- value: [189],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'antsirabe',
- cong_number: 8,
- cong_speakers: [
- {
- person_uid: 'ebc59029-9a8e-461e-8ba1-626ed376f3cb',
- person_name: 'Rafanambinana Adelphe',
- person_displayName: 'R. Adelphe',
- is_elder: true,
- is_ms: false,
- talks: [18],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-07-12T21:09:56.681Z',
- field: 'talks',
- value: [18],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'Tanambao Avartra',
- cong_number: 9,
- cong_speakers: [
- {
- person_uid: '58c3ad0d-ffb2-430e-be8e-cc75f0d3aa04',
- person_name: 'Acosta Elison',
- person_displayName: 'A. Elison',
- is_elder: true,
- is_ms: false,
- talks: [143],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-07-12T21:15:23.249Z',
- field: 'talks',
- value: [143],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'ZAF',
- cong_number: 10,
- cong_speakers: [
- {
- person_uid: '2cbc0397-1838-4faf-8690-c42467589d9b',
- person_name: 'Naude Hein',
- person_displayName: 'N. Hein',
- is_elder: true,
- is_ms: false,
- talks: [70],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-07-12T21:24:34.986Z',
- field: 'talks',
- value: [70],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'South AF',
- cong_number: 12,
- cong_speakers: [
- {
- person_uid: 'f4f12113-d315-4e0b-bb16-63182122c187',
- person_name: 'Bent Symon',
- person_displayName: 'B. Symon',
- is_elder: true,
- is_ms: false,
- talks: [15],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-08-22T19:07:52.340Z',
- field: 'talks',
- value: [15],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'ZAF B',
- cong_number: 13,
- cong_speakers: [
- {
- person_uid: '9a6e7aa1-b89b-4bde-8168-c25fe6d23cd8',
- person_name: 'N Hein',
- person_displayName: 'N. Hein',
- is_elder: true,
- is_ms: false,
- talks: [182],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-08-25T10:20:48.766Z',
- field: 'talks',
- value: [182],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- {
- cong_name: 'ZAF 3',
- cong_number: 14,
- cong_speakers: [
- {
- person_uid: 'c54c8641-1310-4d7f-9273-281d1d0f5e13',
- person_name: 'Irlan Brent',
- person_displayName: 'I. Brent',
- is_elder: true,
- is_ms: false,
- talks: [47],
- is_unavailable: false,
- is_deleted: false,
- email: '',
- phone: '',
- changes: [
- {
- date: '2024-09-07T07:52:24.970Z',
- field: 'talks',
- value: [47],
- },
- ],
- },
- ],
- is_local: true,
- is_deleted: false,
- request_status: 'approved',
- changes: [],
- },
- ];
-
const congregations = [];
const speakers = [];
diff --git a/src/pages/congregation/settings/index.tsx b/src/pages/congregation/settings/index.tsx
index d47d38f74e..7a7399986f 100644
--- a/src/pages/congregation/settings/index.tsx
+++ b/src/pages/congregation/settings/index.tsx
@@ -8,17 +8,27 @@ import MeetingForms from '@features/congregation/settings/meeting_forms';
import MinistrySettings from '@features/congregation/settings/ministry_settings';
import PageTitle from '@components/page_title';
import ImportExport from '@features/congregation/settings/import_export';
+import Button from '@components/button';
+import { IconImportExport } from '@components/icons';
const CongregationSettings = () => {
const { t } = useAppTranslation();
const { desktopUp } = useBreakpoints();
- const { handleCloseExchange, isDataExchangeOpen } = useCongregationSettings();
+ const { handleCloseExchange, isDataExchangeOpen, handleOpenExchange } =
+ useCongregationSettings();
return (
-
+ } onClick={handleOpenExchange}>
+ {t('tr_importExport')}
+
+ }
+ />
{isDataExchangeOpen && (
diff --git a/src/pages/meeting_materials/public_talks_list/index.tsx b/src/pages/meeting_materials/public_talks_list/index.tsx
index 53d7659cde..e6ad02c90e 100644
--- a/src/pages/meeting_materials/public_talks_list/index.tsx
+++ b/src/pages/meeting_materials/public_talks_list/index.tsx
@@ -1,6 +1,6 @@
import { Box } from '@mui/material';
-import { IconExport, IconListView, IconSpreadsheet } from '@components/icons';
-import { useAppTranslation, useBreakpoints } from '@hooks/index';
+import { IconListView, IconSpreadsheet } from '@components/icons';
+import { useAppTranslation } from '@hooks/index';
import usePublicTalksList from './usePublicTalksList';
import Button from '@components/button';
import PageTitle from '@components/page_title';
@@ -8,7 +8,6 @@ import PublicTalks from '@features/meeting_materials/public_talks';
const PublicTalksList = () => {
const { t } = useAppTranslation();
- const { laptopUp } = useBreakpoints();
const { currentView, handleToggleView } = usePublicTalksList();
@@ -17,43 +16,27 @@ const PublicTalksList = () => {
-
- ) : (
-
- )
- }
- onClick={handleToggleView}
- >
- {currentView === 'list' ? t('tr_tableView') : t('tr_listView')}
-
- {currentView === 'table' && laptopUp && (
-
- }
- >
- {t('tr_exportS99')}
-
- )}
- >
+
+ ) : (
+
+ )
+ }
+ onClick={handleToggleView}
+ >
+ {currentView === 'list' ? t('tr_tableView') : t('tr_listView')}
+
}
/>
diff --git a/src/services/dexie/schema.ts b/src/services/dexie/schema.ts
index feb08cd1f8..db80f7b17c 100644
--- a/src/services/dexie/schema.ts
+++ b/src/services/dexie/schema.ts
@@ -377,6 +377,7 @@ export const speakersCongregationSchema: SpeakersCongregationsType = {
};
export const meetingAttendanceSchema: MeetingAttendanceType = {
+ _deleted: { value: false, updatedAt: '' },
month_date: '',
week_1: {
midweek: [
diff --git a/src/states/app.ts b/src/states/app.ts
index ea6701564c..312e817278 100644
--- a/src/states/app.ts
+++ b/src/states/app.ts
@@ -4,7 +4,7 @@ import {
getTranslation,
} from '@services/i18n/translation';
import { localStorageGetItem } from '@utils/common';
-import { SnackBarSeverityType } from '@definition/app';
+import { BackupFileType, SnackBarSeverityType } from '@definition/app';
import { ReactElement } from 'react';
import { LANGUAGE_LIST } from '@constants/index';
import { CongregationUserType } from '@definition/api';
@@ -530,3 +530,18 @@ export const congregationCreateStepState = atom({
key: 'congregationCreateStep',
default: 0,
});
+
+export const backupFileTypeState = atom({
+ key: 'backupFileType',
+ default: '',
+});
+
+export const backupFileNameState = atom({
+ key: 'backupFileName',
+ default: '',
+});
+
+export const backupFileContentsState = atom({
+ key: 'backupFileContents',
+ default: '',
+});
diff --git a/src/states/meeting_attendance.ts b/src/states/meeting_attendance.ts
index ac071a8ff3..92e122e131 100644
--- a/src/states/meeting_attendance.ts
+++ b/src/states/meeting_attendance.ts
@@ -2,10 +2,23 @@
This file holds the source of the truth from the table "meetingAttendance".
*/
-import { atom } from 'recoil';
+import { atom, selector } from 'recoil';
import { MeetingAttendanceType } from '@definition/meeting_attendance';
-export const meetingAttendanceState = atom({
- key: 'meetingAttendance',
+export const meetingAttendanceDbState = atom({
+ key: 'meetingAttendanceDb',
default: [],
});
+
+export const meetingAttendanceState = selector({
+ key: 'meetingAttendance',
+ get: ({ get }) => {
+ const attendance = get(meetingAttendanceDbState);
+
+ const results = attendance.filter(
+ (record) => !record._deleted || !record._deleted?.value
+ );
+
+ return results;
+ },
+});
diff --git a/src/utils/common.ts b/src/utils/common.ts
index 1955e0a469..aa46a71c38 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -210,3 +210,19 @@ export const debounce = void>(
export const getRandomNumber = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
+
+export const updatedAtOverride = (object: T): T => {
+ const objectKeys = Object.keys(object);
+
+ for (const key of objectKeys) {
+ if (key === 'updatedAt') {
+ object[key] = new Date().toISOString();
+ }
+
+ if (object[key] && typeof object[key] === 'object') {
+ updatedAtOverride(object[key]);
+ }
+ }
+
+ return object;
+};
diff --git a/src/wrapper/database_indexeddb/useIndexedDb.tsx b/src/wrapper/database_indexeddb/useIndexedDb.tsx
index 495b95fedc..de3f01be01 100644
--- a/src/wrapper/database_indexeddb/useIndexedDb.tsx
+++ b/src/wrapper/database_indexeddb/useIndexedDb.tsx
@@ -11,7 +11,7 @@ import { sourcesState } from '@states/sources';
import { schedulesState } from '@states/schedules';
import { visitingSpeakersState } from '@states/visiting_speakers';
import { speakersCongregationsState } from '@states/speakers_congregations';
-import { meetingAttendanceState } from '@states/meeting_attendance';
+import { meetingAttendanceDbState } from '@states/meeting_attendance';
import { userFieldServiceReportsState } from '@states/user_field_service_reports';
import { userBibleStudiesState } from '@states/user_bible_studies';
import { fieldServiceReportsState } from '@states/field_service_reports';
@@ -66,7 +66,7 @@ const useIndexedDb = () => {
const setSpeakersCongregations = useSetRecoilState(
speakersCongregationsState
);
- const setMeetingAttendance = useSetRecoilState(meetingAttendanceState);
+ const setMeetingAttendance = useSetRecoilState(meetingAttendanceDbState);
const setUserFieldServiceReports = useSetRecoilState(
userFieldServiceReportsState
);