@striae-org/striae 6.0.1 → 6.1.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.
Files changed (43) hide show
  1. package/app/components/actions/case-export/core-export.ts +11 -2
  2. package/app/components/actions/case-export/download-handlers.ts +3 -1
  3. package/app/components/canvas/canvas.module.css +1 -1
  4. package/app/components/canvas/canvas.tsx +32 -11
  5. package/app/components/colors/colors.module.css +19 -0
  6. package/app/components/colors/colors.tsx +5 -1
  7. package/app/components/icon/icons.svg +1 -1
  8. package/app/components/icon/manifest.json +1 -1
  9. package/app/components/navbar/navbar.tsx +32 -16
  10. package/app/components/sidebar/cases/case-sidebar.tsx +27 -25
  11. package/app/components/sidebar/files/files-modal.tsx +39 -15
  12. package/app/components/sidebar/notes/addl-notes-modal.tsx +9 -2
  13. package/app/components/sidebar/notes/{class-details/class-details-fields.tsx → item-details/item-details-fields.tsx} +10 -10
  14. package/app/components/sidebar/notes/{class-details/class-details-modal.tsx → item-details/item-details-modal.tsx} +20 -22
  15. package/app/components/sidebar/notes/{class-details/class-details-sections.tsx → item-details/item-details-sections.tsx} +16 -16
  16. package/app/components/sidebar/notes/{class-details/class-details-shared.ts → item-details/item-details-shared.ts} +4 -3
  17. package/app/components/sidebar/notes/{class-details/use-class-details-state.ts → item-details/use-item-details-state.ts} +4 -4
  18. package/app/components/sidebar/notes/notes-editor-form.tsx +357 -146
  19. package/app/components/sidebar/notes/notes-editor-modal.tsx +3 -0
  20. package/app/components/sidebar/notes/notes.module.css +40 -20
  21. package/app/components/sidebar/sidebar-container.tsx +1 -1
  22. package/app/components/sidebar/sidebar.tsx +3 -3
  23. package/app/components/toolbar/toolbar.tsx +5 -5
  24. package/app/hooks/useFileListPreferences.ts +22 -17
  25. package/app/routes/striae/striae.tsx +6 -13
  26. package/app/types/annotations.ts +29 -5
  27. package/app/utils/data/confirmation-summary/summary-core.ts +40 -8
  28. package/app/utils/data/file-filters.ts +39 -17
  29. package/app/utils/data/permissions.ts +123 -0
  30. package/package.json +12 -12
  31. package/workers/audit-worker/package.json +2 -2
  32. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  33. package/workers/data-worker/package.json +2 -2
  34. package/workers/data-worker/wrangler.jsonc.example +1 -1
  35. package/workers/image-worker/package.json +2 -2
  36. package/workers/image-worker/wrangler.jsonc.example +1 -1
  37. package/workers/pdf-worker/package.json +2 -2
  38. package/workers/pdf-worker/src/formats/format-striae.ts +65 -8
  39. package/workers/pdf-worker/src/report-types.ts +18 -4
  40. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  41. package/workers/user-worker/package.json +2 -2
  42. package/workers/user-worker/wrangler.jsonc.example +1 -1
  43. package/wrangler.toml.example +1 -1
@@ -2,10 +2,10 @@ import { useState, useEffect, useCallback, useLayoutEffect } from 'react';
2
2
  import type { User } from 'firebase/auth';
3
3
  import { ColorSelector } from '~/components/colors/colors';
4
4
  import { AddlNotesModal } from './addl-notes-modal';
5
- import { ClassDetailsModal } from './class-details/class-details-modal';
6
- import { buildClassDetailsSummary } from './class-details/class-details-shared';
5
+ import { ItemDetailsModal } from './item-details/item-details-modal';
6
+ import { buildItemDetailsSummary } from './item-details/item-details-shared';
7
7
  import { getNotes, saveNotes } from '~/components/actions/notes-manage';
8
- import { type AnnotationData, type BulletAnnotationData, type CartridgeCaseAnnotationData, type ShotshellAnnotationData } from '~/types/annotations';
8
+ import { type AnnotationData, type BulletAnnotationData, type CartridgeCaseAnnotationData, type ShotshellAnnotationData, type ItemType, type SupportLevel, type IndexType } from '~/types/annotations';
9
9
  import { resolveEarliestAnnotationTimestamp } from '~/utils/ui';
10
10
  import { auditService } from '~/services/audit';
11
11
  import styles from './notes.module.css';
@@ -17,33 +17,41 @@ interface NotesEditorFormProps {
17
17
  onAnnotationRefresh?: () => void;
18
18
  originalFileName?: string;
19
19
  isUploading?: boolean;
20
+ isReadOnly?: boolean;
20
21
  showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
21
22
  onDirtyChange?: (isDirty: boolean) => void;
22
23
  onRegisterSaveHandler?: (saveHandler: (() => Promise<boolean>) | null) => void;
23
24
  }
24
25
 
25
- type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
26
- type ClassType = 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
27
- type IndexType = 'number' | 'color';
28
-
29
26
  interface NotesFormSnapshot {
30
27
  leftCase: string;
31
28
  rightCase: string;
32
29
  leftItem: string;
33
30
  rightItem: string;
34
31
  caseFontColor: string;
35
- classType: ClassType | '';
36
- customClass: string;
37
- classNote: string;
38
- hasSubclass: boolean;
39
- bulletData: BulletAnnotationData | undefined;
40
- cartridgeCaseData: CartridgeCaseAnnotationData | undefined;
41
- shotshellData: ShotshellAnnotationData | undefined;
32
+ // Left item class characteristics
33
+ leftItemType: ItemType | '';
34
+ leftCustomClass: string;
35
+ leftClassNote: string;
36
+ leftHasSubclass: boolean;
37
+ leftBulletData: BulletAnnotationData | undefined;
38
+ leftCartridgeCaseData: CartridgeCaseAnnotationData | undefined;
39
+ leftShotshellData: ShotshellAnnotationData | undefined;
40
+ // Right item class characteristics
41
+ rightItemType: ItemType | '';
42
+ rightCustomClass: string;
43
+ rightClassNote: string;
44
+ rightHasSubclass: boolean;
45
+ rightBulletData: BulletAnnotationData | undefined;
46
+ rightCartridgeCaseData: CartridgeCaseAnnotationData | undefined;
47
+ rightShotshellData: ShotshellAnnotationData | undefined;
42
48
  indexType: IndexType;
43
49
  indexNumber: string;
44
50
  indexColor: string;
45
51
  supportLevel: SupportLevel | '';
46
52
  includeConfirmation: boolean;
53
+ leftAdditionalNotes: string;
54
+ rightAdditionalNotes: string;
47
55
  additionalNotes: string;
48
56
  }
49
57
 
@@ -73,16 +81,19 @@ const normalizeNestedAnnotationData = <T extends object>(data: T | undefined): T
73
81
 
74
82
  const normalizeNotesSnapshot = (snapshot: NotesFormSnapshot): NotesFormSnapshot => ({
75
83
  ...snapshot,
76
- bulletData: normalizeNestedAnnotationData(snapshot.bulletData),
77
- cartridgeCaseData: normalizeNestedAnnotationData(snapshot.cartridgeCaseData),
78
- shotshellData: normalizeNestedAnnotationData(snapshot.shotshellData),
84
+ leftBulletData: normalizeNestedAnnotationData(snapshot.leftBulletData),
85
+ leftCartridgeCaseData: normalizeNestedAnnotationData(snapshot.leftCartridgeCaseData),
86
+ leftShotshellData: normalizeNestedAnnotationData(snapshot.leftShotshellData),
87
+ rightBulletData: normalizeNestedAnnotationData(snapshot.rightBulletData),
88
+ rightCartridgeCaseData: normalizeNestedAnnotationData(snapshot.rightCartridgeCaseData),
89
+ rightShotshellData: normalizeNestedAnnotationData(snapshot.rightShotshellData),
79
90
  });
80
91
 
81
92
  const serializeNotesSnapshot = (snapshot: NotesFormSnapshot): string => JSON.stringify(normalizeNotesSnapshot(snapshot));
82
93
  const DIRTY_CHECK_DEBOUNCE_MS = 180;
83
94
  const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
84
95
 
85
- export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, showNotification: externalShowNotification, onDirtyChange, onRegisterSaveHandler }: NotesEditorFormProps) => {
96
+ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, isReadOnly = false, showNotification: externalShowNotification, onDirtyChange, onRegisterSaveHandler }: NotesEditorFormProps) => {
86
97
  // Loading/Saving Notes States
87
98
  const [isLoading, setIsLoading] = useState(false);
88
99
  const [loadError, setLoadError] = useState<string>();
@@ -96,14 +107,27 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
96
107
  const [useCurrentCaseRight, setUseCurrentCaseRight] = useState(false);
97
108
  const [caseFontColor, setCaseFontColor] = useState('');
98
109
 
99
- // Class characteristics state
100
- const [classType, setClassType] = useState<ClassType | ''>('');
101
- const [customClass, setCustomClass] = useState('');
102
- const [classNote, setClassNote] = useState('');
103
- const [hasSubclass, setHasSubclass] = useState(false);
104
- const [bulletData, setBulletData] = useState<BulletAnnotationData | undefined>(undefined);
105
- const [cartridgeCaseData, setCartridgeCaseData] = useState<CartridgeCaseAnnotationData | undefined>(undefined);
106
- const [shotshellData, setShotshellData] = useState<ShotshellAnnotationData | undefined>(undefined);
110
+ // Class characteristics state - selected item indicator
111
+ const [selectedItem, setSelectedItem] = useState<'left' | 'right'>('left');
112
+
113
+ // Left item class characteristics state
114
+ const [leftItemType, setLeftItemType] = useState<ItemType | ''>('');
115
+ const [leftCustomClass, setLeftCustomClass] = useState('');
116
+ const [leftClassNote, setLeftClassNote] = useState('');
117
+ const [leftHasSubclass, setLeftHasSubclass] = useState(false);
118
+ const [leftBulletData, setLeftBulletData] = useState<BulletAnnotationData | undefined>(undefined);
119
+ const [leftCartridgeCaseData, setLeftCartridgeCaseData] = useState<CartridgeCaseAnnotationData | undefined>(undefined);
120
+ const [leftShotshellData, setLeftShotshellData] = useState<ShotshellAnnotationData | undefined>(undefined);
121
+
122
+ // Right item class characteristics state
123
+ const [rightItemType, setRightItemType] = useState<ItemType | ''>('');
124
+ const [rightCustomClass, setRightCustomClass] = useState('');
125
+ const [rightClassNote, setRightClassNote] = useState('');
126
+ const [rightHasSubclass, setRightHasSubclass] = useState(false);
127
+ const [rightBulletData, setRightBulletData] = useState<BulletAnnotationData | undefined>(undefined);
128
+ const [rightCartridgeCaseData, setRightCartridgeCaseData] = useState<CartridgeCaseAnnotationData | undefined>(undefined);
129
+ const [rightShotshellData, setRightShotshellData] = useState<ShotshellAnnotationData | undefined>(undefined);
130
+
107
131
  const [isClassDetailsOpen, setIsClassDetailsOpen] = useState(false);
108
132
 
109
133
  // Index state
@@ -117,6 +141,8 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
117
141
 
118
142
  // Additional Notes Modal
119
143
  const [isModalOpen, setIsModalOpen] = useState(false);
144
+ const [leftAdditionalNotes, setLeftAdditionalNotes] = useState('');
145
+ const [rightAdditionalNotes, setRightAdditionalNotes] = useState('');
120
146
  const [additionalNotes, setAdditionalNotes] = useState('');
121
147
  const [isCaseInfoOpen, setIsCaseInfoOpen] = useState(true);
122
148
  const [isClassOpen, setIsClassOpen] = useState(true);
@@ -125,7 +151,9 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
125
151
  const [savedSnapshot, setSavedSnapshot] = useState<string>('');
126
152
  const [hasLoadedSnapshot, setHasLoadedSnapshot] = useState(false);
127
153
  const [isDirty, setIsDirty] = useState(false);
128
- const areInputsDisabled = isUploading || isConfirmedImage;
154
+ const areEditsDisabled = isUploading || isReadOnly || isConfirmedImage;
155
+ const isReadOnlyMode = isConfirmedImage || isReadOnly;
156
+ const canOpenModals = !isUploading;
129
157
 
130
158
  const notificationHandler = useCallback((message: string, type: 'success' | 'error' | 'warning' = 'success') => {
131
159
  if (externalShowNotification) {
@@ -133,6 +161,58 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
133
161
  }
134
162
  }, [externalShowNotification]);
135
163
 
164
+ // Helper functions for selected item data access
165
+ const getSelectedItemData = useCallback(() => {
166
+ if (selectedItem === 'left') {
167
+ return {
168
+ itemType: leftItemType,
169
+ customClass: leftCustomClass,
170
+ classNote: leftClassNote,
171
+ hasSubclass: leftHasSubclass,
172
+ bulletData: leftBulletData,
173
+ cartridgeCaseData: leftCartridgeCaseData,
174
+ shotshellData: leftShotshellData,
175
+ };
176
+ }
177
+ return {
178
+ itemType: rightItemType,
179
+ customClass: rightCustomClass,
180
+ classNote: rightClassNote,
181
+ hasSubclass: rightHasSubclass,
182
+ bulletData: rightBulletData,
183
+ cartridgeCaseData: rightCartridgeCaseData,
184
+ shotshellData: rightShotshellData,
185
+ };
186
+ }, [selectedItem, leftItemType, leftCustomClass, leftClassNote, leftHasSubclass, leftBulletData, leftCartridgeCaseData, leftShotshellData, rightItemType, rightCustomClass, rightClassNote, rightHasSubclass, rightBulletData, rightCartridgeCaseData, rightShotshellData]);
187
+
188
+ const setSelectedItemData = useCallback((newData: {
189
+ itemType?: ItemType | '';
190
+ customClass?: string;
191
+ classNote?: string;
192
+ hasSubclass?: boolean;
193
+ bulletData?: BulletAnnotationData;
194
+ cartridgeCaseData?: CartridgeCaseAnnotationData;
195
+ shotshellData?: ShotshellAnnotationData;
196
+ }) => {
197
+ if (selectedItem === 'left') {
198
+ if (newData.itemType !== undefined) setLeftItemType(newData.itemType);
199
+ if (newData.customClass !== undefined) setLeftCustomClass(newData.customClass);
200
+ if (newData.classNote !== undefined) setLeftClassNote(newData.classNote);
201
+ if (newData.hasSubclass !== undefined) setLeftHasSubclass(newData.hasSubclass);
202
+ if (newData.bulletData !== undefined) setLeftBulletData(newData.bulletData);
203
+ if (newData.cartridgeCaseData !== undefined) setLeftCartridgeCaseData(newData.cartridgeCaseData);
204
+ if (newData.shotshellData !== undefined) setLeftShotshellData(newData.shotshellData);
205
+ } else {
206
+ if (newData.itemType !== undefined) setRightItemType(newData.itemType);
207
+ if (newData.customClass !== undefined) setRightCustomClass(newData.customClass);
208
+ if (newData.classNote !== undefined) setRightClassNote(newData.classNote);
209
+ if (newData.hasSubclass !== undefined) setRightHasSubclass(newData.hasSubclass);
210
+ if (newData.bulletData !== undefined) setRightBulletData(newData.bulletData);
211
+ if (newData.cartridgeCaseData !== undefined) setRightCartridgeCaseData(newData.cartridgeCaseData);
212
+ if (newData.shotshellData !== undefined) setRightShotshellData(newData.shotshellData);
213
+ }
214
+ }, [selectedItem]);
215
+
136
216
  useEffect(() => {
137
217
  if (!hasLoadedSnapshot) {
138
218
  return;
@@ -145,18 +225,27 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
145
225
  leftItem,
146
226
  rightItem,
147
227
  caseFontColor,
148
- classType,
149
- customClass,
150
- classNote,
151
- hasSubclass,
152
- bulletData,
153
- cartridgeCaseData,
154
- shotshellData,
228
+ leftItemType,
229
+ leftCustomClass,
230
+ leftClassNote,
231
+ leftHasSubclass,
232
+ leftBulletData,
233
+ leftCartridgeCaseData,
234
+ leftShotshellData,
235
+ rightItemType,
236
+ rightCustomClass,
237
+ rightClassNote,
238
+ rightHasSubclass,
239
+ rightBulletData,
240
+ rightCartridgeCaseData,
241
+ rightShotshellData,
155
242
  indexType,
156
243
  indexNumber,
157
244
  indexColor,
158
245
  supportLevel,
159
246
  includeConfirmation,
247
+ leftAdditionalNotes,
248
+ rightAdditionalNotes,
160
249
  additionalNotes,
161
250
  });
162
251
 
@@ -168,24 +257,33 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
168
257
  };
169
258
  }, [
170
259
  additionalNotes,
171
- bulletData,
172
- cartridgeCaseData,
173
- caseFontColor,
174
- classNote,
175
- classType,
176
- customClass,
177
260
  hasLoadedSnapshot,
178
- hasSubclass,
179
261
  includeConfirmation,
180
262
  indexColor,
181
263
  indexNumber,
182
264
  indexType,
265
+ leftBulletData,
266
+ leftCartridgeCaseData,
183
267
  leftCase,
268
+ leftClassNote,
269
+ leftCustomClass,
270
+ leftHasSubclass,
271
+ leftItemType,
184
272
  leftItem,
273
+ leftShotshellData,
274
+ rightBulletData,
275
+ rightCartridgeCaseData,
185
276
  rightCase,
277
+ rightClassNote,
278
+ rightCustomClass,
279
+ rightHasSubclass,
280
+ rightItemType,
186
281
  rightItem,
282
+ rightShotshellData,
283
+ caseFontColor,
187
284
  savedSnapshot,
188
- shotshellData,
285
+ leftAdditionalNotes,
286
+ rightAdditionalNotes,
189
287
  supportLevel,
190
288
  ]);
191
289
 
@@ -213,19 +311,42 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
213
311
  setLeftItem(existingNotes.leftItem);
214
312
  setRightItem(existingNotes.rightItem);
215
313
  setCaseFontColor(existingNotes.caseFontColor || '');
216
- setClassType(existingNotes.classType || '');
217
- setCustomClass(existingNotes.customClass || '');
218
- setClassNote(existingNotes.classNote || '');
219
- setHasSubclass(existingNotes.hasSubclass ?? false);
220
- setBulletData(existingNotes.bulletData);
221
- setCartridgeCaseData(existingNotes.cartridgeCaseData);
222
- setShotshellData(existingNotes.shotshellData);
314
+
315
+ // Migration: if old single-set fields exist, map to left item; otherwise use new left/right fields
316
+ const migratedLeftItemType = existingNotes.leftItemType || existingNotes.itemType || (existingNotes.classType as ItemType | undefined) || '';
317
+ const migratedLeftCustomClass = existingNotes.leftCustomClass || existingNotes.customClass || '';
318
+ const migratedLeftClassNote = existingNotes.leftClassNote || existingNotes.classNote || '';
319
+ const migratedLeftHasSubclass = existingNotes.leftHasSubclass ?? existingNotes.hasSubclass ?? false;
320
+ const migratedLeftBulletData = existingNotes.leftBulletData || existingNotes.bulletData;
321
+ const migratedLeftCartridgeCaseData = existingNotes.leftCartridgeCaseData || existingNotes.cartridgeCaseData;
322
+ const migratedLeftShotshellData = existingNotes.leftShotshellData || existingNotes.shotshellData;
323
+
324
+ setLeftItemType(migratedLeftItemType);
325
+ setLeftCustomClass(migratedLeftCustomClass);
326
+ setLeftClassNote(migratedLeftClassNote);
327
+ setLeftHasSubclass(migratedLeftHasSubclass);
328
+ setLeftBulletData(migratedLeftBulletData);
329
+ setLeftCartridgeCaseData(migratedLeftCartridgeCaseData);
330
+ setLeftShotshellData(migratedLeftShotshellData);
331
+
332
+ // Set right item fields (new structure)
333
+ setRightItemType(existingNotes.rightItemType || existingNotes.itemType || (existingNotes.classType as ItemType | undefined) || '');
334
+ setRightCustomClass(existingNotes.rightCustomClass || '');
335
+ setRightClassNote(existingNotes.rightClassNote || '');
336
+ setRightHasSubclass(existingNotes.rightHasSubclass ?? false);
337
+ setRightBulletData(existingNotes.rightBulletData);
338
+ setRightCartridgeCaseData(existingNotes.rightCartridgeCaseData);
339
+ setRightShotshellData(existingNotes.rightShotshellData);
340
+
223
341
  setIndexType(existingNotes.indexType || 'color');
224
342
  setIndexNumber(existingNotes.indexNumber || '');
225
343
  setIndexColor(existingNotes.indexColor || '');
226
344
  setSupportLevel(existingNotes.supportLevel || '');
227
345
  setIncludeConfirmation(existingNotes.includeConfirmation);
346
+ setLeftAdditionalNotes(existingNotes.leftAdditionalNotes || '');
347
+ setRightAdditionalNotes(existingNotes.rightAdditionalNotes || '');
228
348
  setAdditionalNotes(existingNotes.additionalNotes || '');
349
+ setSelectedItem('left'); // Always default to left item
229
350
 
230
351
  setSavedSnapshot(serializeNotesSnapshot({
231
352
  leftCase: existingNotes.leftCase || '',
@@ -233,18 +354,27 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
233
354
  leftItem: existingNotes.leftItem || '',
234
355
  rightItem: existingNotes.rightItem || '',
235
356
  caseFontColor: existingNotes.caseFontColor || '',
236
- classType: existingNotes.classType || '',
237
- customClass: existingNotes.customClass || '',
238
- classNote: existingNotes.classNote || '',
239
- hasSubclass: existingNotes.hasSubclass ?? false,
240
- bulletData: existingNotes.bulletData,
241
- cartridgeCaseData: existingNotes.cartridgeCaseData,
242
- shotshellData: existingNotes.shotshellData,
357
+ leftItemType: migratedLeftItemType,
358
+ leftCustomClass: migratedLeftCustomClass,
359
+ leftClassNote: migratedLeftClassNote,
360
+ leftHasSubclass: migratedLeftHasSubclass,
361
+ leftBulletData: migratedLeftBulletData,
362
+ leftCartridgeCaseData: migratedLeftCartridgeCaseData,
363
+ leftShotshellData: migratedLeftShotshellData,
364
+ rightItemType: existingNotes.rightItemType || '',
365
+ rightCustomClass: existingNotes.rightCustomClass || '',
366
+ rightClassNote: existingNotes.rightClassNote || '',
367
+ rightHasSubclass: existingNotes.rightHasSubclass ?? false,
368
+ rightBulletData: existingNotes.rightBulletData,
369
+ rightCartridgeCaseData: existingNotes.rightCartridgeCaseData,
370
+ rightShotshellData: existingNotes.rightShotshellData,
243
371
  indexType: existingNotes.indexType || 'color',
244
372
  indexNumber: existingNotes.indexNumber || '',
245
373
  indexColor: existingNotes.indexColor || '',
246
374
  supportLevel: existingNotes.supportLevel || '',
247
375
  includeConfirmation: existingNotes.includeConfirmation,
376
+ leftAdditionalNotes: existingNotes.leftAdditionalNotes || '',
377
+ rightAdditionalNotes: existingNotes.rightAdditionalNotes || '',
248
378
  additionalNotes: existingNotes.additionalNotes || ''
249
379
  }));
250
380
  } else {
@@ -256,18 +386,27 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
256
386
  leftItem: '',
257
387
  rightItem: '',
258
388
  caseFontColor: '',
259
- classType: '',
260
- customClass: '',
261
- classNote: '',
262
- hasSubclass: false,
263
- bulletData: undefined,
264
- cartridgeCaseData: undefined,
265
- shotshellData: undefined,
389
+ leftItemType: '',
390
+ leftCustomClass: '',
391
+ leftClassNote: '',
392
+ leftHasSubclass: false,
393
+ leftBulletData: undefined,
394
+ leftCartridgeCaseData: undefined,
395
+ leftShotshellData: undefined,
396
+ rightItemType: '',
397
+ rightCustomClass: '',
398
+ rightClassNote: '',
399
+ rightHasSubclass: false,
400
+ rightBulletData: undefined,
401
+ rightCartridgeCaseData: undefined,
402
+ rightShotshellData: undefined,
266
403
  indexType: 'color',
267
404
  indexNumber: '',
268
405
  indexColor: '',
269
406
  supportLevel: '',
270
407
  includeConfirmation: false,
408
+ leftAdditionalNotes: '',
409
+ rightAdditionalNotes: '',
271
410
  additionalNotes: ''
272
411
  }));
273
412
  }
@@ -303,6 +442,11 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
303
442
  return false;
304
443
  }
305
444
 
445
+ if (isReadOnly) {
446
+ notificationHandler('This case is read-only. Notes cannot be modified.', 'error');
447
+ return false;
448
+ }
449
+
306
450
  let existingData: AnnotationData | null = null;
307
451
 
308
452
  try {
@@ -315,9 +459,12 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
315
459
  return false;
316
460
  }
317
461
 
318
- const normalizedBulletData = normalizeNestedAnnotationData(bulletData);
319
- const normalizedCartridgeCaseData = normalizeNestedAnnotationData(cartridgeCaseData);
320
- const normalizedShotshellData = normalizeNestedAnnotationData(shotshellData);
462
+ const normalizedLeftBulletData = normalizeNestedAnnotationData(leftBulletData);
463
+ const normalizedLeftCartridgeCaseData = normalizeNestedAnnotationData(leftCartridgeCaseData);
464
+ const normalizedLeftShotshellData = normalizeNestedAnnotationData(leftShotshellData);
465
+ const normalizedRightBulletData = normalizeNestedAnnotationData(rightBulletData);
466
+ const normalizedRightCartridgeCaseData = normalizeNestedAnnotationData(rightCartridgeCaseData);
467
+ const normalizedRightShotshellData = normalizeNestedAnnotationData(rightShotshellData);
321
468
 
322
469
  // Create updated annotation data, preserving box annotations and earliest timestamp
323
470
  const now = new Date().toISOString();
@@ -329,14 +476,23 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
329
476
  rightItem: rightItem || '',
330
477
  caseFontColor: caseFontColor || undefined,
331
478
 
332
- // Class Characteristics
333
- classType: classType as ClassType || undefined,
334
- customClass: customClass,
335
- classNote: classNote || undefined,
336
- hasSubclass: hasSubclass,
337
- bulletData: normalizedBulletData,
338
- cartridgeCaseData: normalizedCartridgeCaseData,
339
- shotshellData: normalizedShotshellData,
479
+ // Left item class characteristics
480
+ leftItemType: leftItemType as ItemType || undefined,
481
+ leftCustomClass: leftCustomClass,
482
+ leftClassNote: leftClassNote || undefined,
483
+ leftHasSubclass: leftHasSubclass,
484
+ leftBulletData: normalizedLeftBulletData,
485
+ leftCartridgeCaseData: normalizedLeftCartridgeCaseData,
486
+ leftShotshellData: normalizedLeftShotshellData,
487
+
488
+ // Right item class characteristics
489
+ rightItemType: rightItemType as ItemType || undefined,
490
+ rightCustomClass: rightCustomClass,
491
+ rightClassNote: rightClassNote || undefined,
492
+ rightHasSubclass: rightHasSubclass,
493
+ rightBulletData: normalizedRightBulletData,
494
+ rightCartridgeCaseData: normalizedRightCartridgeCaseData,
495
+ rightShotshellData: normalizedRightShotshellData,
340
496
 
341
497
  // Index Information
342
498
  indexType: indexType,
@@ -348,7 +504,9 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
348
504
  includeConfirmation: includeConfirmation,
349
505
 
350
506
  // Additional Notes
351
- additionalNotes: additionalNotes || undefined, // Keep as optional
507
+ leftAdditionalNotes: leftAdditionalNotes || undefined,
508
+ rightAdditionalNotes: rightAdditionalNotes || undefined,
509
+ additionalNotes: additionalNotes || undefined, // General notes (including box-annotation notes)
352
510
 
353
511
  // Preserve existing box annotations
354
512
  boxAnnotations: existingData?.boxAnnotations || [],
@@ -385,18 +543,27 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
385
543
  leftItem,
386
544
  rightItem,
387
545
  caseFontColor,
388
- classType,
389
- customClass,
390
- classNote,
391
- hasSubclass,
392
- bulletData,
393
- cartridgeCaseData,
394
- shotshellData,
546
+ leftItemType,
547
+ leftCustomClass,
548
+ leftClassNote,
549
+ leftHasSubclass,
550
+ leftBulletData,
551
+ leftCartridgeCaseData,
552
+ leftShotshellData,
553
+ rightItemType,
554
+ rightCustomClass,
555
+ rightClassNote,
556
+ rightHasSubclass,
557
+ rightBulletData,
558
+ rightCartridgeCaseData,
559
+ rightShotshellData,
395
560
  indexType,
396
561
  indexNumber,
397
562
  indexColor,
398
563
  supportLevel,
399
564
  includeConfirmation,
565
+ leftAdditionalNotes,
566
+ rightAdditionalNotes,
400
567
  additionalNotes,
401
568
  }));
402
569
  setIsDirty(false);
@@ -438,28 +605,38 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
438
605
  }
439
606
  }, [
440
607
  additionalNotes,
441
- bulletData,
442
- cartridgeCaseData,
608
+ leftBulletData,
609
+ leftCartridgeCaseData,
443
610
  caseFontColor,
444
- classNote,
445
- classType,
611
+ leftClassNote,
612
+ leftItemType,
446
613
  currentCase,
447
- customClass,
448
- hasSubclass,
614
+ leftCustomClass,
615
+ leftHasSubclass,
449
616
  imageId,
450
617
  includeConfirmation,
451
618
  indexColor,
452
619
  indexNumber,
453
620
  indexType,
621
+ isReadOnly,
454
622
  leftCase,
455
623
  leftItem,
456
624
  notificationHandler,
457
625
  onAnnotationRefresh,
458
626
  onDirtyChange,
459
627
  originalFileName,
628
+ rightBulletData,
629
+ rightCartridgeCaseData,
460
630
  rightCase,
631
+ rightClassNote,
632
+ rightCustomClass,
633
+ rightHasSubclass,
634
+ rightItemType,
461
635
  rightItem,
462
- shotshellData,
636
+ rightShotshellData,
637
+ leftShotshellData,
638
+ leftAdditionalNotes,
639
+ rightAdditionalNotes,
463
640
  supportLevel,
464
641
  user,
465
642
  ]);
@@ -509,7 +686,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
509
686
  type="text"
510
687
  value={leftCase}
511
688
  onChange={(e) => setLeftCase(e.target.value)}
512
- disabled={useCurrentCaseLeft || areInputsDisabled}
689
+ disabled={useCurrentCaseLeft || areEditsDisabled}
513
690
  />
514
691
  </div>
515
692
  <label className={`${styles.checkboxLabel} mb-4`}>
@@ -518,7 +695,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
518
695
  checked={useCurrentCaseLeft}
519
696
  onChange={(e) => setUseCurrentCaseLeft(e.target.checked)}
520
697
  className={styles.checkbox}
521
- disabled={areInputsDisabled}
698
+ disabled={areEditsDisabled}
522
699
  />
523
700
  <span>Use current case number</span>
524
701
  </label>
@@ -529,7 +706,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
529
706
  type="text"
530
707
  value={leftItem}
531
708
  onChange={(e) => setLeftItem(e.target.value)}
532
- disabled={areInputsDisabled}
709
+ disabled={areEditsDisabled}
533
710
  />
534
711
  </div>
535
712
  </div>
@@ -542,7 +719,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
542
719
  type="text"
543
720
  value={rightCase}
544
721
  onChange={(e) => setRightCase(e.target.value)}
545
- disabled={useCurrentCaseRight || areInputsDisabled}
722
+ disabled={useCurrentCaseRight || areEditsDisabled}
546
723
  />
547
724
  </div>
548
725
  <label className={`${styles.checkboxLabel} mb-4`}>
@@ -551,7 +728,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
551
728
  checked={useCurrentCaseRight}
552
729
  onChange={(e) => setUseCurrentCaseRight(e.target.checked)}
553
730
  className={styles.checkbox}
554
- disabled={areInputsDisabled}
731
+ disabled={areEditsDisabled}
555
732
  />
556
733
  <span>Use current case number</span>
557
734
  </label>
@@ -562,17 +739,18 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
562
739
  type="text"
563
740
  value={rightItem}
564
741
  onChange={(e) => setRightItem(e.target.value)}
565
- disabled={areInputsDisabled}
742
+ disabled={areEditsDisabled}
566
743
  />
567
744
  </div>
568
745
  </div>
569
746
  </div>
570
747
  <hr />
571
748
  <div className={styles.fontColorRow}>
572
- <label htmlFor="colorSelect">Font</label>
749
+ <label htmlFor="colorSelect">Case & Item Font Color</label>
573
750
  <ColorSelector
574
751
  selectedColor={caseFontColor}
575
752
  onColorSelect={setCaseFontColor}
753
+ disabled={areEditsDisabled}
576
754
  />
577
755
  </div>
578
756
  </>
@@ -587,68 +765,83 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
587
765
  onClick={() => setIsClassOpen((prev) => !prev)}
588
766
  aria-expanded={isClassOpen}
589
767
  >
590
- <span className={styles.sectionTitle}>Class Characteristics</span>
768
+ <span className={styles.sectionTitle}>Class Characteristics & GRC</span>
591
769
  <span className={styles.sectionToggleIcon}>{isClassOpen ? '−' : '+'}</span>
592
770
  </button>
593
771
  {isClassOpen && (
594
772
  <>
773
+ <hr />
774
+ <div className={styles.itemSelectorRow}>
775
+ <label htmlFor="itemSelector">Select Item</label>
776
+ <select
777
+ id="itemSelector"
778
+ aria-label="Select item to edit"
779
+ value={selectedItem}
780
+ onChange={(e) => setSelectedItem(e.target.value as 'left' | 'right')}
781
+ className={styles.select}
782
+ >
783
+ <option value="left">{`Case: ${leftCase || '—'} Item: ${leftItem || '—'}`}</option>
784
+ <option value="right" disabled={!rightItem && !rightCase}>
785
+ {`Case: ${rightCase || '—'} Item: ${rightItem || '—'}`}
786
+ </option>
787
+ </select>
788
+ </div>
595
789
  <div className={styles.classCharacteristicsColumns}>
596
790
  <div className={styles.classCharacteristicsMain}>
597
791
  <div className={styles.classCharacteristics}>
598
792
  <select
599
- id="classType"
600
- aria-label="Class Type"
601
- value={classType}
602
- onChange={(e) => setClassType(e.target.value as ClassType)}
793
+ id="itemType"
794
+ aria-label="Item Type"
795
+ value={getSelectedItemData().itemType}
796
+ onChange={(e) => setSelectedItemData({ itemType: e.target.value as ItemType })}
603
797
  className={styles.select}
604
- disabled={areInputsDisabled}
798
+ disabled={areEditsDisabled}
605
799
  >
606
- <option value="">Select class type...</option>
800
+ <option value="">Select item type...</option>
607
801
  <option value="Bullet">Bullet</option>
608
802
  <option value="Cartridge Case">Cartridge Case</option>
609
803
  <option value="Shotshell">Shotshell</option>
610
804
  <option value="Other">Other</option>
611
805
  </select>
612
806
 
613
- {classType === 'Other' && (
807
+ {getSelectedItemData().itemType === 'Other' && (
614
808
  <input
615
809
  type="text"
616
- value={customClass}
617
- onChange={(e) => setCustomClass(e.target.value)}
810
+ value={getSelectedItemData().customClass}
811
+ onChange={(e) => setSelectedItemData({ customClass: e.target.value })}
618
812
  placeholder="Specify object type"
619
- disabled={areInputsDisabled}
813
+ disabled={areEditsDisabled}
620
814
  />
621
815
  )}
622
816
 
623
817
  <textarea
624
- value={classNote}
625
- onChange={(e) => setClassNote(e.target.value)}
626
- placeholder="Enter class characteristic details..."
818
+ value={getSelectedItemData().classNote}
819
+ onChange={(e) => setSelectedItemData({ classNote: e.target.value })}
820
+ placeholder="Enter item details..."
627
821
  className={styles.textarea}
628
- disabled={areInputsDisabled}
822
+ disabled={areEditsDisabled}
629
823
  />
630
824
  </div>
631
- <label className={`${styles.checkboxLabel} mb-4`}>
632
- <input
633
- type="checkbox"
634
- checked={hasSubclass}
635
- onChange={(e) => setHasSubclass(e.target.checked)}
636
- className={styles.checkbox}
637
- disabled={areInputsDisabled}
638
- />
639
- <span>Potential subclass?</span>
640
- </label>
641
825
  </div>
642
826
 
643
- <div className={styles.classDetailsPanel}>
827
+ <div className={styles.itemDetailsPanel}>
644
828
  <button
645
829
  type="button"
646
830
  onClick={() => setIsClassDetailsOpen(true)}
647
- className={styles.classDetailsButton}
648
- disabled={areInputsDisabled}
831
+ className={styles.itemDetailsButton}
649
832
  >
650
- Enter Class Characteristic Details
833
+ Class Characteristics & GRC
651
834
  </button>
835
+ <label className={`${styles.checkboxLabel} mb-4`}>
836
+ <input
837
+ type="checkbox"
838
+ checked={getSelectedItemData().hasSubclass}
839
+ onChange={(e) => setSelectedItemData({ hasSubclass: e.target.checked })}
840
+ className={styles.checkbox}
841
+ disabled={areEditsDisabled}
842
+ />
843
+ <span>Potential subclass?</span>
844
+ </label>
652
845
  </div>
653
846
  </div>
654
847
  </>
@@ -673,7 +866,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
673
866
  type="radio"
674
867
  checked={indexType === 'color'}
675
868
  onChange={() => setIndexType('color')}
676
- disabled={areInputsDisabled}
869
+ disabled={areEditsDisabled}
677
870
  />
678
871
  <span>Color</span>
679
872
  </label>
@@ -682,7 +875,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
682
875
  type="radio"
683
876
  checked={indexType === 'number'}
684
877
  onChange={() => setIndexType('number')}
685
- disabled={areInputsDisabled}
878
+ disabled={areEditsDisabled}
686
879
  />
687
880
  <span>Number/Letter</span>
688
881
  </label>
@@ -694,12 +887,13 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
694
887
  value={indexNumber}
695
888
  onChange={(e) => setIndexNumber(e.target.value)}
696
889
  placeholder="Enter index number"
697
- disabled={areInputsDisabled}
890
+ disabled={areEditsDisabled}
698
891
  />
699
892
  ) : indexType === 'color' ? (
700
893
  <ColorSelector
701
894
  selectedColor={indexColor}
702
895
  onColorSelect={setIndexColor}
896
+ disabled={areEditsDisabled}
703
897
  />
704
898
  ) : null}
705
899
  </div>
@@ -733,7 +927,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
733
927
  }
734
928
  }}
735
929
  className={styles.select}
736
- disabled={areInputsDisabled}
930
+ disabled={areEditsDisabled}
737
931
  >
738
932
  <option value="">Select support level...</option>
739
933
  <option value="ID">Identification</option>
@@ -746,7 +940,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
746
940
  checked={includeConfirmation}
747
941
  onChange={(e) => setIncludeConfirmation(e.target.checked)}
748
942
  className={styles.checkbox}
749
- disabled={areInputsDisabled}
943
+ disabled={areEditsDisabled}
750
944
  />
751
945
  <span>Include confirmation field</span>
752
946
  </label>
@@ -760,8 +954,8 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
760
954
  <button
761
955
  onClick={() => setIsModalOpen(true)}
762
956
  className={styles.notesButton}
763
- disabled={areInputsDisabled}
764
- title={isConfirmedImage ? "Cannot edit notes for confirmed images" : isUploading ? "Cannot add notes while uploading" : undefined}
957
+ disabled={!canOpenModals}
958
+ title={isUploading ? "Cannot open notes while uploading" : undefined}
765
959
  >
766
960
  Additional Notes
767
961
  </button>
@@ -771,8 +965,8 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
771
965
  <button
772
966
  onClick={handleSave}
773
967
  className={styles.saveButton}
774
- disabled={areInputsDisabled}
775
- title={isConfirmedImage ? "Cannot save notes for confirmed images" : isUploading ? "Cannot save notes while uploading" : undefined}
968
+ disabled={areEditsDisabled}
969
+ title={isConfirmedImage ? "Cannot save notes - image is confirmed" : isUploading ? "Cannot save notes while uploading" : isReadOnly ? "Cannot save notes - case is read-only" : undefined}
776
970
  >
777
971
  Save Notes
778
972
  </button>
@@ -781,27 +975,44 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
781
975
  isOpen={isModalOpen}
782
976
  onClose={() => setIsModalOpen(false)}
783
977
  notes={additionalNotes}
784
- onSave={setAdditionalNotes}
978
+ onSave={(notes) => {
979
+ if (areEditsDisabled) {
980
+ return;
981
+ }
982
+
983
+ setAdditionalNotes(notes);
984
+ }}
985
+ isReadOnly={isReadOnlyMode}
785
986
  showNotification={notificationHandler}
786
987
  />
787
- <ClassDetailsModal
988
+ <ItemDetailsModal
788
989
  isOpen={isClassDetailsOpen}
789
990
  onClose={() => setIsClassDetailsOpen(false)}
790
- classType={classType}
791
- bulletData={bulletData}
792
- cartridgeCaseData={cartridgeCaseData}
793
- shotshellData={shotshellData}
991
+ itemType={getSelectedItemData().itemType}
992
+ bulletData={getSelectedItemData().bulletData}
993
+ cartridgeCaseData={getSelectedItemData().cartridgeCaseData}
994
+ shotshellData={getSelectedItemData().shotshellData}
794
995
  onSave={(b, c, s) => {
795
- if (b !== undefined) setBulletData(b);
796
- if (c !== undefined) setCartridgeCaseData(c);
797
- if (s !== undefined) setShotshellData(s);
798
- const summary = buildClassDetailsSummary(b, c, s, classType);
996
+ if (areEditsDisabled) {
997
+ return;
998
+ }
999
+
1000
+ setSelectedItemData({
1001
+ bulletData: b,
1002
+ cartridgeCaseData: c,
1003
+ shotshellData: s,
1004
+ });
1005
+ const summary = buildItemDetailsSummary(b, c, s, getSelectedItemData().itemType);
799
1006
  if (summary) {
800
- setAdditionalNotes((prev) => prev ? `${prev}\n${summary}` : summary);
1007
+ if (selectedItem === 'left') {
1008
+ setLeftAdditionalNotes((prev) => (prev ? `${prev}\n${summary}` : summary));
1009
+ } else {
1010
+ setRightAdditionalNotes((prev) => (prev ? `${prev}\n${summary}` : summary));
1011
+ }
801
1012
  }
802
1013
  }}
803
1014
  showNotification={notificationHandler}
804
- isReadOnly={areInputsDisabled}
1015
+ isReadOnly={isReadOnlyMode}
805
1016
  />
806
1017
  </>
807
1018
  )}