@striae-org/striae 4.2.1 → 4.3.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/app/components/actions/case-import/confirmation-import.ts +20 -1
- package/app/components/actions/case-import/orchestrator.ts +3 -0
- package/app/components/actions/case-manage.ts +5 -1
- package/app/components/actions/confirm-export.ts +12 -3
- package/app/components/audit/viewer/audit-entries-list.tsx +20 -2
- package/app/components/audit/viewer/use-audit-viewer-export.ts +2 -2
- package/app/components/audit/viewer/use-audit-viewer-filters.ts +11 -1
- package/app/components/canvas/canvas.tsx +2 -1
- 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.module.css +11 -0
- package/app/components/navbar/navbar.tsx +38 -19
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +27 -3
- package/app/components/sidebar/cases/cases-modal.module.css +312 -10
- package/app/components/sidebar/cases/cases-modal.tsx +690 -110
- package/app/components/sidebar/cases/cases.module.css +23 -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 +452 -145
- 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-editor-form.tsx +43 -5
- package/app/components/sidebar/notes/notes.module.css +236 -4
- 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 +8 -1
- package/app/hooks/useCaseListPreferences.ts +99 -0
- package/app/hooks/useFileListPreferences.ts +106 -0
- package/app/routes/striae/striae.tsx +45 -1
- package/app/services/audit/audit-export-csv.ts +4 -2
- package/app/services/audit/audit-export-report.ts +36 -4
- package/app/services/audit/audit.service.ts +2 -0
- package/app/services/audit/builders/audit-entry-builder.ts +1 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -2
- 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 +18 -2
- package/app/utils/data/file-filters.ts +201 -0
- package/app/utils/forensics/confirmation-signature.ts +20 -5
- package/functions/api/image/[[path]].ts +4 -0
- package/package.json +3 -4
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/signing-payload-utils.ts +5 -0
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +84 -118
- package/workers/pdf-worker/src/pdf-worker.example.ts +28 -10
- package/workers/pdf-worker/src/report-layout.ts +227 -0
- package/workers/pdf-worker/src/report-types.ts +20 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- 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,6 +1,7 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
2
|
import { fetchDataApi } from '~/utils/api';
|
|
3
|
-
import {
|
|
3
|
+
import { upsertFileConfirmationSummary } from '~/utils/data';
|
|
4
|
+
import { type AnnotationData, type ConfirmationImportResult, type ConfirmationImportData } from '~/types';
|
|
4
5
|
import { checkExistingCase } from '../case-manage';
|
|
5
6
|
import { extractConfirmationImportPackage } from './confirmation-package';
|
|
6
7
|
import { validateExporterUid, validateConfirmationHash, validateConfirmationSignatureFile } from './validation';
|
|
@@ -37,6 +38,7 @@ export async function importConfirmationData(
|
|
|
37
38
|
let signatureKeyId: string | undefined;
|
|
38
39
|
let confirmationDataForAudit: ConfirmationImportData | null = null;
|
|
39
40
|
let confirmationJsonFileNameForAudit = confirmationFile.name;
|
|
41
|
+
const confirmedFileNames = new Set<string>();
|
|
40
42
|
|
|
41
43
|
const result: ConfirmationImportResult = {
|
|
42
44
|
success: false,
|
|
@@ -234,6 +236,21 @@ export async function importConfirmationData(
|
|
|
234
236
|
if (saveResponse.ok) {
|
|
235
237
|
result.imagesUpdated++;
|
|
236
238
|
result.confirmationsImported += confirmations.length;
|
|
239
|
+
confirmedFileNames.add(displayFilename);
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
await upsertFileConfirmationSummary(
|
|
243
|
+
user,
|
|
244
|
+
result.caseNumber,
|
|
245
|
+
currentImageId,
|
|
246
|
+
updatedAnnotationData as AnnotationData
|
|
247
|
+
);
|
|
248
|
+
} catch (summaryError) {
|
|
249
|
+
console.warn(
|
|
250
|
+
`Failed to update confirmation summary for imported confirmation ${result.caseNumber}/${currentImageId}:`,
|
|
251
|
+
summaryError
|
|
252
|
+
);
|
|
253
|
+
}
|
|
237
254
|
|
|
238
255
|
// Audit log successful confirmation import
|
|
239
256
|
try {
|
|
@@ -298,6 +315,7 @@ export async function importConfirmationData(
|
|
|
298
315
|
result.success ? (result.errors && result.errors.length > 0 ? 'warning' : 'success') : 'failure',
|
|
299
316
|
hashValid,
|
|
300
317
|
result.confirmationsImported, // Successfully imported confirmations
|
|
318
|
+
Array.from(confirmedFileNames).sort((left, right) => left.localeCompare(right)),
|
|
301
319
|
result.errors || [],
|
|
302
320
|
confirmationData.metadata.exportedByUid,
|
|
303
321
|
{
|
|
@@ -390,6 +408,7 @@ export async function importConfirmationData(
|
|
|
390
408
|
'failure',
|
|
391
409
|
hashValidForAudit,
|
|
392
410
|
0, // No confirmations successfully imported for failures
|
|
411
|
+
[],
|
|
393
412
|
result.errors || [],
|
|
394
413
|
reviewingExaminerUidForAudit,
|
|
395
414
|
{
|
|
@@ -286,6 +286,9 @@ export async function importCaseForReview(
|
|
|
286
286
|
if (existingRegularCase && !isArchivedExport) {
|
|
287
287
|
throw new Error(`Case "${result.caseNumber}" already exists in your case list. You cannot import a case for review if you were the original analyst.`);
|
|
288
288
|
}
|
|
289
|
+
if (existingRegularCase && isArchivedExport) {
|
|
290
|
+
throw new Error(`Cannot import this archive because case "${result.caseNumber}" already exists in your case list (active or archived). To import this archive, the existing case must first be deleted.`);
|
|
291
|
+
}
|
|
289
292
|
|
|
290
293
|
// Step 2b: Check if read-only case already exists
|
|
291
294
|
const existingCase = await checkReadOnlyCaseExists(user, result.caseNumber);
|
|
@@ -807,7 +807,11 @@ export const archiveCase = async (
|
|
|
807
807
|
)
|
|
808
808
|
);
|
|
809
809
|
|
|
810
|
-
const auditEntries = await auditService.getAuditEntriesForUser(user.uid, {
|
|
810
|
+
const auditEntries = await auditService.getAuditEntriesForUser(user.uid, {
|
|
811
|
+
caseNumber,
|
|
812
|
+
startDate: caseData.createdAt,
|
|
813
|
+
endDate: archivedAt,
|
|
814
|
+
});
|
|
811
815
|
const auditTrail: AuditTrail = {
|
|
812
816
|
caseNumber,
|
|
813
817
|
workflowId: `${caseNumber}-archive-${Date.now()}`,
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
getCurrentPublicSigningKeyDetails,
|
|
6
6
|
getVerificationPublicKey
|
|
7
7
|
} from '~/utils/forensics';
|
|
8
|
-
import { getUserData, getCaseData, updateCaseData, signConfirmationData } from '~/utils/data';
|
|
9
|
-
import { type ConfirmationData, type CaseConfirmations, type CaseDataWithConfirmations, type ConfirmationImportData } from '~/types';
|
|
8
|
+
import { getUserData, getCaseData, updateCaseData, signConfirmationData, upsertFileConfirmationSummary } from '~/utils/data';
|
|
9
|
+
import { type AnnotationData, type ConfirmationData, type CaseConfirmations, type CaseDataWithConfirmations, type ConfirmationImportData } from '~/types';
|
|
10
10
|
import { auditService } from '~/services/audit';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -17,7 +17,8 @@ export async function storeConfirmation(
|
|
|
17
17
|
caseNumber: string,
|
|
18
18
|
currentImageId: string,
|
|
19
19
|
confirmationData: ConfirmationData,
|
|
20
|
-
originalImageFileName?: string
|
|
20
|
+
originalImageFileName?: string,
|
|
21
|
+
annotationDataForSummary?: AnnotationData
|
|
21
22
|
): Promise<boolean> {
|
|
22
23
|
const startTime = Date.now();
|
|
23
24
|
let originalImageId: string | undefined; // Declare at function level for error handling
|
|
@@ -63,6 +64,14 @@ export async function storeConfirmation(
|
|
|
63
64
|
// Store the updated case data using centralized function
|
|
64
65
|
await updateCaseData(user, caseNumber, caseData);
|
|
65
66
|
|
|
67
|
+
if (annotationDataForSummary) {
|
|
68
|
+
try {
|
|
69
|
+
await upsertFileConfirmationSummary(user, caseNumber, currentImageId, annotationDataForSummary);
|
|
70
|
+
} catch (summaryError) {
|
|
71
|
+
console.warn(`Failed to update confirmation summary for ${caseNumber}/${currentImageId}:`, summaryError);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
console.log(`Confirmation stored for original image ${originalImageId}:`, confirmationData);
|
|
67
76
|
|
|
68
77
|
// Log successful confirmation creation
|
|
@@ -6,6 +6,13 @@ interface AuditEntriesListProps {
|
|
|
6
6
|
entries: ValidationAuditEntry[];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
const isConfirmationImportEntry = (entry: ValidationAuditEntry): boolean => {
|
|
10
|
+
return (
|
|
11
|
+
entry.action === 'confirmation-import' ||
|
|
12
|
+
(entry.action === 'import' && entry.details.workflowPhase === 'confirmation')
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
9
16
|
export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
|
|
10
17
|
return (
|
|
11
18
|
<div className={styles.entriesList}>
|
|
@@ -47,13 +54,24 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
|
|
|
47
54
|
</div>
|
|
48
55
|
)}
|
|
49
56
|
|
|
50
|
-
{entry
|
|
57
|
+
{isConfirmationImportEntry(entry) && entry.details.reviewerBadgeId && (
|
|
51
58
|
<div className={styles.detailRow}>
|
|
52
|
-
<span className={styles.detailLabel}>
|
|
59
|
+
<span className={styles.detailLabel}>Confirming Examiner Badge/ID:</span>
|
|
53
60
|
<span className={styles.badgeTag}>{entry.details.reviewerBadgeId}</span>
|
|
54
61
|
</div>
|
|
55
62
|
)}
|
|
56
63
|
|
|
64
|
+
{isConfirmationImportEntry(entry) &&
|
|
65
|
+
entry.details.caseDetails?.confirmedFileNames &&
|
|
66
|
+
entry.details.caseDetails.confirmedFileNames.length > 0 && (
|
|
67
|
+
<div className={styles.detailRow}>
|
|
68
|
+
<span className={styles.detailLabel}>Confirmed Files:</span>
|
|
69
|
+
<span className={styles.detailValue}>
|
|
70
|
+
{entry.details.caseDetails.confirmedFileNames.join(', ')}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
57
75
|
{entry.result === 'failure' && entry.details.validationErrors.length > 0 && (
|
|
58
76
|
<div className={styles.detailRow}>
|
|
59
77
|
<span className={styles.detailLabel}>Error:</span>
|
|
@@ -48,7 +48,7 @@ export const useAuditViewerExport = ({
|
|
|
48
48
|
const filename = auditExportService.generateFilename(
|
|
49
49
|
exportContextData.scopeType,
|
|
50
50
|
exportContextData.identifier,
|
|
51
|
-
'
|
|
51
|
+
'csv'
|
|
52
52
|
);
|
|
53
53
|
|
|
54
54
|
try {
|
|
@@ -72,7 +72,7 @@ export const useAuditViewerExport = ({
|
|
|
72
72
|
const filename = auditExportService.generateFilename(
|
|
73
73
|
exportContextData.scopeType,
|
|
74
74
|
exportContextData.identifier,
|
|
75
|
-
'
|
|
75
|
+
'json'
|
|
76
76
|
);
|
|
77
77
|
|
|
78
78
|
try {
|
|
@@ -2,6 +2,10 @@ import { useCallback, useMemo, useState } from 'react';
|
|
|
2
2
|
import type { AuditAction, AuditResult, ValidationAuditEntry } from '~/types';
|
|
3
3
|
import type { DateRangeFilter } from './types';
|
|
4
4
|
|
|
5
|
+
const isConfirmationImportEntry = (entry: ValidationAuditEntry): boolean => {
|
|
6
|
+
return entry.action === 'import' && entry.details.workflowPhase === 'confirmation';
|
|
7
|
+
};
|
|
8
|
+
|
|
5
9
|
export const useAuditViewerFilters = (caseNumber?: string) => {
|
|
6
10
|
const [filterAction, setFilterAction] = useState<AuditAction | 'all'>('all');
|
|
7
11
|
const [filterResult, setFilterResult] = useState<AuditResult | 'all'>('all');
|
|
@@ -76,7 +80,13 @@ export const useAuditViewerFilters = (caseNumber?: string) => {
|
|
|
76
80
|
|
|
77
81
|
const resultMatch = filterResult === 'all' || entry.result === filterResult;
|
|
78
82
|
const entryBadgeId = entry.details.userProfileDetails?.badgeId?.trim().toLowerCase() || '';
|
|
79
|
-
const
|
|
83
|
+
const reviewerBadgeId = isConfirmationImportEntry(entry)
|
|
84
|
+
? entry.details.reviewerBadgeId?.trim().toLowerCase() || ''
|
|
85
|
+
: '';
|
|
86
|
+
const badgeMatch =
|
|
87
|
+
normalizedBadgeFilter === '' ||
|
|
88
|
+
entryBadgeId.includes(normalizedBadgeFilter) ||
|
|
89
|
+
reviewerBadgeId.includes(normalizedBadgeFilter);
|
|
80
90
|
|
|
81
91
|
return actionMatch && resultMatch && badgeMatch;
|
|
82
92
|
});
|
|
@@ -1,52 +1,5 @@
|
|
|
1
|
-
.overlay {
|
|
2
|
-
position: fixed;
|
|
3
|
-
inset: 0;
|
|
4
|
-
background: rgba(0, 0, 0, 0.45);
|
|
5
|
-
display: flex;
|
|
6
|
-
align-items: center;
|
|
7
|
-
justify-content: center;
|
|
8
|
-
z-index: 120;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
1
|
.modal {
|
|
12
|
-
position: relative;
|
|
13
2
|
width: min(560px, calc(100vw - 2rem));
|
|
14
|
-
background: #ffffff;
|
|
15
|
-
border-radius: 12px;
|
|
16
|
-
border: 1px solid #d9e0e7;
|
|
17
|
-
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.2);
|
|
18
|
-
padding: 1.1rem;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.title {
|
|
22
|
-
margin: 0;
|
|
23
|
-
color: #212529;
|
|
24
|
-
font-size: 1.02rem;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.subtitle {
|
|
28
|
-
margin: 0.4rem 0 0.9rem;
|
|
29
|
-
color: #6c757d;
|
|
30
|
-
font-size: 0.85rem;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.warningPanel {
|
|
34
|
-
border: 1px solid color-mix(in lab, #dc3545 25%, transparent);
|
|
35
|
-
background: color-mix(in lab, #dc3545 7%, #ffffff);
|
|
36
|
-
border-radius: 10px;
|
|
37
|
-
padding: 0.75rem;
|
|
38
|
-
margin-bottom: 0.8rem;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.warningPanel p {
|
|
42
|
-
margin: 0;
|
|
43
|
-
color: #3f2a2e;
|
|
44
|
-
font-size: 0.86rem;
|
|
45
|
-
line-height: 1.35;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.warningPanel p + p {
|
|
49
|
-
margin-top: 0.45rem;
|
|
50
3
|
}
|
|
51
4
|
|
|
52
5
|
.reasonLabel {
|
|
@@ -74,37 +27,8 @@
|
|
|
74
27
|
box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2);
|
|
75
28
|
}
|
|
76
29
|
|
|
77
|
-
.actions {
|
|
78
|
-
display: flex;
|
|
79
|
-
justify-content: flex-end;
|
|
80
|
-
gap: 0.65rem;
|
|
81
|
-
margin-top: 1rem;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.cancelButton,
|
|
85
|
-
.confirmButton {
|
|
86
|
-
border: 1px solid transparent;
|
|
87
|
-
border-radius: 8px;
|
|
88
|
-
padding: 0.55rem 0.9rem;
|
|
89
|
-
font-size: 0.86rem;
|
|
90
|
-
font-weight: 500;
|
|
91
|
-
cursor: pointer;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
.cancelButton {
|
|
95
|
-
background: #f3f4f6;
|
|
96
|
-
color: #3c4651;
|
|
97
|
-
border-color: #d6dce2;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
30
|
.confirmButton {
|
|
101
31
|
background: #dc3545;
|
|
102
32
|
color: #ffffff;
|
|
103
33
|
border-color: #c82333;
|
|
104
34
|
}
|
|
105
|
-
|
|
106
|
-
.cancelButton:disabled,
|
|
107
|
-
.confirmButton:disabled {
|
|
108
|
-
cursor: not-allowed;
|
|
109
|
-
opacity: 0.6;
|
|
110
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
3
|
+
import sharedStyles from './case-modal-shared.module.css';
|
|
3
4
|
import styles from './archive-case-modal.module.css';
|
|
4
5
|
|
|
5
6
|
interface ArchiveCaseModalProps {
|
|
@@ -65,18 +66,18 @@ export const ArchiveCaseModal = ({
|
|
|
65
66
|
|
|
66
67
|
return (
|
|
67
68
|
<div
|
|
68
|
-
className={
|
|
69
|
+
className={sharedStyles.overlay}
|
|
69
70
|
aria-label="Close archive case dialog"
|
|
70
71
|
{...overlayProps}
|
|
71
72
|
>
|
|
72
|
-
<div className={styles.modal} role="dialog" aria-modal="true" aria-label="Archive Case">
|
|
73
|
+
<div className={`${sharedStyles.modal} ${styles.modal}`} role="dialog" aria-modal="true" aria-label="Archive Case">
|
|
73
74
|
<button {...getCloseButtonProps({ ariaLabel: 'Close archive case dialog' })}>
|
|
74
75
|
×
|
|
75
76
|
</button>
|
|
76
|
-
<h3 className={
|
|
77
|
-
<p className={
|
|
77
|
+
<h3 className={sharedStyles.title}>Archive Case</h3>
|
|
78
|
+
<p className={sharedStyles.subtitle}>Case: {currentCase}</p>
|
|
78
79
|
|
|
79
|
-
<div className={
|
|
80
|
+
<div className={sharedStyles.warningPanel}>
|
|
80
81
|
<p>
|
|
81
82
|
Archiving a case permanently renders it read-only.
|
|
82
83
|
</p>
|
|
@@ -103,10 +104,10 @@ export const ArchiveCaseModal = ({
|
|
|
103
104
|
rows={3}
|
|
104
105
|
/>
|
|
105
106
|
|
|
106
|
-
<div className={
|
|
107
|
+
<div className={sharedStyles.actions}>
|
|
107
108
|
<button
|
|
108
109
|
type="button"
|
|
109
|
-
className={
|
|
110
|
+
className={sharedStyles.cancelButton}
|
|
110
111
|
onClick={requestClose}
|
|
111
112
|
disabled={isCloseBlocked}
|
|
112
113
|
>
|
|
@@ -114,7 +115,7 @@ export const ArchiveCaseModal = ({
|
|
|
114
115
|
</button>
|
|
115
116
|
<button
|
|
116
117
|
type="button"
|
|
117
|
-
className={styles.confirmButton}
|
|
118
|
+
className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
|
|
118
119
|
onClick={() => {
|
|
119
120
|
void handleSubmit();
|
|
120
121
|
}}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
.overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
background: rgba(0, 0, 0, 0.45);
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
z-index: 120;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.modal {
|
|
12
|
+
position: relative;
|
|
13
|
+
background: #ffffff;
|
|
14
|
+
border-radius: var(--spaceXS);
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
border: 1px solid #d9e0e7;
|
|
17
|
+
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.2);
|
|
18
|
+
padding: 1.1rem;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.title {
|
|
22
|
+
margin: 0;
|
|
23
|
+
color: #212529;
|
|
24
|
+
font-size: 1.02rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.subtitle {
|
|
28
|
+
margin: 0.4rem 0 0.9rem;
|
|
29
|
+
color: #6c757d;
|
|
30
|
+
font-size: 0.85rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.input {
|
|
34
|
+
width: 100%;
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
border: 1px solid #cdd5dd;
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
padding: 0.6rem 0.75rem;
|
|
39
|
+
font-size: 0.92rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.input:focus {
|
|
43
|
+
outline: none;
|
|
44
|
+
border-color: #1f6feb;
|
|
45
|
+
box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.warningPanel {
|
|
49
|
+
border: 1px solid color-mix(in lab, #dc3545 25%, transparent);
|
|
50
|
+
background: color-mix(in lab, #dc3545 7%, #ffffff);
|
|
51
|
+
border-radius: 10px;
|
|
52
|
+
padding: 0.75rem;
|
|
53
|
+
margin-bottom: 0.8rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.warningPanel p {
|
|
57
|
+
margin: 0;
|
|
58
|
+
color: #3f2a2e;
|
|
59
|
+
font-size: 0.86rem;
|
|
60
|
+
line-height: 1.35;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.warningPanel p + p {
|
|
64
|
+
margin-top: 0.45rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.actions {
|
|
68
|
+
display: flex;
|
|
69
|
+
justify-content: flex-end;
|
|
70
|
+
gap: 0.65rem;
|
|
71
|
+
margin-top: 1rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.cancelButton,
|
|
75
|
+
.confirmButton {
|
|
76
|
+
border: 1px solid transparent;
|
|
77
|
+
border-radius: 8px;
|
|
78
|
+
padding: 0.55rem 0.9rem;
|
|
79
|
+
font-size: 0.86rem;
|
|
80
|
+
font-weight: 500;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.cancelButton {
|
|
85
|
+
background: #f3f4f6;
|
|
86
|
+
color: #3c4651;
|
|
87
|
+
border-color: #d6dce2;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.cancelButton:disabled,
|
|
91
|
+
.confirmButton:disabled {
|
|
92
|
+
cursor: not-allowed;
|
|
93
|
+
opacity: 0.6;
|
|
94
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
2
|
+
import sharedStyles from './case-modal-shared.module.css';
|
|
3
|
+
import styles from './delete-case-modal.module.css';
|
|
4
|
+
|
|
5
|
+
interface DeleteCaseModalProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
currentCase: string;
|
|
8
|
+
isSubmitting?: boolean;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
onSubmit: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DeleteCaseModal = ({
|
|
14
|
+
isOpen,
|
|
15
|
+
currentCase,
|
|
16
|
+
isSubmitting = false,
|
|
17
|
+
onClose,
|
|
18
|
+
onSubmit,
|
|
19
|
+
}: DeleteCaseModalProps) => {
|
|
20
|
+
const isCloseBlocked = isSubmitting;
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
requestClose,
|
|
24
|
+
overlayProps,
|
|
25
|
+
getCloseButtonProps,
|
|
26
|
+
} = useOverlayDismiss({
|
|
27
|
+
isOpen,
|
|
28
|
+
onClose,
|
|
29
|
+
canDismiss: !isCloseBlocked,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!isOpen) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={sharedStyles.overlay}
|
|
39
|
+
aria-label="Close delete case dialog"
|
|
40
|
+
{...overlayProps}
|
|
41
|
+
>
|
|
42
|
+
<div className={`${sharedStyles.modal} ${styles.modal}`} role="dialog" aria-modal="true" aria-label="Delete Case">
|
|
43
|
+
<button {...getCloseButtonProps({ ariaLabel: 'Close delete case dialog' })}>
|
|
44
|
+
×
|
|
45
|
+
</button>
|
|
46
|
+
|
|
47
|
+
<h3 className={sharedStyles.title}>Delete Case</h3>
|
|
48
|
+
<p className={sharedStyles.subtitle}>Case: {currentCase}</p>
|
|
49
|
+
|
|
50
|
+
<div className={sharedStyles.warningPanel}>
|
|
51
|
+
<p>This action permanently deletes the case and all associated files.</p>
|
|
52
|
+
<p>This operation cannot be undone.</p>
|
|
53
|
+
<p>Any image assets that are already missing will be skipped automatically.</p>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className={sharedStyles.actions}>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
className={sharedStyles.cancelButton}
|
|
60
|
+
onClick={requestClose}
|
|
61
|
+
disabled={isCloseBlocked}
|
|
62
|
+
>
|
|
63
|
+
Cancel
|
|
64
|
+
</button>
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
|
|
68
|
+
onClick={() => {
|
|
69
|
+
void onSubmit();
|
|
70
|
+
}}
|
|
71
|
+
disabled={isSubmitting}
|
|
72
|
+
>
|
|
73
|
+
{isSubmitting ? 'Deleting...' : 'Confirm Delete'}
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
position: relative;
|
|
13
13
|
width: min(460px, calc(100vw - 2rem));
|
|
14
14
|
background: #ffffff;
|
|
15
|
-
border-radius:
|
|
15
|
+
border-radius: var(--spaceXS);
|
|
16
|
+
overflow: hidden;
|
|
16
17
|
border: 1px solid #d9e0e7;
|
|
17
18
|
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.2);
|
|
18
19
|
padding: 1.1rem;
|
|
@@ -1,71 +1,5 @@
|
|
|
1
|
-
.overlay {
|
|
2
|
-
position: fixed;
|
|
3
|
-
inset: 0;
|
|
4
|
-
background: rgba(0, 0, 0, 0.45);
|
|
5
|
-
display: flex;
|
|
6
|
-
align-items: center;
|
|
7
|
-
justify-content: center;
|
|
8
|
-
z-index: 120;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
1
|
.modal {
|
|
12
|
-
position: relative;
|
|
13
2
|
width: min(460px, calc(100vw - 2rem));
|
|
14
|
-
background: #ffffff;
|
|
15
|
-
border-radius: 12px;
|
|
16
|
-
border: 1px solid #d9e0e7;
|
|
17
|
-
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.2);
|
|
18
|
-
padding: 1.1rem;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.title {
|
|
22
|
-
margin: 0;
|
|
23
|
-
color: #212529;
|
|
24
|
-
font-size: 1.02rem;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.subtitle {
|
|
28
|
-
margin: 0.4rem 0 0.9rem;
|
|
29
|
-
color: #6c757d;
|
|
30
|
-
font-size: 0.85rem;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.input {
|
|
34
|
-
width: 100%;
|
|
35
|
-
box-sizing: border-box;
|
|
36
|
-
border: 1px solid #cdd5dd;
|
|
37
|
-
border-radius: 8px;
|
|
38
|
-
padding: 0.6rem 0.75rem;
|
|
39
|
-
font-size: 0.92rem;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.input:focus {
|
|
43
|
-
outline: none;
|
|
44
|
-
border-color: #1f6feb;
|
|
45
|
-
box-shadow: 0 0 0 2px rgba(31, 111, 235, 0.2);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.actions {
|
|
49
|
-
display: flex;
|
|
50
|
-
justify-content: flex-end;
|
|
51
|
-
gap: 0.65rem;
|
|
52
|
-
margin-top: 1rem;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.cancelButton,
|
|
56
|
-
.confirmButton {
|
|
57
|
-
border: 1px solid transparent;
|
|
58
|
-
border-radius: 8px;
|
|
59
|
-
padding: 0.55rem 0.9rem;
|
|
60
|
-
font-size: 0.86rem;
|
|
61
|
-
font-weight: 500;
|
|
62
|
-
cursor: pointer;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.cancelButton {
|
|
66
|
-
background: #f3f4f6;
|
|
67
|
-
color: #3c4651;
|
|
68
|
-
border-color: #d6dce2;
|
|
69
3
|
}
|
|
70
4
|
|
|
71
5
|
.confirmButton {
|
|
@@ -73,9 +7,3 @@
|
|
|
73
7
|
color: #3f2f00;
|
|
74
8
|
border-color: #e8b103;
|
|
75
9
|
}
|
|
76
|
-
|
|
77
|
-
.cancelButton:disabled,
|
|
78
|
-
.confirmButton:disabled {
|
|
79
|
-
cursor: not-allowed;
|
|
80
|
-
opacity: 0.6;
|
|
81
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
3
|
+
import sharedStyles from './case-modal-shared.module.css';
|
|
3
4
|
import styles from './rename-case-modal.module.css';
|
|
4
5
|
|
|
5
6
|
interface RenameCaseModalProps {
|
|
@@ -59,22 +60,22 @@ export const RenameCaseModal = ({
|
|
|
59
60
|
|
|
60
61
|
return (
|
|
61
62
|
<div
|
|
62
|
-
className={
|
|
63
|
+
className={sharedStyles.overlay}
|
|
63
64
|
aria-label="Close rename case dialog"
|
|
64
65
|
{...overlayProps}
|
|
65
66
|
>
|
|
66
|
-
<div className={styles.modal} role="dialog" aria-modal="true" aria-label="Rename Case">
|
|
67
|
+
<div className={`${sharedStyles.modal} ${styles.modal}`} role="dialog" aria-modal="true" aria-label="Rename Case">
|
|
67
68
|
<button {...getCloseButtonProps({ ariaLabel: 'Close rename case dialog' })}>
|
|
68
69
|
×
|
|
69
70
|
</button>
|
|
70
|
-
<h3 className={
|
|
71
|
-
<p className={
|
|
71
|
+
<h3 className={sharedStyles.title}>Rename Case</h3>
|
|
72
|
+
<p className={sharedStyles.subtitle}>Current case: {currentCase}</p>
|
|
72
73
|
<input
|
|
73
74
|
ref={inputRef}
|
|
74
75
|
type="text"
|
|
75
76
|
value={newCaseName}
|
|
76
77
|
onChange={(event) => setNewCaseName(event.target.value)}
|
|
77
|
-
className={
|
|
78
|
+
className={sharedStyles.input}
|
|
78
79
|
placeholder="New case number"
|
|
79
80
|
disabled={isSubmitting}
|
|
80
81
|
onKeyDown={(event) => {
|
|
@@ -83,10 +84,10 @@ export const RenameCaseModal = ({
|
|
|
83
84
|
}
|
|
84
85
|
}}
|
|
85
86
|
/>
|
|
86
|
-
<div className={
|
|
87
|
+
<div className={sharedStyles.actions}>
|
|
87
88
|
<button
|
|
88
89
|
type="button"
|
|
89
|
-
className={
|
|
90
|
+
className={sharedStyles.cancelButton}
|
|
90
91
|
onClick={requestClose}
|
|
91
92
|
disabled={isCloseBlocked}
|
|
92
93
|
>
|
|
@@ -94,7 +95,7 @@ export const RenameCaseModal = ({
|
|
|
94
95
|
</button>
|
|
95
96
|
<button
|
|
96
97
|
type="button"
|
|
97
|
-
className={styles.confirmButton}
|
|
98
|
+
className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
|
|
98
99
|
onClick={() => void handleSubmit()}
|
|
99
100
|
disabled={isSubmitting || !newCaseName.trim()}
|
|
100
101
|
>
|