@striae-org/striae 4.2.1 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/app/components/actions/case-import/confirmation-import.ts +20 -1
  2. package/app/components/actions/case-import/orchestrator.ts +3 -0
  3. package/app/components/actions/case-manage.ts +5 -1
  4. package/app/components/actions/confirm-export.ts +12 -3
  5. package/app/components/audit/viewer/audit-entries-list.tsx +20 -2
  6. package/app/components/audit/viewer/use-audit-viewer-export.ts +2 -2
  7. package/app/components/audit/viewer/use-audit-viewer-filters.ts +11 -1
  8. package/app/components/canvas/canvas.tsx +2 -1
  9. package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -76
  10. package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
  11. package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
  12. package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
  13. package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
  14. package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
  15. package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
  16. package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
  17. package/app/components/navbar/navbar.module.css +11 -0
  18. package/app/components/navbar/navbar.tsx +38 -19
  19. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -0
  20. package/app/components/sidebar/cases/case-sidebar.tsx +27 -3
  21. package/app/components/sidebar/cases/cases-modal.module.css +312 -10
  22. package/app/components/sidebar/cases/cases-modal.tsx +690 -110
  23. package/app/components/sidebar/cases/cases.module.css +23 -0
  24. package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
  25. package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
  26. package/app/components/sidebar/files/files-modal.module.css +285 -44
  27. package/app/components/sidebar/files/files-modal.tsx +452 -145
  28. package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
  29. package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
  30. package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
  31. package/app/components/sidebar/notes/class-details-shared.ts +239 -0
  32. package/app/components/sidebar/notes/notes-editor-form.tsx +43 -5
  33. package/app/components/sidebar/notes/notes.module.css +236 -4
  34. package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
  35. package/app/components/sidebar/sidebar-container.tsx +2 -0
  36. package/app/components/sidebar/sidebar.tsx +8 -1
  37. package/app/hooks/useCaseListPreferences.ts +99 -0
  38. package/app/hooks/useFileListPreferences.ts +106 -0
  39. package/app/routes/striae/striae.tsx +45 -1
  40. package/app/services/audit/audit-export-csv.ts +4 -2
  41. package/app/services/audit/audit-export-report.ts +36 -4
  42. package/app/services/audit/audit.service.ts +2 -0
  43. package/app/services/audit/builders/audit-entry-builder.ts +1 -0
  44. package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -2
  45. package/app/types/annotations.ts +48 -1
  46. package/app/types/audit.ts +1 -0
  47. package/app/utils/data/case-filters.ts +127 -0
  48. package/app/utils/data/confirmation-summary/summary-core.ts +18 -2
  49. package/app/utils/data/file-filters.ts +201 -0
  50. package/app/utils/forensics/confirmation-signature.ts +20 -5
  51. package/functions/api/image/[[path]].ts +4 -0
  52. package/package.json +3 -4
  53. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  54. package/workers/data-worker/src/signing-payload-utils.ts +5 -0
  55. package/workers/data-worker/wrangler.jsonc.example +1 -1
  56. package/workers/image-worker/wrangler.jsonc.example +1 -1
  57. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  58. package/workers/pdf-worker/src/formats/format-striae.ts +84 -118
  59. package/workers/pdf-worker/src/pdf-worker.example.ts +28 -10
  60. package/workers/pdf-worker/src/report-layout.ts +227 -0
  61. package/workers/pdf-worker/src/report-types.ts +20 -0
  62. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  63. package/workers/user-worker/wrangler.jsonc.example +1 -1
  64. package/wrangler.toml.example +1 -1
  65. package/workers/pdf-worker/src/assets/icon-256.png +0 -0
  66. /package/workers/pdf-worker/src/assets/{generated-assets.ts → generated-assets.example.ts} +0 -0
@@ -0,0 +1,371 @@
1
+ import { useState } from 'react';
2
+ import type {
3
+ BulletAnnotationData,
4
+ CartridgeCaseAnnotationData,
5
+ ShotshellAnnotationData,
6
+ } from '~/types/annotations';
7
+ import {
8
+ ALL_CALIBERS,
9
+ BULLET_BARREL_TYPE_OPTIONS,
10
+ BULLET_CORE_METAL_OPTIONS,
11
+ BULLET_JACKET_METAL_OPTIONS,
12
+ BULLET_TYPE_OPTIONS,
13
+ CARTRIDGE_APERTURE_SHAPE_OPTIONS,
14
+ CARTRIDGE_FPI_SHAPE_OPTIONS,
15
+ CARTRIDGE_METAL_OPTIONS,
16
+ CARTRIDGE_PRIMER_TYPE_OPTIONS,
17
+ SHOTSHELL_GAUGES,
18
+ calculateBulletDiameter,
19
+ formatCalculatedDiameter,
20
+ isCustomValue,
21
+ } from './class-details-shared';
22
+
23
+ interface UseClassDetailsStateParams {
24
+ bulletData?: BulletAnnotationData;
25
+ cartridgeCaseData?: CartridgeCaseAnnotationData;
26
+ shotshellData?: ShotshellAnnotationData;
27
+ }
28
+
29
+ interface BuildSaveDataParams {
30
+ showBullet: boolean;
31
+ showCartridge: boolean;
32
+ showShotshell: boolean;
33
+ }
34
+
35
+ interface BuildSaveDataResult {
36
+ bulletData: BulletAnnotationData | undefined;
37
+ cartridgeCaseData: CartridgeCaseAnnotationData | undefined;
38
+ shotshellData: ShotshellAnnotationData | undefined;
39
+ }
40
+
41
+ export interface BulletDetailsState {
42
+ caliber: string;
43
+ caliberIsCustom: boolean;
44
+ mass: string;
45
+ diameter: string;
46
+ lgNumber: string;
47
+ lgDirection: string;
48
+ lWidths: string[];
49
+ gWidths: string[];
50
+ jacketMetal: string;
51
+ jacketMetalIsCustom: boolean;
52
+ coreMetal: string;
53
+ coreMetalIsCustom: boolean;
54
+ bulletType: string;
55
+ bulletTypeIsCustom: boolean;
56
+ barrelType: string;
57
+ barrelTypeIsCustom: boolean;
58
+ lgCount: number;
59
+ calculatedDiameter: number | null;
60
+ setCaliber: (value: string) => void;
61
+ setCaliberIsCustom: (value: boolean) => void;
62
+ setMass: (value: string) => void;
63
+ setDiameter: (value: string) => void;
64
+ setLgNumber: (value: string) => void;
65
+ setLgDirection: (value: string) => void;
66
+ updateLWidth: (index: number, value: string) => void;
67
+ updateGWidth: (index: number, value: string) => void;
68
+ setJacketMetal: (value: string) => void;
69
+ setJacketMetalIsCustom: (value: boolean) => void;
70
+ setCoreMetal: (value: string) => void;
71
+ setCoreMetalIsCustom: (value: boolean) => void;
72
+ setBulletType: (value: string) => void;
73
+ setBulletTypeIsCustom: (value: boolean) => void;
74
+ setBarrelType: (value: string) => void;
75
+ setBarrelTypeIsCustom: (value: boolean) => void;
76
+ }
77
+
78
+ export interface CartridgeCaseDetailsState {
79
+ caliber: string;
80
+ caliberIsCustom: boolean;
81
+ brand: string;
82
+ metal: string;
83
+ metalIsCustom: boolean;
84
+ primerType: string;
85
+ primerTypeIsCustom: boolean;
86
+ fpiShape: string;
87
+ fpiShapeIsCustom: boolean;
88
+ apertureShape: string;
89
+ apertureShapeIsCustom: boolean;
90
+ hasFpDrag: boolean;
91
+ hasExtractorMarks: boolean;
92
+ hasEjectorMarks: boolean;
93
+ hasChamberMarks: boolean;
94
+ hasMagazineLipMarks: boolean;
95
+ hasPrimerShear: boolean;
96
+ hasEjectionPortMarks: boolean;
97
+ setCaliber: (value: string) => void;
98
+ setCaliberIsCustom: (value: boolean) => void;
99
+ setBrand: (value: string) => void;
100
+ setMetal: (value: string) => void;
101
+ setMetalIsCustom: (value: boolean) => void;
102
+ setPrimerType: (value: string) => void;
103
+ setPrimerTypeIsCustom: (value: boolean) => void;
104
+ setFpiShape: (value: string) => void;
105
+ setFpiShapeIsCustom: (value: boolean) => void;
106
+ setApertureShape: (value: string) => void;
107
+ setApertureShapeIsCustom: (value: boolean) => void;
108
+ setHasFpDrag: (value: boolean) => void;
109
+ setHasExtractorMarks: (value: boolean) => void;
110
+ setHasEjectorMarks: (value: boolean) => void;
111
+ setHasChamberMarks: (value: boolean) => void;
112
+ setHasMagazineLipMarks: (value: boolean) => void;
113
+ setHasPrimerShear: (value: boolean) => void;
114
+ setHasEjectionPortMarks: (value: boolean) => void;
115
+ }
116
+
117
+ export interface ShotshellDetailsState {
118
+ gauge: string;
119
+ gaugeIsCustom: boolean;
120
+ shotSize: string;
121
+ metal: string;
122
+ metalIsCustom: boolean;
123
+ brand: string;
124
+ fpiShape: string;
125
+ fpiShapeIsCustom: boolean;
126
+ hasExtractorMarks: boolean;
127
+ hasEjectorMarks: boolean;
128
+ hasChamberMarks: boolean;
129
+ setGauge: (value: string) => void;
130
+ setGaugeIsCustom: (value: boolean) => void;
131
+ setShotSize: (value: string) => void;
132
+ setMetal: (value: string) => void;
133
+ setMetalIsCustom: (value: boolean) => void;
134
+ setBrand: (value: string) => void;
135
+ setFpiShape: (value: string) => void;
136
+ setFpiShapeIsCustom: (value: boolean) => void;
137
+ setHasExtractorMarks: (value: boolean) => void;
138
+ setHasEjectorMarks: (value: boolean) => void;
139
+ setHasChamberMarks: (value: boolean) => void;
140
+ }
141
+
142
+ export const useClassDetailsState = ({
143
+ bulletData,
144
+ cartridgeCaseData,
145
+ shotshellData,
146
+ }: UseClassDetailsStateParams) => {
147
+ const [bCaliber, setBCaliber] = useState(() => bulletData?.caliber || '');
148
+ const [bCaliberIsCustom, setBCaliberIsCustom] = useState(() => isCustomValue(bulletData?.caliber, ALL_CALIBERS));
149
+ const [bMass, setBMass] = useState(() => bulletData?.mass || '');
150
+ const [bDiameter, setBDiameter] = useState(() => bulletData?.diameter || '');
151
+ const [bLgNumber, setBLgNumber] = useState(() => bulletData?.lgNumber !== undefined ? String(bulletData.lgNumber) : '');
152
+ const [bLgDirection, setBLgDirection] = useState(() => bulletData?.lgDirection || '');
153
+ const [bLWidths, setBLWidths] = useState<string[]>(() => bulletData?.lWidths || []);
154
+ const [bGWidths, setBGWidths] = useState<string[]>(() => bulletData?.gWidths || []);
155
+ const [bJacketMetal, setBJacketMetal] = useState(() => bulletData?.jacketMetal || '');
156
+ const [bJacketMetalIsCustom, setBJacketMetalIsCustom] = useState(() => isCustomValue(bulletData?.jacketMetal, BULLET_JACKET_METAL_OPTIONS));
157
+ const [bCoreMetal, setBCoreMetal] = useState(() => bulletData?.coreMetal || '');
158
+ const [bCoreMetalIsCustom, setBCoreMetalIsCustom] = useState(() => isCustomValue(bulletData?.coreMetal, BULLET_CORE_METAL_OPTIONS));
159
+ const [bBulletType, setBBulletType] = useState(() => bulletData?.bulletType || '');
160
+ const [bBulletTypeIsCustom, setBBulletTypeIsCustom] = useState(() => isCustomValue(bulletData?.bulletType, BULLET_TYPE_OPTIONS));
161
+ const [bBarrelType, setBBarrelType] = useState(() => bulletData?.barrelType || '');
162
+ const [bBarrelTypeIsCustom, setBBarrelTypeIsCustom] = useState(() => isCustomValue(bulletData?.barrelType, BULLET_BARREL_TYPE_OPTIONS));
163
+
164
+ const [cCaliber, setCCaliber] = useState(() => cartridgeCaseData?.caliber || '');
165
+ const [cCaliberIsCustom, setCCaliberIsCustom] = useState(() => isCustomValue(cartridgeCaseData?.caliber, ALL_CALIBERS));
166
+ const [cBrand, setCBrand] = useState(() => cartridgeCaseData?.brand || '');
167
+ const [cMetal, setCMetal] = useState(() => cartridgeCaseData?.metal || '');
168
+ const [cMetalIsCustom, setCMetalIsCustom] = useState(() => isCustomValue(cartridgeCaseData?.metal, CARTRIDGE_METAL_OPTIONS));
169
+ const [cPrimerType, setCPrimerType] = useState(() => cartridgeCaseData?.primerType || '');
170
+ const [cPrimerTypeIsCustom, setCPrimerTypeIsCustom] = useState(() => isCustomValue(cartridgeCaseData?.primerType, CARTRIDGE_PRIMER_TYPE_OPTIONS));
171
+ const [cFpiShape, setCFpiShape] = useState(() => cartridgeCaseData?.fpiShape || '');
172
+ const [cFpiShapeIsCustom, setCFpiShapeIsCustom] = useState(() => isCustomValue(cartridgeCaseData?.fpiShape, CARTRIDGE_FPI_SHAPE_OPTIONS));
173
+ const [cApertureShape, setCApertureShape] = useState(() => cartridgeCaseData?.apertureShape || '');
174
+ const [cApertureShapeIsCustom, setCApertureShapeIsCustom] = useState(() => isCustomValue(cartridgeCaseData?.apertureShape, CARTRIDGE_APERTURE_SHAPE_OPTIONS));
175
+ const [cHasFpDrag, setCHasFpDrag] = useState(() => cartridgeCaseData?.hasFpDrag ?? false);
176
+ const [cHasExtractorMarks, setCHasExtractorMarks] = useState(() => cartridgeCaseData?.hasExtractorMarks ?? false);
177
+ const [cHasEjectorMarks, setCHasEjectorMarks] = useState(() => cartridgeCaseData?.hasEjectorMarks ?? false);
178
+ const [cHasChamberMarks, setCHasChamberMarks] = useState(() => cartridgeCaseData?.hasChamberMarks ?? false);
179
+ const [cHasMagazineLipMarks, setCHasMagazineLipMarks] = useState(() => cartridgeCaseData?.hasMagazineLipMarks ?? false);
180
+ const [cHasPrimerShear, setCHasPrimerShear] = useState(() => cartridgeCaseData?.hasPrimerShear ?? false);
181
+ const [cHasEjectionPortMarks, setCHasEjectionPortMarks] = useState(() => cartridgeCaseData?.hasEjectionPortMarks ?? false);
182
+
183
+ const [sGauge, setSGauge] = useState(() => shotshellData?.gauge || '');
184
+ const [sGaugeIsCustom, setSGaugeIsCustom] = useState(() => isCustomValue(shotshellData?.gauge, SHOTSHELL_GAUGES));
185
+ const [sShotSize, setSShotSize] = useState(() => shotshellData?.shotSize || '');
186
+ const [sMetal, setSMetal] = useState(() => shotshellData?.metal || '');
187
+ const [sMetalIsCustom, setSMetalIsCustom] = useState(() => isCustomValue(shotshellData?.metal, CARTRIDGE_METAL_OPTIONS));
188
+ const [sBrand, setSBrand] = useState(() => shotshellData?.brand || '');
189
+ const [sFpiShape, setSFpiShape] = useState(() => shotshellData?.fpiShape || '');
190
+ const [sFpiShapeIsCustom, setSFpiShapeIsCustom] = useState(() => isCustomValue(shotshellData?.fpiShape, CARTRIDGE_FPI_SHAPE_OPTIONS));
191
+ const [sHasExtractorMarks, setSHasExtractorMarks] = useState(() => shotshellData?.hasExtractorMarks ?? false);
192
+ const [sHasEjectorMarks, setSHasEjectorMarks] = useState(() => shotshellData?.hasEjectorMarks ?? false);
193
+ const [sHasChamberMarks, setSHasChamberMarks] = useState(() => shotshellData?.hasChamberMarks ?? false);
194
+
195
+ const [isSaving, setIsSaving] = useState(false);
196
+
197
+ const lgCount = Math.max(0, Math.min(30, Number(bLgNumber) || 0));
198
+ const calculatedDiameter = calculateBulletDiameter(lgCount, bLWidths, bGWidths);
199
+
200
+ const updateLWidth = (index: number, value: string) => {
201
+ setBLWidths((previous) => {
202
+ const next = [...previous];
203
+ next[index] = value;
204
+ return next;
205
+ });
206
+ };
207
+
208
+ const updateGWidth = (index: number, value: string) => {
209
+ setBGWidths((previous) => {
210
+ const next = [...previous];
211
+ next[index] = value;
212
+ return next;
213
+ });
214
+ };
215
+
216
+ const buildSaveData = ({
217
+ showBullet,
218
+ showCartridge,
219
+ showShotshell,
220
+ }: BuildSaveDataParams): BuildSaveDataResult => ({
221
+ bulletData: showBullet ? {
222
+ caliber: bCaliber || undefined,
223
+ mass: bMass || undefined,
224
+ diameter: bDiameter || undefined,
225
+ calcDiameter: calculatedDiameter !== null ? formatCalculatedDiameter(calculatedDiameter) : undefined,
226
+ lgNumber: bLgNumber ? Number(bLgNumber) : undefined,
227
+ lgDirection: bLgDirection || undefined,
228
+ lWidths: bLWidths.some(Boolean) ? bLWidths : undefined,
229
+ gWidths: bGWidths.some(Boolean) ? bGWidths : undefined,
230
+ jacketMetal: bJacketMetal || undefined,
231
+ coreMetal: bCoreMetal || undefined,
232
+ bulletType: bBulletType || undefined,
233
+ barrelType: bBarrelType || undefined,
234
+ } : undefined,
235
+ cartridgeCaseData: showCartridge ? {
236
+ caliber: cCaliber || undefined,
237
+ brand: cBrand || undefined,
238
+ metal: cMetal || undefined,
239
+ primerType: cPrimerType || undefined,
240
+ fpiShape: cFpiShape || undefined,
241
+ apertureShape: cApertureShape || undefined,
242
+ hasFpDrag: cHasFpDrag || undefined,
243
+ hasExtractorMarks: cHasExtractorMarks || undefined,
244
+ hasEjectorMarks: cHasEjectorMarks || undefined,
245
+ hasChamberMarks: cHasChamberMarks || undefined,
246
+ hasMagazineLipMarks: cHasMagazineLipMarks || undefined,
247
+ hasPrimerShear: cHasPrimerShear || undefined,
248
+ hasEjectionPortMarks: cHasEjectionPortMarks || undefined,
249
+ } : undefined,
250
+ shotshellData: showShotshell ? {
251
+ gauge: sGauge || undefined,
252
+ shotSize: sShotSize || undefined,
253
+ metal: sMetal || undefined,
254
+ brand: sBrand || undefined,
255
+ fpiShape: sFpiShape || undefined,
256
+ hasExtractorMarks: sHasExtractorMarks || undefined,
257
+ hasEjectorMarks: sHasEjectorMarks || undefined,
258
+ hasChamberMarks: sHasChamberMarks || undefined,
259
+ } : undefined,
260
+ });
261
+
262
+ const bullet: BulletDetailsState = {
263
+ caliber: bCaliber,
264
+ caliberIsCustom: bCaliberIsCustom,
265
+ mass: bMass,
266
+ diameter: bDiameter,
267
+ lgNumber: bLgNumber,
268
+ lgDirection: bLgDirection,
269
+ lWidths: bLWidths,
270
+ gWidths: bGWidths,
271
+ jacketMetal: bJacketMetal,
272
+ jacketMetalIsCustom: bJacketMetalIsCustom,
273
+ coreMetal: bCoreMetal,
274
+ coreMetalIsCustom: bCoreMetalIsCustom,
275
+ bulletType: bBulletType,
276
+ bulletTypeIsCustom: bBulletTypeIsCustom,
277
+ barrelType: bBarrelType,
278
+ barrelTypeIsCustom: bBarrelTypeIsCustom,
279
+ lgCount,
280
+ calculatedDiameter,
281
+ setCaliber: setBCaliber,
282
+ setCaliberIsCustom: setBCaliberIsCustom,
283
+ setMass: setBMass,
284
+ setDiameter: setBDiameter,
285
+ setLgNumber: setBLgNumber,
286
+ setLgDirection: setBLgDirection,
287
+ updateLWidth,
288
+ updateGWidth,
289
+ setJacketMetal: setBJacketMetal,
290
+ setJacketMetalIsCustom: setBJacketMetalIsCustom,
291
+ setCoreMetal: setBCoreMetal,
292
+ setCoreMetalIsCustom: setBCoreMetalIsCustom,
293
+ setBulletType: setBBulletType,
294
+ setBulletTypeIsCustom: setBBulletTypeIsCustom,
295
+ setBarrelType: setBBarrelType,
296
+ setBarrelTypeIsCustom: setBBarrelTypeIsCustom,
297
+ };
298
+
299
+ const cartridgeCase: CartridgeCaseDetailsState = {
300
+ caliber: cCaliber,
301
+ caliberIsCustom: cCaliberIsCustom,
302
+ brand: cBrand,
303
+ metal: cMetal,
304
+ metalIsCustom: cMetalIsCustom,
305
+ primerType: cPrimerType,
306
+ primerTypeIsCustom: cPrimerTypeIsCustom,
307
+ fpiShape: cFpiShape,
308
+ fpiShapeIsCustom: cFpiShapeIsCustom,
309
+ apertureShape: cApertureShape,
310
+ apertureShapeIsCustom: cApertureShapeIsCustom,
311
+ hasFpDrag: cHasFpDrag,
312
+ hasExtractorMarks: cHasExtractorMarks,
313
+ hasEjectorMarks: cHasEjectorMarks,
314
+ hasChamberMarks: cHasChamberMarks,
315
+ hasMagazineLipMarks: cHasMagazineLipMarks,
316
+ hasPrimerShear: cHasPrimerShear,
317
+ hasEjectionPortMarks: cHasEjectionPortMarks,
318
+ setCaliber: setCCaliber,
319
+ setCaliberIsCustom: setCCaliberIsCustom,
320
+ setBrand: setCBrand,
321
+ setMetal: setCMetal,
322
+ setMetalIsCustom: setCMetalIsCustom,
323
+ setPrimerType: setCPrimerType,
324
+ setPrimerTypeIsCustom: setCPrimerTypeIsCustom,
325
+ setFpiShape: setCFpiShape,
326
+ setFpiShapeIsCustom: setCFpiShapeIsCustom,
327
+ setApertureShape: setCApertureShape,
328
+ setApertureShapeIsCustom: setCApertureShapeIsCustom,
329
+ setHasFpDrag: setCHasFpDrag,
330
+ setHasExtractorMarks: setCHasExtractorMarks,
331
+ setHasEjectorMarks: setCHasEjectorMarks,
332
+ setHasChamberMarks: setCHasChamberMarks,
333
+ setHasMagazineLipMarks: setCHasMagazineLipMarks,
334
+ setHasPrimerShear: setCHasPrimerShear,
335
+ setHasEjectionPortMarks: setCHasEjectionPortMarks,
336
+ };
337
+
338
+ const shotshell: ShotshellDetailsState = {
339
+ gauge: sGauge,
340
+ gaugeIsCustom: sGaugeIsCustom,
341
+ shotSize: sShotSize,
342
+ metal: sMetal,
343
+ metalIsCustom: sMetalIsCustom,
344
+ brand: sBrand,
345
+ fpiShape: sFpiShape,
346
+ fpiShapeIsCustom: sFpiShapeIsCustom,
347
+ hasExtractorMarks: sHasExtractorMarks,
348
+ hasEjectorMarks: sHasEjectorMarks,
349
+ hasChamberMarks: sHasChamberMarks,
350
+ setGauge: setSGauge,
351
+ setGaugeIsCustom: setSGaugeIsCustom,
352
+ setShotSize: setSShotSize,
353
+ setMetal: setSMetal,
354
+ setMetalIsCustom: setSMetalIsCustom,
355
+ setBrand: setSBrand,
356
+ setFpiShape: setSFpiShape,
357
+ setFpiShapeIsCustom: setSFpiShapeIsCustom,
358
+ setHasExtractorMarks: setSHasExtractorMarks,
359
+ setHasEjectorMarks: setSHasEjectorMarks,
360
+ setHasChamberMarks: setSHasChamberMarks,
361
+ };
362
+
363
+ return {
364
+ bullet,
365
+ cartridgeCase,
366
+ shotshell,
367
+ isSaving,
368
+ setIsSaving,
369
+ buildSaveData,
370
+ };
371
+ };
@@ -24,10 +24,12 @@ interface SidebarContainerProps {
24
24
  setShowNotes: (show: boolean) => void;
25
25
  onAnnotationRefresh?: () => void;
26
26
  isReadOnly?: boolean;
27
+ isArchivedCase?: boolean;
27
28
  isConfirmed?: boolean;
28
29
  confirmationSaveVersion?: number;
29
30
  isUploading?: boolean;
30
31
  onUploadStatusChange?: (isUploading: boolean) => void;
32
+ onOpenCaseExport?: () => void;
31
33
  }
32
34
 
33
35
  export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
@@ -1,4 +1,5 @@
1
1
  import type { User } from 'firebase/auth';
2
+ import type React from 'react';
2
3
  import { useState, useCallback } from 'react';
3
4
  import styles from './sidebar.module.css';
4
5
  import { CaseSidebar } from './cases/case-sidebar';
@@ -19,10 +20,12 @@ interface SidebarProps {
19
20
  setShowNotes: (show: boolean) => void;
20
21
  onAnnotationRefresh?: () => void;
21
22
  isReadOnly?: boolean;
23
+ isArchivedCase?: boolean;
22
24
  isConfirmed?: boolean;
23
25
  confirmationSaveVersion?: number;
24
26
  isUploading?: boolean;
25
27
  onUploadStatusChange?: (isUploading: boolean) => void;
28
+ onOpenCaseExport?: () => void;
26
29
  }
27
30
 
28
31
  export const Sidebar = ({
@@ -37,10 +40,12 @@ export const Sidebar = ({
37
40
  setFiles,
38
41
  setShowNotes,
39
42
  isReadOnly = false,
43
+ isArchivedCase = false,
40
44
  isConfirmed = false,
41
45
  confirmationSaveVersion = 0,
42
46
  isUploading: initialIsUploading = false,
43
47
  onUploadStatusChange,
48
+ onOpenCaseExport,
44
49
  }: SidebarProps) => {
45
50
  const [isUploading, setIsUploading] = useState(initialIsUploading);
46
51
  const [toastMessage, setToastMessage] = useState('');
@@ -69,7 +74,7 @@ export const Sidebar = ({
69
74
  setToastMessage(`${result.successCount} file${result.successCount !== 1 ? 's' : ''} uploaded!`);
70
75
  }
71
76
  setIsToastVisible(true);
72
- }, []);
77
+ }, []);
73
78
 
74
79
  return (
75
80
  <div className={styles.sidebar}>
@@ -84,12 +89,14 @@ export const Sidebar = ({
84
89
  setFiles={setFiles}
85
90
  onNotesClick={() => setShowNotes(true)}
86
91
  isReadOnly={isReadOnly}
92
+ isArchivedCase={isArchivedCase}
87
93
  isConfirmed={isConfirmed}
88
94
  confirmationSaveVersion={confirmationSaveVersion}
89
95
  selectedFileId={imageId}
90
96
  isUploading={isUploading}
91
97
  onUploadStatusChange={handleUploadStatusChange}
92
98
  onUploadComplete={handleUploadComplete}
99
+ onOpenCaseExport={onOpenCaseExport}
93
100
  />
94
101
  <Toast
95
102
  message={toastMessage}
@@ -0,0 +1,99 @@
1
+ import { useEffect, useState } from 'react';
2
+ import {
3
+ type CasesModalPreferences,
4
+ type CasesModalSortBy,
5
+ type CasesModalConfirmationFilter,
6
+ } from '~/utils/data/case-filters';
7
+
8
+ const CASES_MODAL_PREFERENCES_STORAGE_KEY = 'striae.casesModal.preferences';
9
+
10
+ export const DEFAULT_CASES_MODAL_PREFERENCES: CasesModalPreferences = {
11
+ sortBy: 'recent',
12
+ confirmationFilter: 'all',
13
+ showArchivedOnly: false,
14
+ };
15
+
16
+ function parseStoredPreferences(value: string | null): CasesModalPreferences {
17
+ if (!value) {
18
+ return DEFAULT_CASES_MODAL_PREFERENCES;
19
+ }
20
+
21
+ try {
22
+ const parsed = JSON.parse(value) as Partial<CasesModalPreferences>;
23
+
24
+ const sortBy: CasesModalSortBy =
25
+ parsed.sortBy === 'alphabetical' || parsed.sortBy === 'recent'
26
+ ? parsed.sortBy
27
+ : DEFAULT_CASES_MODAL_PREFERENCES.sortBy;
28
+
29
+ const confirmationFilter: CasesModalConfirmationFilter =
30
+ parsed.confirmationFilter === 'pending' ||
31
+ parsed.confirmationFilter === 'confirmed' ||
32
+ parsed.confirmationFilter === 'none-requested' ||
33
+ parsed.confirmationFilter === 'all'
34
+ ? parsed.confirmationFilter
35
+ : DEFAULT_CASES_MODAL_PREFERENCES.confirmationFilter;
36
+
37
+ const showArchivedOnly =
38
+ typeof parsed.showArchivedOnly === 'boolean'
39
+ ? parsed.showArchivedOnly
40
+ : DEFAULT_CASES_MODAL_PREFERENCES.showArchivedOnly;
41
+
42
+ return {
43
+ sortBy,
44
+ confirmationFilter,
45
+ showArchivedOnly,
46
+ };
47
+ } catch {
48
+ return DEFAULT_CASES_MODAL_PREFERENCES;
49
+ }
50
+ }
51
+
52
+ function loadCasesModalPreferences(): CasesModalPreferences {
53
+ if (typeof window === 'undefined') {
54
+ return DEFAULT_CASES_MODAL_PREFERENCES;
55
+ }
56
+
57
+ return parseStoredPreferences(window.localStorage.getItem(CASES_MODAL_PREFERENCES_STORAGE_KEY));
58
+ }
59
+
60
+ export function useCaseListPreferences() {
61
+ const [preferences, setPreferences] = useState<CasesModalPreferences>(() =>
62
+ loadCasesModalPreferences()
63
+ );
64
+
65
+ useEffect(() => {
66
+ if (typeof window === 'undefined') {
67
+ return;
68
+ }
69
+
70
+ window.localStorage.setItem(
71
+ CASES_MODAL_PREFERENCES_STORAGE_KEY,
72
+ JSON.stringify(preferences)
73
+ );
74
+ }, [preferences]);
75
+
76
+ const setSortBy = (sortBy: CasesModalSortBy) => {
77
+ setPreferences((current) => ({ ...current, sortBy }));
78
+ };
79
+
80
+ const setConfirmationFilter = (confirmationFilter: CasesModalConfirmationFilter) => {
81
+ setPreferences((current) => ({ ...current, confirmationFilter }));
82
+ };
83
+
84
+ const setShowArchivedOnly = (showArchivedOnly: boolean) => {
85
+ setPreferences((current) => ({ ...current, showArchivedOnly }));
86
+ };
87
+
88
+ const resetPreferences = () => {
89
+ setPreferences(DEFAULT_CASES_MODAL_PREFERENCES);
90
+ };
91
+
92
+ return {
93
+ preferences,
94
+ setSortBy,
95
+ setConfirmationFilter,
96
+ setShowArchivedOnly,
97
+ resetPreferences,
98
+ };
99
+ }
@@ -0,0 +1,106 @@
1
+ import { useEffect, useState } from 'react';
2
+ import {
3
+ type FilesModalPreferences,
4
+ type FilesModalSortBy,
5
+ type FilesModalConfirmationFilter,
6
+ type FilesModalClassTypeFilter,
7
+ } from '~/utils/data/file-filters';
8
+
9
+ const FILES_MODAL_PREFERENCES_STORAGE_KEY = 'striae.filesModal.preferences';
10
+
11
+ export const DEFAULT_FILES_MODAL_PREFERENCES: FilesModalPreferences = {
12
+ sortBy: 'recent',
13
+ confirmationFilter: 'all',
14
+ classTypeFilter: 'all',
15
+ };
16
+
17
+ function parseStoredPreferences(value: string | null): FilesModalPreferences {
18
+ if (!value) {
19
+ return DEFAULT_FILES_MODAL_PREFERENCES;
20
+ }
21
+
22
+ try {
23
+ const parsed = JSON.parse(value) as Partial<FilesModalPreferences>;
24
+
25
+ const sortBy: FilesModalSortBy =
26
+ parsed.sortBy === 'filename' ||
27
+ parsed.sortBy === 'confirmation' ||
28
+ parsed.sortBy === 'classType' ||
29
+ parsed.sortBy === 'recent'
30
+ ? parsed.sortBy
31
+ : DEFAULT_FILES_MODAL_PREFERENCES.sortBy;
32
+
33
+ const confirmationFilter: FilesModalConfirmationFilter =
34
+ parsed.confirmationFilter === 'pending' ||
35
+ parsed.confirmationFilter === 'confirmed' ||
36
+ parsed.confirmationFilter === 'none-requested' ||
37
+ parsed.confirmationFilter === 'all'
38
+ ? parsed.confirmationFilter
39
+ : DEFAULT_FILES_MODAL_PREFERENCES.confirmationFilter;
40
+
41
+ const classTypeFilter: FilesModalClassTypeFilter =
42
+ parsed.classTypeFilter === 'Bullet' ||
43
+ parsed.classTypeFilter === 'Cartridge Case' ||
44
+ parsed.classTypeFilter === 'Shotshell' ||
45
+ parsed.classTypeFilter === 'Other' ||
46
+ parsed.classTypeFilter === 'all'
47
+ ? parsed.classTypeFilter
48
+ : parsed.classTypeFilter === 'unset'
49
+ ? 'Other'
50
+ : DEFAULT_FILES_MODAL_PREFERENCES.classTypeFilter;
51
+
52
+ return {
53
+ sortBy,
54
+ confirmationFilter,
55
+ classTypeFilter,
56
+ };
57
+ } catch {
58
+ return DEFAULT_FILES_MODAL_PREFERENCES;
59
+ }
60
+ }
61
+
62
+ function loadFilesModalPreferences(): FilesModalPreferences {
63
+ if (typeof window === 'undefined') {
64
+ return DEFAULT_FILES_MODAL_PREFERENCES;
65
+ }
66
+
67
+ return parseStoredPreferences(window.localStorage.getItem(FILES_MODAL_PREFERENCES_STORAGE_KEY));
68
+ }
69
+
70
+ export function useFileListPreferences() {
71
+ const [preferences, setPreferences] = useState<FilesModalPreferences>(() =>
72
+ loadFilesModalPreferences()
73
+ );
74
+
75
+ useEffect(() => {
76
+ if (typeof window === 'undefined') {
77
+ return;
78
+ }
79
+
80
+ window.localStorage.setItem(FILES_MODAL_PREFERENCES_STORAGE_KEY, JSON.stringify(preferences));
81
+ }, [preferences]);
82
+
83
+ const setSortBy = (sortBy: FilesModalSortBy) => {
84
+ setPreferences((current) => ({ ...current, sortBy }));
85
+ };
86
+
87
+ const setConfirmationFilter = (confirmationFilter: FilesModalConfirmationFilter) => {
88
+ setPreferences((current) => ({ ...current, confirmationFilter }));
89
+ };
90
+
91
+ const setClassTypeFilter = (classTypeFilter: FilesModalClassTypeFilter) => {
92
+ setPreferences((current) => ({ ...current, classTypeFilter }));
93
+ };
94
+
95
+ const resetPreferences = () => {
96
+ setPreferences(DEFAULT_FILES_MODAL_PREFERENCES);
97
+ };
98
+
99
+ return {
100
+ preferences,
101
+ setSortBy,
102
+ setConfirmationFilter,
103
+ setClassTypeFilter,
104
+ resetPreferences,
105
+ };
106
+ }