@striae-org/striae 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +3 -1
  2. package/app/components/actions/case-export/core-export.ts +11 -2
  3. package/app/components/actions/case-export/download-handlers.ts +3 -1
  4. package/app/components/canvas/canvas.module.css +1 -1
  5. package/app/components/canvas/canvas.tsx +32 -11
  6. package/app/components/icon/icons.svg +1 -1
  7. package/app/components/icon/manifest.json +1 -1
  8. package/app/components/navbar/navbar.tsx +10 -9
  9. package/app/components/sidebar/cases/case-sidebar.tsx +6 -1
  10. package/app/components/sidebar/files/files-modal.tsx +39 -15
  11. package/app/components/sidebar/notes/addl-notes-modal.tsx +9 -2
  12. package/app/components/sidebar/notes/{class-details/class-details-fields.tsx → item-details/item-details-fields.tsx} +10 -10
  13. package/app/components/sidebar/notes/{class-details/class-details-modal.tsx → item-details/item-details-modal.tsx} +20 -22
  14. package/app/components/sidebar/notes/{class-details/class-details-sections.tsx → item-details/item-details-sections.tsx} +16 -16
  15. package/app/components/sidebar/notes/{class-details/class-details-shared.ts → item-details/item-details-shared.ts} +4 -3
  16. package/app/components/sidebar/notes/{class-details/use-class-details-state.ts → item-details/use-item-details-state.ts} +4 -4
  17. package/app/components/sidebar/notes/notes-editor-form.tsx +333 -124
  18. package/app/components/sidebar/notes/notes-editor-modal.tsx +3 -0
  19. package/app/components/sidebar/notes/notes.module.css +40 -20
  20. package/app/components/sidebar/sidebar-container.tsx +8 -0
  21. package/app/components/sidebar/sidebar.tsx +3 -0
  22. package/app/components/toolbar/toolbar.tsx +5 -5
  23. package/{members.emails.example → app/config-example/members.emails} +1 -1
  24. package/{primershear.emails.example → app/config-example/primershear.emails} +1 -1
  25. package/app/hooks/useFileListPreferences.ts +22 -17
  26. package/app/routes/striae/striae.tsx +4 -10
  27. package/app/types/annotations.ts +28 -5
  28. package/app/utils/data/confirmation-summary/summary-core.ts +40 -8
  29. package/app/utils/data/file-filters.ts +39 -17
  30. package/package.json +139 -141
  31. package/scripts/deploy-config.sh +33 -0
  32. package/scripts/deploy-members-emails.sh +4 -4
  33. package/scripts/deploy-primershear-emails.sh +3 -3
  34. package/workers/audit-worker/package.json +2 -2
  35. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  36. package/workers/data-worker/package.json +2 -2
  37. package/workers/data-worker/wrangler.jsonc.example +1 -1
  38. package/workers/image-worker/package.json +2 -2
  39. package/workers/image-worker/wrangler.jsonc.example +1 -1
  40. package/workers/pdf-worker/package.json +2 -2
  41. package/workers/pdf-worker/src/formats/format-striae.ts +65 -8
  42. package/workers/pdf-worker/src/report-types.ts +13 -1
  43. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  44. package/workers/user-worker/package.json +2 -2
  45. package/workers/user-worker/wrangler.jsonc.example +1 -1
  46. 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 } 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,13 +17,13 @@ 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
26
  type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
26
- type ClassType = 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
27
27
  type IndexType = 'number' | 'color';
28
28
 
29
29
  interface NotesFormSnapshot {
@@ -32,18 +32,29 @@ interface NotesFormSnapshot {
32
32
  leftItem: string;
33
33
  rightItem: string;
34
34
  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;
35
+ // Left item class characteristics
36
+ leftItemType: ItemType | '';
37
+ leftCustomClass: string;
38
+ leftClassNote: string;
39
+ leftHasSubclass: boolean;
40
+ leftBulletData: BulletAnnotationData | undefined;
41
+ leftCartridgeCaseData: CartridgeCaseAnnotationData | undefined;
42
+ leftShotshellData: ShotshellAnnotationData | undefined;
43
+ // Right item class characteristics
44
+ rightItemType: ItemType | '';
45
+ rightCustomClass: string;
46
+ rightClassNote: string;
47
+ rightHasSubclass: boolean;
48
+ rightBulletData: BulletAnnotationData | undefined;
49
+ rightCartridgeCaseData: CartridgeCaseAnnotationData | undefined;
50
+ rightShotshellData: ShotshellAnnotationData | undefined;
42
51
  indexType: IndexType;
43
52
  indexNumber: string;
44
53
  indexColor: string;
45
54
  supportLevel: SupportLevel | '';
46
55
  includeConfirmation: boolean;
56
+ leftAdditionalNotes: string;
57
+ rightAdditionalNotes: string;
47
58
  additionalNotes: string;
48
59
  }
49
60
 
@@ -73,16 +84,19 @@ const normalizeNestedAnnotationData = <T extends object>(data: T | undefined): T
73
84
 
74
85
  const normalizeNotesSnapshot = (snapshot: NotesFormSnapshot): NotesFormSnapshot => ({
75
86
  ...snapshot,
76
- bulletData: normalizeNestedAnnotationData(snapshot.bulletData),
77
- cartridgeCaseData: normalizeNestedAnnotationData(snapshot.cartridgeCaseData),
78
- shotshellData: normalizeNestedAnnotationData(snapshot.shotshellData),
87
+ leftBulletData: normalizeNestedAnnotationData(snapshot.leftBulletData),
88
+ leftCartridgeCaseData: normalizeNestedAnnotationData(snapshot.leftCartridgeCaseData),
89
+ leftShotshellData: normalizeNestedAnnotationData(snapshot.leftShotshellData),
90
+ rightBulletData: normalizeNestedAnnotationData(snapshot.rightBulletData),
91
+ rightCartridgeCaseData: normalizeNestedAnnotationData(snapshot.rightCartridgeCaseData),
92
+ rightShotshellData: normalizeNestedAnnotationData(snapshot.rightShotshellData),
79
93
  });
80
94
 
81
95
  const serializeNotesSnapshot = (snapshot: NotesFormSnapshot): string => JSON.stringify(normalizeNotesSnapshot(snapshot));
82
96
  const DIRTY_CHECK_DEBOUNCE_MS = 180;
83
97
  const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
84
98
 
85
- export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, showNotification: externalShowNotification, onDirtyChange, onRegisterSaveHandler }: NotesEditorFormProps) => {
99
+ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, isReadOnly = false, showNotification: externalShowNotification, onDirtyChange, onRegisterSaveHandler }: NotesEditorFormProps) => {
86
100
  // Loading/Saving Notes States
87
101
  const [isLoading, setIsLoading] = useState(false);
88
102
  const [loadError, setLoadError] = useState<string>();
@@ -96,14 +110,27 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
96
110
  const [useCurrentCaseRight, setUseCurrentCaseRight] = useState(false);
97
111
  const [caseFontColor, setCaseFontColor] = useState('');
98
112
 
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);
113
+ // Class characteristics state - selected item indicator
114
+ const [selectedItem, setSelectedItem] = useState<'left' | 'right'>('left');
115
+
116
+ // Left item class characteristics state
117
+ const [leftItemType, setLeftItemType] = useState<ItemType | ''>('');
118
+ const [leftCustomClass, setLeftCustomClass] = useState('');
119
+ const [leftClassNote, setLeftClassNote] = useState('');
120
+ const [leftHasSubclass, setLeftHasSubclass] = useState(false);
121
+ const [leftBulletData, setLeftBulletData] = useState<BulletAnnotationData | undefined>(undefined);
122
+ const [leftCartridgeCaseData, setLeftCartridgeCaseData] = useState<CartridgeCaseAnnotationData | undefined>(undefined);
123
+ const [leftShotshellData, setLeftShotshellData] = useState<ShotshellAnnotationData | undefined>(undefined);
124
+
125
+ // Right item class characteristics state
126
+ const [rightItemType, setRightItemType] = useState<ItemType | ''>('');
127
+ const [rightCustomClass, setRightCustomClass] = useState('');
128
+ const [rightClassNote, setRightClassNote] = useState('');
129
+ const [rightHasSubclass, setRightHasSubclass] = useState(false);
130
+ const [rightBulletData, setRightBulletData] = useState<BulletAnnotationData | undefined>(undefined);
131
+ const [rightCartridgeCaseData, setRightCartridgeCaseData] = useState<CartridgeCaseAnnotationData | undefined>(undefined);
132
+ const [rightShotshellData, setRightShotshellData] = useState<ShotshellAnnotationData | undefined>(undefined);
133
+
107
134
  const [isClassDetailsOpen, setIsClassDetailsOpen] = useState(false);
108
135
 
109
136
  // Index state
@@ -117,6 +144,8 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
117
144
 
118
145
  // Additional Notes Modal
119
146
  const [isModalOpen, setIsModalOpen] = useState(false);
147
+ const [leftAdditionalNotes, setLeftAdditionalNotes] = useState('');
148
+ const [rightAdditionalNotes, setRightAdditionalNotes] = useState('');
120
149
  const [additionalNotes, setAdditionalNotes] = useState('');
121
150
  const [isCaseInfoOpen, setIsCaseInfoOpen] = useState(true);
122
151
  const [isClassOpen, setIsClassOpen] = useState(true);
@@ -125,7 +154,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
125
154
  const [savedSnapshot, setSavedSnapshot] = useState<string>('');
126
155
  const [hasLoadedSnapshot, setHasLoadedSnapshot] = useState(false);
127
156
  const [isDirty, setIsDirty] = useState(false);
128
- const areInputsDisabled = isUploading || isConfirmedImage;
157
+ const areInputsDisabled = isUploading || isConfirmedImage || isReadOnly;
129
158
 
130
159
  const notificationHandler = useCallback((message: string, type: 'success' | 'error' | 'warning' = 'success') => {
131
160
  if (externalShowNotification) {
@@ -133,6 +162,58 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
133
162
  }
134
163
  }, [externalShowNotification]);
135
164
 
165
+ // Helper functions for selected item data access
166
+ const getSelectedItemData = useCallback(() => {
167
+ if (selectedItem === 'left') {
168
+ return {
169
+ itemType: leftItemType,
170
+ customClass: leftCustomClass,
171
+ classNote: leftClassNote,
172
+ hasSubclass: leftHasSubclass,
173
+ bulletData: leftBulletData,
174
+ cartridgeCaseData: leftCartridgeCaseData,
175
+ shotshellData: leftShotshellData,
176
+ };
177
+ }
178
+ return {
179
+ itemType: rightItemType,
180
+ customClass: rightCustomClass,
181
+ classNote: rightClassNote,
182
+ hasSubclass: rightHasSubclass,
183
+ bulletData: rightBulletData,
184
+ cartridgeCaseData: rightCartridgeCaseData,
185
+ shotshellData: rightShotshellData,
186
+ };
187
+ }, [selectedItem, leftItemType, leftCustomClass, leftClassNote, leftHasSubclass, leftBulletData, leftCartridgeCaseData, leftShotshellData, rightItemType, rightCustomClass, rightClassNote, rightHasSubclass, rightBulletData, rightCartridgeCaseData, rightShotshellData]);
188
+
189
+ const setSelectedItemData = useCallback((newData: {
190
+ itemType?: ItemType | '';
191
+ customClass?: string;
192
+ classNote?: string;
193
+ hasSubclass?: boolean;
194
+ bulletData?: BulletAnnotationData;
195
+ cartridgeCaseData?: CartridgeCaseAnnotationData;
196
+ shotshellData?: ShotshellAnnotationData;
197
+ }) => {
198
+ if (selectedItem === 'left') {
199
+ if (newData.itemType !== undefined) setLeftItemType(newData.itemType);
200
+ if (newData.customClass !== undefined) setLeftCustomClass(newData.customClass);
201
+ if (newData.classNote !== undefined) setLeftClassNote(newData.classNote);
202
+ if (newData.hasSubclass !== undefined) setLeftHasSubclass(newData.hasSubclass);
203
+ if (newData.bulletData !== undefined) setLeftBulletData(newData.bulletData);
204
+ if (newData.cartridgeCaseData !== undefined) setLeftCartridgeCaseData(newData.cartridgeCaseData);
205
+ if (newData.shotshellData !== undefined) setLeftShotshellData(newData.shotshellData);
206
+ } else {
207
+ if (newData.itemType !== undefined) setRightItemType(newData.itemType);
208
+ if (newData.customClass !== undefined) setRightCustomClass(newData.customClass);
209
+ if (newData.classNote !== undefined) setRightClassNote(newData.classNote);
210
+ if (newData.hasSubclass !== undefined) setRightHasSubclass(newData.hasSubclass);
211
+ if (newData.bulletData !== undefined) setRightBulletData(newData.bulletData);
212
+ if (newData.cartridgeCaseData !== undefined) setRightCartridgeCaseData(newData.cartridgeCaseData);
213
+ if (newData.shotshellData !== undefined) setRightShotshellData(newData.shotshellData);
214
+ }
215
+ }, [selectedItem]);
216
+
136
217
  useEffect(() => {
137
218
  if (!hasLoadedSnapshot) {
138
219
  return;
@@ -145,18 +226,27 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
145
226
  leftItem,
146
227
  rightItem,
147
228
  caseFontColor,
148
- classType,
149
- customClass,
150
- classNote,
151
- hasSubclass,
152
- bulletData,
153
- cartridgeCaseData,
154
- shotshellData,
229
+ leftItemType,
230
+ leftCustomClass,
231
+ leftClassNote,
232
+ leftHasSubclass,
233
+ leftBulletData,
234
+ leftCartridgeCaseData,
235
+ leftShotshellData,
236
+ rightItemType,
237
+ rightCustomClass,
238
+ rightClassNote,
239
+ rightHasSubclass,
240
+ rightBulletData,
241
+ rightCartridgeCaseData,
242
+ rightShotshellData,
155
243
  indexType,
156
244
  indexNumber,
157
245
  indexColor,
158
246
  supportLevel,
159
247
  includeConfirmation,
248
+ leftAdditionalNotes,
249
+ rightAdditionalNotes,
160
250
  additionalNotes,
161
251
  });
162
252
 
@@ -168,24 +258,33 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
168
258
  };
169
259
  }, [
170
260
  additionalNotes,
171
- bulletData,
172
- cartridgeCaseData,
173
- caseFontColor,
174
- classNote,
175
- classType,
176
- customClass,
177
261
  hasLoadedSnapshot,
178
- hasSubclass,
179
262
  includeConfirmation,
180
263
  indexColor,
181
264
  indexNumber,
182
265
  indexType,
266
+ leftBulletData,
267
+ leftCartridgeCaseData,
183
268
  leftCase,
269
+ leftClassNote,
270
+ leftCustomClass,
271
+ leftHasSubclass,
272
+ leftItemType,
184
273
  leftItem,
274
+ leftShotshellData,
275
+ rightBulletData,
276
+ rightCartridgeCaseData,
185
277
  rightCase,
278
+ rightClassNote,
279
+ rightCustomClass,
280
+ rightHasSubclass,
281
+ rightItemType,
186
282
  rightItem,
283
+ rightShotshellData,
284
+ caseFontColor,
187
285
  savedSnapshot,
188
- shotshellData,
286
+ leftAdditionalNotes,
287
+ rightAdditionalNotes,
189
288
  supportLevel,
190
289
  ]);
191
290
 
@@ -213,19 +312,42 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
213
312
  setLeftItem(existingNotes.leftItem);
214
313
  setRightItem(existingNotes.rightItem);
215
314
  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);
315
+
316
+ // Migration: if old single-set fields exist, map to left item; otherwise use new left/right fields
317
+ const migratedLeftItemType = existingNotes.leftItemType || existingNotes.itemType || (existingNotes.classType as ItemType | undefined) || '';
318
+ const migratedLeftCustomClass = existingNotes.leftCustomClass || existingNotes.customClass || '';
319
+ const migratedLeftClassNote = existingNotes.leftClassNote || existingNotes.classNote || '';
320
+ const migratedLeftHasSubclass = existingNotes.leftHasSubclass ?? existingNotes.hasSubclass ?? false;
321
+ const migratedLeftBulletData = existingNotes.leftBulletData || existingNotes.bulletData;
322
+ const migratedLeftCartridgeCaseData = existingNotes.leftCartridgeCaseData || existingNotes.cartridgeCaseData;
323
+ const migratedLeftShotshellData = existingNotes.leftShotshellData || existingNotes.shotshellData;
324
+
325
+ setLeftItemType(migratedLeftItemType);
326
+ setLeftCustomClass(migratedLeftCustomClass);
327
+ setLeftClassNote(migratedLeftClassNote);
328
+ setLeftHasSubclass(migratedLeftHasSubclass);
329
+ setLeftBulletData(migratedLeftBulletData);
330
+ setLeftCartridgeCaseData(migratedLeftCartridgeCaseData);
331
+ setLeftShotshellData(migratedLeftShotshellData);
332
+
333
+ // Set right item fields (new structure)
334
+ setRightItemType(existingNotes.rightItemType || existingNotes.itemType || (existingNotes.classType as ItemType | undefined) || '');
335
+ setRightCustomClass(existingNotes.rightCustomClass || '');
336
+ setRightClassNote(existingNotes.rightClassNote || '');
337
+ setRightHasSubclass(existingNotes.rightHasSubclass ?? false);
338
+ setRightBulletData(existingNotes.rightBulletData);
339
+ setRightCartridgeCaseData(existingNotes.rightCartridgeCaseData);
340
+ setRightShotshellData(existingNotes.rightShotshellData);
341
+
223
342
  setIndexType(existingNotes.indexType || 'color');
224
343
  setIndexNumber(existingNotes.indexNumber || '');
225
344
  setIndexColor(existingNotes.indexColor || '');
226
345
  setSupportLevel(existingNotes.supportLevel || '');
227
346
  setIncludeConfirmation(existingNotes.includeConfirmation);
347
+ setLeftAdditionalNotes(existingNotes.leftAdditionalNotes || '');
348
+ setRightAdditionalNotes(existingNotes.rightAdditionalNotes || '');
228
349
  setAdditionalNotes(existingNotes.additionalNotes || '');
350
+ setSelectedItem('left'); // Always default to left item
229
351
 
230
352
  setSavedSnapshot(serializeNotesSnapshot({
231
353
  leftCase: existingNotes.leftCase || '',
@@ -233,18 +355,27 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
233
355
  leftItem: existingNotes.leftItem || '',
234
356
  rightItem: existingNotes.rightItem || '',
235
357
  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,
358
+ leftItemType: migratedLeftItemType,
359
+ leftCustomClass: migratedLeftCustomClass,
360
+ leftClassNote: migratedLeftClassNote,
361
+ leftHasSubclass: migratedLeftHasSubclass,
362
+ leftBulletData: migratedLeftBulletData,
363
+ leftCartridgeCaseData: migratedLeftCartridgeCaseData,
364
+ leftShotshellData: migratedLeftShotshellData,
365
+ rightItemType: existingNotes.rightItemType || '',
366
+ rightCustomClass: existingNotes.rightCustomClass || '',
367
+ rightClassNote: existingNotes.rightClassNote || '',
368
+ rightHasSubclass: existingNotes.rightHasSubclass ?? false,
369
+ rightBulletData: existingNotes.rightBulletData,
370
+ rightCartridgeCaseData: existingNotes.rightCartridgeCaseData,
371
+ rightShotshellData: existingNotes.rightShotshellData,
243
372
  indexType: existingNotes.indexType || 'color',
244
373
  indexNumber: existingNotes.indexNumber || '',
245
374
  indexColor: existingNotes.indexColor || '',
246
375
  supportLevel: existingNotes.supportLevel || '',
247
376
  includeConfirmation: existingNotes.includeConfirmation,
377
+ leftAdditionalNotes: existingNotes.leftAdditionalNotes || '',
378
+ rightAdditionalNotes: existingNotes.rightAdditionalNotes || '',
248
379
  additionalNotes: existingNotes.additionalNotes || ''
249
380
  }));
250
381
  } else {
@@ -256,18 +387,27 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
256
387
  leftItem: '',
257
388
  rightItem: '',
258
389
  caseFontColor: '',
259
- classType: '',
260
- customClass: '',
261
- classNote: '',
262
- hasSubclass: false,
263
- bulletData: undefined,
264
- cartridgeCaseData: undefined,
265
- shotshellData: undefined,
390
+ leftItemType: '',
391
+ leftCustomClass: '',
392
+ leftClassNote: '',
393
+ leftHasSubclass: false,
394
+ leftBulletData: undefined,
395
+ leftCartridgeCaseData: undefined,
396
+ leftShotshellData: undefined,
397
+ rightItemType: '',
398
+ rightCustomClass: '',
399
+ rightClassNote: '',
400
+ rightHasSubclass: false,
401
+ rightBulletData: undefined,
402
+ rightCartridgeCaseData: undefined,
403
+ rightShotshellData: undefined,
266
404
  indexType: 'color',
267
405
  indexNumber: '',
268
406
  indexColor: '',
269
407
  supportLevel: '',
270
408
  includeConfirmation: false,
409
+ leftAdditionalNotes: '',
410
+ rightAdditionalNotes: '',
271
411
  additionalNotes: ''
272
412
  }));
273
413
  }
@@ -303,6 +443,11 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
303
443
  return false;
304
444
  }
305
445
 
446
+ if (isReadOnly) {
447
+ notificationHandler('This case is read-only. Notes cannot be modified.', 'error');
448
+ return false;
449
+ }
450
+
306
451
  let existingData: AnnotationData | null = null;
307
452
 
308
453
  try {
@@ -315,9 +460,12 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
315
460
  return false;
316
461
  }
317
462
 
318
- const normalizedBulletData = normalizeNestedAnnotationData(bulletData);
319
- const normalizedCartridgeCaseData = normalizeNestedAnnotationData(cartridgeCaseData);
320
- const normalizedShotshellData = normalizeNestedAnnotationData(shotshellData);
463
+ const normalizedLeftBulletData = normalizeNestedAnnotationData(leftBulletData);
464
+ const normalizedLeftCartridgeCaseData = normalizeNestedAnnotationData(leftCartridgeCaseData);
465
+ const normalizedLeftShotshellData = normalizeNestedAnnotationData(leftShotshellData);
466
+ const normalizedRightBulletData = normalizeNestedAnnotationData(rightBulletData);
467
+ const normalizedRightCartridgeCaseData = normalizeNestedAnnotationData(rightCartridgeCaseData);
468
+ const normalizedRightShotshellData = normalizeNestedAnnotationData(rightShotshellData);
321
469
 
322
470
  // Create updated annotation data, preserving box annotations and earliest timestamp
323
471
  const now = new Date().toISOString();
@@ -329,14 +477,23 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
329
477
  rightItem: rightItem || '',
330
478
  caseFontColor: caseFontColor || undefined,
331
479
 
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,
480
+ // Left item class characteristics
481
+ leftItemType: leftItemType as ItemType || undefined,
482
+ leftCustomClass: leftCustomClass,
483
+ leftClassNote: leftClassNote || undefined,
484
+ leftHasSubclass: leftHasSubclass,
485
+ leftBulletData: normalizedLeftBulletData,
486
+ leftCartridgeCaseData: normalizedLeftCartridgeCaseData,
487
+ leftShotshellData: normalizedLeftShotshellData,
488
+
489
+ // Right item class characteristics
490
+ rightItemType: rightItemType as ItemType || undefined,
491
+ rightCustomClass: rightCustomClass,
492
+ rightClassNote: rightClassNote || undefined,
493
+ rightHasSubclass: rightHasSubclass,
494
+ rightBulletData: normalizedRightBulletData,
495
+ rightCartridgeCaseData: normalizedRightCartridgeCaseData,
496
+ rightShotshellData: normalizedRightShotshellData,
340
497
 
341
498
  // Index Information
342
499
  indexType: indexType,
@@ -348,7 +505,9 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
348
505
  includeConfirmation: includeConfirmation,
349
506
 
350
507
  // Additional Notes
351
- additionalNotes: additionalNotes || undefined, // Keep as optional
508
+ leftAdditionalNotes: leftAdditionalNotes || undefined,
509
+ rightAdditionalNotes: rightAdditionalNotes || undefined,
510
+ additionalNotes: additionalNotes || undefined, // General notes (including box-annotation notes)
352
511
 
353
512
  // Preserve existing box annotations
354
513
  boxAnnotations: existingData?.boxAnnotations || [],
@@ -385,18 +544,27 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
385
544
  leftItem,
386
545
  rightItem,
387
546
  caseFontColor,
388
- classType,
389
- customClass,
390
- classNote,
391
- hasSubclass,
392
- bulletData,
393
- cartridgeCaseData,
394
- shotshellData,
547
+ leftItemType,
548
+ leftCustomClass,
549
+ leftClassNote,
550
+ leftHasSubclass,
551
+ leftBulletData,
552
+ leftCartridgeCaseData,
553
+ leftShotshellData,
554
+ rightItemType,
555
+ rightCustomClass,
556
+ rightClassNote,
557
+ rightHasSubclass,
558
+ rightBulletData,
559
+ rightCartridgeCaseData,
560
+ rightShotshellData,
395
561
  indexType,
396
562
  indexNumber,
397
563
  indexColor,
398
564
  supportLevel,
399
565
  includeConfirmation,
566
+ leftAdditionalNotes,
567
+ rightAdditionalNotes,
400
568
  additionalNotes,
401
569
  }));
402
570
  setIsDirty(false);
@@ -438,28 +606,38 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
438
606
  }
439
607
  }, [
440
608
  additionalNotes,
441
- bulletData,
442
- cartridgeCaseData,
609
+ leftBulletData,
610
+ leftCartridgeCaseData,
443
611
  caseFontColor,
444
- classNote,
445
- classType,
612
+ leftClassNote,
613
+ leftItemType,
446
614
  currentCase,
447
- customClass,
448
- hasSubclass,
615
+ leftCustomClass,
616
+ leftHasSubclass,
449
617
  imageId,
450
618
  includeConfirmation,
451
619
  indexColor,
452
620
  indexNumber,
453
621
  indexType,
622
+ isReadOnly,
454
623
  leftCase,
455
624
  leftItem,
456
625
  notificationHandler,
457
626
  onAnnotationRefresh,
458
627
  onDirtyChange,
459
628
  originalFileName,
629
+ rightBulletData,
630
+ rightCartridgeCaseData,
460
631
  rightCase,
632
+ rightClassNote,
633
+ rightCustomClass,
634
+ rightHasSubclass,
635
+ rightItemType,
461
636
  rightItem,
462
- shotshellData,
637
+ rightShotshellData,
638
+ leftShotshellData,
639
+ leftAdditionalNotes,
640
+ rightAdditionalNotes,
463
641
  supportLevel,
464
642
  user,
465
643
  ]);
@@ -569,7 +747,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
569
747
  </div>
570
748
  <hr />
571
749
  <div className={styles.fontColorRow}>
572
- <label htmlFor="colorSelect">Font</label>
750
+ <label htmlFor="colorSelect">Case & Item Font Color</label>
573
751
  <ColorSelector
574
752
  selectedColor={caseFontColor}
575
753
  onColorSelect={setCaseFontColor}
@@ -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
798
  disabled={areInputsDisabled}
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
813
  disabled={areInputsDisabled}
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
822
  disabled={areInputsDisabled}
629
823
  />
630
824
  </div>
825
+ </div>
826
+
827
+ <div className={styles.itemDetailsPanel}>
828
+ <button
829
+ type="button"
830
+ onClick={() => setIsClassDetailsOpen(true)}
831
+ className={styles.itemDetailsButton}
832
+ >
833
+ Class Characteristics & GRC
834
+ </button>
631
835
  <label className={`${styles.checkboxLabel} mb-4`}>
632
836
  <input
633
837
  type="checkbox"
634
- checked={hasSubclass}
635
- onChange={(e) => setHasSubclass(e.target.checked)}
838
+ checked={getSelectedItemData().hasSubclass}
839
+ onChange={(e) => setSelectedItemData({ hasSubclass: e.target.checked })}
636
840
  className={styles.checkbox}
637
841
  disabled={areInputsDisabled}
638
842
  />
639
843
  <span>Potential subclass?</span>
640
844
  </label>
641
- </div>
642
-
643
- <div className={styles.classDetailsPanel}>
644
- <button
645
- type="button"
646
- onClick={() => setIsClassDetailsOpen(true)}
647
- className={styles.classDetailsButton}
648
- disabled={areInputsDisabled}
649
- >
650
- Enter Class Characteristic Details
651
- </button>
652
845
  </div>
653
846
  </div>
654
847
  </>
@@ -760,7 +953,6 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
760
953
  <button
761
954
  onClick={() => setIsModalOpen(true)}
762
955
  className={styles.notesButton}
763
- disabled={areInputsDisabled}
764
956
  title={isConfirmedImage ? "Cannot edit notes for confirmed images" : isUploading ? "Cannot add notes while uploading" : undefined}
765
957
  >
766
958
  Additional Notes
@@ -781,23 +973,40 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
781
973
  isOpen={isModalOpen}
782
974
  onClose={() => setIsModalOpen(false)}
783
975
  notes={additionalNotes}
784
- onSave={setAdditionalNotes}
976
+ onSave={(notes) => {
977
+ if (areInputsDisabled) {
978
+ return;
979
+ }
980
+
981
+ setAdditionalNotes(notes);
982
+ }}
983
+ isReadOnly={areInputsDisabled}
785
984
  showNotification={notificationHandler}
786
985
  />
787
- <ClassDetailsModal
986
+ <ItemDetailsModal
788
987
  isOpen={isClassDetailsOpen}
789
988
  onClose={() => setIsClassDetailsOpen(false)}
790
- classType={classType}
791
- bulletData={bulletData}
792
- cartridgeCaseData={cartridgeCaseData}
793
- shotshellData={shotshellData}
989
+ itemType={getSelectedItemData().itemType}
990
+ bulletData={getSelectedItemData().bulletData}
991
+ cartridgeCaseData={getSelectedItemData().cartridgeCaseData}
992
+ shotshellData={getSelectedItemData().shotshellData}
794
993
  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);
994
+ if (areInputsDisabled) {
995
+ return;
996
+ }
997
+
998
+ setSelectedItemData({
999
+ bulletData: b,
1000
+ cartridgeCaseData: c,
1001
+ shotshellData: s,
1002
+ });
1003
+ const summary = buildItemDetailsSummary(b, c, s, getSelectedItemData().itemType);
799
1004
  if (summary) {
800
- setAdditionalNotes((prev) => prev ? `${prev}\n${summary}` : summary);
1005
+ if (selectedItem === 'left') {
1006
+ setLeftAdditionalNotes((prev) => (prev ? `${prev}\n${summary}` : summary));
1007
+ } else {
1008
+ setRightAdditionalNotes((prev) => (prev ? `${prev}\n${summary}` : summary));
1009
+ }
801
1010
  }
802
1011
  }}
803
1012
  showNotification={notificationHandler}