@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,237 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState, memo } from 'react';
|
|
2
|
+
import type { CalendarFilterCtx, Case } from '@/types';
|
|
3
|
+
import Box from '@mui/material/Box';
|
|
4
|
+
import Stack from '@mui/material/Stack';
|
|
5
|
+
import Typography from '@mui/material/Typography';
|
|
6
|
+
import { formatEvidence } from '@/helpers/formatter';
|
|
7
|
+
import CircularProgress from '@mui/material/CircularProgress';
|
|
8
|
+
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
|
9
|
+
import CaseToolbar from './CaseToolbar';
|
|
10
|
+
import { isCaseMissingEvidence, isCaseSettled } from '@/helpers/cases';
|
|
11
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
12
|
+
import CancelIcon from '@mui/icons-material/Cancel';
|
|
13
|
+
|
|
14
|
+
const CaseViewer = memo(function CaseViewer({
|
|
15
|
+
cases,
|
|
16
|
+
isFetchingCases,
|
|
17
|
+
filterCtx,
|
|
18
|
+
setFilterCtx,
|
|
19
|
+
setClickedCase,
|
|
20
|
+
isVillage,
|
|
21
|
+
}: {
|
|
22
|
+
cases: Case[];
|
|
23
|
+
isFetchingCases: boolean;
|
|
24
|
+
filterCtx: CalendarFilterCtx;
|
|
25
|
+
setFilterCtx: (ctx: CalendarFilterCtx) => void;
|
|
26
|
+
setClickedCase: (caseItem: Case | null) => void;
|
|
27
|
+
isVillage: boolean;
|
|
28
|
+
}) {
|
|
29
|
+
const columns: GridColDef[] = [
|
|
30
|
+
{
|
|
31
|
+
field: 'scarIndexNumber',
|
|
32
|
+
headerName: 'Index',
|
|
33
|
+
width: 120,
|
|
34
|
+
disableColumnMenu: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
field: 'owner',
|
|
38
|
+
headerName: 'Owner',
|
|
39
|
+
width: 250,
|
|
40
|
+
disableColumnMenu: true,
|
|
41
|
+
renderCell: (params) => {
|
|
42
|
+
if (params.value === 'Loading...') {
|
|
43
|
+
return <CircularProgress size={18} sx={{ color: 'var(--text)' }} />;
|
|
44
|
+
}
|
|
45
|
+
return params.value;
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
field: 'address',
|
|
50
|
+
headerName: 'Address',
|
|
51
|
+
width: 175,
|
|
52
|
+
disableColumnMenu: true,
|
|
53
|
+
renderCell: (params) => {
|
|
54
|
+
if (params.value === 'Loading...') {
|
|
55
|
+
return <CircularProgress size={18} sx={{ color: 'var(--text)' }} />;
|
|
56
|
+
}
|
|
57
|
+
return params.value;
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
field: 'reviewed',
|
|
62
|
+
headerName: 'Reviewed',
|
|
63
|
+
width: 100,
|
|
64
|
+
disableColumnMenu: true,
|
|
65
|
+
renderCell: (params) => {
|
|
66
|
+
const isReviewed = params.value === 'Yes';
|
|
67
|
+
return (
|
|
68
|
+
<Box display='flex' alignItems='center' justifyContent='center' width='100%' mt={1.7}>
|
|
69
|
+
{isReviewed ? (
|
|
70
|
+
<CheckCircleIcon sx={{ color: '#4caf50', fontSize: 24 }} />
|
|
71
|
+
) : (
|
|
72
|
+
<CancelIcon sx={{ color: '#f44336', fontSize: 24 }} />
|
|
73
|
+
)}
|
|
74
|
+
</Box>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
field: 'evidence',
|
|
80
|
+
headerName: 'Evidence',
|
|
81
|
+
width: 100,
|
|
82
|
+
disableColumnMenu: true,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
field: 'determination',
|
|
86
|
+
headerName: 'Determination',
|
|
87
|
+
width: 120,
|
|
88
|
+
disableColumnMenu: true,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
field: 'pid',
|
|
92
|
+
headerName: 'PID',
|
|
93
|
+
width: 60,
|
|
94
|
+
disableColumnMenu: true,
|
|
95
|
+
sortable: false,
|
|
96
|
+
filterable: false,
|
|
97
|
+
renderCell: (params) => {
|
|
98
|
+
const pid = params.row.pid;
|
|
99
|
+
const handleCopy = (e: React.MouseEvent) => {
|
|
100
|
+
e.stopPropagation();
|
|
101
|
+
if (navigator && navigator.clipboard) {
|
|
102
|
+
navigator.clipboard.writeText(pid);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
return (
|
|
106
|
+
<button
|
|
107
|
+
onClick={handleCopy}
|
|
108
|
+
style={{
|
|
109
|
+
cursor: 'pointer',
|
|
110
|
+
padding: '2px 8px',
|
|
111
|
+
border: '1px solid #ccc',
|
|
112
|
+
borderRadius: 4,
|
|
113
|
+
background: '#f5f5f5',
|
|
114
|
+
}}
|
|
115
|
+
title='Copy PID'
|
|
116
|
+
>
|
|
117
|
+
Copy
|
|
118
|
+
</button>
|
|
119
|
+
);
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const rows = useMemo(() => {
|
|
125
|
+
if (cases.length <= 0) return [];
|
|
126
|
+
const filteredCases = cases.filter((caseItem) => {
|
|
127
|
+
// filter by unsettled
|
|
128
|
+
if (filterCtx.showOnlyUnsettled) {
|
|
129
|
+
if (isCaseSettled(caseItem, isVillage)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// filter by unreviewed
|
|
134
|
+
if (filterCtx.showOnlyUnreviewed) {
|
|
135
|
+
if (caseItem.DateCompleted) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// filter by without evidence
|
|
140
|
+
if (filterCtx.showOnlyWithoutEvidence) {
|
|
141
|
+
if (!isCaseMissingEvidence(caseItem)) {
|
|
142
|
+
return false; // hide cases that have evidence
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// filter by search term
|
|
146
|
+
if (filterCtx.searchTerm) {
|
|
147
|
+
const term = filterCtx.searchTerm.toLowerCase();
|
|
148
|
+
const owner = caseItem.property_data?.PropertyOwnerFull.toLowerCase() || '';
|
|
149
|
+
const address = caseItem.property_data?.Address.toLowerCase() || '';
|
|
150
|
+
const pid = caseItem.ParcelID.toLowerCase();
|
|
151
|
+
const scarid = caseItem.SCARIndexNumber.toLowerCase();
|
|
152
|
+
if (!owner.includes(term) && !address.includes(term) && !pid.includes(term) && !scarid.includes(term)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
});
|
|
158
|
+
return filteredCases.map((caseItem, index) => ({
|
|
159
|
+
id: index,
|
|
160
|
+
scarIndexNumber: isVillage ? caseItem.VillageSCARIndexNumber : caseItem.SCARIndexNumber,
|
|
161
|
+
owner: caseItem.property_data?.PropertyOwnerFull || 'Loading...',
|
|
162
|
+
reviewed: caseItem.DateCompleted ? 'Yes' : 'No',
|
|
163
|
+
address: caseItem.property_data?.Address || 'Loading...',
|
|
164
|
+
evidence: caseItem.evidence ? formatEvidence(caseItem.evidence) : 'N/A',
|
|
165
|
+
determination: getActionDetail(caseItem),
|
|
166
|
+
pid: caseItem.ParcelID || 'N/A',
|
|
167
|
+
_case: caseItem,
|
|
168
|
+
}));
|
|
169
|
+
}, [cases, filterCtx]);
|
|
170
|
+
|
|
171
|
+
function getActionDetail(caseItem: Case): string {
|
|
172
|
+
if (!isVillage) {
|
|
173
|
+
return caseItem.SCARDeterminationAction;
|
|
174
|
+
} else if (isVillage) {
|
|
175
|
+
return caseItem.VillageSCARDeterminationAction;
|
|
176
|
+
}
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// For height animation
|
|
181
|
+
const [tableOpen, setTableOpen] = useState(false);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
setTableOpen(!isFetchingCases && cases.length > 0);
|
|
184
|
+
}, [isFetchingCases, cases.length]);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Box justifyContent={'center'} width={'100%'} mt={2}>
|
|
188
|
+
{cases.length === 0 && !isFetchingCases && <Typography>No cases associated with this date.</Typography>}
|
|
189
|
+
{isFetchingCases && (
|
|
190
|
+
<Box display='flex' justifyContent='center' alignItems='center'>
|
|
191
|
+
<Typography>Loading cases...</Typography>
|
|
192
|
+
<CircularProgress size={40} sx={{ color: 'var(--text)' }} />
|
|
193
|
+
</Box>
|
|
194
|
+
)}
|
|
195
|
+
<div
|
|
196
|
+
className='case-table-anim'
|
|
197
|
+
style={{
|
|
198
|
+
maxHeight: tableOpen ? 1000 : 0,
|
|
199
|
+
overflow: 'hidden',
|
|
200
|
+
transition: 'max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
{!isFetchingCases && cases.length > 0 && (
|
|
204
|
+
<Stack>
|
|
205
|
+
<Box mb={2}>
|
|
206
|
+
<CaseToolbar filterCtx={filterCtx} setFilterCtx={setFilterCtx} />
|
|
207
|
+
</Box>
|
|
208
|
+
<Box mx={1} mb={1} justifyContent={'center'} alignItems={'center'} display='flex'>
|
|
209
|
+
<Typography variant='body1' fontStyle={''}>
|
|
210
|
+
{rows.length} case{rows.length !== 1 ? 's' : ''} found, click a row to view additional
|
|
211
|
+
case details.
|
|
212
|
+
</Typography>
|
|
213
|
+
</Box>
|
|
214
|
+
<DataGrid
|
|
215
|
+
autoHeight
|
|
216
|
+
rows={rows}
|
|
217
|
+
columns={columns}
|
|
218
|
+
hideFooter
|
|
219
|
+
onRowClick={(params) => setClickedCase(params.row._case)}
|
|
220
|
+
sx={{
|
|
221
|
+
'& .MuiDataGrid-row': {
|
|
222
|
+
cursor: 'pointer',
|
|
223
|
+
transition: 'background-color 0.2s ease-in-out',
|
|
224
|
+
'&:hover': {
|
|
225
|
+
backgroundColor: 'var(--fc-today-bg-color) !important',
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
}}
|
|
229
|
+
/>
|
|
230
|
+
</Stack>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
</Box>
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
export default CaseViewer;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { HearingType, type Case, type CourtDate } from '../../../types';
|
|
2
|
+
import Box from '@mui/material/Box';
|
|
3
|
+
import Typography from '@mui/material/Typography';
|
|
4
|
+
import FormRow from '../../Shared/FormRow';
|
|
5
|
+
import InfoBox from './InfoBox';
|
|
6
|
+
import Stack from '@mui/material/Stack';
|
|
7
|
+
import { allUsers } from '@/helpers/people';
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import InfoBoxBtn from './InfoBoxBtn';
|
|
10
|
+
import SnoozeIcon from '@mui/icons-material/Snooze';
|
|
11
|
+
import NoticeFileLink from './NoticeFileLink';
|
|
12
|
+
import { snoozeUploadDeadline } from '@/helpers/courtDates';
|
|
13
|
+
import { allMuniNames, getTownshipName } from '@/helpers/munis';
|
|
14
|
+
|
|
15
|
+
export default function DateDetails({
|
|
16
|
+
selectedCourtDate,
|
|
17
|
+
apiKey,
|
|
18
|
+
}: {
|
|
19
|
+
selectedCourtDate: CourtDate | null;
|
|
20
|
+
apiKey: string;
|
|
21
|
+
}) {
|
|
22
|
+
const [firstChairName, setFirstChairName] = useState<string>('-');
|
|
23
|
+
const [secondChairName, setSecondChairName] = useState<string>('-');
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!selectedCourtDate) {
|
|
27
|
+
setFirstChairName('-');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (selectedCourtDate.FirstChair) {
|
|
31
|
+
const user = allUsers[selectedCourtDate.FirstChair];
|
|
32
|
+
setFirstChairName(user ? `${user.UserFirstName} ${user.UserLastName}` : '-');
|
|
33
|
+
} else {
|
|
34
|
+
setFirstChairName('-');
|
|
35
|
+
}
|
|
36
|
+
if (selectedCourtDate.SecondChair) {
|
|
37
|
+
const user = allUsers[selectedCourtDate.SecondChair];
|
|
38
|
+
setSecondChairName(user ? `${user.UserFirstName} ${user.UserLastName}` : '-');
|
|
39
|
+
} else {
|
|
40
|
+
setSecondChairName('-');
|
|
41
|
+
}
|
|
42
|
+
}, [selectedCourtDate]);
|
|
43
|
+
|
|
44
|
+
function formatDate(date: Date | null | undefined): string {
|
|
45
|
+
if (!date) return '-';
|
|
46
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
47
|
+
year: 'numeric',
|
|
48
|
+
month: '2-digit',
|
|
49
|
+
day: '2-digit',
|
|
50
|
+
}).format(new Date(date));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// biome-ignore lint/correctness/noUnusedVariables: may be used in the future
|
|
54
|
+
async function handleSnoozeClick() {
|
|
55
|
+
if (!selectedCourtDate || !selectedCourtDate.UploadDeadline) return;
|
|
56
|
+
const newDate = new Date(new Date(selectedCourtDate.UploadDeadline).getTime() + 24 * 60 * 60 * 1000);
|
|
57
|
+
const res = window.confirm(
|
|
58
|
+
`Snooze upload deadline by 1 business day? \nThe new date will be on ${formatDate(newDate)}`,
|
|
59
|
+
);
|
|
60
|
+
if (res) {
|
|
61
|
+
await snoozeUploadDeadline(selectedCourtDate.CourtDateID, apiKey);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Box className='themed'>
|
|
67
|
+
{selectedCourtDate && (
|
|
68
|
+
<Stack spacing={0} direction='column'>
|
|
69
|
+
<Box>
|
|
70
|
+
<Typography variant='h6' align='center' gutterBottom>
|
|
71
|
+
{`${getTownshipName(selectedCourtDate.MuniCode) || 'Unknown Municipality'} (${selectedCourtDate.MuniCode}) ${selectedCourtDate.FirstChair || selectedCourtDate.SecondChair ? '' : '- Unassigned'}`}
|
|
72
|
+
</Typography>
|
|
73
|
+
</Box>
|
|
74
|
+
<FormRow gap={7}>
|
|
75
|
+
<InfoBox label='First Chair' info={firstChairName} color='#2196f3' />
|
|
76
|
+
<InfoBox label='Second Chair' info={secondChairName} color='#00bcd4' />
|
|
77
|
+
</FormRow>
|
|
78
|
+
<FormRow gap={3}>
|
|
79
|
+
<InfoBox label='Court Date' info={formatDate(selectedCourtDate.CourtDate)} color='#ff9800' />
|
|
80
|
+
<InfoBox
|
|
81
|
+
label='Adjournment Date'
|
|
82
|
+
info={formatDate(selectedCourtDate.AdjournmentDate)}
|
|
83
|
+
color='#9c27b0'
|
|
84
|
+
/>
|
|
85
|
+
<InfoBox
|
|
86
|
+
label='Upload Due Date'
|
|
87
|
+
info={formatDate(selectedCourtDate.UploadDeadline)}
|
|
88
|
+
color='#f44336'
|
|
89
|
+
/>
|
|
90
|
+
</FormRow>
|
|
91
|
+
<FormRow gap={7}>
|
|
92
|
+
<InfoBox label='Status' info={selectedCourtDate.Lifecycle || '-'} color='#4caf50' />
|
|
93
|
+
</FormRow>
|
|
94
|
+
<NoticeFileLink fileKey={selectedCourtDate.NoticeFile} />
|
|
95
|
+
<Stack mt={2} spacing={1}>
|
|
96
|
+
<Typography variant='body1'>{selectedCourtDate.Notes || 'No notes provided.'}</Typography>
|
|
97
|
+
<Typography variant='body2' sx={{ opacity: 0.85 }}>
|
|
98
|
+
<span>{`The hearing is `}</span>
|
|
99
|
+
<strong>
|
|
100
|
+
{selectedCourtDate.Type === HearingType.VIRTUAL
|
|
101
|
+
? 'virtual'
|
|
102
|
+
: selectedCourtDate.Type === HearingType.IN_PERSON
|
|
103
|
+
? 'in person'
|
|
104
|
+
: 'of unknown type'}
|
|
105
|
+
</strong>
|
|
106
|
+
<span>{` and is scheduled for `}</span>
|
|
107
|
+
<strong>{selectedCourtDate.HearingTime}</strong>
|
|
108
|
+
<span>{`.`}</span>
|
|
109
|
+
<br />
|
|
110
|
+
<span>{`The hearing officer is `}</span>
|
|
111
|
+
<strong>{selectedCourtDate.HearingOfficer || 'Not Assigned'}</strong>
|
|
112
|
+
<span>{`.`}</span>
|
|
113
|
+
<br />
|
|
114
|
+
{selectedCourtDate.Type === HearingType.VIRTUAL && (
|
|
115
|
+
<>
|
|
116
|
+
<span>{`The virtual hearing link is `}</span>
|
|
117
|
+
{selectedCourtDate.HearingLink ? (
|
|
118
|
+
<a
|
|
119
|
+
href={selectedCourtDate.HearingLink}
|
|
120
|
+
target='_blank'
|
|
121
|
+
rel='noopener noreferrer'
|
|
122
|
+
style={{ fontWeight: 'bold' }}
|
|
123
|
+
>
|
|
124
|
+
here
|
|
125
|
+
</a>
|
|
126
|
+
) : (
|
|
127
|
+
<strong>No Link Provided</strong>
|
|
128
|
+
)}
|
|
129
|
+
<span>{`.`}</span>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
</Typography>
|
|
133
|
+
</Stack>
|
|
134
|
+
</Stack>
|
|
135
|
+
)}
|
|
136
|
+
</Box>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Stack from '@mui/material/Stack';
|
|
2
|
+
import Typography from '@mui/material/Typography';
|
|
3
|
+
|
|
4
|
+
export default function InfoBox({ label, info, color }: { label: string; info: string; color?: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<Stack
|
|
7
|
+
spacing={0}
|
|
8
|
+
direction='column'
|
|
9
|
+
alignItems='center'
|
|
10
|
+
justifyContent='center'
|
|
11
|
+
border={'2px solid var(--fc-border-color)'}
|
|
12
|
+
p={1}
|
|
13
|
+
borderRadius={2}
|
|
14
|
+
width={'100%'}
|
|
15
|
+
>
|
|
16
|
+
<Typography variant='h5' color={`${color} !important`}>
|
|
17
|
+
{info}
|
|
18
|
+
</Typography>
|
|
19
|
+
<Typography variant='subtitle1'>{label}</Typography>
|
|
20
|
+
</Stack>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import Box from '@mui/material/Box';
|
|
2
|
+
import Typography from '@mui/material/Typography';
|
|
3
|
+
import DescriptionIcon from '@mui/icons-material/Description';
|
|
4
|
+
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
|
|
5
|
+
|
|
6
|
+
const S3_BASE_URL = 'https://aventine-court-notices.s3.us-east-1.amazonaws.com';
|
|
7
|
+
|
|
8
|
+
export default function NoticeFileLink({ fileKey }: { fileKey: string | null }) {
|
|
9
|
+
const hasFile = Boolean(fileKey);
|
|
10
|
+
|
|
11
|
+
function handleClick() {
|
|
12
|
+
if (!fileKey) return;
|
|
13
|
+
const url = fileKey.startsWith('http') ? fileKey : `${S3_BASE_URL}/${fileKey}`;
|
|
14
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box
|
|
19
|
+
mt={1}
|
|
20
|
+
mb={1}
|
|
21
|
+
display='flex'
|
|
22
|
+
alignItems='center'
|
|
23
|
+
justifyContent='center'
|
|
24
|
+
gap={1}
|
|
25
|
+
border='2px solid var(--fc-border-color)'
|
|
26
|
+
borderRadius={2}
|
|
27
|
+
p={1.5}
|
|
28
|
+
sx={{
|
|
29
|
+
cursor: hasFile ? 'pointer' : 'default',
|
|
30
|
+
opacity: hasFile ? 1 : 0.5,
|
|
31
|
+
transition: 'opacity 0.2s',
|
|
32
|
+
'&:hover': hasFile ? { opacity: 0.8 } : undefined,
|
|
33
|
+
}}
|
|
34
|
+
onClick={handleClick}
|
|
35
|
+
>
|
|
36
|
+
{hasFile ? (
|
|
37
|
+
<DescriptionIcon sx={{ color: '#66bb6a' }} />
|
|
38
|
+
) : (
|
|
39
|
+
<InsertDriveFileOutlinedIcon sx={{ color: 'text.secondary' }} />
|
|
40
|
+
)}
|
|
41
|
+
<Typography variant='subtitle1'>{hasFile ? 'View Notice File' : 'No Notice File'}</Typography>
|
|
42
|
+
</Box>
|
|
43
|
+
);
|
|
44
|
+
}
|