Skip to content

Commit 183c261

Browse files
OneChanceLancerhahao
authored andcommitted
CU-86c1dwh1e - [FIX] Allow only one empty CO visit field – Congregation settings
1 parent e82d139 commit 183c261

File tree

10 files changed

+344
-71
lines changed

10 files changed

+344
-71
lines changed

src/components/date_picker/index.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const DatePicker = ({
3131
shortDateFormat,
3232
view,
3333
hideNav,
34+
error,
35+
helperText,
3436
}: CustomDatePickerProps) => {
3537
const poperRef = useRef<HTMLDivElement>(null);
3638

@@ -40,7 +42,7 @@ const DatePicker = ({
4042

4143
const [height, setHeight] = useState(240); // Initial height
4244
const [open, setOpen] = useState(false);
43-
const [valueTmp, setValueTmp] = useState<Date>(value);
45+
const [valueTmp, setValueTmp] = useState<Date | null>(value ?? null);
4446

4547
const slotFieldProps =
4648
view === 'button' ? { field: ButtonField } : { textField: InputTextField };
@@ -56,18 +58,18 @@ const DatePicker = ({
5658
const handleKeyDown = (e: KeyboardEvent<Element>) => {
5759
if (e.key !== 'Enter') return;
5860

59-
const isValidDate = isValid(valueTmp);
61+
const isValidDate = valueTmp instanceof Date && isValid(valueTmp);
6062

6163
if (!isValidDate) return;
6264

6365
onChange?.(valueTmp);
6466
setOpen(false);
6567
};
6668

67-
const handleValueChange = (value: Date) => {
69+
const handleValueChange = (value: Date | null) => {
6870
setValueTmp(value);
6971

70-
const isValidDate = isValid(value);
72+
const isValidDate = value instanceof Date && isValid(value);
7173

7274
onChange?.(value);
7375

@@ -77,15 +79,19 @@ const DatePicker = ({
7779
};
7880

7981
useEffect(() => {
80-
if (getWeeksInMonth(valueTmp, { weekStartsOn: 0 }) === 6) {
81-
setHeight(290);
82+
if (valueTmp) {
83+
if (getWeeksInMonth(valueTmp, { weekStartsOn: 0 }) === 6) {
84+
setHeight(290);
85+
} else {
86+
setHeight(240);
87+
}
8288
} else {
8389
setHeight(240);
8490
}
8591
}, [valueTmp]);
8692

8793
useEffect(() => {
88-
setValueTmp(value);
94+
setValueTmp(value ?? null);
8995
}, [value]);
9096

9197
return (
@@ -160,6 +166,8 @@ const DatePicker = ({
160166
},
161167
onKeyDown: handleKeyDown,
162168
ref: poperRef,
169+
error,
170+
helperText,
163171
},
164172
}}
165173
/>

src/components/date_picker/index.types.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ export type CustomDatePickerView = 'button' | 'input';
77
* Props for the CustomDatePicker component.
88
*/
99
export interface CustomDatePickerProps {
10-
/**
11-
* The selected date value.
12-
*/
13-
value?: Date;
10+
/**
11+
* The selected date value.
12+
*/
13+
value?: Date | null;
1414

1515
/**
1616
* The view type of the date picker.
@@ -40,8 +40,8 @@ export interface CustomDatePickerProps {
4040
/**
4141
* Function called when the selected date changes.
4242
* @param value - The new selected date value.
43-
*/
44-
onChange?: (value: Date) => void | Promise<void>;
43+
*/
44+
onChange?: (value: Date | null) => void | Promise<void>;
4545

4646
/**
4747
* The minimum selectable date.
@@ -56,4 +56,8 @@ export interface CustomDatePickerProps {
5656
readOnly?: boolean;
5757

5858
hideNav?: boolean;
59+
60+
error?: boolean;
61+
62+
helperText?: string;
5963
}

src/components/date_picker/slots/toolbar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { useAppTranslation } from '@hooks/index';
44
import { formatLongDateWithShortVars, isValidDate } from '@utils/date';
55
import Typography from '@components/typography';
66

7-
type ToolbarProps = { selected: Date };
7+
type ToolbarProps = { selected: Date | null };
88

99
const Toolbar = ({ selected }: ToolbarProps) => {
1010
const { t } = useAppTranslation();
1111

1212
const value = useMemo(() => {
13+
if (!selected) return '***';
1314
if (!isValidDate(selected)) return '***';
1415

1516
return formatLongDateWithShortVars(selected);

src/features/congregation/settings/circuit_overseer/useCircuitOverseer.tsx

Lines changed: 90 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import { dbAppSettingsUpdate } from '@services/dexie/settings';
99
import { generateDisplayName } from '@utils/common';
1010

1111
const useCircuitOverseer = () => {
12-
const timer = useRef<NodeJS.Timeout>();
12+
type FieldKey = 'firstname' | 'lastname' | 'displayname';
13+
14+
const saveTimers = useRef<Partial<Record<FieldKey, ReturnType<typeof setTimeout>>>>({});
1315

1416
const settings = useAtomValue(settingsState);
1517
const fullnameOption = useAtomValue(fullnameOptionState);
@@ -18,70 +20,116 @@ const useCircuitOverseer = () => {
1820
const [firstname, setFirstname] = useState('');
1921
const [lastname, setLastname] = useState('');
2022
const [displayname, setDisplayname] = useState('');
23+
const [editing, setEditing] = useState<Record<FieldKey, boolean>>({
24+
firstname: false,
25+
lastname: false,
26+
displayname: false,
27+
});
28+
29+
const clearTimer = (key: FieldKey) => {
30+
const timer = saveTimers.current[key];
31+
if (timer) {
32+
clearTimeout(timer);
33+
saveTimers.current[key] = undefined;
34+
}
35+
};
36+
37+
const scheduleSave = (key: FieldKey, fn: () => Promise<void>, markCompleteKeys: FieldKey[]) => {
38+
clearTimer(key);
39+
40+
saveTimers.current[key] = setTimeout(async () => {
41+
saveTimers.current[key] = undefined;
42+
await fn();
43+
setEditing((prev) => {
44+
const next = { ...prev };
45+
for (const field of markCompleteKeys) {
46+
next[field] = false;
47+
}
48+
return next;
49+
});
50+
}, 1000);
51+
};
52+
53+
const markEditing = (keys: FieldKey[]) => {
54+
setEditing((prev) => {
55+
const next = { ...prev };
56+
for (const field of keys) {
57+
next[field] = true;
58+
}
59+
return next;
60+
});
61+
};
2162

2263
const handleFirstnameChange = (value: string) => {
64+
markEditing(['firstname', 'displayname']);
2365
setFirstname(value);
2466

2567
const dispName = generateDisplayName(lastname, value);
2668
setDisplayname(dispName);
2769
};
2870

2971
const handleLastnameChange = (value: string) => {
72+
markEditing(['lastname', 'displayname']);
3073
setLastname(value);
3174

3275
const dispName = generateDisplayName(value, firstname);
3376
setDisplayname(dispName);
3477
};
3578

36-
const handleDisplaynameChange = (value: string) => setDisplayname(value);
79+
const handleDisplaynameChange = (value: string) => {
80+
markEditing(['displayname']);
81+
setDisplayname(value);
82+
};
3783

3884
const handleFirstnameSave = () => {
39-
if (timer.current) clearTimeout(timer.current);
40-
41-
timer.current = setTimeout(handleFirstnameSaveDb, 1000);
85+
scheduleSave('firstname', handleFirstnameSaveDb, ['firstname', 'displayname']);
4286
};
4387

4488
const handleLastnameSave = () => {
45-
if (timer.current) clearTimeout(timer.current);
46-
47-
timer.current = setTimeout(handleLastnameSaveDb, 1000);
89+
scheduleSave('lastname', handleLastnameSaveDb, ['lastname', 'displayname']);
4890
};
4991

5092
const handleDisplaynameSave = () => {
51-
if (timer.current) clearTimeout(timer.current);
52-
53-
timer.current = setTimeout(handleDisplaynameSaveDb, 1000);
93+
scheduleSave('displayname', handleDisplaynameSaveDb, ['displayname']);
5494
};
5595

5696
const handleFirstnameSaveDb = async () => {
57-
const circuitOverseer = structuredClone(
58-
settings.cong_settings.circuit_overseer
97+
const firstnameField = structuredClone(
98+
settings.cong_settings.circuit_overseer.firstname
99+
);
100+
const displayNameField = structuredClone(
101+
settings.cong_settings.circuit_overseer.display_name
59102
);
60103

61-
circuitOverseer.firstname.value = firstname;
62-
circuitOverseer.firstname.updatedAt = new Date().toISOString();
104+
firstnameField.value = firstname;
105+
firstnameField.updatedAt = new Date().toISOString();
63106

64-
circuitOverseer.display_name.value = displayname;
65-
circuitOverseer.display_name.updatedAt = new Date().toISOString();
107+
displayNameField.value = displayname;
108+
displayNameField.updatedAt = new Date().toISOString();
66109

67110
await dbAppSettingsUpdate({
68-
'cong_settings.circuit_overseer': circuitOverseer,
111+
'cong_settings.circuit_overseer.firstname': firstnameField,
112+
'cong_settings.circuit_overseer.display_name': displayNameField,
69113
});
70114
};
71115

72116
const handleLastnameSaveDb = async () => {
73-
const circuitOverseer = structuredClone(
74-
settings.cong_settings.circuit_overseer
117+
const lastnameField = structuredClone(
118+
settings.cong_settings.circuit_overseer.lastname
119+
);
120+
const displayNameField = structuredClone(
121+
settings.cong_settings.circuit_overseer.display_name
75122
);
76123

77-
circuitOverseer.lastname.value = lastname;
78-
circuitOverseer.lastname.updatedAt = new Date().toISOString();
124+
lastnameField.value = lastname;
125+
lastnameField.updatedAt = new Date().toISOString();
79126

80-
circuitOverseer.display_name.value = displayname;
81-
circuitOverseer.display_name.updatedAt = new Date().toISOString();
127+
displayNameField.value = displayname;
128+
displayNameField.updatedAt = new Date().toISOString();
82129

83130
await dbAppSettingsUpdate({
84-
'cong_settings.circuit_overseer': circuitOverseer,
131+
'cong_settings.circuit_overseer.lastname': lastnameField,
132+
'cong_settings.circuit_overseer.display_name': displayNameField,
85133
});
86134
};
87135

@@ -101,10 +149,23 @@ const useCircuitOverseer = () => {
101149
useEffect(() => {
102150
const co = settings.cong_settings.circuit_overseer;
103151

104-
setFirstname(co.firstname.value);
105-
setLastname(co.lastname.value);
106-
setDisplayname(co.display_name.value);
107-
}, [settings]);
152+
setFirstname((prev) => (editing.firstname ? prev : co.firstname.value));
153+
setLastname((prev) => (editing.lastname ? prev : co.lastname.value));
154+
setDisplayname((prev) => (editing.displayname ? prev : co.display_name.value));
155+
}, [settings, editing]);
156+
157+
useEffect(() => {
158+
const timersOnUnmount = saveTimers.current;
159+
160+
return () => {
161+
(Object.keys(timersOnUnmount) as FieldKey[]).forEach((key) => {
162+
const timer = timersOnUnmount[key];
163+
if (timer) {
164+
clearTimeout(timer);
165+
}
166+
});
167+
};
168+
}, []);
108169

109170
return {
110171
fullnameOption,

src/features/congregation/settings/circuit_overseer/week_item/index.tsx

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,63 @@ import { DatePicker } from '@components/index';
55
import { WeekItemType } from './index.types';
66
import useWeekItem from './useWeekItem';
77
import IconButton from '@components/icon_button';
8+
import { formatDate, getWeekDate } from '@utils/date';
89

9-
const WeekItem = ({ visit }: WeekItemType) => {
10+
const WeekItem = ({ visit, error, helperText, onWeekChange, onDelete }: WeekItemType) => {
1011
const { t } = useAppTranslation();
1112

1213
const { isAdmin } = useCurrentUser();
1314

1415
const { handleDateChange, handleDeleteVisit } = useWeekItem(visit);
1516

17+
const computeWeekOf = (date: Date | null) => {
18+
if (date === null) {
19+
return '';
20+
}
21+
22+
return formatDate(getWeekDate(date), 'yyyy/MM/dd');
23+
};
24+
25+
const handlePickerChange = async (date: Date | null) => {
26+
const optimisticWeekOf = computeWeekOf(date);
27+
28+
if (onWeekChange) {
29+
onWeekChange(visit.id, optimisticWeekOf);
30+
}
31+
32+
try {
33+
const nextWeekOf = await handleDateChange(date);
34+
35+
if (onWeekChange && nextWeekOf !== optimisticWeekOf) {
36+
onWeekChange(visit.id, nextWeekOf);
37+
}
38+
} catch (error) {
39+
if (onWeekChange) {
40+
onWeekChange(visit.id, visit.weekOf);
41+
}
42+
43+
console.error(error);
44+
}
45+
};
46+
47+
const handleDeleteClick = async () => {
48+
await handleDeleteVisit();
49+
50+
if (onDelete) {
51+
onDelete(visit.id);
52+
}
53+
};
54+
1655
return (
1756
<Box sx={{ display: 'flex', gap: '16px' }}>
1857
<DatePicker
1958
disablePast
2059
label={t('tr_coNextVisitWeek')}
2160
value={visit.weekOf === '' ? null : new Date(visit.weekOf)}
22-
onChange={(date) => handleDateChange(date)}
61+
onChange={handlePickerChange}
2362
readOnly={!isAdmin}
63+
error={error}
64+
helperText={helperText}
2465
/>
2566

2667
{isAdmin && (
@@ -31,7 +72,7 @@ const WeekItem = ({ visit }: WeekItemType) => {
3172
width: '48px',
3273
height: '48px',
3374
}}
34-
onClick={handleDeleteVisit}
75+
onClick={handleDeleteClick}
3576
>
3677
<IconDelete color="var(--red-main)" />
3778
</IconButton>

src/features/congregation/settings/circuit_overseer/week_item/index.types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@ import { CircuitOverseerVisitType } from '@definition/settings';
22

33
export type WeekItemType = {
44
visit: CircuitOverseerVisitType;
5+
error?: boolean;
6+
helperText?: string;
7+
onWeekChange?: (id: string, nextWeekOf: string) => void;
8+
onDelete?: (id: string) => void;
59
};

0 commit comments

Comments
 (0)