@striae-org/striae 4.1.0 → 4.2.1
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/.env.example +8 -0
- package/LICENSE +1 -1
- package/app/components/actions/case-export/core-export.ts +14 -8
- package/app/components/actions/case-export/data-processing.ts +1 -0
- package/app/components/actions/case-export/download-handlers.ts +7 -0
- package/app/components/actions/case-export/metadata-helpers.ts +2 -1
- package/app/components/actions/case-import/confirmation-import.ts +12 -2
- package/app/components/actions/case-import/orchestrator.ts +78 -32
- package/app/components/actions/case-import/storage-operations.ts +97 -8
- package/app/components/actions/case-import/zip-processing.ts +159 -86
- package/app/components/actions/case-manage.ts +463 -8
- package/app/components/actions/confirm-export.ts +9 -2
- package/app/components/actions/image-manage.ts +77 -44
- package/app/components/audit/user-audit-viewer.tsx +19 -8
- package/app/components/audit/user-audit.module.css +21 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +12 -2
- package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +24 -1
- package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
- package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
- package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
- package/app/components/canvas/canvas.module.css +64 -54
- package/app/components/canvas/canvas.tsx +14 -16
- package/app/components/canvas/confirmation/confirmation.module.css +1 -0
- package/app/components/canvas/confirmation/confirmation.tsx +12 -14
- package/app/components/colors/colors.module.css +4 -3
- package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
- package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
- package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
- package/app/components/navbar/navbar.module.css +447 -0
- package/app/components/navbar/navbar.tsx +402 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
- package/app/components/sidebar/case-export/case-export.module.css +1 -0
- package/app/components/sidebar/case-export/case-export.tsx +8 -46
- package/app/components/sidebar/case-import/case-import.module.css +23 -0
- package/app/components/sidebar/case-import/case-import.tsx +64 -16
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +68 -588
- package/app/components/sidebar/cases/cases-modal.module.css +1 -0
- package/app/components/sidebar/cases/cases-modal.tsx +82 -43
- package/app/components/sidebar/cases/cases.module.css +82 -21
- package/app/components/sidebar/files/files-modal.module.css +1 -0
- package/app/components/sidebar/files/files-modal.tsx +49 -52
- package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
- package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +187 -138
- package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
- package/app/components/sidebar/notes/notes-editor-modal.tsx +64 -0
- package/app/components/sidebar/notes/notes.module.css +170 -1
- package/app/components/sidebar/sidebar-container.tsx +16 -28
- package/app/components/sidebar/sidebar.module.css +5 -69
- package/app/components/sidebar/sidebar.tsx +27 -125
- package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
- package/app/components/user/inactivity-warning.module.css +1 -0
- package/app/components/user/inactivity-warning.tsx +15 -2
- package/app/components/user/manage-profile.tsx +23 -10
- package/app/{tailwind.css → global.css} +1 -3
- package/app/hooks/useOverlayDismiss.ts +54 -4
- package/app/root.tsx +1 -1
- package/app/routes/auth/login.tsx +785 -774
- package/app/routes/striae/striae.module.css +10 -3
- package/app/routes/striae/striae.tsx +475 -30
- package/app/services/audit/audit.service.ts +173 -27
- package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
- package/app/services/audit/builders/index.ts +1 -0
- package/app/types/audit.ts +4 -1
- package/app/types/case.ts +29 -0
- package/app/types/import.ts +3 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
- package/app/utils/data/data-operations.ts +17 -861
- 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/data/permissions.ts +16 -1
- package/app/utils/forensics/audit-export-signature.ts +5 -1
- package/app/utils/forensics/confirmation-signature.ts +3 -0
- package/app/utils/forensics/export-verification.ts +426 -22
- package/functions/api/_shared/firebase-auth.ts +2 -7
- package/functions/api/image/[[path]].ts +20 -23
- package/functions/api/pdf/[[path]].ts +27 -8
- package/package.json +7 -12
- package/scripts/deploy-primershear-emails.sh +2 -1
- package/worker-configuration.d.ts +3 -3
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/worker-configuration.d.ts +7448 -11323
- 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/worker-configuration.d.ts +7447 -11322
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +1 -1
- package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
- 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 +9 -14
- package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
- package/workers/pdf-worker/src/report-types.ts +3 -3
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
- 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/worker-configuration.d.ts +7448 -11323
- 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 -53
- package/postcss.config.js +0 -6
- package/public/.well-known/keybase.txt +0 -56
- package/tailwind.config.ts +0 -22
|
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|
|
2
2
|
import type { User } from 'firebase/auth';
|
|
3
3
|
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
4
4
|
import { listCases } from '~/components/actions/case-manage';
|
|
5
|
-
import {
|
|
5
|
+
import { ensureCaseConfirmationSummary, getConfirmationSummaryDocument } from '~/utils/data';
|
|
6
6
|
import { fetchFiles } from '~/components/actions/image-manage';
|
|
7
7
|
import styles from './cases-modal.module.css';
|
|
8
8
|
|
|
@@ -12,16 +12,25 @@ interface CasesModalProps {
|
|
|
12
12
|
onSelectCase: (caseNum: string) => void;
|
|
13
13
|
currentCase: string;
|
|
14
14
|
user: User;
|
|
15
|
+
confirmationSaveVersion?: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export const CasesModal = ({
|
|
18
|
+
export const CasesModal = ({
|
|
19
|
+
isOpen,
|
|
20
|
+
onClose,
|
|
21
|
+
onSelectCase,
|
|
22
|
+
currentCase,
|
|
23
|
+
user,
|
|
24
|
+
confirmationSaveVersion = 0
|
|
25
|
+
}: CasesModalProps) => {
|
|
18
26
|
const [cases, setCases] = useState<string[]>([]);
|
|
19
27
|
const [isLoading, setIsLoading] = useState(false);
|
|
20
28
|
const [error, setError] = useState<string>('');
|
|
21
29
|
const [currentPage, setCurrentPage] = useState(0);
|
|
22
30
|
const {
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
requestClose,
|
|
32
|
+
overlayProps,
|
|
33
|
+
getCloseButtonProps
|
|
25
34
|
} = useOverlayDismiss({
|
|
26
35
|
isOpen,
|
|
27
36
|
onClose
|
|
@@ -69,6 +78,43 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
|
|
|
69
78
|
|
|
70
79
|
// Fetch confirmation status only for currently visible paginated cases
|
|
71
80
|
useEffect(() => {
|
|
81
|
+
let isCancelled = false;
|
|
82
|
+
|
|
83
|
+
const loadConfirmationSummary = async () => {
|
|
84
|
+
if (!isOpen) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const summary = await getConfirmationSummaryDocument(user).catch((err) => {
|
|
89
|
+
console.error('Failed to load confirmation summary:', err);
|
|
90
|
+
return null;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!summary || isCancelled) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const statuses: { [caseNum: string]: { includeConfirmation: boolean; isConfirmed: boolean } } = {};
|
|
98
|
+
for (const [caseNum, entry] of Object.entries(summary.cases)) {
|
|
99
|
+
statuses[caseNum] = {
|
|
100
|
+
includeConfirmation: entry.includeConfirmation,
|
|
101
|
+
isConfirmed: entry.isConfirmed
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setCaseConfirmationStatus(statuses);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
loadConfirmationSummary();
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
isCancelled = true;
|
|
112
|
+
};
|
|
113
|
+
}, [isOpen, user, confirmationSaveVersion]);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
let isCancelled = false;
|
|
117
|
+
|
|
72
118
|
const fetchCaseConfirmationStatuses = async () => {
|
|
73
119
|
const visibleCases = cases.slice(
|
|
74
120
|
currentPage * CASES_PER_PAGE,
|
|
@@ -79,34 +125,21 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
|
|
|
79
125
|
return;
|
|
80
126
|
}
|
|
81
127
|
|
|
82
|
-
|
|
83
|
-
|
|
128
|
+
const missingCaseNumbers = visibleCases.filter((caseNum) => !caseConfirmationStatus[caseNum]);
|
|
129
|
+
if (missingCaseNumbers.length === 0) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const caseStatusPromises = missingCaseNumbers.map(async (caseNum) => {
|
|
84
134
|
try {
|
|
85
135
|
const files = await fetchFiles(user, caseNum, { skipValidation: true });
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const fileStatuses = await Promise.all(
|
|
89
|
-
files.map(async (file) => {
|
|
90
|
-
try {
|
|
91
|
-
const annotations = await getFileAnnotations(user, caseNum, file.id);
|
|
92
|
-
return {
|
|
93
|
-
includeConfirmation: annotations?.includeConfirmation ?? false,
|
|
94
|
-
isConfirmed: !!(annotations?.includeConfirmation && annotations?.confirmationData),
|
|
95
|
-
};
|
|
96
|
-
} catch {
|
|
97
|
-
return { includeConfirmation: false, isConfirmed: false };
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
// Calculate case status
|
|
103
|
-
const filesRequiringConfirmation = fileStatuses.filter(s => s.includeConfirmation);
|
|
104
|
-
const allConfirmedFiles = filesRequiringConfirmation.every(s => s.isConfirmed);
|
|
136
|
+
|
|
137
|
+
const caseSummary = await ensureCaseConfirmationSummary(user, caseNum, files);
|
|
105
138
|
|
|
106
139
|
return {
|
|
107
140
|
caseNum,
|
|
108
|
-
includeConfirmation:
|
|
109
|
-
isConfirmed:
|
|
141
|
+
includeConfirmation: caseSummary.includeConfirmation,
|
|
142
|
+
isConfirmed: caseSummary.isConfirmed,
|
|
110
143
|
};
|
|
111
144
|
} catch (err) {
|
|
112
145
|
console.error(`Error fetching confirmation status for case ${caseNum}:`, err);
|
|
@@ -121,36 +154,42 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
|
|
|
121
154
|
// Wait for all case status fetches to complete
|
|
122
155
|
const results = await Promise.all(caseStatusPromises);
|
|
123
156
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
statuses[result.caseNum] = {
|
|
128
|
-
includeConfirmation: result.includeConfirmation,
|
|
129
|
-
isConfirmed: result.isConfirmed,
|
|
130
|
-
};
|
|
131
|
-
});
|
|
157
|
+
if (isCancelled) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
132
160
|
|
|
133
|
-
setCaseConfirmationStatus(
|
|
161
|
+
setCaseConfirmationStatus((previous) => {
|
|
162
|
+
const next = { ...previous };
|
|
163
|
+
results.forEach((result) => {
|
|
164
|
+
next[result.caseNum] = {
|
|
165
|
+
includeConfirmation: result.includeConfirmation,
|
|
166
|
+
isConfirmed: result.isConfirmed,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return next;
|
|
171
|
+
});
|
|
134
172
|
};
|
|
135
173
|
|
|
136
174
|
fetchCaseConfirmationStatuses();
|
|
137
|
-
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
isCancelled = true;
|
|
178
|
+
};
|
|
179
|
+
}, [isOpen, currentPage, cases, user, caseConfirmationStatus]);
|
|
138
180
|
|
|
139
181
|
if (!isOpen) return null;
|
|
140
182
|
|
|
141
183
|
return (
|
|
142
184
|
<div
|
|
143
185
|
className={styles.modalOverlay}
|
|
144
|
-
onMouseDown={handleOverlayMouseDown}
|
|
145
|
-
onKeyDown={handleOverlayKeyDown}
|
|
146
|
-
role="button"
|
|
147
|
-
tabIndex={0}
|
|
148
186
|
aria-label="Close cases dialog"
|
|
187
|
+
{...overlayProps}
|
|
149
188
|
>
|
|
150
189
|
<div className={styles.modal}>
|
|
151
190
|
<header className={styles.modalHeader}>
|
|
152
191
|
<h2>All Cases</h2>
|
|
153
|
-
<button
|
|
192
|
+
<button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close cases dialog' })}>×</button>
|
|
154
193
|
</header>
|
|
155
194
|
|
|
156
195
|
<div className={styles.modalContent}>
|
|
@@ -178,7 +217,7 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
|
|
|
178
217
|
className={`${styles.caseItem} ${currentCase === caseNum ? styles.active : ''} ${confirmationClass}`}
|
|
179
218
|
onClick={() => {
|
|
180
219
|
onSelectCase(caseNum);
|
|
181
|
-
|
|
220
|
+
requestClose();
|
|
182
221
|
}}
|
|
183
222
|
>
|
|
184
223
|
{caseNum}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/* Case Management */
|
|
2
2
|
.caseSection {
|
|
3
|
-
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 0.75rem;
|
|
6
|
+
height: 100%;
|
|
7
|
+
min-height: 0;
|
|
8
|
+
margin-bottom: 0;
|
|
4
9
|
}
|
|
5
10
|
|
|
6
11
|
.caseSection h4 {
|
|
@@ -141,17 +146,18 @@
|
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
.filesModalSection {
|
|
144
|
-
margin:
|
|
149
|
+
margin: 0.25rem 0 0;
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
.filesModalButton {
|
|
148
153
|
width: 100%;
|
|
149
|
-
padding: 0.75rem;
|
|
154
|
+
padding: 0.625rem 0.75rem;
|
|
150
155
|
background-color: #17a2b8;
|
|
151
156
|
color: white;
|
|
152
157
|
border: none;
|
|
153
158
|
border-radius: 6px;
|
|
154
159
|
font-weight: 500;
|
|
160
|
+
font-size: 0.9rem;
|
|
155
161
|
cursor: pointer;
|
|
156
162
|
transition: all 0.2s;
|
|
157
163
|
box-sizing: border-box;
|
|
@@ -179,16 +185,57 @@
|
|
|
179
185
|
|
|
180
186
|
/* Files Section */
|
|
181
187
|
.filesSection {
|
|
182
|
-
|
|
188
|
+
display: flex;
|
|
189
|
+
flex: 1;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
gap: 0.625rem;
|
|
192
|
+
min-height: 0;
|
|
193
|
+
margin-top: 0.25rem;
|
|
183
194
|
}
|
|
184
195
|
|
|
185
196
|
.filesSection h4 {
|
|
186
|
-
margin-bottom:
|
|
187
|
-
font-size: 1.
|
|
197
|
+
margin-bottom: 0;
|
|
198
|
+
font-size: 1.1rem;
|
|
188
199
|
font-weight: 900;
|
|
189
200
|
text-align: center;
|
|
190
201
|
}
|
|
191
202
|
|
|
203
|
+
.emptyCaseHeader {
|
|
204
|
+
margin-top: 0.75rem;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.openCaseButton {
|
|
208
|
+
width: 100%;
|
|
209
|
+
padding: 0.75rem 1rem;
|
|
210
|
+
background-color: var(--primary);
|
|
211
|
+
color: white;
|
|
212
|
+
border: none;
|
|
213
|
+
border-radius: 6px;
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
font-size: 0.95rem;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
transition: all 0.2s;
|
|
218
|
+
box-sizing: border-box;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.openCaseButton:hover {
|
|
222
|
+
background-color: color-mix(in lab, var(--primary) 85%, var(--black));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.fileListPlaceholder {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
min-height: 5rem;
|
|
230
|
+
padding: 0.875rem;
|
|
231
|
+
border: 1px dashed #dee2e6;
|
|
232
|
+
border-radius: 6px;
|
|
233
|
+
background: #f8f9fa;
|
|
234
|
+
color: #6c757d;
|
|
235
|
+
font-size: 0.875rem;
|
|
236
|
+
text-align: center;
|
|
237
|
+
}
|
|
238
|
+
|
|
192
239
|
.emptyState {
|
|
193
240
|
color: #6c757d;
|
|
194
241
|
font-size: 0.9rem;
|
|
@@ -207,10 +254,20 @@
|
|
|
207
254
|
border: 1px solid #dee2e6;
|
|
208
255
|
border-radius: 6px;
|
|
209
256
|
overflow: hidden;
|
|
210
|
-
|
|
257
|
+
flex: 1;
|
|
258
|
+
min-height: 0;
|
|
259
|
+
max-height: none;
|
|
211
260
|
overflow-y: auto;
|
|
212
261
|
}
|
|
213
262
|
|
|
263
|
+
.fileListMessage {
|
|
264
|
+
padding: 0.875rem;
|
|
265
|
+
color: #6c757d;
|
|
266
|
+
font-size: 0.875rem;
|
|
267
|
+
text-align: center;
|
|
268
|
+
background: #f8f9fa;
|
|
269
|
+
}
|
|
270
|
+
|
|
214
271
|
.fileList::-webkit-scrollbar {
|
|
215
272
|
width: 6px;
|
|
216
273
|
}
|
|
@@ -231,7 +288,7 @@
|
|
|
231
288
|
.fileItem {
|
|
232
289
|
display: flex;
|
|
233
290
|
align-items: center;
|
|
234
|
-
padding: 0.
|
|
291
|
+
padding: 0.375rem 0.625rem;
|
|
235
292
|
border-bottom: 1px solid #dee2e6;
|
|
236
293
|
background: white;
|
|
237
294
|
transition: background-color 0.2s;
|
|
@@ -240,13 +297,14 @@
|
|
|
240
297
|
.fileButton {
|
|
241
298
|
flex: 1;
|
|
242
299
|
text-align: left;
|
|
243
|
-
padding: 0.
|
|
300
|
+
padding: 0.375rem;
|
|
244
301
|
background: none;
|
|
245
302
|
border: none;
|
|
246
303
|
cursor: pointer;
|
|
247
304
|
overflow: hidden;
|
|
248
305
|
text-overflow: ellipsis;
|
|
249
306
|
white-space: nowrap;
|
|
307
|
+
font-size: 0.875rem;
|
|
250
308
|
}
|
|
251
309
|
|
|
252
310
|
.fileItem:last-child {
|
|
@@ -335,14 +393,14 @@
|
|
|
335
393
|
background: none;
|
|
336
394
|
border: none;
|
|
337
395
|
color: #dc3545;
|
|
338
|
-
font-size:
|
|
396
|
+
font-size: 1rem;
|
|
339
397
|
cursor: pointer;
|
|
340
|
-
padding: 0.
|
|
398
|
+
padding: 0.375rem;
|
|
341
399
|
display: flex;
|
|
342
400
|
align-items: center;
|
|
343
401
|
justify-content: center;
|
|
344
|
-
min-width:
|
|
345
|
-
height:
|
|
402
|
+
min-width: 28px;
|
|
403
|
+
height: 28px;
|
|
346
404
|
}
|
|
347
405
|
|
|
348
406
|
.deleteButton:hover {
|
|
@@ -404,18 +462,19 @@
|
|
|
404
462
|
/* Notes Toggle */
|
|
405
463
|
|
|
406
464
|
.sidebarToggle {
|
|
407
|
-
margin-
|
|
408
|
-
padding:
|
|
465
|
+
margin-top: 0;
|
|
466
|
+
padding: 0;
|
|
409
467
|
}
|
|
410
468
|
|
|
411
469
|
.sidebarToggle button {
|
|
412
470
|
width: 100%;
|
|
413
|
-
padding: 0.75rem;
|
|
471
|
+
padding: 0.625rem 0.75rem;
|
|
414
472
|
background-color: var(--primary);
|
|
415
473
|
color: white;
|
|
416
474
|
border: none;
|
|
417
475
|
border-radius: 6px;
|
|
418
476
|
font-weight: 500;
|
|
477
|
+
font-size: 0.9rem;
|
|
419
478
|
cursor: pointer;
|
|
420
479
|
transition: all 0.2s;
|
|
421
480
|
}
|
|
@@ -637,33 +696,35 @@
|
|
|
637
696
|
/* Case Header Container */
|
|
638
697
|
.caseHeader {
|
|
639
698
|
/* Normal case header styling (no background) */
|
|
699
|
+
margin-top: 0.75rem;
|
|
640
700
|
}
|
|
641
701
|
|
|
642
702
|
.readOnlyContainer {
|
|
643
703
|
background: #fff3cd;
|
|
644
704
|
border: 1px solid #ffeaa7;
|
|
645
705
|
border-radius: 4px;
|
|
646
|
-
padding: 0.75rem;
|
|
647
|
-
margin-
|
|
706
|
+
padding: 0.625rem 0.75rem;
|
|
707
|
+
margin-top: 0.75rem;
|
|
708
|
+
margin-bottom: 0;
|
|
648
709
|
}
|
|
649
710
|
|
|
650
711
|
.caseNumber {
|
|
651
712
|
margin: 0;
|
|
652
|
-
font-size:
|
|
713
|
+
font-size: 0.95rem;
|
|
653
714
|
font-weight: 600;
|
|
654
715
|
}
|
|
655
716
|
|
|
656
717
|
/* Case Confirmation Status Indicators */
|
|
657
718
|
.caseNumber.caseNotConfirmed {
|
|
658
719
|
background-color: #fffacd;
|
|
659
|
-
padding: 0.75rem;
|
|
720
|
+
padding: 0.625rem 0.75rem;
|
|
660
721
|
border-radius: 4px;
|
|
661
722
|
margin: 0;
|
|
662
723
|
}
|
|
663
724
|
|
|
664
725
|
.caseNumber.caseConfirmed {
|
|
665
726
|
background-color: #c8e6c9;
|
|
666
|
-
padding: 0.75rem;
|
|
727
|
+
padding: 0.625rem 0.75rem;
|
|
667
728
|
border-radius: 4px;
|
|
668
729
|
margin: 0;
|
|
669
730
|
}
|
|
@@ -3,7 +3,7 @@ import { useState, useContext, useEffect } from 'react';
|
|
|
3
3
|
import { AuthContext } from '~/contexts/auth.context';
|
|
4
4
|
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
5
5
|
import { deleteFile } from '~/components/actions/image-manage';
|
|
6
|
-
import {
|
|
6
|
+
import { ensureCaseConfirmationSummary } from '~/utils/data';
|
|
7
7
|
import { type FileData } from '~/types';
|
|
8
8
|
import styles from './files-modal.module.css';
|
|
9
9
|
|
|
@@ -16,6 +16,7 @@ interface FilesModalProps {
|
|
|
16
16
|
setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
|
|
17
17
|
isReadOnly?: boolean;
|
|
18
18
|
selectedFileId?: string;
|
|
19
|
+
confirmationSaveVersion?: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const FILES_PER_PAGE = 10;
|
|
@@ -28,15 +29,26 @@ interface FileConfirmationStatus {
|
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
export const FilesModal = ({
|
|
32
|
+
export const FilesModal = ({
|
|
33
|
+
isOpen,
|
|
34
|
+
onClose,
|
|
35
|
+
onFileSelect,
|
|
36
|
+
currentCase,
|
|
37
|
+
files,
|
|
38
|
+
setFiles,
|
|
39
|
+
isReadOnly = false,
|
|
40
|
+
selectedFileId,
|
|
41
|
+
confirmationSaveVersion = 0
|
|
42
|
+
}: FilesModalProps) => {
|
|
32
43
|
const { user } = useContext(AuthContext);
|
|
33
44
|
const [error, setError] = useState<string | null>(null);
|
|
34
45
|
const [currentPage, setCurrentPage] = useState(0);
|
|
35
46
|
const [deletingFileId, setDeletingFileId] = useState<string | null>(null);
|
|
36
47
|
const [fileConfirmationStatus, setFileConfirmationStatus] = useState<FileConfirmationStatus>({});
|
|
37
48
|
const {
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
requestClose,
|
|
50
|
+
overlayProps,
|
|
51
|
+
getCloseButtonProps
|
|
40
52
|
} = useOverlayDismiss({
|
|
41
53
|
isOpen,
|
|
42
54
|
onClose
|
|
@@ -47,58 +59,40 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
47
59
|
const endIndex = startIndex + FILES_PER_PAGE;
|
|
48
60
|
const currentFiles = files.slice(startIndex, endIndex);
|
|
49
61
|
|
|
50
|
-
//
|
|
62
|
+
// Hydrate confirmation status from shared summary document.
|
|
51
63
|
useEffect(() => {
|
|
52
|
-
|
|
53
|
-
const visibleFiles = files.slice(
|
|
54
|
-
currentPage * FILES_PER_PAGE,
|
|
55
|
-
currentPage * FILES_PER_PAGE + FILES_PER_PAGE
|
|
56
|
-
);
|
|
64
|
+
let isCancelled = false;
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
const fetchConfirmationStatuses = async () => {
|
|
67
|
+
if (!isOpen || !currentCase || !user || files.length === 0) {
|
|
68
|
+
if (!isCancelled) {
|
|
69
|
+
setFileConfirmationStatus({});
|
|
70
|
+
}
|
|
59
71
|
return;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const annotations = await getFileAnnotations(user, currentCase, file.id);
|
|
66
|
-
return {
|
|
67
|
-
fileId: file.id,
|
|
68
|
-
includeConfirmation: annotations?.includeConfirmation ?? false,
|
|
69
|
-
isConfirmed: !!(annotations?.includeConfirmation && annotations?.confirmationData),
|
|
70
|
-
};
|
|
71
|
-
} catch (err) {
|
|
72
|
-
console.error(`Error fetching annotations for file ${file.id}:`, err);
|
|
73
|
-
return {
|
|
74
|
-
fileId: file.id,
|
|
75
|
-
includeConfirmation: false,
|
|
76
|
-
isConfirmed: false,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
74
|
+
const caseSummary = await ensureCaseConfirmationSummary(user, currentCase, files).catch((err) => {
|
|
75
|
+
console.error(`Error fetching confirmation summary for case ${currentCase}:`, err);
|
|
76
|
+
return null;
|
|
79
77
|
});
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Build the statuses map from results
|
|
85
|
-
const statuses: FileConfirmationStatus = {};
|
|
86
|
-
results.forEach((result) => {
|
|
87
|
-
statuses[result.fileId] = {
|
|
88
|
-
includeConfirmation: result.includeConfirmation,
|
|
89
|
-
isConfirmed: result.isConfirmed,
|
|
90
|
-
};
|
|
91
|
-
});
|
|
79
|
+
if (!caseSummary || isCancelled) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
92
82
|
|
|
93
|
-
setFileConfirmationStatus(
|
|
83
|
+
setFileConfirmationStatus(caseSummary.filesById);
|
|
94
84
|
};
|
|
95
85
|
|
|
96
86
|
fetchConfirmationStatuses();
|
|
97
|
-
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
isCancelled = true;
|
|
90
|
+
};
|
|
91
|
+
}, [isOpen, currentCase, files, user, confirmationSaveVersion]);
|
|
98
92
|
|
|
99
93
|
const handleFileSelect = (file: FileData) => {
|
|
100
94
|
onFileSelect?.(file);
|
|
101
|
-
|
|
95
|
+
requestClose();
|
|
102
96
|
};
|
|
103
97
|
|
|
104
98
|
const handleDeleteFile = async (fileId: string, event: React.MouseEvent) => {
|
|
@@ -116,10 +110,20 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
116
110
|
setDeletingFileId(fileId);
|
|
117
111
|
|
|
118
112
|
try {
|
|
119
|
-
await deleteFile(user, currentCase, fileId);
|
|
113
|
+
const deleteResult = await deleteFile(user, currentCase, fileId);
|
|
120
114
|
// Remove the deleted file from the list
|
|
121
115
|
const updatedFiles = files.filter(f => f.id !== fileId);
|
|
122
116
|
setFiles(updatedFiles);
|
|
117
|
+
setFileConfirmationStatus((previous) => {
|
|
118
|
+
const next = { ...previous };
|
|
119
|
+
delete next[fileId];
|
|
120
|
+
return next;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (deleteResult.imageMissing) {
|
|
124
|
+
setError(`File record deleted. Image asset "${deleteResult.fileName}" was not found and was skipped.`);
|
|
125
|
+
setTimeout(() => setError(null), 4000);
|
|
126
|
+
}
|
|
123
127
|
|
|
124
128
|
// Adjust page if needed
|
|
125
129
|
const newTotalPages = Math.ceil(updatedFiles.length / FILES_PER_PAGE);
|
|
@@ -161,20 +165,13 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
161
165
|
return (
|
|
162
166
|
<div
|
|
163
167
|
className={styles.modalOverlay}
|
|
164
|
-
onMouseDown={handleOverlayMouseDown}
|
|
165
|
-
onKeyDown={handleOverlayKeyDown}
|
|
166
|
-
role="button"
|
|
167
|
-
tabIndex={0}
|
|
168
168
|
aria-label="Close files dialog"
|
|
169
|
+
{...overlayProps}
|
|
169
170
|
>
|
|
170
171
|
<div className={styles.modal}>
|
|
171
172
|
<div className={styles.modalHeader}>
|
|
172
173
|
<h2>Files in Case {currentCase}</h2>
|
|
173
|
-
<button
|
|
174
|
-
className={styles.closeButton}
|
|
175
|
-
onClick={onClose}
|
|
176
|
-
aria-label="Close modal"
|
|
177
|
-
>
|
|
174
|
+
<button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close files dialog' })}>
|
|
178
175
|
×
|
|
179
176
|
</button>
|
|
180
177
|
</div>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
3
|
+
import styles from './notes.module.css';
|
|
4
|
+
|
|
5
|
+
interface AddlNotesModalProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
notes: string;
|
|
9
|
+
onSave: (notes: string) => void;
|
|
10
|
+
showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const AddlNotesModal = ({ isOpen, onClose, notes, onSave, showNotification }: AddlNotesModalProps) => {
|
|
14
|
+
const [tempNotes, setTempNotes] = useState(notes);
|
|
15
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (isOpen) {
|
|
19
|
+
setTempNotes(notes);
|
|
20
|
+
}
|
|
21
|
+
}, [isOpen, notes]);
|
|
22
|
+
const {
|
|
23
|
+
requestClose,
|
|
24
|
+
overlayProps,
|
|
25
|
+
getCloseButtonProps
|
|
26
|
+
} = useOverlayDismiss({
|
|
27
|
+
isOpen,
|
|
28
|
+
onClose
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!isOpen) return null;
|
|
32
|
+
|
|
33
|
+
const handleSave = async () => {
|
|
34
|
+
setIsSaving(true);
|
|
35
|
+
try {
|
|
36
|
+
await Promise.resolve(onSave(tempNotes));
|
|
37
|
+
showNotification?.('Notes saved successfully.', 'success');
|
|
38
|
+
requestClose();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : 'Failed to save notes.';
|
|
41
|
+
showNotification?.(message, 'error');
|
|
42
|
+
} finally {
|
|
43
|
+
setIsSaving(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={styles.modalOverlay}
|
|
50
|
+
aria-label="Close notes dialog"
|
|
51
|
+
{...overlayProps}
|
|
52
|
+
>
|
|
53
|
+
<div className={styles.modal}>
|
|
54
|
+
<button {...getCloseButtonProps({ ariaLabel: 'Close notes dialog' })}>×</button>
|
|
55
|
+
<h5 className={styles.modalTitle}>Additional Notes</h5>
|
|
56
|
+
<textarea
|
|
57
|
+
value={tempNotes}
|
|
58
|
+
onChange={(e) => setTempNotes(e.target.value)}
|
|
59
|
+
className={styles.modalTextarea}
|
|
60
|
+
placeholder="Enter additional notes..."
|
|
61
|
+
/>
|
|
62
|
+
<div className={styles.modalButtons}>
|
|
63
|
+
<button
|
|
64
|
+
onClick={handleSave}
|
|
65
|
+
className={styles.saveButton}
|
|
66
|
+
disabled={isSaving}
|
|
67
|
+
aria-busy={isSaving}
|
|
68
|
+
>
|
|
69
|
+
{isSaving ? 'Saving...' : 'Save'}
|
|
70
|
+
</button>
|
|
71
|
+
<button
|
|
72
|
+
onClick={requestClose}
|
|
73
|
+
className={styles.cancelButton}
|
|
74
|
+
disabled={isSaving}
|
|
75
|
+
>
|
|
76
|
+
Cancel
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
};
|