@propriety/court-calendar 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.editorconfig +26 -0
  2. package/README.md +0 -0
  3. package/biome.json +302 -0
  4. package/dev/App.tsx +51 -0
  5. package/dev/main.tsx +10 -0
  6. package/index.html +12 -0
  7. package/package.json +54 -0
  8. package/public/vite.svg +1 -0
  9. package/src/_components/CCalendar.css +463 -0
  10. package/src/_components/CCalendar.tsx +726 -0
  11. package/src/_components/List/CalendarList.tsx +288 -0
  12. package/src/_components/Modal/CaseDetails/CaseDetails.tsx +414 -0
  13. package/src/_components/Modal/CaseDetails/EvidenceRow.tsx +83 -0
  14. package/src/_components/Modal/CaseDetails/EvidenceSection.tsx +94 -0
  15. package/src/_components/Modal/CreateEdit/CreateEditCase.tsx +241 -0
  16. package/src/_components/Modal/CreateEdit/DateSelector.tsx +42 -0
  17. package/src/_components/Modal/CreateEdit/EditUserFieldDropdown.tsx +54 -0
  18. package/src/_components/Modal/CreateEdit/EnumDropdown.tsx +54 -0
  19. package/src/_components/Modal/CreateEdit/HearingOfficerDropdown.tsx +48 -0
  20. package/src/_components/Modal/CreateEdit/TextFieldList.tsx +186 -0
  21. package/src/_components/Modal/CreateEdit/ToggleableTextField.tsx +91 -0
  22. package/src/_components/Modal/Modal.css +15 -0
  23. package/src/_components/Modal/Modal.tsx +325 -0
  24. package/src/_components/Modal/ModalActions.tsx +99 -0
  25. package/src/_components/Modal/View/CaseToolbar.tsx +81 -0
  26. package/src/_components/Modal/View/CaseViewer.tsx +237 -0
  27. package/src/_components/Modal/View/DateDetails.tsx +138 -0
  28. package/src/_components/Modal/View/InfoBox.tsx +22 -0
  29. package/src/_components/Modal/View/InfoBoxBtn.css +39 -0
  30. package/src/_components/Modal/View/InfoBoxBtn.tsx +29 -0
  31. package/src/_components/Modal/View/NoticeFileLink.tsx +44 -0
  32. package/src/_components/Shared/FirstSecondChairIcons.tsx +247 -0
  33. package/src/_components/Shared/FormRow.tsx +37 -0
  34. package/src/_components/Shared/MuniDropdown.tsx +94 -0
  35. package/src/_components/Shared/SearchBar.tsx +87 -0
  36. package/src/_components/Toolbar/CaseFilter.tsx +77 -0
  37. package/src/_components/Toolbar/DateTypeFilter.tsx +63 -0
  38. package/src/_components/Toolbar/HearingTypeFilter.tsx +63 -0
  39. package/src/_components/Toolbar/Toolbar.tsx +159 -0
  40. package/src/_components/Toolbar/UserFilter.tsx +105 -0
  41. package/src/_components/Toolbar/ViewFilter.tsx +48 -0
  42. package/src/helpers/cache.ts +89 -0
  43. package/src/helpers/cases.ts +79 -0
  44. package/src/helpers/courtDates.ts +139 -0
  45. package/src/helpers/formatter.ts +16 -0
  46. package/src/helpers/munis.ts +44 -0
  47. package/src/helpers/people.ts +46 -0
  48. package/src/index.ts +2 -0
  49. package/src/types.ts +129 -0
  50. package/tsconfig.app.json +32 -0
  51. package/tsconfig.json +4 -0
  52. package/tsconfig.node.json +30 -0
  53. package/vite.config.ts +27 -0
@@ -0,0 +1,241 @@
1
+ import TextField from '@mui/material/TextField';
2
+ import Box from '@mui/material/Box';
3
+ import Divider from '@mui/material/Divider';
4
+ import Stack from '@mui/material/Stack';
5
+ import { HearingType, Lifecycle, ModalMode, Status, type Case, type CourtDate } from '@/types';
6
+ import DateSelector from './DateSelector';
7
+ import FormRow from '../../Shared/FormRow';
8
+ import EnumDropdown from './EnumDropdown';
9
+ import EditUserFieldDropdown from './EditUserFieldDropdown';
10
+ import HearingOfficerDropdown from './HearingOfficerDropdown';
11
+ import { TimePicker } from '@mui/x-date-pickers';
12
+ import dayjs from 'dayjs';
13
+ import TextFieldList from './TextFieldList';
14
+ import Switch from '@mui/material/Switch';
15
+ import InputLabel from '@mui/material/InputLabel';
16
+ import { memo, useState, useEffect, useMemo, useCallback } from 'react';
17
+ import MuniDropdown from '@/_components/Shared/MuniDropdown';
18
+
19
+ const CreateEditCase = memo(function CreateEditCase({
20
+ edited,
21
+ setEdited,
22
+ editedCases,
23
+ setEditedCases,
24
+ modalMode,
25
+ }: {
26
+ edited: CourtDate | undefined;
27
+ setEdited: (data: CourtDate) => void;
28
+ editedCases: Case[];
29
+ setEditedCases: (cases: Case[]) => void;
30
+ modalMode: ModalMode;
31
+ }) {
32
+ // Track which fields are toggled to Parcel ID (false = SCAR ID, true = Parcel ID)
33
+ const [isParcelIdMode, setIsParcelIdMode] = useState<boolean[]>(editedCases.map(() => false));
34
+
35
+ // Initialize toggle states based on editedCases length
36
+ useEffect(() => {
37
+ if (isParcelIdMode.length !== editedCases.length) {
38
+ const newToggles = editedCases.map((_, index) => isParcelIdMode[index] || false);
39
+ setIsParcelIdMode(newToggles);
40
+ }
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ }, [editedCases.length]);
43
+
44
+ function setEditedInterrupt(data: CourtDate) {
45
+ setEdited({
46
+ ...data,
47
+ LastUpdateDate: new Date(),
48
+ HearingLink: data.Type === HearingType.VIRTUAL ? data.HearingLink : '',
49
+ Lifecycle:
50
+ data.Lifecycle !== Lifecycle.UPLOADED && data.FirstChair ? Lifecycle.ASSIGNED : Lifecycle.SCHEDULED,
51
+ });
52
+ }
53
+
54
+ // Memoize the textFields array to prevent unnecessary re-renders
55
+ const textFieldsArray = useMemo(
56
+ () => editedCases.map((c, index) => (isParcelIdMode[index] ? c.ParcelID : c.SCARIndexNumber)),
57
+ [editedCases, isParcelIdMode],
58
+ );
59
+
60
+ // Memoize the setTextFields callback to prevent unnecessary re-renders
61
+ const handleSetTextFields = useCallback(
62
+ (ids: string[], toggles?: boolean[]) => {
63
+ const updatedCases = ids.map((id, index) => {
64
+ const isParcelMode = toggles ? toggles[index] : isParcelIdMode[index];
65
+ return {
66
+ ...editedCases[index],
67
+ ...(isParcelMode ? { ParcelID: id } : { SCARIndexNumber: id }),
68
+ };
69
+ });
70
+ setEditedCases(updatedCases);
71
+ },
72
+ [editedCases, isParcelIdMode, setEditedCases],
73
+ );
74
+
75
+ if (!edited) return null;
76
+
77
+ return (
78
+ <Stack spacing={1.5}>
79
+ {/* Court Date Info */}
80
+ <Box>
81
+ <FormRow gap={7} header='Court Date Information' subheader='Basic details about the court case.'>
82
+ <MuniDropdown
83
+ selectedMuni={edited.MuniCode || ''}
84
+ setSelectedMuni={(muni) => {
85
+ setEditedInterrupt({
86
+ ...edited,
87
+ MuniCode: muni,
88
+ });
89
+ }}
90
+ allowClear={false}
91
+ size='medium'
92
+ />
93
+ {/* <EnumDropdown<typeof Lifecycle>
94
+ edited={edited}
95
+ setEdited={setEditedInterrupt}
96
+ enumObject={Lifecycle}
97
+ label='Lifecycle'
98
+ /> */}
99
+ </FormRow>
100
+ <FormRow gap={7}>
101
+ <EditUserFieldDropdown
102
+ label='First Chair'
103
+ edited={edited}
104
+ setEdited={setEditedInterrupt}
105
+ userPropertyName='FirstChair'
106
+ />
107
+ <EditUserFieldDropdown
108
+ label='Second Chair'
109
+ edited={edited}
110
+ setEdited={setEditedInterrupt}
111
+ userPropertyName='SecondChair'
112
+ />
113
+ </FormRow>
114
+ <FormRow gap={7}>
115
+ <TextField
116
+ label='Notes'
117
+ value={edited.Notes || ''}
118
+ onChange={(e) =>
119
+ setEditedInterrupt({
120
+ ...edited,
121
+ Notes: e.target.value,
122
+ })
123
+ }
124
+ fullWidth
125
+ multiline
126
+ rows={2}
127
+ />
128
+ </FormRow>
129
+ </Box>
130
+
131
+ <Divider />
132
+
133
+ {/* Dates */}
134
+ <Box>
135
+ <FormRow
136
+ gap={7}
137
+ header='Dates'
138
+ subheader='Select the relevant dates for the court case. Upload date is set at 10 days before the court date (or ajournment date if applicable).'
139
+ >
140
+ <DateSelector
141
+ editing={edited}
142
+ property='CourtDate'
143
+ disabled={modalMode === ModalMode.EDIT}
144
+ label='Court Date'
145
+ setEdited={setEditedInterrupt}
146
+ />
147
+ <Stack direction='column' alignItems='center' spacing={1}>
148
+ <InputLabel htmlFor='adjourned-switch'>Adjourned</InputLabel>
149
+ <Switch
150
+ checked={!!edited.IsAdjourned}
151
+ onChange={(e) =>
152
+ setEditedInterrupt({
153
+ ...edited,
154
+ IsAdjourned: e.target.checked,
155
+ })
156
+ }
157
+ disabled={modalMode === ModalMode.CREATE}
158
+ />
159
+ </Stack>
160
+ <DateSelector
161
+ editing={edited}
162
+ property='AdjournmentDate'
163
+ disabled={modalMode === ModalMode.CREATE || !edited.IsAdjourned}
164
+ label='Adjournment Date'
165
+ setEdited={setEditedInterrupt}
166
+ />
167
+ </FormRow>
168
+ </Box>
169
+
170
+ <Divider />
171
+
172
+ {/* Hearing Details */}
173
+ <Box>
174
+ <FormRow
175
+ gap={7}
176
+ header='Hearing Details'
177
+ subheader='You can specify the hearing link if the hearing is virtual. '
178
+ >
179
+ <TimePicker
180
+ label='Hearing Time'
181
+ onChange={(val) => {
182
+ setEditedInterrupt({
183
+ ...edited,
184
+ HearingTime: val ? val.format('HH:mm') : '',
185
+ });
186
+ }}
187
+ value={edited.HearingTime ? dayjs(`1970-01-01T${edited.HearingTime}:00`) : null}
188
+ slotProps={{
189
+ popper: { className: 'themed' },
190
+ field: { style: { width: '100%' } },
191
+ }}
192
+ />
193
+ <HearingOfficerDropdown label='Hearing Officer' edited={edited} setEdited={setEditedInterrupt} />
194
+ </FormRow>
195
+ <FormRow gap={7}>
196
+ <EnumDropdown<typeof HearingType>
197
+ edited={edited}
198
+ setEdited={setEditedInterrupt}
199
+ enumObject={HearingType}
200
+ label='Type'
201
+ />
202
+ <TextField
203
+ label='Hearing Link'
204
+ fullWidth
205
+ value={edited.HearingLink || ''}
206
+ onChange={(e) =>
207
+ setEditedInterrupt({
208
+ ...edited,
209
+ HearingLink: e.target.value,
210
+ })
211
+ }
212
+ disabled={edited.Type !== HearingType.VIRTUAL}
213
+ slotProps={{
214
+ inputLabel: {
215
+ className: edited.Type !== HearingType.VIRTUAL ? 'ccdisabled' : '',
216
+ },
217
+ }}
218
+ />
219
+ </FormRow>
220
+ </Box>
221
+
222
+ <Divider />
223
+
224
+ {/* Scar IDs / Parcel IDs */}
225
+ <Box>
226
+ <TextFieldList
227
+ label='Case IDs'
228
+ textFields={textFieldsArray}
229
+ setTextFields={handleSetTextFields}
230
+ fieldLabelPrefix='SCAR ID'
231
+ alternateFieldLabelPrefix='Parcel ID'
232
+ toggleStates={isParcelIdMode}
233
+ setToggleStates={setIsParcelIdMode}
234
+ modalMode={modalMode}
235
+ />
236
+ </Box>
237
+ </Stack>
238
+ );
239
+ });
240
+
241
+ export default CreateEditCase;
@@ -0,0 +1,42 @@
1
+ import { DatePicker } from '@mui/x-date-pickers';
2
+ import dayjs from 'dayjs';
3
+ import type { CourtDate } from '@/types';
4
+
5
+ export default function DateSelector({
6
+ editing,
7
+ property,
8
+ label,
9
+ setEdited,
10
+ disabled,
11
+ }: {
12
+ editing: CourtDate | undefined;
13
+ property: keyof CourtDate;
14
+ label: string;
15
+ setEdited: (data: CourtDate) => void;
16
+ disabled: boolean;
17
+ }) {
18
+ return (
19
+ <DatePicker
20
+ label={label}
21
+ disabled={disabled}
22
+ value={
23
+ editing && editing[property] && typeof editing[property] !== 'boolean'
24
+ ? dayjs(editing[property] as string | number | Date)
25
+ : null
26
+ }
27
+ onChange={(newValue) => {
28
+ setEdited({
29
+ ...editing!,
30
+ [property]: newValue!.toDate(),
31
+ });
32
+ }}
33
+ reduceAnimations
34
+ sx={{ width: '100%' }}
35
+ slotProps={{
36
+ popper: {
37
+ className: 'themed',
38
+ },
39
+ }}
40
+ />
41
+ );
42
+ }
@@ -0,0 +1,54 @@
1
+ import Autocomplete from '@mui/material/Autocomplete';
2
+ import TextField from '@mui/material/TextField';
3
+ import { allUsers } from '@/helpers/people';
4
+ import type { CourtDate } from '@/types';
5
+
6
+ export default function EditUserFieldDropdown({
7
+ label,
8
+ edited,
9
+ setEdited,
10
+ userPropertyName,
11
+ }: {
12
+ label: string;
13
+ edited: CourtDate;
14
+ setEdited: (edited: CourtDate) => void;
15
+ userPropertyName: keyof CourtDate;
16
+ }) {
17
+ const excludedUserLastnames = ['Import', 'Memos', 'Table', 'Note', 'Robot', 'Ingestion'];
18
+
19
+ // Prepare filtered user options
20
+ const userOptions = Object.values(allUsers).filter(
21
+ (option) => option.UserLastName && !excludedUserLastnames.includes(option.UserLastName),
22
+ );
23
+
24
+ // Find the selected user object based on the value in edited
25
+ const selectedUser = userOptions.find((u) => u.UserID === edited[userPropertyName]) || null;
26
+
27
+ return (
28
+ <Autocomplete
29
+ options={userOptions}
30
+ getOptionLabel={(option) => `${option.UserFirstName} ${option.UserLastName}`}
31
+ value={selectedUser}
32
+ onChange={(_, newValue) =>
33
+ setEdited({
34
+ ...edited,
35
+ [userPropertyName]: newValue ? newValue.UserID : '',
36
+ })
37
+ }
38
+ isOptionEqualToValue={(option, value) => option.UserID === value.UserID}
39
+ renderInput={(params) => <TextField {...params} label={label} fullWidth />}
40
+ size='medium'
41
+ fullWidth
42
+ classes={{ paper: 'themed' }}
43
+ sx={{
44
+ minWidth: 120,
45
+ '& .MuiInputBase-root:hover': {
46
+ backgroundColor: 'var(--fc-today-bg-color) !important',
47
+ },
48
+ '& button': {
49
+ transition: 'none !important',
50
+ },
51
+ }}
52
+ />
53
+ );
54
+ }
@@ -0,0 +1,54 @@
1
+ import type { CourtDate } from '@/types';
2
+ import FormControl from '@mui/material/FormControl';
3
+ import InputLabel from '@mui/material/InputLabel';
4
+ import MenuItem from '@mui/material/MenuItem';
5
+ import Select from '@mui/material/Select';
6
+
7
+ export default function EnumDropdown<T extends object>({
8
+ edited,
9
+ setEdited,
10
+ enumObject,
11
+ label,
12
+ }: {
13
+ edited: CourtDate;
14
+ setEdited: (data: CourtDate) => void;
15
+ enumObject: T;
16
+ label: string;
17
+ }) {
18
+ return (
19
+ <FormControl size='medium' sx={{ minWidth: 120 }} fullWidth>
20
+ <InputLabel id={`${label.replace(/\s+/g, '-').toLowerCase()}-select-label`}>{label}</InputLabel>
21
+ <Select
22
+ labelId={`${label.replace(/\s+/g, '-').toLowerCase()}-select-label`}
23
+ value={edited[label as keyof CourtDate] || ''}
24
+ label={label}
25
+ onChange={(e) =>
26
+ setEdited({
27
+ ...edited!,
28
+ [label]: e.target.value,
29
+ })
30
+ }
31
+ MenuProps={{
32
+ MenuListProps: {
33
+ className: 'themed',
34
+ },
35
+ }}
36
+ sx={{
37
+ '& .MuiSelect-icon': {
38
+ color: 'var(--text)',
39
+ },
40
+ }}
41
+ >
42
+ {Object.values(enumObject).map(
43
+ (option) =>
44
+ (option as unknown as string) &&
45
+ option !== 'All' && (
46
+ <MenuItem key={option} value={option}>
47
+ {option}
48
+ </MenuItem>
49
+ ),
50
+ )}
51
+ </Select>
52
+ </FormControl>
53
+ );
54
+ }
@@ -0,0 +1,48 @@
1
+ import Autocomplete from '@mui/material/Autocomplete';
2
+ import TextField from '@mui/material/TextField';
3
+ import { allHearingOfficers } from '@/helpers/people';
4
+ import type { CourtDate } from '@/types';
5
+
6
+ export default function HearingOfficerDropdown({
7
+ label,
8
+ edited,
9
+ setEdited,
10
+ }: {
11
+ label: string;
12
+ edited: CourtDate;
13
+ setEdited: (edited: CourtDate) => void;
14
+ }) {
15
+ // Prepare filtered officer options
16
+ const officerOptions = Object.values(allHearingOfficers).filter((option) => !!option.OfficerName);
17
+
18
+ // Find the selected officer object based on the value in edited
19
+ const selectedOfficer = officerOptions.find((o) => o.OfficerID === edited.HearingOfficer) || null;
20
+
21
+ return (
22
+ <Autocomplete
23
+ options={officerOptions}
24
+ getOptionLabel={(option) => option.OfficerName}
25
+ value={selectedOfficer}
26
+ onChange={(_, newValue) =>
27
+ setEdited({
28
+ ...edited,
29
+ HearingOfficer: newValue ? newValue.OfficerID : null,
30
+ })
31
+ }
32
+ isOptionEqualToValue={(option, value) => option.OfficerID === value.OfficerID}
33
+ renderInput={(params) => <TextField {...params} label={label} fullWidth />}
34
+ size='medium'
35
+ fullWidth
36
+ classes={{ paper: 'themed' }}
37
+ sx={{
38
+ minWidth: 120,
39
+ '& .MuiInputBase-root:hover': {
40
+ backgroundColor: 'var(--fc-today-bg-color) !important',
41
+ },
42
+ '& button': {
43
+ transition: 'none !important',
44
+ },
45
+ }}
46
+ />
47
+ );
48
+ }
@@ -0,0 +1,186 @@
1
+ import Stack from '@mui/material/Stack';
2
+ import { useRef, useState, useCallback, useEffect } from 'react';
3
+ import TrashIcon from '@mui/icons-material/Delete';
4
+ import AddIcon from '@mui/icons-material/Add';
5
+ import TextField from '@mui/material/TextField';
6
+ import Fab from '@mui/material/Fab';
7
+ import Typography from '@mui/material/Typography';
8
+ import { ModalMode } from '@/types';
9
+ import ToggleableTextField from './ToggleableTextField';
10
+ import Box from '@mui/material/Box';
11
+
12
+ export default function TextFieldList({
13
+ textFields,
14
+ setTextFields,
15
+ label,
16
+ fieldLabelPrefix,
17
+ modalMode,
18
+ toggleStates,
19
+ setToggleStates,
20
+ alternateFieldLabelPrefix,
21
+ }: {
22
+ textFields: string[];
23
+ setTextFields: (fields: string[], toggles?: boolean[]) => void;
24
+ label: string;
25
+ fieldLabelPrefix: string;
26
+ modalMode: ModalMode;
27
+ toggleStates?: boolean[];
28
+ setToggleStates?: (toggles: boolean[]) => void;
29
+ alternateFieldLabelPrefix?: string;
30
+ }) {
31
+ const [numFields, setNumFields] = useState<number>(Math.max(textFields.length, 1));
32
+ // Local state for text values to avoid re-rendering parent on every keystroke
33
+ const [localFields, setLocalFields] = useState<string[]>(textFields);
34
+ const boxRef = useRef<HTMLDivElement>(null);
35
+
36
+ // Sync local state when textFields prop changes (e.g., on initial load or external updates)
37
+ useEffect(() => {
38
+ setLocalFields(textFields);
39
+ }, [textFields]);
40
+
41
+ // Sync numFields with textFields length when it changes
42
+ useEffect(() => {
43
+ setNumFields(Math.max(textFields.length, 1));
44
+ }, [textFields.length]);
45
+
46
+ const addField = useCallback(() => {
47
+ setNumFields((prev) => prev + 1);
48
+ setLocalFields((prev) => [...prev, '']);
49
+ // Add default toggle state (false) for new field if toggles are enabled
50
+ if (setToggleStates && toggleStates) {
51
+ const newToggles = [...toggleStates, false];
52
+ setToggleStates(newToggles);
53
+ }
54
+ // scroll to bottom to show new field
55
+ setTimeout(() => {
56
+ if (boxRef.current) {
57
+ boxRef.current.scrollTop = boxRef.current.scrollHeight;
58
+ }
59
+ }, 0);
60
+ }, [setToggleStates, toggleStates]);
61
+
62
+ const updateField = useCallback(
63
+ (index: number, value: string) => {
64
+ // Update local state immediately for responsive typing
65
+ setLocalFields((prev) => {
66
+ const newFields = [...prev];
67
+ newFields[index] = value;
68
+ return newFields;
69
+ });
70
+ },
71
+ [],
72
+ );
73
+
74
+ const syncToParent = useCallback(() => {
75
+ // Sync local state to parent on blur
76
+ const newToggles = toggleStates ? [...toggleStates] : [];
77
+ setTextFields(localFields, newToggles);
78
+ }, [localFields, toggleStates, setTextFields]);
79
+
80
+ const removeField = useCallback(
81
+ (index: number) => {
82
+ const newFields = [...localFields];
83
+ newFields.splice(index, 1);
84
+ const newToggles = toggleStates ? [...toggleStates] : [];
85
+ if (toggleStates) {
86
+ newToggles.splice(index, 1);
87
+ }
88
+ setLocalFields(newFields);
89
+ setTextFields(newFields, newToggles);
90
+ setNumFields(Math.max(newFields.length, 1));
91
+ },
92
+ [localFields, toggleStates, setTextFields],
93
+ );
94
+
95
+ const handleToggleChange = useCallback(
96
+ (index: number, checked: boolean) => {
97
+ if (!setToggleStates || !toggleStates) return;
98
+ const newToggles = [...toggleStates];
99
+ newToggles[index] = checked;
100
+ setToggleStates(newToggles);
101
+ },
102
+ [setToggleStates, toggleStates],
103
+ );
104
+
105
+ return (
106
+ <Stack spacing={2}>
107
+ <Stack direction='row' spacing={2} alignItems='center'>
108
+ <Fab
109
+ color='primary'
110
+ size='small'
111
+ onClick={addField}
112
+ disabled={numFields > textFields.length || modalMode === ModalMode.EDIT}
113
+ style={{
114
+ boxShadow: '1px 1px 3px rgba(0,0,0,0.2)',
115
+ minWidth: 40,
116
+ }}
117
+ >
118
+ <AddIcon fontSize='small' />
119
+ </Fab>
120
+ <Stack direction='column'>
121
+ <Typography variant='h6'>{label}</Typography>
122
+ <Typography variant='body2' sx={{ opacity: 0.85 }}>
123
+ {alternateFieldLabelPrefix
124
+ ? `Add or remove fields as needed. Toggle between ${fieldLabelPrefix} and ${alternateFieldLabelPrefix} for each field.`
125
+ : `Add or remove ${fieldLabelPrefix.toLowerCase()}s as needed. You can add more fields after completing the current ones.`}
126
+ </Typography>
127
+ </Stack>
128
+ </Stack>
129
+
130
+ <Box
131
+ sx={{
132
+ maxHeight: numFields > 3 ? '60vh' : 'none',
133
+ overflow: numFields > 3 ? 'auto' : 'visible',
134
+ paddingY: 1,
135
+ }}
136
+ ref={boxRef}
137
+ >
138
+ <Stack spacing={2}>
139
+ {Array.from({ length: numFields }).map((_, index) => {
140
+ const isToggled = toggleStates && toggleStates[index];
141
+ const currentLabel = isToggled ? alternateFieldLabelPrefix : fieldLabelPrefix;
142
+
143
+ return (
144
+ <Stack direction='row' spacing={1} alignItems='center' key={index}>
145
+ <Fab
146
+ color='error'
147
+ size='small'
148
+ disabled={modalMode === ModalMode.EDIT || numFields <= 1}
149
+ onClick={() => removeField(index)}
150
+ style={{
151
+ minWidth: 40,
152
+ boxShadow: '1px 1px 3px rgba(0,0,0,0.2)',
153
+ }}
154
+ >
155
+ <TrashIcon fontSize='small' />
156
+ </Fab>
157
+
158
+ {toggleStates && setToggleStates && alternateFieldLabelPrefix ? (
159
+ <ToggleableTextField
160
+ index={index}
161
+ value={localFields[index] || ''}
162
+ onChange={(value) => updateField(index, value)}
163
+ onBlur={syncToParent}
164
+ isToggled={isToggled || false}
165
+ onToggleChange={(checked) => handleToggleChange(index, checked)}
166
+ fieldLabelPrefix={fieldLabelPrefix}
167
+ alternateFieldLabelPrefix={alternateFieldLabelPrefix}
168
+ />
169
+ ) : (
170
+ <TextField
171
+ key={index}
172
+ label={`${currentLabel} ${index + 1}`}
173
+ value={localFields[index] || ''}
174
+ onChange={(e) => updateField(index, e.target.value)}
175
+ onBlur={syncToParent}
176
+ fullWidth
177
+ />
178
+ )}
179
+ </Stack>
180
+ );
181
+ })}
182
+ </Stack>
183
+ </Box>
184
+ </Stack>
185
+ );
186
+ }