@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
package/LICENSE
CHANGED
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
deleteCaseData,
|
|
12
12
|
duplicateCaseData,
|
|
13
13
|
deleteFileAnnotations,
|
|
14
|
-
signForensicManifest
|
|
14
|
+
signForensicManifest,
|
|
15
|
+
removeCaseConfirmationSummary
|
|
15
16
|
} from '~/utils/data';
|
|
16
|
-
import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail } from '~/types';
|
|
17
|
+
import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail, type CaseExportData } from '~/types';
|
|
17
18
|
import { auditService } from '~/services/audit';
|
|
18
19
|
import { fetchImageApi } from '~/utils/api';
|
|
19
20
|
import { exportCaseData, formatDateForFilename } from '~/components/actions/case-export';
|
|
@@ -569,6 +570,13 @@ export const deleteCase = async (user: User, caseNumber: string): Promise<Delete
|
|
|
569
570
|
// Delete case data using centralized function (skip validation since user no longer has access)
|
|
570
571
|
await deleteCaseData(user, caseNumber, { skipValidation: true });
|
|
571
572
|
|
|
573
|
+
// Clean up confirmation status metadata for this case
|
|
574
|
+
try {
|
|
575
|
+
await removeCaseConfirmationSummary(user, caseNumber);
|
|
576
|
+
} catch (summaryError) {
|
|
577
|
+
console.warn(`Failed to remove confirmation summary for case ${caseNumber}:`, summaryError);
|
|
578
|
+
}
|
|
579
|
+
|
|
572
580
|
// Add a small delay before audit logging to reduce rate limiting
|
|
573
581
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
574
582
|
|
|
@@ -593,6 +601,13 @@ export const deleteCase = async (user: User, caseNumber: string): Promise<Delete
|
|
|
593
601
|
// Delete case data using centralized function (skip validation since user no longer has access)
|
|
594
602
|
await deleteCaseData(user, caseNumber, { skipValidation: true });
|
|
595
603
|
|
|
604
|
+
// Clean up confirmation status metadata for this case
|
|
605
|
+
try {
|
|
606
|
+
await removeCaseConfirmationSummary(user, caseNumber);
|
|
607
|
+
} catch (summaryError) {
|
|
608
|
+
console.warn(`Failed to remove confirmation summary for case ${caseNumber}:`, summaryError);
|
|
609
|
+
}
|
|
610
|
+
|
|
596
611
|
// Add a small delay before audit logging to reduce rate limiting
|
|
597
612
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
598
613
|
|
|
@@ -736,22 +751,19 @@ export const archiveCase = async (
|
|
|
736
751
|
isReadOnly: true,
|
|
737
752
|
} as CaseData;
|
|
738
753
|
|
|
739
|
-
await updateCaseData(user, caseNumber, archiveData);
|
|
740
|
-
|
|
741
|
-
await auditService.logCaseArchive(
|
|
742
|
-
user,
|
|
743
|
-
caseNumber,
|
|
744
|
-
caseNumber,
|
|
745
|
-
archiveReason?.trim() || 'No reason provided',
|
|
746
|
-
'success',
|
|
747
|
-
[],
|
|
748
|
-
archiveData.files?.length || 0,
|
|
749
|
-
archivedAt,
|
|
750
|
-
Date.now() - startTime
|
|
751
|
-
);
|
|
752
|
-
|
|
753
754
|
const exportData = await exportCaseData(user, caseNumber, { includeMetadata: true });
|
|
754
|
-
const
|
|
755
|
+
const archivedExportData: CaseExportData = {
|
|
756
|
+
...exportData,
|
|
757
|
+
metadata: {
|
|
758
|
+
...exportData.metadata,
|
|
759
|
+
archived: true,
|
|
760
|
+
archivedAt,
|
|
761
|
+
archivedBy: user.uid,
|
|
762
|
+
archivedByDisplay,
|
|
763
|
+
archiveReason: archiveReason?.trim() || undefined,
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
const caseJsonContent = JSON.stringify(archivedExportData, null, 2);
|
|
755
767
|
|
|
756
768
|
const JSZip = (await import('jszip')).default;
|
|
757
769
|
const zip = new JSZip();
|
|
@@ -876,6 +888,27 @@ export const archiveCase = async (
|
|
|
876
888
|
compressionOptions: { level: 6 },
|
|
877
889
|
});
|
|
878
890
|
|
|
891
|
+
await updateCaseData(user, caseNumber, archiveData);
|
|
892
|
+
|
|
893
|
+
// Clean up confirmation status metadata for this archived case
|
|
894
|
+
try {
|
|
895
|
+
await removeCaseConfirmationSummary(user, caseNumber);
|
|
896
|
+
} catch (summaryError) {
|
|
897
|
+
console.warn(`Failed to remove confirmation summary for case ${caseNumber}:`, summaryError);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
await auditService.logCaseArchive(
|
|
901
|
+
user,
|
|
902
|
+
caseNumber,
|
|
903
|
+
caseNumber,
|
|
904
|
+
archiveReason?.trim() || 'No reason provided',
|
|
905
|
+
'success',
|
|
906
|
+
[],
|
|
907
|
+
archiveData.files?.length || 0,
|
|
908
|
+
archivedAt,
|
|
909
|
+
Date.now() - startTime
|
|
910
|
+
);
|
|
911
|
+
|
|
879
912
|
const downloadUrl = URL.createObjectURL(zipBlob);
|
|
880
913
|
const archiveFileName = `striae-case-${caseNumber}-archive-${formatDateForFilename(new Date())}.zip`;
|
|
881
914
|
const anchor = document.createElement('a');
|
|
@@ -15,8 +15,11 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
|
|
|
15
15
|
<p>No activities match the current filters.</p>
|
|
16
16
|
</div>
|
|
17
17
|
) : (
|
|
18
|
-
entries.map((entry
|
|
19
|
-
<div
|
|
18
|
+
entries.map((entry) => (
|
|
19
|
+
<div
|
|
20
|
+
key={`${entry.timestamp}-${entry.userId}-${entry.action}-${entry.details.fileName || ''}`}
|
|
21
|
+
className={`${styles.entry} ${styles[entry.result]}`}
|
|
22
|
+
>
|
|
20
23
|
<div className={styles.entryHeader}>
|
|
21
24
|
<div className={styles.entryIcons}>
|
|
22
25
|
<span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
|
|
@@ -112,10 +112,13 @@ export const useAuditViewerData = ({
|
|
|
112
112
|
|
|
113
113
|
if (effectiveCaseNumber) {
|
|
114
114
|
const caseData = await getCaseData(user, effectiveCaseNumber);
|
|
115
|
-
const
|
|
116
|
-
|
|
115
|
+
const isArchiveBundleCase = Boolean(
|
|
116
|
+
caseData?.archived === true &&
|
|
117
|
+
caseData?.bundledAuditTrail?.source === 'archive-bundle'
|
|
118
|
+
);
|
|
119
|
+
setIsArchivedReadOnlyCase(isArchiveBundleCase);
|
|
117
120
|
|
|
118
|
-
if (
|
|
121
|
+
if (isArchiveBundleCase && !Array.isArray(caseData?.bundledAuditTrail?.entries)) {
|
|
119
122
|
setBundledAuditWarning(
|
|
120
123
|
'This imported archived case does not include bundled audit trail data. No audit entries are available for this case.'
|
|
121
124
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useContext } from 'react';
|
|
1
|
+
import { useState, useEffect, useContext, useRef } from 'react';
|
|
2
2
|
import { type ConfirmationData } from '~/types/annotations';
|
|
3
3
|
import { AuthContext } from '~/contexts/auth.context';
|
|
4
4
|
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
@@ -33,6 +33,7 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
33
33
|
const [badgeId, setBadgeId] = useState('');
|
|
34
34
|
const [error, setError] = useState('');
|
|
35
35
|
const [isConfirming, setIsConfirming] = useState(false);
|
|
36
|
+
const wasOpenRef = useRef(false);
|
|
36
37
|
|
|
37
38
|
const fullName = user?.displayName || user?.email || 'Unknown User';
|
|
38
39
|
const userEmail = user?.email || 'No email available';
|
|
@@ -54,7 +55,10 @@ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, default
|
|
|
54
55
|
|
|
55
56
|
// Reset form when modal opens
|
|
56
57
|
useEffect(() => {
|
|
57
|
-
|
|
58
|
+
const justOpened = isOpen && !wasOpenRef.current;
|
|
59
|
+
wasOpenRef.current = isOpen;
|
|
60
|
+
|
|
61
|
+
if (justOpened) {
|
|
58
62
|
if (existingConfirmation) {
|
|
59
63
|
setBadgeId(existingConfirmation.badgeId);
|
|
60
64
|
} else {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-direction: column;
|
|
4
4
|
gap: 0.75rem;
|
|
5
|
+
width: fit-content;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
.colorHeader {
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
.colorWheel {
|
|
29
|
-
width:
|
|
30
|
+
width: 180px;
|
|
30
31
|
height: 40px;
|
|
31
32
|
padding: 0;
|
|
32
33
|
border: 2px solid #ced4da;
|
|
@@ -55,5 +56,5 @@
|
|
|
55
56
|
|
|
56
57
|
.colorSwatch.selected {
|
|
57
58
|
border-color: #0d6efd;
|
|
58
|
-
box-shadow: 0 0 0 2px rgba(13,110,253
|
|
59
|
-
}
|
|
59
|
+
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
|
|
60
|
+
}
|
|
@@ -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
|
>
|