@striae-org/striae 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/app/components/actions/case-export/core-export.ts +11 -2
- package/app/components/actions/case-export/download-handlers.ts +3 -1
- package/app/components/canvas/canvas.module.css +1 -1
- package/app/components/canvas/canvas.tsx +32 -11
- package/app/components/icon/icons.svg +1 -1
- package/app/components/icon/manifest.json +1 -1
- package/app/components/navbar/navbar.tsx +10 -9
- package/app/components/sidebar/cases/case-sidebar.tsx +6 -1
- package/app/components/sidebar/files/files-modal.tsx +39 -15
- package/app/components/sidebar/notes/addl-notes-modal.tsx +9 -2
- package/app/components/sidebar/notes/{class-details/class-details-fields.tsx → item-details/item-details-fields.tsx} +10 -10
- package/app/components/sidebar/notes/{class-details/class-details-modal.tsx → item-details/item-details-modal.tsx} +20 -22
- package/app/components/sidebar/notes/{class-details/class-details-sections.tsx → item-details/item-details-sections.tsx} +16 -16
- package/app/components/sidebar/notes/{class-details/class-details-shared.ts → item-details/item-details-shared.ts} +4 -3
- package/app/components/sidebar/notes/{class-details/use-class-details-state.ts → item-details/use-item-details-state.ts} +4 -4
- package/app/components/sidebar/notes/notes-editor-form.tsx +333 -124
- package/app/components/sidebar/notes/notes-editor-modal.tsx +3 -0
- package/app/components/sidebar/notes/notes.module.css +40 -20
- package/app/components/sidebar/sidebar-container.tsx +8 -0
- package/app/components/sidebar/sidebar.tsx +3 -0
- package/app/components/toolbar/toolbar.tsx +5 -5
- package/{members.emails.example → app/config-example/members.emails} +1 -1
- package/{primershear.emails.example → app/config-example/primershear.emails} +1 -1
- package/app/hooks/useFileListPreferences.ts +22 -17
- package/app/routes/striae/striae.tsx +4 -10
- package/app/types/annotations.ts +28 -5
- package/app/utils/data/confirmation-summary/summary-core.ts +40 -8
- package/app/utils/data/file-filters.ts +39 -17
- package/package.json +139 -141
- package/scripts/deploy-config.sh +33 -0
- package/scripts/deploy-members-emails.sh +4 -4
- package/scripts/deploy-primershear-emails.sh +3 -3
- package/workers/audit-worker/package.json +2 -2
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +2 -2
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +2 -2
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +2 -2
- package/workers/pdf-worker/src/formats/format-striae.ts +65 -8
- package/workers/pdf-worker/src/report-types.ts +13 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +2 -2
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
|
@@ -13,6 +13,7 @@ interface NotesEditorModalProps {
|
|
|
13
13
|
originalFileName?: string;
|
|
14
14
|
onAnnotationRefresh?: () => void;
|
|
15
15
|
isUploading?: boolean;
|
|
16
|
+
isReadOnly?: boolean;
|
|
16
17
|
showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -25,6 +26,7 @@ export const NotesEditorModal = ({
|
|
|
25
26
|
originalFileName,
|
|
26
27
|
onAnnotationRefresh,
|
|
27
28
|
isUploading = false,
|
|
29
|
+
isReadOnly = false,
|
|
28
30
|
showNotification,
|
|
29
31
|
}: NotesEditorModalProps) => {
|
|
30
32
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
@@ -186,6 +188,7 @@ export const NotesEditorModal = ({
|
|
|
186
188
|
onAnnotationRefresh={onAnnotationRefresh}
|
|
187
189
|
originalFileName={originalFileName}
|
|
188
190
|
isUploading={isUploading}
|
|
191
|
+
isReadOnly={isReadOnly}
|
|
189
192
|
showNotification={showNotification}
|
|
190
193
|
onDirtyChange={handleDirtyChange}
|
|
191
194
|
onRegisterSaveHandler={handleRegisterSaveHandler}
|
|
@@ -54,6 +54,26 @@
|
|
|
54
54
|
padding-top: 1.25rem;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
.itemSelectorRow {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
gap: 0.5rem;
|
|
61
|
+
margin-bottom: 1rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.itemSelectorRow label {
|
|
65
|
+
font-weight: 500;
|
|
66
|
+
font-size: 0.875rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.itemSelectorRow select {
|
|
70
|
+
padding: 0.5rem 0.75rem;
|
|
71
|
+
border: 1px solid #ced4da;
|
|
72
|
+
border-radius: 0.25rem;
|
|
73
|
+
background-color: white;
|
|
74
|
+
font-size: 0.875rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
@media (max-width: 980px) {
|
|
58
78
|
.editorLayout .caseNumbers,
|
|
59
79
|
.compactSectionGrid {
|
|
@@ -249,14 +269,14 @@ textarea:focus {
|
|
|
249
269
|
|
|
250
270
|
/* Class Details Panel — replaces placeholder in right column */
|
|
251
271
|
|
|
252
|
-
.
|
|
272
|
+
.itemDetailsPanel {
|
|
253
273
|
display: flex;
|
|
254
274
|
flex-direction: column;
|
|
255
275
|
gap: 0.75rem;
|
|
256
276
|
min-height: 150px;
|
|
257
277
|
}
|
|
258
278
|
|
|
259
|
-
.
|
|
279
|
+
.itemDetailsButton {
|
|
260
280
|
width: 100%;
|
|
261
281
|
padding: 0.65rem 1rem;
|
|
262
282
|
background: transparent;
|
|
@@ -270,18 +290,18 @@ textarea:focus {
|
|
|
270
290
|
text-align: center;
|
|
271
291
|
}
|
|
272
292
|
|
|
273
|
-
.
|
|
293
|
+
.itemDetailsButton:hover:not(:disabled) {
|
|
274
294
|
background-color: color-mix(in lab, var(--primary) 10%, transparent);
|
|
275
295
|
}
|
|
276
296
|
|
|
277
|
-
.
|
|
297
|
+
.itemDetailsButton:disabled {
|
|
278
298
|
opacity: 0.5;
|
|
279
299
|
cursor: not-allowed;
|
|
280
300
|
}
|
|
281
301
|
|
|
282
302
|
/* Class Details Modal */
|
|
283
303
|
|
|
284
|
-
.
|
|
304
|
+
.itemDetailsModal {
|
|
285
305
|
max-width: 680px;
|
|
286
306
|
max-height: 80vh;
|
|
287
307
|
display: flex;
|
|
@@ -289,7 +309,7 @@ textarea:focus {
|
|
|
289
309
|
gap: 1rem;
|
|
290
310
|
}
|
|
291
311
|
|
|
292
|
-
.
|
|
312
|
+
.itemDetailsContent {
|
|
293
313
|
overflow-y: auto;
|
|
294
314
|
flex: 1;
|
|
295
315
|
min-height: 0;
|
|
@@ -301,13 +321,13 @@ textarea:focus {
|
|
|
301
321
|
scrollbar-width: thin;
|
|
302
322
|
}
|
|
303
323
|
|
|
304
|
-
.
|
|
324
|
+
.itemDetailsSection {
|
|
305
325
|
display: flex;
|
|
306
326
|
flex-direction: column;
|
|
307
327
|
gap: 1rem;
|
|
308
328
|
}
|
|
309
329
|
|
|
310
|
-
.
|
|
330
|
+
.itemDetailsSectionHeader {
|
|
311
331
|
margin: 0;
|
|
312
332
|
font-size: 0.95rem;
|
|
313
333
|
font-weight: 700;
|
|
@@ -316,29 +336,29 @@ textarea:focus {
|
|
|
316
336
|
border-bottom: 1.5px solid #dee2e6;
|
|
317
337
|
}
|
|
318
338
|
|
|
319
|
-
.
|
|
339
|
+
.itemDetailsFieldGrid {
|
|
320
340
|
display: grid;
|
|
321
341
|
grid-template-columns: 1fr 1fr;
|
|
322
342
|
gap: 0.85rem 1.25rem;
|
|
323
343
|
}
|
|
324
344
|
|
|
325
|
-
.
|
|
345
|
+
.itemDetailsField {
|
|
326
346
|
display: flex;
|
|
327
347
|
flex-direction: column;
|
|
328
348
|
gap: 0.25rem;
|
|
329
349
|
}
|
|
330
350
|
|
|
331
|
-
.
|
|
351
|
+
.itemDetailsFieldFull {
|
|
332
352
|
grid-column: 1 / -1;
|
|
333
353
|
}
|
|
334
354
|
|
|
335
|
-
.
|
|
355
|
+
.itemDetailsLabel {
|
|
336
356
|
font-size: 0.8rem;
|
|
337
357
|
font-weight: 600;
|
|
338
358
|
color: #495057;
|
|
339
359
|
}
|
|
340
360
|
|
|
341
|
-
.
|
|
361
|
+
.itemDetailsInput {
|
|
342
362
|
padding: 0.5rem 0.65rem;
|
|
343
363
|
border: 1.5px solid #ced4da;
|
|
344
364
|
border-radius: 6px;
|
|
@@ -348,23 +368,23 @@ textarea:focus {
|
|
|
348
368
|
box-sizing: border-box;
|
|
349
369
|
}
|
|
350
370
|
|
|
351
|
-
.
|
|
371
|
+
.itemDetailsInput:focus {
|
|
352
372
|
border-color: var(--primary);
|
|
353
373
|
outline: none;
|
|
354
374
|
}
|
|
355
375
|
|
|
356
|
-
.
|
|
376
|
+
.itemDetailsInput:disabled {
|
|
357
377
|
background-color: #f8f9fa;
|
|
358
378
|
cursor: not-allowed;
|
|
359
379
|
}
|
|
360
380
|
|
|
361
|
-
.
|
|
381
|
+
.itemDetailsCheckboxGroup {
|
|
362
382
|
display: flex;
|
|
363
383
|
flex-wrap: wrap;
|
|
364
384
|
gap: 0.4rem 1.25rem;
|
|
365
385
|
}
|
|
366
386
|
|
|
367
|
-
.
|
|
387
|
+
.itemDetailsCheckboxLabel {
|
|
368
388
|
display: flex;
|
|
369
389
|
align-items: center;
|
|
370
390
|
gap: 0.4rem;
|
|
@@ -373,11 +393,11 @@ textarea:focus {
|
|
|
373
393
|
cursor: pointer;
|
|
374
394
|
}
|
|
375
395
|
|
|
376
|
-
.
|
|
396
|
+
.itemDetailsCheckboxLabel input[type="checkbox"] {
|
|
377
397
|
cursor: pointer;
|
|
378
398
|
}
|
|
379
399
|
|
|
380
|
-
.
|
|
400
|
+
.itemDetailsCheckboxLabel input[type="checkbox"]:disabled {
|
|
381
401
|
cursor: not-allowed;
|
|
382
402
|
}
|
|
383
403
|
|
|
@@ -697,7 +717,7 @@ textarea:focus {
|
|
|
697
717
|
justify-content: center;
|
|
698
718
|
}
|
|
699
719
|
|
|
700
|
-
.
|
|
720
|
+
.itemDetailsModalButtons .itemDetailsModalAction {
|
|
701
721
|
min-height: 42px;
|
|
702
722
|
margin: 0;
|
|
703
723
|
display: inline-flex;
|
|
@@ -24,6 +24,7 @@ interface SidebarContainerProps {
|
|
|
24
24
|
setShowNotes: (show: boolean) => void;
|
|
25
25
|
onAnnotationRefresh?: () => void;
|
|
26
26
|
isReadOnly?: boolean;
|
|
27
|
+
isReviewOnlyCase?: boolean;
|
|
27
28
|
isArchivedCase?: boolean;
|
|
28
29
|
isConfirmed?: boolean;
|
|
29
30
|
confirmationSaveVersion?: number;
|
|
@@ -109,6 +110,13 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = (props) => {
|
|
|
109
110
|
rel="noopener noreferrer"
|
|
110
111
|
className={styles.footerModalLink}>
|
|
111
112
|
Security Policy
|
|
113
|
+
</Link>
|
|
114
|
+
<Link
|
|
115
|
+
to="https://community.striae.org"
|
|
116
|
+
target="_blank"
|
|
117
|
+
rel="noopener noreferrer"
|
|
118
|
+
className={styles.footerModalLink}>
|
|
119
|
+
Striae Community
|
|
112
120
|
</Link>
|
|
113
121
|
</div>
|
|
114
122
|
|
|
@@ -20,6 +20,7 @@ interface SidebarProps {
|
|
|
20
20
|
setShowNotes: (show: boolean) => void;
|
|
21
21
|
onAnnotationRefresh?: () => void;
|
|
22
22
|
isReadOnly?: boolean;
|
|
23
|
+
isReviewOnlyCase?: boolean;
|
|
23
24
|
isArchivedCase?: boolean;
|
|
24
25
|
isConfirmed?: boolean;
|
|
25
26
|
confirmationSaveVersion?: number;
|
|
@@ -40,6 +41,7 @@ export const Sidebar = ({
|
|
|
40
41
|
setFiles,
|
|
41
42
|
setShowNotes,
|
|
42
43
|
isReadOnly = false,
|
|
44
|
+
isReviewOnlyCase = false,
|
|
43
45
|
isArchivedCase = false,
|
|
44
46
|
isConfirmed = false,
|
|
45
47
|
confirmationSaveVersion = 0,
|
|
@@ -89,6 +91,7 @@ export const Sidebar = ({
|
|
|
89
91
|
setFiles={setFiles}
|
|
90
92
|
onNotesClick={() => setShowNotes(true)}
|
|
91
93
|
isReadOnly={isReadOnly}
|
|
94
|
+
isReviewOnlyCase={isReviewOnlyCase}
|
|
92
95
|
isArchivedCase={isArchivedCase}
|
|
93
96
|
isConfirmed={isConfirmed}
|
|
94
97
|
confirmationSaveVersion={confirmationSaveVersion}
|
|
@@ -3,7 +3,7 @@ import { Button } from '../button/button';
|
|
|
3
3
|
import { ToolbarColorSelector } from './toolbar-color-selector';
|
|
4
4
|
import styles from './toolbar.module.css';
|
|
5
5
|
|
|
6
|
-
type ToolId = 'number' | '
|
|
6
|
+
type ToolId = 'number' | 'item' | 'index' | 'id' | 'notes' | 'print' | 'visibility' | 'box';
|
|
7
7
|
|
|
8
8
|
interface ToolbarProps {
|
|
9
9
|
onToolSelect?: (toolId: ToolId, active: boolean) => void;
|
|
@@ -125,10 +125,10 @@ export const Toolbar = ({
|
|
|
125
125
|
ariaLabel="Case & Item Numbers"
|
|
126
126
|
/>
|
|
127
127
|
<Button
|
|
128
|
-
iconId="
|
|
129
|
-
isActive={activeTools.has('
|
|
130
|
-
onClick={() => handleToolClick('
|
|
131
|
-
ariaLabel="
|
|
128
|
+
iconId="item"
|
|
129
|
+
isActive={activeTools.has('item')}
|
|
130
|
+
onClick={() => handleToolClick('item')}
|
|
131
|
+
ariaLabel="Item Type"
|
|
132
132
|
/>
|
|
133
133
|
<Button
|
|
134
134
|
iconId="index"
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type FilesModalPreferences,
|
|
4
4
|
type FilesModalSortBy,
|
|
5
5
|
type FilesModalConfirmationFilter,
|
|
6
|
-
type
|
|
6
|
+
type FilesModalItemTypeFilter,
|
|
7
7
|
} from '~/utils/data/file-filters';
|
|
8
8
|
|
|
9
9
|
const FILES_MODAL_PREFERENCES_STORAGE_KEY = 'striae.filesModal.preferences';
|
|
@@ -11,7 +11,7 @@ const FILES_MODAL_PREFERENCES_STORAGE_KEY = 'striae.filesModal.preferences';
|
|
|
11
11
|
export const DEFAULT_FILES_MODAL_PREFERENCES: FilesModalPreferences = {
|
|
12
12
|
sortBy: 'recent',
|
|
13
13
|
confirmationFilter: 'all',
|
|
14
|
-
|
|
14
|
+
itemTypeFilter: 'all',
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
function parseStoredPreferences(value: string | null): FilesModalPreferences {
|
|
@@ -25,7 +25,7 @@ function parseStoredPreferences(value: string | null): FilesModalPreferences {
|
|
|
25
25
|
const sortBy: FilesModalSortBy =
|
|
26
26
|
parsed.sortBy === 'filename' ||
|
|
27
27
|
parsed.sortBy === 'confirmation' ||
|
|
28
|
-
parsed.sortBy === '
|
|
28
|
+
parsed.sortBy === 'itemType' ||
|
|
29
29
|
parsed.sortBy === 'recent'
|
|
30
30
|
? parsed.sortBy
|
|
31
31
|
: DEFAULT_FILES_MODAL_PREFERENCES.sortBy;
|
|
@@ -38,21 +38,26 @@ function parseStoredPreferences(value: string | null): FilesModalPreferences {
|
|
|
38
38
|
? parsed.confirmationFilter
|
|
39
39
|
: DEFAULT_FILES_MODAL_PREFERENCES.confirmationFilter;
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
parsed.
|
|
44
|
-
parsed.
|
|
45
|
-
parsed.
|
|
46
|
-
parsed.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
// Support both new 'itemTypeFilter' and legacy 'classTypeFilter' properties
|
|
42
|
+
const itemTypeFilter: FilesModalItemTypeFilter =
|
|
43
|
+
parsed.itemTypeFilter === 'Bullet' ||
|
|
44
|
+
parsed.itemTypeFilter === 'Cartridge Case' ||
|
|
45
|
+
parsed.itemTypeFilter === 'Shotshell' ||
|
|
46
|
+
parsed.itemTypeFilter === 'Other' ||
|
|
47
|
+
parsed.itemTypeFilter === 'all'
|
|
48
|
+
? parsed.itemTypeFilter
|
|
49
|
+
: parsed.classTypeFilter === 'Bullet' ||
|
|
50
|
+
parsed.classTypeFilter === 'Cartridge Case' ||
|
|
51
|
+
parsed.classTypeFilter === 'Shotshell' ||
|
|
52
|
+
parsed.classTypeFilter === 'Other' ||
|
|
53
|
+
parsed.classTypeFilter === 'all'
|
|
54
|
+
? parsed.classTypeFilter
|
|
55
|
+
: 'all';
|
|
51
56
|
|
|
52
57
|
return {
|
|
53
58
|
sortBy,
|
|
54
59
|
confirmationFilter,
|
|
55
|
-
|
|
60
|
+
itemTypeFilter,
|
|
56
61
|
};
|
|
57
62
|
} catch {
|
|
58
63
|
return DEFAULT_FILES_MODAL_PREFERENCES;
|
|
@@ -88,8 +93,8 @@ export function useFileListPreferences() {
|
|
|
88
93
|
setPreferences((current) => ({ ...current, confirmationFilter }));
|
|
89
94
|
};
|
|
90
95
|
|
|
91
|
-
const
|
|
92
|
-
setPreferences((current) => ({ ...current,
|
|
96
|
+
const setItemTypeFilter = (itemTypeFilter: FilesModalItemTypeFilter) => {
|
|
97
|
+
setPreferences((current) => ({ ...current, itemTypeFilter }));
|
|
93
98
|
};
|
|
94
99
|
|
|
95
100
|
const resetPreferences = () => {
|
|
@@ -100,7 +105,7 @@ export function useFileListPreferences() {
|
|
|
100
105
|
preferences,
|
|
101
106
|
setSortBy,
|
|
102
107
|
setConfirmationFilter,
|
|
103
|
-
|
|
108
|
+
setItemTypeFilter,
|
|
104
109
|
resetPreferences,
|
|
105
110
|
};
|
|
106
111
|
}
|
|
@@ -677,24 +677,16 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
677
677
|
const notes = await getNotes(user, currentCase, imageId);
|
|
678
678
|
if (notes) {
|
|
679
679
|
setAnnotationData({
|
|
680
|
+
...notes,
|
|
680
681
|
leftCase: notes.leftCase || '',
|
|
681
682
|
rightCase: notes.rightCase || '',
|
|
682
683
|
leftItem: notes.leftItem || '',
|
|
683
684
|
rightItem: notes.rightItem || '',
|
|
684
685
|
caseFontColor: notes.caseFontColor || '#FFDE21',
|
|
685
|
-
classType: notes.classType || 'Other',
|
|
686
|
-
customClass: notes.customClass,
|
|
687
|
-
classNote: notes.classNote, // Optional - pass as-is
|
|
688
686
|
indexType: notes.indexType || 'number',
|
|
689
|
-
indexNumber: notes.indexNumber,
|
|
690
|
-
indexColor: notes.indexColor,
|
|
691
687
|
supportLevel: notes.supportLevel || 'Inconclusive',
|
|
692
|
-
|
|
693
|
-
includeConfirmation: notes.includeConfirmation ?? false, // Required
|
|
694
|
-
confirmationData: notes.confirmationData, // Add imported confirmation data
|
|
695
|
-
additionalNotes: notes.additionalNotes, // Optional - pass as-is
|
|
688
|
+
includeConfirmation: notes.includeConfirmation ?? false,
|
|
696
689
|
boxAnnotations: notes.boxAnnotations || [],
|
|
697
|
-
earliestAnnotationTimestamp: notes.earliestAnnotationTimestamp,
|
|
698
690
|
updatedAt: notes.updatedAt || new Date().toISOString()
|
|
699
691
|
});
|
|
700
692
|
} else {
|
|
@@ -874,6 +866,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
874
866
|
setShowNotes={setShowNotes}
|
|
875
867
|
onAnnotationRefresh={refreshAnnotationData}
|
|
876
868
|
isReadOnly={isReadOnlyCase}
|
|
869
|
+
isReviewOnlyCase={isReviewOnlyCase}
|
|
877
870
|
isArchivedCase={archiveDetails.archived}
|
|
878
871
|
isConfirmed={!!annotationData?.confirmationData}
|
|
879
872
|
confirmationSaveVersion={confirmationSaveVersion}
|
|
@@ -954,6 +947,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
954
947
|
onAnnotationRefresh={refreshAnnotationData}
|
|
955
948
|
originalFileName={files.find(file => file.id === imageId)?.originalFilename}
|
|
956
949
|
isUploading={isUploading}
|
|
950
|
+
isReadOnly={isReadOnlyCase}
|
|
957
951
|
showNotification={showNotification}
|
|
958
952
|
/>
|
|
959
953
|
<UserAuditViewer
|
package/app/types/annotations.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Annotation-related types and interfaces
|
|
2
2
|
|
|
3
|
+
export type ItemType = 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
|
|
4
|
+
|
|
3
5
|
export interface BoxAnnotation {
|
|
4
6
|
id: string;
|
|
5
7
|
x: number;
|
|
@@ -72,19 +74,40 @@ export interface AnnotationData {
|
|
|
72
74
|
leftItem: string;
|
|
73
75
|
rightItem: string;
|
|
74
76
|
caseFontColor?: string;
|
|
75
|
-
|
|
77
|
+
// Left item class characteristics
|
|
78
|
+
leftItemType?: ItemType;
|
|
79
|
+
leftCustomClass?: string;
|
|
80
|
+
leftClassNote?: string;
|
|
81
|
+
leftBulletData?: BulletAnnotationData;
|
|
82
|
+
leftCartridgeCaseData?: CartridgeCaseAnnotationData;
|
|
83
|
+
leftShotshellData?: ShotshellAnnotationData;
|
|
84
|
+
leftHasSubclass?: boolean;
|
|
85
|
+
// Right item class characteristics
|
|
86
|
+
rightItemType?: ItemType;
|
|
87
|
+
rightCustomClass?: string;
|
|
88
|
+
rightClassNote?: string;
|
|
89
|
+
rightBulletData?: BulletAnnotationData;
|
|
90
|
+
rightCartridgeCaseData?: CartridgeCaseAnnotationData;
|
|
91
|
+
rightShotshellData?: ShotshellAnnotationData;
|
|
92
|
+
rightHasSubclass?: boolean;
|
|
93
|
+
// Deprecated (kept for backward compatibility): use leftItemType, rightItemType, etc.
|
|
94
|
+
itemType?: ItemType;
|
|
95
|
+
/** @deprecated Pre-split legacy field; use leftItemType / rightItemType instead. */
|
|
96
|
+
classType?: string;
|
|
76
97
|
customClass?: string;
|
|
77
98
|
classNote?: string;
|
|
78
|
-
indexType?: 'number' | 'color';
|
|
79
|
-
indexNumber?: string;
|
|
80
|
-
indexColor?: string;
|
|
81
|
-
supportLevel?: 'ID' | 'Exclusion' | 'Inconclusive';
|
|
82
99
|
bulletData?: BulletAnnotationData;
|
|
83
100
|
cartridgeCaseData?: CartridgeCaseAnnotationData;
|
|
84
101
|
shotshellData?: ShotshellAnnotationData;
|
|
85
102
|
hasSubclass?: boolean;
|
|
103
|
+
indexType?: 'number' | 'color';
|
|
104
|
+
indexNumber?: string;
|
|
105
|
+
indexColor?: string;
|
|
106
|
+
supportLevel?: 'ID' | 'Exclusion' | 'Inconclusive';
|
|
86
107
|
includeConfirmation: boolean;
|
|
87
108
|
confirmationData?: ConfirmationData;
|
|
109
|
+
leftAdditionalNotes?: string;
|
|
110
|
+
rightAdditionalNotes?: string;
|
|
88
111
|
additionalNotes?: string;
|
|
89
112
|
boxAnnotations?: BoxAnnotation[];
|
|
90
113
|
updatedAt: string;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
|
-
import { type AnnotationData } from '~/types';
|
|
2
|
+
import { type AnnotationData, type ItemType } from '~/types';
|
|
3
3
|
|
|
4
4
|
export interface FileConfirmationSummary {
|
|
5
5
|
includeConfirmation: boolean;
|
|
6
6
|
isConfirmed: boolean;
|
|
7
7
|
updatedAt: string;
|
|
8
|
-
|
|
8
|
+
itemType?: ItemType;
|
|
9
|
+
leftItemType?: ItemType;
|
|
10
|
+
rightItemType?: ItemType;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface CaseConfirmationSummary {
|
|
@@ -195,8 +197,17 @@ function normalizeFileConfirmationSummary(value: unknown): FileConfirmationSumma
|
|
|
195
197
|
};
|
|
196
198
|
}
|
|
197
199
|
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
+
// Support both new 'itemType' and legacy 'classType' properties
|
|
201
|
+
const itemType = value.itemType ?? value.classType;
|
|
202
|
+
const normalizedItemType = typeof itemType === 'string' && ['Bullet', 'Cartridge Case', 'Shotshell', 'Other'].includes(itemType) ? (itemType as ItemType) : undefined;
|
|
203
|
+
const normalizedLeftItemType =
|
|
204
|
+
typeof value.leftItemType === 'string' && ['Bullet', 'Cartridge Case', 'Shotshell', 'Other'].includes(value.leftItemType)
|
|
205
|
+
? (value.leftItemType as ItemType)
|
|
206
|
+
: undefined;
|
|
207
|
+
const normalizedRightItemType =
|
|
208
|
+
typeof value.rightItemType === 'string' && ['Bullet', 'Cartridge Case', 'Shotshell', 'Other'].includes(value.rightItemType)
|
|
209
|
+
? (value.rightItemType as ItemType)
|
|
210
|
+
: undefined;
|
|
200
211
|
|
|
201
212
|
const summary: FileConfirmationSummary = {
|
|
202
213
|
includeConfirmation: value.includeConfirmation === true,
|
|
@@ -204,8 +215,16 @@ function normalizeFileConfirmationSummary(value: unknown): FileConfirmationSumma
|
|
|
204
215
|
updatedAt: typeof value.updatedAt === 'string' && value.updatedAt.length > 0 ? value.updatedAt : getIsoNow()
|
|
205
216
|
};
|
|
206
217
|
|
|
207
|
-
if (
|
|
208
|
-
summary.
|
|
218
|
+
if (normalizedItemType) {
|
|
219
|
+
summary.itemType = normalizedItemType;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (normalizedLeftItemType) {
|
|
223
|
+
summary.leftItemType = normalizedLeftItemType;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (normalizedRightItemType) {
|
|
227
|
+
summary.rightItemType = normalizedRightItemType;
|
|
209
228
|
}
|
|
210
229
|
|
|
211
230
|
return summary;
|
|
@@ -237,6 +256,8 @@ export function computeCaseConfirmationAggregate(filesById: Record<string, FileC
|
|
|
237
256
|
|
|
238
257
|
export function toFileConfirmationSummary(annotationData: AnnotationData | null): FileConfirmationSummary {
|
|
239
258
|
const includeConfirmation = annotationData?.includeConfirmation === true;
|
|
259
|
+
const leftItemType = annotationData?.leftItemType || annotationData?.itemType || (annotationData?.classType as ItemType | undefined);
|
|
260
|
+
const rightItemType = annotationData?.rightItemType;
|
|
240
261
|
|
|
241
262
|
const summary: FileConfirmationSummary = {
|
|
242
263
|
includeConfirmation,
|
|
@@ -244,8 +265,19 @@ export function toFileConfirmationSummary(annotationData: AnnotationData | null)
|
|
|
244
265
|
updatedAt: getIsoNow()
|
|
245
266
|
};
|
|
246
267
|
|
|
247
|
-
if (
|
|
248
|
-
summary.
|
|
268
|
+
if (leftItemType) {
|
|
269
|
+
summary.leftItemType = leftItemType;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (rightItemType) {
|
|
273
|
+
summary.rightItemType = rightItemType;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Keep legacy single-value item type for existing consumers.
|
|
277
|
+
if (leftItemType) {
|
|
278
|
+
summary.itemType = leftItemType;
|
|
279
|
+
} else if (rightItemType) {
|
|
280
|
+
summary.itemType = rightItemType;
|
|
249
281
|
}
|
|
250
282
|
|
|
251
283
|
return summary;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FileData } from '~/types';
|
|
2
2
|
import type { FileConfirmationSummary } from '~/utils/data';
|
|
3
3
|
|
|
4
|
-
export type FilesModalSortBy = 'recent' | 'filename' | 'confirmation' | '
|
|
4
|
+
export type FilesModalSortBy = 'recent' | 'filename' | 'confirmation' | 'itemType';
|
|
5
5
|
|
|
6
6
|
export type FilesModalConfirmationFilter =
|
|
7
7
|
| 'all'
|
|
@@ -9,17 +9,22 @@ export type FilesModalConfirmationFilter =
|
|
|
9
9
|
| 'confirmed'
|
|
10
10
|
| 'none-requested';
|
|
11
11
|
|
|
12
|
-
export type
|
|
12
|
+
export type FilesModalItemTypeFilter =
|
|
13
13
|
| 'all'
|
|
14
14
|
| 'Bullet'
|
|
15
15
|
| 'Cartridge Case'
|
|
16
16
|
| 'Shotshell'
|
|
17
17
|
| 'Other';
|
|
18
18
|
|
|
19
|
+
// Backwards compatibility alias
|
|
20
|
+
export type FilesModalClassTypeFilter = FilesModalItemTypeFilter;
|
|
21
|
+
|
|
19
22
|
export interface FilesModalPreferences {
|
|
20
23
|
sortBy: FilesModalSortBy;
|
|
21
24
|
confirmationFilter: FilesModalConfirmationFilter;
|
|
22
|
-
|
|
25
|
+
itemTypeFilter: FilesModalItemTypeFilter;
|
|
26
|
+
// Backwards compatibility: legacy classTypeFilter will be migrated to itemTypeFilter
|
|
27
|
+
classTypeFilter?: FilesModalItemTypeFilter;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
export type FileConfirmationById = Record<string, FileConfirmationSummary>;
|
|
@@ -34,6 +39,21 @@ function getFileConfirmationState(fileId: string, statusById: FileConfirmationBy
|
|
|
34
39
|
return statusById[fileId] || DEFAULT_CONFIRMATION_SUMMARY;
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
function getSummaryItemTypes(summary: FileConfirmationSummary): Array<NonNullable<FileConfirmationSummary['itemType']>> {
|
|
43
|
+
const types = [
|
|
44
|
+
summary.leftItemType,
|
|
45
|
+
summary.rightItemType,
|
|
46
|
+
summary.itemType,
|
|
47
|
+
].filter((value): value is NonNullable<FileConfirmationSummary['itemType']> => Boolean(value));
|
|
48
|
+
|
|
49
|
+
return Array.from(new Set(types));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getPrimaryItemType(summary: FileConfirmationSummary): FileConfirmationSummary['itemType'] {
|
|
53
|
+
const [first] = getSummaryItemTypes(summary);
|
|
54
|
+
return first;
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
function getConfirmationRank(summary: FileConfirmationSummary): number {
|
|
38
58
|
if (summary.includeConfirmation && !summary.isConfirmed) {
|
|
39
59
|
return 0;
|
|
@@ -46,20 +66,20 @@ function getConfirmationRank(summary: FileConfirmationSummary): number {
|
|
|
46
66
|
return 2;
|
|
47
67
|
}
|
|
48
68
|
|
|
49
|
-
function
|
|
50
|
-
if (
|
|
69
|
+
function getItemTypeRank(itemType: FileConfirmationSummary['itemType']): number {
|
|
70
|
+
if (itemType === 'Bullet') {
|
|
51
71
|
return 0;
|
|
52
72
|
}
|
|
53
73
|
|
|
54
|
-
if (
|
|
74
|
+
if (itemType === 'Cartridge Case') {
|
|
55
75
|
return 1;
|
|
56
76
|
}
|
|
57
77
|
|
|
58
|
-
if (
|
|
78
|
+
if (itemType === 'Shotshell') {
|
|
59
79
|
return 2;
|
|
60
80
|
}
|
|
61
81
|
|
|
62
|
-
if (
|
|
82
|
+
if (itemType === 'Other') {
|
|
63
83
|
return 3;
|
|
64
84
|
}
|
|
65
85
|
|
|
@@ -90,20 +110,22 @@ function matchesConfirmationFilter(
|
|
|
90
110
|
return !summary.includeConfirmation;
|
|
91
111
|
}
|
|
92
112
|
|
|
93
|
-
function
|
|
113
|
+
function matchesItemTypeFilter(
|
|
94
114
|
summary: FileConfirmationSummary,
|
|
95
|
-
|
|
115
|
+
itemTypeFilter: FilesModalItemTypeFilter
|
|
96
116
|
): boolean {
|
|
97
|
-
if (
|
|
117
|
+
if (itemTypeFilter === 'all') {
|
|
98
118
|
return true;
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
const itemTypes = getSummaryItemTypes(summary);
|
|
122
|
+
|
|
123
|
+
if (itemTypeFilter === 'Other') {
|
|
124
|
+
// Treat legacy/unset item types as Other for filtering.
|
|
125
|
+
return itemTypes.length === 0 || itemTypes.includes('Other');
|
|
104
126
|
}
|
|
105
127
|
|
|
106
|
-
return
|
|
128
|
+
return itemTypes.includes(itemTypeFilter);
|
|
107
129
|
}
|
|
108
130
|
|
|
109
131
|
function matchesSearch(file: FileData, query: string): boolean {
|
|
@@ -127,7 +149,7 @@ export function filterFilesForModal(
|
|
|
127
149
|
return (
|
|
128
150
|
matchesSearch(file, searchQuery) &&
|
|
129
151
|
matchesConfirmationFilter(summary, preferences.confirmationFilter) &&
|
|
130
|
-
|
|
152
|
+
matchesItemTypeFilter(summary, preferences.itemTypeFilter)
|
|
131
153
|
);
|
|
132
154
|
});
|
|
133
155
|
}
|
|
@@ -177,7 +199,7 @@ export function sortFilesForModal(
|
|
|
177
199
|
return next.sort((left, right) => {
|
|
178
200
|
const leftSummary = getFileConfirmationState(left.id, statusById);
|
|
179
201
|
const rightSummary = getFileConfirmationState(right.id, statusById);
|
|
180
|
-
const difference =
|
|
202
|
+
const difference = getItemTypeRank(getPrimaryItemType(leftSummary)) - getItemTypeRank(getPrimaryItemType(rightSummary));
|
|
181
203
|
|
|
182
204
|
if (difference !== 0) {
|
|
183
205
|
return difference;
|