diff --git a/src/constants/table_encryption_map.ts b/src/constants/table_encryption_map.ts index 2ff0597d3e3..5679c1faba5 100644 --- a/src/constants/table_encryption_map.ts +++ b/src/constants/table_encryption_map.ts @@ -3,6 +3,7 @@ export const TABLE_ENCRYPTION_MAP = { _deleted: 'shared', person_uid: 'public', person_firstname: 'shared', + person_middlename: 'shared', person_lastname: 'shared', person_display_name: 'shared', male: 'shared', @@ -35,6 +36,7 @@ export const TABLE_ENCRYPTION_MAP = { cong_circuit: 'public', cong_discoverable: 'public', fullname_option: 'shared', + print_fullname_option: 'shared', short_date_format: 'shared', display_name_enabled: 'shared', schedule_exact_date_enabled: 'shared', @@ -68,6 +70,7 @@ export const TABLE_ENCRYPTION_MAP = { user_local_uid: 'public', user_members_delegate: 'public', firstname: 'public', + middlename: 'public', lastname: 'public', backup_automatic: 'shared', theme_follow_os_enabled: 'shared', diff --git a/src/definition/api.ts b/src/definition/api.ts index 0ac54818c45..94a357f20d4 100644 --- a/src/definition/api.ts +++ b/src/definition/api.ts @@ -89,6 +89,7 @@ export type UserLoginResponseType = { app_settings: { user_settings: { firstname: { value: string; updatedAt: string }; + middlename?: { value: string; updatedAt: string }; lastname: { value: string; updatedAt: string }; role: UserGlobalRoleType; mfa: 'not_enabled' | 'enabled'; diff --git a/src/definition/person.ts b/src/definition/person.ts index de760f826a6..c7e1aae1e2c 100644 --- a/src/definition/person.ts +++ b/src/definition/person.ts @@ -68,6 +68,7 @@ export type PersonType = { person_uid: string; person_data: { person_firstname: { value: string; updatedAt: string }; + person_middlename?: { value: string; updatedAt: string }; person_lastname: { value: string; updatedAt: string }; person_display_name: { value: string; updatedAt: string }; male: { value: boolean; updatedAt: string }; diff --git a/src/definition/settings.ts b/src/definition/settings.ts index 0cb849da469..2d2b54dead9 100644 --- a/src/definition/settings.ts +++ b/src/definition/settings.ts @@ -4,6 +4,10 @@ import { AssignmentFieldType } from './assignment'; export enum FullnameOption { FIRST_BEFORE_LAST = 1, LAST_BEFORE_FIRST = 2, + FIRST_MIDDLE_LAST = 3, + LAST_FIRST_MIDDLE = 4, + LAST_COMMA_FIRST = 5, + LAST_COMMA_FIRST_MIDDLE = 6, } export type AccountTypeState = 'vip' | 'pocket'; @@ -69,6 +73,12 @@ export type SettingsType = { updatedAt: string; _deleted: boolean; }[]; + print_fullname_option?: { + type: string; + value: FullnameOption; + updatedAt: string; + _deleted: boolean; + }[]; short_date_format: { type: string; value: string; @@ -145,6 +155,7 @@ export type SettingsType = { }[]; circuit_overseer: { firstname: { value: string; updatedAt: string }; + middlename?: { value: string; updatedAt: string }; lastname: { value: string; updatedAt: string }; display_name: { value: string; updatedAt: string }; visits: CircuitOverseerVisitType[]; @@ -201,6 +212,7 @@ export type SettingsType = { user_local_uid: string; user_members_delegate: string[]; firstname: { value: string; updatedAt: string }; + middlename?: { value: string; updatedAt: string }; lastname: { value: string; updatedAt: string }; backup_automatic: { enabled: { value: boolean; updatedAt: string }; diff --git a/src/definition/visiting_speakers.ts b/src/definition/visiting_speakers.ts index 0af984aa6f0..494d4ef11e7 100644 --- a/src/definition/visiting_speakers.ts +++ b/src/definition/visiting_speakers.ts @@ -11,6 +11,7 @@ export type VisitingSpeakerType = { speaker_data: { cong_id: string; person_firstname: { value: string; updatedAt: string }; + person_middlename?: { value: string; updatedAt: string }; person_lastname: { value: string; updatedAt: string }; person_display_name: { value: string; updatedAt: string }; person_notes: { value: string; updatedAt: string }; diff --git a/src/features/app_start/pocket/signup/useSignup.tsx b/src/features/app_start/pocket/signup/useSignup.tsx index b7036f08300..d232a91d40a 100644 --- a/src/features/app_start/pocket/signup/useSignup.tsx +++ b/src/features/app_start/pocket/signup/useSignup.tsx @@ -109,6 +109,9 @@ const useSignup = () => { 'user_settings.account_type': 'pocket', 'user_settings.lastname': app_settings.user_settings.lastname, 'user_settings.firstname': app_settings.user_settings.firstname, + ...(app_settings.user_settings.middlename && { + 'user_settings.middlename': app_settings.user_settings.middlename, + }), 'user_settings.user_local_uid': app_settings.user_settings.user_local_uid, 'user_settings.user_members_delegate': app_settings.user_settings.user_members_delegate, diff --git a/src/features/app_start/vip/hooks/useAuth.tsx b/src/features/app_start/vip/hooks/useAuth.tsx index d685ed4e324..90da9f2458f 100644 --- a/src/features/app_start/vip/hooks/useAuth.tsx +++ b/src/features/app_start/vip/hooks/useAuth.tsx @@ -102,6 +102,9 @@ const useAuth = () => { 'user_settings.account_type': 'vip', 'user_settings.lastname': app_settings.user_settings.lastname, 'user_settings.firstname': app_settings.user_settings.firstname, + ...(app_settings.user_settings.middlename && { + 'user_settings.middlename': app_settings.user_settings.middlename, + }), }); } diff --git a/src/features/app_start/vip/verify_mfa/useVerifyMFA.tsx b/src/features/app_start/vip/verify_mfa/useVerifyMFA.tsx index 3263abcc030..b9a22f165b4 100644 --- a/src/features/app_start/vip/verify_mfa/useVerifyMFA.tsx +++ b/src/features/app_start/vip/verify_mfa/useVerifyMFA.tsx @@ -58,6 +58,9 @@ const useVerifyMFA = () => { 'user_settings.account_type': 'vip', 'user_settings.lastname': app_settings.user_settings.lastname, 'user_settings.firstname': app_settings.user_settings.firstname, + ...(app_settings.user_settings.middlename && { + 'user_settings.middlename': app_settings.user_settings.middlename, + }), }); } diff --git a/src/features/congregation/app_access/join_requests/accept/useAccept.tsx b/src/features/congregation/app_access/join_requests/accept/useAccept.tsx index 96b252aa094..f938cf6bf2a 100644 --- a/src/features/congregation/app_access/join_requests/accept/useAccept.tsx +++ b/src/features/congregation/app_access/join_requests/accept/useAccept.tsx @@ -33,7 +33,8 @@ const useAcceptRequest = ({ onConfirm }: AcceptRequestProps) => { person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }); diff --git a/src/features/congregation/app_access/user_add/person_select/usePersonSelect.tsx b/src/features/congregation/app_access/user_add/person_select/usePersonSelect.tsx index c3e1fbbc5e3..6dc8666c7ca 100644 --- a/src/features/congregation/app_access/user_add/person_select/usePersonSelect.tsx +++ b/src/features/congregation/app_access/user_add/person_select/usePersonSelect.tsx @@ -69,7 +69,8 @@ const usePersonSelect = ({ person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }); 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 0341919585e..82c628066c5 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 @@ -31,7 +31,8 @@ const useProfileSettings = () => { person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }); @@ -48,7 +49,8 @@ const useProfileSettings = () => { person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }); @@ -151,7 +153,8 @@ const useProfileSettings = () => { person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }); } @@ -169,7 +172,8 @@ const useProfileSettings = () => { person_name: buildPersonFullname( found.person_data.person_lastname.value, found.person_data.person_firstname.value, - fullnameOption + fullnameOption, + found.person_data.person_middlename?.value ), }); } diff --git a/src/features/congregation/field_service_groups/create_group/group_details/useGroupDetails.tsx b/src/features/congregation/field_service_groups/create_group/group_details/useGroupDetails.tsx index 767c7b5db51..fa9c869e162 100644 --- a/src/features/congregation/field_service_groups/create_group/group_details/useGroupDetails.tsx +++ b/src/features/congregation/field_service_groups/create_group/group_details/useGroupDetails.tsx @@ -67,7 +67,8 @@ const useGroupDetails = ({ group, onChange }: GroupDetailsProps) => { person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), } as UsersOption; }, [group, persons, fullnameOption]); @@ -95,7 +96,8 @@ const useGroupDetails = ({ group, onChange }: GroupDetailsProps) => { person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), } as UsersOption; }, [group, persons, fullnameOption]); @@ -109,7 +111,8 @@ const useGroupDetails = ({ group, onChange }: GroupDetailsProps) => { person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), elder: personIsElder(person), }; @@ -125,7 +128,8 @@ const useGroupDetails = ({ group, onChange }: GroupDetailsProps) => { person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), elder: personIsElder(person), }; diff --git a/src/features/congregation/field_service_groups/export_groups/useExportGroups.tsx b/src/features/congregation/field_service_groups/export_groups/useExportGroups.tsx index 460d42c7738..ff47b1ba59f 100644 --- a/src/features/congregation/field_service_groups/export_groups/useExportGroups.tsx +++ b/src/features/congregation/field_service_groups/export_groups/useExportGroups.tsx @@ -10,7 +10,7 @@ import { personsActiveState } from '@states/persons'; import { JWLangLocaleState, congNameState, - fullnameOptionState, + printFullnameOptionState, publishersSortState, } from '@states/settings'; import { buildPersonFullname } from '@utils/common'; @@ -26,7 +26,7 @@ const useExportGroups = () => { const groups_list = useAtomValue(fieldWithLanguageGroupsState); const persons = useAtomValue(personsActiveState); - const fullnameOption = useAtomValue(fullnameOptionState); + const fullnameOption = useAtomValue(printFullnameOptionState); const congName = useAtomValue(congNameState); const sortMethod = useAtomValue(publishersSortState); const locale = useAtomValue(JWLangLocaleState); @@ -74,7 +74,8 @@ const useExportGroups = () => { person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }) diff --git a/src/features/congregation/field_service_groups/group_item/member/useMember.tsx b/src/features/congregation/field_service_groups/group_item/member/useMember.tsx index 1a091af33ac..59de530b4c6 100644 --- a/src/features/congregation/field_service_groups/group_item/member/useMember.tsx +++ b/src/features/congregation/field_service_groups/group_item/member/useMember.tsx @@ -70,7 +70,8 @@ const useMember = ({ member, index, group_id }: GroupMemberProps) => { return buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); }, [person, fullnameOption]); diff --git a/src/features/congregation/field_service_groups/group_item/remove_person/useRemovePerson.tsx b/src/features/congregation/field_service_groups/group_item/remove_person/useRemovePerson.tsx index 581dadcc121..29381227d47 100644 --- a/src/features/congregation/field_service_groups/group_item/remove_person/useRemovePerson.tsx +++ b/src/features/congregation/field_service_groups/group_item/remove_person/useRemovePerson.tsx @@ -25,7 +25,8 @@ const useRemovePerson = ({ group_id, index, member }: RemovePersonProps) => { const name = buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); return name; diff --git a/src/features/congregation/field_service_groups/group_members/member_item/useMemberItem.tsx b/src/features/congregation/field_service_groups/group_members/member_item/useMemberItem.tsx index c852e5195a1..65c56d89219 100644 --- a/src/features/congregation/field_service_groups/group_members/member_item/useMemberItem.tsx +++ b/src/features/congregation/field_service_groups/group_members/member_item/useMemberItem.tsx @@ -17,7 +17,8 @@ const useMemberItem = ({ member, onDelete }: MemberItemProps) => { return buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); }, [member, persons, fullnameOption]); diff --git a/src/features/congregation/field_service_groups/group_members/useGroupMembers.tsx b/src/features/congregation/field_service_groups/group_members/useGroupMembers.tsx index d1ea1705f73..b57a967784d 100644 --- a/src/features/congregation/field_service_groups/group_members/useGroupMembers.tsx +++ b/src/features/congregation/field_service_groups/group_members/useGroupMembers.tsx @@ -66,7 +66,8 @@ const useGroupMembers = ({ group, onChange }: GroupMembersProps) => { person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }); diff --git a/src/features/congregation/settings/circuit_overseer/index.tsx b/src/features/congregation/settings/circuit_overseer/index.tsx index 2b06a199dd4..3181e2a7138 100644 --- a/src/features/congregation/settings/circuit_overseer/index.tsx +++ b/src/features/congregation/settings/circuit_overseer/index.tsx @@ -1,29 +1,20 @@ -import { FullnameOption } from '@definition/settings'; -import { - useAppTranslation, - useBreakpoints, - useCurrentUser, -} from '@hooks/index'; +import { useAppTranslation, useCurrentUser } from '@hooks/index'; import { CardSection, CardSectionContent, CardSectionHeader, - TwoColumnsRow, } from '../shared_styles'; import useCircuitOverseer from './useCircuitOverseer'; import TextField from '@components/textfield'; import WeeksList from './weeks_list'; -import { Stack } from '@mui/material'; +import { Box, Stack } from '@mui/material'; const CircuitOverseer = () => { const { t } = useAppTranslation(); - const { tablet600Up } = useBreakpoints(); - const { isAdmin } = useCurrentUser(); const { - fullnameOption, displayNameEnabled, displayname, firstname, @@ -31,6 +22,9 @@ const CircuitOverseer = () => { handleDisplaynameSave, handleFirstnameChange, handleFirstnameSave, + handleMiddlenameChange, + handleMiddlenameSave, + middlename, handleLastnameChange, handleLastnameSave, lastname, @@ -45,15 +39,12 @@ const CircuitOverseer = () => { - { onChange={(e) => handleFirstnameChange(e.target.value)} onKeyUp={handleFirstnameSave} slotProps={{ input: { readOnly: !isAdmin } }} + sx={{ flex: '1 0 200px' }} + /> + handleMiddlenameChange(e.target.value)} + onKeyUp={handleMiddlenameSave} + slotProps={{ input: { readOnly: !isAdmin } }} + sx={{ flex: '1 0 200px' }} /> { onChange={(e) => handleLastnameChange(e.target.value)} onKeyUp={handleLastnameSave} slotProps={{ input: { readOnly: !isAdmin } }} + sx={{ flex: '1 0 200px' }} /> - + {displayNameEnabled && ( { - type FieldKey = 'firstname' | 'lastname' | 'displayname'; + type FieldKey = 'firstname' | 'middlename' | 'lastname' | 'displayname'; - const saveTimers = useRef>>>({}); + const saveTimers = useRef< + Partial>> + >({}); const settings = useAtomValue(settingsState); - const fullnameOption = useAtomValue(fullnameOptionState); const displayNameEnabled = useAtomValue(displayNameMeetingsEnableState); const [firstname, setFirstname] = useState(''); + const [middlename, setMiddlename] = useState(''); const [lastname, setLastname] = useState(''); const [displayname, setDisplayname] = useState(''); const [editing, setEditing] = useState>({ firstname: false, + middlename: false, lastname: false, displayname: false, }); @@ -34,7 +36,11 @@ const useCircuitOverseer = () => { } }; - const scheduleSave = (key: FieldKey, fn: () => Promise, markCompleteKeys: FieldKey[]) => { + const scheduleSave = ( + key: FieldKey, + fn: () => Promise, + markCompleteKeys: FieldKey[] + ) => { clearTimer(key); saveTimers.current[key] = setTimeout(async () => { @@ -68,6 +74,11 @@ const useCircuitOverseer = () => { setDisplayname(dispName); }; + const handleMiddlenameChange = (value: string) => { + markEditing(['middlename']); + setMiddlename(value); + }; + const handleLastnameChange = (value: string) => { markEditing(['lastname', 'displayname']); setLastname(value); @@ -82,7 +93,14 @@ const useCircuitOverseer = () => { }; const handleFirstnameSave = () => { - scheduleSave('firstname', handleFirstnameSaveDb, ['firstname', 'displayname']); + scheduleSave('firstname', handleFirstnameSaveDb, [ + 'firstname', + 'displayname', + ]); + }; + + const handleMiddlenameSave = () => { + scheduleSave('middlename', handleMiddlenameSaveDb, ['middlename']); }; const handleLastnameSave = () => { @@ -113,6 +131,22 @@ const useCircuitOverseer = () => { }); }; + const handleMiddlenameSaveDb = async () => { + const middlenameField = structuredClone( + settings.cong_settings.circuit_overseer.middlename ?? { + value: '', + updatedAt: '', + } + ); + + middlenameField.value = middlename; + middlenameField.updatedAt = new Date().toISOString(); + + await dbAppSettingsUpdate({ + 'cong_settings.circuit_overseer.middlename': middlenameField, + }); + }; + const handleLastnameSaveDb = async () => { const lastnameField = structuredClone( settings.cong_settings.circuit_overseer.lastname @@ -150,8 +184,13 @@ const useCircuitOverseer = () => { const co = settings.cong_settings.circuit_overseer; setFirstname((prev) => (editing.firstname ? prev : co.firstname.value)); + setMiddlename((prev) => + editing.middlename ? prev : (co.middlename?.value ?? '') + ); setLastname((prev) => (editing.lastname ? prev : co.lastname.value)); - setDisplayname((prev) => (editing.displayname ? prev : co.display_name.value)); + setDisplayname((prev) => + editing.displayname ? prev : co.display_name.value + ); }, [settings, editing]); useEffect(() => { @@ -168,11 +207,13 @@ const useCircuitOverseer = () => { }, []); return { - fullnameOption, displayNameEnabled, firstname, handleFirstnameChange, handleFirstnameSave, + middlename, + handleMiddlenameChange, + handleMiddlenameSave, lastname, handleLastnameChange, handleLastnameSave, diff --git a/src/features/congregation/settings/language_groups/group_delete/useGroupDelete.tsx b/src/features/congregation/settings/language_groups/group_delete/useGroupDelete.tsx index 28644da87ee..01973e15917 100644 --- a/src/features/congregation/settings/language_groups/group_delete/useGroupDelete.tsx +++ b/src/features/congregation/settings/language_groups/group_delete/useGroupDelete.tsx @@ -100,6 +100,19 @@ const useGroupDelete = ({ group }: GroupDeleteProps) => { findOption.updatedAt = new Date().toISOString(); } + const printFullnameOption = structuredClone( + settings.cong_settings.print_fullname_option ?? [] + ); + + const findPrintOption = printFullnameOption.find( + (record) => record.type === group.group_id + ); + + if (findPrintOption) { + findPrintOption._deleted = true; + findPrintOption.updatedAt = new Date().toISOString(); + } + const shortDateFormat = structuredClone( settings.cong_settings.short_date_format ); @@ -202,6 +215,7 @@ const useGroupDelete = ({ group }: GroupDeleteProps) => { 'cong_settings.cong_circuit': circuits, 'cong_settings.display_name_enabled': displayName, 'cong_settings.fullname_option': fullnameOption, + 'cong_settings.print_fullname_option': printFullnameOption, 'cong_settings.short_date_format': shortDateFormat, 'cong_settings.format_24h_enabled': format24h, 'cong_settings.attendance_online_record': onlineRecord, diff --git a/src/features/congregation/settings/language_groups/group_members/useGroupMembers.tsx b/src/features/congregation/settings/language_groups/group_members/useGroupMembers.tsx index dcb75464517..97dfc0e441c 100644 --- a/src/features/congregation/settings/language_groups/group_members/useGroupMembers.tsx +++ b/src/features/congregation/settings/language_groups/group_members/useGroupMembers.tsx @@ -50,7 +50,8 @@ const useLanguageGroupMembers = ({ person_name: buildPersonFullname( record.person_data.person_lastname.value, record.person_data.person_firstname.value, - fullnameOption + fullnameOption, + record.person_data.person_middlename?.value ), }; }); diff --git a/src/features/congregation/settings/meeting_forms/index.tsx b/src/features/congregation/settings/meeting_forms/index.tsx index e04901e352c..a2135e5dda9 100644 --- a/src/features/congregation/settings/meeting_forms/index.tsx +++ b/src/features/congregation/settings/meeting_forms/index.tsx @@ -13,6 +13,7 @@ import DisplayName from './display_name'; import MenuItem from '@components/menuitem'; import MidweekExactDate from './midweek_exact_date'; import NameFormat from './name_format'; +import PrintNameFormat from './print_name_format'; import PublishersSort from '../congregation_privacy/publishers_sort'; import Select from '@components/select'; import SourceLanguage from './source_language'; @@ -91,6 +92,8 @@ const MeetingForms = () => { + + diff --git a/src/features/congregation/settings/meeting_forms/name_format/index.tsx b/src/features/congregation/settings/meeting_forms/name_format/index.tsx index de766cc7b1a..084d88acca5 100644 --- a/src/features/congregation/settings/meeting_forms/name_format/index.tsx +++ b/src/features/congregation/settings/meeting_forms/name_format/index.tsx @@ -1,9 +1,6 @@ import { useAppTranslation, useCurrentUser } from '@hooks/index'; -import { FullnameOption } from '@definition/settings'; import useNameFormat from './useNameFormat'; -import MenuItem from '@components/menuitem'; -import Select from '@components/select'; -import Typography from '@components/typography'; +import NameFormatSelect from '../name_format_select'; const NameFormat = () => { const { t } = useAppTranslation(); @@ -14,21 +11,14 @@ const NameFormat = () => { const { fullnameOption, handleFullnameOptionChange } = useNameFormat(); return ( - + /> ); }; diff --git a/src/features/congregation/settings/meeting_forms/name_format/useNameFormat.tsx b/src/features/congregation/settings/meeting_forms/name_format/useNameFormat.tsx index 1b871f30f41..b35437dab40 100644 --- a/src/features/congregation/settings/meeting_forms/name_format/useNameFormat.tsx +++ b/src/features/congregation/settings/meeting_forms/name_format/useNameFormat.tsx @@ -1,55 +1,18 @@ -import { useEffect, useState } from 'react'; import { useAtomValue } from 'jotai'; -import { - fullnameOptionState, - settingsState, - userDataViewState, -} from '@states/settings'; -import { dbAppSettingsUpdate } from '@services/dexie/settings'; -import { FullnameOption } from '@definition/settings'; +import { fullnameOptionState } from '@states/settings'; +import useNameFormatOption from '../name_format_select/useNameFormatOption'; const useNameFormat = () => { - const settings = useAtomValue(settingsState); - const dataView = useAtomValue(userDataViewState); const optionInitial = useAtomValue(fullnameOptionState); - const [fullnameOption, setFullnameOption] = useState( - FullnameOption.FIRST_BEFORE_LAST + const { option: fullnameOption, handleOptionChange } = useNameFormatOption( + 'fullname_option', + optionInitial ); - const handleFullnameOptionChange = async (value: FullnameOption) => { - const fullnameOption = structuredClone( - settings.cong_settings.fullname_option - ); - - const current = fullnameOption.find((record) => record.type === dataView); - - if (current) { - current.value = value; - current.updatedAt = new Date().toISOString(); - } - - if (!current) { - fullnameOption.push({ - _deleted: false, - type: dataView, - updatedAt: new Date().toISOString(), - value, - }); - } - - await dbAppSettingsUpdate({ - 'cong_settings.fullname_option': fullnameOption, - }); - }; - - useEffect(() => { - setFullnameOption(optionInitial); - }, [optionInitial]); - return { fullnameOption, - handleFullnameOptionChange, + handleFullnameOptionChange: handleOptionChange, }; }; diff --git a/src/features/congregation/settings/meeting_forms/name_format_select/index.tsx b/src/features/congregation/settings/meeting_forms/name_format_select/index.tsx new file mode 100644 index 00000000000..18367a60fea --- /dev/null +++ b/src/features/congregation/settings/meeting_forms/name_format_select/index.tsx @@ -0,0 +1,90 @@ +import { Stack } from '@mui/material'; +import { useAppTranslation } from '@hooks/index'; +import { FullnameOption } from '@definition/settings'; +import { NAME_FORMAT_INHERIT, NameFormatSelectType } from './index.types'; +import { buildPersonFullname } from '@utils/common'; +import MenuItem from '@components/menuitem'; +import Select from '@components/select'; +import Typography from '@components/typography'; + +const NAME_FORMAT_OPTIONS = [ + { value: FullnameOption.FIRST_BEFORE_LAST, label: 'tr_nameFormatFirstLast' }, + { value: FullnameOption.LAST_BEFORE_FIRST, label: 'tr_nameFormatLastFirst' }, + { + value: FullnameOption.FIRST_MIDDLE_LAST, + label: 'tr_nameFormatFirstMiddleLast', + }, + { + value: FullnameOption.LAST_FIRST_MIDDLE, + label: 'tr_nameFormatLastFirstMiddle', + }, + { + value: FullnameOption.LAST_COMMA_FIRST, + label: 'tr_nameFormatLastCommaFirst', + }, + { + value: FullnameOption.LAST_COMMA_FIRST_MIDDLE, + label: 'tr_nameFormatLastCommaFirstMiddle', + }, +]; + +const NameFormatSelect = ({ + label, + value, + onChange, + readOnly, + inherit, +}: NameFormatSelectType) => { + const { t } = useAppTranslation(); + + const buildSample = (option: FullnameOption) => + buildPersonFullname( + t('tr_nameFormatSampleLastname'), + t('tr_nameFormatSampleFirstname'), + option, + t('tr_nameFormatSampleMiddlename') + ); + + return ( + + ); +}; + +export default NameFormatSelect; diff --git a/src/features/congregation/settings/meeting_forms/name_format_select/index.types.ts b/src/features/congregation/settings/meeting_forms/name_format_select/index.types.ts new file mode 100644 index 00000000000..a105a6a4d2e --- /dev/null +++ b/src/features/congregation/settings/meeting_forms/name_format_select/index.types.ts @@ -0,0 +1,13 @@ +import { FullnameOption } from '@definition/settings'; + +// sentinel for "follow the in-app format"; deliberately not a FullnameOption +// member so it can never be persisted as a real format +export const NAME_FORMAT_INHERIT = 0 as FullnameOption; + +export type NameFormatSelectType = { + label: string; + value: FullnameOption; + onChange: (value: FullnameOption) => void; + readOnly: boolean; + inherit?: boolean; +}; diff --git a/src/features/congregation/settings/meeting_forms/name_format_select/useNameFormatOption.tsx b/src/features/congregation/settings/meeting_forms/name_format_select/useNameFormatOption.tsx new file mode 100644 index 00000000000..09dc97c3894 --- /dev/null +++ b/src/features/congregation/settings/meeting_forms/name_format_select/useNameFormatOption.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from 'react'; +import { useAtomValue } from 'jotai'; +import { settingsState, userDataViewState } from '@states/settings'; +import { dbAppSettingsUpdate } from '@services/dexie/settings'; +import { FullnameOption } from '@definition/settings'; + +const useNameFormatOption = ( + field: 'fullname_option' | 'print_fullname_option', + optionInitial: FullnameOption +) => { + const settings = useAtomValue(settingsState); + const dataView = useAtomValue(userDataViewState); + + const [option, setOption] = useState(FullnameOption.FIRST_BEFORE_LAST); + + const hasRecord = (settings.cong_settings[field] ?? []).some( + (record) => record.type === dataView && !record._deleted + ); + + const handleOptionChange = async (value: FullnameOption) => { + const records = structuredClone(settings.cong_settings[field] ?? []); + + const current = records.find((record) => record.type === dataView); + + if (current) { + current.value = value; + current.updatedAt = new Date().toISOString(); + current._deleted = false; + } + + if (!current) { + records.push({ + _deleted: false, + type: dataView, + updatedAt: new Date().toISOString(), + value, + }); + } + + const update = + field === 'fullname_option' + ? { 'cong_settings.fullname_option': records } + : { 'cong_settings.print_fullname_option': records }; + + await dbAppSettingsUpdate(update); + }; + + const handleOptionClear = async () => { + const records = structuredClone(settings.cong_settings[field] ?? []); + + const current = records.find( + (record) => record.type === dataView && !record._deleted + ); + + if (!current) return; + + current._deleted = true; + current.updatedAt = new Date().toISOString(); + + const update = + field === 'fullname_option' + ? { 'cong_settings.fullname_option': records } + : { 'cong_settings.print_fullname_option': records }; + + await dbAppSettingsUpdate(update); + }; + + useEffect(() => { + setOption(optionInitial); + }, [optionInitial]); + + return { option, hasRecord, handleOptionChange, handleOptionClear }; +}; + +export default useNameFormatOption; diff --git a/src/features/congregation/settings/meeting_forms/print_name_format/index.tsx b/src/features/congregation/settings/meeting_forms/print_name_format/index.tsx new file mode 100644 index 00000000000..ac4c0d40057 --- /dev/null +++ b/src/features/congregation/settings/meeting_forms/print_name_format/index.tsx @@ -0,0 +1,27 @@ +import { useAppTranslation, useCurrentUser } from '@hooks/index'; +import usePrintNameFormat from './usePrintNameFormat'; +import NameFormatSelect from '../name_format_select'; + +const PrintNameFormat = () => { + const { t } = useAppTranslation(); + + const { isMidweekEditor, isWeekendEditor, isPublicTalkCoordinator } = + useCurrentUser(); + + const { printFullnameOption, handlePrintFullnameOptionChange } = + usePrintNameFormat(); + + return ( + + ); +}; + +export default PrintNameFormat; diff --git a/src/features/congregation/settings/meeting_forms/print_name_format/usePrintNameFormat.tsx b/src/features/congregation/settings/meeting_forms/print_name_format/usePrintNameFormat.tsx new file mode 100644 index 00000000000..470a1cb8b9d --- /dev/null +++ b/src/features/congregation/settings/meeting_forms/print_name_format/usePrintNameFormat.tsx @@ -0,0 +1,31 @@ +import { useAtomValue } from 'jotai'; +import { printFullnameOptionState } from '@states/settings'; +import { FullnameOption } from '@definition/settings'; +import { NAME_FORMAT_INHERIT } from '../name_format_select/index.types'; +import useNameFormatOption from '../name_format_select/useNameFormatOption'; + +const usePrintNameFormat = () => { + const optionInitial = useAtomValue(printFullnameOptionState); + + const { option, hasRecord, handleOptionChange, handleOptionClear } = + useNameFormatOption('print_fullname_option', optionInitial); + + // while no explicit choice is stored, the select shows the inherit entry + const printFullnameOption = hasRecord ? option : NAME_FORMAT_INHERIT; + + const handlePrintFullnameOptionChange = async (value: FullnameOption) => { + if (value === NAME_FORMAT_INHERIT) { + await handleOptionClear(); + return; + } + + await handleOptionChange(value); + }; + + return { + printFullnameOption, + handlePrintFullnameOptionChange, + }; +}; + +export default usePrintNameFormat; diff --git a/src/features/dashboard/initial_setup/person_record/index.tsx b/src/features/dashboard/initial_setup/person_record/index.tsx index 8ee7ab92eb4..af7dc1420db 100644 --- a/src/features/dashboard/initial_setup/person_record/index.tsx +++ b/src/features/dashboard/initial_setup/person_record/index.tsx @@ -1,5 +1,5 @@ import { Box, Stack } from '@mui/material'; -import { useAppTranslation, useBreakpoints } from '@hooks/index'; +import { useAppTranslation } from '@hooks/index'; import { PersonRecordProps } from './index.types'; import usePersonRecord from './usePersonRecord'; import Button from '@components/button'; @@ -10,12 +10,12 @@ import IconLoading from '@components/icon_loading'; const PersonRecord = ({ onPrevious }: PersonRecordProps) => { const { t } = useAppTranslation(); - const { tabletDown } = useBreakpoints(); - const { handleSavePerson, firstname, handleFirstnameChange, + middlename, + handleMiddlenameChange, handleLastnameChange, lastname, isProcessing, @@ -31,9 +31,7 @@ const PersonRecord = ({ onPrevious }: PersonRecordProps) => { sx={{ display: 'flex', gap: '16px', - flexWrap: tabletDown ? 'wrap' : 'nowrap', - justifyContent: 'space-between', - flexDirection: 'row', + flexWrap: 'wrap', }} > { height={48} value={firstname} onChange={(e) => handleFirstnameChange(e.target.value)} + sx={{ flex: '1 0 200px' }} + /> + handleMiddlenameChange(e.target.value)} + sx={{ flex: '1 0 200px' }} /> handleLastnameChange(e.target.value)} + sx={{ flex: '1 0 200px' }} /> diff --git a/src/features/dashboard/initial_setup/person_record/usePersonRecord.tsx b/src/features/dashboard/initial_setup/person_record/usePersonRecord.tsx index 2231639bc7d..0ea583dc874 100644 --- a/src/features/dashboard/initial_setup/person_record/usePersonRecord.tsx +++ b/src/features/dashboard/initial_setup/person_record/usePersonRecord.tsx @@ -17,12 +17,17 @@ const usePersonRecord = () => { const [firstname, setFirstname] = useState( settings.user_settings.firstname.value ); + const [middlename, setMiddlename] = useState( + settings.user_settings.middlename?.value || '' + ); const [lastname, setLastname] = useState( settings.user_settings.lastname.value ); const handleFirstnameChange = (value: string) => setFirstname(value); + const handleMiddlenameChange = (value: string) => setMiddlename(value); + const handleLastnameChange = (value: string) => setLastname(value); const handleSavePerson = async () => { @@ -44,6 +49,11 @@ const usePersonRecord = () => { updatedAt: new Date().toISOString(), }; + person.person_data.person_middlename = { + value: middlename, + updatedAt: new Date().toISOString(), + }; + person.person_data.person_lastname = { value: lastname, updatedAt: new Date().toISOString(), @@ -80,8 +90,10 @@ const usePersonRecord = () => { return { handleSavePerson, firstname, + middlename, lastname, handleFirstnameChange, + handleMiddlenameChange, handleLastnameChange, isProcessing, }; diff --git a/src/features/language_switcher/useLanguage.tsx b/src/features/language_switcher/useLanguage.tsx index 21c9cf151e3..fa09f3d52be 100644 --- a/src/features/language_switcher/useLanguage.tsx +++ b/src/features/language_switcher/useLanguage.tsx @@ -58,12 +58,40 @@ const useLanguage = () => { (record) => record.threeLettersCode === ui_lang ); - const fullnameOption = + const languageOption = findLanguage?.fullnameOption || FullnameOption.FIRST_BEFORE_LAST; const nameOption = structuredClone(settings.cong_settings.fullname_option); const current = nameOption.find((record) => record.type === dataView); + // the language decides whether the first or the last name comes first, + // but it must not silently drop the middle name or the sorted-list comma + // the congregation asked for; the comma only exists in last-first shape + const keepsMiddlename = + current?.value === FullnameOption.FIRST_MIDDLE_LAST || + current?.value === FullnameOption.LAST_FIRST_MIDDLE || + current?.value === FullnameOption.LAST_COMMA_FIRST_MIDDLE; + + const keepsComma = + current?.value === FullnameOption.LAST_COMMA_FIRST || + current?.value === FullnameOption.LAST_COMMA_FIRST_MIDDLE; + + let fullnameOption: FullnameOption; + + if (languageOption === FullnameOption.FIRST_BEFORE_LAST) { + fullnameOption = keepsMiddlename + ? FullnameOption.FIRST_MIDDLE_LAST + : FullnameOption.FIRST_BEFORE_LAST; + } else if (keepsComma) { + fullnameOption = keepsMiddlename + ? FullnameOption.LAST_COMMA_FIRST_MIDDLE + : FullnameOption.LAST_COMMA_FIRST; + } else { + fullnameOption = keepsMiddlename + ? FullnameOption.LAST_FIRST_MIDDLE + : FullnameOption.LAST_BEFORE_FIRST; + } + if (current) { current.value = fullnameOption; current.updatedAt = new Date().toISOString(); diff --git a/src/features/meetings/my_assignments/assignment_item/useAssignmentItem.tsx b/src/features/meetings/my_assignments/assignment_item/useAssignmentItem.tsx index 9ee165b699a..0bbaeccaff2 100644 --- a/src/features/meetings/my_assignments/assignment_item/useAssignmentItem.tsx +++ b/src/features/meetings/my_assignments/assignment_item/useAssignmentItem.tsx @@ -95,7 +95,8 @@ const useAssignmentItem = ({ history }: AssignmentItemProps) => { const name = buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); return name; diff --git a/src/features/ministry/ap_application/main_form/useMainForm.tsx b/src/features/ministry/ap_application/main_form/useMainForm.tsx index dd37119ef64..77f9e1a98f0 100644 --- a/src/features/ministry/ap_application/main_form/useMainForm.tsx +++ b/src/features/ministry/ap_application/main_form/useMainForm.tsx @@ -67,7 +67,8 @@ const useMainForm = () => { const fullname = buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); setFormData((prev) => { diff --git a/src/features/ministry/application_form/body/useFormBody.tsx b/src/features/ministry/application_form/body/useFormBody.tsx index 6872fd42f3e..d9c7f145afd 100644 --- a/src/features/ministry/application_form/body/useFormBody.tsx +++ b/src/features/ministry/application_form/body/useFormBody.tsx @@ -61,7 +61,8 @@ const useFormBody = ({ application, onChange }: ApplicationFormProps) => { return buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); }, [settings, persons, fullnameOption]); @@ -74,7 +75,8 @@ const useFormBody = ({ application, onChange }: ApplicationFormProps) => { return buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); }, [settings, persons, fullnameOption]); @@ -87,7 +89,8 @@ const useFormBody = ({ application, onChange }: ApplicationFormProps) => { return buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); }, [settings, persons, fullnameOption]); diff --git a/src/features/ministry/delegate_reports/report_item/useReportItem.tsx b/src/features/ministry/delegate_reports/report_item/useReportItem.tsx index 3da09b37da3..3029d294981 100644 --- a/src/features/ministry/delegate_reports/report_item/useReportItem.tsx +++ b/src/features/ministry/delegate_reports/report_item/useReportItem.tsx @@ -15,7 +15,8 @@ const useReportItem = (person: PersonType) => { return buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); }, [fullnameOption, person]); diff --git a/src/features/my_profile/user_profile_details/index.tsx b/src/features/my_profile/user_profile_details/index.tsx index f66005a681e..f97cf922ae2 100644 --- a/src/features/my_profile/user_profile_details/index.tsx +++ b/src/features/my_profile/user_profile_details/index.tsx @@ -12,9 +12,10 @@ const UserProfileDetails = () => { const { accountType } = useCurrentUser(); const { - tabletDown, firstNameTmp, handleChangeFirstName, + middleNameTmp, + handleChangeMiddleName, handleChangeLastName, lastNameTmp, userEmail, @@ -35,9 +36,7 @@ const UserProfileDetails = () => { sx={{ display: 'flex', gap: '16px', - flexWrap: tabletDown ? 'wrap' : 'nowrap', - justifyContent: 'space-between', - flexDirection: 'row', + flexWrap: 'wrap', }} > { height={48} value={firstNameTmp} onChange={(e) => handleChangeFirstName(e.target.value)} + sx={{ flex: '1 0 200px' }} + /> + handleChangeMiddleName(e.target.value)} + sx={{ flex: '1 0 200px' }} /> handleChangeLastName(e.target.value)} + sx={{ flex: '1 0 200px' }} /> {isConnected && accountType === 'vip' && ( diff --git a/src/features/my_profile/user_profile_details/useUserProfileDetails.tsx b/src/features/my_profile/user_profile_details/useUserProfileDetails.tsx index e6ffb99409f..0db10075846 100644 --- a/src/features/my_profile/user_profile_details/useUserProfileDetails.tsx +++ b/src/features/my_profile/user_profile_details/useUserProfileDetails.tsx @@ -1,23 +1,26 @@ import { useState } from 'react'; import { useAtomValue } from 'jotai'; -import { firstnameState, lastnameState } from '@states/settings'; -import useBreakpoints from '@hooks/useBreakpoints'; +import { + firstnameState, + lastnameState, + middlenameState, +} from '@states/settings'; import useFirebaseAuth from '@hooks/useFirebaseAuth'; import { dbAppSettingsUpdate } from '@services/dexie/settings'; import { congAccountConnectedState } from '@states/app'; const useUserProfileDetails = () => { - const { tabletDown } = useBreakpoints(); - const { user } = useFirebaseAuth(); const userEmail = user?.email || ''; const firstName = useAtomValue(firstnameState); + const middleName = useAtomValue(middlenameState); const lastName = useAtomValue(lastnameState); const isConnected = useAtomValue(congAccountConnectedState); const [firstNameTmp, setFirstNameTmp] = useState(firstName); + const [middleNameTmp, setMiddleNameTmp] = useState(middleName); const [lastNameTmp, setLastNameTmp] = useState(lastName); const handleChangeFirstName = async (value) => { @@ -28,6 +31,17 @@ const useUserProfileDetails = () => { }); }; + const handleChangeMiddleName = async (value: string) => { + setMiddleNameTmp(value); + + await dbAppSettingsUpdate({ + 'user_settings.middlename': { + value, + updatedAt: new Date().toISOString(), + }, + }); + }; + const handleChangeLastName = async (value) => { setLastNameTmp(value); @@ -37,10 +51,11 @@ const useUserProfileDetails = () => { }; return { - tabletDown, firstNameTmp, + middleNameTmp, lastNameTmp, handleChangeFirstName, + handleChangeMiddleName, handleChangeLastName, userEmail, isConnected, diff --git a/src/features/persons/application_person/useApplicationPerson.tsx b/src/features/persons/application_person/useApplicationPerson.tsx index affd6ca85f9..7b4494f29ad 100644 --- a/src/features/persons/application_person/useApplicationPerson.tsx +++ b/src/features/persons/application_person/useApplicationPerson.tsx @@ -54,7 +54,8 @@ const useApplicationPerson = () => { return buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); }, [application, persons, fullnameOption]); diff --git a/src/features/persons/applications/application/useApplication.tsx b/src/features/persons/applications/application/useApplication.tsx index dd4ba07e357..a33848e0f7a 100644 --- a/src/features/persons/applications/application/useApplication.tsx +++ b/src/features/persons/applications/application/useApplication.tsx @@ -29,7 +29,8 @@ const useApplication = ({ application }: ApplicationProps) => { return buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); }, [person, fullnameOption]); diff --git a/src/features/persons/basic_info/index.tsx b/src/features/persons/basic_info/index.tsx index 8587f86c0b2..f5aa6c1ed21 100644 --- a/src/features/persons/basic_info/index.tsx +++ b/src/features/persons/basic_info/index.tsx @@ -25,18 +25,18 @@ const PersonBasicInfo = () => { age, handleToggleGender, handleChangeFirstname, + handleChangeMiddlename, handleChangeLastname, handleChangeDisplayName, handleChangeAddress, handleChangeEmailAddress, handleChangePhone, ageToYearsAndMonths, - nameFlex, isInactive, displayNameEnabled, isCurrentPersonMemberOfAFamily, familyHeadName, - isFamilyHead + isFamilyHead, } = useBasicInfo(); return ( @@ -130,8 +130,7 @@ const PersonBasicInfo = () => { display: 'flex', gap: '16px', alignItems: 'center', - justifyContent: 'space-between', - flexDirection: nameFlex, + flexWrap: 'wrap', }} > { value={person.person_data.person_firstname.value} onChange={(e) => handleChangeFirstname(e.target.value)} slotProps={{ input: { readOnly: !isPersonEditor } }} + sx={{ flex: '1 0 200px' }} + /> + handleChangeMiddlename(e.target.value)} + slotProps={{ input: { readOnly: !isPersonEditor } }} + sx={{ flex: '1 0 200px' }} /> handleChangeLastname(e.target.value)} slotProps={{ input: { readOnly: !isPersonEditor } }} + sx={{ flex: '1 0 200px' }} /> {displayNameEnabled && ( diff --git a/src/features/persons/basic_info/useBasicInfo.tsx b/src/features/persons/basic_info/useBasicInfo.tsx index 0d5da664788..470f5a6710b 100644 --- a/src/features/persons/basic_info/useBasicInfo.tsx +++ b/src/features/persons/basic_info/useBasicInfo.tsx @@ -1,11 +1,10 @@ import { useEffect, useMemo, useState } from 'react'; import { useAtomValue } from 'jotai'; -import { useBreakpoints } from '@hooks/index'; import { setPersonCurrentDetails } from '@services/states/persons'; import { personCurrentDetailsState, personsActiveState } from '@states/persons'; import { computeYearsDiff } from '@utils/date'; import { buildPersonFullname, generateDisplayName } from '@utils/common'; -import { appLangState } from '@states/app'; +import { changeMiddlename } from '@utils/person'; import { displayNameMeetingsEnableState, fullnameOptionState, @@ -15,16 +14,10 @@ import useFamilyMembers from '../family_members/useFamilyMembers'; const useBasicInfo = () => { const person = useAtomValue(personCurrentDetailsState); - const appLang = useAtomValue(appLangState); const displayNameEnabled = useAtomValue(displayNameMeetingsEnableState); - const { tabletDown } = useBreakpoints(); - const [isInactive, setIsInactive] = useState(false); const [age, setAge] = useState('0'); - const [nameFlex, setNameFlex] = useState< - 'row' | 'row-reverse' | 'column' | 'column-reverse' - >('row'); const personsActive = useAtomValue(personsActiveState); const fullnameOption = useAtomValue(fullnameOptionState); const { isFamilyHead, familyHeadName, isCurrentPersonMemberOfAFamily } = @@ -39,7 +32,8 @@ const useBasicInfo = () => { person_name: buildPersonFullname( p.person_data.person_lastname.value, p.person_data.person_firstname.value, - fullnameOption + fullnameOption, + p.person_data.person_middlename?.value ), }; }); @@ -68,6 +62,14 @@ const useBasicInfo = () => { setPersonCurrentDetails(newPerson); }; + const handleChangeMiddlename = async (value: string) => { + const newPerson = structuredClone(person); + + changeMiddlename(newPerson, value); + + setPersonCurrentDetails(newPerson); + }; + const handleChangeLastname = async (value: string) => { const newPerson = structuredClone(person); @@ -183,24 +185,6 @@ const useBasicInfo = () => { } }, [person.person_data.birth_date.value]); - useEffect(() => { - if (tabletDown) { - if (appLang === 'mg-MG') { - setNameFlex('column-reverse'); - } else { - setNameFlex('column'); - } - } - - if (!tabletDown) { - if (appLang === 'mg-MG') { - setNameFlex('row-reverse'); - } else { - setNameFlex('row'); - } - } - }, [tabletDown, appLang]); - useEffect(() => { if (person.person_data.publisher_baptized.active.value) { const isActive = @@ -229,13 +213,13 @@ const useBasicInfo = () => { age, handleToggleGender, handleChangeFirstname, + handleChangeMiddlename, handleChangeLastname, handleChangeDisplayName, handleChangeEmailAddress, handleChangePhone, handleChangeAddress, ageToYearsAndMonths, - nameFlex, isInactive, displayNameEnabled, persons, diff --git a/src/features/persons/family_members/useFamilyMembers.ts b/src/features/persons/family_members/useFamilyMembers.ts index d27969b1f01..60bd3570b02 100644 --- a/src/features/persons/family_members/useFamilyMembers.ts +++ b/src/features/persons/family_members/useFamilyMembers.ts @@ -49,7 +49,8 @@ const useFamilyMembers = () => { return buildPersonFullname( currentPerson.person_data?.person_lastname.value, currentPerson.person_data?.person_firstname.value, - fullnameOption + fullnameOption, + currentPerson.person_data?.person_middlename?.value ); } else { if (!currentFamily) { @@ -59,12 +60,14 @@ const useFamilyMembers = () => { return buildPersonFullname( currentFamily.person_data?.person_lastname.value, currentFamily.person_data?.person_firstname.value, - fullnameOption + fullnameOption, + currentFamily.person_data?.person_middlename?.value ); } }, [ currentFamily, currentPerson.person_data?.person_firstname.value, + currentPerson.person_data?.person_middlename?.value, currentPerson.person_data?.person_lastname.value, fullnameOption, isFamilyHead, @@ -103,7 +106,8 @@ const useFamilyMembers = () => { person_name: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }), diff --git a/src/features/persons/hooks/usePerson.tsx b/src/features/persons/hooks/usePerson.tsx index f35a77b1d7b..67d219837e6 100644 --- a/src/features/persons/hooks/usePerson.tsx +++ b/src/features/persons/hooks/usePerson.tsx @@ -15,7 +15,8 @@ const usePerson = () => { return buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); }; diff --git a/src/features/persons/import_export/confirm_import/usePersonsImportConfig.ts b/src/features/persons/import_export/confirm_import/usePersonsImportConfig.ts index 48baab58faf..950121a31b0 100644 --- a/src/features/persons/import_export/confirm_import/usePersonsImportConfig.ts +++ b/src/features/persons/import_export/confirm_import/usePersonsImportConfig.ts @@ -7,6 +7,7 @@ import { AssignmentCode } from '@definition/assignment'; import { ASSIGNMENT_SECTIONS } from '@features/persons/assignments/assignmentStructure'; import { changeFirstname, + changeMiddlename, changeLastname, changeBirthDate, changeEmailAddress, @@ -72,6 +73,14 @@ const usePersonsImportConfig = () => { examples: ['Mike', 'Andry', 'Luana', 'Ava'], handler: (p, v) => changeFirstname(p, v), }, + { + key: 'personalInfo.middlename', + label: 'tr_middlename', + group: 'personalInfo', + groupLabel: 'tr_personalInfo', + examples: ['Robert', 'Herizo', 'Maria', 'Grace'], + handler: (p, v) => changeMiddlename(p, v), + }, { key: 'personalInfo.lastname', label: 'tr_lastname', diff --git a/src/features/persons/import_export/export/useExportPersons.tsx b/src/features/persons/import_export/export/useExportPersons.tsx index da9aeaef61f..f81180193a0 100644 --- a/src/features/persons/import_export/export/useExportPersons.tsx +++ b/src/features/persons/import_export/export/useExportPersons.tsx @@ -22,8 +22,8 @@ const useExportPersons = () => { const personGetGroup = (person_uid: string) => { return groups.find((group) => group.group_data.members.some( - (member) => member.person_uid === person_uid, - ), + (member) => member.person_uid === person_uid + ) ); }; @@ -37,6 +37,7 @@ const useExportPersons = () => { const header_row: Row = [ { value: t('tr_lastname', { lng }), fontWeight: 'bold' }, { value: t('tr_firstname', { lng }), fontWeight: 'bold' }, + { value: t('tr_middlename', { lng }), fontWeight: 'bold' }, { value: t('tr_phoneNumber', { lng }), fontWeight: 'bold' }, { value: t('tr_address', { lng }), fontWeight: 'bold' }, { value: t('tr_emergencyContacts', { lng }), fontWeight: 'bold' }, @@ -49,7 +50,7 @@ const useExportPersons = () => { .filter( (person) => person.person_data.publisher_baptized.active.value || - person.person_data.publisher_unbaptized.active.value, + person.person_data.publisher_unbaptized.active.value ) .map((person) => { const group = personGetGroup(person.person_uid); @@ -80,6 +81,7 @@ const useExportPersons = () => { return [ { value: person.person_data.person_lastname.value }, { value: person.person_data.person_firstname.value }, + { value: person.person_data.person_middlename?.value || '' }, { value: person.person_data.phone.value, type: String }, { value: person.person_data.address.value }, { value: emergencyContacts.join('; ') }, @@ -95,6 +97,7 @@ const useExportPersons = () => { columns: [ { width: 30 }, { width: 35 }, + { width: 35 }, { width: 45 }, { width: 45 }, { width: 45 }, @@ -112,7 +115,7 @@ const useExportPersons = () => { header: getMessageByCode('error_app_generic-title'), message: error.message, severity: 'error', - icon: , + icon: , }); } }; diff --git a/src/features/persons/list/person_card/index.tsx b/src/features/persons/list/person_card/index.tsx index e78de036a0e..26149344dbd 100644 --- a/src/features/persons/list/person_card/index.tsx +++ b/src/features/persons/list/person_card/index.tsx @@ -44,7 +44,8 @@ const PersonCard = ({ person }: PersonCardType) => { name={buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value )} female={person.person_data.female.value} onDelete={isPersonEditor ? handleDelete : null} diff --git a/src/features/persons/speakers_catalog/my_congregation/speaker_edit/index.tsx b/src/features/persons/speakers_catalog/my_congregation/speaker_edit/index.tsx index d47e49795c5..0bdb1003bbe 100644 --- a/src/features/persons/speakers_catalog/my_congregation/speaker_edit/index.tsx +++ b/src/features/persons/speakers_catalog/my_congregation/speaker_edit/index.tsx @@ -100,7 +100,8 @@ const SpeakerEditView = (props: SpeakerEditViewType) => { {buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value )} diff --git a/src/features/persons/speakers_catalog/other_congregations/speakers_list/edit/index.tsx b/src/features/persons/speakers_catalog/other_congregations/speakers_list/edit/index.tsx index ff9092fa41b..22a345539fb 100644 --- a/src/features/persons/speakers_catalog/other_congregations/speakers_list/edit/index.tsx +++ b/src/features/persons/speakers_catalog/other_congregations/speakers_list/edit/index.tsx @@ -1,7 +1,7 @@ import { Box, FormControlLabel, RadioGroup } from '@mui/material'; import { IconDelete, IconMale, IconSong } from '@components/icons'; import { IncomingSpeakerEditType } from './index.types'; -import { FullnameOption } from '@definition/settings'; + import { PublicTalkType } from '@definition/public_talks'; import { useAppTranslation, useBreakpoints } from '@hooks/index'; import useEdit from './useEdit'; @@ -20,10 +20,11 @@ const IncomingSpeakerEdit = ({ speaker }: IncomingSpeakerEditType) => { const { tabletDown } = useBreakpoints(); const { - fullnameOption, displayNameEnabled, handleFirstnameChange, firstname, + handleMiddlenameChange, + middlename, handleLastnameChange, lastname, handleToggleGender, @@ -80,37 +81,30 @@ const IncomingSpeakerEdit = ({ speaker }: IncomingSpeakerEditType) => { display: 'flex', gap: '16px', alignItems: 'center', - justifyContent: 'space-between', - flexDirection: tabletDown - ? fullnameOption === FullnameOption.FIRST_BEFORE_LAST - ? 'column' - : 'column-reverse' - : fullnameOption === FullnameOption.FIRST_BEFORE_LAST - ? 'row' - : 'row-reverse', + flexWrap: 'wrap', }} > - ) : null - } + startIcon={} value={firstname} onChange={(e) => handleFirstnameChange(e.target.value)} + sx={{ flex: '1 0 200px' }} + /> + handleMiddlenameChange(e.target.value)} + sx={{ flex: '1 0 200px' }} /> - ) : null - } value={lastname} onChange={(e) => handleLastnameChange(e.target.value)} + sx={{ flex: '1 0 200px' }} /> diff --git a/src/features/persons/speakers_catalog/other_congregations/speakers_list/edit/useEdit.tsx b/src/features/persons/speakers_catalog/other_congregations/speakers_list/edit/useEdit.tsx index dadc83afcf0..928db8014ea 100644 --- a/src/features/persons/speakers_catalog/other_congregations/speakers_list/edit/useEdit.tsx +++ b/src/features/persons/speakers_catalog/other_congregations/speakers_list/edit/useEdit.tsx @@ -1,9 +1,6 @@ import { useState } from 'react'; import { useAtomValue } from 'jotai'; -import { - displayNameMeetingsEnableState, - fullnameOptionState, -} from '@states/settings'; +import { displayNameMeetingsEnableState } from '@states/settings'; import { VisitingSpeakerType } from '@definition/visiting_speakers'; import { dbVisitingSpeakersDelete, @@ -15,13 +12,15 @@ import { PublicTalkType } from '@definition/public_talks'; import { SongType } from '@definition/songs'; const useEdit = (speaker: VisitingSpeakerType) => { - const fullnameOption = useAtomValue(fullnameOptionState); const displayNameEnabled = useAtomValue(displayNameMeetingsEnableState); const publicTalks = useAtomValue(publicTalksLocaleState); const [firstname, setFirstname] = useState( speaker.speaker_data.person_firstname.value ); + const [middlename, setMiddlename] = useState( + speaker.speaker_data.person_middlename?.value ?? '' + ); const [lastname, setLastname] = useState( speaker.speaker_data.person_lastname.value ); @@ -53,6 +52,20 @@ const useEdit = (speaker: VisitingSpeakerType) => { return talk; }) || []; + const handleMiddlenameChange = async (value: string) => { + setMiddlename(value); + + await dbVisitingSpeakersUpdate( + { + 'speaker_data.person_middlename': { + value: value, + updatedAt: new Date().toISOString(), + }, + }, + speaker.person_uid + ); + }; + const handleFirstnameChange = async (value: string) => { setFirstname(value); @@ -308,10 +321,11 @@ const useEdit = (speaker: VisitingSpeakerType) => { }; return { - fullnameOption, displayNameEnabled, handleFirstnameChange, firstname, + handleMiddlenameChange, + middlename, handleLastnameChange, lastname, handleToggleGender, diff --git a/src/features/persons/speakers_catalog/speaker_details/useSpeakerDetails.tsx b/src/features/persons/speakers_catalog/speaker_details/useSpeakerDetails.tsx index 5c79fb2d0f7..d0e6870d747 100644 --- a/src/features/persons/speakers_catalog/speaker_details/useSpeakerDetails.tsx +++ b/src/features/persons/speakers_catalog/speaker_details/useSpeakerDetails.tsx @@ -17,7 +17,8 @@ const useSpeakerDetails = (speaker: VisitingSpeakerType) => { const personName = buildPersonFullname( speaker.speaker_data.person_lastname.value, speaker.speaker_data.person_firstname.value, - fullnameOption + fullnameOption, + speaker.speaker_data.person_middlename?.value ); const congId = congregations.find( diff --git a/src/features/persons/speakers_catalog/speaker_row_view/index.tsx b/src/features/persons/speakers_catalog/speaker_row_view/index.tsx index 948c7d5ff8a..c2edbb133c3 100644 --- a/src/features/persons/speakers_catalog/speaker_row_view/index.tsx +++ b/src/features/persons/speakers_catalog/speaker_row_view/index.tsx @@ -76,7 +76,8 @@ const SpeakerRowView = ({ speaker }: SpeakerReadOnlyViewType) => { {buildPersonFullname( speaker.speaker_data.person_lastname.value, speaker.speaker_data.person_firstname.value, - fullnameOption + fullnameOption, + speaker.speaker_data.person_middlename?.value )} {speaker.speaker_data.person_notes.value.length > 0 && ( diff --git a/src/features/persons/spiritual_status/baptized_publisher/useBaptizedPublisher.tsx b/src/features/persons/spiritual_status/baptized_publisher/useBaptizedPublisher.tsx index 446d9395a8c..a8a3a1f0409 100644 --- a/src/features/persons/spiritual_status/baptized_publisher/useBaptizedPublisher.tsx +++ b/src/features/persons/spiritual_status/baptized_publisher/useBaptizedPublisher.tsx @@ -72,7 +72,8 @@ const useBaptizedPublisher = () => { const name = buildPersonFullname( findPerson.person_data.person_lastname.value, findPerson.person_data.person_firstname.value, - fullnameOption + fullnameOption, + findPerson.person_data.person_middlename?.value ); return t('tr_groupWithOverseerName', { name }); diff --git a/src/features/persons/spiritual_status/midweek_meeting_student/useMidweekMeetingStudent.tsx b/src/features/persons/spiritual_status/midweek_meeting_student/useMidweekMeetingStudent.tsx index 379695310ec..fcb856c7905 100644 --- a/src/features/persons/spiritual_status/midweek_meeting_student/useMidweekMeetingStudent.tsx +++ b/src/features/persons/spiritual_status/midweek_meeting_student/useMidweekMeetingStudent.tsx @@ -63,7 +63,8 @@ const useMidweekMeetingStudent = () => { const name = buildPersonFullname( findPerson.person_data.person_lastname.value, findPerson.person_data.person_firstname.value, - fullnameOption + fullnameOption, + findPerson.person_data.person_middlename?.value ); return t('tr_groupWithOverseerName', { name }); diff --git a/src/features/persons/spiritual_status/unbaptized_publisher/useUnbaptizedPublisher.tsx b/src/features/persons/spiritual_status/unbaptized_publisher/useUnbaptizedPublisher.tsx index d80872f720a..26b87e8a9b0 100644 --- a/src/features/persons/spiritual_status/unbaptized_publisher/useUnbaptizedPublisher.tsx +++ b/src/features/persons/spiritual_status/unbaptized_publisher/useUnbaptizedPublisher.tsx @@ -71,7 +71,8 @@ const useUnbaptizedPublisher = () => { const name = buildPersonFullname( findPerson.person_data.person_lastname.value, findPerson.person_data.person_firstname.value, - fullnameOption + fullnameOption, + findPerson.person_data.person_middlename?.value ); return t('tr_groupWithOverseerName', { name }); diff --git a/src/features/reports/hooks/usePublisherCard.tsx b/src/features/reports/hooks/usePublisherCard.tsx index ddb0f2b2e24..30dd5a79a4c 100644 --- a/src/features/reports/hooks/usePublisherCard.tsx +++ b/src/features/reports/hooks/usePublisherCard.tsx @@ -11,7 +11,7 @@ import { personsState } from '@states/persons'; import { PersonType } from '@definition/person'; import { congFieldServiceReportsState } from '@states/field_service_reports'; import { - fullnameOptionState, + printFullnameOptionState, JWLangLocaleState, JWLangState, shortDateFormatState, @@ -33,7 +33,7 @@ const usePublisherCard = () => { const persons = useAtomValue(personsState); const reports = useAtomValue(congFieldServiceReportsState); const dateFormat = useAtomValue(shortDateFormatState); - const fullnameOption = useAtomValue(fullnameOptionState); + const fullnameOption = useAtomValue(printFullnameOptionState); const sourceLocale = useAtomValue(JWLangLocaleState); const years = useMemo(() => { @@ -77,7 +77,8 @@ const usePublisherCard = () => { card.name = buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); const isElder = personIsPrivilegeYearActive(person, 'elder', year); diff --git a/src/features/reports/publisher_records/export_S21/specific_records/active_publishers/useActivePublishers.tsx b/src/features/reports/publisher_records/export_S21/specific_records/active_publishers/useActivePublishers.tsx index 61b40261741..a4db2bb54fe 100644 --- a/src/features/reports/publisher_records/export_S21/specific_records/active_publishers/useActivePublishers.tsx +++ b/src/features/reports/publisher_records/export_S21/specific_records/active_publishers/useActivePublishers.tsx @@ -49,7 +49,8 @@ const useActivePublishers = ({ onExport }: ActivePublishersProps) => { label: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }) @@ -69,7 +70,8 @@ const useActivePublishers = ({ onExport }: ActivePublishersProps) => { label: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }), @@ -86,7 +88,8 @@ const useActivePublishers = ({ onExport }: ActivePublishersProps) => { label: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }), diff --git a/src/features/reports/publisher_records/export_S21/specific_records/field_service_groups/useFieldServiceGroups.tsx b/src/features/reports/publisher_records/export_S21/specific_records/field_service_groups/useFieldServiceGroups.tsx index bab020f963f..bc8a5776eac 100644 --- a/src/features/reports/publisher_records/export_S21/specific_records/field_service_groups/useFieldServiceGroups.tsx +++ b/src/features/reports/publisher_records/export_S21/specific_records/field_service_groups/useFieldServiceGroups.tsx @@ -77,7 +77,8 @@ const useFieldServiceGroups = ({ onExport }: FieldServiceGroupsProps) => { label: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }) diff --git a/src/features/reports/publisher_records/export_S21/specific_records/inactive_publishers/useInactivePublishers.tsx b/src/features/reports/publisher_records/export_S21/specific_records/inactive_publishers/useInactivePublishers.tsx index 26361c7b0b0..8056f69a761 100644 --- a/src/features/reports/publisher_records/export_S21/specific_records/inactive_publishers/useInactivePublishers.tsx +++ b/src/features/reports/publisher_records/export_S21/specific_records/inactive_publishers/useInactivePublishers.tsx @@ -41,7 +41,8 @@ const useInactivePublishers = ({ onExport }: InactivePublishersProps) => { label: buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ), }; }) diff --git a/src/locales/en/congregation.json b/src/locales/en/congregation.json index 0913da93a82..04d21c119e0 100644 --- a/src/locales/en/congregation.json +++ b/src/locales/en/congregation.json @@ -389,6 +389,17 @@ "tr_everyMonth": "Every month", "tr_autoCheckFrequency": "Auto-check frequency", "tr_nameFormat": "Name format", + "tr_printNameFormat": "Name format for printed forms", + "tr_nameFormatSameAsApp": "Same as in-app format", + "tr_nameFormatFirstLast": "First Last", + "tr_nameFormatLastFirst": "Last First", + "tr_nameFormatFirstMiddleLast": "First Middle Last", + "tr_nameFormatLastFirstMiddle": "Last First Middle", + "tr_nameFormatLastCommaFirst": "Last, First", + "tr_nameFormatLastCommaFirstMiddle": "Last, First Middle", + "tr_nameFormatSampleFirstname": "William", + "tr_nameFormatSampleMiddlename": "Martin", + "tr_nameFormatSampleLastname": "Gonzalez", "tr_formatFirstLast": "First Last (William Gonzalez)", "tr_formatLastFirst": "Last First (Gonzalez William)", "tr_baptizedPublishers": "Baptized publishers", diff --git a/src/locales/en/onboarding.json b/src/locales/en/onboarding.json index 117bba27c88..9ecc3fa478e 100644 --- a/src/locales/en/onboarding.json +++ b/src/locales/en/onboarding.json @@ -42,6 +42,7 @@ "tr_yourCongregation": "Your congregation", "tr_incompleteCongregationInfo": "Please make sure you’ve provided all needed details to set up your congregation", "tr_firstname": "First name", + "tr_middlename": "Middle name", "tr_lastname": "Last name", "tr_gender": "Gender", "tr_encryptionCode": "Encryption code", diff --git a/src/pages/persons/application_details/useApplicationDetails.tsx b/src/pages/persons/application_details/useApplicationDetails.tsx index 966eb397eac..287b5931bbb 100644 --- a/src/pages/persons/application_details/useApplicationDetails.tsx +++ b/src/pages/persons/application_details/useApplicationDetails.tsx @@ -28,7 +28,8 @@ const useApplicationDetails = () => { return buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); }, [application, persons, fullnameOption]); diff --git a/src/services/app/persons.ts b/src/services/app/persons.ts index f2f266cc9ae..6edeb7e148f 100644 --- a/src/services/app/persons.ts +++ b/src/services/app/persons.ts @@ -5,11 +5,7 @@ import { PrivilegeType, TimeAwayType, } from '@definition/person'; -import { - displayNameMeetingsEnableState, - fullnameOptionState, - userDataViewState, -} from '@states/settings'; +import { fullnameOptionState, userDataViewState } from '@states/settings'; import { buildPersonFullname } from '@utils/common'; import { addDays, @@ -439,6 +435,9 @@ export const applyNameFilters = ({ const foundFirstName = person.person_data.person_firstname.value .toLowerCase() .includes(searchKey.toLowerCase()); + const foundMiddleName = (person.person_data.person_middlename?.value || '') + .toLowerCase() + .includes(searchKey.toLowerCase()); const foundLastName = person.person_data.person_lastname.value .toLowerCase() .includes(searchKey.toLowerCase()); @@ -446,7 +445,12 @@ export const applyNameFilters = ({ .toLowerCase() .includes(searchKey.toLowerCase()); - if (foundFirstName || foundLastName || foundDisplayName) { + if ( + foundFirstName || + foundMiddleName || + foundLastName || + foundDisplayName + ) { filteredByName.push(person); } } @@ -725,12 +729,14 @@ export const personsSortByName = (persons: PersonType[]) => { const fullnameA = buildPersonFullname( a.person_data.person_lastname.value, a.person_data.person_firstname.value, - fullnameOption + fullnameOption, + a.person_data.person_middlename?.value ); const fullnameB = buildPersonFullname( b.person_data.person_lastname.value, b.person_data.person_firstname.value, - fullnameOption + fullnameOption, + b.person_data.person_middlename?.value ); return fullnameA.localeCompare(fullnameB, undefined, { @@ -890,26 +896,6 @@ export const personsFilterActiveTimeAway = (records: TimeAwayType[]) => { }); }; -export const personGetScheduleName = (person: PersonType) => { - const useDisplayName = store.get(displayNameMeetingsEnableState); - - const fullnameOption = store.get(fullnameOptionState); - const firstName = person.person_data.person_firstname.value; - const lastName = person.person_data.person_lastname.value; - - let result = ''; - - if (useDisplayName) { - result = person.person_data.person_display_name.value; - } - - if (!useDisplayName) { - result = buildPersonFullname(lastName, firstName, fullnameOption); - } - - return result; -}; - export const personIsAway = (person: PersonType, date: string) => { const timeAwaysActive = person.person_data.timeAway diff --git a/src/services/app/schedules.ts b/src/services/app/schedules.ts index fa017c544ed..ff60b1f0890 100644 --- a/src/services/app/schedules.ts +++ b/src/services/app/schedules.ts @@ -1,11 +1,10 @@ import { UpdateSpec } from 'dexie'; import { store } from '@states/index'; import { - CODisplayNameState, COFullnameState, - COScheduleNameState, + COPrintScheduleNameState, displayNameMeetingsEnableState, - fullnameOptionState, + printFullnameOptionState, midweekMeetingAuxCounselorDefaultEnabledState, midweekMeetingAuxCounselorDefaultState, midweekMeetingClassCountState, @@ -805,12 +804,13 @@ export const schedulesWeekGetAssigned = ({ } if (!useDisplayName) { - const fullnameOption = store.get(fullnameOptionState); + const fullnameOption = store.get(printFullnameOptionState); result = buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); } } @@ -2104,7 +2104,7 @@ export const schedulesWeekNoMeeting = (week: Week) => { }; export const schedulesS89Data = (schedule: SchedWeekType, dataView: string) => { - const fullnameOption = store.get(fullnameOptionState); + const fullnameOption = store.get(printFullnameOptionState); const result: S89DataType[] = []; @@ -2168,7 +2168,8 @@ export const schedulesS89Data = (schedule: SchedWeekType, dataView: string) => { obj.student_name = buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); if (assignment.includes('AYFPart')) { @@ -2186,7 +2187,8 @@ export const schedulesS89Data = (schedule: SchedWeekType, dataView: string) => { obj.assistant_name = buildPersonFullname( assistantPerson.person_data.person_lastname.value, assistantPerson.person_data.person_firstname.value, - fullnameOption + fullnameOption, + assistantPerson.person_data.person_middlename?.value ); } } @@ -2760,11 +2762,8 @@ export const schedulesMidweekData = ( } if (week_type === Week.CO_VISIT) { - const COFullname = store.get(COFullnameState); - const CODisplayName = store.get(CODisplayNameState); - result.lc_co_talk = source.midweek_meeting.co_talk_title.src; - result.co_name = useDisplayName ? CODisplayName : COFullname; + result.co_name = store.get(COPrintScheduleNameState); } const concluding_song = sourcesSongConclude({ @@ -2823,7 +2822,7 @@ export const schedulesWeekendData = ( weekendMeetingOpeningPrayerAutoAssignState ); - const fullnameOption = store.get(fullnameOptionState); + const fullnameOption = store.get(printFullnameOptionState); const useDisplayName = store.get(displayNameMeetingsEnableState); const defaultWTStudyConductor = store.get(defaultWTStudyConductorNameState); const lang = store.get(JWLangState); @@ -2972,7 +2971,8 @@ export const schedulesWeekendData = ( result.speaker_1_name = buildPersonFullname( speaker.speaker_data.person_lastname.value, speaker.speaker_data.person_firstname.value, - fullnameOption + fullnameOption, + speaker.speaker_data.person_middlename?.value ); } @@ -3064,7 +3064,7 @@ export const schedulesWeekendData = ( }); if (result.co_name?.length === 0) { - result.co_name = store.get(COScheduleNameState); + result.co_name = store.get(COPrintScheduleNameState); } result.public_talk_title = source.weekend_meeting.co_talk_title.public.src; @@ -3102,7 +3102,7 @@ export const scheduleOutgoingSpeakers = ( schedule: SchedWeekType ): OutgoingSpeakersScheduleType => { const talks = store.get(publicTalksState); - const fullnameOption = store.get(fullnameOptionState); + const fullnameOption = store.get(printFullnameOptionState); const displayNameEnabled = store.get(displayNameMeetingsEnableState); const persons = store.get(personsByViewState); const songs = store.get(songsLocaleState); diff --git a/src/services/app/visiting_speakers.ts b/src/services/app/visiting_speakers.ts index 5cbf8ecc732..2c1b0c5c6a0 100644 --- a/src/services/app/visiting_speakers.ts +++ b/src/services/app/visiting_speakers.ts @@ -10,13 +10,15 @@ export const speakersSortByName = (speakers: VisitingSpeakerType[]) => { const fullnameA = buildPersonFullname( a.speaker_data.person_lastname.value, a.speaker_data.person_firstname.value, - fullnameOption + fullnameOption, + a.speaker_data.person_middlename?.value ); const fullnameB = buildPersonFullname( b.speaker_data.person_lastname.value, b.speaker_data.person_firstname.value, - fullnameOption + fullnameOption, + b.speaker_data.person_middlename?.value ); if (fullnameA === '') return 1; diff --git a/src/services/dexie/schema.ts b/src/services/dexie/schema.ts index c39bfd275b2..6f8601222ea 100644 --- a/src/services/dexie/schema.ts +++ b/src/services/dexie/schema.ts @@ -194,6 +194,7 @@ export const personSchema: PersonType = { person_uid: '', person_data: { person_firstname: { value: '', updatedAt: '' }, + person_middlename: { value: '', updatedAt: '' }, person_lastname: { value: '', updatedAt: '' }, person_display_name: { value: '', updatedAt: '' }, male: { value: true, updatedAt: '' }, @@ -269,6 +270,10 @@ export const settingSchema: SettingsType = { _deleted: false, }, ], + // print_fullname_option is intentionally not seeded: while no record + // exists, printFullnameOptionState follows the in-app format, so a + // congregation only diverges print output by an explicit choice + short_date_format: [ { type: 'main', value: 'MM/dd/yyyy', updatedAt: '', _deleted: false }, ], @@ -293,6 +298,7 @@ export const settingSchema: SettingsType = { }, circuit_overseer: { firstname: { value: '', updatedAt: '' }, + middlename: { value: '', updatedAt: '' }, lastname: { value: '', updatedAt: '' }, display_name: { value: '', updatedAt: '' }, visits: [], @@ -355,6 +361,7 @@ export const settingSchema: SettingsType = { }, hour_credits_enabled: { value: false, updatedAt: '' }, firstname: { value: '', updatedAt: '' }, + middlename: { value: '', updatedAt: '' }, lastname: { value: '', updatedAt: '' }, theme_follow_os_enabled: { value: false, updatedAt: '' }, user_avatar: undefined, @@ -388,6 +395,7 @@ export const vistingSpeakerSchema: VisitingSpeakerType = { cong_id: '', person_display_name: { value: '', updatedAt: '' }, person_firstname: { value: '', updatedAt: '' }, + person_middlename: { value: '', updatedAt: '' }, person_lastname: { value: '', updatedAt: '' }, person_notes: { value: '', updatedAt: '' }, elder: { value: false, updatedAt: '' }, diff --git a/src/services/dexie/settings.ts b/src/services/dexie/settings.ts index 15e2b7cabe0..d8d9689820f 100644 --- a/src/services/dexie/settings.ts +++ b/src/services/dexie/settings.ts @@ -255,6 +255,7 @@ export const dbAppSettingsBuildTest = async () => { baseSettings.cong_settings.circuit_overseer = { firstname: { value: 'Alexander', updatedAt: new Date().toISOString() }, + middlename: { value: 'James', updatedAt: new Date().toISOString() }, lastname: { value: 'Olivier', updatedAt: new Date().toISOString() }, display_name: { value: 'A. Olivier', updatedAt: new Date().toISOString() }, visits: [], diff --git a/src/services/dexie/visiting_speakers.ts b/src/services/dexie/visiting_speakers.ts index a45e5780d56..48ef150f11b 100644 --- a/src/services/dexie/visiting_speakers.ts +++ b/src/services/dexie/visiting_speakers.ts @@ -268,6 +268,10 @@ export const dbVisitingSpeakersDummy = async () => { value: 'Ribeiro', updatedAt: new Date().toISOString(), }, + person_middlename: { + value: 'Santos', + updatedAt: new Date().toISOString(), + }, person_lastname: { value: 'Gonzaga', updatedAt: new Date().toISOString(), diff --git a/src/services/states/persons.ts b/src/services/states/persons.ts index 228995314b2..f2c92f93ac7 100644 --- a/src/services/states/persons.ts +++ b/src/services/states/persons.ts @@ -57,6 +57,7 @@ export const personGetFullname = (person_uid: string) => { return buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); }; diff --git a/src/services/worker/backupUtils.ts b/src/services/worker/backupUtils.ts index 6e3cd1a65e1..1a1287ac78d 100644 --- a/src/services/worker/backupUtils.ts +++ b/src/services/worker/backupUtils.ts @@ -368,6 +368,7 @@ const dbGetTableData = async () => { ms: { value: personIsMS(person), updatedAt: '' }, person_display_name: person.person_data.person_display_name, person_firstname: person.person_data.person_firstname, + person_middlename: person.person_data.person_middlename, person_lastname: person.person_data.person_lastname, person_email: person.person_data.email, person_phone: person.person_data.phone, @@ -1676,6 +1677,7 @@ export const dbExportDataBackup = async (backupData: BackupDataType) => { const userBaseSettings = { firstname: user_settings.firstname, + middlename: user_settings.middlename, lastname: user_settings.lastname, }; diff --git a/src/states/persons.ts b/src/states/persons.ts index 05532889a6f..57e905bd35f 100644 --- a/src/states/persons.ts +++ b/src/states/persons.ts @@ -46,6 +46,7 @@ export const personCurrentDetailsState = atom({ person_uid: '', person_data: { person_firstname: { value: '', updatedAt: '' }, + person_middlename: { value: '', updatedAt: '' }, person_lastname: { value: '', updatedAt: '' }, person_display_name: { value: '', updatedAt: '' }, male: { value: true, updatedAt: '' }, diff --git a/src/states/schedules.ts b/src/states/schedules.ts index 1da66e51531..a548499a564 100644 --- a/src/states/schedules.ts +++ b/src/states/schedules.ts @@ -16,7 +16,7 @@ import { adminRoleState, congRoleState, displayNameMeetingsEnableState, - fullnameOptionState, + printFullnameOptionState, userDataViewState, weekendMeetingWTStudyConductorDefaultState, } from './settings'; @@ -62,11 +62,13 @@ export const S89TemplateState = atom( 'S89_1x1' ); +// only consumed by the weekend document builder, so the composed +// fallback name follows the print format export const defaultWTStudyConductorNameState = atom((get) => { const value = get(weekendMeetingWTStudyConductorDefaultState); const useDisplayName = get(displayNameMeetingsEnableState); const persons = get(personsState); - const fullnameOption = get(fullnameOptionState); + const fullnameOption = get(printFullnameOptionState); if (value.length === 0) return ''; @@ -84,7 +86,8 @@ export const defaultWTStudyConductorNameState = atom((get) => { result = buildPersonFullname( person.person_data.person_lastname.value, person.person_data.person_firstname.value, - fullnameOption + fullnameOption, + person.person_data.person_middlename?.value ); } diff --git a/src/states/settings.ts b/src/states/settings.ts index 540fa4d6dd4..5904fc5db0c 100644 --- a/src/states/settings.ts +++ b/src/states/settings.ts @@ -94,6 +94,19 @@ export const fullnameOptionState = atom((get) => { ); }); +export const printFullnameOptionState = atom((get) => { + const settings = get(settingsState); + const dataView = get(userDataViewState); + + const printOption = settings.cong_settings.print_fullname_option?.find( + (record) => record.type === dataView && !record._deleted + )?.value; + + // congregations that never chose a print format keep printing exactly + // what they print today, so no migration is needed + return printOption || get(fullnameOptionState); +}); + export const shortDateFormatState = atom((get) => { const settings = get(settingsState); const dataView = get(userDataViewState); @@ -122,6 +135,12 @@ export const COFirstnameState = atom((get) => { return settings.cong_settings.circuit_overseer.firstname.value; }); +export const COMiddlenameState = atom((get) => { + const settings = get(settingsState); + + return settings.cong_settings.circuit_overseer.middlename?.value ?? ''; +}); + export const COLastnameState = atom((get) => { const settings = get(settingsState); @@ -136,10 +155,16 @@ export const CODisplayNameState = atom((get) => { export const COFullnameState = atom((get) => { const firstname = get(COFirstnameState); + const middlename = get(COMiddlenameState); const lastname = get(COLastnameState); const fullnameOption = get(fullnameOptionState); - const fullname = buildPersonFullname(lastname, firstname, fullnameOption); + const fullname = buildPersonFullname( + lastname, + firstname, + fullnameOption, + middlename + ); return fullname; }); @@ -154,6 +179,32 @@ export const COScheduleNameState = atom((get) => { return scheduleName; }); +export const COPrintFullnameState = atom((get) => { + const firstname = get(COFirstnameState); + const middlename = get(COMiddlenameState); + const lastname = get(COLastnameState); + const fullnameOption = get(printFullnameOptionState); + + const fullname = buildPersonFullname( + lastname, + firstname, + fullnameOption, + middlename + ); + + return fullname; +}); + +export const COPrintScheduleNameState = atom((get) => { + const fullname = get(COPrintFullnameState); + const displayName = get(CODisplayNameState); + const useDisplayName = get(displayNameMeetingsEnableState); + + const scheduleName = useDisplayName ? displayName : fullname; + + return scheduleName; +}); + export const secretaryRoleState = atom((get) => { const congRole = get(congRoleState); return congRole.includes('secretary'); @@ -521,6 +572,12 @@ export const firstnameState = atom((get) => { return settings.user_settings.firstname.value; }); +export const middlenameState = atom((get) => { + const settings = get(settingsState); + + return settings.user_settings.middlename?.value || ''; +}); + export const lastnameState = atom((get) => { const settings = get(settingsState); @@ -540,10 +597,16 @@ export const firstDayWeekState = atom((get) => { export const fullnameState = atom((get) => { const firstname = get(firstnameState); + const middlename = get(middlenameState); const lastname = get(lastnameState); const fullnameOption = get(fullnameOptionState); - const fullname = buildPersonFullname(lastname, firstname, fullnameOption); + const fullname = buildPersonFullname( + lastname, + firstname, + fullnameOption, + middlename + ); return fullname; }); diff --git a/src/utils/common.ts b/src/utils/common.ts index b2d01fad6e7..043b6b61bb5 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -51,10 +51,39 @@ export const matchIsNumeric = (text) => { export const buildPersonFullname = ( lastname: string, firstname: string, - option?: FullnameOption + option?: FullnameOption, + middlename?: string ) => { const buildOption = option || FullnameOption.FIRST_BEFORE_LAST; + const middle = middlename?.trim() ?? ''; + + if (buildOption === FullnameOption.FIRST_MIDDLE_LAST) { + return [firstname, middle, lastname].filter(Boolean).join(' '); + } + + if (buildOption === FullnameOption.LAST_FIRST_MIDDLE) { + return [lastname, firstname, middle].filter(Boolean).join(' '); + } + + // the comma marks a western sorted-list inversion ("Gonzalez, William"); + // family-name-first languages use LAST_BEFORE_FIRST or LAST_FIRST_MIDDLE, + // whose natural order takes no comma + if ( + buildOption === FullnameOption.LAST_COMMA_FIRST || + buildOption === FullnameOption.LAST_COMMA_FIRST_MIDDLE + ) { + const given = + buildOption === FullnameOption.LAST_COMMA_FIRST_MIDDLE + ? [firstname, middle].filter(Boolean).join(' ') + : firstname; + + if (lastname.length === 0) return given; + if (given.length === 0) return lastname; + + return `${lastname}, ${given}`; + } + if (lastname.length === 0) { return firstname; } @@ -196,7 +225,8 @@ export const personGetDisplayName = ( result = buildPersonFullname( option.person_data.person_lastname.value, option.person_data.person_firstname.value, - fullnameOption + fullnameOption, + option.person_data.person_middlename?.value ); } @@ -218,7 +248,8 @@ export const speakerGetDisplayName = ( result = buildPersonFullname( speaker.speaker_data.person_lastname.value, speaker.speaker_data.person_firstname.value, - fullnameOption + fullnameOption, + speaker.speaker_data.person_middlename?.value ); } diff --git a/src/utils/dev.ts b/src/utils/dev.ts index f0518652512..b085906a2af 100644 --- a/src/utils/dev.ts +++ b/src/utils/dev.ts @@ -75,7 +75,16 @@ export const importDummyPersons = async (showLoading?: boolean) => { 'yyyy/MM/dd' ); - const formattedData: PersonType[] = PERSON_MOCK.map((user) => { + const maleMiddleNames = ['Alexander', 'Robert', 'Martin']; + const femaleMiddleNames = ['Grace', 'Sophie', 'Louise']; + + const formattedData: PersonType[] = PERSON_MOCK.map((user, index) => { + const pool = + user.gender === 'female' ? femaleMiddleNames : maleMiddleNames; + + // every fourth person keeps an empty middle name so both shapes show up + const middleName = index % 4 === 3 ? '' : pool[index % pool.length]; + const obj = { _deleted: { value: false, updatedAt: '' }, person_uid: crypto.randomUUID(), @@ -94,6 +103,10 @@ export const importDummyPersons = async (showLoading?: boolean) => { value: user.firstName, updatedAt: new Date().toISOString(), }, + person_middlename: { + value: middleName, + updatedAt: new Date().toISOString(), + }, person_lastname: { value: user.lastName, updatedAt: new Date().toISOString(), diff --git a/src/utils/person.ts b/src/utils/person.ts index e3e6fa22eed..2d1c8824032 100644 --- a/src/utils/person.ts +++ b/src/utils/person.ts @@ -14,6 +14,13 @@ export const changeFirstname = (newPerson: PersonType, value: string) => { new Date().toISOString(); }; +export const changeMiddlename = (newPerson: PersonType, value: string) => { + newPerson.person_data.person_middlename = { + value, + updatedAt: new Date().toISOString(), + }; +}; + export const changeLastname = (newPerson: PersonType, value: string) => { newPerson.person_data.person_lastname.value = value; newPerson.person_data.person_lastname.updatedAt = new Date().toISOString();