@striae-org/striae 5.2.1 → 5.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/.env.example +2 -10
- package/README.md +5 -46
- package/app/components/actions/case-export/core-export.ts +5 -174
- package/app/components/actions/case-export/download-handlers.ts +84 -751
- package/app/components/actions/case-export/index.ts +6 -30
- package/app/components/actions/case-export/metadata-helpers.ts +0 -78
- package/app/components/actions/case-export/types-constants.ts +0 -43
- package/app/components/actions/case-import/confirmation-import.ts +75 -36
- package/app/components/actions/case-import/confirmation-package.ts +68 -1
- package/app/components/actions/case-import/index.ts +1 -1
- package/app/components/actions/case-import/orchestrator.ts +78 -53
- package/app/components/actions/case-import/zip-processing.ts +160 -330
- package/app/components/actions/generate-pdf.ts +3 -2
- package/app/components/audit/user-audit-viewer.tsx +0 -19
- package/app/components/audit/viewer/audit-viewer-header.tsx +0 -33
- package/app/components/navbar/case-modals/archive-case-modal.tsx +1 -1
- package/app/components/navbar/case-modals/export-case-modal.module.css +27 -0
- package/app/components/navbar/case-modals/export-case-modal.tsx +132 -0
- package/app/components/navbar/case-modals/export-confirmations-modal.module.css +24 -0
- package/app/components/navbar/case-modals/export-confirmations-modal.tsx +108 -0
- package/app/components/navbar/navbar.tsx +1 -1
- package/app/components/sidebar/case-import/case-import.module.css +35 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +51 -3
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +2 -4
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +36 -5
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +5 -9
- package/app/components/sidebar/case-import/index.ts +1 -4
- package/app/components/sidebar/notes/class-details-shared.ts +2 -2
- package/app/components/toast/toast.module.css +36 -0
- package/app/components/toast/toast.tsx +6 -2
- package/app/components/user/manage-profile.tsx +4 -3
- package/app/config-example/config.json +1 -2
- package/app/root.tsx +0 -7
- package/app/routes/_index.tsx +1 -1
- package/app/routes/auth/login.example.tsx +22 -103
- package/app/routes/auth/login.tsx +22 -103
- package/app/routes/auth/route.ts +1 -1
- package/app/routes/striae/striae.tsx +117 -59
- package/app/services/firebase/index.ts +0 -3
- package/app/types/case.ts +1 -0
- package/app/types/export.ts +2 -2
- package/app/types/import.ts +10 -0
- package/app/utils/auth/index.ts +0 -1
- package/app/utils/data/permissions.ts +3 -2
- package/package.json +9 -16
- package/public/_headers +0 -4
- package/public/_routes.json +0 -1
- package/worker-configuration.d.ts +20 -17
- package/workers/audit-worker/src/audit-worker.example.ts +9 -806
- package/workers/audit-worker/src/config.ts +7 -0
- package/workers/audit-worker/src/crypto/data-at-rest.ts +410 -0
- package/workers/audit-worker/src/handlers/audit-routes.ts +125 -0
- package/workers/audit-worker/src/storage/audit-storage.ts +99 -0
- package/workers/audit-worker/src/types.ts +56 -0
- package/workers/audit-worker/worker-configuration.d.ts +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/config.ts +11 -0
- package/workers/data-worker/src/data-worker.example.ts +21 -942
- package/workers/data-worker/src/handlers/decrypt-export.ts +118 -0
- package/workers/data-worker/src/handlers/signing.ts +174 -0
- package/workers/data-worker/src/handlers/storage-routes.ts +129 -0
- package/workers/data-worker/src/registry/key-registry.ts +368 -0
- package/workers/data-worker/src/types.ts +46 -0
- package/workers/data-worker/worker-configuration.d.ts +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/worker-configuration.d.ts +2 -3
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/auth.ts +30 -0
- package/workers/user-worker/src/cleanup/account-deletion.ts +337 -0
- package/workers/user-worker/src/config.ts +4 -0
- package/workers/user-worker/src/encryption-utils.ts +25 -0
- package/workers/user-worker/src/firebase/admin.ts +152 -0
- package/workers/user-worker/src/handlers/user-routes.ts +242 -0
- package/workers/user-worker/src/registry/user-kv.ts +172 -0
- package/workers/user-worker/src/storage/user-records.ts +34 -0
- package/workers/user-worker/src/types.ts +106 -0
- package/workers/user-worker/src/user-worker.example.ts +18 -964
- package/workers/user-worker/worker-configuration.d.ts +4 -2
- package/workers/user-worker/wrangler.jsonc.example +12 -1
- package/wrangler.toml.example +1 -1
- package/app/components/actions/case-export/data-processing.ts +0 -223
- package/app/components/sidebar/case-export/case-export.module.css +0 -418
- package/app/components/sidebar/case-export/case-export.tsx +0 -310
- package/app/types/exceljs-bare.d.ts +0 -9
- package/app/utils/auth/auth.ts +0 -11
- package/public/.well-known/security.txt +0 -6
- package/public/favicon.ico +0 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +0 -39
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/vendor/exceljs.LICENSE +0 -22
- package/public/vendor/exceljs.bare.min.js +0 -45
- package/scripts/deploy-all.sh +0 -166
- package/scripts/deploy-config/modules/env-utils.sh +0 -322
- package/scripts/deploy-config/modules/keys.sh +0 -404
- package/scripts/deploy-config/modules/prompt.sh +0 -372
- package/scripts/deploy-config/modules/scaffolding.sh +0 -344
- package/scripts/deploy-config/modules/validation.sh +0 -365
- package/scripts/deploy-config.sh +0 -236
- package/scripts/deploy-pages-secrets.sh +0 -231
- package/scripts/deploy-pages.sh +0 -34
- package/scripts/deploy-primershear-emails.sh +0 -167
- package/scripts/deploy-worker-secrets.sh +0 -374
- package/scripts/dev.cjs +0 -23
- package/scripts/install-workers.sh +0 -88
- package/scripts/run-eslint.cjs +0 -43
- package/scripts/update-compatibility-dates.cjs +0 -124
- package/scripts/update-markdown-versions.cjs +0 -43
- package/workers/keys-worker/package.json +0 -18
- package/workers/keys-worker/src/keys.example.ts +0 -67
- package/workers/keys-worker/src/keys.ts +0 -67
- package/workers/keys-worker/worker-configuration.d.ts +0 -7447
- package/workers/keys-worker/wrangler.jsonc.example +0 -15
|
@@ -2,50 +2,17 @@ import styles from '../user-audit.module.css';
|
|
|
2
2
|
|
|
3
3
|
interface AuditViewerHeaderProps {
|
|
4
4
|
title: string;
|
|
5
|
-
hasEntries: boolean;
|
|
6
|
-
onExportCSV: () => void;
|
|
7
|
-
onExportJSON: () => void;
|
|
8
|
-
onGenerateReport: () => void;
|
|
9
5
|
onClose: () => void;
|
|
10
6
|
}
|
|
11
7
|
|
|
12
8
|
export const AuditViewerHeader = ({
|
|
13
9
|
title,
|
|
14
|
-
hasEntries,
|
|
15
|
-
onExportCSV,
|
|
16
|
-
onExportJSON,
|
|
17
|
-
onGenerateReport,
|
|
18
10
|
onClose,
|
|
19
11
|
}: AuditViewerHeaderProps) => {
|
|
20
12
|
return (
|
|
21
13
|
<div className={styles.header}>
|
|
22
14
|
<h2 className={styles.title}>{title}</h2>
|
|
23
15
|
<div className={styles.headerActions}>
|
|
24
|
-
{hasEntries && (
|
|
25
|
-
<div className={styles.exportButtons}>
|
|
26
|
-
<button
|
|
27
|
-
onClick={onExportCSV}
|
|
28
|
-
className={styles.exportButton}
|
|
29
|
-
title="CSV - Individual entry log with summary data"
|
|
30
|
-
>
|
|
31
|
-
📊 CSV
|
|
32
|
-
</button>
|
|
33
|
-
<button
|
|
34
|
-
onClick={onExportJSON}
|
|
35
|
-
className={styles.exportButton}
|
|
36
|
-
title="JSON - Complete log data for version capture and auditing"
|
|
37
|
-
>
|
|
38
|
-
📄 JSON
|
|
39
|
-
</button>
|
|
40
|
-
<button
|
|
41
|
-
onClick={onGenerateReport}
|
|
42
|
-
className={styles.exportButton}
|
|
43
|
-
title="Summary report only"
|
|
44
|
-
>
|
|
45
|
-
📋 Report
|
|
46
|
-
</button>
|
|
47
|
-
</div>
|
|
48
|
-
)}
|
|
49
16
|
<button className={styles.closeButton} onClick={onClose}>
|
|
50
17
|
×
|
|
51
18
|
</button>
|
|
@@ -82,7 +82,7 @@ export const ArchiveCaseModal = ({
|
|
|
82
82
|
Archiving a case permanently renders it read-only.
|
|
83
83
|
</p>
|
|
84
84
|
<p>
|
|
85
|
-
The archive will be
|
|
85
|
+
The archive will be packaged as an encrypted case package and will always include all images.
|
|
86
86
|
</p>
|
|
87
87
|
<p>
|
|
88
88
|
The full audit trail is packaged with Striae's current public key and forensic signatures.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
.modal {
|
|
2
|
+
width: min(480px, calc(100vw - 2rem));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.description {
|
|
6
|
+
margin: 0 0 0.9rem;
|
|
7
|
+
color: #4b5563;
|
|
8
|
+
font-size: 0.86rem;
|
|
9
|
+
line-height: 1.5;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.confirmButton {
|
|
13
|
+
background: #1f6feb;
|
|
14
|
+
color: #ffffff;
|
|
15
|
+
border-color: #1560d4;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.confirmButton:not(:disabled):hover {
|
|
19
|
+
background: #1560d4;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.emailError {
|
|
23
|
+
margin: 0.45rem 0 0;
|
|
24
|
+
color: #b91c1c;
|
|
25
|
+
font-size: 0.83rem;
|
|
26
|
+
line-height: 1.4;
|
|
27
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
3
|
+
import sharedStyles from './case-modal-shared.module.css';
|
|
4
|
+
import styles from './export-case-modal.module.css';
|
|
5
|
+
|
|
6
|
+
interface ExportCaseModalProps {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
caseNumber: string;
|
|
9
|
+
currentUserEmail?: string;
|
|
10
|
+
isSubmitting?: boolean;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
onSubmit: (designatedReviewerEmail: string | undefined) => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ExportCaseModal = ({
|
|
16
|
+
isOpen,
|
|
17
|
+
caseNumber,
|
|
18
|
+
currentUserEmail,
|
|
19
|
+
isSubmitting = false,
|
|
20
|
+
onClose,
|
|
21
|
+
onSubmit,
|
|
22
|
+
}: ExportCaseModalProps) => {
|
|
23
|
+
const [email, setEmail] = useState<string>('');
|
|
24
|
+
|
|
25
|
+
const isSelfEmail =
|
|
26
|
+
email.trim().length > 0 &&
|
|
27
|
+
!!currentUserEmail &&
|
|
28
|
+
email.trim().toLowerCase() === currentUserEmail.toLowerCase();
|
|
29
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
30
|
+
|
|
31
|
+
const handleClose = () => {
|
|
32
|
+
setEmail('');
|
|
33
|
+
onClose();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const isSubmitDisabled = isSubmitting || isSelfEmail;
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
requestClose,
|
|
40
|
+
overlayProps,
|
|
41
|
+
getCloseButtonProps,
|
|
42
|
+
} = useOverlayDismiss({
|
|
43
|
+
isOpen,
|
|
44
|
+
onClose: handleClose,
|
|
45
|
+
canDismiss: !isSubmitting,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!isOpen) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const focusId = window.requestAnimationFrame(() => {
|
|
54
|
+
inputRef.current?.focus();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
window.cancelAnimationFrame(focusId);
|
|
59
|
+
};
|
|
60
|
+
}, [isOpen]);
|
|
61
|
+
|
|
62
|
+
if (!isOpen) return null;
|
|
63
|
+
|
|
64
|
+
const handleSubmit = async () => {
|
|
65
|
+
const trimmed = email.trim() || undefined;
|
|
66
|
+
await onSubmit(trimmed);
|
|
67
|
+
setEmail('');
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
className={sharedStyles.overlay}
|
|
73
|
+
aria-label="Close export case dialog"
|
|
74
|
+
{...overlayProps}
|
|
75
|
+
>
|
|
76
|
+
<div
|
|
77
|
+
className={`${sharedStyles.modal} ${styles.modal}`}
|
|
78
|
+
role="dialog"
|
|
79
|
+
aria-modal="true"
|
|
80
|
+
aria-label="Export Case"
|
|
81
|
+
>
|
|
82
|
+
<button {...getCloseButtonProps({ ariaLabel: 'Close export case dialog' })}>
|
|
83
|
+
×
|
|
84
|
+
</button>
|
|
85
|
+
<h3 className={sharedStyles.title}>Export Case</h3>
|
|
86
|
+
<p className={sharedStyles.subtitle}>Case: {caseNumber}</p>
|
|
87
|
+
<p className={styles.description}>
|
|
88
|
+
You may designate a specific email address for review approval. Only the user
|
|
89
|
+
with the supplied email address will be able to open your case for review in
|
|
90
|
+
Striae. (Optional)
|
|
91
|
+
</p>
|
|
92
|
+
<input
|
|
93
|
+
ref={inputRef}
|
|
94
|
+
type="email"
|
|
95
|
+
value={email}
|
|
96
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
97
|
+
className={sharedStyles.input}
|
|
98
|
+
placeholder="Reviewer email address (optional)"
|
|
99
|
+
disabled={isSubmitting}
|
|
100
|
+
onKeyDown={(event) => {
|
|
101
|
+
if (event.key === 'Enter' && !isSubmitDisabled) {
|
|
102
|
+
void handleSubmit();
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
{isSelfEmail && (
|
|
107
|
+
<p className={styles.emailError}>
|
|
108
|
+
You cannot designate yourself as the reviewer. The recipient must be a different Striae user.
|
|
109
|
+
</p>
|
|
110
|
+
)}
|
|
111
|
+
<div className={sharedStyles.actions}>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
className={sharedStyles.cancelButton}
|
|
115
|
+
onClick={requestClose}
|
|
116
|
+
disabled={isSubmitting}
|
|
117
|
+
>
|
|
118
|
+
Cancel
|
|
119
|
+
</button>
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
|
|
123
|
+
onClick={() => void handleSubmit()}
|
|
124
|
+
disabled={isSubmitDisabled}
|
|
125
|
+
>
|
|
126
|
+
{isSubmitting ? 'Exporting...' : 'Export Case'}
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.modal {
|
|
2
|
+
width: min(400px, calc(100vw - 2rem));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.warningPanel {
|
|
6
|
+
margin-bottom: 0.8rem;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.description {
|
|
10
|
+
margin: 0 0 0.3rem;
|
|
11
|
+
color: #4b5563;
|
|
12
|
+
font-size: 0.86rem;
|
|
13
|
+
line-height: 1.5;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.confirmButton {
|
|
17
|
+
background: #1f6feb;
|
|
18
|
+
color: #ffffff;
|
|
19
|
+
border-color: #1560d4;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.confirmButton:not(:disabled):hover {
|
|
23
|
+
background: #1560d4;
|
|
24
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
3
|
+
import sharedStyles from './case-modal-shared.module.css';
|
|
4
|
+
import styles from './export-confirmations-modal.module.css';
|
|
5
|
+
|
|
6
|
+
interface ExportConfirmationsModalProps {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
caseNumber: string;
|
|
9
|
+
confirmedCount: number;
|
|
10
|
+
unconfirmedCount: number;
|
|
11
|
+
isSubmitting?: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
onConfirm: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ExportConfirmationsModal = ({
|
|
17
|
+
isOpen,
|
|
18
|
+
caseNumber,
|
|
19
|
+
confirmedCount,
|
|
20
|
+
unconfirmedCount,
|
|
21
|
+
isSubmitting = false,
|
|
22
|
+
onClose,
|
|
23
|
+
onConfirm,
|
|
24
|
+
}: ExportConfirmationsModalProps) => {
|
|
25
|
+
const confirmButtonRef = useRef<HTMLButtonElement>(null);
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
requestClose,
|
|
29
|
+
overlayProps,
|
|
30
|
+
getCloseButtonProps,
|
|
31
|
+
} = useOverlayDismiss({
|
|
32
|
+
isOpen,
|
|
33
|
+
onClose,
|
|
34
|
+
canDismiss: !isSubmitting,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!isOpen) return;
|
|
39
|
+
|
|
40
|
+
const focusId = window.requestAnimationFrame(() => {
|
|
41
|
+
confirmButtonRef.current?.focus();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
window.cancelAnimationFrame(focusId);
|
|
46
|
+
};
|
|
47
|
+
}, [isOpen]);
|
|
48
|
+
|
|
49
|
+
if (!isOpen) return null;
|
|
50
|
+
|
|
51
|
+
const confirmationLabel = confirmedCount === 1 ? '1 confirmation' : `${confirmedCount} confirmations`;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
className={sharedStyles.overlay}
|
|
56
|
+
aria-label="Close export confirmations dialog"
|
|
57
|
+
{...overlayProps}
|
|
58
|
+
>
|
|
59
|
+
<div
|
|
60
|
+
className={`${sharedStyles.modal} ${styles.modal}`}
|
|
61
|
+
role="dialog"
|
|
62
|
+
aria-modal="true"
|
|
63
|
+
aria-label="Export Confirmations"
|
|
64
|
+
>
|
|
65
|
+
<button {...getCloseButtonProps({ ariaLabel: 'Close export confirmations dialog' })}>
|
|
66
|
+
×
|
|
67
|
+
</button>
|
|
68
|
+
<h3 className={sharedStyles.title}>Export Confirmations</h3>
|
|
69
|
+
<p className={sharedStyles.subtitle}>Case: {caseNumber}</p>
|
|
70
|
+
{unconfirmedCount > 0 && (
|
|
71
|
+
<div className={`${sharedStyles.warningPanel} ${styles.warningPanel}`}>
|
|
72
|
+
<p>
|
|
73
|
+
<strong>
|
|
74
|
+
{unconfirmedCount} image{unconfirmedCount !== 1 ? 's' : ''}{' '}
|
|
75
|
+
{unconfirmedCount !== 1 ? 'are' : 'is'} unconfirmed.
|
|
76
|
+
</strong>
|
|
77
|
+
</p>
|
|
78
|
+
<p>Only confirmed images will be included in this export.</p>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
<p className={styles.description}>
|
|
82
|
+
{confirmedCount === 0
|
|
83
|
+
? 'No confirmed images found for this case.'
|
|
84
|
+
: `${confirmationLabel} will be exported.`}
|
|
85
|
+
</p>
|
|
86
|
+
<div className={sharedStyles.actions}>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className={sharedStyles.cancelButton}
|
|
90
|
+
onClick={requestClose}
|
|
91
|
+
disabled={isSubmitting}
|
|
92
|
+
>
|
|
93
|
+
Cancel
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
ref={confirmButtonRef}
|
|
97
|
+
type="button"
|
|
98
|
+
className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
|
|
99
|
+
onClick={onConfirm}
|
|
100
|
+
disabled={isSubmitting || confirmedCount === 0}
|
|
101
|
+
>
|
|
102
|
+
{isSubmitting ? 'Exporting...' : 'Export Confirmations'}
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
@@ -461,6 +461,41 @@
|
|
|
461
461
|
padding: var(--spaceM);
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
.previewMeta {
|
|
465
|
+
display: flex;
|
|
466
|
+
flex-direction: column;
|
|
467
|
+
gap: var(--spaceS);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.previewMetaRow {
|
|
471
|
+
display: grid;
|
|
472
|
+
grid-template-columns: 100px 1fr;
|
|
473
|
+
gap: var(--spaceS);
|
|
474
|
+
font-size: var(--fontSizeBodyS);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.previewMetaLabel {
|
|
478
|
+
color: var(--textLight);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.previewMetaValue {
|
|
482
|
+
color: var(--textBody);
|
|
483
|
+
font-weight: var(--fontWeightMedium);
|
|
484
|
+
word-break: break-word;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.previewValidBadge {
|
|
488
|
+
color: var(--success);
|
|
489
|
+
font-size: var(--fontSizeBodyS);
|
|
490
|
+
font-weight: var(--fontWeightMedium);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.previewInvalidBadge {
|
|
494
|
+
color: var(--error);
|
|
495
|
+
font-size: var(--fontSizeBodyS);
|
|
496
|
+
font-weight: var(--fontWeightMedium);
|
|
497
|
+
}
|
|
498
|
+
|
|
464
499
|
/* Confirmation Dialog */
|
|
465
500
|
.confirmationOverlay {
|
|
466
501
|
position: fixed;
|
|
@@ -11,6 +11,21 @@ interface CasePreviewSectionProps {
|
|
|
11
11
|
isArchivedRegularCaseImportBlocked?: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function formatDate(isoDate: string | undefined): string {
|
|
15
|
+
if (!isoDate) return 'Unknown';
|
|
16
|
+
|
|
17
|
+
const date = new Date(isoDate);
|
|
18
|
+
if (Number.isNaN(date.getTime())) {
|
|
19
|
+
return isoDate;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return date.toLocaleDateString(undefined, {
|
|
23
|
+
year: 'numeric',
|
|
24
|
+
month: 'short',
|
|
25
|
+
day: 'numeric'
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
export const CasePreviewSection = ({
|
|
15
30
|
casePreview,
|
|
16
31
|
isLoadingPreview,
|
|
@@ -31,9 +46,42 @@ export const CasePreviewSection = ({
|
|
|
31
46
|
return (
|
|
32
47
|
<div className={styles.previewSection}>
|
|
33
48
|
<h3 className={styles.previewTitle}>Case Import Preview</h3>
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
<div className={styles.previewMeta}>
|
|
50
|
+
<div className={styles.previewMetaRow}>
|
|
51
|
+
<span className={styles.previewMetaLabel}>Case</span>
|
|
52
|
+
<span className={styles.previewMetaValue}>{casePreview.caseNumber}</span>
|
|
53
|
+
</div>
|
|
54
|
+
{(casePreview.exportedByName ?? casePreview.exportedBy) && (
|
|
55
|
+
<div className={styles.previewMetaRow}>
|
|
56
|
+
<span className={styles.previewMetaLabel}>Exported by</span>
|
|
57
|
+
<span className={styles.previewMetaValue}>
|
|
58
|
+
{casePreview.exportedByName ?? casePreview.exportedBy}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
{casePreview.exportedByCompany && (
|
|
63
|
+
<div className={styles.previewMetaRow}>
|
|
64
|
+
<span className={styles.previewMetaLabel}>Organization</span>
|
|
65
|
+
<span className={styles.previewMetaValue}>{casePreview.exportedByCompany}</span>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
<div className={styles.previewMetaRow}>
|
|
69
|
+
<span className={styles.previewMetaLabel}>Exported</span>
|
|
70
|
+
<span className={styles.previewMetaValue}>{formatDate(casePreview.exportDate)}</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div className={styles.previewMetaRow}>
|
|
73
|
+
<span className={styles.previewMetaLabel}>Files</span>
|
|
74
|
+
<span className={styles.previewMetaValue}>{casePreview.totalFiles}</span>
|
|
75
|
+
</div>
|
|
76
|
+
{casePreview.hashValid !== undefined && (
|
|
77
|
+
<div className={styles.previewMetaRow}>
|
|
78
|
+
<span className={styles.previewMetaLabel}>Integrity</span>
|
|
79
|
+
<span className={casePreview.hashValid ? styles.previewValidBadge : styles.previewInvalidBadge}>
|
|
80
|
+
{casePreview.hashValid ? 'Passed' : 'Failed'}
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
37
85
|
{casePreview.archived && (
|
|
38
86
|
<div className={styles.archivedImportNote}>
|
|
39
87
|
{ARCHIVED_SELF_IMPORT_NOTE}
|
|
@@ -24,6 +24,7 @@ export const ConfirmationDialog = ({
|
|
|
24
24
|
}: ConfirmationDialogProps) => {
|
|
25
25
|
if (!showConfirmation || !casePreview) return null;
|
|
26
26
|
|
|
27
|
+
const isEncrypted = casePreview.caseNumber === 'ENCRYPTED';
|
|
27
28
|
const hasDetails = casePreview.archived || isArchivedRegularCaseImportBlocked;
|
|
28
29
|
|
|
29
30
|
return (
|
|
@@ -32,10 +33,7 @@ export const ConfirmationDialog = ({
|
|
|
32
33
|
<div className={styles.confirmationContent}>
|
|
33
34
|
<h3 className={styles.confirmationTitle}>Confirm Case Import</h3>
|
|
34
35
|
<p className={styles.confirmationText}>
|
|
35
|
-
Are you sure you want to import this case for review?
|
|
36
|
-
</p>
|
|
37
|
-
<p className={styles.confirmationText}>
|
|
38
|
-
Package details stay hidden until verification completes.
|
|
36
|
+
Are you sure you want to import{isEncrypted ? ' this encrypted case' : ` case ${casePreview.caseNumber}`} for review?
|
|
39
37
|
</p>
|
|
40
38
|
|
|
41
39
|
{hasDetails && (
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { type ConfirmationImportPreview } from '~/types';
|
|
1
2
|
import styles from '../case-import.module.css';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
function formatDate(isoDate: string | undefined): string {
|
|
5
|
+
if (!isoDate) return 'Unknown';
|
|
6
|
+
const date = new Date(isoDate);
|
|
7
|
+
if (Number.isNaN(date.getTime())) return isoDate;
|
|
8
|
+
return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
9
|
+
}
|
|
4
10
|
|
|
5
11
|
interface ConfirmationPreviewSectionProps {
|
|
6
|
-
confirmationPreview:
|
|
12
|
+
confirmationPreview: ConfirmationImportPreview | null;
|
|
7
13
|
isLoadingPreview: boolean;
|
|
8
14
|
}
|
|
9
15
|
|
|
@@ -23,9 +29,34 @@ export const ConfirmationPreviewSection = ({ confirmationPreview, isLoadingPrevi
|
|
|
23
29
|
return (
|
|
24
30
|
<div className={styles.previewSection}>
|
|
25
31
|
<h3 className={styles.previewTitle}>Confirmation Import Preview</h3>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
<div className={styles.previewMeta}>
|
|
33
|
+
<div className={styles.previewMetaRow}>
|
|
34
|
+
<span className={styles.previewMetaLabel}>Case</span>
|
|
35
|
+
<span className={styles.previewMetaValue}>{confirmationPreview.caseNumber}</span>
|
|
36
|
+
</div>
|
|
37
|
+
{(confirmationPreview.exportedByName || confirmationPreview.exportedBy) && (
|
|
38
|
+
<div className={styles.previewMetaRow}>
|
|
39
|
+
<span className={styles.previewMetaLabel}>Exported by</span>
|
|
40
|
+
<span className={styles.previewMetaValue}>
|
|
41
|
+
{confirmationPreview.exportedByName || confirmationPreview.exportedBy}
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
{confirmationPreview.exportedByCompany && (
|
|
46
|
+
<div className={styles.previewMetaRow}>
|
|
47
|
+
<span className={styles.previewMetaLabel}>Organization</span>
|
|
48
|
+
<span className={styles.previewMetaValue}>{confirmationPreview.exportedByCompany}</span>
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
<div className={styles.previewMetaRow}>
|
|
52
|
+
<span className={styles.previewMetaLabel}>Exported</span>
|
|
53
|
+
<span className={styles.previewMetaValue}>{formatDate(confirmationPreview.exportDate)}</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div className={styles.previewMetaRow}>
|
|
56
|
+
<span className={styles.previewMetaLabel}>Confirmations</span>
|
|
57
|
+
<span className={styles.previewMetaValue}>{confirmationPreview.totalConfirmations}</span>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
29
60
|
</div>
|
|
30
61
|
);
|
|
31
62
|
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
2
|
import type { User } from 'firebase/auth';
|
|
3
|
-
import { previewCaseImport,
|
|
4
|
-
import { type CaseImportPreview } from '~/types';
|
|
5
|
-
import { type ConfirmationPreview } from '../components/ConfirmationPreviewSection';
|
|
3
|
+
import { previewCaseImport, previewConfirmationImport } from '~/components/actions/case-review';
|
|
4
|
+
import { type CaseImportPreview, type ConfirmationImportPreview } from '~/types';
|
|
6
5
|
|
|
7
6
|
interface UseFilePreviewReturn {
|
|
8
7
|
casePreview: CaseImportPreview | null;
|
|
9
|
-
confirmationPreview:
|
|
8
|
+
confirmationPreview: ConfirmationImportPreview | null;
|
|
10
9
|
loadCasePreview: (file: File) => Promise<void>;
|
|
11
10
|
loadConfirmationPreview: (file: File) => Promise<void>;
|
|
12
11
|
clearPreviews: () => void;
|
|
@@ -22,7 +21,7 @@ export const useFilePreview = (
|
|
|
22
21
|
clearImportData: () => void
|
|
23
22
|
): UseFilePreviewReturn => {
|
|
24
23
|
const [casePreview, setCasePreview] = useState<CaseImportPreview | null>(null);
|
|
25
|
-
const [confirmationPreview, setConfirmationPreview] = useState<
|
|
24
|
+
const [confirmationPreview, setConfirmationPreview] = useState<ConfirmationImportPreview | null>(null);
|
|
26
25
|
|
|
27
26
|
const loadCasePreview = useCallback(async (file: File) => {
|
|
28
27
|
if (!user) {
|
|
@@ -51,10 +50,7 @@ export const useFilePreview = (
|
|
|
51
50
|
|
|
52
51
|
setIsLoadingPreview(true);
|
|
53
52
|
try {
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
const preview: ConfirmationPreview = {};
|
|
57
|
-
|
|
53
|
+
const preview = await previewConfirmationImport(file, user);
|
|
58
54
|
setConfirmationPreview(preview);
|
|
59
55
|
} catch (error) {
|
|
60
56
|
console.error('Error loading confirmation preview:', error);
|
|
@@ -12,7 +12,4 @@ export { useFilePreview } from './hooks/useFilePreview';
|
|
|
12
12
|
export { useImportExecution } from './hooks/useImportExecution';
|
|
13
13
|
|
|
14
14
|
// Utils
|
|
15
|
-
export * from './utils/file-validation';
|
|
16
|
-
|
|
17
|
-
// Types
|
|
18
|
-
export type { ConfirmationPreview } from './components/ConfirmationPreviewSection';
|
|
15
|
+
export * from './utils/file-validation';
|
|
@@ -89,10 +89,10 @@ export const SHOTSHELL_BUCKSHOT_OPTIONS = [
|
|
|
89
89
|
|
|
90
90
|
export const ALL_CALIBERS: string[] = [...PISTOL_CALIBERS, ...RIFLE_CALIBERS];
|
|
91
91
|
export const BULLET_JACKET_METAL_OPTIONS = ['Cu', 'Brass', 'Ni-plated', 'Al', 'Steel', 'None'] as const;
|
|
92
|
-
export const BULLET_CORE_METAL_OPTIONS = ['Pb', 'Steel'] as const;
|
|
92
|
+
export const BULLET_CORE_METAL_OPTIONS = ['Pb', 'Steel', 'Solid Cu', 'Frangible'] as const;
|
|
93
93
|
export const BULLET_TYPE_OPTIONS = ['FMJ', 'TMJ', 'HP', 'WC'] as const;
|
|
94
94
|
export const BULLET_BARREL_TYPE_OPTIONS = ['Conventional', 'Polygonal'] as const;
|
|
95
|
-
export const CARTRIDGE_METAL_OPTIONS = ['Brass', 'Ni-plated', 'Al', 'Steel'] as const;
|
|
95
|
+
export const CARTRIDGE_METAL_OPTIONS = ['Brass', 'Ni-plated', 'Al', 'Steel', 'Bi-metal'] as const;
|
|
96
96
|
export const CARTRIDGE_PRIMER_TYPE_OPTIONS = ['CF', 'RF'] as const;
|
|
97
97
|
export const CARTRIDGE_FPI_SHAPE_OPTIONS = ['Circular', 'Elliptical', 'Rectangular/Square', 'Tear-drop'] as const;
|
|
98
98
|
export const CARTRIDGE_APERTURE_SHAPE_OPTIONS = ['Circular', 'Rectangular'] as const;
|
|
@@ -90,11 +90,47 @@
|
|
|
90
90
|
font-size: 14px;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
.toast.loading {
|
|
94
|
+
background: var(--backgroundLight);
|
|
95
|
+
border-color: var(--primary);
|
|
96
|
+
box-shadow: 0 8px 32px color-mix(in lab, var(--primary) 20%, transparent);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.toast.loading .icon {
|
|
100
|
+
color: var(--primary);
|
|
101
|
+
background: color-mix(in lab, var(--primary) 15%, transparent);
|
|
102
|
+
border-radius: 50%;
|
|
103
|
+
width: 28px;
|
|
104
|
+
height: 28px;
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
.icon {
|
|
94
111
|
font-weight: bold;
|
|
95
112
|
flex-shrink: 0;
|
|
96
113
|
}
|
|
97
114
|
|
|
115
|
+
.spinner {
|
|
116
|
+
width: 14px;
|
|
117
|
+
height: 14px;
|
|
118
|
+
border: 2px solid color-mix(in lab, var(--primary) 20%, transparent);
|
|
119
|
+
border-top-color: var(--primary);
|
|
120
|
+
border-radius: 50%;
|
|
121
|
+
animation: spin 0.8s linear infinite;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@keyframes spin {
|
|
125
|
+
from {
|
|
126
|
+
transform: rotate(0deg);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
to {
|
|
130
|
+
transform: rotate(360deg);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
98
134
|
.message {
|
|
99
135
|
flex: 1;
|
|
100
136
|
font-size: 16px;
|