@propriety/court-calendar 0.0.13 → 0.0.15

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@propriety/court-calendar",
3
3
  "private": false,
4
- "version": "0.0.13",
4
+ "version": "0.0.15",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "./dist/index.mjs",
@@ -59,7 +59,7 @@ export default function CCalendar({ apiKey, activeUser }: { apiKey: string; acti
59
59
  searchTerm: '',
60
60
  municode: null,
61
61
  user: null,
62
- showOnlyUnsettled: false,
62
+ showOnlyUnsettled: true,
63
63
  showOnlyWithoutEvidence: false,
64
64
  showOnlyUnreviewed: false,
65
65
  });
@@ -81,7 +81,7 @@ export default function CCalendar({ apiKey, activeUser }: { apiKey: string; acti
81
81
  async function getAllCourtDates(apiKey: string) {
82
82
  // Try to load from IndexedDB cache
83
83
  const cachedDates = await getCourtDatesCache();
84
- if (cachedDates) {
84
+ if (cachedDates && cachedDates.length > 0) {
85
85
  setCourtDates(cachedDates);
86
86
  } else {
87
87
  getAllDates(apiKey)
@@ -132,7 +132,7 @@ export default function CCalendar({ apiKey, activeUser }: { apiKey: string; acti
132
132
  updateCourtDateInMemory(updatedCourtDate);
133
133
 
134
134
  // Then persist to API in background
135
- updateCourtDate(courtDateId, updatedData, apiKey).then((success) => {
135
+ updateCourtDate(courtDateId, updatedData, apiKey, undefined, activeUser).then((success) => {
136
136
  if (!success) {
137
137
  // Revert on failure
138
138
  updateCourtDateInMemory(courtDate);
@@ -718,6 +718,7 @@ export default function CCalendar({ apiKey, activeUser }: { apiKey: string; acti
718
718
  deleteCases={deleteCaseMemoryAndCache}
719
719
  isFetchingCases={isFetchingCases}
720
720
  apiKey={apiKey}
721
+ activeUser={activeUser}
721
722
  />
722
723
  <Tooltip id='event-tooltip' className='cc-tooltip' />
723
724
  </div>
@@ -1,42 +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
- }
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
+ }
@@ -1,15 +1,15 @@
1
- /* Modal appearing animation */
2
- .modal-appear-animate {
3
- animation: modalFadeIn 0.35s cubic-bezier(0.4, 0, 0.2, 1);
4
- }
5
-
6
- @keyframes modalFadeIn {
7
- from {
8
- opacity: 0;
9
- transform: scale(0.96) translateY(24px);
10
- }
11
- to {
12
- opacity: 1;
13
- transform: scale(1) translateY(0);
14
- }
15
- }
1
+ /* Modal appearing animation */
2
+ .modal-appear-animate {
3
+ animation: modalFadeIn 0.35s cubic-bezier(0.4, 0, 0.2, 1);
4
+ }
5
+
6
+ @keyframes modalFadeIn {
7
+ from {
8
+ opacity: 0;
9
+ transform: scale(0.96) translateY(24px);
10
+ }
11
+ to {
12
+ opacity: 1;
13
+ transform: scale(1) translateY(0);
14
+ }
15
+ }
@@ -29,6 +29,7 @@ export default function CCModal({
29
29
  deleteCases,
30
30
  isFetchingCases,
31
31
  apiKey,
32
+ activeUser,
32
33
  }: {
33
34
  modalIsOpen: boolean;
34
35
  modalMode: ModalMode;
@@ -43,6 +44,7 @@ export default function CCModal({
43
44
  deleteCases: (courtDateId: string) => void;
44
45
  isFetchingCases: boolean;
45
46
  apiKey: string;
47
+ activeUser: number;
46
48
  }) {
47
49
  const [editedData, setEditedData] = useState<CourtDate | undefined>(undefined);
48
50
  const [editedCases, setEditedCases] = useState<Case[]>([]);
@@ -149,6 +151,7 @@ export default function CCModal({
149
151
  editedData,
150
152
  apiKey,
151
153
  editedCases.map((c) => c.SCARIndexNumber),
154
+ activeUser,
152
155
  );
153
156
  // Update cache after save
154
157
  const courtDateId = editedData.CourtDateID;
@@ -171,6 +174,7 @@ export default function CCModal({
171
174
  editedData.MuniCode,
172
175
  apiKey,
173
176
  editedCases.map((c) => c.SCARIndexNumber),
177
+ editedData.HearingTime || undefined,
174
178
  );
175
179
  if (id) {
176
180
  const updatedData = { ...editedData, CourtDateID: id };
@@ -1,39 +1,39 @@
1
- .info-box-btn {
2
- cursor: pointer;
3
- width: 100%;
4
- display: inherit;
5
- position: relative;
6
- }
7
-
8
- .info-box-btn > .info-box-btn-cover {
9
- position: absolute;
10
- top: 0%;
11
- width: 5%;
12
- right: 0;
13
- height: 100%;
14
- background-color: var(--text) !important;
15
- color: var(--text);
16
- z-index: 99999;
17
- border-top-right-radius: 8px;
18
- border-bottom-right-radius: 8px;
19
- overflow: hidden;
20
- display: flex;
21
- justify-content: center;
22
- align-items: center;
23
- opacity: 0.7;
24
- transition:
25
- background-color 0.3s,
26
- height 0.3s,
27
- width 0.3s,
28
- top 0.3s,
29
- border-radius 0.3s,
30
- opacity 0.3s;
31
- }
32
- .info-box-btn:hover > .info-box-btn-cover {
33
- height: 100%;
34
- width: 100%;
35
- background-color: var(--fc-today-bg-color) !important;
36
- top: 0;
37
- border-radius: 8px;
38
- opacity: 1;
39
- }
1
+ .info-box-btn {
2
+ cursor: pointer;
3
+ width: 100%;
4
+ display: inherit;
5
+ position: relative;
6
+ }
7
+
8
+ .info-box-btn > .info-box-btn-cover {
9
+ position: absolute;
10
+ top: 0%;
11
+ width: 5%;
12
+ right: 0;
13
+ height: 100%;
14
+ background-color: var(--text) !important;
15
+ color: var(--text);
16
+ z-index: 99999;
17
+ border-top-right-radius: 8px;
18
+ border-bottom-right-radius: 8px;
19
+ overflow: hidden;
20
+ display: flex;
21
+ justify-content: center;
22
+ align-items: center;
23
+ opacity: 0.7;
24
+ transition:
25
+ background-color 0.3s,
26
+ height 0.3s,
27
+ width 0.3s,
28
+ top 0.3s,
29
+ border-radius 0.3s,
30
+ opacity 0.3s;
31
+ }
32
+ .info-box-btn:hover > .info-box-btn-cover {
33
+ height: 100%;
34
+ width: 100%;
35
+ background-color: var(--fc-today-bg-color) !important;
36
+ top: 0;
37
+ border-radius: 8px;
38
+ opacity: 1;
39
+ }
@@ -1,29 +1,29 @@
1
- import Typography from '@mui/material/Typography';
2
- import InfoBox from './InfoBox';
3
- import './InfoBoxBtn.css';
4
- import Stack from '@mui/material/Stack';
5
- import Box from '@mui/material/Box';
6
-
7
- export default function InfoBoxBtn({
8
- label,
9
- info,
10
- btnLabel,
11
- btnIcon,
12
- onBtnClick,
13
- }: {
14
- label: string;
15
- info: string;
16
- btnLabel?: string;
17
- btnIcon?: React.ReactNode;
18
- onBtnClick?: () => void;
19
- }) {
20
- return (
21
- <Box className='info-box-btn' onClick={onBtnClick}>
22
- <Stack className='info-box-btn-cover' direction={'column'}>
23
- <Typography variant={'h6'}>{btnLabel}</Typography>
24
- {btnIcon}
25
- </Stack>
26
- <InfoBox label={label} info={info} />
27
- </Box>
28
- );
29
- }
1
+ import Typography from '@mui/material/Typography';
2
+ import InfoBox from './InfoBox';
3
+ import './InfoBoxBtn.css';
4
+ import Stack from '@mui/material/Stack';
5
+ import Box from '@mui/material/Box';
6
+
7
+ export default function InfoBoxBtn({
8
+ label,
9
+ info,
10
+ btnLabel,
11
+ btnIcon,
12
+ onBtnClick,
13
+ }: {
14
+ label: string;
15
+ info: string;
16
+ btnLabel?: string;
17
+ btnIcon?: React.ReactNode;
18
+ onBtnClick?: () => void;
19
+ }) {
20
+ return (
21
+ <Box className='info-box-btn' onClick={onBtnClick}>
22
+ <Stack className='info-box-btn-cover' direction={'column'}>
23
+ <Typography variant={'h6'}>{btnLabel}</Typography>
24
+ {btnIcon}
25
+ </Stack>
26
+ <InfoBox label={label} info={info} />
27
+ </Box>
28
+ );
29
+ }
@@ -1,37 +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
- }
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
+ }
@@ -1,87 +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
- }
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
+ }
@@ -62,11 +62,19 @@ export function isCaseSettled(c: Case, isVillage: boolean): boolean {
62
62
  if (!isVillage) {
63
63
  // town
64
64
  if (c.SCARDeterminationAction === null) return false;
65
- if (c.SCARDeterminationAction.toLowerCase().includes('s')) return true;
65
+ if (
66
+ c.SCARDeterminationAction.toLowerCase().includes('s') ||
67
+ c.SCARDeterminationAction.toLowerCase().includes('w')
68
+ )
69
+ return true;
66
70
  } else {
67
71
  // village
68
72
  if (c.VillageSCARDeterminationAction === null) return false;
69
- if (c.VillageSCARDeterminationAction.toLowerCase().includes('s')) return true;
73
+ if (
74
+ c.VillageSCARDeterminationAction.toLowerCase().includes('s') ||
75
+ c.VillageSCARDeterminationAction.toLowerCase().includes('w')
76
+ )
77
+ return true;
70
78
  }
71
79
  return false;
72
80
  }
@@ -1,5 +1,5 @@
1
1
  import { HearingType, Lifecycle, SourceType, type CourtDate } from '../types';
2
- import { formatDateForAPI } from './formatter';
2
+ import { formatDateTimeForAPI } from './formatter';
3
3
 
4
4
  export async function getAllDates(apiKey: string): Promise<Array<CourtDate>> {
5
5
  if (!apiKey) return [];
@@ -30,8 +30,10 @@ export async function updateCourtDate(
30
30
  updatedData: Partial<CourtDate>,
31
31
  apiKey: string,
32
32
  courtCases?: string[],
33
+ user?: number,
33
34
  ): Promise<boolean> {
34
35
  if (!apiKey) return false;
36
+ console.log(`Updating court date ${courtDateId} by user: ${user} with data:`, updatedData);
35
37
  const res = await fetch(`https://utils.aventine.ai/court-dates/${courtDateId}/update`, {
36
38
  method: 'PUT',
37
39
  mode: 'cors',
@@ -41,20 +43,23 @@ export async function updateCourtDate(
41
43
  },
42
44
  body: JSON.stringify({
43
45
  adjournment_date: updatedData.AdjournmentDate
44
- ? formatDateForAPI(new Date(updatedData.AdjournmentDate))
46
+ ? formatDateTimeForAPI(new Date(updatedData.AdjournmentDate), updatedData.HearingTime || undefined)
47
+ : undefined,
48
+ court_date: updatedData.CourtDate
49
+ ? formatDateTimeForAPI(new Date(updatedData.CourtDate), updatedData.HearingTime || undefined)
45
50
  : undefined,
46
- court_date: updatedData.CourtDate ? formatDateForAPI(new Date(updatedData.CourtDate)) : undefined,
47
51
  hearing_link: updatedData.HearingLink !== '' ? updatedData.HearingLink : undefined,
48
- hearing_officer: updatedData.HearingOfficer !== null ? updatedData.HearingOfficer : undefined,
52
+ hearing_officer: updatedData.HearingOfficer !== undefined ? updatedData.HearingOfficer : undefined,
49
53
  hearing_time: updatedData.HearingTime !== '' ? updatedData.HearingTime : undefined,
50
54
  hearing_type: updatedData.Type !== HearingType.UNKNOWN ? updatedData.Type : undefined,
51
55
  muni: updatedData.MuniCode,
52
- firstChair: updatedData.FirstChair !== null ? updatedData.FirstChair : undefined,
53
- secondChair: updatedData.SecondChair !== null ? updatedData.SecondChair : undefined,
56
+ first_chair: updatedData.FirstChair !== undefined ? updatedData.FirstChair : undefined,
57
+ second_chair: updatedData.SecondChair !== undefined ? updatedData.SecondChair : undefined,
54
58
  lifecycle: updatedData.Lifecycle !== Lifecycle.SCHEDULED ? updatedData.Lifecycle : undefined,
55
59
  court_cases: courtCases || undefined,
56
60
  is_adjourned: updatedData.IsAdjourned !== undefined ? updatedData.IsAdjourned : undefined,
57
61
  notes: updatedData.Notes !== null ? updatedData.Notes : undefined,
62
+ edit_user: user !== undefined ? user : undefined,
58
63
  }),
59
64
  });
60
65
  if (res.ok) {
@@ -90,6 +95,7 @@ export async function createCourtDate(
90
95
  muniCode: string,
91
96
  apiKey: string,
92
97
  courtCases?: string[],
98
+ hearingTime?: string,
93
99
  ): Promise<number | null> {
94
100
  if (!apiKey) return null;
95
101
  const res = await fetch(`https://utils.aventine.ai/court-dates/create`, {
@@ -100,7 +106,7 @@ export async function createCourtDate(
100
106
  'x-api-key': apiKey,
101
107
  },
102
108
  body: JSON.stringify({
103
- court_date: formatDateForAPI(new Date(courtDate)),
109
+ court_date: formatDateTimeForAPI(new Date(courtDate), hearingTime || undefined),
104
110
  muni: muniCode,
105
111
  source: SourceType.MANUAL,
106
112
  court_cases: courtCases || [],