@striae-org/striae 4.2.0 → 4.3.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/LICENSE +1 -1
- package/app/components/actions/case-manage.ts +50 -17
- package/app/components/audit/viewer/audit-entries-list.tsx +5 -2
- package/app/components/audit/viewer/use-audit-viewer-data.ts +6 -3
- package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
- package/app/components/canvas/confirmation/confirmation.tsx +6 -2
- package/app/components/colors/colors.module.css +4 -3
- package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -76
- package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
- package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
- package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
- package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
- package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
- package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
- package/app/components/navbar/navbar.tsx +34 -9
- package/app/components/sidebar/cases/case-sidebar.tsx +93 -73
- package/app/components/sidebar/cases/cases-modal.module.css +312 -10
- package/app/components/sidebar/cases/cases-modal.tsx +737 -116
- package/app/components/sidebar/cases/cases.module.css +43 -0
- package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
- package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
- package/app/components/sidebar/files/files-modal.module.css +285 -44
- package/app/components/sidebar/files/files-modal.tsx +482 -177
- package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
- package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
- package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
- package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
- package/app/components/sidebar/notes/class-details-shared.ts +239 -0
- package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +77 -76
- package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
- package/app/components/sidebar/notes/notes.module.css +262 -14
- package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
- package/app/components/sidebar/sidebar-container.tsx +2 -0
- package/app/components/sidebar/sidebar.tsx +15 -1
- package/app/{tailwind.css → global.css} +1 -3
- package/app/hooks/useCaseListPreferences.ts +99 -0
- package/app/hooks/useFileListPreferences.ts +106 -0
- package/app/hooks/useOverlayDismiss.ts +6 -4
- package/app/root.tsx +1 -1
- package/app/routes/striae/striae.tsx +7 -0
- package/app/services/audit/audit.service.ts +2 -2
- package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
- package/app/types/annotations.ts +48 -1
- package/app/types/audit.ts +1 -0
- package/app/utils/data/case-filters.ts +127 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +295 -0
- package/app/utils/data/data-operations.ts +17 -861
- package/app/utils/data/file-filters.ts +201 -0
- package/app/utils/data/index.ts +11 -1
- package/app/utils/data/operations/batch-operations.ts +113 -0
- package/app/utils/data/operations/case-operations.ts +168 -0
- package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
- package/app/utils/data/operations/file-annotation-operations.ts +196 -0
- package/app/utils/data/operations/index.ts +7 -0
- package/app/utils/data/operations/signing-operations.ts +225 -0
- package/app/utils/data/operations/types.ts +42 -0
- package/app/utils/data/operations/validation-operations.ts +48 -0
- package/app/utils/forensics/export-verification.ts +40 -111
- package/functions/api/_shared/firebase-auth.ts +2 -7
- package/functions/api/image/[[path]].ts +23 -22
- package/functions/api/pdf/[[path]].ts +27 -8
- package/package.json +7 -13
- package/scripts/deploy-primershear-emails.sh +1 -1
- package/worker-configuration.d.ts +2 -2
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/image-worker.example.ts +16 -5
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +84 -124
- package/workers/pdf-worker/src/pdf-worker.example.ts +58 -61
- package/workers/pdf-worker/src/report-layout.ts +227 -0
- package/workers/pdf-worker/src/report-types.ts +23 -3
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/src/user-worker.example.ts +17 -0
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/NOTICE +0 -13
- package/app/components/sidebar/notes/notes-modal.tsx +0 -52
- package/postcss.config.js +0 -6
- package/tailwind.config.ts +0 -22
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- /package/workers/pdf-worker/src/assets/{generated-assets.ts → generated-assets.example.ts} +0 -0
|
@@ -1,8 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type React from 'react';
|
|
2
3
|
import type { User } from 'firebase/auth';
|
|
3
4
|
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { useCaseListPreferences, DEFAULT_CASES_MODAL_PREFERENCES } from '~/hooks/useCaseListPreferences';
|
|
6
|
+
import {
|
|
7
|
+
type CasesModalCaseItem,
|
|
8
|
+
type CasesModalSortBy,
|
|
9
|
+
type CasesModalConfirmationFilter,
|
|
10
|
+
getCasesForModal,
|
|
11
|
+
} from '~/utils/data/case-filters';
|
|
12
|
+
import {
|
|
13
|
+
archiveCase,
|
|
14
|
+
deleteCase,
|
|
15
|
+
renameCase,
|
|
16
|
+
validateCaseNumber,
|
|
17
|
+
} from '~/components/actions/case-manage';
|
|
18
|
+
import { RenameCaseModal } from '../../navbar/case-modals/rename-case-modal';
|
|
19
|
+
import { ArchiveCaseModal } from '../../navbar/case-modals/archive-case-modal';
|
|
20
|
+
import { DeleteCaseModal } from '../../navbar/case-modals/delete-case-modal';
|
|
21
|
+
import {
|
|
22
|
+
ensureCaseConfirmationSummary,
|
|
23
|
+
getCaseData,
|
|
24
|
+
getConfirmationSummaryDocument,
|
|
25
|
+
getUserCases,
|
|
26
|
+
getUserReadOnlyCases,
|
|
27
|
+
} from '~/utils/data';
|
|
6
28
|
import { fetchFiles } from '~/components/actions/image-manage';
|
|
7
29
|
import styles from './cases-modal.module.css';
|
|
8
30
|
|
|
@@ -12,13 +34,58 @@ interface CasesModalProps {
|
|
|
12
34
|
onSelectCase: (caseNum: string) => void;
|
|
13
35
|
currentCase: string;
|
|
14
36
|
user: User;
|
|
37
|
+
confirmationSaveVersion?: number;
|
|
15
38
|
}
|
|
16
39
|
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
interface CaseConfirmationStatus {
|
|
41
|
+
[caseNum: string]: { includeConfirmation: boolean; isConfirmed: boolean };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const CASES_PER_PAGE = 10;
|
|
45
|
+
|
|
46
|
+
const DEFAULT_CONFIRMATION_STATUS = {
|
|
47
|
+
includeConfirmation: false,
|
|
48
|
+
isConfirmed: false,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getCaseUpdatedLabel = (createdAt: string): string => {
|
|
52
|
+
const parsed = Date.parse(createdAt);
|
|
53
|
+
|
|
54
|
+
if (Number.isNaN(parsed)) {
|
|
55
|
+
return 'Date unavailable';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Date(parsed).toLocaleDateString();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const CasesModal = ({
|
|
62
|
+
isOpen,
|
|
63
|
+
onClose,
|
|
64
|
+
onSelectCase,
|
|
65
|
+
currentCase,
|
|
66
|
+
user,
|
|
67
|
+
confirmationSaveVersion = 0
|
|
68
|
+
}: CasesModalProps) => {
|
|
69
|
+
const [allCases, setAllCases] = useState<CasesModalCaseItem[]>([]);
|
|
19
70
|
const [isLoading, setIsLoading] = useState(false);
|
|
71
|
+
const [isRunningAction, setIsRunningAction] = useState(false);
|
|
72
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
73
|
+
const [isRenameModalOpen, setIsRenameModalOpen] = useState(false);
|
|
74
|
+
const [isArchiveModalOpen, setIsArchiveModalOpen] = useState(false);
|
|
75
|
+
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
76
|
+
const [actionNotice, setActionNotice] = useState<{ type: 'success' | 'warning' | 'error'; message: string } | null>(null);
|
|
20
77
|
const [error, setError] = useState<string>('');
|
|
21
78
|
const [currentPage, setCurrentPage] = useState(0);
|
|
79
|
+
const [selectedCaseNumber, setSelectedCaseNumber] = useState<string | null>(currentCase || null);
|
|
80
|
+
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
81
|
+
const caseRowRefs = useRef<Array<HTMLDivElement | null>>([]);
|
|
82
|
+
const {
|
|
83
|
+
preferences,
|
|
84
|
+
setSortBy,
|
|
85
|
+
setConfirmationFilter,
|
|
86
|
+
setShowArchivedOnly,
|
|
87
|
+
resetPreferences,
|
|
88
|
+
} = useCaseListPreferences();
|
|
22
89
|
const {
|
|
23
90
|
requestClose,
|
|
24
91
|
overlayProps,
|
|
@@ -27,10 +94,56 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
|
|
|
27
94
|
isOpen,
|
|
28
95
|
onClose
|
|
29
96
|
});
|
|
30
|
-
const [caseConfirmationStatus, setCaseConfirmationStatus] = useState<{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
97
|
+
const [caseConfirmationStatus, setCaseConfirmationStatus] = useState<CaseConfirmationStatus>({});
|
|
98
|
+
const caseConfirmationStatusRef = useRef<CaseConfirmationStatus>({});
|
|
99
|
+
|
|
100
|
+
const loadCases = useCallback(async () => {
|
|
101
|
+
try {
|
|
102
|
+
const [ownedCases, readOnlyCases] = await Promise.all([
|
|
103
|
+
getUserCases(user),
|
|
104
|
+
getUserReadOnlyCases(user),
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const ownedCaseEntries = await Promise.all(
|
|
108
|
+
ownedCases.map(async (entry) => {
|
|
109
|
+
const caseData = await getCaseData(user, entry.caseNumber).catch(() => null);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
caseNumber: entry.caseNumber,
|
|
113
|
+
createdAt: entry.createdAt,
|
|
114
|
+
archived: caseData?.archived === true,
|
|
115
|
+
isReadOnly: false,
|
|
116
|
+
} as CasesModalCaseItem;
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const readOnlyEntries: CasesModalCaseItem[] = readOnlyCases.map((entry) => ({
|
|
121
|
+
caseNumber: entry.caseNumber,
|
|
122
|
+
createdAt: entry.importedAt,
|
|
123
|
+
archived: false,
|
|
124
|
+
isReadOnly: true,
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
const mergedCasesMap = new Map<string, CasesModalCaseItem>();
|
|
128
|
+
[...ownedCaseEntries, ...readOnlyEntries].forEach((entry) => {
|
|
129
|
+
if (!mergedCasesMap.has(entry.caseNumber)) {
|
|
130
|
+
mergedCasesMap.set(entry.caseNumber, entry);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
setAllCases(Array.from(mergedCasesMap.values()));
|
|
135
|
+
setSelectedCaseNumber((previous) => previous ?? (currentCase || null));
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error('Failed to load cases:', err);
|
|
138
|
+
setError('Failed to load cases');
|
|
139
|
+
} finally {
|
|
140
|
+
setIsLoading(false);
|
|
141
|
+
}
|
|
142
|
+
}, [user, currentCase]);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
caseConfirmationStatusRef.current = caseConfirmationStatus;
|
|
146
|
+
}, [caseConfirmationStatus]);
|
|
34
147
|
|
|
35
148
|
const startLoading = () => {
|
|
36
149
|
setIsLoading(true);
|
|
@@ -42,100 +155,419 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
|
|
|
42
155
|
const loadingTimer = window.setTimeout(() => {
|
|
43
156
|
startLoading();
|
|
44
157
|
}, 0);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.then(fetchedCases => {
|
|
48
|
-
setCases(fetchedCases);
|
|
49
|
-
})
|
|
50
|
-
.catch(err => {
|
|
51
|
-
console.error('Failed to load cases:', err);
|
|
52
|
-
setError('Failed to load cases');
|
|
53
|
-
})
|
|
54
|
-
.finally(() => {
|
|
55
|
-
setIsLoading(false);
|
|
56
|
-
});
|
|
158
|
+
|
|
159
|
+
void loadCases();
|
|
57
160
|
|
|
58
161
|
return () => {
|
|
59
162
|
window.clearTimeout(loadingTimer);
|
|
60
163
|
};
|
|
61
164
|
}
|
|
62
|
-
}, [isOpen,
|
|
165
|
+
}, [isOpen, loadCases]);
|
|
166
|
+
|
|
167
|
+
const archiveScopedCases = useMemo(() => {
|
|
168
|
+
if (preferences.showArchivedOnly) {
|
|
169
|
+
return allCases.filter((entry) => entry.archived && !entry.isReadOnly);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return allCases.filter((entry) => !entry.archived && !entry.isReadOnly);
|
|
173
|
+
}, [allCases, preferences.showArchivedOnly]);
|
|
174
|
+
|
|
175
|
+
const visibleCases = useMemo(() => {
|
|
176
|
+
const baseCases = getCasesForModal(allCases, preferences, caseConfirmationStatus);
|
|
177
|
+
const normalizedQuery = searchQuery.trim().toLowerCase();
|
|
178
|
+
|
|
179
|
+
if (!normalizedQuery) {
|
|
180
|
+
return baseCases;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return baseCases.filter((entry) =>
|
|
184
|
+
entry.caseNumber.toLowerCase().includes(normalizedQuery)
|
|
185
|
+
);
|
|
186
|
+
}, [allCases, preferences, caseConfirmationStatus, searchQuery]);
|
|
187
|
+
|
|
188
|
+
const totalPages = Math.max(1, Math.ceil(visibleCases.length / CASES_PER_PAGE));
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (currentPage > totalPages - 1) {
|
|
192
|
+
setCurrentPage(totalPages - 1);
|
|
193
|
+
}
|
|
194
|
+
}, [currentPage, totalPages]);
|
|
63
195
|
|
|
64
|
-
const paginatedCases =
|
|
196
|
+
const paginatedCases = visibleCases.slice(
|
|
65
197
|
currentPage * CASES_PER_PAGE,
|
|
66
198
|
(currentPage + 1) * CASES_PER_PAGE
|
|
67
199
|
);
|
|
68
200
|
|
|
69
|
-
const
|
|
201
|
+
const hasCustomPreferences =
|
|
202
|
+
preferences.sortBy !== DEFAULT_CASES_MODAL_PREFERENCES.sortBy ||
|
|
203
|
+
preferences.confirmationFilter !== DEFAULT_CASES_MODAL_PREFERENCES.confirmationFilter ||
|
|
204
|
+
preferences.showArchivedOnly !== DEFAULT_CASES_MODAL_PREFERENCES.showArchivedOnly;
|
|
205
|
+
|
|
206
|
+
const selectedCase = useMemo(
|
|
207
|
+
() => allCases.find((entry) => entry.caseNumber === selectedCaseNumber) ?? null,
|
|
208
|
+
[allCases, selectedCaseNumber]
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const canRenameSelectedCase = Boolean(
|
|
212
|
+
selectedCase && !selectedCase.archived && !selectedCase.isReadOnly
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const canArchiveSelectedCase = Boolean(
|
|
216
|
+
selectedCase && !selectedCase.archived && !selectedCase.isReadOnly
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const canDeleteSelectedCase = Boolean(
|
|
220
|
+
selectedCase && selectedCase.caseNumber !== currentCase && !selectedCase.isReadOnly
|
|
221
|
+
);
|
|
70
222
|
|
|
71
|
-
// Fetch confirmation status only for currently visible paginated cases
|
|
72
223
|
useEffect(() => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
currentPage * CASES_PER_PAGE,
|
|
76
|
-
(currentPage + 1) * CASES_PER_PAGE
|
|
77
|
-
);
|
|
224
|
+
setCurrentPage(0);
|
|
225
|
+
}, [preferences.sortBy, preferences.confirmationFilter, preferences.showArchivedOnly]);
|
|
78
226
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (paginatedCases.length === 0) {
|
|
229
|
+
setFocusedIndex(0);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
82
232
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// Fetch annotations for each file in the case (in parallel)
|
|
89
|
-
const fileStatuses = await Promise.all(
|
|
90
|
-
files.map(async (file) => {
|
|
91
|
-
try {
|
|
92
|
-
const annotations = await getFileAnnotations(user, caseNum, file.id);
|
|
93
|
-
return {
|
|
94
|
-
includeConfirmation: annotations?.includeConfirmation ?? false,
|
|
95
|
-
isConfirmed: !!(annotations?.includeConfirmation && annotations?.confirmationData),
|
|
96
|
-
};
|
|
97
|
-
} catch {
|
|
98
|
-
return { includeConfirmation: false, isConfirmed: false };
|
|
99
|
-
}
|
|
100
|
-
})
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
// Calculate case status
|
|
104
|
-
const filesRequiringConfirmation = fileStatuses.filter(s => s.includeConfirmation);
|
|
105
|
-
const allConfirmedFiles = filesRequiringConfirmation.every(s => s.isConfirmed);
|
|
233
|
+
if (focusedIndex > paginatedCases.length - 1) {
|
|
234
|
+
setFocusedIndex(paginatedCases.length - 1);
|
|
235
|
+
}
|
|
236
|
+
}, [paginatedCases, focusedIndex]);
|
|
106
237
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (!selectedCaseNumber) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const exists = allCases.some((entry) => entry.caseNumber === selectedCaseNumber);
|
|
244
|
+
if (!exists) {
|
|
245
|
+
setSelectedCaseNumber(null);
|
|
246
|
+
}
|
|
247
|
+
}, [allCases, selectedCaseNumber]);
|
|
248
|
+
|
|
249
|
+
const hydrateCaseConfirmationStatuses = useCallback(async (caseNumbers: string[]) => {
|
|
250
|
+
const missingCaseNumbers = caseNumbers.filter(
|
|
251
|
+
(caseNum) => !caseConfirmationStatusRef.current[caseNum]
|
|
252
|
+
);
|
|
121
253
|
|
|
122
|
-
|
|
123
|
-
|
|
254
|
+
if (missingCaseNumbers.length === 0) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const caseStatusPromises = missingCaseNumbers.map(async (caseNum) => {
|
|
259
|
+
try {
|
|
260
|
+
const files = await fetchFiles(user, caseNum);
|
|
261
|
+
const caseSummary = await ensureCaseConfirmationSummary(user, caseNum, files);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
caseNum,
|
|
265
|
+
includeConfirmation: caseSummary.includeConfirmation,
|
|
266
|
+
isConfirmed: caseSummary.isConfirmed,
|
|
267
|
+
};
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error(`Error fetching confirmation status for case ${caseNum}:`, err);
|
|
270
|
+
return {
|
|
271
|
+
caseNum,
|
|
272
|
+
includeConfirmation: false,
|
|
273
|
+
isConfirmed: false,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const results = await Promise.all(caseStatusPromises);
|
|
279
|
+
|
|
280
|
+
setCaseConfirmationStatus((previous) => {
|
|
281
|
+
const next = { ...previous };
|
|
124
282
|
|
|
125
|
-
// Build the statuses map from results
|
|
126
|
-
const statuses: { [caseNum: string]: { includeConfirmation: boolean; isConfirmed: boolean } } = {};
|
|
127
283
|
results.forEach((result) => {
|
|
128
|
-
|
|
284
|
+
next[result.caseNum] = {
|
|
129
285
|
includeConfirmation: result.includeConfirmation,
|
|
130
286
|
isConfirmed: result.isConfirmed,
|
|
131
287
|
};
|
|
132
288
|
});
|
|
133
289
|
|
|
290
|
+
return next;
|
|
291
|
+
});
|
|
292
|
+
}, [user]);
|
|
293
|
+
|
|
294
|
+
// Fetch confirmation status only for currently visible paginated cases
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
let isCancelled = false;
|
|
297
|
+
|
|
298
|
+
const loadConfirmationSummary = async () => {
|
|
299
|
+
if (!isOpen) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const summary = await getConfirmationSummaryDocument(user).catch((err) => {
|
|
304
|
+
console.error('Failed to load confirmation summary:', err);
|
|
305
|
+
return null;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!summary || isCancelled) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const statuses: CaseConfirmationStatus = {};
|
|
313
|
+
for (const [caseNum, entry] of Object.entries(summary.cases)) {
|
|
314
|
+
statuses[caseNum] = {
|
|
315
|
+
includeConfirmation: entry.includeConfirmation,
|
|
316
|
+
isConfirmed: entry.isConfirmed
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
134
320
|
setCaseConfirmationStatus(statuses);
|
|
135
321
|
};
|
|
136
322
|
|
|
137
|
-
|
|
138
|
-
|
|
323
|
+
loadConfirmationSummary();
|
|
324
|
+
|
|
325
|
+
return () => {
|
|
326
|
+
isCancelled = true;
|
|
327
|
+
};
|
|
328
|
+
}, [isOpen, user, confirmationSaveVersion]);
|
|
329
|
+
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
if (!isOpen || paginatedCases.length === 0) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
void hydrateCaseConfirmationStatuses(paginatedCases.map((entry) => entry.caseNumber));
|
|
336
|
+
}, [isOpen, paginatedCases, hydrateCaseConfirmationStatuses]);
|
|
337
|
+
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
if (!isOpen || preferences.confirmationFilter === 'all' || archiveScopedCases.length === 0) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
void hydrateCaseConfirmationStatuses(archiveScopedCases.map((entry) => entry.caseNumber));
|
|
344
|
+
}, [
|
|
345
|
+
isOpen,
|
|
346
|
+
preferences.confirmationFilter,
|
|
347
|
+
archiveScopedCases,
|
|
348
|
+
hydrateCaseConfirmationStatuses,
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
const handleSelectCase = (caseNum: string, index: number) => {
|
|
352
|
+
setSelectedCaseNumber(caseNum);
|
|
353
|
+
setFocusedIndex(index);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const handleOpenSelectedCase = () => {
|
|
357
|
+
if (!selectedCaseNumber) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
onSelectCase(selectedCaseNumber);
|
|
362
|
+
requestClose();
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const handleRenameSelectedCase = async () => {
|
|
366
|
+
if (!selectedCase || !canRenameSelectedCase) {
|
|
367
|
+
setActionNotice({
|
|
368
|
+
type: 'warning',
|
|
369
|
+
message: 'Selected case cannot be renamed.',
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
setActionNotice(null);
|
|
375
|
+
setIsRenameModalOpen(true);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const handleRenameSelectedCaseSubmit = async (nextCaseName: string) => {
|
|
379
|
+
if (!selectedCase) {
|
|
380
|
+
setActionNotice({
|
|
381
|
+
type: 'error',
|
|
382
|
+
message: 'No selected case to rename.',
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const nextCaseNumber = nextCaseName.trim();
|
|
388
|
+
if (!nextCaseNumber) {
|
|
389
|
+
setActionNotice({
|
|
390
|
+
type: 'error',
|
|
391
|
+
message: 'Provide a new case number.',
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!validateCaseNumber(nextCaseNumber)) {
|
|
397
|
+
setActionNotice({
|
|
398
|
+
type: 'error',
|
|
399
|
+
message: 'Invalid case number format.',
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
setIsRunningAction(true);
|
|
405
|
+
setActionNotice(null);
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
await renameCase(user, selectedCase.caseNumber, nextCaseNumber);
|
|
409
|
+
await loadCases();
|
|
410
|
+
setSelectedCaseNumber(nextCaseNumber);
|
|
411
|
+
setIsRenameModalOpen(false);
|
|
412
|
+
|
|
413
|
+
if (selectedCase.caseNumber === currentCase) {
|
|
414
|
+
onSelectCase(nextCaseNumber);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
setActionNotice({
|
|
418
|
+
type: 'success',
|
|
419
|
+
message: `Case renamed to ${nextCaseNumber}.`,
|
|
420
|
+
});
|
|
421
|
+
} catch (renameError) {
|
|
422
|
+
setActionNotice({
|
|
423
|
+
type: 'error',
|
|
424
|
+
message: renameError instanceof Error ? renameError.message : 'Failed to rename case.',
|
|
425
|
+
});
|
|
426
|
+
} finally {
|
|
427
|
+
setIsRunningAction(false);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const handleArchiveSelectedCase = async () => {
|
|
432
|
+
if (!selectedCase || !canArchiveSelectedCase) {
|
|
433
|
+
setActionNotice({
|
|
434
|
+
type: 'warning',
|
|
435
|
+
message: 'Selected case cannot be archived.',
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
setActionNotice(null);
|
|
441
|
+
setIsArchiveModalOpen(true);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const handleArchiveSelectedCaseSubmit = async (archiveReason: string) => {
|
|
445
|
+
if (!selectedCase) {
|
|
446
|
+
setActionNotice({
|
|
447
|
+
type: 'error',
|
|
448
|
+
message: 'No selected case to archive.',
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
setIsRunningAction(true);
|
|
454
|
+
setActionNotice(null);
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
await archiveCase(user, selectedCase.caseNumber, archiveReason);
|
|
458
|
+
await loadCases();
|
|
459
|
+
setIsArchiveModalOpen(false);
|
|
460
|
+
|
|
461
|
+
if (selectedCase.caseNumber === currentCase) {
|
|
462
|
+
onSelectCase(selectedCase.caseNumber);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
setActionNotice({
|
|
466
|
+
type: 'success',
|
|
467
|
+
message: 'Case archived successfully.',
|
|
468
|
+
});
|
|
469
|
+
} catch (archiveError) {
|
|
470
|
+
setActionNotice({
|
|
471
|
+
type: 'error',
|
|
472
|
+
message: archiveError instanceof Error ? archiveError.message : 'Failed to archive case.',
|
|
473
|
+
});
|
|
474
|
+
} finally {
|
|
475
|
+
setIsRunningAction(false);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const handleDeleteSelectedCase = async () => {
|
|
480
|
+
if (!selectedCase || !canDeleteSelectedCase) {
|
|
481
|
+
const isCurrentCaseSelection = selectedCase?.caseNumber === currentCase;
|
|
482
|
+
|
|
483
|
+
setActionNotice({
|
|
484
|
+
type: 'warning',
|
|
485
|
+
message: isCurrentCaseSelection
|
|
486
|
+
? 'Open a different case before deleting this one.'
|
|
487
|
+
: 'Selected case cannot be deleted.',
|
|
488
|
+
});
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
setActionNotice(null);
|
|
493
|
+
setIsDeleteModalOpen(true);
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const handleDeleteSelectedCaseSubmit = async () => {
|
|
497
|
+
if (!selectedCase) {
|
|
498
|
+
setActionNotice({
|
|
499
|
+
type: 'error',
|
|
500
|
+
message: 'No selected case to delete.',
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
setIsRunningAction(true);
|
|
506
|
+
setActionNotice(null);
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
const deleteResult = await deleteCase(user, selectedCase.caseNumber);
|
|
510
|
+
await loadCases();
|
|
511
|
+
setSelectedCaseNumber(null);
|
|
512
|
+
setIsDeleteModalOpen(false);
|
|
513
|
+
|
|
514
|
+
if (deleteResult.missingImages.length > 0) {
|
|
515
|
+
setActionNotice({
|
|
516
|
+
type: 'warning',
|
|
517
|
+
message: `Case deleted. ${deleteResult.missingImages.length} image(s) were missing and skipped.`,
|
|
518
|
+
});
|
|
519
|
+
} else {
|
|
520
|
+
setActionNotice({
|
|
521
|
+
type: 'success',
|
|
522
|
+
message: 'Case deleted successfully.',
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
} catch (deleteError) {
|
|
526
|
+
setActionNotice({
|
|
527
|
+
type: 'error',
|
|
528
|
+
message: deleteError instanceof Error ? deleteError.message : 'Failed to delete case.',
|
|
529
|
+
});
|
|
530
|
+
} finally {
|
|
531
|
+
setIsRunningAction(false);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const handleRowKeyDown = (
|
|
536
|
+
event: React.KeyboardEvent<HTMLDivElement>,
|
|
537
|
+
caseNum: string,
|
|
538
|
+
index: number
|
|
539
|
+
) => {
|
|
540
|
+
if (event.key === 'ArrowDown') {
|
|
541
|
+
event.preventDefault();
|
|
542
|
+
const nextIndex = Math.min(index + 1, paginatedCases.length - 1);
|
|
543
|
+
setFocusedIndex(nextIndex);
|
|
544
|
+
window.requestAnimationFrame(() => {
|
|
545
|
+
caseRowRefs.current[nextIndex]?.focus();
|
|
546
|
+
});
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (event.key === 'ArrowUp') {
|
|
551
|
+
event.preventDefault();
|
|
552
|
+
const nextIndex = Math.max(index - 1, 0);
|
|
553
|
+
setFocusedIndex(nextIndex);
|
|
554
|
+
window.requestAnimationFrame(() => {
|
|
555
|
+
caseRowRefs.current[nextIndex]?.focus();
|
|
556
|
+
});
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
561
|
+
event.preventDefault();
|
|
562
|
+
handleSelectCase(caseNum, index);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (event.key === 'Escape') {
|
|
567
|
+
event.preventDefault();
|
|
568
|
+
requestClose();
|
|
569
|
+
}
|
|
570
|
+
};
|
|
139
571
|
|
|
140
572
|
if (!isOpen) return null;
|
|
141
573
|
|
|
@@ -150,62 +582,251 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
|
|
|
150
582
|
<h2>All Cases</h2>
|
|
151
583
|
<button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close cases dialog' })}>×</button>
|
|
152
584
|
</header>
|
|
153
|
-
|
|
585
|
+
|
|
154
586
|
<div className={styles.modalContent}>
|
|
155
587
|
{isLoading ? (
|
|
156
588
|
<p className={styles.loading}>Loading cases...</p>
|
|
157
589
|
) : error ? (
|
|
158
590
|
<p className={styles.error}>{error}</p>
|
|
159
|
-
) :
|
|
591
|
+
) : allCases.length === 0 ? (
|
|
160
592
|
<p className={styles.emptyState}>No cases found</p>
|
|
161
593
|
) : (
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
594
|
+
<>
|
|
595
|
+
<section className={styles.controlsSection} aria-label="Case list controls">
|
|
596
|
+
<div className={styles.controlGroup}>
|
|
597
|
+
<label htmlFor="cases-sort">Sort</label>
|
|
598
|
+
<select
|
|
599
|
+
id="cases-sort"
|
|
600
|
+
value={preferences.sortBy}
|
|
601
|
+
onChange={(event) => setSortBy(event.target.value as CasesModalSortBy)}
|
|
602
|
+
>
|
|
603
|
+
<option value="recent">Most Recent</option>
|
|
604
|
+
<option value="alphabetical">Numerical/Alphabetical</option>
|
|
605
|
+
</select>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<div className={styles.controlGroup}>
|
|
609
|
+
<label htmlFor="cases-confirmation-filter">Confirmation Status</label>
|
|
610
|
+
<select
|
|
611
|
+
id="cases-confirmation-filter"
|
|
612
|
+
value={preferences.confirmationFilter}
|
|
613
|
+
onChange={(event) =>
|
|
614
|
+
setConfirmationFilter(event.target.value as CasesModalConfirmationFilter)
|
|
615
|
+
}
|
|
616
|
+
>
|
|
617
|
+
<option value="all">All Cases</option>
|
|
618
|
+
<option value="pending">Pending Confirmation</option>
|
|
619
|
+
<option value="confirmed">Confirmed</option>
|
|
620
|
+
<option value="none-requested">None Requested</option>
|
|
621
|
+
</select>
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
<label className={styles.archiveToggle}>
|
|
625
|
+
<input
|
|
626
|
+
type="checkbox"
|
|
627
|
+
checked={preferences.showArchivedOnly}
|
|
628
|
+
onChange={(event) => setShowArchivedOnly(event.target.checked)}
|
|
629
|
+
/>
|
|
630
|
+
Archived only
|
|
631
|
+
</label>
|
|
632
|
+
|
|
633
|
+
<button
|
|
634
|
+
type="button"
|
|
635
|
+
className={styles.resetButton}
|
|
636
|
+
onClick={resetPreferences}
|
|
637
|
+
disabled={!hasCustomPreferences && searchQuery.trim().length === 0}
|
|
638
|
+
>
|
|
639
|
+
Reset
|
|
640
|
+
</button>
|
|
641
|
+
</section>
|
|
642
|
+
|
|
643
|
+
<div className={styles.searchSection}>
|
|
644
|
+
<label htmlFor="case-search">Search case number</label>
|
|
645
|
+
<input
|
|
646
|
+
id="case-search"
|
|
647
|
+
type="text"
|
|
648
|
+
value={searchQuery}
|
|
649
|
+
onChange={(event) => setSearchQuery(event.target.value)}
|
|
650
|
+
placeholder="Type to filter case numbers"
|
|
651
|
+
className={styles.searchInput}
|
|
652
|
+
/>
|
|
653
|
+
</div>
|
|
654
|
+
|
|
655
|
+
<p className={styles.caseCount}>
|
|
656
|
+
{visibleCases.length} shown of {allCases.length} total cases
|
|
657
|
+
</p>
|
|
658
|
+
|
|
659
|
+
{actionNotice && (
|
|
660
|
+
<p
|
|
661
|
+
className={`${styles.actionNotice} ${
|
|
662
|
+
actionNotice.type === 'error'
|
|
663
|
+
? styles.actionNoticeError
|
|
664
|
+
: actionNotice.type === 'warning'
|
|
665
|
+
? styles.actionNoticeWarning
|
|
666
|
+
: styles.actionNoticeSuccess
|
|
667
|
+
}`}
|
|
668
|
+
>
|
|
669
|
+
{actionNotice.message}
|
|
670
|
+
</p>
|
|
671
|
+
)}
|
|
672
|
+
|
|
673
|
+
{visibleCases.length === 0 ? (
|
|
674
|
+
<p className={styles.emptyState}>No cases match your filters</p>
|
|
675
|
+
) : (
|
|
676
|
+
<ul className={styles.casesList} role="listbox" aria-label="Cases list">
|
|
677
|
+
{paginatedCases.map((caseEntry, index) => {
|
|
678
|
+
const caseNum = caseEntry.caseNumber;
|
|
679
|
+
const confirmationStatus = caseConfirmationStatus[caseNum] || DEFAULT_CONFIRMATION_STATUS;
|
|
680
|
+
const isSelected = selectedCaseNumber === caseNum;
|
|
681
|
+
const confirmationLabel = confirmationStatus.includeConfirmation
|
|
682
|
+
? confirmationStatus.isConfirmed
|
|
683
|
+
? 'Confirmed'
|
|
684
|
+
: 'Pending'
|
|
685
|
+
: 'None Requested';
|
|
686
|
+
|
|
687
|
+
let confirmationClass = '';
|
|
688
|
+
|
|
689
|
+
if (confirmationStatus.includeConfirmation) {
|
|
690
|
+
confirmationClass = confirmationStatus.isConfirmed
|
|
691
|
+
? styles.caseItemConfirmed
|
|
692
|
+
: styles.caseItemNotConfirmed;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return (
|
|
696
|
+
<li key={caseNum}>
|
|
697
|
+
<div
|
|
698
|
+
ref={(node) => {
|
|
699
|
+
caseRowRefs.current[index] = node;
|
|
700
|
+
}}
|
|
701
|
+
role="option"
|
|
702
|
+
aria-selected={isSelected}
|
|
703
|
+
tabIndex={focusedIndex === index ? 0 : -1}
|
|
704
|
+
className={`${styles.caseItem} ${isSelected ? styles.active : ''}`}
|
|
705
|
+
onClick={() => handleSelectCase(caseNum, index)}
|
|
706
|
+
onFocus={() => setFocusedIndex(index)}
|
|
707
|
+
onKeyDown={(event) => handleRowKeyDown(event, caseNum, index)}
|
|
708
|
+
>
|
|
709
|
+
<div className={styles.caseDetails}>
|
|
710
|
+
<input
|
|
711
|
+
type="text"
|
|
712
|
+
readOnly
|
|
713
|
+
value={caseNum}
|
|
714
|
+
className={styles.caseNumberInput}
|
|
715
|
+
aria-label={`Case number ${caseNum}`}
|
|
716
|
+
onClick={(event) => event.stopPropagation()}
|
|
717
|
+
/>
|
|
718
|
+
<span className={styles.caseMetaText}>
|
|
719
|
+
Created: {getCaseUpdatedLabel(caseEntry.createdAt)}
|
|
720
|
+
</span>
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<span
|
|
724
|
+
className={`${styles.confirmationBadge} ${confirmationClass}`}
|
|
725
|
+
aria-label={`Confirmation status: ${confirmationLabel}`}
|
|
726
|
+
>
|
|
727
|
+
{confirmationLabel}
|
|
728
|
+
</span>
|
|
729
|
+
</div>
|
|
730
|
+
</li>
|
|
731
|
+
);
|
|
732
|
+
})}
|
|
733
|
+
</ul>
|
|
734
|
+
)}
|
|
735
|
+
</>
|
|
188
736
|
)}
|
|
189
737
|
</div>
|
|
190
|
-
|
|
191
|
-
{
|
|
192
|
-
<div className={styles.
|
|
738
|
+
|
|
739
|
+
<div className={styles.footerActions}>
|
|
740
|
+
<div className={styles.maintenanceActions}>
|
|
741
|
+
<button
|
|
742
|
+
type="button"
|
|
743
|
+
className={styles.secondaryActionButton}
|
|
744
|
+
onClick={() => {
|
|
745
|
+
setSearchQuery('');
|
|
746
|
+
resetPreferences();
|
|
747
|
+
}}
|
|
748
|
+
disabled={isRunningAction}
|
|
749
|
+
>
|
|
750
|
+
Clear Filters
|
|
751
|
+
</button>
|
|
752
|
+
<button
|
|
753
|
+
type="button"
|
|
754
|
+
className={`${styles.secondaryActionButton} ${styles.renameActionButton}`}
|
|
755
|
+
onClick={handleRenameSelectedCase}
|
|
756
|
+
disabled={!canRenameSelectedCase || isRunningAction}
|
|
757
|
+
>
|
|
758
|
+
Rename Selected
|
|
759
|
+
</button>
|
|
193
760
|
<button
|
|
194
|
-
|
|
195
|
-
|
|
761
|
+
type="button"
|
|
762
|
+
className={`${styles.secondaryActionButton} ${styles.archiveActionButton}`}
|
|
763
|
+
onClick={handleArchiveSelectedCase}
|
|
764
|
+
disabled={!canArchiveSelectedCase || isRunningAction}
|
|
196
765
|
>
|
|
197
|
-
|
|
766
|
+
Archive Selected
|
|
198
767
|
</button>
|
|
199
|
-
<span>{currentPage + 1} of {totalPages} ({cases.length} total cases)</span>
|
|
200
768
|
<button
|
|
201
|
-
|
|
202
|
-
|
|
769
|
+
type="button"
|
|
770
|
+
className={`${styles.secondaryActionButton} ${styles.deleteActionButton}`}
|
|
771
|
+
onClick={handleDeleteSelectedCase}
|
|
772
|
+
disabled={!canDeleteSelectedCase || isRunningAction}
|
|
203
773
|
>
|
|
204
|
-
|
|
774
|
+
Delete Selected
|
|
205
775
|
</button>
|
|
206
776
|
</div>
|
|
207
|
-
|
|
777
|
+
|
|
778
|
+
<button
|
|
779
|
+
type="button"
|
|
780
|
+
className={styles.openSelectedButton}
|
|
781
|
+
onClick={handleOpenSelectedCase}
|
|
782
|
+
disabled={!selectedCaseNumber || isRunningAction}
|
|
783
|
+
>
|
|
784
|
+
{isRunningAction ? 'Working...' : 'Open Selected Case'}
|
|
785
|
+
</button>
|
|
786
|
+
|
|
787
|
+
{totalPages > 1 && (
|
|
788
|
+
<div className={styles.pagination}>
|
|
789
|
+
<button
|
|
790
|
+
onClick={() => setCurrentPage(p => p - 1)}
|
|
791
|
+
disabled={currentPage === 0}
|
|
792
|
+
>
|
|
793
|
+
Previous
|
|
794
|
+
</button>
|
|
795
|
+
<span>{currentPage + 1} of {totalPages} ({visibleCases.length} filtered cases)</span>
|
|
796
|
+
<button
|
|
797
|
+
onClick={() => setCurrentPage(p => p + 1)}
|
|
798
|
+
disabled={currentPage === totalPages - 1}
|
|
799
|
+
>
|
|
800
|
+
Next
|
|
801
|
+
</button>
|
|
802
|
+
</div>
|
|
803
|
+
)}
|
|
804
|
+
</div>
|
|
208
805
|
</div>
|
|
806
|
+
|
|
807
|
+
<RenameCaseModal
|
|
808
|
+
isOpen={isRenameModalOpen}
|
|
809
|
+
currentCase={selectedCase?.caseNumber || ''}
|
|
810
|
+
isSubmitting={isRunningAction}
|
|
811
|
+
onClose={() => setIsRenameModalOpen(false)}
|
|
812
|
+
onSubmit={handleRenameSelectedCaseSubmit}
|
|
813
|
+
/>
|
|
814
|
+
|
|
815
|
+
<ArchiveCaseModal
|
|
816
|
+
isOpen={isArchiveModalOpen}
|
|
817
|
+
currentCase={selectedCase?.caseNumber || ''}
|
|
818
|
+
isSubmitting={isRunningAction}
|
|
819
|
+
onClose={() => setIsArchiveModalOpen(false)}
|
|
820
|
+
onSubmit={handleArchiveSelectedCaseSubmit}
|
|
821
|
+
/>
|
|
822
|
+
|
|
823
|
+
<DeleteCaseModal
|
|
824
|
+
isOpen={isDeleteModalOpen}
|
|
825
|
+
currentCase={selectedCase?.caseNumber || ''}
|
|
826
|
+
isSubmitting={isRunningAction}
|
|
827
|
+
onClose={() => setIsDeleteModalOpen(false)}
|
|
828
|
+
onSubmit={handleDeleteSelectedCaseSubmit}
|
|
829
|
+
/>
|
|
209
830
|
</div>
|
|
210
831
|
);
|
|
211
832
|
};
|