@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.
- package/LICENSE +1 -1
- package/app/components/actions/case-manage.ts +50 -17
- package/app/components/audit/viewer/audit-entries-list.tsx +5 -2
- package/app/components/audit/viewer/use-audit-viewer-data.ts +6 -3
- package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
- package/app/components/canvas/confirmation/confirmation.tsx +6 -2
- package/app/components/colors/colors.module.css +4 -3
- package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -76
- package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
- package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
- package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
- package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
- package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
- package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
- package/app/components/navbar/navbar.tsx +34 -9
- package/app/components/sidebar/cases/case-sidebar.tsx +93 -73
- package/app/components/sidebar/cases/cases-modal.module.css +312 -10
- package/app/components/sidebar/cases/cases-modal.tsx +737 -116
- package/app/components/sidebar/cases/cases.module.css +43 -0
- package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
- package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
- package/app/components/sidebar/files/files-modal.module.css +285 -44
- package/app/components/sidebar/files/files-modal.tsx +482 -177
- package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
- package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
- package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
- package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
- package/app/components/sidebar/notes/class-details-shared.ts +239 -0
- package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +77 -76
- package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
- package/app/components/sidebar/notes/notes.module.css +262 -14
- package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
- package/app/components/sidebar/sidebar-container.tsx +2 -0
- package/app/components/sidebar/sidebar.tsx +15 -1
- package/app/{tailwind.css → global.css} +1 -3
- package/app/hooks/useCaseListPreferences.ts +99 -0
- package/app/hooks/useFileListPreferences.ts +106 -0
- package/app/hooks/useOverlayDismiss.ts +6 -4
- package/app/root.tsx +1 -1
- package/app/routes/striae/striae.tsx +7 -0
- package/app/services/audit/audit.service.ts +2 -2
- package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
- package/app/types/annotations.ts +48 -1
- package/app/types/audit.ts +1 -0
- package/app/utils/data/case-filters.ts +127 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +295 -0
- package/app/utils/data/data-operations.ts +17 -861
- package/app/utils/data/file-filters.ts +201 -0
- package/app/utils/data/index.ts +11 -1
- package/app/utils/data/operations/batch-operations.ts +113 -0
- package/app/utils/data/operations/case-operations.ts +168 -0
- package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
- package/app/utils/data/operations/file-annotation-operations.ts +196 -0
- package/app/utils/data/operations/index.ts +7 -0
- package/app/utils/data/operations/signing-operations.ts +225 -0
- package/app/utils/data/operations/types.ts +42 -0
- package/app/utils/data/operations/validation-operations.ts +48 -0
- package/app/utils/forensics/export-verification.ts +40 -111
- package/functions/api/_shared/firebase-auth.ts +2 -7
- package/functions/api/image/[[path]].ts +23 -22
- package/functions/api/pdf/[[path]].ts +27 -8
- package/package.json +7 -13
- package/scripts/deploy-primershear-emails.sh +1 -1
- package/worker-configuration.d.ts +2 -2
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/image-worker.example.ts +16 -5
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +84 -124
- package/workers/pdf-worker/src/pdf-worker.example.ts +58 -61
- package/workers/pdf-worker/src/report-layout.ts +227 -0
- package/workers/pdf-worker/src/report-types.ts +23 -3
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/src/user-worker.example.ts +17 -0
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/NOTICE +0 -13
- package/app/components/sidebar/notes/notes-modal.tsx +0 -52
- package/postcss.config.js +0 -6
- package/tailwind.config.ts +0 -22
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- /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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
204
|
+
'notes-editor-form',
|
|
197
205
|
imageId,
|
|
198
206
|
originalFileName
|
|
199
207
|
);
|
|
200
208
|
|
|
201
|
-
|
|
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
|
-
|
|
220
|
+
notificationHandler('This image is confirmed. Notes cannot be modified.', 'error');
|
|
214
221
|
} else {
|
|
215
|
-
|
|
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-
|
|
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.
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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={
|
|
359
|
-
<div className={`${styles.section} ${
|
|
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={
|
|
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
|
-
{
|
|
419
|
-
<
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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} ${
|
|
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} ${
|
|
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} ${
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
|
|
59
|
-
stickyActionBar={true}
|
|
60
|
-
compactLayout={true}
|
|
58
|
+
showNotification={showNotification}
|
|
61
59
|
/>
|
|
62
60
|
</div>
|
|
63
61
|
</div>
|