@striae-org/striae 3.0.4

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 (223) hide show
  1. package/.env.example +100 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +18 -0
  4. package/README.md +133 -0
  5. package/app/components/actions/case-export/core-export.ts +328 -0
  6. package/app/components/actions/case-export/data-processing.ts +167 -0
  7. package/app/components/actions/case-export/download-handlers.ts +900 -0
  8. package/app/components/actions/case-export/index.ts +41 -0
  9. package/app/components/actions/case-export/metadata-helpers.ts +107 -0
  10. package/app/components/actions/case-export/types-constants.ts +56 -0
  11. package/app/components/actions/case-export/validation-utils.ts +25 -0
  12. package/app/components/actions/case-export.ts +4 -0
  13. package/app/components/actions/case-import/annotation-import.ts +35 -0
  14. package/app/components/actions/case-import/confirmation-import.ts +363 -0
  15. package/app/components/actions/case-import/image-operations.ts +61 -0
  16. package/app/components/actions/case-import/index.ts +39 -0
  17. package/app/components/actions/case-import/orchestrator.ts +420 -0
  18. package/app/components/actions/case-import/storage-operations.ts +270 -0
  19. package/app/components/actions/case-import/validation.ts +189 -0
  20. package/app/components/actions/case-import/zip-processing.ts +413 -0
  21. package/app/components/actions/case-manage.ts +524 -0
  22. package/app/components/actions/case-review.ts +4 -0
  23. package/app/components/actions/confirm-export.ts +351 -0
  24. package/app/components/actions/generate-pdf.ts +210 -0
  25. package/app/components/actions/image-manage.ts +385 -0
  26. package/app/components/actions/notes-manage.ts +33 -0
  27. package/app/components/actions/signout.module.css +15 -0
  28. package/app/components/actions/signout.tsx +50 -0
  29. package/app/components/audit/user-audit-viewer.tsx +975 -0
  30. package/app/components/audit/user-audit.module.css +568 -0
  31. package/app/components/auth/auth-provider.tsx +78 -0
  32. package/app/components/auth/mfa-enrollment.module.css +268 -0
  33. package/app/components/auth/mfa-enrollment.tsx +398 -0
  34. package/app/components/auth/mfa-verification.module.css +251 -0
  35. package/app/components/auth/mfa-verification.tsx +295 -0
  36. package/app/components/button/button.module.css +63 -0
  37. package/app/components/button/button.tsx +46 -0
  38. package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
  39. package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
  40. package/app/components/canvas/canvas.module.css +314 -0
  41. package/app/components/canvas/canvas.tsx +449 -0
  42. package/app/components/canvas/confirmation/confirmation.module.css +187 -0
  43. package/app/components/canvas/confirmation/confirmation.tsx +214 -0
  44. package/app/components/colors/colors.module.css +59 -0
  45. package/app/components/colors/colors.tsx +68 -0
  46. package/app/components/form/base-form.tsx +21 -0
  47. package/app/components/form/form-button.tsx +28 -0
  48. package/app/components/form/form-field.tsx +53 -0
  49. package/app/components/form/form-message.tsx +17 -0
  50. package/app/components/form/form-toggle.tsx +23 -0
  51. package/app/components/form/form.module.css +427 -0
  52. package/app/components/form/index.ts +6 -0
  53. package/app/components/icon/icon.module.css +3 -0
  54. package/app/components/icon/icon.tsx +27 -0
  55. package/app/components/icon/icons.svg +102 -0
  56. package/app/components/icon/manifest.json +110 -0
  57. package/app/components/sidebar/case-export/case-export.module.css +386 -0
  58. package/app/components/sidebar/case-export/case-export.tsx +317 -0
  59. package/app/components/sidebar/case-import/case-import.module.css +626 -0
  60. package/app/components/sidebar/case-import/case-import.tsx +404 -0
  61. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
  62. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
  63. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
  64. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
  65. package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
  66. package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
  67. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
  68. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
  69. package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
  70. package/app/components/sidebar/case-import/index.ts +18 -0
  71. package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
  72. package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
  73. package/app/components/sidebar/cases/cases-modal.module.css +166 -0
  74. package/app/components/sidebar/cases/cases-modal.tsx +201 -0
  75. package/app/components/sidebar/cases/cases.module.css +713 -0
  76. package/app/components/sidebar/files/files-modal.module.css +209 -0
  77. package/app/components/sidebar/files/files-modal.tsx +239 -0
  78. package/app/components/sidebar/hash/hash-utility.module.css +366 -0
  79. package/app/components/sidebar/hash/hash-utility.tsx +982 -0
  80. package/app/components/sidebar/notes/notes-modal.tsx +51 -0
  81. package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
  82. package/app/components/sidebar/notes/notes.module.css +360 -0
  83. package/app/components/sidebar/sidebar-container.tsx +149 -0
  84. package/app/components/sidebar/sidebar.module.css +321 -0
  85. package/app/components/sidebar/sidebar.tsx +215 -0
  86. package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
  87. package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
  88. package/app/components/theme-provider/theme-provider.tsx +131 -0
  89. package/app/components/theme-provider/theme.ts +155 -0
  90. package/app/components/toast/toast.module.css +137 -0
  91. package/app/components/toast/toast.tsx +56 -0
  92. package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
  93. package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
  94. package/app/components/toolbar/toolbar.module.css +42 -0
  95. package/app/components/toolbar/toolbar.tsx +167 -0
  96. package/app/components/user/delete-account.module.css +274 -0
  97. package/app/components/user/delete-account.tsx +471 -0
  98. package/app/components/user/inactivity-warning.module.css +145 -0
  99. package/app/components/user/inactivity-warning.tsx +84 -0
  100. package/app/components/user/manage-profile.module.css +190 -0
  101. package/app/components/user/manage-profile.tsx +253 -0
  102. package/app/components/user/mfa-phone-update.tsx +739 -0
  103. package/app/config-example/admin-service.json +13 -0
  104. package/app/config-example/config.json +17 -0
  105. package/app/config-example/firebase.ts +21 -0
  106. package/app/config-example/inactivity.ts +13 -0
  107. package/app/config-example/meta-config.json +6 -0
  108. package/app/contexts/auth.context.ts +12 -0
  109. package/app/entry.client.tsx +12 -0
  110. package/app/entry.server.tsx +44 -0
  111. package/app/hooks/useInactivityTimeout.ts +110 -0
  112. package/app/root.tsx +170 -0
  113. package/app/routes/_index.tsx +16 -0
  114. package/app/routes/auth/emailActionHandler.module.css +232 -0
  115. package/app/routes/auth/emailActionHandler.tsx +405 -0
  116. package/app/routes/auth/emailVerification.tsx +120 -0
  117. package/app/routes/auth/login.module.css +523 -0
  118. package/app/routes/auth/login.tsx +654 -0
  119. package/app/routes/auth/passwordReset.module.css +274 -0
  120. package/app/routes/auth/passwordReset.tsx +154 -0
  121. package/app/routes/auth/route.ts +16 -0
  122. package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
  123. package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
  124. package/app/routes/mobile-prevented/route.ts +14 -0
  125. package/app/routes/striae/striae.module.css +30 -0
  126. package/app/routes/striae/striae.tsx +417 -0
  127. package/app/services/audit-export.service.ts +755 -0
  128. package/app/services/audit.service.ts +1454 -0
  129. package/app/services/firebase-errors.ts +106 -0
  130. package/app/services/firebase.ts +15 -0
  131. package/app/styles/legal-pages.module.css +113 -0
  132. package/app/styles/root.module.css +146 -0
  133. package/app/tailwind.css +225 -0
  134. package/app/types/annotations.ts +45 -0
  135. package/app/types/audit.ts +301 -0
  136. package/app/types/case.ts +90 -0
  137. package/app/types/export.ts +8 -0
  138. package/app/types/file.ts +30 -0
  139. package/app/types/import.ts +107 -0
  140. package/app/types/index.ts +24 -0
  141. package/app/types/user.ts +38 -0
  142. package/app/utils/SHA256.ts +461 -0
  143. package/app/utils/annotation-timestamp.ts +25 -0
  144. package/app/utils/audit-export-signature.ts +117 -0
  145. package/app/utils/auth-action-settings.ts +48 -0
  146. package/app/utils/auth.ts +34 -0
  147. package/app/utils/batch-operations.ts +135 -0
  148. package/app/utils/confirmation-signature.ts +193 -0
  149. package/app/utils/data-operations.ts +871 -0
  150. package/app/utils/device-detection.ts +5 -0
  151. package/app/utils/html-sanitizer.ts +80 -0
  152. package/app/utils/id-generator.ts +36 -0
  153. package/app/utils/meta.ts +48 -0
  154. package/app/utils/mfa-phone.ts +97 -0
  155. package/app/utils/mfa.ts +79 -0
  156. package/app/utils/password-policy.ts +28 -0
  157. package/app/utils/permissions.ts +562 -0
  158. package/app/utils/signature-utils.ts +160 -0
  159. package/app/utils/style.ts +83 -0
  160. package/app/utils/version.ts +5 -0
  161. package/firebase.json +11 -0
  162. package/functions/[[path]].ts +10 -0
  163. package/package.json +138 -0
  164. package/postcss.config.js +6 -0
  165. package/public/.well-known/publickey.info@striae.org.asc +17 -0
  166. package/public/.well-known/security.txt +7 -0
  167. package/public/_headers +28 -0
  168. package/public/_routes.json +13 -0
  169. package/public/assets/striae.jpg +0 -0
  170. package/public/clear.jpg +0 -0
  171. package/public/favicon.ico +0 -0
  172. package/public/favicon.svg +9 -0
  173. package/public/icon-256.png +0 -0
  174. package/public/icon-512.png +0 -0
  175. package/public/logo-dark.png +0 -0
  176. package/public/manifest.json +25 -0
  177. package/public/oin-badge.png +0 -0
  178. package/public/shortcut.png +0 -0
  179. package/public/social-image.png +0 -0
  180. package/public/striae-ascii.txt +10 -0
  181. package/scripts/deploy-all.sh +100 -0
  182. package/scripts/deploy-config.sh +940 -0
  183. package/scripts/deploy-pages.sh +34 -0
  184. package/scripts/deploy-worker-secrets.sh +215 -0
  185. package/scripts/dev.cjs +23 -0
  186. package/scripts/install-workers.sh +88 -0
  187. package/scripts/run-eslint.cjs +35 -0
  188. package/scripts/update-compatibility-dates.cjs +124 -0
  189. package/scripts/update-markdown-versions.cjs +43 -0
  190. package/tailwind.config.ts +22 -0
  191. package/tsconfig.json +33 -0
  192. package/vite.config.ts +35 -0
  193. package/worker-configuration.d.ts +7490 -0
  194. package/workers/audit-worker/package.json +17 -0
  195. package/workers/audit-worker/src/audit-worker.example.ts +195 -0
  196. package/workers/audit-worker/worker-configuration.d.ts +7448 -0
  197. package/workers/audit-worker/wrangler.jsonc.example +29 -0
  198. package/workers/data-worker/package.json +17 -0
  199. package/workers/data-worker/src/data-worker.example.ts +267 -0
  200. package/workers/data-worker/src/signature-utils.ts +79 -0
  201. package/workers/data-worker/src/signing-payload-utils.ts +290 -0
  202. package/workers/data-worker/worker-configuration.d.ts +7448 -0
  203. package/workers/data-worker/wrangler.jsonc.example +30 -0
  204. package/workers/image-worker/package.json +17 -0
  205. package/workers/image-worker/src/image-worker.example.ts +180 -0
  206. package/workers/image-worker/worker-configuration.d.ts +7447 -0
  207. package/workers/image-worker/wrangler.jsonc.example +22 -0
  208. package/workers/keys-worker/package.json +17 -0
  209. package/workers/keys-worker/src/keys.example.ts +66 -0
  210. package/workers/keys-worker/src/keys.ts +66 -0
  211. package/workers/keys-worker/worker-configuration.d.ts +7447 -0
  212. package/workers/keys-worker/wrangler.jsonc.example +22 -0
  213. package/workers/pdf-worker/package.json +17 -0
  214. package/workers/pdf-worker/src/format-striae.ts +534 -0
  215. package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
  216. package/workers/pdf-worker/src/report-types.ts +69 -0
  217. package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
  218. package/workers/pdf-worker/wrangler.jsonc.example +26 -0
  219. package/workers/user-worker/package.json +17 -0
  220. package/workers/user-worker/src/user-worker.example.ts +636 -0
  221. package/workers/user-worker/worker-configuration.d.ts +7448 -0
  222. package/workers/user-worker/wrangler.jsonc.example +29 -0
  223. package/wrangler.toml.example +8 -0
@@ -0,0 +1,417 @@
1
+ import { User } from 'firebase/auth';
2
+ import { useState, useEffect } from 'react';
3
+ import { SidebarContainer } from '~/components/sidebar/sidebar-container';
4
+ import { Toolbar } from '~/components/toolbar/toolbar';
5
+ import { Canvas } from '~/components/canvas/canvas';
6
+ import { Toast } from '~/components/toast/toast';
7
+ import { getImageUrl } from '~/components/actions/image-manage';
8
+ import { getNotes, saveNotes } from '~/components/actions/notes-manage';
9
+ import { generatePDF } from '~/components/actions/generate-pdf';
10
+ import { getUserApiKey } from '~/utils/auth';
11
+ import { resolveEarliestAnnotationTimestamp } from '~/utils/annotation-timestamp';
12
+ import { AnnotationData, FileData } from '~/types';
13
+ import { checkCaseIsReadOnly } from '~/components/actions/case-manage';
14
+ import paths from '~/config/config.json';
15
+ import styles from './striae.module.css';
16
+
17
+ interface StriaePage {
18
+ user: User;
19
+ }
20
+
21
+ export const Striae = ({ user }: StriaePage) => {
22
+ // Image and error states
23
+ const [selectedImage, setSelectedImage] = useState<string>();
24
+ const [selectedFilename, setSelectedFilename] = useState<string>();
25
+ const [imageId, setImageId] = useState<string>();
26
+ const [error, setError] = useState<string>();
27
+ const [imageLoaded, setImageLoaded] = useState(false);
28
+
29
+ // User states
30
+ const [userCompany, setUserCompany] = useState<string>('');
31
+ const [userFirstName, setUserFirstName] = useState<string>('');
32
+
33
+ // Case management states - All managed here
34
+ const [currentCase, setCurrentCase] = useState<string>('');
35
+ const [files, setFiles] = useState<FileData[]>([]);
36
+ const [caseNumber, setCaseNumber] = useState('');
37
+ const [successAction, setSuccessAction] = useState<'loaded' | 'created' | 'deleted' | null>(null);
38
+ const [showNotes, setShowNotes] = useState(false);
39
+ const [isReadOnlyCase, setIsReadOnlyCase] = useState(false);
40
+
41
+ // Annotation states
42
+ const [activeAnnotations, setActiveAnnotations] = useState<Set<string>>(new Set());
43
+ const [annotationData, setAnnotationData] = useState<AnnotationData | null>(null);
44
+ const [annotationRefreshTrigger, setAnnotationRefreshTrigger] = useState(0);
45
+ const [confirmationSaveVersion, setConfirmationSaveVersion] = useState(0);
46
+
47
+ // Box annotation states
48
+ const [isBoxAnnotationMode, setIsBoxAnnotationMode] = useState(false);
49
+ const [boxAnnotationColor, setBoxAnnotationColor] = useState('#ff0000');
50
+
51
+ // PDF generation states
52
+ const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
53
+ const [showToast, setShowToast] = useState(false);
54
+ const [toastMessage, setToastMessage] = useState('');
55
+ const [toastType, setToastType] = useState<'success' | 'error'>('success');
56
+
57
+
58
+ useEffect(() => {
59
+ // Set clear.jpg when case changes or is cleared
60
+ setSelectedImage('/clear.jpg');
61
+ setSelectedFilename(undefined);
62
+ setImageId(undefined);
63
+ setAnnotationData(null);
64
+ setError(undefined);
65
+ setImageLoaded(false);
66
+
67
+ // Reset annotation and UI states when case is cleared
68
+ if (!currentCase) {
69
+ setActiveAnnotations(new Set());
70
+ setIsBoxAnnotationMode(false);
71
+ setIsReadOnlyCase(false);
72
+ }
73
+ }, [currentCase]);
74
+
75
+ // Fetch user company data when component mounts
76
+ useEffect(() => {
77
+ const fetchUserCompany = async () => {
78
+ try {
79
+ const apiKey = await getUserApiKey();
80
+ const response = await fetch(`${paths.user_worker_url}/${user.uid}`, {
81
+ headers: {
82
+ 'X-Custom-Auth-Key': apiKey
83
+ }
84
+ });
85
+
86
+ if (response.ok) {
87
+ const userData = await response.json() as { company?: string; firstName?: string };
88
+ setUserCompany(userData.company || '');
89
+ setUserFirstName(userData.firstName || '');
90
+ }
91
+ } catch (err) {
92
+ console.error('Failed to load user company:', err);
93
+ }
94
+ };
95
+
96
+ if (user?.uid) {
97
+ fetchUserCompany();
98
+ }
99
+ }, [user?.uid]);
100
+
101
+ const handleCaseChange = (caseNumber: string) => {
102
+ setCurrentCase(caseNumber);
103
+ setCaseNumber(caseNumber);
104
+ setAnnotationData(null);
105
+ setSelectedFilename(undefined);
106
+ setImageId(undefined);
107
+ };
108
+
109
+ // Check if current case is read-only when case changes
110
+ useEffect(() => {
111
+ const checkReadOnlyStatus = async () => {
112
+ if (!currentCase || !user?.uid) {
113
+ setIsReadOnlyCase(false);
114
+ return;
115
+ }
116
+
117
+ try {
118
+ // Check if the case data itself has isReadOnly: true
119
+ const isReadOnly = await checkCaseIsReadOnly(user, currentCase);
120
+ setIsReadOnlyCase(isReadOnly);
121
+ } catch (error) {
122
+ console.error('Error checking read-only status:', error);
123
+ setIsReadOnlyCase(false);
124
+ }
125
+ };
126
+
127
+ checkReadOnlyStatus();
128
+ }, [currentCase, user?.uid]);
129
+
130
+ // Disable box annotation mode when notes sidebar is opened
131
+ useEffect(() => {
132
+ if (showNotes && isBoxAnnotationMode) {
133
+ setIsBoxAnnotationMode(false);
134
+ }
135
+ }, [showNotes, isBoxAnnotationMode]);
136
+
137
+ // Handler for toolbar annotation selection
138
+ const handleToolSelect = (toolId: string, active: boolean) => {
139
+ // Always allow visibility toggles (including for read-only cases)
140
+ setActiveAnnotations(prev => {
141
+ const next = new Set(prev);
142
+ if (active) {
143
+ next.add(toolId);
144
+ } else {
145
+ next.delete(toolId);
146
+ }
147
+ return next;
148
+ });
149
+
150
+ // Handle box annotation mode (prevent when notes are open, read-only, or confirmed)
151
+ if (toolId === 'box') {
152
+ setIsBoxAnnotationMode(active && !showNotes && !isReadOnlyCase && !annotationData?.confirmationData);
153
+ }
154
+ };
155
+
156
+ // Handler for color change from toolbar color selector
157
+ const handleColorChange = (color: string) => {
158
+ setBoxAnnotationColor(color);
159
+ };
160
+
161
+ // Generate PDF function
162
+ const handleGeneratePDF = async () => {
163
+ // Prevent PDF generation for read-only cases
164
+ if (isReadOnlyCase) {
165
+ return;
166
+ }
167
+
168
+ await generatePDF({
169
+ user,
170
+ selectedImage,
171
+ selectedFilename,
172
+ userCompany,
173
+ userFirstName,
174
+ currentCase,
175
+ annotationData,
176
+ activeAnnotations,
177
+ setIsGeneratingPDF,
178
+ setToastType,
179
+ setToastMessage,
180
+ setShowToast
181
+ });
182
+ };
183
+
184
+ // Close toast notification
185
+ const closeToast = () => {
186
+ setShowToast(false);
187
+ };
188
+
189
+ // Function to refresh annotation data (called when notes are saved)
190
+ const refreshAnnotationData = () => {
191
+ setAnnotationRefreshTrigger(prev => prev + 1);
192
+ };
193
+
194
+ useEffect(() => {
195
+ // Cleanup function to clear image when component unmounts
196
+ return () => {
197
+ setSelectedImage(undefined);
198
+ setSelectedFilename(undefined);
199
+ setError(undefined);
200
+ setImageLoaded(false);
201
+ setAnnotationData(null);
202
+ };
203
+ }, []); // Empty dependency array means this runs only on mount/unmount
204
+
205
+ // Load annotation data when imageId changes
206
+ useEffect(() => {
207
+ const loadAnnotationData = async () => {
208
+ if (!imageId || !currentCase) {
209
+ setAnnotationData(null);
210
+ return;
211
+ }
212
+
213
+ try {
214
+ const notes = await getNotes(user, currentCase, imageId);
215
+ if (notes) {
216
+ setAnnotationData({
217
+ leftCase: notes.leftCase || '',
218
+ rightCase: notes.rightCase || '',
219
+ leftItem: notes.leftItem || '',
220
+ rightItem: notes.rightItem || '',
221
+ caseFontColor: notes.caseFontColor || '#FFDE21',
222
+ classType: notes.classType || 'Other',
223
+ customClass: notes.customClass,
224
+ classNote: notes.classNote, // Optional - pass as-is
225
+ indexType: notes.indexType || 'number',
226
+ indexNumber: notes.indexNumber,
227
+ indexColor: notes.indexColor,
228
+ supportLevel: notes.supportLevel || 'Inconclusive',
229
+ hasSubclass: notes.hasSubclass,
230
+ includeConfirmation: notes.includeConfirmation ?? false, // Required
231
+ confirmationData: notes.confirmationData, // Add imported confirmation data
232
+ additionalNotes: notes.additionalNotes, // Optional - pass as-is
233
+ boxAnnotations: notes.boxAnnotations || [],
234
+ earliestAnnotationTimestamp: notes.earliestAnnotationTimestamp,
235
+ updatedAt: notes.updatedAt || new Date().toISOString()
236
+ });
237
+ } else {
238
+ setAnnotationData(null);
239
+ }
240
+ } catch (error) {
241
+ console.error('Failed to load annotation data:', error);
242
+ setAnnotationData(null);
243
+ }
244
+ };
245
+
246
+ loadAnnotationData();
247
+ }, [imageId, currentCase, user, annotationRefreshTrigger]);
248
+
249
+
250
+ const handleImageSelect = async (file: FileData) => {
251
+ if (file?.id === 'clear') {
252
+ setSelectedImage('/clear.jpg');
253
+ setSelectedFilename(undefined);
254
+ setImageId(undefined);
255
+ setImageLoaded(false);
256
+ setAnnotationData(null);
257
+ setError(undefined);
258
+ return;
259
+ }
260
+
261
+ if (!file?.id) {
262
+ setError('Invalid file selected');
263
+ return;
264
+ }
265
+
266
+ try {
267
+ setError(undefined);
268
+ setSelectedImage(undefined);
269
+ setSelectedFilename(undefined);
270
+ setImageLoaded(false);
271
+
272
+ const signedUrl = await getImageUrl(user, file, currentCase);
273
+ if (!signedUrl) throw new Error('No URL returned');
274
+
275
+ setSelectedImage(signedUrl);
276
+ setSelectedFilename(file.originalFilename);
277
+ setImageId(file.id);
278
+ setImageLoaded(true);
279
+
280
+ } catch (err) {
281
+ setError('Failed to load image. Please try again.');
282
+ console.error('Image selection error:', err);
283
+ setSelectedImage(undefined);
284
+ setSelectedFilename(undefined);
285
+ }
286
+ };
287
+
288
+ // Automatic save handler for annotation updates
289
+ const handleAnnotationUpdate = async (data: AnnotationData) => {
290
+ if (annotationData?.confirmationData) {
291
+ console.warn('Blocked annotation update for confirmed image');
292
+ return;
293
+ }
294
+
295
+ const now = new Date().toISOString();
296
+ const dataWithEarliestTimestamp: AnnotationData = {
297
+ ...data,
298
+ earliestAnnotationTimestamp: resolveEarliestAnnotationTimestamp(
299
+ data.earliestAnnotationTimestamp,
300
+ annotationData?.earliestAnnotationTimestamp,
301
+ now
302
+ ),
303
+ };
304
+
305
+ const confirmationChanged =
306
+ !!annotationData?.confirmationData !== !!data.confirmationData ||
307
+ !!annotationData?.includeConfirmation !== !!data.includeConfirmation;
308
+
309
+ // Update local state immediately
310
+ setAnnotationData(dataWithEarliestTimestamp);
311
+
312
+ // For read-only cases, only save if it's confirmation data
313
+ if (isReadOnlyCase) {
314
+ // Save confirmation data to server even in read-only cases
315
+ if (data.confirmationData && user && currentCase && imageId) {
316
+ try {
317
+ await saveNotes(user, currentCase, imageId, dataWithEarliestTimestamp);
318
+ if (confirmationChanged) {
319
+ setConfirmationSaveVersion(prev => prev + 1);
320
+ }
321
+ console.log('Confirmation data saved to server in read-only case');
322
+ } catch (saveError) {
323
+ console.error('Failed to save confirmation data:', saveError);
324
+ }
325
+ } else {
326
+ console.log('Read-only case: non-confirmation annotation data updated locally but not saved to server');
327
+ }
328
+ return;
329
+ }
330
+
331
+ // Auto-save to server if we have required data
332
+ if (user && currentCase && imageId) {
333
+ try {
334
+ // Ensure required fields have default values before saving
335
+ const dataToSave: AnnotationData = {
336
+ ...dataWithEarliestTimestamp,
337
+ includeConfirmation: data.includeConfirmation ?? false, // Required field
338
+ };
339
+
340
+ await saveNotes(user, currentCase, imageId, dataToSave);
341
+ if (confirmationChanged) {
342
+ setConfirmationSaveVersion(prev => prev + 1);
343
+ }
344
+ } catch (saveError) {
345
+ console.error('Failed to auto-save annotations:', saveError);
346
+ // Still show the annotations locally even if save fails
347
+ }
348
+ }
349
+ };
350
+
351
+ return (
352
+ <div className={styles.appContainer}>
353
+ <SidebarContainer
354
+ user={user}
355
+ onImageSelect={handleImageSelect}
356
+ imageId={imageId}
357
+ onCaseChange={handleCaseChange}
358
+ currentCase={currentCase}
359
+ setCurrentCase={setCurrentCase}
360
+ imageLoaded={imageLoaded}
361
+ setImageLoaded={setImageLoaded}
362
+ files={files}
363
+ setFiles={setFiles}
364
+ caseNumber={caseNumber}
365
+ setCaseNumber={setCaseNumber}
366
+ error={error ?? ''}
367
+ setError={setError}
368
+ successAction={successAction}
369
+ setSuccessAction={setSuccessAction}
370
+ showNotes={showNotes}
371
+ setShowNotes={setShowNotes}
372
+ onAnnotationRefresh={refreshAnnotationData}
373
+ isReadOnly={isReadOnlyCase}
374
+ isConfirmed={!!annotationData?.confirmationData}
375
+ confirmationSaveVersion={confirmationSaveVersion}
376
+ />
377
+ <main className={styles.mainContent}>
378
+ <div className={styles.canvasArea}>
379
+ <div className={styles.toolbarWrapper}>
380
+ <Toolbar
381
+ onToolSelect={handleToolSelect}
382
+ onGeneratePDF={handleGeneratePDF}
383
+ canGeneratePDF={!!(selectedImage && selectedImage !== '/clear.jpg')}
384
+ isGeneratingPDF={isGeneratingPDF}
385
+ onColorChange={handleColorChange}
386
+ selectedColor={boxAnnotationColor}
387
+ isReadOnly={isReadOnlyCase}
388
+ isConfirmed={!!annotationData?.confirmationData}
389
+ isNotesOpen={showNotes}
390
+ />
391
+ </div>
392
+ <Canvas
393
+ imageUrl={selectedImage}
394
+ filename={selectedFilename}
395
+ company={userCompany}
396
+ firstName={userFirstName}
397
+ error={error ?? ''}
398
+ activeAnnotations={activeAnnotations}
399
+ annotationData={annotationData}
400
+ isBoxAnnotationMode={isBoxAnnotationMode}
401
+ boxAnnotationColor={boxAnnotationColor}
402
+ onAnnotationUpdate={handleAnnotationUpdate}
403
+ isReadOnly={isReadOnlyCase}
404
+ caseNumber={currentCase}
405
+ currentImageId={imageId}
406
+ />
407
+ </div>
408
+ </main>
409
+ <Toast
410
+ message={toastMessage}
411
+ type={toastType}
412
+ isVisible={showToast}
413
+ onClose={closeToast}
414
+ />
415
+ </div>
416
+ );
417
+ };