@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,159 @@
1
+ import { useEffect, useState } from 'react';
2
+ import type { Calendar, CalendarApi } from '@fullcalendar/core/index.js';
3
+ import NavigateNext from '@mui/icons-material/NavigateNext';
4
+ import NavigateBefore from '@mui/icons-material/NavigateBefore';
5
+ import Add from '@mui/icons-material/Add';
6
+ import Box from '@mui/material/Box';
7
+ import Button from '@mui/material/Button';
8
+ import Typography from '@mui/material/Typography';
9
+ import FormControl from '@mui/material/FormControl';
10
+ import type { CalendarFilterCtx } from '../../types';
11
+ import DateTypeFilter from './DateTypeFilter';
12
+ import ViewFilter from './ViewFilter';
13
+ import HearingTypeFilter from './HearingTypeFilter';
14
+ import Stack from '@mui/material/Stack';
15
+ import UserFilter from './UserFilter';
16
+ import SearchBar from '../Shared/SearchBar';
17
+ import CaseFilter from './CaseFilter';
18
+ import MuniDropdown from '../Shared/MuniDropdown';
19
+
20
+ export default function Toolbar({
21
+ calendarApi,
22
+ filterCtx,
23
+ setFilterCtx,
24
+ handleCreateClick,
25
+ currentView,
26
+ setCurrentView,
27
+ setCurrentDate,
28
+ activeUser,
29
+ isFetchingCases,
30
+ }: {
31
+ calendarApi: Calendar | null;
32
+ filterCtx: CalendarFilterCtx;
33
+ setFilterCtx: (ctx: CalendarFilterCtx) => void;
34
+ handleCreateClick: () => void;
35
+ currentView: string;
36
+ setCurrentView: (view: string) => void;
37
+ currentDate: Date;
38
+ setCurrentDate: (date: Date) => void;
39
+ activeUser: number | null;
40
+ isFetchingCases: boolean;
41
+ }) {
42
+ const [currentTitle, setCurrentTitle] = useState('');
43
+
44
+ useEffect(() => {
45
+ if (calendarApi) {
46
+ const handleDatesSet = () => {
47
+ setCurrentTitle(calendarApi.getCurrentData().viewTitle);
48
+ };
49
+ calendarApi.on('datesSet', handleDatesSet);
50
+ return () => {
51
+ calendarApi.off('datesSet', handleDatesSet);
52
+ };
53
+ }
54
+ }, [calendarApi]);
55
+
56
+ useEffect(() => {
57
+ if (calendarApi) {
58
+ if (currentView === 'tableView') {
59
+ setCurrentTitle(calendarApi.getDate().toLocaleDateString('en-US', { month: 'long', year: 'numeric' }));
60
+ return;
61
+ }
62
+ setCurrentTitle(calendarApi.getCurrentData().viewTitle);
63
+ }
64
+ }, [calendarApi, currentView]);
65
+
66
+ function goToToday() {
67
+ if (calendarApi) {
68
+ calendarApi.today();
69
+ setCurrentDate(new Date());
70
+ setCurrentTitle(calendarApi.getCurrentData().viewTitle);
71
+ }
72
+ }
73
+
74
+ function handleNext() {
75
+ if (calendarApi) {
76
+ calendarApi.next();
77
+ setCurrentDate(calendarApi.getDate());
78
+ setCurrentTitle(calendarApi.getCurrentData().viewTitle);
79
+ }
80
+ }
81
+
82
+ function handlePrev() {
83
+ if (calendarApi) {
84
+ calendarApi.prev();
85
+ setCurrentDate(calendarApi.getDate());
86
+ setCurrentTitle(calendarApi.getCurrentData().viewTitle);
87
+ }
88
+ }
89
+
90
+ return (
91
+ <Stack
92
+ direction={'row'}
93
+ display='flex'
94
+ alignItems='center'
95
+ justifyContent='space-between'
96
+ sx={{ p: 2, gap: 2 }}
97
+ className='themed'
98
+ maxWidth='100%'
99
+ flexWrap={'wrap'}
100
+ >
101
+ <Box display='flex' alignItems='center' gap={2}>
102
+ <FormControl size='small'>
103
+ <ViewFilter currentView={currentView} setCurrentView={setCurrentView} />
104
+ </FormControl>
105
+
106
+ <FormControl size='small'>
107
+ <HearingTypeFilter filterCtx={filterCtx} setFilterCtx={setFilterCtx} />
108
+ </FormControl>
109
+ <FormControl size='small'>
110
+ <DateTypeFilter filterCtx={filterCtx} setFilterCtx={setFilterCtx} />
111
+ </FormControl>
112
+ <FormControl size='small'>
113
+ <CaseFilter filterCtx={filterCtx} setFilterCtx={setFilterCtx} isFetchingCases={isFetchingCases} />
114
+ </FormControl>
115
+ </Box>
116
+ <Box display='flex' alignItems='center' gap={2}>
117
+ <FormControl size='small'>
118
+ <SearchBar
119
+ searchTerm={filterCtx.searchTerm}
120
+ setSearchTerm={(term) => setFilterCtx({ ...filterCtx, searchTerm: term })}
121
+ width={200}
122
+ />
123
+ </FormControl>
124
+ <FormControl size='small'>
125
+ <UserFilter filterCtx={filterCtx} setFilterCtx={setFilterCtx} activeUser={activeUser} />
126
+ </FormControl>
127
+ <FormControl size='small' sx={{ minWidth: 250, minHeight: 45 }}>
128
+ <MuniDropdown
129
+ selectedMuni={filterCtx.municode || ''}
130
+ setSelectedMuni={(muni) => setFilterCtx({ ...filterCtx, municode: muni })}
131
+ allowClear={true}
132
+ size='small'
133
+ />
134
+ </FormControl>
135
+ </Box>
136
+ <Box display='flex' alignItems='center' gap={1}>
137
+ <Typography
138
+ variant='h6'
139
+ component='div'
140
+ sx={{ flexGrow: 1, textAlign: 'center', maxWidth: '200px', whiteSpace: 'nowrap' }}
141
+ >
142
+ {currentTitle}
143
+ </Typography>
144
+ <Button size='medium' onClick={handleCreateClick}>
145
+ <Add fontSize='medium' />
146
+ </Button>
147
+ <Button variant='outlined' size='medium' onClick={goToToday}>
148
+ Today
149
+ </Button>
150
+ <Button size='medium' onClick={handlePrev}>
151
+ <NavigateBefore fontSize='medium' />
152
+ </Button>
153
+ <Button size='medium' onClick={handleNext}>
154
+ <NavigateNext fontSize='medium' />
155
+ </Button>
156
+ </Box>
157
+ </Stack>
158
+ );
159
+ }
@@ -0,0 +1,105 @@
1
+ import Stack from '@mui/material/Stack';
2
+ import type { CalendarFilterCtx, User } from '../../types';
3
+ import Tooltip from '@mui/material/Tooltip';
4
+ import PersonIcon from '@mui/icons-material/Person';
5
+ import IconButton from '@mui/material/IconButton';
6
+ import { allUsers } from '@/helpers/people';
7
+ import Autocomplete from '@mui/material/Autocomplete';
8
+ import TextField from '@mui/material/TextField';
9
+
10
+ export default function UserFilter({
11
+ filterCtx,
12
+ setFilterCtx,
13
+ activeUser,
14
+ }: {
15
+ filterCtx: CalendarFilterCtx;
16
+ setFilterCtx: (ctx: CalendarFilterCtx) => void;
17
+ activeUser: number | null;
18
+ }) {
19
+ function handleAssignedToMeClick() {
20
+ if (activeUser) {
21
+ setFilterCtx({
22
+ ...filterCtx,
23
+ showOnlyAssignedToUser: activeUser,
24
+ });
25
+ }
26
+ }
27
+ const excludedUserLastnames = ['Import', 'Memos', 'Table', 'Note', 'Robot', 'Ingestion'];
28
+
29
+ // Prepare filtered user options
30
+ const userOptions = Object.values(allUsers).filter(
31
+ (option) => option.UserLastName && !excludedUserLastnames.includes(option.UserLastName),
32
+ );
33
+
34
+ return (
35
+ <Stack direction='row' spacing={0} alignItems='center' borderRadius={'4px'} border={1}>
36
+ <Tooltip title='Me' arrow placement='top'>
37
+ <IconButton
38
+ onClick={handleAssignedToMeClick}
39
+ sx={{
40
+ border: 'none',
41
+ borderRadius: 0,
42
+ p: '11px',
43
+ boxShadow: 'none',
44
+ background: 'none',
45
+ }}
46
+ size='medium'
47
+ >
48
+ <PersonIcon fontSize='small' />
49
+ </IconButton>
50
+ </Tooltip>
51
+ <Tooltip title='Assigned to' arrow placement='top'>
52
+ <Autocomplete
53
+ size='small'
54
+ options={userOptions}
55
+ getOptionLabel={(option: User) => `${option.UserFirstName} ${option.UserLastName}`}
56
+ value={userOptions.find((u) => u.UserID === filterCtx.showOnlyAssignedToUser) || null}
57
+ onChange={(_, newValue) =>
58
+ setFilterCtx({
59
+ ...filterCtx,
60
+ showOnlyAssignedToUser: newValue ? newValue.UserID : null,
61
+ })
62
+ }
63
+ renderInput={(params) => (
64
+ <TextField
65
+ {...params}
66
+ label='Assigned to'
67
+ fullWidth
68
+ slotProps={{
69
+ input: {
70
+ ...params.InputProps,
71
+ sx: {
72
+ '& .MuiOutlinedInput-notchedOutline': {
73
+ border: 'none',
74
+ },
75
+ '&:hover': {
76
+ backgroundColor: 'var(--fc-today-bg-color) !important',
77
+ },
78
+ '& button': {
79
+ transition: 'none !important',
80
+ },
81
+ },
82
+ },
83
+ }}
84
+ />
85
+ )}
86
+ sx={{
87
+ minWidth: 150,
88
+ width: 150,
89
+ border: 'none',
90
+ borderRadius: 0,
91
+ boxShadow: 'none',
92
+ background: 'none',
93
+ '& .MuiInputBase-root:hover': {
94
+ backgroundColor: 'var(--fc-today-bg-color) !important',
95
+ },
96
+ '& button': {
97
+ transition: 'none !important',
98
+ },
99
+ }}
100
+ classes={{ paper: 'themed' }}
101
+ />
102
+ </Tooltip>
103
+ </Stack>
104
+ );
105
+ }
@@ -0,0 +1,48 @@
1
+ import ToggleButton from '@mui/material/ToggleButton';
2
+ import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
3
+ import Tooltip from '@mui/material/Tooltip';
4
+ import CalendarViewMonthIcon from '@mui/icons-material/CalendarViewMonth';
5
+ import CalendarViewWeekIcon from '@mui/icons-material/CalendarViewWeek';
6
+ import CalendarViewDayIcon from '@mui/icons-material/CalendarViewDay';
7
+ import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
8
+
9
+ export default function ViewFilter({
10
+ currentView,
11
+ setCurrentView,
12
+ }: {
13
+ currentView: string;
14
+ setCurrentView: (view: string) => void;
15
+ }) {
16
+ return (
17
+ <ToggleButtonGroup
18
+ value={currentView}
19
+ onChange={(event, newView) => {
20
+ if (newView !== null) {
21
+ setCurrentView(newView);
22
+ }
23
+ }}
24
+ exclusive
25
+ >
26
+ <Tooltip title='Month' arrow placement='top'>
27
+ <ToggleButton value='dayGridMonth'>
28
+ <CalendarViewMonthIcon fontSize='small' />
29
+ </ToggleButton>
30
+ </Tooltip>
31
+ <Tooltip title='Week' arrow placement='top'>
32
+ <ToggleButton value='dayGridWeek'>
33
+ <CalendarViewWeekIcon fontSize='small' />
34
+ </ToggleButton>
35
+ </Tooltip>
36
+ <Tooltip title='Day' arrow placement='top'>
37
+ <ToggleButton value='listDay'>
38
+ <CalendarViewDayIcon fontSize='small' />
39
+ </ToggleButton>
40
+ </Tooltip>
41
+ <Tooltip title='Table' arrow placement='top'>
42
+ <ToggleButton value='tableView'>
43
+ <FormatListBulletedIcon fontSize='small' />
44
+ </ToggleButton>
45
+ </Tooltip>
46
+ </ToggleButtonGroup>
47
+ );
48
+ }
@@ -0,0 +1,89 @@
1
+ import type { Case, CourtDate } from '@/types';
2
+ import Dexie, { type Table } from 'dexie';
3
+
4
+ const CASES_CACHE_EXPIRY = 1000 * 60 * 60; // 1 hour
5
+ const COURT_DATES_CACHE_EXPIRY = 1000 * 60 * 5; // 5 minutes
6
+
7
+ // Define Dexie database
8
+ class CasesDB extends Dexie {
9
+ cases!: Table<{ courtDateId: string; data: Case[]; timestamp: number }, string>;
10
+ courtDates!: Table<{ id: string; data: CourtDate[]; timestamp: number }, string>;
11
+ constructor() {
12
+ super('CourtCasesDB');
13
+ this.version(1).stores({
14
+ cases: 'courtDateId',
15
+ });
16
+ this.version(2).stores({
17
+ cases: 'courtDateId',
18
+ courtDates: 'id',
19
+ });
20
+ }
21
+ }
22
+
23
+ const db = new CasesDB();
24
+
25
+ // Get all cached cases (for all court dates)
26
+ export async function getCasesCache(): Promise<{
27
+ [courtDateId: string]: { data: Case[]; timestamp: number };
28
+ }> {
29
+ const all = await db.cases.toArray();
30
+ const now = Date.now();
31
+ const cache: { [courtDateId: string]: { data: Case[]; timestamp: number } } = {};
32
+ for (const entry of all) {
33
+ if (now - entry.timestamp < CASES_CACHE_EXPIRY) {
34
+ cache[entry.courtDateId] = { data: entry.data, timestamp: entry.timestamp };
35
+ }
36
+ }
37
+ return cache;
38
+ }
39
+
40
+ // Set all cases (overwrites all per-court-date entries)
41
+ export async function setCasesCache(cache: { [courtDateId: string]: { data: Case[]; timestamp: number } }) {
42
+ await db.cases.clear();
43
+ const entries = Object.entries(cache).map(([courtDateId, value]) => ({
44
+ courtDateId,
45
+ data: value.data,
46
+ timestamp: value.timestamp,
47
+ }));
48
+ await db.cases.bulkPut(entries);
49
+ }
50
+
51
+ export async function getCachedCases(courtDateId: string): Promise<Case[] | null> {
52
+ const entry = await db.cases.get(courtDateId);
53
+ const now = Date.now();
54
+ if (entry && Array.isArray(entry.data) && now - entry.timestamp < CASES_CACHE_EXPIRY) {
55
+ return entry.data;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ export async function updateCasesCache(courtDateId: string, data: Case[]) {
61
+ try {
62
+ await db.cases.put({ courtDateId, data, timestamp: Date.now() });
63
+ } catch (e) {
64
+ // Dexie handles quota errors internally, but you can catch and log if needed
65
+ console.warn('Error updating cases cache for', courtDateId, e);
66
+ }
67
+ }
68
+
69
+ export async function removeCasesCache(courtDateId: string) {
70
+ await db.cases.delete(courtDateId);
71
+ }
72
+
73
+ // Court Dates Cache Functions
74
+ export async function getCourtDatesCache(): Promise<CourtDate[] | null> {
75
+ const entry = await db.courtDates.get('all');
76
+ const now = Date.now();
77
+ if (entry && Array.isArray(entry.data) && now - entry.timestamp < COURT_DATES_CACHE_EXPIRY) {
78
+ return entry.data;
79
+ }
80
+ return null;
81
+ }
82
+
83
+ export async function setCourtDatesCache(data: CourtDate[]) {
84
+ try {
85
+ await db.courtDates.put({ id: 'all', data, timestamp: Date.now() });
86
+ } catch (e) {
87
+ console.warn('Error updating court dates cache', e);
88
+ }
89
+ }
@@ -0,0 +1,79 @@
1
+ import type { Case, CourtDate } from '../types';
2
+
3
+ export async function* searchByCaseTerm(term: string, apiKey: string) {
4
+ if (!apiKey || term.trim() === '') return;
5
+ let page = 1;
6
+ let totalPages = 1;
7
+ do {
8
+ const res = await fetch(
9
+ `https://utils.aventine.ai/court-cases/search?page=${page}&query=${encodeURIComponent(term)}`,
10
+ {
11
+ mode: 'cors',
12
+ headers: { 'x-api-key': apiKey },
13
+ method: 'GET',
14
+ },
15
+ );
16
+
17
+ const data = await res.json();
18
+ const courtIds = data.data.map((c: CourtDate) => c.CourtDateID);
19
+ totalPages = data.total_pages;
20
+ yield courtIds; // Yield partial result for this page
21
+ page++;
22
+ } while (page <= totalPages);
23
+ }
24
+
25
+ export async function fetchCasesByCourtDate(id: string, apiKey: string): Promise<Case[]> {
26
+ if (!apiKey) return [];
27
+ const res = await fetch(`https://utils.aventine.ai/court-dates/${id}/cases`, {
28
+ mode: 'cors',
29
+ headers: {
30
+ 'x-api-key': apiKey,
31
+ },
32
+ });
33
+ const data = await res.json();
34
+ console.log('Fetched cases for court date', id, data);
35
+ return data || [];
36
+ }
37
+
38
+ export async function* fetchAllCasesPaginated(apiKey: string, pageSize = 20) {
39
+ if (!apiKey) return;
40
+ let page = 1;
41
+ let totalPages = 1;
42
+ do {
43
+ const res = await fetch(`https://utils.aventine.ai/court-cases/filtering?page=${page}&page_size=${pageSize}`, {
44
+ mode: 'cors',
45
+ headers: { 'x-api-key': apiKey },
46
+ method: 'GET',
47
+ });
48
+ const data = await res.json();
49
+ totalPages = data.total_pages;
50
+ const allCases: Record<string, Case[]> = {};
51
+ for (const courtDate of data.data) {
52
+ if (courtDate.cases && Array.isArray(courtDate.cases)) {
53
+ allCases[courtDate.CourtDateID] = courtDate.cases;
54
+ }
55
+ }
56
+ yield allCases; // Yield partial result for this page
57
+ page++;
58
+ } while (page <= totalPages);
59
+ }
60
+
61
+ export function isCaseSettled(c: Case, isVillage: boolean): boolean {
62
+ if (!isVillage) {
63
+ // town
64
+ if (c.SCARDeterminationAction === null) return false;
65
+ if (c.SCARDeterminationAction.toLowerCase().includes('s')) return true;
66
+ } else {
67
+ // village
68
+ if (c.VillageSCARDeterminationAction === null) return false;
69
+ if (c.VillageSCARDeterminationAction.toLowerCase().includes('s')) return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ export function isCaseMissingEvidence(c: Case): boolean {
75
+ if (!c.evidence) return true;
76
+ if (!c.evidence.Evidence || c.evidence.Evidence === null) return true;
77
+ if (c.evidence.Evidence.trim() === '') return true;
78
+ return false;
79
+ }
@@ -0,0 +1,139 @@
1
+ import { HearingType, Lifecycle, SourceType, type CourtDate } from '../types';
2
+ import { formatDateForAPI } from './formatter';
3
+
4
+ export async function getAllDates(apiKey: string): Promise<Array<CourtDate>> {
5
+ if (!apiKey) return [];
6
+
7
+ const res = await fetch(`https://utils.aventine.ai/court-dates/all`, {
8
+ mode: 'cors',
9
+ headers: {
10
+ 'x-api-key': apiKey,
11
+ },
12
+ });
13
+ const data = await res.json();
14
+ console.log('Fetched dates:', data);
15
+
16
+ for (const date of data.dates) {
17
+ if (!date.Lifecycle) {
18
+ date.Lifecycle = Lifecycle.SCHEDULED;
19
+ }
20
+ if (!date.Type) {
21
+ date.Type = HearingType.UNKNOWN;
22
+ }
23
+ }
24
+
25
+ return data.dates || [];
26
+ }
27
+
28
+ export async function updateCourtDate(
29
+ courtDateId: number,
30
+ updatedData: Partial<CourtDate>,
31
+ apiKey: string,
32
+ courtCases?: string[],
33
+ ): Promise<boolean> {
34
+ if (!apiKey) return false;
35
+ const res = await fetch(`https://utils.aventine.ai/court-dates/${courtDateId}/update`, {
36
+ method: 'PUT',
37
+ mode: 'cors',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ 'x-api-key': apiKey,
41
+ },
42
+ body: JSON.stringify({
43
+ adjournment_date: updatedData.AdjournmentDate
44
+ ? formatDateForAPI(new Date(updatedData.AdjournmentDate))
45
+ : undefined,
46
+ court_date: updatedData.CourtDate ? formatDateForAPI(new Date(updatedData.CourtDate)) : undefined,
47
+ hearing_link: updatedData.HearingLink !== '' ? updatedData.HearingLink : undefined,
48
+ hearing_officer: updatedData.HearingOfficer !== null ? updatedData.HearingOfficer : undefined,
49
+ hearing_time: updatedData.HearingTime !== '' ? updatedData.HearingTime : undefined,
50
+ hearing_type: updatedData.Type !== HearingType.UNKNOWN ? updatedData.Type : undefined,
51
+ muni: updatedData.MuniCode,
52
+ firstChair: updatedData.FirstChair !== null ? updatedData.FirstChair : undefined,
53
+ secondChair: updatedData.SecondChair !== null ? updatedData.SecondChair : undefined,
54
+ lifecycle: updatedData.Lifecycle !== Lifecycle.SCHEDULED ? updatedData.Lifecycle : undefined,
55
+ court_cases: courtCases || undefined,
56
+ is_adjourned: updatedData.IsAdjourned !== undefined ? updatedData.IsAdjourned : undefined,
57
+ notes: updatedData.Notes !== null ? updatedData.Notes : undefined,
58
+ }),
59
+ });
60
+ if (res.ok) {
61
+ console.log(`Court date ${courtDateId} updated successfully.`);
62
+ return true;
63
+ } else {
64
+ console.error(`Failed to update court date ${courtDateId}. Status: ${res.status}`);
65
+ return false;
66
+ }
67
+ }
68
+
69
+ export async function deleteCourtDate(courtDateId: number, apiKey: string): Promise<boolean> {
70
+ if (!apiKey) return false;
71
+ const res = await fetch(`https://utils.aventine.ai/court-dates/${courtDateId}`, {
72
+ method: 'DELETE',
73
+ mode: 'cors',
74
+ headers: {
75
+ 'x-api-key': apiKey,
76
+ },
77
+ });
78
+ if (res.ok) {
79
+ console.log(`Court date ${courtDateId} deleted successfully.`);
80
+ return true;
81
+ } else {
82
+ console.error(`Failed to delete court date ${courtDateId}. Status: ${res.status}`);
83
+ return false;
84
+ }
85
+ }
86
+
87
+ // get court_date_id from response
88
+ export async function createCourtDate(
89
+ courtDate: Date,
90
+ muniCode: string,
91
+ apiKey: string,
92
+ courtCases?: string[],
93
+ ): Promise<number | null> {
94
+ if (!apiKey) return null;
95
+ const res = await fetch(`https://utils.aventine.ai/court-dates/create`, {
96
+ method: 'POST',
97
+ mode: 'cors',
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ 'x-api-key': apiKey,
101
+ },
102
+ body: JSON.stringify({
103
+ court_date: formatDateForAPI(new Date(courtDate)),
104
+ muni: muniCode,
105
+ source: SourceType.MANUAL,
106
+ court_cases: courtCases || [],
107
+ }),
108
+ });
109
+ if (res.ok) {
110
+ const data = await res.json();
111
+ console.log(`Court date created successfully with ID: ${data.court_date_id}`);
112
+ return data.court_date_id;
113
+ } else {
114
+ console.error(`Failed to create court date. Status: ${res.status}`);
115
+ return null;
116
+ }
117
+ }
118
+
119
+ export async function snoozeUploadDeadline(courtDateId: number, apiKey: string): Promise<boolean> {
120
+ if (!apiKey) return false;
121
+ const res = await fetch(`https://utils.aventine.ai/court-cases/snooze/upload/${courtDateId}`, {
122
+ method: 'GET',
123
+ mode: 'cors',
124
+ headers: {
125
+ 'x-api-key': apiKey,
126
+ },
127
+ });
128
+ if (res.ok) {
129
+ console.log(`Upload deadline for court date ${courtDateId} snoozed successfully.`);
130
+ return true;
131
+ } else {
132
+ console.error(`Failed to snooze upload deadline for court date ${courtDateId}. Status: ${res.status}`);
133
+ return false;
134
+ }
135
+ }
136
+
137
+ export function isVillageDate(muniCode: string): boolean {
138
+ return muniCode.length > 3 && muniCode.slice(-2) !== '00';
139
+ }
@@ -0,0 +1,16 @@
1
+ import type { Evidence } from '../types';
2
+
3
+ export function formatDateForAPI(date: Date): string {
4
+ const year = date.getFullYear();
5
+ const month = String(date.getMonth() + 1).padStart(2, '0');
6
+ const day = String(date.getDate()).padStart(2, '0');
7
+ return `${year}-${month}-${day}`;
8
+ }
9
+
10
+ // [\"Sales\"] -> Sales
11
+ export function formatEvidence(evidence: Evidence): string {
12
+ if (!evidence || !evidence.Evidence) return '';
13
+ const hasBeenUploaded = evidence.Uploaded && evidence.Uploaded.Evidence;
14
+ if (!hasBeenUploaded) return `${evidence.Evidence.replace(/[[\]"]+/g, '')} created`;
15
+ return `${evidence.Uploaded.Evidence.replace(/[[\]"]+/g, '')} uploaded on ${new Date(evidence.Uploaded.UploadDate).toLocaleDateString()}`;
16
+ }
@@ -0,0 +1,44 @@
1
+ import type { Muni } from '../types';
2
+
3
+ async function getAllMuniNames(apiKey: string): Promise<Record<string, Muni>> {
4
+ if (!apiKey) return {};
5
+ const res = await fetch(`https://utils.aventine.ai/utils/get-muni-names`, {
6
+ mode: 'cors',
7
+ headers: {
8
+ 'x-api-key': apiKey,
9
+ },
10
+ });
11
+ const data = await res.json();
12
+ console.log('Fetched muni names:', data);
13
+ const munis: Record<string, Muni> = {};
14
+ if (data == null) {
15
+ return munis;
16
+ }
17
+ for (const muni of Object.keys(data)) {
18
+ munis[muni] = data[muni];
19
+ }
20
+ return munis;
21
+ }
22
+
23
+ export const allMuniNames = await getAllMuniNames(import.meta.env.VITE_CALENDAR_API_KEY);
24
+
25
+ export function getTownshipName(muniCode: string): string {
26
+ if (muniCode.length < 5) {
27
+ muniCode = muniCode + '00';
28
+ }
29
+ const muni = allMuniNames[muniCode];
30
+ if (!muni) return muniCode;
31
+ if (muniCode.slice(-2) !== '00') {
32
+ return muni.Village;
33
+ }
34
+ return muni.Township;
35
+ }
36
+
37
+ export function getCountyName(muniCode: string): string {
38
+ if (muniCode.length < 5) {
39
+ muniCode = muniCode + '00';
40
+ }
41
+ const muni = allMuniNames[muniCode];
42
+ if (!muni) return muniCode;
43
+ return muni.County;
44
+ }