@striae-org/striae 3.0.4
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 +100 -0
- package/LICENSE +190 -0
- package/NOTICE +18 -0
- package/README.md +133 -0
- package/app/components/actions/case-export/core-export.ts +328 -0
- package/app/components/actions/case-export/data-processing.ts +167 -0
- package/app/components/actions/case-export/download-handlers.ts +900 -0
- package/app/components/actions/case-export/index.ts +41 -0
- package/app/components/actions/case-export/metadata-helpers.ts +107 -0
- package/app/components/actions/case-export/types-constants.ts +56 -0
- package/app/components/actions/case-export/validation-utils.ts +25 -0
- package/app/components/actions/case-export.ts +4 -0
- package/app/components/actions/case-import/annotation-import.ts +35 -0
- package/app/components/actions/case-import/confirmation-import.ts +363 -0
- package/app/components/actions/case-import/image-operations.ts +61 -0
- package/app/components/actions/case-import/index.ts +39 -0
- package/app/components/actions/case-import/orchestrator.ts +420 -0
- package/app/components/actions/case-import/storage-operations.ts +270 -0
- package/app/components/actions/case-import/validation.ts +189 -0
- package/app/components/actions/case-import/zip-processing.ts +413 -0
- package/app/components/actions/case-manage.ts +524 -0
- package/app/components/actions/case-review.ts +4 -0
- package/app/components/actions/confirm-export.ts +351 -0
- package/app/components/actions/generate-pdf.ts +210 -0
- package/app/components/actions/image-manage.ts +385 -0
- package/app/components/actions/notes-manage.ts +33 -0
- package/app/components/actions/signout.module.css +15 -0
- package/app/components/actions/signout.tsx +50 -0
- package/app/components/audit/user-audit-viewer.tsx +975 -0
- package/app/components/audit/user-audit.module.css +568 -0
- package/app/components/auth/auth-provider.tsx +78 -0
- package/app/components/auth/mfa-enrollment.module.css +268 -0
- package/app/components/auth/mfa-enrollment.tsx +398 -0
- package/app/components/auth/mfa-verification.module.css +251 -0
- package/app/components/auth/mfa-verification.tsx +295 -0
- package/app/components/button/button.module.css +63 -0
- package/app/components/button/button.tsx +46 -0
- package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
- package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
- package/app/components/canvas/canvas.module.css +314 -0
- package/app/components/canvas/canvas.tsx +449 -0
- package/app/components/canvas/confirmation/confirmation.module.css +187 -0
- package/app/components/canvas/confirmation/confirmation.tsx +214 -0
- package/app/components/colors/colors.module.css +59 -0
- package/app/components/colors/colors.tsx +68 -0
- package/app/components/form/base-form.tsx +21 -0
- package/app/components/form/form-button.tsx +28 -0
- package/app/components/form/form-field.tsx +53 -0
- package/app/components/form/form-message.tsx +17 -0
- package/app/components/form/form-toggle.tsx +23 -0
- package/app/components/form/form.module.css +427 -0
- package/app/components/form/index.ts +6 -0
- package/app/components/icon/icon.module.css +3 -0
- package/app/components/icon/icon.tsx +27 -0
- package/app/components/icon/icons.svg +102 -0
- package/app/components/icon/manifest.json +110 -0
- package/app/components/sidebar/case-export/case-export.module.css +386 -0
- package/app/components/sidebar/case-export/case-export.tsx +317 -0
- package/app/components/sidebar/case-import/case-import.module.css +626 -0
- package/app/components/sidebar/case-import/case-import.tsx +404 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
- package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
- package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
- package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
- package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
- package/app/components/sidebar/case-import/index.ts +18 -0
- package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
- package/app/components/sidebar/cases/cases-modal.module.css +166 -0
- package/app/components/sidebar/cases/cases-modal.tsx +201 -0
- package/app/components/sidebar/cases/cases.module.css +713 -0
- package/app/components/sidebar/files/files-modal.module.css +209 -0
- package/app/components/sidebar/files/files-modal.tsx +239 -0
- package/app/components/sidebar/hash/hash-utility.module.css +366 -0
- package/app/components/sidebar/hash/hash-utility.tsx +982 -0
- package/app/components/sidebar/notes/notes-modal.tsx +51 -0
- package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
- package/app/components/sidebar/notes/notes.module.css +360 -0
- package/app/components/sidebar/sidebar-container.tsx +149 -0
- package/app/components/sidebar/sidebar.module.css +321 -0
- package/app/components/sidebar/sidebar.tsx +215 -0
- package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
- package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
- package/app/components/theme-provider/theme-provider.tsx +131 -0
- package/app/components/theme-provider/theme.ts +155 -0
- package/app/components/toast/toast.module.css +137 -0
- package/app/components/toast/toast.tsx +56 -0
- package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
- package/app/components/toolbar/toolbar.module.css +42 -0
- package/app/components/toolbar/toolbar.tsx +167 -0
- package/app/components/user/delete-account.module.css +274 -0
- package/app/components/user/delete-account.tsx +471 -0
- package/app/components/user/inactivity-warning.module.css +145 -0
- package/app/components/user/inactivity-warning.tsx +84 -0
- package/app/components/user/manage-profile.module.css +190 -0
- package/app/components/user/manage-profile.tsx +253 -0
- package/app/components/user/mfa-phone-update.tsx +739 -0
- package/app/config-example/admin-service.json +13 -0
- package/app/config-example/config.json +17 -0
- package/app/config-example/firebase.ts +21 -0
- package/app/config-example/inactivity.ts +13 -0
- package/app/config-example/meta-config.json +6 -0
- package/app/contexts/auth.context.ts +12 -0
- package/app/entry.client.tsx +12 -0
- package/app/entry.server.tsx +44 -0
- package/app/hooks/useInactivityTimeout.ts +110 -0
- package/app/root.tsx +170 -0
- package/app/routes/_index.tsx +16 -0
- package/app/routes/auth/emailActionHandler.module.css +232 -0
- package/app/routes/auth/emailActionHandler.tsx +405 -0
- package/app/routes/auth/emailVerification.tsx +120 -0
- package/app/routes/auth/login.module.css +523 -0
- package/app/routes/auth/login.tsx +654 -0
- package/app/routes/auth/passwordReset.module.css +274 -0
- package/app/routes/auth/passwordReset.tsx +154 -0
- package/app/routes/auth/route.ts +16 -0
- package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
- package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
- package/app/routes/mobile-prevented/route.ts +14 -0
- package/app/routes/striae/striae.module.css +30 -0
- package/app/routes/striae/striae.tsx +417 -0
- package/app/services/audit-export.service.ts +755 -0
- package/app/services/audit.service.ts +1454 -0
- package/app/services/firebase-errors.ts +106 -0
- package/app/services/firebase.ts +15 -0
- package/app/styles/legal-pages.module.css +113 -0
- package/app/styles/root.module.css +146 -0
- package/app/tailwind.css +225 -0
- package/app/types/annotations.ts +45 -0
- package/app/types/audit.ts +301 -0
- package/app/types/case.ts +90 -0
- package/app/types/export.ts +8 -0
- package/app/types/file.ts +30 -0
- package/app/types/import.ts +107 -0
- package/app/types/index.ts +24 -0
- package/app/types/user.ts +38 -0
- package/app/utils/SHA256.ts +461 -0
- package/app/utils/annotation-timestamp.ts +25 -0
- package/app/utils/audit-export-signature.ts +117 -0
- package/app/utils/auth-action-settings.ts +48 -0
- package/app/utils/auth.ts +34 -0
- package/app/utils/batch-operations.ts +135 -0
- package/app/utils/confirmation-signature.ts +193 -0
- package/app/utils/data-operations.ts +871 -0
- package/app/utils/device-detection.ts +5 -0
- package/app/utils/html-sanitizer.ts +80 -0
- package/app/utils/id-generator.ts +36 -0
- package/app/utils/meta.ts +48 -0
- package/app/utils/mfa-phone.ts +97 -0
- package/app/utils/mfa.ts +79 -0
- package/app/utils/password-policy.ts +28 -0
- package/app/utils/permissions.ts +562 -0
- package/app/utils/signature-utils.ts +160 -0
- package/app/utils/style.ts +83 -0
- package/app/utils/version.ts +5 -0
- package/firebase.json +11 -0
- package/functions/[[path]].ts +10 -0
- package/package.json +138 -0
- package/postcss.config.js +6 -0
- package/public/.well-known/publickey.info@striae.org.asc +17 -0
- package/public/.well-known/security.txt +7 -0
- package/public/_headers +28 -0
- package/public/_routes.json +13 -0
- package/public/assets/striae.jpg +0 -0
- package/public/clear.jpg +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-dark.png +0 -0
- package/public/manifest.json +25 -0
- package/public/oin-badge.png +0 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/striae-ascii.txt +10 -0
- package/scripts/deploy-all.sh +100 -0
- package/scripts/deploy-config.sh +940 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-worker-secrets.sh +215 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/install-workers.sh +88 -0
- package/scripts/run-eslint.cjs +35 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/tailwind.config.ts +22 -0
- package/tsconfig.json +33 -0
- package/vite.config.ts +35 -0
- package/worker-configuration.d.ts +7490 -0
- package/workers/audit-worker/package.json +17 -0
- package/workers/audit-worker/src/audit-worker.example.ts +195 -0
- package/workers/audit-worker/worker-configuration.d.ts +7448 -0
- package/workers/audit-worker/wrangler.jsonc.example +29 -0
- package/workers/data-worker/package.json +17 -0
- package/workers/data-worker/src/data-worker.example.ts +267 -0
- package/workers/data-worker/src/signature-utils.ts +79 -0
- package/workers/data-worker/src/signing-payload-utils.ts +290 -0
- package/workers/data-worker/worker-configuration.d.ts +7448 -0
- package/workers/data-worker/wrangler.jsonc.example +30 -0
- package/workers/image-worker/package.json +17 -0
- package/workers/image-worker/src/image-worker.example.ts +180 -0
- package/workers/image-worker/worker-configuration.d.ts +7447 -0
- package/workers/image-worker/wrangler.jsonc.example +22 -0
- package/workers/keys-worker/package.json +17 -0
- package/workers/keys-worker/src/keys.example.ts +66 -0
- package/workers/keys-worker/src/keys.ts +66 -0
- package/workers/keys-worker/worker-configuration.d.ts +7447 -0
- package/workers/keys-worker/wrangler.jsonc.example +22 -0
- package/workers/pdf-worker/package.json +17 -0
- package/workers/pdf-worker/src/format-striae.ts +534 -0
- package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
- package/workers/pdf-worker/src/report-types.ts +69 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
- package/workers/pdf-worker/wrangler.jsonc.example +26 -0
- package/workers/user-worker/package.json +17 -0
- package/workers/user-worker/src/user-worker.example.ts +636 -0
- package/workers/user-worker/worker-configuration.d.ts +7448 -0
- package/workers/user-worker/wrangler.jsonc.example +29 -0
- package/wrangler.toml.example +8 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import styles from './notes.module.css';
|
|
3
|
+
|
|
4
|
+
interface NotesModalProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
notes: string;
|
|
8
|
+
onSave: (notes: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const NotesModal = ({ isOpen, onClose, notes, onSave }: NotesModalProps) => {
|
|
12
|
+
const [tempNotes, setTempNotes] = useState(notes);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
16
|
+
if (e.key === 'Escape') {
|
|
17
|
+
onClose();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (isOpen) {
|
|
22
|
+
document.addEventListener('keydown', handleEscape);
|
|
23
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
24
|
+
}
|
|
25
|
+
}, [isOpen, onClose]);
|
|
26
|
+
|
|
27
|
+
if (!isOpen) return null;
|
|
28
|
+
|
|
29
|
+
const handleSave = () => {
|
|
30
|
+
onSave(tempNotes);
|
|
31
|
+
onClose();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className={styles.modalOverlay}>
|
|
36
|
+
<div className={styles.modal}>
|
|
37
|
+
<h5 className={styles.modalTitle}>Additional Notes</h5>
|
|
38
|
+
<textarea
|
|
39
|
+
value={tempNotes}
|
|
40
|
+
onChange={(e) => setTempNotes(e.target.value)}
|
|
41
|
+
className={styles.modalTextarea}
|
|
42
|
+
placeholder="Enter additional notes..."
|
|
43
|
+
/>
|
|
44
|
+
<div className={styles.modalButtons}>
|
|
45
|
+
<button onClick={handleSave} className={styles.saveButton}>Save</button>
|
|
46
|
+
<button onClick={onClose} className={styles.cancelButton}>Cancel</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { User } from 'firebase/auth';
|
|
3
|
+
import { ColorSelector } from '~/components/colors/colors';
|
|
4
|
+
import { NotesModal } from './notes-modal';
|
|
5
|
+
import { getNotes, saveNotes } from '~/components/actions/notes-manage';
|
|
6
|
+
import { AnnotationData } from '~/types/annotations';
|
|
7
|
+
import { resolveEarliestAnnotationTimestamp } from '~/utils/annotation-timestamp';
|
|
8
|
+
import { auditService } from '~/services/audit.service';
|
|
9
|
+
import styles from './notes.module.css';
|
|
10
|
+
|
|
11
|
+
interface NotesSidebarProps {
|
|
12
|
+
currentCase: string;
|
|
13
|
+
onReturn: () => void;
|
|
14
|
+
user: User;
|
|
15
|
+
imageId: string;
|
|
16
|
+
onAnnotationRefresh?: () => void;
|
|
17
|
+
originalFileName?: string;
|
|
18
|
+
isUploading?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
|
|
22
|
+
type ClassType = 'Bullet' | 'Cartridge Case' | 'Other';
|
|
23
|
+
type IndexType = 'number' | 'color';
|
|
24
|
+
|
|
25
|
+
export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false }: NotesSidebarProps) => {
|
|
26
|
+
// Loading/Saving Notes States
|
|
27
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
28
|
+
const [loadError, setLoadError] = useState<string>();
|
|
29
|
+
const [saveError, setSaveError] = useState<string>();
|
|
30
|
+
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
31
|
+
const [isConfirmedImage, setIsConfirmedImage] = useState(false);
|
|
32
|
+
// Case numbers state
|
|
33
|
+
const [leftCase, setLeftCase] = useState('');
|
|
34
|
+
const [rightCase, setRightCase] = useState('');
|
|
35
|
+
const [leftItem, setLeftItem] = useState('');
|
|
36
|
+
const [rightItem, setRightItem] = useState('');
|
|
37
|
+
const [useCurrentCaseLeft, setUseCurrentCaseLeft] = useState(false);
|
|
38
|
+
const [useCurrentCaseRight, setUseCurrentCaseRight] = useState(false);
|
|
39
|
+
const [caseFontColor, setCaseFontColor] = useState('');
|
|
40
|
+
|
|
41
|
+
// Class characteristics state
|
|
42
|
+
const [classType, setClassType] = useState<ClassType | ''>('');
|
|
43
|
+
const [customClass, setCustomClass] = useState('');
|
|
44
|
+
const [classNote, setClassNote] = useState('');
|
|
45
|
+
const [hasSubclass, setHasSubclass] = useState(false);
|
|
46
|
+
|
|
47
|
+
// Index state
|
|
48
|
+
const [indexType, setIndexType] = useState<IndexType>('color');
|
|
49
|
+
const [indexNumber, setIndexNumber] = useState('');
|
|
50
|
+
const [indexColor, setIndexColor] = useState('');
|
|
51
|
+
|
|
52
|
+
// Support level and confirmation
|
|
53
|
+
const [supportLevel, setSupportLevel] = useState<SupportLevel | ''>('');
|
|
54
|
+
const [includeConfirmation, setIncludeConfirmation] = useState(false);
|
|
55
|
+
|
|
56
|
+
// Additional Notes Modal
|
|
57
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
58
|
+
const [additionalNotes, setAdditionalNotes] = useState('');
|
|
59
|
+
const areInputsDisabled = isUploading || isConfirmedImage;
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const loadExistingNotes = async () => {
|
|
63
|
+
if (!imageId || !currentCase) return;
|
|
64
|
+
|
|
65
|
+
setIsLoading(true);
|
|
66
|
+
setLoadError(undefined);
|
|
67
|
+
setSaveError(undefined);
|
|
68
|
+
setSaveSuccess(false);
|
|
69
|
+
setIsConfirmedImage(false);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const existingNotes = await getNotes(user, currentCase, imageId);
|
|
73
|
+
|
|
74
|
+
if (existingNotes) {
|
|
75
|
+
const hasExistingConfirmation = !!existingNotes.confirmationData;
|
|
76
|
+
setIsConfirmedImage(hasExistingConfirmation);
|
|
77
|
+
|
|
78
|
+
// Update all form fields with existing data
|
|
79
|
+
setLeftCase(existingNotes.leftCase);
|
|
80
|
+
setRightCase(existingNotes.rightCase);
|
|
81
|
+
setLeftItem(existingNotes.leftItem);
|
|
82
|
+
setRightItem(existingNotes.rightItem);
|
|
83
|
+
setCaseFontColor(existingNotes.caseFontColor || '');
|
|
84
|
+
setClassType(existingNotes.classType || '');
|
|
85
|
+
setCustomClass(existingNotes.customClass || '');
|
|
86
|
+
setClassNote(existingNotes.classNote || '');
|
|
87
|
+
setHasSubclass(existingNotes.hasSubclass ?? false);
|
|
88
|
+
setIndexType(existingNotes.indexType || 'color');
|
|
89
|
+
setIndexNumber(existingNotes.indexNumber || '');
|
|
90
|
+
setIndexColor(existingNotes.indexColor || '');
|
|
91
|
+
setSupportLevel(existingNotes.supportLevel || '');
|
|
92
|
+
setIncludeConfirmation(existingNotes.includeConfirmation);
|
|
93
|
+
setAdditionalNotes(existingNotes.additionalNotes || '');
|
|
94
|
+
} else {
|
|
95
|
+
setIsConfirmedImage(false);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
setLoadError('Failed to load existing notes');
|
|
99
|
+
console.error('Error loading notes:', error);
|
|
100
|
+
} finally {
|
|
101
|
+
setIsLoading(false);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
loadExistingNotes();
|
|
106
|
+
}, [imageId, currentCase, user]);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (useCurrentCaseLeft) {
|
|
110
|
+
setLeftCase(currentCase);
|
|
111
|
+
}
|
|
112
|
+
if (useCurrentCaseRight) {
|
|
113
|
+
setRightCase(currentCase);
|
|
114
|
+
}
|
|
115
|
+
}, [useCurrentCaseLeft, useCurrentCaseRight, currentCase]);
|
|
116
|
+
|
|
117
|
+
const handleSave = async () => {
|
|
118
|
+
|
|
119
|
+
if (!imageId) {
|
|
120
|
+
console.error('No image selected');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
setSaveError(undefined);
|
|
125
|
+
setSaveSuccess(false);
|
|
126
|
+
|
|
127
|
+
let existingData: AnnotationData | null = null;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// First, get existing annotation data to preserve box annotations
|
|
131
|
+
existingData = await getNotes(user, currentCase, imageId);
|
|
132
|
+
|
|
133
|
+
if (existingData?.confirmationData) {
|
|
134
|
+
setIsConfirmedImage(true);
|
|
135
|
+
setSaveError('This image is confirmed. Notes cannot be modified.');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Create updated annotation data, preserving box annotations and earliest timestamp
|
|
140
|
+
const now = new Date().toISOString();
|
|
141
|
+
const annotationData: AnnotationData = {
|
|
142
|
+
// Case Information
|
|
143
|
+
leftCase: leftCase || '',
|
|
144
|
+
rightCase: rightCase || '',
|
|
145
|
+
leftItem: leftItem || '',
|
|
146
|
+
rightItem: rightItem || '',
|
|
147
|
+
caseFontColor: caseFontColor || undefined,
|
|
148
|
+
|
|
149
|
+
// Class Characteristics
|
|
150
|
+
classType: classType as ClassType || undefined,
|
|
151
|
+
customClass: customClass,
|
|
152
|
+
classNote: classNote || undefined,
|
|
153
|
+
hasSubclass: hasSubclass,
|
|
154
|
+
|
|
155
|
+
// Index Information
|
|
156
|
+
indexType: indexType,
|
|
157
|
+
indexNumber: indexNumber,
|
|
158
|
+
indexColor: indexColor || undefined,
|
|
159
|
+
|
|
160
|
+
// Support Level & Confirmation
|
|
161
|
+
supportLevel: supportLevel as SupportLevel || undefined,
|
|
162
|
+
includeConfirmation: includeConfirmation,
|
|
163
|
+
|
|
164
|
+
// Additional Notes
|
|
165
|
+
additionalNotes: additionalNotes || undefined, // Keep as optional
|
|
166
|
+
|
|
167
|
+
// Preserve existing box annotations
|
|
168
|
+
boxAnnotations: existingData?.boxAnnotations || [],
|
|
169
|
+
|
|
170
|
+
// Metadata
|
|
171
|
+
updatedAt: now,
|
|
172
|
+
// Set earliest annotation timestamp on first save (don't overwrite if already exists)
|
|
173
|
+
earliestAnnotationTimestamp: resolveEarliestAnnotationTimestamp(
|
|
174
|
+
undefined,
|
|
175
|
+
existingData?.earliestAnnotationTimestamp,
|
|
176
|
+
now
|
|
177
|
+
)
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
await saveNotes(user, currentCase, imageId, annotationData);
|
|
181
|
+
|
|
182
|
+
// Comprehensive audit logging for annotation save
|
|
183
|
+
await auditService.logAnnotationEdit(
|
|
184
|
+
user,
|
|
185
|
+
`${currentCase}-${imageId}`,
|
|
186
|
+
existingData,
|
|
187
|
+
annotationData,
|
|
188
|
+
currentCase,
|
|
189
|
+
'notes-sidebar',
|
|
190
|
+
imageId,
|
|
191
|
+
originalFileName
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
setSaveSuccess(true);
|
|
195
|
+
setTimeout(() => setSaveSuccess(false), 3000);
|
|
196
|
+
|
|
197
|
+
// Refresh annotation data after saving notes
|
|
198
|
+
if (onAnnotationRefresh) {
|
|
199
|
+
onAnnotationRefresh();
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error('Failed to save notes:', error);
|
|
203
|
+
const errorMessage = error instanceof Error ? error.message : '';
|
|
204
|
+
if (errorMessage.toLowerCase().includes('confirmed image')) {
|
|
205
|
+
setIsConfirmedImage(true);
|
|
206
|
+
setSaveError('This image is confirmed. Notes cannot be modified.');
|
|
207
|
+
} else {
|
|
208
|
+
setSaveError('Failed to save notes. Please try again.');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Audit logging for failed annotation save
|
|
212
|
+
try {
|
|
213
|
+
await auditService.logAnnotationEdit(
|
|
214
|
+
user,
|
|
215
|
+
`${currentCase}-${imageId}`,
|
|
216
|
+
existingData,
|
|
217
|
+
null, // Failed save, no new value
|
|
218
|
+
currentCase,
|
|
219
|
+
'notes-sidebar',
|
|
220
|
+
imageId,
|
|
221
|
+
originalFileName
|
|
222
|
+
);
|
|
223
|
+
} catch (auditError) {
|
|
224
|
+
console.error('Failed to log annotation edit audit:', auditError);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div className={styles.notesSidebar}>
|
|
231
|
+
{isLoading ? (
|
|
232
|
+
<div className={styles.loading}>Loading notes...</div>
|
|
233
|
+
) : loadError ? (
|
|
234
|
+
<div className={styles.error}>{loadError}</div>
|
|
235
|
+
) : (
|
|
236
|
+
<>
|
|
237
|
+
{isConfirmedImage && (
|
|
238
|
+
<div className={styles.immutableNotice}>
|
|
239
|
+
This image is confirmed. Notes are read-only.
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{saveError && (
|
|
244
|
+
<div className={styles.errorMessage}>{saveError}</div>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
<div className={styles.section}>
|
|
248
|
+
<h5 className={styles.sectionTitle}>Case Information</h5>
|
|
249
|
+
<hr />
|
|
250
|
+
<div className={styles.caseNumbers}>
|
|
251
|
+
{/* Left side inputs */}
|
|
252
|
+
<div className={styles.inputGroup}>
|
|
253
|
+
<div className={styles.caseInput}>
|
|
254
|
+
<label htmlFor="leftCase">Left Side Case #</label>
|
|
255
|
+
<input
|
|
256
|
+
id="leftCase"
|
|
257
|
+
type="text"
|
|
258
|
+
value={leftCase}
|
|
259
|
+
onChange={(e) => setLeftCase(e.target.value)}
|
|
260
|
+
disabled={useCurrentCaseLeft || areInputsDisabled}
|
|
261
|
+
/>
|
|
262
|
+
</div>
|
|
263
|
+
<label className={`${styles.checkboxLabel} mb-4`}>
|
|
264
|
+
<input
|
|
265
|
+
type="checkbox"
|
|
266
|
+
checked={useCurrentCaseLeft}
|
|
267
|
+
onChange={(e) => setUseCurrentCaseLeft(e.target.checked)}
|
|
268
|
+
className={styles.checkbox}
|
|
269
|
+
disabled={areInputsDisabled}
|
|
270
|
+
/>
|
|
271
|
+
<span>Use current case number</span>
|
|
272
|
+
</label>
|
|
273
|
+
<div className={styles.caseInput}>
|
|
274
|
+
<label htmlFor="leftItem">Left Side Item #</label>
|
|
275
|
+
<input
|
|
276
|
+
id="leftItem"
|
|
277
|
+
type="text"
|
|
278
|
+
value={leftItem}
|
|
279
|
+
onChange={(e) => setLeftItem(e.target.value)}
|
|
280
|
+
disabled={areInputsDisabled}
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
<hr />
|
|
285
|
+
{/* Right side inputs */}
|
|
286
|
+
<div className={styles.inputGroup}>
|
|
287
|
+
<div className={styles.caseInput}>
|
|
288
|
+
<label htmlFor="rightCase">Right Side Case #</label>
|
|
289
|
+
<input
|
|
290
|
+
id="rightCase"
|
|
291
|
+
type="text"
|
|
292
|
+
value={rightCase}
|
|
293
|
+
onChange={(e) => setRightCase(e.target.value)}
|
|
294
|
+
disabled={useCurrentCaseRight || areInputsDisabled}
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
<label className={`${styles.checkboxLabel} mb-4`}>
|
|
298
|
+
<input
|
|
299
|
+
type="checkbox"
|
|
300
|
+
checked={useCurrentCaseRight}
|
|
301
|
+
onChange={(e) => setUseCurrentCaseRight(e.target.checked)}
|
|
302
|
+
className={styles.checkbox}
|
|
303
|
+
disabled={areInputsDisabled}
|
|
304
|
+
/>
|
|
305
|
+
<span>Use current case number</span>
|
|
306
|
+
</label>
|
|
307
|
+
<div className={styles.caseInput}>
|
|
308
|
+
<label htmlFor="rightItem">Right Side Item #</label>
|
|
309
|
+
<input
|
|
310
|
+
id="rightItem"
|
|
311
|
+
type="text"
|
|
312
|
+
value={rightItem}
|
|
313
|
+
onChange={(e) => setRightItem(e.target.value)}
|
|
314
|
+
disabled={areInputsDisabled}
|
|
315
|
+
/>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
<label htmlFor="colorSelect">Font</label>
|
|
320
|
+
<ColorSelector
|
|
321
|
+
selectedColor={caseFontColor}
|
|
322
|
+
onColorSelect={setCaseFontColor}
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div className={styles.section}>
|
|
327
|
+
<h5 className={styles.sectionTitle}>Class Characteristics</h5>
|
|
328
|
+
<div className={styles.classCharacteristics}>
|
|
329
|
+
<select
|
|
330
|
+
id="classType"
|
|
331
|
+
aria-label="Class Type"
|
|
332
|
+
value={classType}
|
|
333
|
+
onChange={(e) => setClassType(e.target.value as ClassType)}
|
|
334
|
+
className={styles.select}
|
|
335
|
+
disabled={areInputsDisabled}
|
|
336
|
+
>
|
|
337
|
+
<option value="">Select class type...</option>
|
|
338
|
+
<option value="Bullet">Bullet</option>
|
|
339
|
+
<option value="Cartridge Case">Cartridge Case</option>
|
|
340
|
+
<option value="Other">Other</option>
|
|
341
|
+
</select>
|
|
342
|
+
|
|
343
|
+
{classType === 'Other' && (
|
|
344
|
+
<input
|
|
345
|
+
type="text"
|
|
346
|
+
value={customClass}
|
|
347
|
+
onChange={(e) => setCustomClass(e.target.value)}
|
|
348
|
+
placeholder="Specify object type"
|
|
349
|
+
disabled={areInputsDisabled}
|
|
350
|
+
/>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
<textarea
|
|
354
|
+
value={classNote}
|
|
355
|
+
onChange={(e) => setClassNote(e.target.value)}
|
|
356
|
+
placeholder="Enter class characteristic details..."
|
|
357
|
+
className={styles.textarea}
|
|
358
|
+
disabled={areInputsDisabled}
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
<label className={`${styles.checkboxLabel} mb-4`}>
|
|
362
|
+
<input
|
|
363
|
+
type="checkbox"
|
|
364
|
+
checked={hasSubclass}
|
|
365
|
+
onChange={(e) => setHasSubclass(e.target.checked)}
|
|
366
|
+
className={styles.checkbox}
|
|
367
|
+
disabled={areInputsDisabled}
|
|
368
|
+
/>
|
|
369
|
+
<span>Potential subclass?</span>
|
|
370
|
+
</label>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<div className={styles.section}>
|
|
374
|
+
<h5 className={styles.sectionTitle}>Index Type</h5>
|
|
375
|
+
<div className={styles.indexing}>
|
|
376
|
+
<div className={styles.radioGroup}>
|
|
377
|
+
<label className={styles.radioLabel}>
|
|
378
|
+
<input
|
|
379
|
+
type="radio"
|
|
380
|
+
checked={indexType === 'color'}
|
|
381
|
+
onChange={() => setIndexType('color')}
|
|
382
|
+
disabled={areInputsDisabled}
|
|
383
|
+
/>
|
|
384
|
+
<span>Color</span>
|
|
385
|
+
</label>
|
|
386
|
+
<label className={styles.radioLabel}>
|
|
387
|
+
<input
|
|
388
|
+
type="radio"
|
|
389
|
+
checked={indexType === 'number'}
|
|
390
|
+
onChange={() => setIndexType('number')}
|
|
391
|
+
disabled={areInputsDisabled}
|
|
392
|
+
/>
|
|
393
|
+
<span>Number/Letter</span>
|
|
394
|
+
</label>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
{indexType === 'number' ? (
|
|
398
|
+
<input
|
|
399
|
+
type="text"
|
|
400
|
+
value={indexNumber}
|
|
401
|
+
onChange={(e) => setIndexNumber(e.target.value)}
|
|
402
|
+
placeholder="Enter index number"
|
|
403
|
+
disabled={areInputsDisabled}
|
|
404
|
+
/>
|
|
405
|
+
) : indexType === 'color' ? (
|
|
406
|
+
<ColorSelector
|
|
407
|
+
selectedColor={indexColor}
|
|
408
|
+
onColorSelect={setIndexColor}
|
|
409
|
+
/>
|
|
410
|
+
) : null}
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<div className={styles.section}>
|
|
415
|
+
<h5 className={styles.sectionTitle}>Support Level</h5>
|
|
416
|
+
<div className={styles.support}>
|
|
417
|
+
<select
|
|
418
|
+
id="supportLevel"
|
|
419
|
+
aria-label="Support Level"
|
|
420
|
+
value={supportLevel}
|
|
421
|
+
onChange={(e) => {
|
|
422
|
+
const newSupportLevel = e.target.value as SupportLevel;
|
|
423
|
+
setSupportLevel(newSupportLevel);
|
|
424
|
+
|
|
425
|
+
// Automatically check confirmation field when ID is selected
|
|
426
|
+
if (newSupportLevel === 'ID') {
|
|
427
|
+
setIncludeConfirmation(true);
|
|
428
|
+
}
|
|
429
|
+
}}
|
|
430
|
+
className={styles.select}
|
|
431
|
+
disabled={areInputsDisabled}
|
|
432
|
+
>
|
|
433
|
+
<option value="">Select support level...</option>
|
|
434
|
+
<option value="ID">Identification</option>
|
|
435
|
+
<option value="Exclusion">Exclusion</option>
|
|
436
|
+
<option value="Inconclusive">Inconclusive</option>
|
|
437
|
+
</select>
|
|
438
|
+
<label className={`${styles.checkboxLabel} mb-4`}>
|
|
439
|
+
<input
|
|
440
|
+
type="checkbox"
|
|
441
|
+
checked={includeConfirmation}
|
|
442
|
+
onChange={(e) => setIncludeConfirmation(e.target.checked)}
|
|
443
|
+
className={styles.checkbox}
|
|
444
|
+
disabled={areInputsDisabled}
|
|
445
|
+
/>
|
|
446
|
+
<span>Include confirmation field</span>
|
|
447
|
+
</label>
|
|
448
|
+
</div>
|
|
449
|
+
<button
|
|
450
|
+
onClick={() => setIsModalOpen(true)}
|
|
451
|
+
className={styles.notesButton}
|
|
452
|
+
disabled={areInputsDisabled}
|
|
453
|
+
title={isConfirmedImage ? "Cannot edit notes for confirmed images" : isUploading ? "Cannot add notes while uploading" : undefined}
|
|
454
|
+
>
|
|
455
|
+
Additional Notes
|
|
456
|
+
</button>
|
|
457
|
+
</div>
|
|
458
|
+
<button
|
|
459
|
+
onClick={handleSave}
|
|
460
|
+
className={styles.saveButton}
|
|
461
|
+
disabled={areInputsDisabled}
|
|
462
|
+
title={isConfirmedImage ? "Cannot save notes for confirmed images" : isUploading ? "Cannot save notes while uploading" : undefined}
|
|
463
|
+
>
|
|
464
|
+
Save Notes
|
|
465
|
+
</button>
|
|
466
|
+
|
|
467
|
+
{saveSuccess && (
|
|
468
|
+
<div className={styles.successMessage}>
|
|
469
|
+
Notes saved successfully!
|
|
470
|
+
</div>
|
|
471
|
+
)}
|
|
472
|
+
|
|
473
|
+
<button
|
|
474
|
+
onClick={onReturn}
|
|
475
|
+
className={styles.returnButton}
|
|
476
|
+
disabled={isUploading}
|
|
477
|
+
title={isUploading ? "Cannot return while uploading" : undefined}
|
|
478
|
+
>
|
|
479
|
+
Return to Case Management
|
|
480
|
+
</button>
|
|
481
|
+
<NotesModal
|
|
482
|
+
isOpen={isModalOpen}
|
|
483
|
+
onClose={() => setIsModalOpen(false)}
|
|
484
|
+
notes={additionalNotes}
|
|
485
|
+
onSave={setAdditionalNotes}
|
|
486
|
+
/>
|
|
487
|
+
</>
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
);
|
|
491
|
+
};
|