@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.
Files changed (223) hide show
  1. package/.env.example +100 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +18 -0
  4. package/README.md +133 -0
  5. package/app/components/actions/case-export/core-export.ts +328 -0
  6. package/app/components/actions/case-export/data-processing.ts +167 -0
  7. package/app/components/actions/case-export/download-handlers.ts +900 -0
  8. package/app/components/actions/case-export/index.ts +41 -0
  9. package/app/components/actions/case-export/metadata-helpers.ts +107 -0
  10. package/app/components/actions/case-export/types-constants.ts +56 -0
  11. package/app/components/actions/case-export/validation-utils.ts +25 -0
  12. package/app/components/actions/case-export.ts +4 -0
  13. package/app/components/actions/case-import/annotation-import.ts +35 -0
  14. package/app/components/actions/case-import/confirmation-import.ts +363 -0
  15. package/app/components/actions/case-import/image-operations.ts +61 -0
  16. package/app/components/actions/case-import/index.ts +39 -0
  17. package/app/components/actions/case-import/orchestrator.ts +420 -0
  18. package/app/components/actions/case-import/storage-operations.ts +270 -0
  19. package/app/components/actions/case-import/validation.ts +189 -0
  20. package/app/components/actions/case-import/zip-processing.ts +413 -0
  21. package/app/components/actions/case-manage.ts +524 -0
  22. package/app/components/actions/case-review.ts +4 -0
  23. package/app/components/actions/confirm-export.ts +351 -0
  24. package/app/components/actions/generate-pdf.ts +210 -0
  25. package/app/components/actions/image-manage.ts +385 -0
  26. package/app/components/actions/notes-manage.ts +33 -0
  27. package/app/components/actions/signout.module.css +15 -0
  28. package/app/components/actions/signout.tsx +50 -0
  29. package/app/components/audit/user-audit-viewer.tsx +975 -0
  30. package/app/components/audit/user-audit.module.css +568 -0
  31. package/app/components/auth/auth-provider.tsx +78 -0
  32. package/app/components/auth/mfa-enrollment.module.css +268 -0
  33. package/app/components/auth/mfa-enrollment.tsx +398 -0
  34. package/app/components/auth/mfa-verification.module.css +251 -0
  35. package/app/components/auth/mfa-verification.tsx +295 -0
  36. package/app/components/button/button.module.css +63 -0
  37. package/app/components/button/button.tsx +46 -0
  38. package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
  39. package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
  40. package/app/components/canvas/canvas.module.css +314 -0
  41. package/app/components/canvas/canvas.tsx +449 -0
  42. package/app/components/canvas/confirmation/confirmation.module.css +187 -0
  43. package/app/components/canvas/confirmation/confirmation.tsx +214 -0
  44. package/app/components/colors/colors.module.css +59 -0
  45. package/app/components/colors/colors.tsx +68 -0
  46. package/app/components/form/base-form.tsx +21 -0
  47. package/app/components/form/form-button.tsx +28 -0
  48. package/app/components/form/form-field.tsx +53 -0
  49. package/app/components/form/form-message.tsx +17 -0
  50. package/app/components/form/form-toggle.tsx +23 -0
  51. package/app/components/form/form.module.css +427 -0
  52. package/app/components/form/index.ts +6 -0
  53. package/app/components/icon/icon.module.css +3 -0
  54. package/app/components/icon/icon.tsx +27 -0
  55. package/app/components/icon/icons.svg +102 -0
  56. package/app/components/icon/manifest.json +110 -0
  57. package/app/components/sidebar/case-export/case-export.module.css +386 -0
  58. package/app/components/sidebar/case-export/case-export.tsx +317 -0
  59. package/app/components/sidebar/case-import/case-import.module.css +626 -0
  60. package/app/components/sidebar/case-import/case-import.tsx +404 -0
  61. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
  62. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
  63. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
  64. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
  65. package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
  66. package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
  67. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
  68. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
  69. package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
  70. package/app/components/sidebar/case-import/index.ts +18 -0
  71. package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
  72. package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
  73. package/app/components/sidebar/cases/cases-modal.module.css +166 -0
  74. package/app/components/sidebar/cases/cases-modal.tsx +201 -0
  75. package/app/components/sidebar/cases/cases.module.css +713 -0
  76. package/app/components/sidebar/files/files-modal.module.css +209 -0
  77. package/app/components/sidebar/files/files-modal.tsx +239 -0
  78. package/app/components/sidebar/hash/hash-utility.module.css +366 -0
  79. package/app/components/sidebar/hash/hash-utility.tsx +982 -0
  80. package/app/components/sidebar/notes/notes-modal.tsx +51 -0
  81. package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
  82. package/app/components/sidebar/notes/notes.module.css +360 -0
  83. package/app/components/sidebar/sidebar-container.tsx +149 -0
  84. package/app/components/sidebar/sidebar.module.css +321 -0
  85. package/app/components/sidebar/sidebar.tsx +215 -0
  86. package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
  87. package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
  88. package/app/components/theme-provider/theme-provider.tsx +131 -0
  89. package/app/components/theme-provider/theme.ts +155 -0
  90. package/app/components/toast/toast.module.css +137 -0
  91. package/app/components/toast/toast.tsx +56 -0
  92. package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
  93. package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
  94. package/app/components/toolbar/toolbar.module.css +42 -0
  95. package/app/components/toolbar/toolbar.tsx +167 -0
  96. package/app/components/user/delete-account.module.css +274 -0
  97. package/app/components/user/delete-account.tsx +471 -0
  98. package/app/components/user/inactivity-warning.module.css +145 -0
  99. package/app/components/user/inactivity-warning.tsx +84 -0
  100. package/app/components/user/manage-profile.module.css +190 -0
  101. package/app/components/user/manage-profile.tsx +253 -0
  102. package/app/components/user/mfa-phone-update.tsx +739 -0
  103. package/app/config-example/admin-service.json +13 -0
  104. package/app/config-example/config.json +17 -0
  105. package/app/config-example/firebase.ts +21 -0
  106. package/app/config-example/inactivity.ts +13 -0
  107. package/app/config-example/meta-config.json +6 -0
  108. package/app/contexts/auth.context.ts +12 -0
  109. package/app/entry.client.tsx +12 -0
  110. package/app/entry.server.tsx +44 -0
  111. package/app/hooks/useInactivityTimeout.ts +110 -0
  112. package/app/root.tsx +170 -0
  113. package/app/routes/_index.tsx +16 -0
  114. package/app/routes/auth/emailActionHandler.module.css +232 -0
  115. package/app/routes/auth/emailActionHandler.tsx +405 -0
  116. package/app/routes/auth/emailVerification.tsx +120 -0
  117. package/app/routes/auth/login.module.css +523 -0
  118. package/app/routes/auth/login.tsx +654 -0
  119. package/app/routes/auth/passwordReset.module.css +274 -0
  120. package/app/routes/auth/passwordReset.tsx +154 -0
  121. package/app/routes/auth/route.ts +16 -0
  122. package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
  123. package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
  124. package/app/routes/mobile-prevented/route.ts +14 -0
  125. package/app/routes/striae/striae.module.css +30 -0
  126. package/app/routes/striae/striae.tsx +417 -0
  127. package/app/services/audit-export.service.ts +755 -0
  128. package/app/services/audit.service.ts +1454 -0
  129. package/app/services/firebase-errors.ts +106 -0
  130. package/app/services/firebase.ts +15 -0
  131. package/app/styles/legal-pages.module.css +113 -0
  132. package/app/styles/root.module.css +146 -0
  133. package/app/tailwind.css +225 -0
  134. package/app/types/annotations.ts +45 -0
  135. package/app/types/audit.ts +301 -0
  136. package/app/types/case.ts +90 -0
  137. package/app/types/export.ts +8 -0
  138. package/app/types/file.ts +30 -0
  139. package/app/types/import.ts +107 -0
  140. package/app/types/index.ts +24 -0
  141. package/app/types/user.ts +38 -0
  142. package/app/utils/SHA256.ts +461 -0
  143. package/app/utils/annotation-timestamp.ts +25 -0
  144. package/app/utils/audit-export-signature.ts +117 -0
  145. package/app/utils/auth-action-settings.ts +48 -0
  146. package/app/utils/auth.ts +34 -0
  147. package/app/utils/batch-operations.ts +135 -0
  148. package/app/utils/confirmation-signature.ts +193 -0
  149. package/app/utils/data-operations.ts +871 -0
  150. package/app/utils/device-detection.ts +5 -0
  151. package/app/utils/html-sanitizer.ts +80 -0
  152. package/app/utils/id-generator.ts +36 -0
  153. package/app/utils/meta.ts +48 -0
  154. package/app/utils/mfa-phone.ts +97 -0
  155. package/app/utils/mfa.ts +79 -0
  156. package/app/utils/password-policy.ts +28 -0
  157. package/app/utils/permissions.ts +562 -0
  158. package/app/utils/signature-utils.ts +160 -0
  159. package/app/utils/style.ts +83 -0
  160. package/app/utils/version.ts +5 -0
  161. package/firebase.json +11 -0
  162. package/functions/[[path]].ts +10 -0
  163. package/package.json +138 -0
  164. package/postcss.config.js +6 -0
  165. package/public/.well-known/publickey.info@striae.org.asc +17 -0
  166. package/public/.well-known/security.txt +7 -0
  167. package/public/_headers +28 -0
  168. package/public/_routes.json +13 -0
  169. package/public/assets/striae.jpg +0 -0
  170. package/public/clear.jpg +0 -0
  171. package/public/favicon.ico +0 -0
  172. package/public/favicon.svg +9 -0
  173. package/public/icon-256.png +0 -0
  174. package/public/icon-512.png +0 -0
  175. package/public/logo-dark.png +0 -0
  176. package/public/manifest.json +25 -0
  177. package/public/oin-badge.png +0 -0
  178. package/public/shortcut.png +0 -0
  179. package/public/social-image.png +0 -0
  180. package/public/striae-ascii.txt +10 -0
  181. package/scripts/deploy-all.sh +100 -0
  182. package/scripts/deploy-config.sh +940 -0
  183. package/scripts/deploy-pages.sh +34 -0
  184. package/scripts/deploy-worker-secrets.sh +215 -0
  185. package/scripts/dev.cjs +23 -0
  186. package/scripts/install-workers.sh +88 -0
  187. package/scripts/run-eslint.cjs +35 -0
  188. package/scripts/update-compatibility-dates.cjs +124 -0
  189. package/scripts/update-markdown-versions.cjs +43 -0
  190. package/tailwind.config.ts +22 -0
  191. package/tsconfig.json +33 -0
  192. package/vite.config.ts +35 -0
  193. package/worker-configuration.d.ts +7490 -0
  194. package/workers/audit-worker/package.json +17 -0
  195. package/workers/audit-worker/src/audit-worker.example.ts +195 -0
  196. package/workers/audit-worker/worker-configuration.d.ts +7448 -0
  197. package/workers/audit-worker/wrangler.jsonc.example +29 -0
  198. package/workers/data-worker/package.json +17 -0
  199. package/workers/data-worker/src/data-worker.example.ts +267 -0
  200. package/workers/data-worker/src/signature-utils.ts +79 -0
  201. package/workers/data-worker/src/signing-payload-utils.ts +290 -0
  202. package/workers/data-worker/worker-configuration.d.ts +7448 -0
  203. package/workers/data-worker/wrangler.jsonc.example +30 -0
  204. package/workers/image-worker/package.json +17 -0
  205. package/workers/image-worker/src/image-worker.example.ts +180 -0
  206. package/workers/image-worker/worker-configuration.d.ts +7447 -0
  207. package/workers/image-worker/wrangler.jsonc.example +22 -0
  208. package/workers/keys-worker/package.json +17 -0
  209. package/workers/keys-worker/src/keys.example.ts +66 -0
  210. package/workers/keys-worker/src/keys.ts +66 -0
  211. package/workers/keys-worker/worker-configuration.d.ts +7447 -0
  212. package/workers/keys-worker/wrangler.jsonc.example +22 -0
  213. package/workers/pdf-worker/package.json +17 -0
  214. package/workers/pdf-worker/src/format-striae.ts +534 -0
  215. package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
  216. package/workers/pdf-worker/src/report-types.ts +69 -0
  217. package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
  218. package/workers/pdf-worker/wrangler.jsonc.example +26 -0
  219. package/workers/user-worker/package.json +17 -0
  220. package/workers/user-worker/src/user-worker.example.ts +636 -0
  221. package/workers/user-worker/worker-configuration.d.ts +7448 -0
  222. package/workers/user-worker/wrangler.jsonc.example +29 -0
  223. 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
+ };