From 7c09b12d4e8e6a288d68dc676ed220a0477436c2 Mon Sep 17 00:00:00 2001 From: "rhahao.vj" <26148770+rhahao@users.noreply.github.com> Date: Wed, 2 Oct 2024 08:57:17 +0000 Subject: [PATCH 01/16] feat(app): add protected routes --- src/App.tsx | 168 ++++++++++++++---- src/components/route_protected/index.tsx | 7 + src/definition/app.ts | 8 +- .../profile_settings/useProfileSettings.tsx | 63 ++++--- .../pioneer_stats/usePioneerStats.tsx | 6 +- src/features/my_profile/security/index.tsx | 2 +- .../my_profile/security/useSecurity.tsx | 6 +- src/hooks/useCurrentUser.tsx | 83 ++++++++- src/hooks/useUserAutoLogin.tsx | 2 + tsconfig.app.json | 47 +++++ tsconfig.json | 39 +--- tsconfig.node.json | 20 +++ 12 files changed, 344 insertions(+), 107 deletions(-) create mode 100644 src/components/route_protected/index.tsx create mode 100644 tsconfig.app.json create mode 100644 tsconfig.node.json diff --git a/src/App.tsx b/src/App.tsx index 5e5c7e50b7..63b99a535a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,12 @@ import { lazy } from 'react'; import { RouterProvider, createHashRouter } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ErrorBoundary } from '@components/index'; import { RootLayout } from '@layouts/index'; +import { useCurrentUser } from './hooks'; +import { congAccountConnectedState } from '@states/app'; +import RouteProtected from '@components/route_protected'; // lazy loading const Dashboard = lazy(() => import('@pages/dashboard')); @@ -42,9 +46,6 @@ const UserDetails = lazy( ); const WeeklySchedules = lazy(() => import('@pages/meetings/schedules')); const CongregationSettings = lazy(() => import('@pages/congregation/settings')); -const UpcomingEvents = lazy( - () => import('@pages/congregation/upcoming_events') -); const Applications = lazy(() => import('@pages/persons/applications')); const ApplicationDetails = lazy( () => import('@pages/persons/application_details') @@ -53,6 +54,19 @@ const ApplicationDetails = lazy( const queryClient = new QueryClient(); const App = ({ updatePwa }: { updatePwa: VoidFunction }) => { + const { + isAdmin, + isPublisher, + isServiceCommittee, + isElder, + isPersonEditor, + isAttendanceEditor, + isAppointed, + isMidweekEditor, + isWeekendEditor, + } = useCurrentUser(); + const isConnected = useRecoilValue(congAccountConnectedState); + const router = createHashRouter([ { errorElement: , @@ -60,55 +74,137 @@ const App = ({ updatePwa }: { updatePwa: VoidFunction }) => { { element: , children: [ - { path: '/', element: }, - { path: '/persons', element: }, + // public routes + { index: true, element: }, + { path: '/user-profile', element: }, + { path: '/weekly-schedules', element: }, + + // publisher routes { - path: '/reports/branch-office', - element: , + element: , + children: [ + { path: '/ministry-report', element: }, + { path: '/service-year', element: }, + { + path: '/field-service-groups', + element: , + }, + + // only if connected + { + element: , + children: [ + { + path: '/auxiliary-pioneer-application', + element: , + }, + ], + }, + ], }, + + // appointed routes { - path: '/reports/meeting-attendance', - element: , + element: , + children: [ + { path: '/public-talks-list', element: }, + ], }, + + // elder routes { - path: '/reports/field-service', - element: , + element: , + children: [ + { path: '/persons', element: }, + { path: '/persons/:id', element: }, + { + path: '/congregation-settings', + element: , + }, + { path: '/publisher-records', element: }, + { + path: '/publisher-records/:id', + element: , + }, + ], }, - { path: '/persons/:id', element: }, - { path: '/persons/new', element: }, - { path: '/pioneer-applications', element: }, + + // person editor routes { - path: '/pioneer-applications/:id', - element: , + element: , + children: [{ path: '/persons/new', element: }], }, - { path: '/user-profile', element: }, - { path: '/public-talks-list', element: }, - { path: '/ministry-report', element: }, - { path: '/upcoming-events', element: }, + + // attendance editor routes { - path: '/auxiliary-pioneer-application', - element: , + element: , + children: [ + { + path: '/reports/meeting-attendance', + element: , + }, + ], }, - { path: '/speakers-catalog', element: }, - { path: '/midweek-meeting', element: }, - { path: '/weekend-meeting', element: }, - { path: '/field-service-groups', element: }, - { path: '/publisher-records', element: }, + + // midweek editor routes { - path: '/publisher-records/:id', - element: , + element: , + children: [ + { path: '/midweek-meeting', element: }, + ], }, - { path: '/manage-access', element: }, + + // weekend editor routes { - path: '/manage-access/:id', - element: , + element: , + children: [ + { path: '/speakers-catalog', element: }, + { path: '/weekend-meeting', element: }, + ], }, - { path: '/service-year', element: }, - { path: '/weekly-schedules', element: }, + + // service committee routes { - path: '/congregation-settings', - element: , + element: ( + + ), + children: [ + { path: '/pioneer-applications', element: }, + { + path: '/pioneer-applications/:id', + element: , + }, + ], }, + + // congregation admin routes + { + element: , + children: [ + { + path: '/reports/field-service', + element: , + }, + { + path: '/reports/branch-office', + element: , + }, + + // only if connected + { + element: , + children: [ + { path: '/manage-access', element: }, + { + path: '/manage-access/:id', + element: , + }, + ], + }, + ], + }, + + // fallback to dashboard for all invalid routes { path: '*', element: }, ], }, diff --git a/src/components/route_protected/index.tsx b/src/components/route_protected/index.tsx new file mode 100644 index 0000000000..ebd9267afc --- /dev/null +++ b/src/components/route_protected/index.tsx @@ -0,0 +1,7 @@ +import { Navigate, Outlet } from 'react-router-dom'; + +const RouteProtected = ({ allowed }: { allowed: boolean }) => { + return allowed ? : ; +}; + +export default RouteProtected; diff --git a/src/definition/app.ts b/src/definition/app.ts index 9260ace996..73abbe14d4 100644 --- a/src/definition/app.ts +++ b/src/definition/app.ts @@ -41,15 +41,11 @@ export type AppRoleType = | 'coordinator' | 'secretary' | 'service_overseer' - | 'field_service_group_overseer' | 'midweek_schedule' | 'weekend_schedule' | 'public_talk_schedule' | 'attendance_tracking' | 'publisher' | 'view_schedules' - | 'auxiliary_pioneer' - | 'regular_pionner' - | 'special_pioneer' - | 'missionary' - | 'elder'; + | 'elder' + | 'ms'; diff --git a/src/features/congregation/app_access/user_details/profile_settings/useProfileSettings.tsx b/src/features/congregation/app_access/user_details/profile_settings/useProfileSettings.tsx index d1acaa33e5..ff12dff750 100644 --- a/src/features/congregation/app_access/user_details/profile_settings/useProfileSettings.tsx +++ b/src/features/congregation/app_access/user_details/profile_settings/useProfileSettings.tsx @@ -7,16 +7,19 @@ import { fullnameOptionState } from '@states/settings'; import { displaySnackNotification } from '@services/recoil/app'; import { useAppTranslation } from '@hooks/index'; import { getMessageByCode } from '@services/i18n/translation'; -import { - personIsBaptizedPublisher, - personIsMidweekStudent, - personIsUnbaptizedPublisher, -} from '@services/app/persons'; import useUserDetails from '../useUserDetails'; +import usePerson from '@features/persons/hooks/usePerson'; const useProfileSettings = () => { const { t } = useAppTranslation(); + const { + personIsBaptizedPublisher, + personIsMidweekStudent, + personIsUnbaptizedPublisher, + personIsPrivilegeActive, + } = usePerson(); + const { handleSaveDetails, user } = useUserDetails(); const personsActive = useRecoilValue(personsActiveState); @@ -51,27 +54,39 @@ const useProfileSettings = () => { const newUser = structuredClone(user); newUser.profile.user_local_uid = value.person_uid; - if ( - newUser.profile.cong_role.includes('admin') && - newUser.profile.cong_role.length === 1 - ) { - const person = personsActive.find( - (record) => record.person_uid === value.person_uid - ); - - if (personIsMidweekStudent(person)) { - newUser.profile.cong_role.push('view_schedules'); - } - - const isPublisher = - personIsBaptizedPublisher(person) || - personIsUnbaptizedPublisher(person); - - if (isPublisher) { - newUser.profile.cong_role.push('publisher', 'view_schedules'); - } + const userRole = newUser.profile.cong_role; + + const person = personsActive.find( + (record) => record.person_uid === value.person_uid + ); + + const isMidweekStudent = personIsMidweekStudent(person); + + const isPublisher = + personIsBaptizedPublisher(person) || + personIsUnbaptizedPublisher(person); + + const isElder = personIsPrivilegeActive(person, 'elder'); + const isMS = personIsPrivilegeActive(person, 'ms'); + + if (isMidweekStudent || isPublisher) { + userRole.push('view_schedules'); } + if (isPublisher) { + userRole.push('publisher'); + } + + if (isElder) { + userRole.push('elder'); + } + + if (isMS) { + userRole.push('ms'); + } + + newUser.profile.cong_role = Array.from(new Set(userRole)); + await handleSaveDetails(newUser); } catch (error) { console.error(error); diff --git a/src/features/ministry/service_year/yearly_stats/pioneer_stats/usePioneerStats.tsx b/src/features/ministry/service_year/yearly_stats/pioneer_stats/usePioneerStats.tsx index 2a925f95ef..d156702289 100644 --- a/src/features/ministry/service_year/yearly_stats/pioneer_stats/usePioneerStats.tsx +++ b/src/features/ministry/service_year/yearly_stats/pioneer_stats/usePioneerStats.tsx @@ -32,13 +32,13 @@ const usePioneerStats = (year: string) => { }, [year]); const goal = useMemo(() => { - const enrollment = person.person_data.enrollments.find((record) => { + const enrollment = person!.person_data.enrollments.find((record) => { if (record._deleted) return false; if (record.enrollment === 'FR') { let startDate = formatDate(new Date(record.start_date), 'yyyy/MM'); - let endDate: string; + let endDate = ''; if (record.end_date === null) { startDate = start_month; @@ -51,6 +51,8 @@ const usePioneerStats = (year: string) => { return startDate >= start_month && endDate <= end_month; } + + return false; }); if (!enrollment) return 0; diff --git a/src/features/my_profile/security/index.tsx b/src/features/my_profile/security/index.tsx index 72f2545fba..8d909c7c7d 100644 --- a/src/features/my_profile/security/index.tsx +++ b/src/features/my_profile/security/index.tsx @@ -35,7 +35,7 @@ const Security = () => { - + {t('tr_2FA')} diff --git a/src/features/my_profile/security/useSecurity.tsx b/src/features/my_profile/security/useSecurity.tsx index 1202352546..dd5e31b82e 100644 --- a/src/features/my_profile/security/useSecurity.tsx +++ b/src/features/my_profile/security/useSecurity.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { ChangeEvent, useCallback, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { isMFAEnabledState } from '@states/app'; @@ -8,7 +8,9 @@ const useSecurity = () => { const [isOpenMFAEnable, setIsOpenMFAEnable] = useState(false); const [isOpenMFADisable, setIsOpenMFADisable] = useState(false); - const handleToggleMFA = () => { + const handleToggleMFA = (e: ChangeEvent, _: boolean) => { + e.preventDefault(); + if (!isMFAEnabled) { setIsOpenMFAEnable(true); } diff --git a/src/hooks/useCurrentUser.tsx b/src/hooks/useCurrentUser.tsx index fa6d7ac0f5..ea178728b6 100644 --- a/src/hooks/useCurrentUser.tsx +++ b/src/hooks/useCurrentUser.tsx @@ -7,7 +7,11 @@ import { formatDate } from '@services/dateformat'; import usePerson from '@features/persons/hooks/usePerson'; const useCurrentUser = () => { - const { personIsEnrollmentActive, personIsBaptizedPublisher } = usePerson(); + const { + personIsEnrollmentActive, + personIsBaptizedPublisher, + personIsPublisher, + } = usePerson(); const userUID = useRecoilValue(userLocalUIDState); const persons = useRecoilValue(personsState); @@ -67,7 +71,82 @@ const useCurrentUser = () => { settings, ]); - return { person, first_report, enable_AP_application }; + const isPublisher = useMemo(() => { + if (!person) return false; + + return personIsPublisher(person); + }, [person, personIsPublisher]); + + const userRole = useMemo(() => { + return settings.user_settings.cong_role; + }, [settings]); + + const isAdmin = useMemo(() => { + return userRole.some( + (role) => + role === 'admin' || role === 'coordinator' || role === 'secretary' + ); + }, [userRole]); + + const isElder = useMemo(() => { + if (isAdmin) return true; + + return userRole.includes('elder'); + }, [isAdmin, userRole]); + + const isServiceCommittee = useMemo(() => { + if (isAdmin) return true; + + // only check for service overseer since coordinator and secretary are already admin + return userRole.includes('service_overseer'); + }, [isAdmin, userRole]); + + const isPersonEditor = useMemo(() => { + if (isAdmin) return true; + + return userRole.some( + (role) => role === 'midweek_schedule' || role === 'weekend_schedule' + ); + }, [isAdmin, userRole]); + + const isAttendanceEditor = useMemo(() => { + if (isAdmin) return true; + + return userRole.includes('attendance_tracking'); + }, [isAdmin, userRole]); + + const isAppointed = useMemo(() => { + if (isAdmin) return true; + + return userRole.some((role) => role === 'elder' || role === 'ms'); + }, [isAdmin, userRole]); + + const isMidweekEditor = useMemo(() => { + if (isAdmin) return true; + + return userRole.includes('midweek_schedule'); + }, [isAdmin, userRole]); + + const isWeekendEditor = useMemo(() => { + if (isAdmin) return true; + + return userRole.includes('weekend_schedule'); + }, [isAdmin, userRole]); + + return { + person, + first_report, + enable_AP_application, + isAdmin, + isPublisher, + isServiceCommittee, + isElder, + isPersonEditor, + isAttendanceEditor, + isAppointed, + isMidweekEditor, + isWeekendEditor, + }; }; export default useCurrentUser; diff --git a/src/hooks/useUserAutoLogin.tsx b/src/hooks/useUserAutoLogin.tsx index af6a66e897..d7c85edc8f 100644 --- a/src/hooks/useUserAutoLogin.tsx +++ b/src/hooks/useUserAutoLogin.tsx @@ -62,6 +62,8 @@ const useUserAutoLogin = () => { return; } + if (!data) return; + if (data.status === 403) { await userSignOut(); return; diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000000..1a459fce14 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,47 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + "baseUrl": "./src", + "paths": { + "react": ["./node_modules/@types/react"], + "@mui/styled-engine": ["./node_modules/@mui/styled-engine-sc"], + "@assets/*": ["assets/*"], + "@components/*": ["components/*"], + "@icons/*": ["components/icons/*"], + "@constants/*": ["constants/*"], + "@features/*": ["features/*"], + "@hooks/*": ["hooks/*"], + "@layouts/*": ["layouts/*"], + "@pages/*": ["pages/*"], + "@routes/*": ["routes/*"], + "@services/*": ["services/*"], + "@states/*": ["states/*"], + "@utils/*": ["utils/*"], + "@wrapper/*": ["wrapper/*"], + "@locales/*": ["shared/*"], + "@definition/*": ["definition/*"], + "@global/*": ["global/*"], + "@db/*": ["indexedDb/*"], + "@talks/*": ["public_talks/*"], + "@views/*": ["views/*"] + } + }, + "include": ["src/**/*", "types/**/*"] +} diff --git a/tsconfig.json b/tsconfig.json index 93dd80f829..1ffef600d9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,36 +1,7 @@ { - "compilerOptions": { - "jsx": "react", - "skipLibCheck": true, - "baseUrl": "./src", - "paths": { - "@mui/styled-engine": ["./node_modules/@mui/styled-engine-sc"], - "@assets/*": ["assets/*"], - "@components/*": ["components/*"], - "@icons/*": ["components/icons/*"], - "@constants/*": ["constants/*"], - "@features/*": ["features/*"], - "@hooks/*": ["hooks/*"], - "@layouts/*": ["layouts/*"], - "@pages/*": ["pages/*"], - "@routes/*": ["routes/*"], - "@services/*": ["services/*"], - "@states/*": ["states/*"], - "@utils/*": ["utils/*"], - "@wrapper/*": ["wrapper/*"], - "@locales/*": ["shared/*"], - "@definition/*": ["definition/*"], - "@global/*": ["global/*"], - "@db/*": ["indexedDb/*"], - "@talks/*": ["public_talks/*"], - "@views/*": ["views/*"] - }, - "esModuleInterop": true, - "moduleResolution": "Node", - "module": "ESNext", - "types": ["@svgx/vite-plugin-react", "node", "vite/client"], - "lib": ["ESNext", "DOM"], - "target": "ESNext" - }, - "include": ["src/**/*", "types/**/*"] + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] } diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000000..b3fc13e547 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} From 009f397500661dbf01215cc21815e6c8c696ba0f Mon Sep 17 00:00:00 2001 From: "rhahao.vj" <26148770+rhahao@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:36:52 +0000 Subject: [PATCH 02/16] fix(meetings): set default page when viewing schedules --- src/pages/meetings/schedules/useWeeklySchedules.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/meetings/schedules/useWeeklySchedules.tsx b/src/pages/meetings/schedules/useWeeklySchedules.tsx index 6ace8850a1..49c5acaff5 100644 --- a/src/pages/meetings/schedules/useWeeklySchedules.tsx +++ b/src/pages/meetings/schedules/useWeeklySchedules.tsx @@ -10,6 +10,8 @@ const useWeeklySchedules = () => { ) as WeeklySchedulesType; const value = useMemo(() => { + if (!scheduleType) return 0; + if (scheduleType === 'midweek') return 0; if (scheduleType === 'weekend') return 1; if (scheduleType === 'outgoing') return 2; @@ -22,7 +24,7 @@ const useWeeklySchedules = () => { if (value === 1) type = 'weekend'; if (value === 2) type = 'outgoing'; - localStorage.setItem(LOCALSTORAGE_KEY, type); + localStorage.setItem(LOCALSTORAGE_KEY, type!); }; return { value, handleScheduleChange }; From 58a3307aa3f53acbf86e1dbe5843dab6b208ea55 Mon Sep 17 00:00:00 2001 From: "rhahao.vj" <26148770+rhahao@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:05:59 +0000 Subject: [PATCH 03/16] fix mui v6 issues --- src/components/autocomplete/index.tsx | 3 +- .../date_picker/date_picker.types.ts | 8 +- src/components/date_picker/index.tsx | 38 ++--- src/components/date_picker/view/button.tsx | 6 +- src/components/textfield/index.tsx | 146 ++++++++---------- .../user_add/invitation_code/index.tsx | 2 +- .../invitation_code/useInvitationCode.tsx | 2 +- .../user_details/invitation_code/index.tsx | 6 +- .../invitation_code/useInvitationCode.tsx | 16 +- .../user_details/useUserDetails.tsx | 13 +- .../access_code_view/index.tsx | 2 +- .../master_key_view/index.tsx | 2 +- .../outgoing_talks/schedule_item/index.tsx | 6 +- .../monthly_report/comments/index.tsx | 2 +- .../my_profile/security/mfaEnable/index.tsx | 4 +- .../my_profile/user_profile_details/index.tsx | 2 +- .../incoming/congregation_info/edit/index.tsx | 14 +- .../report_details/comments/index.tsx | 2 +- .../publisher_details/index.tsx | 6 +- src/services/api/congregation.ts | 14 +- 20 files changed, 140 insertions(+), 154 deletions(-) diff --git a/src/components/autocomplete/index.tsx b/src/components/autocomplete/index.tsx index 6a59076034..5bd0216527 100644 --- a/src/components/autocomplete/index.tsx +++ b/src/components/autocomplete/index.tsx @@ -47,7 +47,6 @@ export const CustomListBoxComponent = forwardRef((props: BoxProps, ref) => { (props: AutocompletePropsType) => { variant={variant || 'outlined'} label={props.value ? label : ''} placeholder={props.value ? '' : label} - InputProps={{ ...params.InputProps }} + slotProps={{ input: params.InputProps }} startIcon={startIcon} endIcon={endIcon} height={48} diff --git a/src/components/date_picker/date_picker.types.ts b/src/components/date_picker/date_picker.types.ts index a5c6e5e5c2..5e6b7bdbb4 100644 --- a/src/components/date_picker/date_picker.types.ts +++ b/src/components/date_picker/date_picker.types.ts @@ -10,7 +10,7 @@ export interface CustomDatePickerProps { /** * The selected date value. */ - value?: Date; + value?: Date | undefined; /** * The view type of the date picker. @@ -46,17 +46,17 @@ export interface CustomDatePickerProps { * Function called when the selected date changes. * @param value - The new selected date value. */ - onChange?: (value: Date) => void | Promise; + onChange?: (value: Date | undefined) => void | Promise; /** * The minimum selectable date. */ - minDate?: Date | null; + minDate?: Date; /** * The maximum selectable date. */ - maxDate?: Date | null; + maxDate?: Date; readOnly?: boolean; } diff --git a/src/components/date_picker/index.tsx b/src/components/date_picker/index.tsx index 14442fea20..56cf856c91 100644 --- a/src/components/date_picker/index.tsx +++ b/src/components/date_picker/index.tsx @@ -39,15 +39,15 @@ import { shortDateFormatState } from '@states/settings'; * @returns {JSX.Element} CustomDatePicker component. */ const DatePicker = ({ - value = null, + value, onChange, view = 'input', label, disablePast, shortDateFormat, longDateFormat, - maxDate = null, - minDate = null, + maxDate, + minDate, readOnly = false, }: CustomDatePickerProps) => { const { t } = useAppTranslation(); @@ -58,14 +58,14 @@ const DatePicker = ({ const longDateFormatLocale = longDateFormat || t('tr_longDateFormat'); const [open, setOpen] = useState(false); - const [valueTmp, setValueTmp] = useState(value); - const [innerValue, setInnerValue] = useState(value); + const [valueTmp, setValueTmp] = useState(value); + const [innerValue, setInnerValue] = useState(value); const [height, setHeight] = useState(240); // Initial height - const changeHeight = (event) => { + const changeHeight = (value: Date) => { if ( - getWeeksInMonth(new Date(event), { locale: enUS, weekStartsOn: 0 }) === 6 + getWeeksInMonth(new Date(value), { locale: enUS, weekStartsOn: 0 }) === 6 ) setHeight(290); else setHeight(240); @@ -80,13 +80,13 @@ const DatePicker = ({ ? { field: ButtonField } : { textField: DatePickerInputField }; - const handleFormatSelected = (value) => { - if (isNaN(Date.parse(value))) return '***'; + const handleFormatSelected = (value: Date | undefined) => { + if (isNaN(Date.parse(value as unknown as string))) return '***'; - return format(value, longDateFormatLocale); + return format(value as Date, longDateFormatLocale); }; - const handleValueChange = (value: Date) => { + const handleValueChange = (value: Date | undefined) => { setInnerValue(value); if (view === 'button') { @@ -119,8 +119,8 @@ const DatePicker = ({ slots={{ ...viewProps, actionBar: - view === 'button' - ? null + view === 'input' + ? undefined : () => ( { setOpen(false); - setValueTmp(null); - onChange?.(null); + setValueTmp(undefined); + onChange?.(undefined); }} > {t('tr_clear')} @@ -152,7 +152,7 @@ const DatePicker = ({ ), toolbar: view === 'button' - ? null + ? undefined : () => ( & { setOpen?: Dispatch>; - value: Date; + value: Date | undefined; }; export type FieldProps = SlotComponentPropsFromProps< ButtonFieldProps, - unknown, + {}, UsePickerProps >; @@ -62,7 +62,7 @@ const ButtonField: FC = ({ padding: '4px 8px', }} > - {value ? `${formatDate(value, format)}` : 'Pick a date'} + {value ? `${formatDate(value, format!)}` : 'Pick a date'} ); }; diff --git a/src/components/textfield/index.tsx b/src/components/textfield/index.tsx index 14568041c3..de1195e487 100644 --- a/src/components/textfield/index.tsx +++ b/src/components/textfield/index.tsx @@ -19,19 +19,18 @@ const TextField = (props: TextFieldTypeProps) => { startIcon, endIcon, styleIcon, - InputProps, + slotProps, resetHelperPadding, success, + type = 'text', ...defaultProps } = props; const [showAccessCode, setShowAccessCode] = useState(false); - const [inputType, setInputType] = useState( - props.type - ); + const [inputType, setInputType] = useState(type); const heightLocal = height || 44; const endIconLocal = - props.type === 'password' ? ( + type === 'password' ? ( showAccessCode ? ( ) : ( @@ -49,7 +48,7 @@ const TextField = (props: TextFieldTypeProps) => { const handleToggleAccessCode = () => { setShowAccessCode((prev) => { if (!prev) setInputType('text'); - if (prev) setInputType(props.type); + if (prev) setInputType(type); return !prev; }); @@ -138,81 +137,70 @@ const TextField = (props: TextFieldTypeProps) => { '& > .MuiAutocomplete-popupIndicator': { '& svg, & svg g, & svg g path': 'var(--black)', }, + + '& .MuiAutocomplete-startAdornment .MuiSvgIcon-root': { + color: startIcon?.props.color || 'var(--black)', + }, + + '& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { + color: endIcon?.props.color || 'var(--black)', + }, + ...props.sx, }} - InputProps={{ - ...InputProps, - className: props.className, - startAdornment: startIcon ? ( - - {startIcon} - - ) : ( - InputProps?.startAdornment - ), - endAdornment: endIconLocal ? ( - - {props.type !== 'password' && endIconLocal} - {props.type === 'password' && ( - - {endIconLocal} - - )} - - ) : ( - - {InputProps?.endAdornment} - - ), - }} - FormHelperTextProps={{ - component: 'div', - style: { - margin: 0, - padding: resetHelperPadding ? 0 : '4px 16px 0px 16px', - color: success - ? 'var(--green-main)' - : props.error - ? 'var(--red-main)' - : 'inherit', + slotProps={{ + input: { + className: props.className, + startAdornment: startIcon && ( + + {startIcon} + + ), + endAdornment: endIconLocal && ( + + {props.type !== 'password' && endIconLocal} + {props.type === 'password' && ( + + {endIconLocal} + + )} + + ), + ...slotProps?.input, + }, + formHelperText: { + component: 'div', + style: { + margin: 0, + padding: resetHelperPadding ? 0 : '4px 16px 0px 16px', + color: success + ? 'var(--green-main)' + : props.error + ? 'var(--red-main)' + : 'inherit', + }, }, }} /> diff --git a/src/features/congregation/app_access/user_add/invitation_code/index.tsx b/src/features/congregation/app_access/user_add/invitation_code/index.tsx index 73ead9d2d6..ff311ef0cb 100644 --- a/src/features/congregation/app_access/user_add/invitation_code/index.tsx +++ b/src/features/congregation/app_access/user_add/invitation_code/index.tsx @@ -34,7 +34,7 @@ const InvitationCode = ({ onClose, user }: InvitationCodeType) => { { - const isShareSupported = navigator.share; + const isShareSupported = 'share' in navigator; const handleShareCode = async () => { await navigator.share({ text: code }); diff --git a/src/features/congregation/app_access/user_details/invitation_code/index.tsx b/src/features/congregation/app_access/user_details/invitation_code/index.tsx index fb66676739..8975675f42 100644 --- a/src/features/congregation/app_access/user_details/invitation_code/index.tsx +++ b/src/features/congregation/app_access/user_details/invitation_code/index.tsx @@ -55,12 +55,12 @@ const InvitationCode = () => { - {user.pocket_invitation_code && ( + {user.profile.pocket_invitation_code && ( <> { )} - {!user.pocket_invitation_code && ( + {!user.profile.pocket_invitation_code && ( ); }; diff --git a/src/components/date_picker/view/input.tsx b/src/components/date_picker/view/input.tsx index 5f3669ab04..77566acc2b 100644 --- a/src/components/date_picker/view/input.tsx +++ b/src/components/date_picker/view/input.tsx @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction } from 'react'; +import { Dispatch, forwardRef, Ref, SetStateAction } from 'react'; import { TextFieldProps } from '@mui/material'; import TextField from '@components/textfield'; import { IconDate } from '@icons/index'; @@ -14,7 +14,10 @@ type DateTextFieldProps = TextFieldProps & { * @param {Dispatch>} props.setOpen - Function to set the open state of the date picker. * @returns {JSX.Element} DatePickerInputField component. */ -const DatePickerInputField = (props: DateTextFieldProps) => { +const DatePickerInputField = forwardRef(function DatePickerInputField( + props: DateTextFieldProps, + ref: Ref +) { const { setOpen, ...defaultProps } = props; const handleClick = () => { @@ -23,6 +26,7 @@ const DatePickerInputField = (props: DateTextFieldProps) => { return ( { } /> ); -}; +}); export default DatePickerInputField; diff --git a/src/components/textfield/index.tsx b/src/components/textfield/index.tsx index de1195e487..0cd791b539 100644 --- a/src/components/textfield/index.tsx +++ b/src/components/textfield/index.tsx @@ -138,7 +138,7 @@ const TextField = (props: TextFieldTypeProps) => { '& svg, & svg g, & svg g path': 'var(--black)', }, - '& .MuiAutocomplete-startAdornment .MuiSvgIcon-root': { + '& .MuiInputAdornment-positionStart .MuiSvgIcon-root': { color: startIcon?.props.color || 'var(--black)', }, @@ -149,9 +149,11 @@ const TextField = (props: TextFieldTypeProps) => { ...props.sx, }} slotProps={{ + ...slotProps, input: { + ...slotProps?.input, className: props.className, - startAdornment: startIcon && ( + startAdornment: startIcon ? ( { > {startIcon} + ) : ( + slotProps?.input['startAdornment'] ), - endAdornment: endIconLocal && ( + endAdornment: endIconLocal ? ( { )} + ) : ( + slotProps?.input['endAdornment'] ), - ...slotProps?.input, }, formHelperText: { component: 'div', diff --git a/tsconfig.app.json b/tsconfig.app.json deleted file mode 100644 index 1a459fce14..0000000000 --- a/tsconfig.app.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - - "baseUrl": "./src", - "paths": { - "react": ["./node_modules/@types/react"], - "@mui/styled-engine": ["./node_modules/@mui/styled-engine-sc"], - "@assets/*": ["assets/*"], - "@components/*": ["components/*"], - "@icons/*": ["components/icons/*"], - "@constants/*": ["constants/*"], - "@features/*": ["features/*"], - "@hooks/*": ["hooks/*"], - "@layouts/*": ["layouts/*"], - "@pages/*": ["pages/*"], - "@routes/*": ["routes/*"], - "@services/*": ["services/*"], - "@states/*": ["states/*"], - "@utils/*": ["utils/*"], - "@wrapper/*": ["wrapper/*"], - "@locales/*": ["shared/*"], - "@definition/*": ["definition/*"], - "@global/*": ["global/*"], - "@db/*": ["indexedDb/*"], - "@talks/*": ["public_talks/*"], - "@views/*": ["views/*"] - } - }, - "include": ["src/**/*", "types/**/*"] -} diff --git a/tsconfig.json b/tsconfig.json index 1ffef600d9..93dd80f829 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,36 @@ { - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "compilerOptions": { + "jsx": "react", + "skipLibCheck": true, + "baseUrl": "./src", + "paths": { + "@mui/styled-engine": ["./node_modules/@mui/styled-engine-sc"], + "@assets/*": ["assets/*"], + "@components/*": ["components/*"], + "@icons/*": ["components/icons/*"], + "@constants/*": ["constants/*"], + "@features/*": ["features/*"], + "@hooks/*": ["hooks/*"], + "@layouts/*": ["layouts/*"], + "@pages/*": ["pages/*"], + "@routes/*": ["routes/*"], + "@services/*": ["services/*"], + "@states/*": ["states/*"], + "@utils/*": ["utils/*"], + "@wrapper/*": ["wrapper/*"], + "@locales/*": ["shared/*"], + "@definition/*": ["definition/*"], + "@global/*": ["global/*"], + "@db/*": ["indexedDb/*"], + "@talks/*": ["public_talks/*"], + "@views/*": ["views/*"] + }, + "esModuleInterop": true, + "moduleResolution": "Node", + "module": "ESNext", + "types": ["@svgx/vite-plugin-react", "node", "vite/client"], + "lib": ["ESNext", "DOM"], + "target": "ESNext" + }, + "include": ["src/**/*", "types/**/*"] } diff --git a/tsconfig.node.json b/tsconfig.node.json deleted file mode 100644 index b3fc13e547..0000000000 --- a/tsconfig.node.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["vite.config.ts"] -} From 5a5c5a1ddebaa610f91a04e38011f125d11a5fe6 Mon Sep 17 00:00:00 2001 From: rhahao <26148770+rhahao@users.noreply.github.com> Date: Wed, 2 Oct 2024 19:02:22 +0300 Subject: [PATCH 05/16] fix(components): update anchor props for time picker --- src/components/time_picker/index.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/time_picker/index.tsx b/src/components/time_picker/index.tsx index 4d29bc8258..a3fe145198 100644 --- a/src/components/time_picker/index.tsx +++ b/src/components/time_picker/index.tsx @@ -17,6 +17,7 @@ import { SetStateAction, useCallback, useEffect, + useRef, useState, } from 'react'; import { CustomTimePickerProps } from './time_picker.types'; @@ -110,6 +111,8 @@ const TimePicker = ({ sx, readOnly = false, }: CustomTimePickerProps) => { + const inputRef = useRef(null); + const { t } = useAppTranslation(); const [currentValue, setCurrentValue] = useState(value); @@ -174,6 +177,7 @@ const TimePicker = ({ onClear: handleClear, } as never, textField: { + inputRef, label: label, value: currentValue, onClick: () => setOpen(!open), @@ -225,6 +229,7 @@ const TimePicker = ({ className: 'pop-up pop-up-shadow', }, popper: { + anchorEl: inputRef.current, sx: { ...StyleTimePickerPopper, '.MuiPickersToolbar-content': { From 66c8b32c7130b1f1f1d719ff9c8165c8a26312f1 Mon Sep 17 00:00:00 2001 From: rhahao <26148770+rhahao@users.noreply.github.com> Date: Thu, 3 Oct 2024 06:53:59 +0300 Subject: [PATCH 06/16] pocket authentication --- src/definition/api.ts | 1 + .../container/useContainer.tsx | 6 +- .../app_start/pocket/signup/index.tsx | 23 ++- .../app_start/pocket/signup/useSignup.tsx | 133 ++++++++++++++++-- .../app_start/pocket/startup/index.tsx | 10 +- .../app_start/pocket/startup/useStartup.tsx | 131 ++++++++++++++++- .../person_select/usePersonSelect.tsx | 13 +- src/hooks/useCurrentUser.tsx | 8 +- .../manage_access/all_users/useAllUsers.tsx | 30 ++-- src/pages/dashboard/congregation/index.tsx | 36 +++-- src/pages/dashboard/index.tsx | 10 +- src/pages/my_profile/index.tsx | 17 ++- src/services/api/pocket.ts | 39 +++++ src/services/i18n/translation.ts | 2 + src/states/app.ts | 38 +++++ 15 files changed, 420 insertions(+), 77 deletions(-) create mode 100644 src/services/api/pocket.ts diff --git a/src/definition/api.ts b/src/definition/api.ts index 5bad3b1807..0b37f56843 100644 --- a/src/definition/api.ts +++ b/src/definition/api.ts @@ -86,6 +86,7 @@ export type UserLoginResponseType = { mfa: 'not_enabled' | 'enabled'; user_local_uid?: string; cong_role?: AppRoleType[]; + user_members_delegate?: string[]; }; cong_settings?: { id: string; diff --git a/src/features/app_notification/container/useContainer.tsx b/src/features/app_notification/container/useContainer.tsx index dfb80a361c..1be5909a38 100644 --- a/src/features/app_notification/container/useContainer.tsx +++ b/src/features/app_notification/container/useContainer.tsx @@ -7,7 +7,7 @@ import { encryptedMasterKeyState, speakersKeyState, } from '@states/app'; -import { useAppTranslation } from '@hooks/index'; +import { useAppTranslation, useCurrentUser } from '@hooks/index'; import { NotificationRecordType } from '@definition/notification'; import { notificationsState } from '@states/notification'; import { apiGetCongregationUpdates } from '@services/api/congregation'; @@ -33,6 +33,8 @@ import usePendingRequests from './usePendingRequests'; const useContainer = () => { const { t } = useAppTranslation(); + const { isAdmin } = useCurrentUser(); + const { updatePendingRequestsNotification } = usePendingRequests(); const [notifications, setNotifications] = useRecoilState(notificationsState); @@ -51,7 +53,7 @@ const useContainer = () => { const congAccessCode = useRecoilValue(congAccessCodeState); const { isLoading, data } = useQuery({ - enabled: congAccountConnected, + enabled: congAccountConnected && isAdmin, queryKey: ['congregation_updates'], queryFn: apiGetCongregationUpdates, refetchInterval: 60 * 1000, diff --git a/src/features/app_start/pocket/signup/index.tsx b/src/features/app_start/pocket/signup/index.tsx index e752017a41..c5fbc6acaa 100644 --- a/src/features/app_start/pocket/signup/index.tsx +++ b/src/features/app_start/pocket/signup/index.tsx @@ -1,22 +1,21 @@ import { Box } from '@mui/material'; +import { IconError, IconLoading } from '@icons/index'; +import { useAppTranslation } from '@hooks/index'; +import useSignup from './useSignup'; import Button from '@components/button'; import InfoMessage from '@components/info-message'; -import TextField from '@components/textfield'; import PageHeader from '@features/app_start/shared/page_header'; -import { IconError, IconLoading } from '@icons/index'; -import useAppTranslation from '@hooks/useAppTranslation'; -import useSignup from './useSignup'; +import TextField from '@components/textfield'; const PocketSignUp = () => { const { t } = useAppTranslation(); const { handleReturnChooser, - handleSignUp, + handleValidate, isOnline, isProcessing, - setCode, - visitorID, + handleCodeChange, code, hideMessage, title, @@ -45,18 +44,14 @@ const PocketSignUp = () => { setCode(e.target.value)} + onChange={(e) => handleCodeChange(e.target.value)} sx={{ width: '100%', color: 'var(--black)' }} className="h4" /> ); - }, [t, groups]); + }, [t, groups, isServiceCommittee]); return { buttons, diff --git a/src/pages/congregation/manage_access/all_users/index.tsx b/src/pages/congregation/manage_access/all_users/index.tsx index a144db2b73..fb06a8fd7b 100644 --- a/src/pages/congregation/manage_access/all_users/index.tsx +++ b/src/pages/congregation/manage_access/all_users/index.tsx @@ -20,6 +20,7 @@ const UsersAll = () => { congregationsPersons, appAdmin, baptizedPersons, + sync_disabled, } = useAllUsers(); return ( @@ -30,6 +31,7 @@ const UsersAll = () => {