@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
|
@@ -13,6 +13,7 @@ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
|
13
13
|
interface SidebarContainerProps {
|
|
14
14
|
user: User;
|
|
15
15
|
onImageSelect: (file: FileData) => void;
|
|
16
|
+
onOpenCase: () => void;
|
|
16
17
|
imageId?: string;
|
|
17
18
|
currentCase: string;
|
|
18
19
|
files: FileData[];
|
|
@@ -23,6 +24,7 @@ interface SidebarContainerProps {
|
|
|
23
24
|
setShowNotes: (show: boolean) => void;
|
|
24
25
|
onAnnotationRefresh?: () => void;
|
|
25
26
|
isReadOnly?: boolean;
|
|
27
|
+
isArchivedCase?: boolean;
|
|
26
28
|
isConfirmed?: boolean;
|
|
27
29
|
confirmationSaveVersion?: number;
|
|
28
30
|
isUploading?: boolean;
|
|
@@ -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';
|
|
@@ -8,6 +9,7 @@ import { type FileData } from '~/types';
|
|
|
8
9
|
interface SidebarProps {
|
|
9
10
|
user: User;
|
|
10
11
|
onImageSelect: (file: FileData) => void;
|
|
12
|
+
onOpenCase: () => void;
|
|
11
13
|
imageId?: string;
|
|
12
14
|
currentCase: string;
|
|
13
15
|
files: FileData[];
|
|
@@ -18,6 +20,7 @@ interface SidebarProps {
|
|
|
18
20
|
setShowNotes: (show: boolean) => void;
|
|
19
21
|
onAnnotationRefresh?: () => void;
|
|
20
22
|
isReadOnly?: boolean;
|
|
23
|
+
isArchivedCase?: boolean;
|
|
21
24
|
isConfirmed?: boolean;
|
|
22
25
|
confirmationSaveVersion?: number;
|
|
23
26
|
isUploading?: boolean;
|
|
@@ -27,6 +30,7 @@ interface SidebarProps {
|
|
|
27
30
|
export const Sidebar = ({
|
|
28
31
|
user,
|
|
29
32
|
onImageSelect,
|
|
33
|
+
onOpenCase,
|
|
30
34
|
imageId,
|
|
31
35
|
currentCase,
|
|
32
36
|
imageLoaded,
|
|
@@ -35,6 +39,7 @@ export const Sidebar = ({
|
|
|
35
39
|
setFiles,
|
|
36
40
|
setShowNotes,
|
|
37
41
|
isReadOnly = false,
|
|
42
|
+
isArchivedCase = false,
|
|
38
43
|
isConfirmed = false,
|
|
39
44
|
confirmationSaveVersion = 0,
|
|
40
45
|
isUploading: initialIsUploading = false,
|
|
@@ -67,13 +72,20 @@ export const Sidebar = ({
|
|
|
67
72
|
setToastMessage(`${result.successCount} file${result.successCount !== 1 ? 's' : ''} uploaded!`);
|
|
68
73
|
}
|
|
69
74
|
setIsToastVisible(true);
|
|
70
|
-
}, []);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const handleExportNotification = useCallback((message: string, type: 'success' | 'error') => {
|
|
78
|
+
setToastType(type);
|
|
79
|
+
setToastMessage(message);
|
|
80
|
+
setIsToastVisible(true);
|
|
81
|
+
}, []);
|
|
71
82
|
|
|
72
83
|
return (
|
|
73
84
|
<div className={styles.sidebar}>
|
|
74
85
|
<CaseSidebar
|
|
75
86
|
user={user}
|
|
76
87
|
onImageSelect={onImageSelect}
|
|
88
|
+
onOpenCase={onOpenCase}
|
|
77
89
|
currentCase={currentCase}
|
|
78
90
|
imageLoaded={imageLoaded}
|
|
79
91
|
setImageLoaded={setImageLoaded}
|
|
@@ -81,12 +93,14 @@ export const Sidebar = ({
|
|
|
81
93
|
setFiles={setFiles}
|
|
82
94
|
onNotesClick={() => setShowNotes(true)}
|
|
83
95
|
isReadOnly={isReadOnly}
|
|
96
|
+
isArchivedCase={isArchivedCase}
|
|
84
97
|
isConfirmed={isConfirmed}
|
|
85
98
|
confirmationSaveVersion={confirmationSaveVersion}
|
|
86
99
|
selectedFileId={imageId}
|
|
87
100
|
isUploading={isUploading}
|
|
88
101
|
onUploadStatusChange={handleUploadStatusChange}
|
|
89
102
|
onUploadComplete={handleUploadComplete}
|
|
103
|
+
onExportNotification={handleExportNotification}
|
|
90
104
|
/>
|
|
91
105
|
<Toast
|
|
92
106
|
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
|
+
}
|
|
@@ -85,11 +85,13 @@ export const useOverlayDismiss = ({
|
|
|
85
85
|
}
|
|
86
86
|
}, [closeOnBackdrop, requestClose]);
|
|
87
87
|
|
|
88
|
+
const isBackdropDismissInteractive = closeOnBackdrop && canDismiss;
|
|
89
|
+
|
|
88
90
|
const overlayProps = {
|
|
89
|
-
role: 'button'
|
|
90
|
-
tabIndex: 0,
|
|
91
|
-
onMouseDown: handleOverlayMouseDown,
|
|
92
|
-
onKeyDown: handleOverlayKeyDown,
|
|
91
|
+
role: isBackdropDismissInteractive ? 'button' : 'presentation',
|
|
92
|
+
tabIndex: isBackdropDismissInteractive ? 0 : undefined,
|
|
93
|
+
onMouseDown: isBackdropDismissInteractive ? handleOverlayMouseDown : undefined,
|
|
94
|
+
onKeyDown: isBackdropDismissInteractive ? handleOverlayKeyDown : undefined,
|
|
93
95
|
style: { cursor: 'default' as const },
|
|
94
96
|
};
|
|
95
97
|
|
package/app/root.tsx
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { AuthProvider } from '~/components/auth/auth-provider';
|
|
17
17
|
import { auth } from '~/services/firebase';
|
|
18
18
|
import styles from '~/styles/root.module.css';
|
|
19
|
-
import './
|
|
19
|
+
import './global.css';
|
|
20
20
|
|
|
21
21
|
export const links: LinksFunction = () => [
|
|
22
22
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
|
@@ -732,6 +732,9 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
732
732
|
<SidebarContainer
|
|
733
733
|
user={user}
|
|
734
734
|
onImageSelect={handleImageSelect}
|
|
735
|
+
onOpenCase={() => {
|
|
736
|
+
void handleOpenCaseModal();
|
|
737
|
+
}}
|
|
735
738
|
imageId={imageId}
|
|
736
739
|
currentCase={currentCase}
|
|
737
740
|
imageLoaded={imageLoaded}
|
|
@@ -742,6 +745,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
742
745
|
setShowNotes={setShowNotes}
|
|
743
746
|
onAnnotationRefresh={refreshAnnotationData}
|
|
744
747
|
isReadOnly={isReadOnlyCase}
|
|
748
|
+
isArchivedCase={archiveDetails.archived}
|
|
745
749
|
isConfirmed={!!annotationData?.confirmationData}
|
|
746
750
|
confirmationSaveVersion={confirmationSaveVersion}
|
|
747
751
|
isUploading={isUploading}
|
|
@@ -797,6 +801,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
797
801
|
}}
|
|
798
802
|
currentCase={currentCase || ''}
|
|
799
803
|
user={user}
|
|
804
|
+
confirmationSaveVersion={confirmationSaveVersion}
|
|
800
805
|
/>
|
|
801
806
|
<FilesModal
|
|
802
807
|
isOpen={isFilesModalOpen}
|
|
@@ -809,6 +814,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
809
814
|
setFiles={setFiles}
|
|
810
815
|
isReadOnly={isReadOnlyCase}
|
|
811
816
|
selectedFileId={imageId}
|
|
817
|
+
confirmationSaveVersion={confirmationSaveVersion}
|
|
812
818
|
/>
|
|
813
819
|
<NotesEditorModal
|
|
814
820
|
isOpen={showNotes}
|
|
@@ -819,6 +825,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
819
825
|
onAnnotationRefresh={refreshAnnotationData}
|
|
820
826
|
originalFileName={files.find(file => file.id === imageId)?.originalFilename}
|
|
821
827
|
isUploading={isUploading}
|
|
828
|
+
showNotification={showNotification}
|
|
822
829
|
/>
|
|
823
830
|
<CaseExport
|
|
824
831
|
isOpen={isCaseExportModalOpen}
|
|
@@ -1067,7 +1067,7 @@ export class AuditService {
|
|
|
1067
1067
|
|
|
1068
1068
|
const bundledEntries = caseData.bundledAuditTrail?.entries;
|
|
1069
1069
|
if (!Array.isArray(bundledEntries)) {
|
|
1070
|
-
return
|
|
1070
|
+
return null;
|
|
1071
1071
|
}
|
|
1072
1072
|
|
|
1073
1073
|
const sortedEntries = sortAuditEntriesNewestFirst(bundledEntries);
|
|
@@ -1087,7 +1087,7 @@ export class AuditService {
|
|
|
1087
1087
|
private async getAuditEntries(params: AuditQueryParams, requestingUser?: User): Promise<ValidationAuditEntry[]> {
|
|
1088
1088
|
try {
|
|
1089
1089
|
const bundledArchivedEntries = await this.getBundledArchivedCaseAuditEntries(params, requestingUser);
|
|
1090
|
-
if (bundledArchivedEntries) {
|
|
1090
|
+
if (bundledArchivedEntries !== null) {
|
|
1091
1091
|
return bundledArchivedEntries;
|
|
1092
1092
|
}
|
|
1093
1093
|
|
|
@@ -118,7 +118,7 @@ export const buildCaseArchiveAuditParams = (
|
|
|
118
118
|
workflowPhase: 'casework',
|
|
119
119
|
caseDetails: {
|
|
120
120
|
newCaseName: input.caseName,
|
|
121
|
-
|
|
121
|
+
archiveReason: input.archiveReason,
|
|
122
122
|
totalFiles: input.totalFiles,
|
|
123
123
|
lastModified: archivedAt,
|
|
124
124
|
},
|
package/app/types/annotations.ts
CHANGED
|
@@ -22,19 +22,66 @@ export interface ConfirmationData {
|
|
|
22
22
|
confirmedAt: string; // ISO timestamp of confirmation
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface BulletAnnotationData {
|
|
26
|
+
caliber?: string;
|
|
27
|
+
mass?: string;
|
|
28
|
+
diameter?: string;
|
|
29
|
+
calcDiameter?: string;
|
|
30
|
+
lgNumber?: number;
|
|
31
|
+
lgDirection?: string;
|
|
32
|
+
barrelType?: string;
|
|
33
|
+
// Width arrays should align with lgNumber:
|
|
34
|
+
// L1..Ln stored in order at lWidths[0..n-1], G1..Gn at gWidths[0..n-1].
|
|
35
|
+
lWidths?: string[];
|
|
36
|
+
gWidths?: string[];
|
|
37
|
+
jacketMetal?: string;
|
|
38
|
+
coreMetal?: string;
|
|
39
|
+
bulletType?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CartridgeCaseAnnotationData {
|
|
43
|
+
caliber?: string;
|
|
44
|
+
brand?: string;
|
|
45
|
+
metal?: string;
|
|
46
|
+
primerType?: string;
|
|
47
|
+
fpiShape?: string;
|
|
48
|
+
apertureShape?: string;
|
|
49
|
+
hasFpDrag?: boolean;
|
|
50
|
+
hasExtractorMarks?: boolean;
|
|
51
|
+
hasEjectorMarks?: boolean;
|
|
52
|
+
hasChamberMarks?: boolean;
|
|
53
|
+
hasMagazineLipMarks?: boolean;
|
|
54
|
+
hasPrimerShear?: boolean;
|
|
55
|
+
hasEjectionPortMarks?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ShotshellAnnotationData {
|
|
59
|
+
gauge?: string;
|
|
60
|
+
shotSize?: string;
|
|
61
|
+
metal?: string;
|
|
62
|
+
brand?: string;
|
|
63
|
+
fpiShape?: string;
|
|
64
|
+
hasExtractorMarks?: boolean;
|
|
65
|
+
hasEjectorMarks?: boolean;
|
|
66
|
+
hasChamberMarks?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
25
69
|
export interface AnnotationData {
|
|
26
70
|
leftCase: string;
|
|
27
71
|
rightCase: string;
|
|
28
72
|
leftItem: string;
|
|
29
73
|
rightItem: string;
|
|
30
74
|
caseFontColor?: string;
|
|
31
|
-
classType?: 'Bullet' | 'Cartridge Case' | 'Other';
|
|
75
|
+
classType?: 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
|
|
32
76
|
customClass?: string;
|
|
33
77
|
classNote?: string;
|
|
34
78
|
indexType?: 'number' | 'color';
|
|
35
79
|
indexNumber?: string;
|
|
36
80
|
indexColor?: string;
|
|
37
81
|
supportLevel?: 'ID' | 'Exclusion' | 'Inconclusive';
|
|
82
|
+
bulletData?: BulletAnnotationData;
|
|
83
|
+
cartridgeCaseData?: CartridgeCaseAnnotationData;
|
|
84
|
+
shotshellData?: ShotshellAnnotationData;
|
|
38
85
|
hasSubclass?: boolean;
|
|
39
86
|
includeConfirmation: boolean;
|
|
40
87
|
confirmationData?: ConfirmationData;
|
package/app/types/audit.ts
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export type CasesModalSortBy = 'recent' | 'alphabetical';
|
|
2
|
+
|
|
3
|
+
export type CasesModalConfirmationFilter =
|
|
4
|
+
| 'all'
|
|
5
|
+
| 'pending'
|
|
6
|
+
| 'confirmed'
|
|
7
|
+
| 'none-requested';
|
|
8
|
+
|
|
9
|
+
export interface CasesModalPreferences {
|
|
10
|
+
sortBy: CasesModalSortBy;
|
|
11
|
+
confirmationFilter: CasesModalConfirmationFilter;
|
|
12
|
+
showArchivedOnly: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CasesModalCaseItem {
|
|
16
|
+
caseNumber: string;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
archived: boolean;
|
|
19
|
+
isReadOnly: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CaseConfirmationStatusValue {
|
|
23
|
+
includeConfirmation: boolean;
|
|
24
|
+
isConfirmed: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_CASE_CONFIRMATION_STATUS: CaseConfirmationStatusValue = {
|
|
28
|
+
includeConfirmation: false,
|
|
29
|
+
isConfirmed: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function compareCaseNumbersAlphabetically(a: string, b: string): number {
|
|
33
|
+
const getComponents = (value: string) => {
|
|
34
|
+
const numbers = value.match(/\d+/g)?.map(Number) || [];
|
|
35
|
+
const letters = value.match(/[A-Za-z]+/g)?.join('') || '';
|
|
36
|
+
return { numbers, letters };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const left = getComponents(a);
|
|
40
|
+
const right = getComponents(b);
|
|
41
|
+
|
|
42
|
+
const maxLength = Math.max(left.numbers.length, right.numbers.length);
|
|
43
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
44
|
+
const leftNumber = left.numbers[index] || 0;
|
|
45
|
+
const rightNumber = right.numbers[index] || 0;
|
|
46
|
+
|
|
47
|
+
if (leftNumber !== rightNumber) {
|
|
48
|
+
return leftNumber - rightNumber;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return left.letters.localeCompare(right.letters);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseTimestamp(value: string): number {
|
|
56
|
+
const parsed = Date.parse(value);
|
|
57
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function matchesConfirmationFilter(
|
|
61
|
+
caseNumber: string,
|
|
62
|
+
confirmationFilter: CasesModalConfirmationFilter,
|
|
63
|
+
caseConfirmationStatus: Record<string, CaseConfirmationStatusValue>
|
|
64
|
+
): boolean {
|
|
65
|
+
if (confirmationFilter === 'all') {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const status = caseConfirmationStatus[caseNumber] || DEFAULT_CASE_CONFIRMATION_STATUS;
|
|
70
|
+
|
|
71
|
+
if (confirmationFilter === 'pending') {
|
|
72
|
+
return status.includeConfirmation && !status.isConfirmed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (confirmationFilter === 'confirmed') {
|
|
76
|
+
return status.includeConfirmation && status.isConfirmed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return !status.includeConfirmation;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function filterCasesForModal(
|
|
83
|
+
cases: CasesModalCaseItem[],
|
|
84
|
+
preferences: CasesModalPreferences,
|
|
85
|
+
caseConfirmationStatus: Record<string, CaseConfirmationStatusValue>
|
|
86
|
+
): CasesModalCaseItem[] {
|
|
87
|
+
const archiveFilteredCases = preferences.showArchivedOnly
|
|
88
|
+
? cases.filter((entry) => entry.archived && !entry.isReadOnly)
|
|
89
|
+
: cases.filter((entry) => !entry.archived && !entry.isReadOnly);
|
|
90
|
+
|
|
91
|
+
return archiveFilteredCases.filter((entry) =>
|
|
92
|
+
matchesConfirmationFilter(entry.caseNumber, preferences.confirmationFilter, caseConfirmationStatus)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function sortCasesForModal(
|
|
97
|
+
cases: CasesModalCaseItem[],
|
|
98
|
+
sortBy: CasesModalSortBy
|
|
99
|
+
): CasesModalCaseItem[] {
|
|
100
|
+
const next = [...cases];
|
|
101
|
+
|
|
102
|
+
if (sortBy === 'recent') {
|
|
103
|
+
return next.sort((left, right) => {
|
|
104
|
+
const difference = parseTimestamp(right.createdAt) - parseTimestamp(left.createdAt);
|
|
105
|
+
if (difference !== 0) {
|
|
106
|
+
return difference;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return compareCaseNumbersAlphabetically(left.caseNumber, right.caseNumber);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return next.sort((left, right) =>
|
|
114
|
+
compareCaseNumbersAlphabetically(left.caseNumber, right.caseNumber)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getCasesForModal(
|
|
119
|
+
cases: CasesModalCaseItem[],
|
|
120
|
+
preferences: CasesModalPreferences,
|
|
121
|
+
caseConfirmationStatus: Record<string, CaseConfirmationStatusValue>
|
|
122
|
+
): CasesModalCaseItem[] {
|
|
123
|
+
return sortCasesForModal(
|
|
124
|
+
filterCasesForModal(cases, preferences, caseConfirmationStatus),
|
|
125
|
+
preferences.sortBy
|
|
126
|
+
);
|
|
127
|
+
}
|