@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.
- package/README.md +3 -1
- package/app/components/actions/case-export/core-export.ts +11 -2
- package/app/components/actions/case-export/download-handlers.ts +3 -1
- package/app/components/canvas/canvas.module.css +1 -1
- package/app/components/canvas/canvas.tsx +32 -11
- package/app/components/icon/icons.svg +1 -1
- package/app/components/icon/manifest.json +1 -1
- package/app/components/navbar/navbar.tsx +10 -9
- package/app/components/sidebar/cases/case-sidebar.tsx +6 -1
- package/app/components/sidebar/files/files-modal.tsx +39 -15
- package/app/components/sidebar/notes/addl-notes-modal.tsx +9 -2
- package/app/components/sidebar/notes/{class-details/class-details-fields.tsx → item-details/item-details-fields.tsx} +10 -10
- package/app/components/sidebar/notes/{class-details/class-details-modal.tsx → item-details/item-details-modal.tsx} +20 -22
- package/app/components/sidebar/notes/{class-details/class-details-sections.tsx → item-details/item-details-sections.tsx} +16 -16
- package/app/components/sidebar/notes/{class-details/class-details-shared.ts → item-details/item-details-shared.ts} +4 -3
- package/app/components/sidebar/notes/{class-details/use-class-details-state.ts → item-details/use-item-details-state.ts} +4 -4
- package/app/components/sidebar/notes/notes-editor-form.tsx +333 -124
- package/app/components/sidebar/notes/notes-editor-modal.tsx +3 -0
- package/app/components/sidebar/notes/notes.module.css +40 -20
- package/app/components/sidebar/sidebar-container.tsx +8 -0
- package/app/components/sidebar/sidebar.tsx +3 -0
- package/app/components/toolbar/toolbar.tsx +5 -5
- package/{members.emails.example → app/config-example/members.emails} +1 -1
- package/{primershear.emails.example → app/config-example/primershear.emails} +1 -1
- package/app/hooks/useFileListPreferences.ts +22 -17
- package/app/routes/striae/striae.tsx +4 -10
- package/app/types/annotations.ts +28 -5
- package/app/utils/data/confirmation-summary/summary-core.ts +40 -8
- package/app/utils/data/file-filters.ts +39 -17
- package/package.json +139 -141
- package/scripts/deploy-config.sh +33 -0
- package/scripts/deploy-members-emails.sh +4 -4
- package/scripts/deploy-primershear-emails.sh +3 -3
- package/workers/audit-worker/package.json +2 -2
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +2 -2
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +2 -2
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +2 -2
- package/workers/pdf-worker/src/formats/format-striae.ts +65 -8
- package/workers/pdf-worker/src/report-types.ts +13 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +2 -2
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- 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 {
|
|
6
|
-
import {
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 [
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const [
|
|
104
|
-
const [
|
|
105
|
-
const [
|
|
106
|
-
const [
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
319
|
-
const
|
|
320
|
-
const
|
|
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
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
442
|
-
|
|
609
|
+
leftBulletData,
|
|
610
|
+
leftCartridgeCaseData,
|
|
443
611
|
caseFontColor,
|
|
444
|
-
|
|
445
|
-
|
|
612
|
+
leftClassNote,
|
|
613
|
+
leftItemType,
|
|
446
614
|
currentCase,
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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="
|
|
600
|
-
aria-label="
|
|
601
|
-
value={
|
|
602
|
-
onChange={(e) =>
|
|
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
|
|
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
|
-
{
|
|
807
|
+
{getSelectedItemData().itemType === 'Other' && (
|
|
614
808
|
<input
|
|
615
809
|
type="text"
|
|
616
|
-
value={customClass}
|
|
617
|
-
onChange={(e) =>
|
|
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) =>
|
|
626
|
-
placeholder="Enter
|
|
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) =>
|
|
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={
|
|
976
|
+
onSave={(notes) => {
|
|
977
|
+
if (areInputsDisabled) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
setAdditionalNotes(notes);
|
|
982
|
+
}}
|
|
983
|
+
isReadOnly={areInputsDisabled}
|
|
785
984
|
showNotification={notificationHandler}
|
|
786
985
|
/>
|
|
787
|
-
<
|
|
986
|
+
<ItemDetailsModal
|
|
788
987
|
isOpen={isClassDetailsOpen}
|
|
789
988
|
onClose={() => setIsClassDetailsOpen(false)}
|
|
790
|
-
|
|
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 (
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
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}
|