@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,247 @@
1
+ import { useState } from 'react';
2
+ import { allUsers } from '@/helpers/people';
3
+ import type { User } from '@/types';
4
+ import Avatar from '@mui/material/Avatar';
5
+ import Stack from '@mui/material/Stack';
6
+ import Menu from '@mui/material/Menu';
7
+ import MenuItem from '@mui/material/MenuItem';
8
+ import ListItemAvatar from '@mui/material/ListItemAvatar';
9
+ import ListItemText from '@mui/material/ListItemText';
10
+ import Tooltip from '@mui/material/Tooltip';
11
+
12
+ type ChairPosition = 'first' | 'second';
13
+
14
+ export default function FirstSecondChairIcons({
15
+ user1ID,
16
+ user2ID,
17
+ onUpdateChair,
18
+ }: {
19
+ user1ID: number | null;
20
+ user2ID: number | null;
21
+ onUpdateChair?: (position: ChairPosition, userId: number | null) => void;
22
+ }) {
23
+ const firstChair = user1ID ? allUsers[user1ID] : null;
24
+ const secondChair = user2ID ? allUsers[user2ID] : null;
25
+
26
+ const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
27
+ const [activePosition, setActivePosition] = useState<ChairPosition | null>(null);
28
+ const [otherAnchorEl, setOtherAnchorEl] = useState<HTMLElement | null>(null);
29
+
30
+ const height = 30;
31
+ const width = 30;
32
+
33
+ const primaryUserIds = [1, 10, 22, 23, 24, 29];
34
+ const excludedUserLastnames = ['Import', 'Memos', 'Table', 'Note', 'Robot', 'Ingestion'];
35
+ const allFilteredUsers = Object.values(allUsers).filter(
36
+ (option) => option.UserLastName && !excludedUserLastnames.includes(option.UserLastName),
37
+ );
38
+ const primaryUsers = allFilteredUsers.filter((u) => primaryUserIds.includes(u.UserID));
39
+ const otherUsers = allFilteredUsers.filter((u) => !primaryUserIds.includes(u.UserID));
40
+
41
+ // #region Avatar helper functions
42
+ function stringToColor(string: string) {
43
+ let hash = 0;
44
+ let i;
45
+
46
+ /* eslint-disable no-bitwise */
47
+ for (i = 0; i < string.length; i += 1) {
48
+ hash = string.charCodeAt(i) + ((hash << 5) - hash);
49
+ }
50
+
51
+ let color = '#';
52
+
53
+ for (i = 0; i < 3; i += 1) {
54
+ const value = (hash >> (i * 8)) & 0xff;
55
+ color += `00${value.toString(16)}`.slice(-2);
56
+ }
57
+ /* eslint-enable no-bitwise */
58
+
59
+ return color;
60
+ }
61
+
62
+ function stringAvatar(name: string) {
63
+ return {
64
+ sx: {
65
+ backgroundColor: `${stringToColor(name)} !important`,
66
+ },
67
+ children: `${name.split(' ')[0][0]}${name.split(' ')[1][0]}`,
68
+ };
69
+ }
70
+
71
+ function decideIcon(user: User | null, position: ChairPosition) {
72
+ const handleClick = (event: React.MouseEvent<HTMLElement>) => {
73
+ if (onUpdateChair) {
74
+ event.stopPropagation();
75
+ setAnchorEl(event.currentTarget);
76
+ setActivePosition(position);
77
+ }
78
+ };
79
+
80
+ const tooltipTitle = user ? `${user.UserFirstName} ${user.UserLastName}` : 'Unassigned';
81
+
82
+ if (!user) {
83
+ return (
84
+ <Tooltip title={tooltipTitle} arrow>
85
+ <Avatar
86
+ sx={{ width: width, height: height, cursor: onUpdateChair ? 'pointer' : 'default' }}
87
+ onClick={handleClick}
88
+ className='CCAvatarIcon'
89
+ >
90
+ ?
91
+ </Avatar>
92
+ </Tooltip>
93
+ );
94
+ }
95
+ if (user.Picture) {
96
+ return (
97
+ <Tooltip title={tooltipTitle} arrow>
98
+ <Avatar
99
+ alt={`${user.UserFirstName} ${user.UserLastName}`}
100
+ src={user.Picture}
101
+ sx={{ width: width, height: height, cursor: onUpdateChair ? 'pointer' : 'default' }}
102
+ onClick={handleClick}
103
+ className='CCAvatarIcon'
104
+ />
105
+ </Tooltip>
106
+ );
107
+ } else {
108
+ const avatarProps = stringAvatar(`${user.UserFirstName} ${user.UserLastName}`);
109
+ return (
110
+ <Tooltip title={tooltipTitle} arrow placement='top'>
111
+ <Avatar
112
+ sx={{
113
+ ...avatarProps.sx,
114
+ width: width,
115
+ height: height,
116
+ cursor: onUpdateChair ? 'pointer' : 'default',
117
+ }}
118
+ onClick={handleClick}
119
+ className='CCAvatarIcon'
120
+ >
121
+ {avatarProps.children}
122
+ </Avatar>
123
+ </Tooltip>
124
+ );
125
+ }
126
+ }
127
+ // #endregion
128
+
129
+ function handleMenuClose() {
130
+ setAnchorEl(null);
131
+ setActivePosition(null);
132
+ setOtherAnchorEl(null);
133
+ }
134
+
135
+ function handleUserSelect(userId: number | null) {
136
+ const position = activePosition;
137
+ // Close menu immediately for snappy UX
138
+ handleMenuClose();
139
+ // Defer update to next tick so menu close renders first
140
+ if (onUpdateChair && position) {
141
+ setTimeout(() => onUpdateChair(position, userId), 0);
142
+ }
143
+ }
144
+
145
+ return (
146
+ <>
147
+ <Stack
148
+ direction='row'
149
+ spacing={1}
150
+ alignItems='center'
151
+ mt={1.2}
152
+ justifyContent='center'
153
+ className='iconHolder'
154
+ onClick={onUpdateChair ? (e) => e.stopPropagation() : undefined}
155
+ >
156
+ {decideIcon(firstChair, 'first')}
157
+ {decideIcon(secondChair, 'second')}
158
+ </Stack>
159
+ <Menu
160
+ anchorEl={anchorEl}
161
+ open={Boolean(anchorEl)}
162
+ onClose={handleMenuClose}
163
+ onClick={(e) => e.stopPropagation()}
164
+ classes={{ paper: 'themed' }}
165
+ transitionDuration={200}
166
+ >
167
+ <MenuItem onClick={() => handleUserSelect(null)}>
168
+ <ListItemAvatar>
169
+ <Avatar sx={{ width: 24, height: 24, fontSize: 11 }}>?</Avatar>
170
+ </ListItemAvatar>
171
+ <ListItemText primary='Unassign' />
172
+ </MenuItem>
173
+ {primaryUsers.map((user) => (
174
+ <MenuItem key={user.UserID} onClick={() => handleUserSelect(user.UserID)}>
175
+ <ListItemAvatar>
176
+ {user.Picture ? (
177
+ <Avatar
178
+ alt={`${user.UserFirstName} ${user.UserLastName}`}
179
+ src={user.Picture}
180
+ sx={{ width: 24, height: 24 }}
181
+ />
182
+ ) : (
183
+ (() => {
184
+ const avatarProps = stringAvatar(`${user.UserFirstName} ${user.UserLastName}`);
185
+ return (
186
+ <Avatar sx={{ ...avatarProps.sx, width: 24, height: 24, fontSize: 11 }}>
187
+ {avatarProps.children}
188
+ </Avatar>
189
+ );
190
+ })()
191
+ )}
192
+ </ListItemAvatar>
193
+ <ListItemText primary={`${user.UserFirstName} ${user.UserLastName}`} />
194
+ </MenuItem>
195
+ ))}
196
+ {otherUsers.length > 0 && (
197
+ <MenuItem
198
+ onMouseEnter={(e) => setOtherAnchorEl(e.currentTarget)}
199
+ sx={{ justifyContent: 'space-between' }}
200
+ >
201
+ <ListItemText primary='Other' />
202
+ <span style={{ fontSize: 12, marginLeft: 8 }}>&#9654;</span>
203
+ </MenuItem>
204
+ )}
205
+ </Menu>
206
+ <Menu
207
+ anchorEl={otherAnchorEl}
208
+ open={Boolean(otherAnchorEl)}
209
+ onClose={() => setOtherAnchorEl(null)}
210
+ onClick={(e) => e.stopPropagation()}
211
+ anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
212
+ transformOrigin={{ vertical: 'top', horizontal: 'left' }}
213
+ classes={{ paper: 'themed' }}
214
+ transitionDuration={150}
215
+ slotProps={{
216
+ paper: {
217
+ onMouseLeave: () => setOtherAnchorEl(null),
218
+ },
219
+ }}
220
+ >
221
+ {otherUsers.map((user) => (
222
+ <MenuItem key={user.UserID} onClick={() => handleUserSelect(user.UserID)}>
223
+ <ListItemAvatar>
224
+ {user.Picture ? (
225
+ <Avatar
226
+ alt={`${user.UserFirstName} ${user.UserLastName}`}
227
+ src={user.Picture}
228
+ sx={{ width: 24, height: 24 }}
229
+ />
230
+ ) : (
231
+ (() => {
232
+ const avatarProps = stringAvatar(`${user.UserFirstName} ${user.UserLastName}`);
233
+ return (
234
+ <Avatar sx={{ ...avatarProps.sx, width: 24, height: 24, fontSize: 11 }}>
235
+ {avatarProps.children}
236
+ </Avatar>
237
+ );
238
+ })()
239
+ )}
240
+ </ListItemAvatar>
241
+ <ListItemText primary={`${user.UserFirstName} ${user.UserLastName}`} />
242
+ </MenuItem>
243
+ ))}
244
+ </Menu>
245
+ </>
246
+ );
247
+ }
@@ -0,0 +1,37 @@
1
+ import Box from '@mui/material/Box';
2
+ import Stack from '@mui/material/Stack';
3
+ import Typography from '@mui/material/Typography';
4
+
5
+ export default function FormRow({
6
+ children,
7
+ gap = 7,
8
+ header,
9
+ subheader,
10
+ }: {
11
+ children: React.ReactNode;
12
+ gap?: number;
13
+ header?: string;
14
+ subheader?: string;
15
+ }) {
16
+ return (
17
+ <>
18
+ <Stack direction='column' spacing={0.5} mb={header !== undefined ? 3 : 0}>
19
+ <Typography variant='h6'>{header}</Typography>
20
+ <Typography variant='body2' sx={{ opacity: 0.85 }}>
21
+ {subheader}
22
+ </Typography>
23
+ </Stack>
24
+ <Box
25
+ marginTop={1}
26
+ marginBottom={1}
27
+ display='flex'
28
+ flexDirection='row'
29
+ alignItems={'center'}
30
+ justifyContent={'space-between'}
31
+ gap={gap}
32
+ >
33
+ {children}
34
+ </Box>
35
+ </>
36
+ );
37
+ }
@@ -0,0 +1,94 @@
1
+ import { allMuniNames } from '@/helpers/munis';
2
+ import Autocomplete from '@mui/material/Autocomplete';
3
+ import TextField from '@mui/material/TextField';
4
+ import { useState, useEffect, use } from 'react';
5
+ import { isVillageDate } from '@/helpers/courtDates';
6
+
7
+ // Normalize muni code to 5 digits (pad with "00" if needed)
8
+ const normalizeMuniCode = (code: string): string => {
9
+ if (!code) return code;
10
+ return code.length < 5 ? code + '00' : code;
11
+ };
12
+
13
+ export default function MuniDropdown({
14
+ selectedMuni,
15
+ setSelectedMuni,
16
+ allowClear,
17
+ size,
18
+ }: {
19
+ selectedMuni: string | null;
20
+ setSelectedMuni: (muni: string) => void;
21
+ allowClear: boolean;
22
+ size: 'small' | 'medium';
23
+ }) {
24
+ const [muniOptions, setMuniOptions] = useState<string[]>([]);
25
+ const [selectedValue, setSelectedValue] = useState<string | null>(null);
26
+
27
+ useEffect(() => {
28
+ const options: string[] = [];
29
+ Object.keys(allMuniNames).forEach((key) => {
30
+ let muniName = key;
31
+ let isVillage = isVillageDate(muniName);
32
+ const village = isVillage
33
+ ? (allMuniNames[key].Village || '').replace(/\s+$/, '')
34
+ : (allMuniNames[key].Township || '').replace(/\s+$/, '');
35
+ options.push(`(${muniName}) ${village}, ${allMuniNames[key].County}`);
36
+ });
37
+ setMuniOptions(options);
38
+ const normalizedCode = normalizeMuniCode(selectedMuni || '');
39
+ setSelectedValue(normalizedCode ? options.find((option) => option.includes(`(${normalizedCode})`)) || null : null);
40
+ }, [allMuniNames]);
41
+
42
+ useEffect(() => {
43
+ const normalizedCode = normalizeMuniCode(selectedMuni || '');
44
+ setSelectedValue(
45
+ normalizedCode ? muniOptions.find((option) => option.includes(`(${normalizedCode})`)) || null : null,
46
+ );
47
+ }, [selectedMuni, muniOptions]);
48
+
49
+ function handleMuniChange(event: React.SyntheticEvent, value: string | null) {
50
+ setSelectedValue(value);
51
+ console.log('Selected Muni:', value);
52
+ const municode = value ? value.match(/\(([^)]+)\)/)?.[1] : '';
53
+ console.log('Muni Code:', municode);
54
+ setSelectedMuni(municode || '');
55
+ }
56
+
57
+ return (
58
+ <Autocomplete
59
+ options={muniOptions}
60
+ renderInput={(params) => (
61
+ <TextField
62
+ {...params}
63
+ label='Muni Code'
64
+ slotProps={{
65
+ inputLabel: {
66
+ sx: {
67
+ paddingTop: size === 'small' ? '1.4px' : 'unset',
68
+ },
69
+ },
70
+ }}
71
+ />
72
+ )}
73
+ onChange={handleMuniChange}
74
+ disableClearable={!allowClear}
75
+ fullWidth
76
+ value={selectedValue || ''}
77
+ size={size}
78
+ slotProps={{
79
+ paper: { className: 'themed' },
80
+ }}
81
+ sx={{
82
+ '& .MuiInputBase-root': {
83
+ minHeight: size === 'small' ? '43.6px !important' : 'unset',
84
+ },
85
+ '& .MuiInputBase-root:hover': {
86
+ backgroundColor: 'var(--fc-today-bg-color) !important',
87
+ },
88
+ '& button': {
89
+ transition: 'none !important',
90
+ },
91
+ }}
92
+ />
93
+ );
94
+ }
@@ -0,0 +1,87 @@
1
+ import InputAdornment from '@mui/material/InputAdornment';
2
+ import TextField from '@mui/material/TextField';
3
+ import CancelIcon from '@mui/icons-material/Cancel';
4
+ import SearchIcon from '@mui/icons-material/Search';
5
+ import IconButton from '@mui/material/IconButton';
6
+ import { useState, useEffect, useRef } from 'react';
7
+
8
+ export default function SearchBar({
9
+ searchTerm,
10
+ setSearchTerm,
11
+ width,
12
+ }: {
13
+ searchTerm: string;
14
+ setSearchTerm: (term: string) => void;
15
+ width?: string | number;
16
+ }) {
17
+ const [inputValue, setInputValue] = useState(searchTerm);
18
+ const inputRef = useRef<HTMLInputElement>(null);
19
+
20
+ // Keep local inputValue in sync with searchTerm prop
21
+ useEffect(() => {
22
+ setInputValue(searchTerm);
23
+ }, [searchTerm]);
24
+
25
+ useEffect(() => {
26
+ const handler = setTimeout(() => {
27
+ if (inputValue !== searchTerm) {
28
+ setSearchTerm(inputValue);
29
+ }
30
+ }, 300);
31
+ return () => clearTimeout(handler);
32
+ }, [inputValue, searchTerm, setSearchTerm]);
33
+
34
+ return (
35
+ <TextField
36
+ aria-label='Search'
37
+ placeholder='Search...'
38
+ size='small'
39
+ variant='outlined'
40
+ value={inputValue}
41
+ onChange={(e) => setInputValue(e.target.value)}
42
+ fullWidth={width === undefined}
43
+ style={{ width: width ?? '100%' }}
44
+ sx={{
45
+ '& .MuiOutlinedInput-root:hover': {
46
+ backgroundColor: 'var(--fc-today-bg-color) !important',
47
+ '& .MuiInputAdornment-root': {
48
+ backgroundColor: 'inherit !important',
49
+ },
50
+ },
51
+ }}
52
+ slotProps={{
53
+ input: {
54
+ ref: inputRef,
55
+ sx: { paddingY: '2px' },
56
+ startAdornment: (
57
+ <InputAdornment
58
+ position='start'
59
+ onClick={() => inputRef.current?.focus()}
60
+ sx={{ cursor: 'pointer' }}
61
+ >
62
+ <SearchIcon
63
+ fontSize='small'
64
+ sx={{
65
+ userSelect: 'none',
66
+ pointerEvents: 'none',
67
+ }}
68
+ />
69
+ </InputAdornment>
70
+ ),
71
+ endAdornment: inputValue ? (
72
+ <InputAdornment position='end'>
73
+ <IconButton
74
+ size='small'
75
+ aria-label='Clear search'
76
+ sx={{ marginRight: -0.75, paddingY: '2px' }}
77
+ onClick={() => setInputValue('')}
78
+ >
79
+ <CancelIcon fontSize='small' />
80
+ </IconButton>
81
+ </InputAdornment>
82
+ ) : null,
83
+ },
84
+ }}
85
+ />
86
+ );
87
+ }
@@ -0,0 +1,77 @@
1
+ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
2
+ import type { CalendarFilterCtx } from '@/types';
3
+ import { useEffect, useState } from 'react';
4
+ import ToggleButton from '@mui/material/ToggleButton';
5
+ import Tooltip from '@mui/material/Tooltip';
6
+ import PendingActionsIcon from '@mui/icons-material/PendingActions';
7
+ import WarningAmberIcon from '@mui/icons-material/WarningAmber';
8
+ import ManageSearchIcon from '@mui/icons-material/ManageSearch';
9
+ import CircularProgress from '@mui/material/CircularProgress';
10
+
11
+ export default function CaseFilter({
12
+ filterCtx,
13
+ setFilterCtx,
14
+ isFetchingCases,
15
+ }: {
16
+ filterCtx: CalendarFilterCtx;
17
+ setFilterCtx: (ctx: CalendarFilterCtx) => void;
18
+ isFetchingCases: boolean;
19
+ }) {
20
+ const [filters, setFilters] = useState<string[]>([]);
21
+
22
+ function updateFilters() {
23
+ const newFilters: string[] = [];
24
+ if (filterCtx.showOnlyUnsettled) newFilters.push('unsettled');
25
+ if (filterCtx.showOnlyWithoutEvidence) newFilters.push('noevidence');
26
+ if (filterCtx.showOnlyUnreviewed) newFilters.push('unreviewed');
27
+ setFilters(newFilters);
28
+ }
29
+
30
+ useEffect(() => {
31
+ updateFilters();
32
+ }, [filterCtx]);
33
+
34
+ useEffect(() => {
35
+ updateFilters();
36
+ }, []);
37
+
38
+ function handleFilterChange(e: React.MouseEvent<HTMLElement>, newValue: string[]) {
39
+ setFilterCtx({
40
+ ...filterCtx,
41
+ showOnlyUnsettled: newValue.includes('unsettled'),
42
+ showOnlyWithoutEvidence: newValue.includes('noevidence'),
43
+ showOnlyUnreviewed: newValue.includes('unreviewed'),
44
+ });
45
+ }
46
+
47
+ return (
48
+ <ToggleButtonGroup value={filters} onChange={handleFilterChange}>
49
+ <Tooltip title='Show Only Unsettled ' arrow placement='top'>
50
+ <ToggleButton value='unsettled'>
51
+ <PendingActionsIcon fontSize='small' />
52
+ </ToggleButton>
53
+ </Tooltip>
54
+ <Tooltip title='Cases Without Evidence' arrow placement='top'>
55
+ <ToggleButton value='noevidence'>
56
+ <WarningAmberIcon fontSize='small' />
57
+ </ToggleButton>
58
+ </Tooltip>
59
+ <Tooltip title='Show Only Unreviewed ' arrow placement='top'>
60
+ <ToggleButton value='unreviewed'>
61
+ <ManageSearchIcon fontSize='small' />
62
+ </ToggleButton>
63
+ </Tooltip>
64
+ <Tooltip title={isFetchingCases ? 'Updating Cases...' : ''} arrow placement='top'>
65
+ <CircularProgress
66
+ size={24}
67
+ sx={{
68
+ color: 'var(--text)',
69
+ visibility: isFetchingCases ? 'visible' : 'hidden',
70
+ marginLeft: '8px',
71
+ marginTop: '8px',
72
+ }}
73
+ />
74
+ </Tooltip>
75
+ </ToggleButtonGroup>
76
+ );
77
+ }
@@ -0,0 +1,63 @@
1
+ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
2
+ import type { CalendarFilterCtx } from '@/types';
3
+ import { useEffect, useState } from 'react';
4
+ import ToggleButton from '@mui/material/ToggleButton';
5
+ import Tooltip from '@mui/material/Tooltip';
6
+ import FileUploadIcon from '@mui/icons-material/FileUpload';
7
+ import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
8
+ import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
9
+
10
+ export default function DateTypeFilter({
11
+ filterCtx,
12
+ setFilterCtx,
13
+ }: {
14
+ filterCtx: CalendarFilterCtx;
15
+ setFilterCtx: (ctx: CalendarFilterCtx) => void;
16
+ }) {
17
+ const [filters, setFilters] = useState<string[]>([]);
18
+
19
+ function updateFilters() {
20
+ const newFilters: string[] = [];
21
+ if (filterCtx.showUploadDeadlines) newFilters.push('upload');
22
+ if (filterCtx.showAdjournmentDates) newFilters.push('adjournment');
23
+ if (filterCtx.showCourtDates) newFilters.push('courtdate');
24
+ setFilters(newFilters);
25
+ }
26
+
27
+ useEffect(() => {
28
+ updateFilters();
29
+ }, [filterCtx]);
30
+
31
+ useEffect(() => {
32
+ updateFilters();
33
+ }, []);
34
+
35
+ function handleFilterChange(e: React.MouseEvent<HTMLElement>, newValue: string[]) {
36
+ setFilterCtx({
37
+ ...filterCtx,
38
+ showUploadDeadlines: newValue.includes('upload'),
39
+ showAdjournmentDates: newValue.includes('adjournment'),
40
+ showCourtDates: newValue.includes('courtdate'),
41
+ });
42
+ }
43
+
44
+ return (
45
+ <ToggleButtonGroup value={filters} onChange={handleFilterChange}>
46
+ <Tooltip title='Court Dates' arrow placement='top'>
47
+ <ToggleButton value='courtdate'>
48
+ <CalendarMonthIcon fontSize='small' />
49
+ </ToggleButton>
50
+ </Tooltip>
51
+ <Tooltip title='Adjournment Dates' arrow placement='top'>
52
+ <ToggleButton value='adjournment'>
53
+ <HourglassEmptyIcon fontSize='small' />
54
+ </ToggleButton>
55
+ </Tooltip>
56
+ <Tooltip title='Upload Deadlines' arrow placement='top'>
57
+ <ToggleButton value='upload'>
58
+ <FileUploadIcon fontSize='small' />
59
+ </ToggleButton>
60
+ </Tooltip>
61
+ </ToggleButtonGroup>
62
+ );
63
+ }
@@ -0,0 +1,63 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { CalendarFilterCtx } from '@/types';
3
+ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
4
+ import ToggleButton from '@mui/material/ToggleButton';
5
+ import ComputerIcon from '@mui/icons-material/Computer';
6
+ import DriveEtaIcon from '@mui/icons-material/DriveEta';
7
+ import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
8
+ import Tooltip from '@mui/material/Tooltip';
9
+
10
+ export default function HearingTypeFilter({
11
+ filterCtx,
12
+ setFilterCtx,
13
+ }: {
14
+ filterCtx: CalendarFilterCtx;
15
+ setFilterCtx: (ctx: CalendarFilterCtx) => void;
16
+ }) {
17
+ const [filters, setFilters] = useState<string[]>([]);
18
+
19
+ function updateFilters() {
20
+ const newFilters: string[] = [];
21
+ if (filterCtx.showVirtual) newFilters.push('virtual');
22
+ if (filterCtx.showInPerson) newFilters.push('inPerson');
23
+ if (filterCtx.showUnknown) newFilters.push('unknown');
24
+ setFilters(newFilters);
25
+ }
26
+
27
+ useEffect(() => {
28
+ updateFilters();
29
+ }, [filterCtx]);
30
+
31
+ useEffect(() => {
32
+ updateFilters();
33
+ }, []);
34
+
35
+ function handleFilterChange(e: React.MouseEvent<HTMLElement>, newValue: string[]) {
36
+ setFilterCtx({
37
+ ...filterCtx,
38
+ showVirtual: newValue.includes('virtual'),
39
+ showInPerson: newValue.includes('inPerson'),
40
+ showUnknown: newValue.includes('unknown'),
41
+ });
42
+ }
43
+
44
+ return (
45
+ <ToggleButtonGroup value={filters} onChange={handleFilterChange}>
46
+ <Tooltip title='Virtual Hearing' arrow placement='top'>
47
+ <ToggleButton value='virtual'>
48
+ <ComputerIcon fontSize='small' sx={{ color: 'var(--fc-event-virtual-color)' }} />
49
+ </ToggleButton>
50
+ </Tooltip>
51
+ <Tooltip title='In Person Hearing' arrow placement='top'>
52
+ <ToggleButton value='inPerson'>
53
+ <DriveEtaIcon fontSize='small' sx={{ color: 'var(--fc-event-inperson-color)' }} />
54
+ </ToggleButton>
55
+ </Tooltip>
56
+ <Tooltip title='Other Hearing Type' arrow placement='top'>
57
+ <ToggleButton value='unknown'>
58
+ <QuestionMarkIcon fontSize='small' sx={{ color: 'var(--fc-event-unknown-color)' }} />
59
+ </ToggleButton>
60
+ </Tooltip>
61
+ </ToggleButtonGroup>
62
+ );
63
+ }