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')} + + } + /> + + + + + + + + + + + ); +}; + +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 ( - - {t('tr_importExportTitle')} + {state === 'import/export' && ( + + {t('tr_importExportTitle')} - - {value === 0 ? t('tr_exportDesc') : t('tr_importDesc')} - + + {value === 0 ? t('tr_exportDesc') : t('tr_importDesc')} + - - - - + + + + + )} + + {state === 'import/confirm' && ( + + )} ); }; 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 = () => { - - {currentView === 'table' && laptopUp && ( - - )} - + } /> 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 );