@striae-org/striae 4.2.0 → 4.3.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 (90) hide show
  1. package/LICENSE +1 -1
  2. package/app/components/actions/case-manage.ts +50 -17
  3. package/app/components/audit/viewer/audit-entries-list.tsx +5 -2
  4. package/app/components/audit/viewer/use-audit-viewer-data.ts +6 -3
  5. package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
  6. package/app/components/canvas/confirmation/confirmation.tsx +6 -2
  7. package/app/components/colors/colors.module.css +4 -3
  8. package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -76
  9. package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
  10. package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
  11. package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
  12. package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
  13. package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
  14. package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
  15. package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
  16. package/app/components/navbar/navbar.tsx +34 -9
  17. package/app/components/sidebar/cases/case-sidebar.tsx +93 -73
  18. package/app/components/sidebar/cases/cases-modal.module.css +312 -10
  19. package/app/components/sidebar/cases/cases-modal.tsx +737 -116
  20. package/app/components/sidebar/cases/cases.module.css +43 -0
  21. package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
  22. package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
  23. package/app/components/sidebar/files/files-modal.module.css +285 -44
  24. package/app/components/sidebar/files/files-modal.tsx +482 -177
  25. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  26. package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
  27. package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
  28. package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
  29. package/app/components/sidebar/notes/class-details-shared.ts +239 -0
  30. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +77 -76
  31. package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
  32. package/app/components/sidebar/notes/notes.module.css +262 -14
  33. package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
  34. package/app/components/sidebar/sidebar-container.tsx +2 -0
  35. package/app/components/sidebar/sidebar.tsx +15 -1
  36. package/app/{tailwind.css → global.css} +1 -3
  37. package/app/hooks/useCaseListPreferences.ts +99 -0
  38. package/app/hooks/useFileListPreferences.ts +106 -0
  39. package/app/hooks/useOverlayDismiss.ts +6 -4
  40. package/app/root.tsx +1 -1
  41. package/app/routes/striae/striae.tsx +7 -0
  42. package/app/services/audit/audit.service.ts +2 -2
  43. package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
  44. package/app/types/annotations.ts +48 -1
  45. package/app/types/audit.ts +1 -0
  46. package/app/utils/data/case-filters.ts +127 -0
  47. package/app/utils/data/confirmation-summary/summary-core.ts +295 -0
  48. package/app/utils/data/data-operations.ts +17 -861
  49. package/app/utils/data/file-filters.ts +201 -0
  50. package/app/utils/data/index.ts +11 -1
  51. package/app/utils/data/operations/batch-operations.ts +113 -0
  52. package/app/utils/data/operations/case-operations.ts +168 -0
  53. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  54. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  55. package/app/utils/data/operations/index.ts +7 -0
  56. package/app/utils/data/operations/signing-operations.ts +225 -0
  57. package/app/utils/data/operations/types.ts +42 -0
  58. package/app/utils/data/operations/validation-operations.ts +48 -0
  59. package/app/utils/forensics/export-verification.ts +40 -111
  60. package/functions/api/_shared/firebase-auth.ts +2 -7
  61. package/functions/api/image/[[path]].ts +23 -22
  62. package/functions/api/pdf/[[path]].ts +27 -8
  63. package/package.json +7 -13
  64. package/scripts/deploy-primershear-emails.sh +1 -1
  65. package/worker-configuration.d.ts +2 -2
  66. package/workers/audit-worker/package.json +1 -1
  67. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  68. package/workers/data-worker/package.json +1 -1
  69. package/workers/data-worker/wrangler.jsonc.example +1 -1
  70. package/workers/image-worker/package.json +1 -1
  71. package/workers/image-worker/src/image-worker.example.ts +16 -5
  72. package/workers/image-worker/wrangler.jsonc.example +1 -1
  73. package/workers/keys-worker/package.json +1 -1
  74. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  75. package/workers/pdf-worker/package.json +1 -1
  76. package/workers/pdf-worker/src/formats/format-striae.ts +84 -124
  77. package/workers/pdf-worker/src/pdf-worker.example.ts +58 -61
  78. package/workers/pdf-worker/src/report-layout.ts +227 -0
  79. package/workers/pdf-worker/src/report-types.ts +23 -3
  80. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  81. package/workers/user-worker/package.json +1 -1
  82. package/workers/user-worker/src/user-worker.example.ts +17 -0
  83. package/workers/user-worker/wrangler.jsonc.example +1 -1
  84. package/wrangler.toml.example +1 -1
  85. package/NOTICE +0 -13
  86. package/app/components/sidebar/notes/notes-modal.tsx +0 -52
  87. package/postcss.config.js +0 -6
  88. package/tailwind.config.ts +0 -22
  89. package/workers/pdf-worker/src/assets/icon-256.png +0 -0
  90. /package/workers/pdf-worker/src/assets/{generated-assets.ts → generated-assets.example.ts} +0 -0
@@ -0,0 +1,239 @@
1
+ import type { BulletAnnotationData, CartridgeCaseAnnotationData, ShotshellAnnotationData } from '~/types/annotations';
2
+
3
+ export type ClassType = 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
4
+
5
+ export const CUSTOM = '__custom__';
6
+
7
+ export const PISTOL_CALIBERS: string[] = [
8
+ '.22 LR',
9
+ '.25 ACP',
10
+ '.32 ACP',
11
+ '.380 ACP (9 mm Kurz, 9×17)',
12
+ '9 mm Luger / 9×19 (9 mm Parabellum, 9 mm NATO)',
13
+ '.38 Special',
14
+ '.357 Magnum',
15
+ '.40 S&W',
16
+ '10 mm Auto',
17
+ '.44 Special',
18
+ '.44 Magnum',
19
+ '.45 ACP',
20
+ '.45 Colt (.45 Long Colt)',
21
+ '.454 Casull',
22
+ '.50 AE',
23
+ ];
24
+
25
+ export const RIFLE_CALIBERS: string[] = [
26
+ '.22 LR',
27
+ '.17 HMR',
28
+ '.22 WMR (.22 Magnum)',
29
+ '.223 Remington / 5.56×45 NATO',
30
+ '.243 Winchester',
31
+ '6 mm Creedmoor / .243 class',
32
+ '6.5×55 Swedish',
33
+ '6.5 Creedmoor',
34
+ '.270 Winchester',
35
+ '7 mm-08 Remington / 7 mm Remington Magnum',
36
+ '.30-30 Winchester',
37
+ '.308 Winchester / 7.62×51 NATO',
38
+ '.30-06 Springfield',
39
+ '7.62×39 (AK family)',
40
+ '7.62×54R',
41
+ '.300 Winchester Magnum',
42
+ '.300 AAC Blackout',
43
+ '.338 Winchester Magnum',
44
+ '.45-70 Government',
45
+ '.50 BMG (12.7×99)',
46
+ ];
47
+
48
+ export const SHOTSHELL_GAUGES: string[] = [
49
+ '10 gauge',
50
+ '12 gauge',
51
+ '16 gauge',
52
+ '20 gauge',
53
+ '28 gauge',
54
+ '.410 bore',
55
+ ];
56
+
57
+ export const SHOTSHELL_BIRDSHOT_OPTIONS = [
58
+ '#9',
59
+ '#8 1/2',
60
+ '#8',
61
+ '#7 1/2',
62
+ '#7',
63
+ '#6',
64
+ '#5',
65
+ '#4',
66
+ '#3',
67
+ '#2',
68
+ '#1',
69
+ ] as const;
70
+
71
+ export const SHOTSHELL_STEEL_WATERFOWL_OPTIONS = [
72
+ '#4',
73
+ '#3',
74
+ '#2',
75
+ '#1',
76
+ 'B',
77
+ 'BB',
78
+ 'BBB',
79
+ 'T',
80
+ ] as const;
81
+
82
+ export const SHOTSHELL_BUCKSHOT_OPTIONS = [
83
+ '#4 buck',
84
+ '#1 buck',
85
+ '0 buck',
86
+ '00 buck',
87
+ '000 buck',
88
+ ] as const;
89
+
90
+ export const ALL_CALIBERS: string[] = [...PISTOL_CALIBERS, ...RIFLE_CALIBERS];
91
+ export const BULLET_JACKET_METAL_OPTIONS = ['Cu', 'Brass', 'Ni-plated', 'Al', 'Steel', 'None'] as const;
92
+ export const BULLET_CORE_METAL_OPTIONS = ['Pb', 'Steel'] as const;
93
+ export const BULLET_TYPE_OPTIONS = ['FMJ', 'TMJ', 'HP', 'WC'] as const;
94
+ export const BULLET_BARREL_TYPE_OPTIONS = ['Conventional', 'Polygonal'] as const;
95
+ export const CARTRIDGE_METAL_OPTIONS = ['Brass', 'Ni-plated', 'Al', 'Steel'] as const;
96
+ export const CARTRIDGE_PRIMER_TYPE_OPTIONS = ['CF', 'RF'] as const;
97
+ export const CARTRIDGE_FPI_SHAPE_OPTIONS = ['Circular', 'Elliptical', 'Rectangular/Square', 'Tear-drop'] as const;
98
+ export const CARTRIDGE_APERTURE_SHAPE_OPTIONS = ['Circular', 'Rectangular'] as const;
99
+
100
+ export const handleSelectWithCustom = (
101
+ value: string,
102
+ setValue: (nextValue: string) => void,
103
+ setIsCustom: (nextValue: boolean) => void,
104
+ ) => {
105
+ if (value === CUSTOM) {
106
+ setIsCustom(true);
107
+ setValue('');
108
+ return;
109
+ }
110
+
111
+ setIsCustom(false);
112
+ setValue(value);
113
+ };
114
+
115
+ export const isCustomValue = (value: string | undefined, knownValues: readonly string[]): boolean =>
116
+ value !== undefined && value !== '' && !knownValues.includes(value);
117
+
118
+ export const parseMeasurementValue = (value: string): number | null => {
119
+ const trimmed = value.trim();
120
+ if (!trimmed) return null;
121
+
122
+ const parsed = Number.parseFloat(trimmed);
123
+ return Number.isFinite(parsed) ? parsed : null;
124
+ };
125
+
126
+ export const formatCalculatedDiameter = (value: number): string =>
127
+ value.toFixed(4).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1');
128
+
129
+ const avgWidth = (widths: string[] | undefined): number | null => {
130
+ if (!widths || widths.length === 0) return null;
131
+ const vals = widths.map(parseMeasurementValue).filter((n): n is number => n !== null);
132
+ if (vals.length === 0) return null;
133
+ return vals.reduce((a, b) => a + b, 0) / vals.length;
134
+ };
135
+
136
+ export const buildClassDetailsSummary = (
137
+ bulletData: BulletAnnotationData | undefined,
138
+ cartridgeCaseData: CartridgeCaseAnnotationData | undefined,
139
+ shotshellData: ShotshellAnnotationData | undefined,
140
+ classType: string,
141
+ ): string => {
142
+ const showBullet = classType === 'Bullet' || classType === 'Other' || classType === '';
143
+ const showCartridge = classType === 'Cartridge Case' || classType === 'Other' || classType === '';
144
+ const showShotshell = classType === 'Shotshell' || classType === 'Other' || classType === '';
145
+ const showHeaders = classType === 'Other' || classType === '';
146
+
147
+ const allLines: string[] = [];
148
+
149
+ const pushSection = (header: string, sectionRows: string[]) => {
150
+ if (sectionRows.length === 0) return;
151
+ if (allLines.length > 0) allLines.push('');
152
+ if (showHeaders) allLines.push(header);
153
+ allLines.push(...sectionRows);
154
+ };
155
+
156
+ const r = (label: string, value: string | number | undefined): string | null =>
157
+ value ? `${label}: ${value}` : null;
158
+
159
+ if (showBullet && bulletData) {
160
+ const rows: string[] = [];
161
+ const add = (v: string | null) => { if (v) rows.push(v); };
162
+ add(r('Caliber', bulletData.caliber));
163
+ add(r('Mass', bulletData.mass));
164
+ add(r('Diameter', bulletData.diameter));
165
+ add(r('L/G Count', bulletData.lgNumber));
166
+ add(r('L/G Direction', bulletData.lgDirection));
167
+ if (bulletData.lgNumber && bulletData.calcDiameter) {
168
+ const avgL = avgWidth(bulletData.lWidths);
169
+ const avgG = avgWidth(bulletData.gWidths);
170
+ if (avgL !== null) add(r('Avg L Width', `${parseFloat(avgL.toFixed(4))}"`));
171
+ if (avgG !== null) add(r('Avg G Width', `${parseFloat(avgG.toFixed(4))}"`));
172
+ add(r('Calc. Diameter', `${bulletData.calcDiameter}"`));
173
+ }
174
+ add(r('Jacket Metal', bulletData.jacketMetal));
175
+ add(r('Core Metal', bulletData.coreMetal));
176
+ add(r('Bullet Type', bulletData.bulletType));
177
+ add(r('Barrel Type', bulletData.barrelType));
178
+ pushSection('[Bullet]', rows);
179
+ }
180
+
181
+ if (showCartridge && cartridgeCaseData) {
182
+ const rows: string[] = [];
183
+ const add = (v: string | null) => { if (v) rows.push(v); };
184
+ add(r('Caliber', cartridgeCaseData.caliber));
185
+ add(r('Brand', cartridgeCaseData.brand));
186
+ add(r('Metal', cartridgeCaseData.metal));
187
+ add(r('Primer Type', cartridgeCaseData.primerType));
188
+ add(r('FPI Shape', cartridgeCaseData.fpiShape));
189
+ add(r('Aperture Shape', cartridgeCaseData.apertureShape));
190
+ if (cartridgeCaseData.hasFpDrag) rows.push('FP Drag: Yes');
191
+ if (cartridgeCaseData.hasExtractorMarks) rows.push('Extractor Marks: Yes');
192
+ if (cartridgeCaseData.hasEjectorMarks) rows.push('Ejector Marks: Yes');
193
+ if (cartridgeCaseData.hasChamberMarks) rows.push('Chamber Marks: Yes');
194
+ if (cartridgeCaseData.hasMagazineLipMarks) rows.push('Magazine Lip Marks: Yes');
195
+ if (cartridgeCaseData.hasPrimerShear) rows.push('Primer Shear: Yes');
196
+ if (cartridgeCaseData.hasEjectionPortMarks) rows.push('Ejection Port Marks: Yes');
197
+ pushSection('[Cartridge Case]', rows);
198
+ }
199
+
200
+ if (showShotshell && shotshellData) {
201
+ const rows: string[] = [];
202
+ const add = (v: string | null) => { if (v) rows.push(v); };
203
+ add(r('Gauge', shotshellData.gauge));
204
+ add(r('Shot Size', shotshellData.shotSize));
205
+ add(r('Metal', shotshellData.metal));
206
+ add(r('Brand', shotshellData.brand));
207
+ add(r('FPI Shape', shotshellData.fpiShape));
208
+ if (shotshellData.hasExtractorMarks) rows.push('Extractor Marks: Yes');
209
+ if (shotshellData.hasEjectorMarks) rows.push('Ejector Marks: Yes');
210
+ if (shotshellData.hasChamberMarks) rows.push('Chamber Marks: Yes');
211
+ pushSection('[Shotshell]', rows);
212
+ }
213
+
214
+ if (allLines.length === 0) return '';
215
+ return ['--- Class Details ---', ...allLines].join('\n');
216
+ };
217
+
218
+ export const calculateBulletDiameter = (
219
+ lgCount: number,
220
+ lWidths: string[],
221
+ gWidths: string[],
222
+ ): number | null => {
223
+ if (lgCount <= 0) return null;
224
+
225
+ const lWidthValues = lWidths.slice(0, lgCount).map(parseMeasurementValue);
226
+ const gWidthValues = gWidths.slice(0, lgCount).map(parseMeasurementValue);
227
+ const hasAllMeasurements = lWidthValues.length === lgCount
228
+ && gWidthValues.length === lgCount
229
+ && lWidthValues.every((value) => value !== null)
230
+ && gWidthValues.every((value) => value !== null);
231
+
232
+ if (!hasAllMeasurements) return null;
233
+
234
+ const lAverage = lWidthValues.reduce((sum, value) => sum + (value ?? 0), 0) / lgCount;
235
+ const gAverage = gWidthValues.reduce((sum, value) => sum + (value ?? 0), 0) / lgCount;
236
+ const circumference = (lAverage + gAverage) * lgCount;
237
+
238
+ return circumference / Math.PI;
239
+ };
@@ -1,36 +1,33 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import type { User } from 'firebase/auth';
3
3
  import { ColorSelector } from '~/components/colors/colors';
4
- import { NotesModal } from './notes-modal';
4
+ import { AddlNotesModal } from './addl-notes-modal';
5
+ import { ClassDetailsModal } from './class-details-modal';
6
+ import { buildClassDetailsSummary } from './class-details-shared';
5
7
  import { getNotes, saveNotes } from '~/components/actions/notes-manage';
6
- import { type AnnotationData } from '~/types/annotations';
8
+ import { type AnnotationData, type BulletAnnotationData, type CartridgeCaseAnnotationData, type ShotshellAnnotationData } from '~/types/annotations';
7
9
  import { resolveEarliestAnnotationTimestamp } from '~/utils/ui';
8
10
  import { auditService } from '~/services/audit';
9
11
  import styles from './notes.module.css';
10
12
 
11
- interface NotesSidebarProps {
13
+ interface NotesEditorFormProps {
12
14
  currentCase: string;
13
- onReturn: () => void;
14
15
  user: User;
15
16
  imageId: string;
16
17
  onAnnotationRefresh?: () => void;
17
18
  originalFileName?: string;
18
19
  isUploading?: boolean;
19
- showReturnButton?: boolean;
20
- stickyActionBar?: boolean;
21
- compactLayout?: boolean;
20
+ showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
22
21
  }
23
22
 
24
23
  type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
25
- type ClassType = 'Bullet' | 'Cartridge Case' | 'Other';
24
+ type ClassType = 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
26
25
  type IndexType = 'number' | 'color';
27
26
 
28
- export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, showReturnButton = true, stickyActionBar = false, compactLayout = false }: NotesSidebarProps) => {
27
+ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, showNotification: externalShowNotification }: NotesEditorFormProps) => {
29
28
  // Loading/Saving Notes States
30
29
  const [isLoading, setIsLoading] = useState(false);
31
30
  const [loadError, setLoadError] = useState<string>();
32
- const [saveError, setSaveError] = useState<string>();
33
- const [saveSuccess, setSaveSuccess] = useState(false);
34
31
  const [isConfirmedImage, setIsConfirmedImage] = useState(false);
35
32
  // Case numbers state
36
33
  const [leftCase, setLeftCase] = useState('');
@@ -46,6 +43,10 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
46
43
  const [customClass, setCustomClass] = useState('');
47
44
  const [classNote, setClassNote] = useState('');
48
45
  const [hasSubclass, setHasSubclass] = useState(false);
46
+ const [bulletData, setBulletData] = useState<BulletAnnotationData | undefined>(undefined);
47
+ const [cartridgeCaseData, setCartridgeCaseData] = useState<CartridgeCaseAnnotationData | undefined>(undefined);
48
+ const [shotshellData, setShotshellData] = useState<ShotshellAnnotationData | undefined>(undefined);
49
+ const [isClassDetailsOpen, setIsClassDetailsOpen] = useState(false);
49
50
 
50
51
  // Index state
51
52
  const [indexType, setIndexType] = useState<IndexType>('color');
@@ -65,14 +66,18 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
65
66
  const [isSupportOpen, setIsSupportOpen] = useState(true);
66
67
  const areInputsDisabled = isUploading || isConfirmedImage;
67
68
 
69
+ const notificationHandler = (message: string, type: 'success' | 'error' | 'warning' = 'success') => {
70
+ if (externalShowNotification) {
71
+ externalShowNotification(message, type);
72
+ }
73
+ };
74
+
68
75
  useEffect(() => {
69
76
  const loadExistingNotes = async () => {
70
77
  if (!imageId || !currentCase) return;
71
78
 
72
79
  setIsLoading(true);
73
80
  setLoadError(undefined);
74
- setSaveError(undefined);
75
- setSaveSuccess(false);
76
81
  setIsConfirmedImage(false);
77
82
 
78
83
  try {
@@ -92,6 +97,9 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
92
97
  setCustomClass(existingNotes.customClass || '');
93
98
  setClassNote(existingNotes.classNote || '');
94
99
  setHasSubclass(existingNotes.hasSubclass ?? false);
100
+ setBulletData(existingNotes.bulletData);
101
+ setCartridgeCaseData(existingNotes.cartridgeCaseData);
102
+ setShotshellData(existingNotes.shotshellData);
95
103
  setIndexType(existingNotes.indexType || 'color');
96
104
  setIndexNumber(existingNotes.indexNumber || '');
97
105
  setIndexColor(existingNotes.indexColor || '');
@@ -128,9 +136,6 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
128
136
  return;
129
137
  }
130
138
 
131
- setSaveError(undefined);
132
- setSaveSuccess(false);
133
-
134
139
  let existingData: AnnotationData | null = null;
135
140
 
136
141
  try {
@@ -139,7 +144,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
139
144
 
140
145
  if (existingData?.confirmationData) {
141
146
  setIsConfirmedImage(true);
142
- setSaveError('This image is confirmed. Notes cannot be modified.');
147
+ notificationHandler('This image is confirmed. Notes cannot be modified.', 'error');
143
148
  return;
144
149
  }
145
150
 
@@ -158,6 +163,9 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
158
163
  customClass: customClass,
159
164
  classNote: classNote || undefined,
160
165
  hasSubclass: hasSubclass,
166
+ bulletData: bulletData,
167
+ cartridgeCaseData: cartridgeCaseData,
168
+ shotshellData: shotshellData,
161
169
 
162
170
  // Index Information
163
171
  indexType: indexType,
@@ -193,13 +201,12 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
193
201
  existingData,
194
202
  annotationData,
195
203
  currentCase,
196
- 'notes-sidebar',
204
+ 'notes-editor-form',
197
205
  imageId,
198
206
  originalFileName
199
207
  );
200
208
 
201
- setSaveSuccess(true);
202
- setTimeout(() => setSaveSuccess(false), 3000);
209
+ notificationHandler('Notes saved successfully.', 'success');
203
210
 
204
211
  // Refresh annotation data after saving notes
205
212
  if (onAnnotationRefresh) {
@@ -210,9 +217,9 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
210
217
  const errorMessage = error instanceof Error ? error.message : '';
211
218
  if (errorMessage.toLowerCase().includes('confirmed image')) {
212
219
  setIsConfirmedImage(true);
213
- setSaveError('This image is confirmed. Notes cannot be modified.');
220
+ notificationHandler('This image is confirmed. Notes cannot be modified.', 'error');
214
221
  } else {
215
- setSaveError('Failed to save notes. Please try again.');
222
+ notificationHandler('Failed to save notes. Please try again.', 'error');
216
223
  }
217
224
 
218
225
  // Audit logging for failed annotation save
@@ -223,7 +230,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
223
230
  existingData,
224
231
  null, // Failed save, no new value
225
232
  currentCase,
226
- 'notes-sidebar',
233
+ 'notes-editor-form',
227
234
  imageId,
228
235
  originalFileName
229
236
  );
@@ -234,7 +241,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
234
241
  };
235
242
 
236
243
  return (
237
- <div className={`${styles.notesSidebar} ${compactLayout ? styles.compactLayout : ''}`}>
244
+ <div className={`${styles.notesEditorForm} ${styles.editorLayout}`}>
238
245
  {isLoading ? (
239
246
  <div className={styles.loading}>Loading notes...</div>
240
247
  ) : loadError ? (
@@ -247,10 +254,6 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
247
254
  </div>
248
255
  )}
249
256
 
250
- {saveError && (
251
- <div className={styles.errorMessage}>{saveError}</div>
252
- )}
253
-
254
257
  <div className={styles.section}>
255
258
  <button
256
259
  type="button"
@@ -297,17 +300,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
297
300
  disabled={areInputsDisabled}
298
301
  />
299
302
  </div>
300
- {compactLayout && (
301
- <div className={styles.caseInput}>
302
- <label htmlFor="colorSelect">Font</label>
303
- <ColorSelector
304
- selectedColor={caseFontColor}
305
- onColorSelect={setCaseFontColor}
306
- />
307
- </div>
308
- )}
309
303
  </div>
310
- {!compactLayout && <hr />}
311
304
  {/* Right side inputs */}
312
305
  <div className={styles.inputGroup}>
313
306
  <div className={styles.caseInput}>
@@ -342,21 +335,20 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
342
335
  </div>
343
336
  </div>
344
337
  </div>
345
- {!compactLayout && (
346
- <>
347
- <label htmlFor="colorSelect">Font</label>
348
- <ColorSelector
349
- selectedColor={caseFontColor}
350
- onColorSelect={setCaseFontColor}
351
- />
352
- </>
353
- )}
338
+ <hr />
339
+ <div className={styles.fontColorRow}>
340
+ <label htmlFor="colorSelect">Font</label>
341
+ <ColorSelector
342
+ selectedColor={caseFontColor}
343
+ onColorSelect={setCaseFontColor}
344
+ />
345
+ </div>
354
346
  </>
355
347
  )}
356
348
  </div>
357
349
 
358
- <div className={compactLayout ? styles.compactSectionGrid : undefined}>
359
- <div className={`${styles.section} ${compactLayout ? styles.compactFullSection : ''}`}>
350
+ <div className={styles.compactSectionGrid}>
351
+ <div className={`${styles.section} ${styles.compactFullSection}`}>
360
352
  <button
361
353
  type="button"
362
354
  className={styles.sectionToggle}
@@ -368,7 +360,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
368
360
  </button>
369
361
  {isClassOpen && (
370
362
  <>
371
- <div className={compactLayout ? styles.classCharacteristicsColumns : undefined}>
363
+ <div className={styles.classCharacteristicsColumns}>
372
364
  <div className={styles.classCharacteristicsMain}>
373
365
  <div className={styles.classCharacteristics}>
374
366
  <select
@@ -382,6 +374,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
382
374
  <option value="">Select class type...</option>
383
375
  <option value="Bullet">Bullet</option>
384
376
  <option value="Cartridge Case">Cartridge Case</option>
377
+ <option value="Shotshell">Shotshell</option>
385
378
  <option value="Other">Other</option>
386
379
  </select>
387
380
 
@@ -415,18 +408,22 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
415
408
  </label>
416
409
  </div>
417
410
 
418
- {compactLayout && (
419
- <div className={styles.characteristicsPlaceholder}>
420
- <h6 className={styles.placeholderTitle}>Characteristics Details</h6>
421
- <p className={styles.placeholderText}>This section is reserved for future development.</p>
422
- </div>
423
- )}
411
+ <div className={styles.classDetailsPanel}>
412
+ <button
413
+ type="button"
414
+ onClick={() => setIsClassDetailsOpen(true)}
415
+ className={styles.classDetailsButton}
416
+ disabled={areInputsDisabled}
417
+ >
418
+ Enter Class Characteristic Details
419
+ </button>
420
+ </div>
424
421
  </div>
425
422
  </>
426
423
  )}
427
424
  </div>
428
425
 
429
- <div className={`${styles.section} ${compactLayout ? styles.compactHalfSection : ''}`}>
426
+ <div className={`${styles.section} ${styles.compactHalfSection}`}>
430
427
  <button
431
428
  type="button"
432
429
  className={styles.sectionToggle}
@@ -477,7 +474,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
477
474
  )}
478
475
  </div>
479
476
 
480
- <div className={`${styles.section} ${compactLayout ? styles.compactHalfSection : ''}`}>
477
+ <div className={`${styles.section} ${styles.compactHalfSection}`}>
481
478
  <button
482
479
  type="button"
483
480
  className={styles.sectionToggle}
@@ -538,7 +535,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
538
535
  </button>
539
536
  </div>
540
537
 
541
- <div className={`${styles.notesActionBar} ${stickyActionBar ? styles.notesActionBarSticky : ''}`}>
538
+ <div className={`${styles.notesActionBar} ${styles.notesActionBarSticky}`}>
542
539
  <button
543
540
  onClick={handleSave}
544
541
  className={styles.saveButton}
@@ -547,28 +544,32 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
547
544
  >
548
545
  Save Notes
549
546
  </button>
550
- {showReturnButton && (
551
- <button
552
- onClick={onReturn}
553
- className={styles.returnButton}
554
- disabled={isUploading}
555
- title={isUploading ? "Cannot return while uploading" : undefined}
556
- >
557
- Return to Case Management
558
- </button>
559
- )}
560
547
  </div>
561
-
562
- {saveSuccess && (
563
- <div className={styles.successMessage}>
564
- Notes saved successfully!
565
- </div>
566
- )}
567
- <NotesModal
548
+ <AddlNotesModal
568
549
  isOpen={isModalOpen}
569
550
  onClose={() => setIsModalOpen(false)}
570
551
  notes={additionalNotes}
571
552
  onSave={setAdditionalNotes}
553
+ showNotification={notificationHandler}
554
+ />
555
+ <ClassDetailsModal
556
+ isOpen={isClassDetailsOpen}
557
+ onClose={() => setIsClassDetailsOpen(false)}
558
+ classType={classType}
559
+ bulletData={bulletData}
560
+ cartridgeCaseData={cartridgeCaseData}
561
+ shotshellData={shotshellData}
562
+ onSave={(b, c, s) => {
563
+ if (b !== undefined) setBulletData(b);
564
+ if (c !== undefined) setCartridgeCaseData(c);
565
+ if (s !== undefined) setShotshellData(s);
566
+ const summary = buildClassDetailsSummary(b, c, s, classType);
567
+ if (summary) {
568
+ setAdditionalNotes((prev) => prev ? `${prev}\n${summary}` : summary);
569
+ }
570
+ }}
571
+ showNotification={notificationHandler}
572
+ isReadOnly={areInputsDisabled}
572
573
  />
573
574
  </>
574
575
  )}
@@ -1,6 +1,6 @@
1
1
  import type { User } from 'firebase/auth';
2
2
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
- import { NotesSidebar } from './notes-sidebar';
3
+ import { NotesEditorForm } from './notes-editor-form';
4
4
  import styles from './notes-editor-modal.module.css';
5
5
 
6
6
  interface NotesEditorModalProps {
@@ -12,6 +12,7 @@ interface NotesEditorModalProps {
12
12
  originalFileName?: string;
13
13
  onAnnotationRefresh?: () => void;
14
14
  isUploading?: boolean;
15
+ showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
15
16
  }
16
17
 
17
18
  export const NotesEditorModal = ({
@@ -23,9 +24,9 @@ export const NotesEditorModal = ({
23
24
  originalFileName,
24
25
  onAnnotationRefresh,
25
26
  isUploading = false,
27
+ showNotification,
26
28
  }: NotesEditorModalProps) => {
27
29
  const {
28
- requestClose,
29
30
  overlayProps,
30
31
  getCloseButtonProps,
31
32
  } = useOverlayDismiss({
@@ -47,17 +48,14 @@ export const NotesEditorModal = ({
47
48
  </button>
48
49
  </div>
49
50
  <div className={styles.content}>
50
- <NotesSidebar
51
+ <NotesEditorForm
51
52
  currentCase={currentCase}
52
- onReturn={requestClose}
53
53
  user={user}
54
54
  imageId={imageId}
55
55
  onAnnotationRefresh={onAnnotationRefresh}
56
56
  originalFileName={originalFileName}
57
57
  isUploading={isUploading}
58
- showReturnButton={false}
59
- stickyActionBar={true}
60
- compactLayout={true}
58
+ showNotification={showNotification}
61
59
  />
62
60
  </div>
63
61
  </div>