@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,561 @@
1
+ import { useState } from 'react';
2
+ import styles from './notes.module.css';
3
+ import {
4
+ BULLET_BARREL_TYPE_OPTIONS,
5
+ BULLET_CORE_METAL_OPTIONS,
6
+ BULLET_JACKET_METAL_OPTIONS,
7
+ BULLET_TYPE_OPTIONS,
8
+ CARTRIDGE_APERTURE_SHAPE_OPTIONS,
9
+ CARTRIDGE_FPI_SHAPE_OPTIONS,
10
+ CARTRIDGE_METAL_OPTIONS,
11
+ CARTRIDGE_PRIMER_TYPE_OPTIONS,
12
+ SHOTSHELL_BIRDSHOT_OPTIONS,
13
+ SHOTSHELL_BUCKSHOT_OPTIONS,
14
+ PISTOL_CALIBERS,
15
+ RIFLE_CALIBERS,
16
+ SHOTSHELL_GAUGES,
17
+ SHOTSHELL_STEEL_WATERFOWL_OPTIONS,
18
+ formatCalculatedDiameter,
19
+ } from './class-details-shared';
20
+ import { CheckboxField, SelectField, SelectWithCustomField, TextField } from './class-details-fields';
21
+ import type { BulletDetailsState, CartridgeCaseDetailsState, ShotshellDetailsState } from './use-class-details-state';
22
+
23
+ interface BulletSectionProps {
24
+ showHeader: boolean;
25
+ isReadOnly: boolean;
26
+ bullet: BulletDetailsState;
27
+ }
28
+
29
+ interface CartridgeSectionProps {
30
+ showHeader: boolean;
31
+ isReadOnly: boolean;
32
+ cartridgeCase: CartridgeCaseDetailsState;
33
+ }
34
+
35
+ interface ShotshellSectionProps {
36
+ showHeader: boolean;
37
+ isReadOnly: boolean;
38
+ shotshell: ShotshellDetailsState;
39
+ }
40
+
41
+ type SelectOption = {
42
+ value: string;
43
+ label?: string;
44
+ };
45
+
46
+ type SelectOptionGroup = {
47
+ groupLabel: string;
48
+ options: SelectOption[];
49
+ };
50
+
51
+ type FieldOption = SelectOption | SelectOptionGroup;
52
+
53
+ type ConfiguredField =
54
+ | {
55
+ key: string;
56
+ kind: 'text';
57
+ label: string;
58
+ value: string;
59
+ onChange: (value: string) => void;
60
+ placeholder: string;
61
+ fullWidth?: boolean;
62
+ type?: 'text' | 'number';
63
+ min?: number;
64
+ }
65
+ | {
66
+ key: string;
67
+ kind: 'select';
68
+ label: string;
69
+ value: string;
70
+ onChange: (value: string) => void;
71
+ placeholder: string;
72
+ options: FieldOption[];
73
+ fullWidth?: boolean;
74
+ }
75
+ | {
76
+ key: string;
77
+ kind: 'select-custom';
78
+ label: string;
79
+ value: string;
80
+ isCustom: boolean;
81
+ onChange: (value: string) => void;
82
+ onCustomChange: (value: boolean) => void;
83
+ placeholder: string;
84
+ customPlaceholder: string;
85
+ options: FieldOption[];
86
+ fullWidth?: boolean;
87
+ };
88
+
89
+ type CheckboxConfig = {
90
+ key: string;
91
+ label: string;
92
+ checked: boolean;
93
+ onChange: (value: boolean) => void;
94
+ };
95
+
96
+ const CALIBER_GROUPED_OPTIONS: FieldOption[] = [
97
+ {
98
+ groupLabel: 'Pistol',
99
+ options: PISTOL_CALIBERS.map((caliber) => ({ value: caliber })),
100
+ },
101
+ {
102
+ groupLabel: 'Rifle',
103
+ options: RIFLE_CALIBERS.map((caliber) => ({ value: caliber })),
104
+ },
105
+ ];
106
+
107
+ const isOptionGroup = (option: FieldOption): option is SelectOptionGroup => 'groupLabel' in option;
108
+
109
+ const renderOptions = (options: FieldOption[]) => options.map((option) => {
110
+ if (isOptionGroup(option)) {
111
+ return (
112
+ <optgroup key={option.groupLabel} label={option.groupLabel}>
113
+ {option.options.map((groupOption) => (
114
+ <option key={groupOption.value} value={groupOption.value}>
115
+ {groupOption.label || groupOption.value}
116
+ </option>
117
+ ))}
118
+ </optgroup>
119
+ );
120
+ }
121
+
122
+ return (
123
+ <option key={option.value} value={option.value}>
124
+ {option.label || option.value}
125
+ </option>
126
+ );
127
+ });
128
+
129
+ const renderConfiguredField = (field: ConfiguredField, isReadOnly: boolean) => {
130
+ if (field.kind === 'text') {
131
+ return (
132
+ <TextField
133
+ key={field.key}
134
+ label={field.label}
135
+ value={field.value}
136
+ onChange={field.onChange}
137
+ disabled={isReadOnly}
138
+ placeholder={field.placeholder}
139
+ fullWidth={field.fullWidth}
140
+ type={field.type}
141
+ min={field.min}
142
+ />
143
+ );
144
+ }
145
+
146
+ if (field.kind === 'select') {
147
+ return (
148
+ <SelectField
149
+ key={field.key}
150
+ label={field.label}
151
+ value={field.value}
152
+ onChange={field.onChange}
153
+ placeholder={field.placeholder}
154
+ disabled={isReadOnly}
155
+ fullWidth={field.fullWidth}
156
+ >
157
+ {renderOptions(field.options)}
158
+ </SelectField>
159
+ );
160
+ }
161
+
162
+ return (
163
+ <SelectWithCustomField
164
+ key={field.key}
165
+ label={field.label}
166
+ value={field.value}
167
+ isCustom={field.isCustom}
168
+ onChange={field.onChange}
169
+ onCustomChange={field.onCustomChange}
170
+ placeholder={field.placeholder}
171
+ customPlaceholder={field.customPlaceholder}
172
+ disabled={isReadOnly}
173
+ fullWidth={field.fullWidth}
174
+ >
175
+ {renderOptions(field.options)}
176
+ </SelectWithCustomField>
177
+ );
178
+ };
179
+
180
+ const renderCheckboxes = (items: CheckboxConfig[], isReadOnly: boolean) => items.map((item) => (
181
+ <CheckboxField
182
+ key={item.key}
183
+ label={item.label}
184
+ checked={item.checked}
185
+ onChange={item.onChange}
186
+ disabled={isReadOnly}
187
+ />
188
+ ));
189
+
190
+ export const BulletSection = ({
191
+ showHeader,
192
+ isReadOnly,
193
+ bullet,
194
+ }: BulletSectionProps) => {
195
+ const [showCalcExplanation, setShowCalcExplanation] = useState(false);
196
+ const bulletFields: ConfiguredField[] = [
197
+ {
198
+ key: 'caliber',
199
+ kind: 'select-custom',
200
+ label: 'Caliber',
201
+ value: bullet.caliber,
202
+ isCustom: bullet.caliberIsCustom,
203
+ onChange: bullet.setCaliber,
204
+ onCustomChange: bullet.setCaliberIsCustom,
205
+ placeholder: 'Select caliber...',
206
+ customPlaceholder: 'Enter caliber...',
207
+ options: CALIBER_GROUPED_OPTIONS,
208
+ },
209
+ {
210
+ key: 'mass',
211
+ kind: 'text',
212
+ label: 'Mass',
213
+ value: bullet.mass,
214
+ onChange: bullet.setMass,
215
+ placeholder: 'e.g. 158 gr',
216
+ },
217
+ {
218
+ key: 'diameter',
219
+ kind: 'text',
220
+ label: 'Diameter',
221
+ value: bullet.diameter,
222
+ onChange: bullet.setDiameter,
223
+ placeholder: 'e.g. 0.357 in',
224
+ },
225
+ {
226
+ key: 'lgNumber',
227
+ kind: 'text',
228
+ label: 'L/G Count',
229
+ value: bullet.lgNumber,
230
+ onChange: bullet.setLgNumber,
231
+ placeholder: 'e.g. 6',
232
+ type: 'number',
233
+ min: 0,
234
+ },
235
+ {
236
+ key: 'lgDirection',
237
+ kind: 'select',
238
+ label: 'L/G Direction',
239
+ value: bullet.lgDirection,
240
+ onChange: bullet.setLgDirection,
241
+ placeholder: 'Select direction...',
242
+ options: [{ value: 'Left' }, { value: 'Right' }],
243
+ },
244
+ {
245
+ key: 'jacketMetal',
246
+ kind: 'select-custom',
247
+ label: 'Jacket Metal',
248
+ value: bullet.jacketMetal,
249
+ isCustom: bullet.jacketMetalIsCustom,
250
+ onChange: bullet.setJacketMetal,
251
+ onCustomChange: bullet.setJacketMetalIsCustom,
252
+ placeholder: 'Select jacket metal...',
253
+ customPlaceholder: 'Enter jacket metal...',
254
+ options: BULLET_JACKET_METAL_OPTIONS.map((option) => ({ value: option })),
255
+ },
256
+ {
257
+ key: 'coreMetal',
258
+ kind: 'select-custom',
259
+ label: 'Core Metal',
260
+ value: bullet.coreMetal,
261
+ isCustom: bullet.coreMetalIsCustom,
262
+ onChange: bullet.setCoreMetal,
263
+ onCustomChange: bullet.setCoreMetalIsCustom,
264
+ placeholder: 'Select core metal...',
265
+ customPlaceholder: 'Enter core metal...',
266
+ options: BULLET_CORE_METAL_OPTIONS.map((option) => ({ value: option })),
267
+ },
268
+ {
269
+ key: 'bulletType',
270
+ kind: 'select-custom',
271
+ label: 'Bullet Type',
272
+ value: bullet.bulletType,
273
+ isCustom: bullet.bulletTypeIsCustom,
274
+ onChange: bullet.setBulletType,
275
+ onCustomChange: bullet.setBulletTypeIsCustom,
276
+ placeholder: 'Select bullet type...',
277
+ customPlaceholder: 'Enter bullet type...',
278
+ options: BULLET_TYPE_OPTIONS.map((option) => ({ value: option })),
279
+ fullWidth: true,
280
+ },
281
+ {
282
+ key: 'barrelType',
283
+ kind: 'select-custom',
284
+ label: 'Barrel Type',
285
+ value: bullet.barrelType,
286
+ isCustom: bullet.barrelTypeIsCustom,
287
+ onChange: bullet.setBarrelType,
288
+ onCustomChange: bullet.setBarrelTypeIsCustom,
289
+ placeholder: 'Select barrel type...',
290
+ customPlaceholder: 'Enter barrel type...',
291
+ options: BULLET_BARREL_TYPE_OPTIONS.map((option) => ({ value: option })),
292
+ fullWidth: true,
293
+ },
294
+ ];
295
+
296
+ return (
297
+ <div className={styles.classDetailsSection}>
298
+ {showHeader && <h6 className={styles.classDetailsSectionHeader}>Bullet</h6>}
299
+ <div className={styles.classDetailsFieldGrid}>
300
+ {bulletFields.map((field) => renderConfiguredField(field, isReadOnly))}
301
+ </div>
302
+ {bullet.lgCount > 0 && (
303
+ <div className={styles.lgWidthsSection}>
304
+ <h6 className={styles.classDetailsSectionHeader}>L / G Widths</h6>
305
+ <div className={styles.lgWidthsLayout}>
306
+ <div className={styles.lgWidthsColumn}>
307
+ {Array.from({ length: bullet.lgCount }, (_, index) => (
308
+ <TextField
309
+ key={`l-${index}`}
310
+ label={`L${index + 1}`}
311
+ value={bullet.lWidths[index] || ''}
312
+ onChange={(value) => bullet.updateLWidth(index, value)}
313
+ disabled={isReadOnly}
314
+ placeholder="e.g. 0.075"
315
+ />
316
+ ))}
317
+ </div>
318
+ <div className={styles.lgWidthsColumn}>
319
+ {Array.from({ length: bullet.lgCount }, (_, index) => (
320
+ <TextField
321
+ key={`g-${index}`}
322
+ label={`G${index + 1}`}
323
+ value={bullet.gWidths[index] || ''}
324
+ onChange={(value) => bullet.updateGWidth(index, value)}
325
+ disabled={isReadOnly}
326
+ placeholder="e.g. 0.111"
327
+ />
328
+ ))}
329
+ </div>
330
+ </div>
331
+ {bullet.calculatedDiameter !== null && (
332
+ <div className={styles.calculatedDiameterWrapper}>
333
+ <div className={styles.calculatedDiameterDisplay}>
334
+ <span className={styles.classDetailsLabel}>Calculated Diameter</span>
335
+ <span className={styles.calculatedDiameterValue}>{formatCalculatedDiameter(bullet.calculatedDiameter)}</span>
336
+ </div>
337
+ <button
338
+ type="button"
339
+ className={styles.calcExplanationToggle}
340
+ onClick={() => setShowCalcExplanation((prev) => !prev)}
341
+ aria-expanded={showCalcExplanation}
342
+ >
343
+ {showCalcExplanation ? 'Hide explanation' : 'How is this calculated?'}
344
+ </button>
345
+ {showCalcExplanation && (
346
+ <div className={styles.calcExplanationPanel}>
347
+ <p className={styles.calcExplanationFormula}>
348
+ diameter&nbsp;=&nbsp;(L&#772;&nbsp;+&nbsp;G&#772;)&nbsp;&times;&nbsp;n&nbsp;&divide;&nbsp;&pi;
349
+ </p>
350
+ <ul className={styles.calcExplanationList}>
351
+ <li><strong>L&#772;</strong> — average land width</li>
352
+ <li><strong>G&#772;</strong> — average groove width</li>
353
+ <li><strong>n</strong> — L/G count</li>
354
+ <li><strong>&pi;</strong> — 3.14159&hellip;</li>
355
+ </ul>
356
+ <p className={styles.calcExplanationNote}>
357
+ The bullet&rsquo;s circumference approximates the sum of all land and groove
358
+ widths. Dividing by &pi; converts circumference to diameter.
359
+ </p>
360
+ <p className={styles.calcExplanationExample}>
361
+ <strong>Example:</strong> 6 L/G with L&#772;&nbsp;=&nbsp;0.076&Prime; and
362
+ G&#772;&nbsp;=&nbsp;0.111&Prime;&nbsp;&rarr;&nbsp;(0.076&nbsp;+&nbsp;0.111)&nbsp;&times;&nbsp;6&nbsp;&divide;&nbsp;&pi;&nbsp;&asymp;&nbsp;0.357&Prime;
363
+ </p>
364
+ </div>
365
+ )}
366
+ </div>
367
+ )}
368
+ </div>
369
+ )}
370
+ </div>
371
+ );
372
+ };
373
+
374
+ export const CartridgeCaseSection = ({
375
+ showHeader,
376
+ isReadOnly,
377
+ cartridgeCase,
378
+ }: CartridgeSectionProps) => {
379
+ const cartridgeFields: ConfiguredField[] = [
380
+ {
381
+ key: 'caliber',
382
+ kind: 'select-custom',
383
+ label: 'Caliber',
384
+ value: cartridgeCase.caliber,
385
+ isCustom: cartridgeCase.caliberIsCustom,
386
+ onChange: cartridgeCase.setCaliber,
387
+ onCustomChange: cartridgeCase.setCaliberIsCustom,
388
+ placeholder: 'Select caliber...',
389
+ customPlaceholder: 'Enter caliber...',
390
+ options: CALIBER_GROUPED_OPTIONS,
391
+ },
392
+ {
393
+ key: 'brand',
394
+ kind: 'text',
395
+ label: 'Brand',
396
+ value: cartridgeCase.brand,
397
+ onChange: cartridgeCase.setBrand,
398
+ placeholder: 'e.g. Federal',
399
+ },
400
+ {
401
+ key: 'metal',
402
+ kind: 'select-custom',
403
+ label: 'Metal',
404
+ value: cartridgeCase.metal,
405
+ isCustom: cartridgeCase.metalIsCustom,
406
+ onChange: cartridgeCase.setMetal,
407
+ onCustomChange: cartridgeCase.setMetalIsCustom,
408
+ placeholder: 'Select metal...',
409
+ customPlaceholder: 'Enter metal...',
410
+ options: CARTRIDGE_METAL_OPTIONS.map((option) => ({ value: option })),
411
+ },
412
+ {
413
+ key: 'primerType',
414
+ kind: 'select-custom',
415
+ label: 'Primer Type',
416
+ value: cartridgeCase.primerType,
417
+ isCustom: cartridgeCase.primerTypeIsCustom,
418
+ onChange: cartridgeCase.setPrimerType,
419
+ onCustomChange: cartridgeCase.setPrimerTypeIsCustom,
420
+ placeholder: 'Select primer type...',
421
+ customPlaceholder: 'Enter primer type...',
422
+ options: CARTRIDGE_PRIMER_TYPE_OPTIONS.map((option) => ({ value: option })),
423
+ },
424
+ {
425
+ key: 'fpiShape',
426
+ kind: 'select-custom',
427
+ label: 'FPI Shape',
428
+ value: cartridgeCase.fpiShape,
429
+ isCustom: cartridgeCase.fpiShapeIsCustom,
430
+ onChange: cartridgeCase.setFpiShape,
431
+ onCustomChange: cartridgeCase.setFpiShapeIsCustom,
432
+ placeholder: 'Select FPI shape...',
433
+ customPlaceholder: 'Enter FPI shape...',
434
+ options: CARTRIDGE_FPI_SHAPE_OPTIONS.map((option) => ({ value: option })),
435
+ },
436
+ {
437
+ key: 'apertureShape',
438
+ kind: 'select-custom',
439
+ label: 'Aperture Shape',
440
+ value: cartridgeCase.apertureShape,
441
+ isCustom: cartridgeCase.apertureShapeIsCustom,
442
+ onChange: cartridgeCase.setApertureShape,
443
+ onCustomChange: cartridgeCase.setApertureShapeIsCustom,
444
+ placeholder: 'Select aperture shape...',
445
+ customPlaceholder: 'Enter aperture shape...',
446
+ options: CARTRIDGE_APERTURE_SHAPE_OPTIONS.map((option) => ({ value: option })),
447
+ },
448
+ ];
449
+
450
+ return (
451
+ <div className={styles.classDetailsSection}>
452
+ {showHeader && <h6 className={styles.classDetailsSectionHeader}>Cartridge Case</h6>}
453
+ <div className={styles.classDetailsFieldGrid}>
454
+ {cartridgeFields.map((field) => renderConfiguredField(field, isReadOnly))}
455
+ </div>
456
+ <div className={styles.classDetailsCheckboxGroup}>
457
+ {renderCheckboxes([
458
+ { key: 'fpDrag', label: 'FP Drag', checked: cartridgeCase.hasFpDrag, onChange: cartridgeCase.setHasFpDrag },
459
+ { key: 'extractor', label: 'Extractor Marks', checked: cartridgeCase.hasExtractorMarks, onChange: cartridgeCase.setHasExtractorMarks },
460
+ { key: 'ejector', label: 'Ejector Marks', checked: cartridgeCase.hasEjectorMarks, onChange: cartridgeCase.setHasEjectorMarks },
461
+ { key: 'chamber', label: 'Chamber Marks', checked: cartridgeCase.hasChamberMarks, onChange: cartridgeCase.setHasChamberMarks },
462
+ { key: 'magazineLip', label: 'Magazine Lip Marks', checked: cartridgeCase.hasMagazineLipMarks, onChange: cartridgeCase.setHasMagazineLipMarks },
463
+ { key: 'primerShear', label: 'Primer Shear', checked: cartridgeCase.hasPrimerShear, onChange: cartridgeCase.setHasPrimerShear },
464
+ { key: 'ejectionPort', label: 'Ejection Port Marks', checked: cartridgeCase.hasEjectionPortMarks, onChange: cartridgeCase.setHasEjectionPortMarks },
465
+ ], isReadOnly)}
466
+ </div>
467
+ </div>
468
+ );
469
+ };
470
+
471
+ export const ShotshellSection = ({
472
+ showHeader,
473
+ isReadOnly,
474
+ shotshell,
475
+ }: ShotshellSectionProps) => {
476
+ const shotshellFields: ConfiguredField[] = [
477
+ {
478
+ key: 'gauge',
479
+ kind: 'select-custom',
480
+ label: 'Gauge',
481
+ value: shotshell.gauge,
482
+ isCustom: shotshell.gaugeIsCustom,
483
+ onChange: shotshell.setGauge,
484
+ onCustomChange: shotshell.setGaugeIsCustom,
485
+ placeholder: 'Select gauge...',
486
+ customPlaceholder: 'Enter gauge...',
487
+ options: SHOTSHELL_GAUGES.map((option) => ({ value: option })),
488
+ },
489
+ {
490
+ key: 'shotSize',
491
+ kind: 'select',
492
+ label: 'Shot Size',
493
+ value: shotshell.shotSize,
494
+ onChange: shotshell.setShotSize,
495
+ placeholder: 'Select shot size...',
496
+ options: [
497
+ {
498
+ groupLabel: 'Birdshot',
499
+ options: SHOTSHELL_BIRDSHOT_OPTIONS.map((option) => ({ value: option })),
500
+ },
501
+ {
502
+ groupLabel: 'Steel/Waterfowl Shot',
503
+ options: SHOTSHELL_STEEL_WATERFOWL_OPTIONS.map((option) => ({ value: option })),
504
+ },
505
+ {
506
+ groupLabel: 'Buckshot',
507
+ options: SHOTSHELL_BUCKSHOT_OPTIONS.map((option) => ({ value: option })),
508
+ },
509
+ ],
510
+ },
511
+ {
512
+ key: 'metal',
513
+ kind: 'select-custom',
514
+ label: 'Metal',
515
+ value: shotshell.metal,
516
+ isCustom: shotshell.metalIsCustom,
517
+ onChange: shotshell.setMetal,
518
+ onCustomChange: shotshell.setMetalIsCustom,
519
+ placeholder: 'Select metal...',
520
+ customPlaceholder: 'Enter metal...',
521
+ options: CARTRIDGE_METAL_OPTIONS.map((option) => ({ value: option })),
522
+ },
523
+ {
524
+ key: 'brand',
525
+ kind: 'text',
526
+ label: 'Brand',
527
+ value: shotshell.brand,
528
+ onChange: shotshell.setBrand,
529
+ placeholder: 'e.g. Winchester',
530
+ },
531
+ {
532
+ key: 'fpiShape',
533
+ kind: 'select-custom',
534
+ label: 'FPI Shape',
535
+ value: shotshell.fpiShape,
536
+ isCustom: shotshell.fpiShapeIsCustom,
537
+ onChange: shotshell.setFpiShape,
538
+ onCustomChange: shotshell.setFpiShapeIsCustom,
539
+ placeholder: 'Select FPI shape...',
540
+ customPlaceholder: 'Enter FPI shape...',
541
+ options: CARTRIDGE_FPI_SHAPE_OPTIONS.map((option) => ({ value: option })),
542
+ fullWidth: true,
543
+ },
544
+ ];
545
+
546
+ return (
547
+ <div className={styles.classDetailsSection}>
548
+ {showHeader && <h6 className={styles.classDetailsSectionHeader}>Shotshell</h6>}
549
+ <div className={styles.classDetailsFieldGrid}>
550
+ {shotshellFields.map((field) => renderConfiguredField(field, isReadOnly))}
551
+ </div>
552
+ <div className={styles.classDetailsCheckboxGroup}>
553
+ {renderCheckboxes([
554
+ { key: 'extractor', label: 'Extractor Marks', checked: shotshell.hasExtractorMarks, onChange: shotshell.setHasExtractorMarks },
555
+ { key: 'ejector', label: 'Ejector Marks', checked: shotshell.hasEjectorMarks, onChange: shotshell.setHasEjectorMarks },
556
+ { key: 'chamber', label: 'Chamber Marks', checked: shotshell.hasChamberMarks, onChange: shotshell.setHasChamberMarks },
557
+ ], isReadOnly)}
558
+ </div>
559
+ </div>
560
+ );
561
+ };