@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.
- package/.editorconfig +26 -0
- package/README.md +0 -0
- package/biome.json +302 -0
- package/dev/App.tsx +51 -0
- package/dev/main.tsx +10 -0
- package/index.html +12 -0
- package/package.json +54 -0
- package/public/vite.svg +1 -0
- package/src/_components/CCalendar.css +463 -0
- package/src/_components/CCalendar.tsx +726 -0
- package/src/_components/List/CalendarList.tsx +288 -0
- package/src/_components/Modal/CaseDetails/CaseDetails.tsx +414 -0
- package/src/_components/Modal/CaseDetails/EvidenceRow.tsx +83 -0
- package/src/_components/Modal/CaseDetails/EvidenceSection.tsx +94 -0
- package/src/_components/Modal/CreateEdit/CreateEditCase.tsx +241 -0
- package/src/_components/Modal/CreateEdit/DateSelector.tsx +42 -0
- package/src/_components/Modal/CreateEdit/EditUserFieldDropdown.tsx +54 -0
- package/src/_components/Modal/CreateEdit/EnumDropdown.tsx +54 -0
- package/src/_components/Modal/CreateEdit/HearingOfficerDropdown.tsx +48 -0
- package/src/_components/Modal/CreateEdit/TextFieldList.tsx +186 -0
- package/src/_components/Modal/CreateEdit/ToggleableTextField.tsx +91 -0
- package/src/_components/Modal/Modal.css +15 -0
- package/src/_components/Modal/Modal.tsx +325 -0
- package/src/_components/Modal/ModalActions.tsx +99 -0
- package/src/_components/Modal/View/CaseToolbar.tsx +81 -0
- package/src/_components/Modal/View/CaseViewer.tsx +237 -0
- package/src/_components/Modal/View/DateDetails.tsx +138 -0
- package/src/_components/Modal/View/InfoBox.tsx +22 -0
- package/src/_components/Modal/View/InfoBoxBtn.css +39 -0
- package/src/_components/Modal/View/InfoBoxBtn.tsx +29 -0
- package/src/_components/Modal/View/NoticeFileLink.tsx +44 -0
- package/src/_components/Shared/FirstSecondChairIcons.tsx +247 -0
- package/src/_components/Shared/FormRow.tsx +37 -0
- package/src/_components/Shared/MuniDropdown.tsx +94 -0
- package/src/_components/Shared/SearchBar.tsx +87 -0
- package/src/_components/Toolbar/CaseFilter.tsx +77 -0
- package/src/_components/Toolbar/DateTypeFilter.tsx +63 -0
- package/src/_components/Toolbar/HearingTypeFilter.tsx +63 -0
- package/src/_components/Toolbar/Toolbar.tsx +159 -0
- package/src/_components/Toolbar/UserFilter.tsx +105 -0
- package/src/_components/Toolbar/ViewFilter.tsx +48 -0
- package/src/helpers/cache.ts +89 -0
- package/src/helpers/cases.ts +79 -0
- package/src/helpers/courtDates.ts +139 -0
- package/src/helpers/formatter.ts +16 -0
- package/src/helpers/munis.ts +44 -0
- package/src/helpers/people.ts +46 -0
- package/src/index.ts +2 -0
- package/src/types.ts +129 -0
- package/tsconfig.app.json +32 -0
- package/tsconfig.json +4 -0
- package/tsconfig.node.json +30 -0
- 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
|
+
}
|