oceanhelm 0.0.12 → 0.0.13

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.
@@ -3,16 +3,13 @@
3
3
 
4
4
  <!-- ── Missing Reports Alert Banner ── -->
5
5
  <transition name="slide-down">
6
- <div class="missing-banner" v-if="pendingContext || missingReports.length > 0">
6
+ <div class="missing-banner" v-if="!bannerDismissed && (pendingContext || missingReports.length > 0)">
7
7
  <div class="missing-banner-inner">
8
- <div class="missing-icon">
9
- <i class="bi bi-exclamation-triangle-fill"></i>
10
- </div>
8
+ <div class="missing-icon"><i class="bi bi-exclamation-triangle-fill"></i></div>
11
9
  <div class="missing-content">
12
10
  <strong v-if="pendingContext">
13
11
  Action Required — Report for
14
- <span class="ref-chip">{{ pendingContext.entity_ref }}</span>
15
- must be submitted
12
+ <span class="ref-chip">{{ pendingContext.entity_ref }}</span> must be submitted
16
13
  </strong>
17
14
  <strong v-else>
18
15
  {{ missingReports.length }} report{{ missingReports.length > 1 ? 's' : '' }} awaiting
@@ -20,7 +17,7 @@
20
17
  </strong>
21
18
  <p v-if="pendingContext">{{ pendingContext.title }}</p>
22
19
  </div>
23
- <button class="btn btn-warning btn-sm fw-bold" @click="openReportForm">
20
+ <button class="btn btn-warning btn-sm fw-bold" @click="openReportForm('qhse')">
24
21
  <i class="bi bi-pencil-square me-1"></i> Submit Report
25
22
  </button>
26
23
  <button class="btn-dismiss" @click="dismissBanner" title="Dismiss">
@@ -37,30 +34,61 @@
37
34
  <p class="page-subtitle">Document Management &amp; QHSE Reports</p>
38
35
  </div>
39
36
  <div class="header-actions">
40
- <button class="btn btn-outline-primary me-2" @click="openReportForm">
41
- <i class="bi bi-file-earmark-plus"></i> New Report
42
- </button>
43
- <button class="btn btn-primary" @click="showNewFolderModal = true">
44
- <i class="bi bi-folder-plus"></i> New Folder
45
- </button>
37
+ <div class="btn-group-split">
38
+ <button class="btn btn-outline-primary" @click="openReportForm('qhse')">
39
+ <i class="bi bi-shield-check"></i> QHSE Report
40
+ </button>
41
+ <button class="btn btn-outline-secondary" @click="openReportForm('manual')">
42
+ <i class="bi bi-file-earmark-plus"></i> Manual Report
43
+ </button>
44
+ <button class="btn btn-primary" @click="showNewFolderModal = true">
45
+ <i class="bi bi-folder-plus"></i> New Folder
46
+ </button>
47
+ </div>
46
48
  </div>
47
49
  </div>
48
50
 
49
- <!-- ── Submitted Reports Section ── -->
50
- <div class="submitted-section" v-if="linkedReports.length > 0">
51
- <div class="section-label">
52
- <i class="bi bi-file-earmark-check-fill text-success"></i>
53
- Submitted QHSE Reports
54
- </div>
55
- <div class="report-cards">
56
- <div v-for="report in linkedReports" :key="report.id" :ref="'report-' + report.entity_ref"
57
- :class="['report-card', { 'report-card--highlighted': highlightedRef === report.entity_ref }]"
58
- @click="viewReport(report)">
59
- <div class="rc-badge" :class="report.entity_type">
60
- {{ report.entity_type === 'drill' ? 'DRILL' : 'INC' }}
61
- </div>
62
- <div class="rc-body">
63
- <strong class="rc-ref">{{ report.entity_ref }}</strong>
51
+ <!-- ── Tab Navigation ── -->
52
+ <div class="tab-nav">
53
+ <button :class="['tab-btn', { active: activeTab === 'all' }]" @click="activeTab = 'all'">
54
+ All Reports <span class="tab-count">{{ reports.length }}</span>
55
+ </button>
56
+ <button :class="['tab-btn', { active: activeTab === 'drill' }]" @click="activeTab = 'drill'">
57
+ <i class="bi bi-people-fill me-1"></i>Drills
58
+ <span class="tab-count drill">{{ drillReports.length }}</span>
59
+ </button>
60
+ <button :class="['tab-btn', { active: activeTab === 'incident' }]" @click="activeTab = 'incident'">
61
+ <i class="bi bi-exclamation-octagon-fill me-1"></i>Incidents
62
+ <span class="tab-count incident">{{ incidentReports.length }}</span>
63
+ </button>
64
+ <button :class="['tab-btn', { active: activeTab === 'manual' }]" @click="activeTab = 'manual'">
65
+ <i class="bi bi-file-earmark-text me-1"></i>Manual
66
+ <span class="tab-count manual">{{ manualReports.length }}</span>
67
+ </button>
68
+ <button :class="['tab-btn', { active: activeTab === 'folders' }]" @click="activeTab = 'folders'">
69
+ <i class="bi bi-folder-fill me-1"></i>Folders
70
+ <span class="tab-count">{{ reports.length + folders.length }}</span>
71
+ </button>
72
+ </div>
73
+
74
+ <!-- ── Loading ── -->
75
+ <div v-if="isLoading" class="loading-container">
76
+ <div class="spinner"></div>
77
+ <p>Loading reports…</p>
78
+ </div>
79
+
80
+ <!-- ════════════════════════════════════════
81
+ REPORTS LIST TABS (all/drill/incident/manual)
82
+ ════════════════════════════════════════ -->
83
+ <div v-if="activeTab !== 'folders' && !isLoading">
84
+ <div class="report-cards" v-if="filteredReports.length > 0">
85
+ <div v-for="report in filteredReports" :key="report.id" :ref="'report-' + report.entityRef"
86
+ :class="['report-card', { 'report-card--highlighted': highlightedRef === report.entityRef }]">
87
+ <div class="rc-type-bar" :class="reportTypeClass(report)"></div>
88
+ <div class="rc-badge" :class="reportTypeClass(report)">{{ reportTypeLabel(report) }}</div>
89
+
90
+ <div class="rc-body" @click="viewReport(report)">
91
+ <strong class="rc-ref">{{ report.entityRef || report.id?.slice(0, 8).toUpperCase() }}</strong>
64
92
  <span class="rc-title">{{ report.title }}</span>
65
93
  <span class="rc-meta">
66
94
  <i class="bi bi-person-fill me-1"></i>{{ report.submittedBy }}
@@ -68,79 +96,241 @@
68
96
  <i class="bi bi-calendar3 me-1"></i>{{ formatDate(report.submittedAt) }}
69
97
  </span>
70
98
  </div>
71
- <div class="rc-folder" v-if="report.folderName">
72
- <i class="bi bi-folder-fill me-1"></i>{{ report.folderName }}
99
+
100
+ <div class="rc-files" v-if="getFilesForReport(report.id).length > 0">
101
+ <i class="bi bi-paperclip"></i> {{ getFilesForReport(report.id).length }}
73
102
  </div>
74
- <i class="bi bi-chevron-right rc-arrow"></i>
75
- </div>
76
- </div>
77
- </div>
78
103
 
79
- <!-- ── Folders Section ── -->
80
- <div class="section-label mt-4" v-if="folders.length > 0 || linkedReports.length === 0">
81
- <i class="bi bi-folder-fill text-warning"></i> Document Folders
82
- </div>
83
- <div class="folders-section" v-if="folders.length > 0">
84
- <div class="folder-card" v-for="folder in folders" :key="folder.id" @mouseenter="showPreview(folder.id)"
85
- @mouseleave="hidePreview">
86
- <div class="folder-content" @click="toggleFolder(folder.id)">
87
- <i class="bi bi-folder-fill text-warning" style="font-size: 2.5rem;"></i>
88
- <div class="folder-name">{{ folder.name }}</div>
89
- <small class="text-muted">{{ folder.files.length }} file(s)</small>
90
- </div>
91
- <div class="folder-actions-overlay">
92
- <button class="btn btn-sm btn-icon" @click.stop="selectFolder(folder)" title="Add Files">
93
- <i class="bi bi-file-earmark-plus"></i>
94
- </button>
95
- <button class="btn btn-sm btn-icon btn-icon-danger" @click.stop="deleteFolder(folder.id)"
96
- title="Delete Folder">
97
- <i class="bi bi-trash"></i>
104
+ <!-- Folder pill: click jumps to the auto-folder -->
105
+ <button class="rc-folder-pill" :class="reportTypeClass(report)" @click.stop="jumpToFolder(report)"
106
+ title="View folder">
107
+ <i class="bi bi-folder-fill me-1"></i>
108
+ {{ report.entityRef || report.title }}
98
109
  </button>
110
+
111
+ <div class="rc-actions">
112
+ <button class="rc-btn rc-btn--view" @click="viewReport(report)" title="View">
113
+ <i class="bi bi-eye"></i>
114
+ </button>
115
+ <button class="rc-btn rc-btn--download" @click="downloadReport(report)"
116
+ :disabled="generatingPdf === report.id" title="Download ZIP">
117
+ <span v-if="generatingPdf === report.id" class="spinner-xs"></span>
118
+ <i v-else class="bi bi-file-zip"></i>
119
+ </button>
120
+ </div>
99
121
  </div>
100
122
  </div>
101
- </div>
102
123
 
103
- <!-- Empty State -->
104
- <div class="empty-state text-center" v-if="folders.length === 0 && linkedReports.length === 0">
105
- <i class="bi bi-folder2-open" style="font-size: 4rem; color: #ccc;"></i>
106
- <h5 class="mt-3 text-muted">No reports or folders yet</h5>
107
- <p class="text-muted">Submit a report or create a folder to get started</p>
124
+ <div class="empty-state text-center" v-if="filteredReports.length === 0">
125
+ <i class="bi bi-file-earmark-x" style="font-size:3.5rem;color:#ccc;"></i>
126
+ <h5 class="mt-3 text-muted">No {{ activeTab === 'all' ? '' : activeTab }} reports yet</h5>
127
+ <p class="text-muted">
128
+ <span v-if="activeTab === 'manual'">Create a manual report using the button above</span>
129
+ <span v-else-if="activeTab === 'drill'">Submit a report after logging a drill</span>
130
+ <span v-else-if="activeTab === 'incident'">Submit a report after logging an incident</span>
131
+ <span v-else>Submit a report or create a folder to get started</span>
132
+ </p>
133
+ </div>
108
134
  </div>
109
135
 
110
- <!-- ── Expanded Folder Modal ── -->
111
- <div class="folder-modal-overlay" v-if="expandedFolders.length > 0" @click="closeAllFolders">
112
- <div class="folder-expanded" @click.stop>
113
- <div class="expanded-header">
114
- <strong>{{ getExpandedFolder().name }}</strong>
115
- <button class="btn-close-expanded" @click="closeAllFolders">
116
- <i class="bi bi-x-lg"></i>
117
- </button>
136
+ <!-- ════════════════════════════════════════
137
+ FOLDERS TAB
138
+ ════════════════════════════════════════ -->
139
+ <div v-if="activeTab === 'folders' && !isLoading">
140
+
141
+ <!-- ─ Auto-folders ─ -->
142
+ <div v-if="reports.length > 0">
143
+ <div class="section-heading">
144
+ <i class="bi bi-folder-symlink-fill text-primary me-1"></i>
145
+ Report Folders
146
+ <span class="section-count">{{ reports.length }}</span>
147
+ <span class="section-hint">Auto-generated · one per submitted report</span>
118
148
  </div>
119
- <div class="files-list" v-if="getExpandedFolder().files.length > 0">
120
- <div class="file-item d-flex justify-content-between align-items-center"
121
- v-for="file in getExpandedFolder().files" :key="file.id">
122
- <div class="file-info d-flex align-items-center">
123
- <i class="bi bi-file-earmark-text me-2"></i>
124
- <span>{{ file.name }}</span>
125
- <small class="text-muted ms-2">({{ formatFileSize(file.size) }})</small>
149
+
150
+ <div class="folders-list">
151
+ <div v-for="report in reports" :key="'af-' + report.id"
152
+ :ref="'folder-' + (report.entityRef || report.id)"
153
+ :class="['auto-folder-card', reportTypeClass(report), { 'is-open': expandedFolders.includes('auto-' + report.id) }]">
154
+ <div class="af-header" @click="toggleFolder('auto-' + report.id)">
155
+ <div class="af-icon-wrap">
156
+ <i class="bi bi-folder-fill af-folder-icon"
157
+ :class="'color--' + reportTypeClass(report)"></i>
158
+ <span class="af-count-badge">{{ 1 + getFilesForReport(report.id).length }}</span>
159
+ </div>
160
+ <div class="af-meta">
161
+ <span class="af-ref">{{ report.entityRef || report.id?.slice(0, 8).toUpperCase()
162
+ }}</span>
163
+ <span class="af-title">{{ report.title }}</span>
164
+ <div class="af-sub">
165
+ <span class="mini-badge" :class="reportTypeClass(report)">{{ reportTypeLabel(report)
166
+ }}</span>
167
+ <span class="text-muted">{{ formatDate(report.submittedAt) }}</span>
168
+ <span class="text-muted" v-if="getFilesForReport(report.id).length">
169
+ · {{ getFilesForReport(report.id).length }} attachment{{
170
+ getFilesForReport(report.id).length !== 1 ? 's' : '' }}
171
+ </span>
172
+ </div>
173
+ </div>
174
+ <div class="af-actions">
175
+ <button class="rc-btn rc-btn--download" @click.stop="downloadReport(report)"
176
+ :disabled="generatingPdf === report.id" title="Download ZIP">
177
+ <span v-if="generatingPdf === report.id" class="spinner-xs"></span>
178
+ <i v-else class="bi bi-file-zip"></i>
179
+ </button>
180
+ <i class="bi chevron-icon"
181
+ :class="expandedFolders.includes('auto-' + report.id) ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
182
+ </div>
126
183
  </div>
127
- <button class="btn btn-sm btn-outline-danger"
128
- @click="removeFile(getExpandedFolder().id, file.id)">
129
- <i class="bi bi-x-circle"></i>
130
- </button>
184
+
185
+ <transition name="folder-expand">
186
+ <div class="af-contents" v-if="expandedFolders.includes('auto-' + report.id)">
187
+
188
+ <div class="ffi ffi--report" @click="viewReport(report)">
189
+ <div class="ffi-icon ffi-icon--pdf"><i class="bi bi-file-earmark-pdf-fill"></i>
190
+ </div>
191
+ <div class="ffi-meta">
192
+ <span class="ffi-name">{{ safeRef(report) }}_report.pdf</span>
193
+ <span class="ffi-sub">Generated report sheet · click to preview</span>
194
+ </div>
195
+ <div class="ffi-acts">
196
+ <button class="rc-btn rc-btn--view" @click.stop="viewReport(report)"
197
+ title="Preview"><i class="bi bi-eye"></i></button>
198
+ <button class="rc-btn rc-btn--download"
199
+ @click.stop="downloadReportPdfOnly(report)"
200
+ :disabled="generatingPdf === report.id + '-pdf'" title="Download PDF only">
201
+ <span v-if="generatingPdf === report.id + '-pdf'" class="spinner-xs"></span>
202
+ <i v-else class="bi bi-download"></i>
203
+ </button>
204
+ </div>
205
+ </div>
206
+
207
+ <div v-for="f in getFilesForReport(report.id)" :key="f.id" class="ffi">
208
+ <div class="ffi-icon" :class="ffiIconClass(f)">
209
+ <i class="bi" :class="fileIcon(f.type)"></i>
210
+ </div>
211
+ <div class="ffi-meta">
212
+ <span class="ffi-name">{{ f.name }}</span>
213
+ <span class="ffi-sub">{{ formatFileSize(f.size) }}</span>
214
+ </div>
215
+ <div class="ffi-acts">
216
+ <a v-if="f.publicUrl" :href="f.publicUrl" target="_blank" rel="noopener"
217
+ class="rc-btn rc-btn--view" title="Open"><i
218
+ class="bi bi-box-arrow-up-right"></i></a>
219
+ </div>
220
+ </div>
221
+
222
+ <div class="ffi-empty" v-if="getFilesForReport(report.id).length === 0">
223
+ <i class="bi bi-paperclip me-1"></i>No attachments — ZIP will contain the report PDF
224
+ only
225
+ </div>
226
+
227
+ <div class="af-zip-row">
228
+ <button class="btn btn-sm btn-outline-primary" @click="downloadReport(report)"
229
+ :disabled="generatingPdf === report.id">
230
+ <span v-if="generatingPdf === report.id">
231
+ <span class="spinner-xs me-1"></span>Building ZIP…
232
+ </span>
233
+ <span v-else>
234
+ <i class="bi bi-file-zip me-1"></i>Download all as ZIP
235
+ </span>
236
+ </button>
237
+ <span class="af-zip-hint">
238
+ {{ safeRef(report) }}.zip
239
+ · report PDF{{ getFilesForReport(report.id).length ? ' + ' +
240
+ getFilesForReport(report.id).length + ' file(s)' : '' }}
241
+ </span>
242
+ </div>
243
+
244
+ </div>
245
+ </transition>
131
246
  </div>
132
247
  </div>
133
- <div v-else class="empty-folder-message">
134
- <i class="bi bi-inbox"></i>
135
- <p>No files in this folder</p>
136
- <button class="btn btn-sm btn-primary" @click="selectFolder(getExpandedFolder())">
137
- <i class="bi bi-file-earmark-plus"></i> Add Files
138
- </button>
248
+ </div>
249
+
250
+ <!-- User folders ─ -->
251
+ <div :class="{ 'mt-4': reports.length > 0 }" v-if="folders.length > 0">
252
+ <div class="section-heading">
253
+ <i class="bi bi-folder-plus text-warning me-1"></i>
254
+ Custom Folders
255
+ <span class="section-count">{{ folders.length }}</span>
256
+ </div>
257
+
258
+ <div class="folders-list">
259
+ <div v-for="folder in folders" :key="'uf-' + folder.id"
260
+ :class="['user-folder-card', { 'is-open': expandedFolders.includes('user-' + folder.id) }]">
261
+ <div class="af-header" @click="toggleFolder('user-' + folder.id)">
262
+ <div class="af-icon-wrap">
263
+ <i class="bi bi-folder-fill af-folder-icon color--user"></i>
264
+ <span class="af-count-badge" style="background:#f0ad4e;color:#664d03;">{{
265
+ folder.files.length }}</span>
266
+ </div>
267
+ <div class="af-meta">
268
+ <span class="af-ref">{{ folder.name }}</span>
269
+ <span class="af-sub">
270
+ <span class="text-muted">{{ folder.files.length }} file{{ folder.files.length !== 1
271
+ ? 's' : '' }}</span>
272
+ </span>
273
+ </div>
274
+ <div class="af-actions">
275
+ <button class="rc-btn" style="color:#6f42c1;" @click.stop="selectFolder(folder)"
276
+ title="Add Files">
277
+ <i class="bi bi-file-earmark-plus"></i>
278
+ </button>
279
+ <button class="rc-btn rc-btn--danger"
280
+ @click.stop="$emit('folder-delete-requested', folder.id)" title="Delete">
281
+ <i class="bi bi-trash"></i>
282
+ </button>
283
+ <i class="bi chevron-icon"
284
+ :class="expandedFolders.includes('user-' + folder.id) ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
285
+ </div>
286
+ </div>
287
+
288
+ <transition name="folder-expand">
289
+ <div class="af-contents" v-if="expandedFolders.includes('user-' + folder.id)">
290
+ <div v-for="file in folder.files" :key="file.id" class="ffi">
291
+ <div class="ffi-icon" :class="ffiIconClass(file)">
292
+ <i class="bi" :class="fileIcon(file.type)"></i>
293
+ </div>
294
+ <div class="ffi-meta">
295
+ <span class="ffi-name">{{ file.name }}</span>
296
+ <span class="ffi-sub">{{ formatFileSize(file.size) }}</span>
297
+ </div>
298
+ <div class="ffi-acts">
299
+ <button class="rc-btn rc-btn--danger"
300
+ @click="$emit('file-remove-requested', { folderId: folder.id, fileId: file.id })"
301
+ title="Remove"><i class="bi bi-x-circle"></i></button>
302
+ </div>
303
+ </div>
304
+
305
+ <div class="ffi-empty" v-if="folder.files.length === 0">
306
+ <i class="bi bi-inbox me-1"></i>Empty folder
307
+ <button class="btn btn-sm btn-outline-secondary ms-2" @click="selectFolder(folder)">
308
+ <i class="bi bi-file-earmark-plus me-1"></i>Add Files
309
+ </button>
310
+ </div>
311
+ </div>
312
+ </transition>
313
+ </div>
139
314
  </div>
140
315
  </div>
316
+
317
+ <!-- All empty -->
318
+ <div class="empty-state text-center" v-if="reports.length === 0 && folders.length === 0">
319
+ <i class="bi bi-folder2-open" style="font-size:3.5rem;color:#ccc;"></i>
320
+ <h5 class="mt-3 text-muted">No folders yet</h5>
321
+ <p class="text-muted">Each submitted report auto-creates a folder here. You can also create custom
322
+ folders.</p>
323
+ <button class="btn btn-primary mt-2" @click="showNewFolderModal = true">
324
+ <i class="bi bi-folder-plus me-1"></i> New Folder
325
+ </button>
326
+ </div>
141
327
  </div>
142
328
 
143
- <!-- ── New Folder Modal ── -->
329
+ <!-- ════════════════════════════════════════
330
+ MODALS
331
+ ════════════════════════════════════════ -->
332
+
333
+ <!-- New Folder -->
144
334
  <div class="modal" :class="{ 'show': showNewFolderModal }" @click.self="showNewFolderModal = false">
145
335
  <div class="modal-dialog modal-dialog-centered">
146
336
  <div class="modal-content">
@@ -150,52 +340,56 @@
150
340
  </div>
151
341
  <div class="modal-body">
152
342
  <label class="form-label">Folder Name</label>
153
- <input type="text" class="form-control" v-model="newFolderName" @keyup.enter="createFolder"
154
- placeholder="e.g., Q1 2025 Drills" />
343
+ <input type="text" class="form-control" v-model="newFolderName"
344
+ @keyup.enter="requestCreateFolder" placeholder="e.g., Q1 2025 Drills" />
155
345
  </div>
156
346
  <div class="modal-footer">
157
347
  <button type="button" class="btn btn-secondary"
158
348
  @click="showNewFolderModal = false">Cancel</button>
159
- <button type="button" class="btn btn-primary" @click="createFolder"
160
- :disabled="!newFolderName.trim()">
161
- Create Folder
162
- </button>
349
+ <button type="button" class="btn btn-primary" @click="requestCreateFolder"
350
+ :disabled="!newFolderName.trim()">Create Folder</button>
163
351
  </div>
164
352
  </div>
165
353
  </div>
166
354
  </div>
167
355
 
168
- <!-- ── Submit Report Modal ── -->
356
+ <!-- Submit Report -->
169
357
  <div class="modal" :class="{ 'show': showReportModal }" @click.self="closeReportForm">
170
358
  <div class="modal-dialog modal-dialog-centered modal-lg">
171
359
  <div class="modal-content">
172
360
  <div class="modal-header rpt-modal-header">
173
361
  <h5 class="modal-title">
174
- <i class="bi bi-file-earmark-text me-2"></i>Submit QHSE Report
362
+ <i
363
+ :class="reportForm.report_type === 'manual' ? 'bi bi-file-earmark-text me-2' : 'bi bi-shield-check me-2'"></i>
364
+ {{ reportForm.report_type === 'manual' ? 'New Manual Report' : 'Submit QHSE Report' }}
175
365
  </h5>
366
+ <div class="modal-type-toggle" v-if="!pendingContext">
367
+ <button :class="['type-toggle-btn', { active: reportForm.report_type === 'qhse' }]"
368
+ @click="reportForm.report_type = 'qhse'">
369
+ <i class="bi bi-shield-check me-1"></i>QHSE
370
+ </button>
371
+ <button :class="['type-toggle-btn', { active: reportForm.report_type === 'manual' }]"
372
+ @click="reportForm.report_type = 'manual'">
373
+ <i class="bi bi-file-earmark-text me-1"></i>Manual
374
+ </button>
375
+ </div>
176
376
  <button type="button" class="btn-close btn-close-white" @click="closeReportForm"></button>
177
377
  </div>
178
378
  <div class="modal-body">
179
379
 
180
- <!-- Link to entity -->
181
- <div class="rpt-entity-box" v-if="reportForm.entity_ref || missingReports.length > 0">
182
- <label class="form-label fw-semibold">
183
- <i class="bi bi-link-45deg me-1"></i> Link to Drill / Incident
184
- </label>
185
-
186
- <!-- Pre-filled from navigation -->
380
+ <!-- QHSE: entity link -->
381
+ <div class="rpt-entity-box" v-if="reportForm.report_type === 'qhse'">
382
+ <label class="form-label fw-semibold"><i class="bi bi-link-45deg me-1"></i>Link to Drill /
383
+ Incident</label>
187
384
  <div class="prefilled-entity" v-if="reportForm.entity_ref && reportForm.entity_type">
188
385
  <span class="entity-chip" :class="reportForm.entity_type">
189
386
  {{ reportForm.entity_type === 'drill' ? 'DRILL' : 'INCIDENT' }}
190
387
  </span>
191
388
  <strong>{{ reportForm.entity_ref }}</strong>
192
389
  <span class="text-muted ms-2">{{ reportForm.entity_label }}</span>
193
- <button class="btn btn-link btn-sm text-danger ms-auto" @click="clearEntityLink">
194
- <i class="bi bi-x-circle"></i> Clear
195
- </button>
390
+ <button class="btn btn-link btn-sm text-danger ms-auto" @click="clearEntityLink"><i
391
+ class="bi bi-x-circle"></i> Clear</button>
196
392
  </div>
197
-
198
- <!-- Dropdown when no pre-fill -->
199
393
  <div v-else>
200
394
  <select class="form-control" v-model="reportForm.selectedMissing"
201
395
  @change="applyMissingSelection">
@@ -213,12 +407,71 @@
213
407
  </option>
214
408
  </optgroup>
215
409
  </select>
216
- <small class="text-muted">Linking a report here will mark the drill/incident as
217
- reported.</small>
410
+ <small class="text-muted">Linking will mark the drill / incident as reported.</small>
411
+ </div>
412
+ </div>
413
+
414
+ <!-- Manual: category + ref -->
415
+ <div class="input-row" v-if="reportForm.report_type === 'manual'">
416
+ <div class="form-group">
417
+ <label class="form-label fw-semibold">Report Category</label>
418
+ <select class="form-control" v-model="reportForm.manual_category">
419
+ <option>General</option>
420
+ <option>Maintenance</option>
421
+ <option>Safety Observation</option>
422
+ <option>Environmental</option>
423
+ <option>Audit</option>
424
+ <option>Training</option>
425
+ <option>Other</option>
426
+ </select>
427
+ </div>
428
+ <div class="form-group">
429
+ <label class="form-label fw-semibold">Reference Number</label>
430
+ <input type="text" class="form-control" v-model="reportForm.manual_ref"
431
+ placeholder="e.g. RPT-2025-001" />
432
+ </div>
433
+ </div>
434
+
435
+ <!-- Drill-specific hint -->
436
+ <div class="crew-hint" v-if="reportForm.entity_type === 'drill'">
437
+ <i class="bi bi-info-circle-fill me-1"></i>
438
+ <span>
439
+ <strong>Crew list tip:</strong> Start your Findings with
440
+ <code>CREW: Name (Rank), Name (Rank), …</code>
441
+ to generate a named crew participation table in the PDF.
442
+ Otherwise the PDF will show the total count ({{ reportForm.entity_participants || 0 }}
443
+ members) with a note to attach the signed list.
444
+ </span>
445
+ </div>
446
+
447
+ <!-- Drill checklist — only shown when linked to a drill -->
448
+ <div class="drill-checklist-box" v-if="reportForm.entity_type === 'drill'">
449
+ <div class="drill-checklist-title">
450
+ <i class="bi bi-clipboard2-check me-1"></i>
451
+ Comments Checklist
452
+ <span class="checklist-hint">Tick = YES · unticked = NO in the PDF</span>
453
+ </div>
454
+ <div class="checklist-grid">
455
+ <label v-for="(q, idx) in drillChecklistQuestions" :key="idx"
456
+ :class="['checklist-item', { checked: reportForm.drillChecklist[idx] }]">
457
+ <div class="checklist-checkbox">
458
+ <input type="checkbox" :checked="reportForm.drillChecklist[idx]"
459
+ @change="reportForm.drillChecklist[idx] = $event.target.checked" />
460
+ <span class="custom-check">
461
+ <i class="bi"
462
+ :class="reportForm.drillChecklist[idx] ? 'bi-check-lg' : 'bi-x-lg'"></i>
463
+ </span>
464
+ </div>
465
+ <span class="checklist-q">{{ q }}</span>
466
+ <span class="checklist-answer"
467
+ :class="reportForm.drillChecklist[idx] ? 'answer-yes' : 'answer-no'">
468
+ {{ reportForm.drillChecklist[idx] ? 'YES' : 'NO' }}
469
+ </span>
470
+ </label>
218
471
  </div>
219
472
  </div>
220
473
 
221
- <!-- Report Fields -->
474
+ <!-- Common fields -->
222
475
  <div class="input-row mt-3">
223
476
  <div class="form-group">
224
477
  <label class="form-label fw-semibold">Report Title *</label>
@@ -231,42 +484,104 @@
231
484
  placeholder="Your name" />
232
485
  </div>
233
486
  </div>
234
-
235
487
  <div class="input-row">
236
- <div class="form-group">
237
- <label class="form-label fw-semibold">Save to Folder</label>
238
- <select class="form-control" v-model="reportForm.folderId">
239
- <option value="">— No folder —</option>
240
- <option v-for="f in folders" :key="f.id" :value="f.id">{{ f.name }}</option>
241
- </select>
242
- </div>
243
488
  <div class="form-group">
244
489
  <label class="form-label fw-semibold">Date</label>
245
490
  <input type="date" class="form-control" v-model="reportForm.date" />
246
491
  </div>
247
492
  </div>
248
-
249
493
  <div class="form-group">
250
- <label class="form-label fw-semibold">Findings / Summary *</label>
251
- <textarea class="form-control" rows="4" v-model="reportForm.findings"
252
- placeholder="Describe findings, observations, and outcomes..."></textarea>
494
+ <label class="form-label fw-semibold">
495
+ {{ reportForm.entity_type === 'incident' ? 'Description of Event (Findings) *' :
496
+ 'Findings / Summary *' }}
497
+ <span class="form-hint" v-if="reportForm.entity_type === 'incident'">Pre-filled from
498
+ incident objective — edit as needed</span>
499
+ </label>
500
+ <textarea class="form-control" rows="4" v-model="reportForm.findings" :placeholder="reportForm.entity_type === 'drill'
501
+ ? 'CREW: John Smith (Master), Jane Doe (Chief Officer), …\n\nThen describe the drill scenario and findings…'
502
+ : reportForm.entity_type === 'incident'
503
+ ? 'Describe what happened, when, and where…'
504
+ : 'Describe findings, observations, and outcomes…'">
505
+ </textarea>
253
506
  </div>
254
507
 
255
- <div class="form-group">
256
- <label class="form-label fw-semibold">Corrective Actions</label>
257
- <textarea class="form-control" rows="3" v-model="reportForm.correctiveActions"
258
- placeholder="List any corrective actions required or taken..."></textarea>
259
- </div>
508
+ <!-- Incident-specific structured fields -->
509
+ <template v-if="reportForm.entity_type === 'incident'">
510
+ <div class="incident-fields-box">
511
+ <div class="incident-fields-title">
512
+ <i class="bi bi-layout-text-window-reverse me-1"></i>
513
+ Incident Report Fields
514
+ <span class="checklist-hint">These map directly to the PDF sections</span>
515
+ </div>
516
+ <div class="form-group">
517
+ <label class="form-label fw-semibold">Possible Consequences</label>
518
+ <div class="field-hint">e.g. Personal injury, damage, collision, grounding, fire,
519
+ pollution</div>
520
+ <textarea class="form-control" rows="2" v-model="reportForm.inc_consequences"
521
+ placeholder="Describe possible consequences…"></textarea>
522
+ </div>
523
+ <div class="form-group">
524
+ <label class="form-label fw-semibold">Relevant Factors / Conditions Surrounding the
525
+ Event</label>
526
+ <div class="field-hint">e.g. weather, lighting, fatigue, time pressure</div>
527
+ <textarea class="form-control" rows="2" v-model="reportForm.inc_factors"
528
+ placeholder="Describe relevant factors and conditions…"></textarea>
529
+ </div>
530
+ <div class="form-group">
531
+ <label class="form-label fw-semibold">Immediate Action Taken</label>
532
+ <textarea class="form-control" rows="2" v-model="reportForm.inc_immediate_action"
533
+ placeholder="What was done immediately after the event?"></textarea>
534
+ </div>
535
+ <div class="input-row" style="margin-bottom:0;">
536
+ <div class="form-group">
537
+ <label class="form-label fw-semibold">Direct Cause</label>
538
+ <div class="field-hint">e.g. failure to follow procedures, defective equipment
539
+ </div>
540
+ <textarea class="form-control" rows="3" v-model="reportForm.inc_direct_cause"
541
+ placeholder="Direct cause of the event…"></textarea>
542
+ </div>
543
+ <div class="form-group">
544
+ <label class="form-label fw-semibold">Root Cause</label>
545
+ <div class="field-hint">e.g. lack of training, management factors</div>
546
+ <textarea class="form-control" rows="3" v-model="reportForm.inc_root_cause"
547
+ placeholder="Root / underlying cause…"></textarea>
548
+ </div>
549
+ </div>
550
+ <div class="form-group">
551
+ <label class="form-label fw-semibold">Action Taken to Avoid Re-occurrence</label>
552
+ <textarea class="form-control" rows="2" v-model="reportForm.correctiveActions"
553
+ placeholder="Corrective actions and preventive measures…"></textarea>
554
+ </div>
555
+ <div class="form-group" style="margin-bottom:0;">
556
+ <label class="form-label fw-semibold">Any Other Remarks</label>
557
+ <textarea class="form-control" rows="2" v-model="reportForm.inc_remarks"
558
+ placeholder="Additional remarks…"></textarea>
559
+ </div>
560
+ </div>
561
+ </template>
260
562
 
563
+ <!-- Non-incident: standard corrective actions -->
564
+ <template v-else>
565
+ <div class="form-group">
566
+ <label class="form-label fw-semibold">Corrective Actions</label>
567
+ <textarea class="form-control" rows="3" v-model="reportForm.correctiveActions"
568
+ placeholder="List any corrective actions required or taken…"></textarea>
569
+ </div>
570
+ </template>
571
+
572
+ <!-- Attachments -->
261
573
  <div class="form-group">
262
- <label class="form-label fw-semibold">Attach Files</label>
574
+ <label class="form-label fw-semibold">
575
+ Attach Files
576
+ <span class="form-hint">Images embed in PDF · all files go into the ZIP</span>
577
+ </label>
263
578
  <div class="file-drop-zone" @click="$refs.reportFileInput.click()" @dragover.prevent
264
579
  @drop.prevent="handleReportFileDrop">
265
580
  <i class="bi bi-cloud-upload" style="font-size:2rem;color:#6c757d;"></i>
266
- <p class="mb-0 mt-2 text-muted">Click or drag files here</p>
581
+ <p class="mb-0 mt-2 text-muted">Click or drag files here (PNG, JPG, PDF…)</p>
267
582
  <div class="attached-files" v-if="reportForm.files.length > 0">
268
583
  <span class="attached-chip" v-for="(f, i) in reportForm.files" :key="i">
269
- <i class="bi bi-paperclip"></i> {{ f.name }}
584
+ <i class="bi me-1" :class="fileIcon(f.type)"></i>{{ f.name }}
270
585
  <button @click.stop="removeReportFile(i)"><i class="bi bi-x"></i></button>
271
586
  </span>
272
587
  </div>
@@ -275,43 +590,68 @@
275
590
  @change="handleReportFileAttach" />
276
591
  </div>
277
592
 
593
+ <!-- ZIP preview -->
594
+ <div class="zip-hint" v-if="reportForm.title || reportForm.files.length > 0">
595
+ <i class="bi bi-file-zip me-1"></i>
596
+ Download will produce <strong>{{ zipName }}.zip</strong>
597
+ containing the report PDF
598
+ <span v-if="reportForm.files.length"> + {{ reportForm.files.length }} attachment{{
599
+ reportForm.files.length !== 1 ?
600
+ 's' : '' }}</span>
601
+ </div>
278
602
  </div>
279
603
  <div class="modal-footer">
280
604
  <button type="button" class="btn btn-secondary" @click="closeReportForm">Cancel</button>
281
- <button type="button" class="btn btn-success" @click="submitReport"
282
- :disabled="!reportForm.title || !reportForm.submittedBy || !reportForm.findings">
283
- <i class="bi bi-check-circle me-1"></i> Submit Report
605
+ <button type="button" class="btn btn-success" @click="requestSubmitReport"
606
+ :disabled="isSubmitting || !reportForm.title || !reportForm.submittedBy || !reportForm.findings">
607
+ <span v-if="isSubmitting"><span class="spinner-sm"></span> Submitting…</span>
608
+ <span v-else><i class="bi bi-check-circle me-1"></i>Submit Report</span>
284
609
  </button>
285
610
  </div>
286
611
  </div>
287
612
  </div>
288
613
  </div>
289
614
 
290
- <!-- ── Report Viewer Modal ── -->
291
- <div class="modal" :class="{ 'show': showViewModal }" @click.self="showViewModal = false">
615
+ <!-- Report Viewer -->
616
+ <div class="modal" :class="{ 'show': showViewModal }" @click.self="closeViewer">
292
617
  <div class="modal-dialog modal-dialog-centered modal-lg">
293
618
  <div class="modal-content" v-if="viewingReport">
294
619
  <div class="modal-header rpt-modal-header">
295
- <div>
296
- <span class="entity-chip me-2" :class="viewingReport.entity_type">
297
- {{ viewingReport.entity_type === 'drill' ? 'DRILL' : 'INCIDENT' }}
298
- </span>
620
+ <div class="d-flex align-items-center gap-2">
621
+ <span class="entity-chip me-2" :class="reportTypeClass(viewingReport)">{{
622
+ reportTypeLabel(viewingReport) }}</span>
299
623
  <strong style="color:#fff">{{ viewingReport.title }}</strong>
300
624
  </div>
301
- <button type="button" class="btn-close btn-close-white" @click="showViewModal = false"></button>
625
+ <button type="button" class="btn-close btn-close-white" @click="closeViewer"></button>
302
626
  </div>
303
627
  <div class="modal-body">
304
628
  <div class="view-grid">
305
629
  <div class="view-item"><label>Reference</label><span class="ref-mono">{{
306
- viewingReport.entity_ref }}</span></div>
307
- <div class="view-item"><label>Type</label><span>{{ viewingReport.entity_type }}</span></div>
630
+ viewingReport.entityRef ||
631
+ '—' }}</span></div>
632
+ <div class="view-item"><label>Type</label><span>{{ reportTypeLabel(viewingReport) }}</span>
633
+ </div>
634
+ <div class="view-item" v-if="viewingReport.manualCategory"><label>Category</label><span>{{
635
+ viewingReport.manualCategory }}</span></div>
308
636
  <div class="view-item"><label>Submitted By</label><span>{{ viewingReport.submittedBy
309
637
  }}</span></div>
310
638
  <div class="view-item"><label>Date</label><span>{{ viewingReport.date }}</span></div>
311
- <div class="view-item"><label>Folder</label><span>{{ viewingReport.folderName || '—'
312
- }}</span></div>
639
+ <div class="view-item" v-if="viewingReport.vessel"><label>Vessel / Location</label><span>{{
640
+ viewingReport.vessel }}</span></div>
313
641
  <div class="view-item"><label>Submitted At</label><span>{{
314
- formatDate(viewingReport.submittedAt) }}</span></div>
642
+ formatDate(viewingReport.submittedAt)
643
+ }}</span></div>
644
+ <!-- Drill-specific meta -->
645
+ <div class="view-item" v-if="viewingReport.entity_subtype"><label>Drill Type</label><span>{{
646
+ viewingReport.entity_subtype }}</span></div>
647
+ <div class="view-item" v-if="viewingReport.entity_creator"><label>Drill
648
+ Master</label><span>{{
649
+ viewingReport.entity_creator }}</span></div>
650
+ <div class="view-item" v-if="viewingReport.entity_duration"><label>Duration</label><span>{{
651
+ viewingReport.entity_duration }}</span></div>
652
+ <div class="view-item" v-if="viewingReport.entity_participants">
653
+ <label>Participants</label><span>{{
654
+ viewingReport.entity_participants }}</span></div>
315
655
  </div>
316
656
  <div class="view-section">
317
657
  <label>Findings / Summary</label>
@@ -321,24 +661,36 @@
321
661
  <label>Corrective Actions</label>
322
662
  <div class="view-text">{{ viewingReport.correctiveActions }}</div>
323
663
  </div>
324
- <div class="view-section" v-if="viewingReport.files && viewingReport.files.length > 0">
664
+ <div class="view-section" v-if="getFilesForReport(viewingReport.id).length > 0">
325
665
  <label>Attachments</label>
326
666
  <div class="attached-files mt-1">
327
- <span class="attached-chip" v-for="(f, i) in viewingReport.files" :key="i">
328
- <i class="bi bi-paperclip"></i> {{ f.name }}
329
- </span>
667
+ <a v-for="f in getFilesForReport(viewingReport.id)" :key="f.id" :href="f.publicUrl"
668
+ target="_blank" rel="noopener" class="attached-chip attached-chip--link">
669
+ <i class="bi me-1" :class="fileIcon(f.type)"></i>{{ f.name }}
670
+ <small class="text-muted ms-1">({{ formatFileSize(f.size) }})</small>
671
+ <i class="bi bi-box-arrow-up-right ms-1" style="font-size:10px;"></i>
672
+ </a>
330
673
  </div>
331
674
  </div>
332
675
  </div>
333
676
  <div class="modal-footer">
334
- <button class="btn btn-secondary" @click="showViewModal = false">Close</button>
677
+ <button class="btn btn-danger btn-sm me-auto" @click="requestDeleteReport(viewingReport.id)">
678
+ <i class="bi bi-trash"></i> Delete
679
+ </button>
680
+ <button class="btn btn-secondary" @click="closeViewer">Close</button>
681
+ <button class="btn btn-primary" @click="downloadReport(viewingReport)"
682
+ :disabled="generatingPdf === viewingReport.id">
683
+ <span v-if="generatingPdf === viewingReport.id"><span class="spinner-sm"></span> Building
684
+ ZIP…</span>
685
+ <span v-else><i class="bi bi-file-zip me-1"></i>Download ZIP</span>
686
+ </button>
335
687
  </div>
336
688
  </div>
337
689
  </div>
338
690
  </div>
339
691
 
340
- <!-- Hidden file input for folders -->
341
- <input ref="fileInput" type="file" multiple style="display: none;" @change="handleFileSelection" />
692
+ <!-- Hidden file input for user folders -->
693
+ <input ref="fileInput" type="file" multiple style="display:none;" @change="handleFileSelection" />
342
694
  </div>
343
695
  </template>
344
696
 
@@ -347,61 +699,76 @@ export default {
347
699
  name: 'Reports',
348
700
 
349
701
  props: {
350
- // Passed from ReportsView when navigating from HSE
351
- pendingContext: {
352
- type: Object,
353
- default: null
354
- // { entity_type, entity_id, entity_ref, title }
355
- },
356
- // All missing reports (drills + incidents without reports)
357
- missingReports: {
358
- type: Array,
359
- default: () => []
360
- // [{ entity_type, entity_id, entity_ref, label }]
361
- },
362
- // Ref to scroll to / highlight (from ?ref= query param)
363
- highlightRef: {
364
- type: String,
365
- default: null
366
- }
702
+ reports: { type: Array, default: () => [] },
703
+ reportFiles: { type: Array, default: () => [] },
704
+ folders: { type: Array, default: () => [] },
705
+ isLoading: { type: Boolean, default: false },
706
+ isSubmitting: { type: Boolean, default: false },
707
+ pendingContext: { type: Object, default: null },
708
+ missingReports: { type: Array, default: () => [] },
709
+ highlightRef: { type: String, default: null },
367
710
  },
368
711
 
369
712
  emits: [
370
- 'folder-created', 'files-added', 'file-removed', 'folder-deleted',
371
- 'data-updated', 'report-submitted', 'report-viewed'
713
+ 'report-submit-requested',
714
+ 'report-delete-requested',
715
+ 'folder-create-requested',
716
+ 'files-add-requested',
717
+ 'file-remove-requested',
718
+ 'folder-delete-requested',
719
+ 'report-viewed',
372
720
  ],
373
721
 
374
722
  data() {
375
723
  return {
376
- folders: [],
377
- linkedReports: [], // submitted QHSE reports
724
+ activeTab: 'all',
378
725
  showNewFolderModal: false,
379
726
  newFolderName: '',
380
727
  selectedFolder: null,
381
728
  expandedFolders: [],
382
- previewFolder: null,
729
+
383
730
  showReportModal: false,
384
731
  showViewModal: false,
385
732
  viewingReport: null,
386
733
  highlightedRef: null,
387
734
  bannerDismissed: false,
388
- reportForm: {
389
- title: '',
390
- submittedBy: '',
391
- folderId: '',
392
- date: new Date().toISOString().split('T')[0],
393
- findings: '',
394
- correctiveActions: '',
395
- files: [],
396
- entity_type: '',
397
- entity_id: '',
398
- entity_ref: '',
399
- entity_label: '',
400
- selectedMissing: '',
401
- }
735
+ generatingPdf: null,
736
+
737
+ drillChecklistQuestions: [
738
+ 'Is the reaction of crew members satisfactory?',
739
+ 'Is the equipment functioning properly?',
740
+ 'Is the crew properly trained and ready?',
741
+ 'Was debriefing conducted on completion of drill?',
742
+ 'Is time log of various activities marked on checklist?',
743
+ ],
744
+
745
+ reportForm: this.emptyForm(),
402
746
  };
403
747
  },
404
748
 
749
+ computed: {
750
+ drillReports() { return this.reports.filter(r => r.entityType === 'drill'); },
751
+ incidentReports() { return this.reports.filter(r => r.entityType === 'incident'); },
752
+ manualReports() {
753
+ return this.reports.filter(r =>
754
+ r.entityType === 'manual' ||
755
+ (!r.entityType && !r.entityRef?.startsWith('DRILL') && !r.entityRef?.startsWith('INC'))
756
+ );
757
+ },
758
+ filteredReports() {
759
+ switch (this.activeTab) {
760
+ case 'drill': return this.drillReports;
761
+ case 'incident': return this.incidentReports;
762
+ case 'manual': return this.manualReports;
763
+ default: return this.reports;
764
+ }
765
+ },
766
+ zipName() {
767
+ const ref = this.reportForm.entity_ref || this.reportForm.manual_ref || this.reportForm.title;
768
+ return (ref || 'report').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 50);
769
+ },
770
+ },
771
+
405
772
  watch: {
406
773
  pendingContext: {
407
774
  immediate: true,
@@ -412,8 +779,18 @@ export default {
412
779
  this.reportForm.entity_ref = ctx.entity_ref;
413
780
  this.reportForm.entity_label = ctx.title;
414
781
  this.reportForm.title = ctx.title || `Report — ${ctx.entity_ref}`;
782
+ this.reportForm.report_type = 'qhse';
783
+ this.reportForm.entity_creator = ctx.entity_creator;
784
+ this.reportForm.entity_objectives = ctx.entity_objectives;
785
+ this.reportForm.entity_subtype = ctx.entity_subtype;
786
+ this.reportForm.entity_duration = ctx.entity_duration;
787
+ this.reportForm.entity_participants = ctx.entity_participants;
788
+ // Pre-fill findings with the entity objective for incidents
789
+ if (ctx.entity_type === 'incident' && ctx.entity_objectives) {
790
+ this.reportForm.findings = ctx.entity_objectives;
791
+ }
415
792
  }
416
- }
793
+ },
417
794
  },
418
795
  highlightRef: {
419
796
  immediate: true,
@@ -422,35 +799,114 @@ export default {
422
799
  this.highlightedRef = ref;
423
800
  this.$nextTick(() => this.scrollToReport(ref));
424
801
  }
802
+ },
803
+ },
804
+ isSubmitting(val, prev) {
805
+ if (prev === true && val === false) {
806
+ this.showReportModal = false;
807
+ this.resetForm();
425
808
  }
426
- }
809
+ },
427
810
  },
428
811
 
429
812
  methods: {
430
- // ── Banner ──────────────────────────────────────────────
813
+
814
+ // ─── Helpers ────────────────────────────────────────────────────────────
815
+
816
+ emptyForm() {
817
+ return {
818
+ report_type: 'qhse', manual_category: 'General', manual_ref: '',
819
+ title: '', submittedBy: '', vessel: '', folderId: '',
820
+ date: new Date().toISOString().split('T')[0],
821
+ findings: '', correctiveActions: '', files: [],
822
+ entity_type: '', entity_id: '', entity_ref: '', entity_label: '',
823
+ entity_creator: '', entity_objectives: '', entity_subtype: '',
824
+ entity_duration: '', entity_participants: null,
825
+ selectedMissing: '',
826
+ drillChecklist: [true, true, true, true, true],
827
+ // incident-specific structured fields
828
+ inc_consequences: '',
829
+ inc_factors: '',
830
+ inc_immediate_action: '',
831
+ inc_direct_cause: '',
832
+ inc_root_cause: '',
833
+ inc_remarks: '',
834
+ };
835
+ },
836
+
837
+ getFilesForReport(reportId) {
838
+ return this.reportFiles.filter(f => f.report_id === reportId || f.reportId === reportId);
839
+ },
840
+
841
+ safeRef(report) {
842
+ return (report.entityRef || report.title || 'report').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 60);
843
+ },
844
+
845
+ reportTypeClass(report) {
846
+ const t = report?.entityType || report?.entity_type;
847
+ if (t === 'drill') return 'drill';
848
+ if (t === 'incident') return 'incident';
849
+ return 'manual';
850
+ },
851
+
852
+ reportTypeLabel(report) {
853
+ const t = report?.entityType || report?.entity_type;
854
+ if (t === 'drill') return 'DRILL';
855
+ if (t === 'incident') return 'INCIDENT';
856
+ const cat = report?.manualCategory || report?.manual_category;
857
+ return cat ? cat.toUpperCase() : 'MANUAL';
858
+ },
859
+
860
+ fileIcon(type) {
861
+ if (!type) return 'bi-file-earmark';
862
+ if (type.startsWith('image/')) return 'bi-file-earmark-image';
863
+ if (type === 'application/pdf') return 'bi-file-earmark-pdf-fill';
864
+ if (type.includes('word')) return 'bi-file-earmark-word';
865
+ if (type.includes('sheet') || type.includes('excel')) return 'bi-file-earmark-excel';
866
+ return 'bi-file-earmark';
867
+ },
868
+
869
+ ffiIconClass(file) {
870
+ const t = file?.type || '';
871
+ if (t.startsWith('image/')) return 'ffi-icon--img';
872
+ if (t === 'application/pdf') return 'ffi-icon--pdf';
873
+ return 'ffi-icon--generic';
874
+ },
875
+
876
+ // ─── Banner ─────────────────────────────────────────────────────────────
431
877
  dismissBanner() { this.bannerDismissed = true; },
432
878
 
433
- openReportForm() {
434
- if (this.pendingContext && !this.reportForm.entity_ref) {
879
+ // ─── Report form ─────────────────────────────────────────────────────────
880
+
881
+ openReportForm(type = 'qhse') {
882
+ this.reportForm = this.emptyForm();
883
+ this.reportForm.report_type = type;
884
+ if (type === 'qhse' && this.pendingContext) {
885
+ this.reportForm.entity_creator = this.pendingContext.entity_creator;
886
+ this.reportForm.entity_objectives = this.pendingContext.entity_objectives;
887
+ this.reportForm.entity_subtype = this.pendingContext.entity_subtype;
888
+ this.reportForm.entity_duration = this.pendingContext.entity_duration;
889
+ this.reportForm.entity_participants = this.pendingContext.entity_participants;
435
890
  this.reportForm.entity_type = this.pendingContext.entity_type;
436
891
  this.reportForm.entity_id = this.pendingContext.entity_id;
437
892
  this.reportForm.entity_ref = this.pendingContext.entity_ref;
438
893
  this.reportForm.entity_label = this.pendingContext.title;
439
894
  this.reportForm.title = this.pendingContext.title || `Report — ${this.pendingContext.entity_ref}`;
895
+ if (this.pendingContext.entity_type === 'incident' && this.pendingContext.entity_objectives) {
896
+ this.reportForm.findings = this.pendingContext.entity_objectives;
897
+ }
440
898
  }
441
899
  this.showReportModal = true;
442
900
  },
443
901
 
444
- closeReportForm() {
445
- this.showReportModal = false;
446
- },
902
+ closeReportForm() { this.showReportModal = false; },
447
903
 
448
904
  clearEntityLink() {
449
- this.reportForm.entity_type = '';
450
- this.reportForm.entity_id = '';
451
- this.reportForm.entity_ref = '';
452
- this.reportForm.entity_label = '';
453
- this.reportForm.selectedMissing = '';
905
+ Object.assign(this.reportForm, {
906
+ entity_type: '', entity_id: '', entity_ref: '', entity_label: '',
907
+ entity_creator: '', entity_objectives: '', entity_subtype: '',
908
+ entity_duration: '', entity_participants: null, selectedMissing: '',
909
+ });
454
910
  },
455
911
 
456
912
  applyMissingSelection() {
@@ -460,163 +916,817 @@ export default {
460
916
  this.reportForm.entity_id = r.entity_id;
461
917
  this.reportForm.entity_ref = r.entity_ref;
462
918
  this.reportForm.entity_label = r.label;
463
- this.reportForm.title = this.reportForm.title || `Report — ${r.entity_ref}`;
919
+ this.reportForm.entity_creator = r.entity_creator || '';
920
+ this.reportForm.entity_objectives = r.entity_objectives || '';
921
+ this.reportForm.entity_subtype = r.entity_subtype || '';
922
+ this.reportForm.entity_duration = r.entity_duration || '';
923
+ this.reportForm.entity_participants = r.entity_participants || null;
924
+ if (!this.reportForm.title) this.reportForm.title = `Report — ${r.entity_ref}`;
925
+ if (r.entity_type === 'incident' && r.entity_objectives && !this.reportForm.findings) {
926
+ this.reportForm.findings = r.entity_objectives;
927
+ }
464
928
  },
465
929
 
466
- // ── Submit Report ────────────────────────────────────────
467
- submitReport() {
468
- const folder = this.folders.find(f => f.id === this.reportForm.folderId);
469
- const report = {
470
- id: Date.now(),
930
+ requestSubmitReport() {
931
+ if (!this.reportForm.title || !this.reportForm.submittedBy || !this.reportForm.findings) return;
932
+ this.$emit('report-submit-requested', {
933
+ entity_type: this.reportForm.report_type === 'manual' ? 'manual' : this.reportForm.entity_type,
934
+ entity_id: this.reportForm.entity_id || null,
935
+ entity_ref: this.reportForm.report_type === 'manual'
936
+ ? (this.reportForm.manual_ref || null)
937
+ : this.reportForm.entity_ref,
471
938
  title: this.reportForm.title,
472
939
  submittedBy: this.reportForm.submittedBy,
940
+ vessel: this.reportForm.vessel || null,
473
941
  date: this.reportForm.date,
474
942
  findings: this.reportForm.findings,
475
- correctiveActions: this.reportForm.correctiveActions,
476
- files: [...this.reportForm.files],
477
- entity_type: this.reportForm.entity_type,
478
- entity_id: this.reportForm.entity_id,
479
- entity_ref: this.reportForm.entity_ref,
480
- folderId: this.reportForm.folderId || null,
481
- folderName: folder?.name || null,
482
- submittedAt: new Date(),
483
- };
484
-
485
- this.linkedReports.push(report);
486
- this.$emit('report-submitted', report);
487
- this.$emit('data-updated', {
488
- folders: this.folders,
489
- allFiles: this.getAllFilesData(),
490
- linkedReports: this.linkedReports,
943
+ correctiveActions: this.reportForm.correctiveActions || null,
944
+ folderId: null,
945
+ folderName: null,
946
+ manual_category: this.reportForm.report_type === 'manual' ? this.reportForm.manual_category : null,
947
+ files: this.reportForm.files,
948
+ // drill / incident enrichment fields
949
+ entity_subtype: this.reportForm.entity_subtype || null,
950
+ entity_creator: this.reportForm.entity_creator || null,
951
+ entity_objectives: this.reportForm.entity_objectives || null,
952
+ entity_duration: this.reportForm.entity_duration || null,
953
+ entity_participants: this.reportForm.entity_participants || null,
954
+ drill_checklist: [...this.reportForm.drillChecklist],
955
+ // incident structured fields
956
+ inc_consequences: this.reportForm.inc_consequences || null,
957
+ inc_factors: this.reportForm.inc_factors || null,
958
+ inc_immediate_action: this.reportForm.inc_immediate_action || null,
959
+ inc_direct_cause: this.reportForm.inc_direct_cause || null,
960
+ inc_root_cause: this.reportForm.inc_root_cause || null,
961
+ inc_remarks: this.reportForm.inc_remarks || null,
491
962
  });
963
+ },
492
964
 
493
- // Reset form
494
- this.reportForm = {
495
- title: '', submittedBy: '', folderId: '',
496
- date: new Date().toISOString().split('T')[0],
497
- findings: '', correctiveActions: '', files: [],
498
- entity_type: '', entity_id: '', entity_ref: '',
499
- entity_label: '', selectedMissing: '',
500
- };
501
- this.showReportModal = false;
965
+ resetForm() { this.reportForm = this.emptyForm(); },
502
966
 
503
- // Highlight the new report
504
- this.$nextTick(() => {
505
- this.highlightedRef = report.entity_ref;
506
- this.scrollToReport(report.entity_ref);
507
- setTimeout(() => { this.highlightedRef = null; }, 3000);
508
- });
967
+ highlightReport(entityRef) {
968
+ this.highlightedRef = entityRef;
969
+ this.$nextTick(() => this.scrollToReport(entityRef));
970
+ setTimeout(() => { this.highlightedRef = null; }, 3000);
509
971
  },
510
972
 
511
- // ── View Report ──────────────────────────────────────────
973
+ // ─── View ────────────────────────────────────────────────────────────────
974
+
512
975
  viewReport(report) {
513
976
  this.viewingReport = report;
514
977
  this.showViewModal = true;
515
978
  this.$emit('report-viewed', report);
516
979
  },
517
980
 
981
+ closeViewer() {
982
+ this.showViewModal = false;
983
+ this.viewingReport = null;
984
+ },
985
+
986
+ // Closes the modal immediately, then emits so ReportsView can confirm + delete
987
+ requestDeleteReport(reportId) {
988
+ this.closeViewer();
989
+ this.$emit('report-delete-requested', reportId);
990
+ },
991
+
518
992
  scrollToReport(ref) {
519
993
  this.$nextTick(() => {
520
994
  const el = this.$refs['report-' + ref];
521
- if (el) {
522
- const target = Array.isArray(el) ? el[0] : el;
523
- target?.scrollIntoView({ behavior: 'smooth', block: 'center' });
524
- }
995
+ if (el) (Array.isArray(el) ? el[0] : el)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
525
996
  });
526
997
  },
527
998
 
528
- // ── File Helpers ─────────────────────────────────────────
529
- handleReportFileAttach(e) {
530
- this.reportForm.files.push(...Array.from(e.target.files));
531
- e.target.value = '';
999
+ jumpToFolder(report) {
1000
+ this.activeTab = 'folders';
1001
+ const key = 'auto-' + report.id;
1002
+ if (!this.expandedFolders.includes(key)) this.expandedFolders.push(key);
1003
+ this.$nextTick(() => {
1004
+ const ref = report.entityRef || report.id;
1005
+ const el = this.$refs['folder-' + ref];
1006
+ if (el) (Array.isArray(el) ? el[0] : el)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
1007
+ });
532
1008
  },
533
- handleReportFileDrop(e) {
534
- this.reportForm.files.push(...Array.from(e.dataTransfer.files));
1009
+
1010
+ // ─── Download ────────────────────────────────────────────────────────────
1011
+
1012
+ async downloadReport(report) {
1013
+ this.generatingPdf = report.id;
1014
+ try {
1015
+ await this._loadLibraries();
1016
+ const attachments = this.getFilesForReport(report.id);
1017
+ const imageFiles = attachments.filter(f => f.type?.startsWith('image/'));
1018
+ const reportPdfBytes = await this._buildReportPdf(report, imageFiles);
1019
+ await this._buildAndDownloadZip(report, reportPdfBytes, attachments);
1020
+ } catch (err) {
1021
+ console.error('ZIP generation failed:', err);
1022
+ alert('Failed to generate download: ' + err.message);
1023
+ } finally {
1024
+ this.generatingPdf = null;
1025
+ }
535
1026
  },
536
- removeReportFile(i) { this.reportForm.files.splice(i, 1); },
537
1027
 
538
- // ── Folder Methods ───────────────────────────────────────
539
- createFolder() {
540
- if (!this.newFolderName.trim()) return;
541
- const newFolder = { id: Date.now(), name: this.newFolderName.trim(), files: [], createdAt: new Date() };
542
- this.folders.push(newFolder);
543
- this.$emit('folder-created', newFolder);
544
- this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData(), linkedReports: this.linkedReports });
545
- this.newFolderName = '';
546
- this.showNewFolderModal = false;
1028
+ async downloadReportPdfOnly(report) {
1029
+ this.generatingPdf = report.id + '-pdf';
1030
+ try {
1031
+ await this._loadLibraries();
1032
+ const imageFiles = this.getFilesForReport(report.id).filter(f => f.type?.startsWith('image/'));
1033
+ const reportPdfBytes = await this._buildReportPdf(report, imageFiles);
1034
+ const blob = new Blob([reportPdfBytes], { type: 'application/pdf' });
1035
+ this._triggerDownload(blob, `${this.safeRef(report)}_report.pdf`);
1036
+ } catch (err) {
1037
+ alert('Failed to generate PDF: ' + err.message);
1038
+ } finally {
1039
+ this.generatingPdf = null;
1040
+ }
547
1041
  },
548
1042
 
549
- selectFolder(folder) { this.selectedFolder = folder; this.$refs.fileInput.click(); },
1043
+ async _loadLibraries() {
1044
+ if (!window.jspdf) await this._loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js');
1045
+ if (!window.JSZip) await this._loadScript('https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js');
1046
+ },
550
1047
 
551
- handleFileSelection(event) {
552
- const files = Array.from(event.target.files);
553
- if (!this.selectedFolder || files.length === 0) return;
554
- const newFiles = files.map(file => ({ id: Date.now() + Math.random(), name: file.name, size: file.size, type: file.type, file, addedAt: new Date() }));
555
- this.selectedFolder.files.push(...newFiles);
556
- this.$emit('files-added', { folderId: this.selectedFolder.id, files: newFiles });
557
- this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData(), linkedReports: this.linkedReports });
558
- event.target.value = '';
559
- this.selectedFolder = null;
1048
+ _loadScript(src) {
1049
+ return new Promise((resolve, reject) => {
1050
+ if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; }
1051
+ const s = document.createElement('script');
1052
+ s.src = src; s.onload = resolve; s.onerror = reject;
1053
+ document.head.appendChild(s);
1054
+ });
1055
+ },
1056
+
1057
+ // ─── PDF router ──────────────────────────────────────────────────────────
1058
+
1059
+ async _buildReportPdf(report, imageFiles = []) {
1060
+ const type = report.entityType || report.entity_type;
1061
+ if (type === 'drill') return this._buildDrillPdf(report, imageFiles);
1062
+ if (type === 'incident') return this._buildIncidentPdf(report, imageFiles);
1063
+ return this._buildManualPdf(report, imageFiles);
560
1064
  },
561
1065
 
562
- removeFile(folderId, fileId) {
563
- const folder = this.folders.find(f => f.id === folderId);
564
- if (folder) {
565
- const i = folder.files.findIndex(f => f.id === fileId);
566
- if (i > -1) {
567
- const removedFile = folder.files.splice(i, 1)[0];
568
- this.$emit('file-removed', { folderId, file: removedFile });
569
- this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData(), linkedReports: this.linkedReports });
1066
+ // ─── DRILL PDF ───────────────────────────────────────────────────────────
1067
+
1068
+ async _buildDrillPdf(report, imageFiles = []) {
1069
+ const { jsPDF } = window.jspdf;
1070
+ const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
1071
+ const pageW = 210, margin = 15, contentW = pageW - margin * 2;
1072
+ let y = 0;
1073
+
1074
+ const checkPage = (needed = 10) => {
1075
+ if (y + needed > 275) { doc.addPage(); y = 20; }
1076
+ };
1077
+
1078
+ const sectionHeading = (text) => {
1079
+ checkPage(12);
1080
+ doc.setFillColor(26, 54, 107);
1081
+ doc.rect(margin, y, contentW, 7, 'F');
1082
+ doc.setTextColor(255, 255, 255);
1083
+ doc.setFontSize(8.5);
1084
+ doc.setFont('helvetica', 'bold');
1085
+ doc.text(text.toUpperCase(), margin + 3, y + 5);
1086
+ y += 10;
1087
+ doc.setTextColor(30, 30, 30);
1088
+ };
1089
+
1090
+ const fieldRow = (label, value, labelWidth = 68) => {
1091
+ checkPage(7);
1092
+ doc.setFontSize(9);
1093
+ doc.setFont('helvetica', 'bold');
1094
+ doc.setTextColor(80, 90, 110);
1095
+ doc.text(label, margin + 2, y);
1096
+ doc.setFont('helvetica', 'normal');
1097
+ doc.setTextColor(20, 20, 20);
1098
+ doc.text(':', margin + labelWidth - 6, y);
1099
+ const valLines = doc.splitTextToSize(String(value || '—'), contentW - labelWidth - 4);
1100
+ doc.text(valLines, margin + labelWidth, y);
1101
+ y += Math.max(valLines.length * 5, 5) + 2;
1102
+ };
1103
+
1104
+ // ── Header band ──────────────────────────────────────────────────────
1105
+ doc.setFillColor(26, 54, 107);
1106
+ doc.rect(0, 0, pageW, 24, 'F');
1107
+ doc.setDrawColor(255, 200, 50);
1108
+ doc.setLineWidth(1.2);
1109
+ doc.line(margin, 23.4, pageW - margin, 23.4);
1110
+ doc.setTextColor(255, 255, 255);
1111
+ doc.setFontSize(14);
1112
+ doc.setFont('helvetica', 'bold');
1113
+ doc.text('EMERGENCY DRILL REPORT', pageW / 2, 10, { align: 'center' });
1114
+ doc.setFontSize(8);
1115
+ doc.setFont('helvetica', 'normal');
1116
+ doc.setTextColor(200, 215, 240);
1117
+ doc.text('Quality, Health, Safety & Environment Management System', pageW / 2, 17, { align: 'center' });
1118
+ y = 30;
1119
+
1120
+ // Ref badge
1121
+ if (report.entityRef) {
1122
+ doc.setFillColor(240, 245, 255);
1123
+ doc.roundedRect(pageW - margin - 44, 26, 44, 8, 2, 2, 'F');
1124
+ doc.setTextColor(26, 54, 107);
1125
+ doc.setFontSize(8);
1126
+ doc.setFont('helvetica', 'bold');
1127
+ doc.text(report.entityRef, pageW - margin - 2, 31, { align: 'right' });
1128
+ doc.setTextColor(30, 30, 30);
1129
+ }
1130
+
1131
+ // ── Vessel / Drill kind grid ──────────────────────────────────────────
1132
+ doc.setDrawColor(180, 190, 210);
1133
+ doc.setLineWidth(0.4);
1134
+ doc.rect(margin, y, contentW, 28, 'S');
1135
+ doc.line(margin + contentW / 2, y, margin + contentW / 2, y + 28);
1136
+ doc.line(margin, y + 14, margin + contentW / 2, y + 14);
1137
+
1138
+ // Vessel name
1139
+ doc.setFontSize(8); doc.setFont('helvetica', 'bold'); doc.setTextColor(80, 90, 110);
1140
+ doc.text('VESSEL:', margin + 3, y + 6);
1141
+ doc.setFont('helvetica', 'bold'); doc.setTextColor(20, 20, 20); doc.setFontSize(10);
1142
+ doc.text(String(report.vessel || '—').toUpperCase(), margin + 22, y + 6);
1143
+
1144
+ // Date / place
1145
+ doc.setFontSize(8); doc.setFont('helvetica', 'bold'); doc.setTextColor(80, 90, 110);
1146
+ doc.text('DATE / PLACE:', margin + 3, y + 20);
1147
+ doc.setFont('helvetica', 'normal'); doc.setTextColor(20, 20, 20);
1148
+ doc.text(`${report.date || '—'} / ${report.vessel || '—'}`, margin + 33, y + 20);
1149
+
1150
+ // Kind of drill
1151
+ const hx = margin + contentW / 2 + 3;
1152
+ doc.setFontSize(8); doc.setFont('helvetica', 'bold'); doc.setTextColor(80, 90, 110);
1153
+ doc.text('KIND OF DRILL:', hx, y + 6);
1154
+ doc.setFontSize(11); doc.setFont('helvetica', 'bold'); doc.setTextColor(26, 54, 107);
1155
+ doc.text(String(report.entity_subtype || '—').toUpperCase(), hx, y + 16);
1156
+ doc.setFontSize(7.5); doc.setFont('helvetica', 'italic'); doc.setTextColor(120, 120, 120);
1157
+ doc.text('RELEVANT IMAGES/NOTES ARE ATTACHED', hx, y + 23);
1158
+
1159
+ y += 32;
1160
+
1161
+ // ── Drill details ─────────────────────────────────────────────────────
1162
+ sectionHeading('Drill Details');
1163
+ fieldRow('Drill Master / Conducted By', report.entity_creator);
1164
+ fieldRow('Duration', report.entity_duration);
1165
+ fieldRow('Objectives', report.entity_objectives);
1166
+ fieldRow('Submitted By', report.submittedBy);
1167
+ fieldRow('Submitted At', this.formatDate(report.submittedAt));
1168
+ y += 2;
1169
+
1170
+ // ── Crew participation table ──────────────────────────────────────────
1171
+ const participantCount = report.entity_participants || 0;
1172
+ sectionHeading(`Crew Participation — ${participantCount} member${participantCount !== 1 ? 's' : ''} participated`);
1173
+
1174
+ // Parse crew from findings if prefixed with CREW:
1175
+ const crewLines = (() => {
1176
+ const raw = (report.findings || '').trim();
1177
+ if (/^CREW:/i.test(raw)) {
1178
+ // Everything up to first blank line after CREW: is the crew list
1179
+ const crewSection = raw.replace(/^CREW:\s*/i, '').split(/\n\n/)[0];
1180
+ return crewSection.split(/[\n,]+/).map(s => s.trim()).filter(Boolean);
1181
+ }
1182
+ // Fallback: show drill master + submitter
1183
+ const list = [];
1184
+ if (report.entity_creator) list.push(`${report.entity_creator} (Drill Master)`);
1185
+ if (report.submittedBy && report.submittedBy !== report.entity_creator)
1186
+ list.push(`${report.submittedBy} (Reporter)`);
1187
+ return list;
1188
+ })();
1189
+
1190
+ // Table header
1191
+ checkPage(14);
1192
+ const colNr = margin, wNr = 12;
1193
+ const colName = margin + wNr, wName = contentW - wNr;
1194
+
1195
+ doc.setFillColor(235, 240, 250);
1196
+ doc.rect(colNr, y, contentW, 7, 'F');
1197
+ doc.setFontSize(8); doc.setFont('helvetica', 'bold'); doc.setTextColor(26, 54, 107);
1198
+ doc.text('NO.', colNr + 2, y + 5);
1199
+ doc.text('NAME / RANK', colName + 2, y + 5);
1200
+ y += 8;
1201
+
1202
+ if (crewLines.length > 0) {
1203
+ crewLines.forEach((name, idx) => {
1204
+ checkPage(7);
1205
+ if (idx % 2 === 0) {
1206
+ doc.setFillColor(249, 250, 252);
1207
+ doc.rect(colNr, y - 1, contentW, 7, 'F');
1208
+ }
1209
+ doc.setFontSize(8.5); doc.setFont('helvetica', 'normal'); doc.setTextColor(30, 30, 30);
1210
+ doc.text(String(idx + 1), colNr + 2, y + 4);
1211
+ doc.text(doc.splitTextToSize(name, wName - 4)[0], colName + 2, y + 4);
1212
+ y += 7;
1213
+ });
1214
+ } else {
1215
+ checkPage(8);
1216
+ doc.setFontSize(8.5); doc.setFont('helvetica', 'italic'); doc.setTextColor(150, 150, 150);
1217
+ doc.text(
1218
+ `${participantCount} crew members participated — attach signed crew list with participants' signatures`,
1219
+ colName + 2, y + 4
1220
+ );
1221
+ y += 8;
1222
+ }
1223
+ y += 4;
1224
+
1225
+ // ── Comments checklist — driven by report.drill_checklist ─────────────
1226
+ sectionHeading('Comments');
1227
+ const checkQuestions = [
1228
+ 'Is the reaction of crew members satisfactory?',
1229
+ 'Is the equipment functioning properly?',
1230
+ 'Is the crew properly trained and ready?',
1231
+ 'Was debriefing conducted on completion of drill?',
1232
+ 'Is time log of various activities marked on checklist?',
1233
+ ];
1234
+ // Parse stored answers (array of booleans). Fall back to all-true if absent.
1235
+ const checkAnswers = (() => {
1236
+ const raw = report.drill_checklist || report.drillChecklist;
1237
+ if (Array.isArray(raw)) return raw;
1238
+ try { return JSON.parse(raw); } catch { return [true, true, true, true, true]; }
1239
+ })();
1240
+ checkQuestions.forEach((q, idx) => {
1241
+ checkPage(9);
1242
+ const yes = checkAnswers[idx] !== false; // undefined → true
1243
+ doc.setFontSize(8.5); doc.setFont('helvetica', 'normal'); doc.setTextColor(30, 30, 30);
1244
+ doc.text(`${idx + 1}. ${q}`, margin + 3, y + 5);
1245
+ if (yes) {
1246
+ doc.setFillColor(212, 237, 218);
1247
+ doc.roundedRect(pageW - margin - 18, y + 1, 18, 6, 2, 2, 'F');
1248
+ doc.setTextColor(21, 87, 36); doc.setFont('helvetica', 'bold'); doc.setFontSize(7.5);
1249
+ doc.text('YES', pageW - margin - 9, y + 5.5, { align: 'center' });
1250
+ } else {
1251
+ doc.setFillColor(248, 215, 218);
1252
+ doc.roundedRect(pageW - margin - 18, y + 1, 18, 6, 2, 2, 'F');
1253
+ doc.setTextColor(114, 28, 36); doc.setFont('helvetica', 'bold'); doc.setFontSize(7.5);
1254
+ doc.text('NO', pageW - margin - 9, y + 5.5, { align: 'center' });
1255
+ }
1256
+ y += 9;
1257
+ });
1258
+ y += 3;
1259
+
1260
+ // ── Drill scenario / findings ─────────────────────────────────────────
1261
+ // Strip the CREW: block from findings before printing
1262
+ const scenarioText = (() => {
1263
+ const raw = (report.findings || '').trim();
1264
+ if (/^CREW:/i.test(raw)) {
1265
+ // Remove everything up to the first blank line
1266
+ const parts = raw.split(/\n\n+/);
1267
+ return parts.slice(1).join('\n\n').trim();
570
1268
  }
1269
+ return raw;
1270
+ })();
1271
+
1272
+ if (scenarioText) {
1273
+ sectionHeading('Drill Scenario & Findings');
1274
+ checkPage(10);
1275
+ doc.setFontSize(9); doc.setFont('helvetica', 'normal'); doc.setTextColor(30, 30, 30);
1276
+ const lines = doc.splitTextToSize(scenarioText, contentW - 6);
1277
+ lines.forEach(line => {
1278
+ checkPage(6);
1279
+ doc.text(line, margin + 3, y);
1280
+ y += 5.5;
1281
+ });
1282
+ y += 4;
1283
+ }
1284
+
1285
+ // ── Suggestions / corrective actions ─────────────────────────────────
1286
+ if (report.correctiveActions) {
1287
+ sectionHeading('Suggestions for Improvement & Corrective Actions');
1288
+ checkPage(10);
1289
+ doc.setFontSize(9); doc.setFont('helvetica', 'normal'); doc.setTextColor(30, 30, 30);
1290
+ const lines = doc.splitTextToSize(report.correctiveActions, contentW - 6);
1291
+ lines.forEach(line => {
1292
+ checkPage(6);
1293
+ doc.text(line, margin + 3, y);
1294
+ y += 5.5;
1295
+ });
1296
+ y += 4;
1297
+ }
1298
+
1299
+ // ── Remarks box ───────────────────────────────────────────────────────
1300
+ checkPage(22);
1301
+ sectionHeading('Remarks');
1302
+ doc.setDrawColor(180, 190, 210); doc.setLineWidth(0.3);
1303
+ doc.rect(margin, y, contentW, 16, 'S');
1304
+ doc.setFontSize(8.5); doc.setFont('helvetica', 'italic'); doc.setTextColor(140, 140, 140);
1305
+ doc.text('Drill carried out satisfactorily.', margin + 3, y + 8);
1306
+ y += 20;
1307
+
1308
+ // ── Embedded images ───────────────────────────────────────────────────
1309
+ for (const imgFile of imageFiles) {
1310
+ try {
1311
+ let dataUrl;
1312
+ if (imgFile.file instanceof File) dataUrl = await this._fileToDataUrl(imgFile.file);
1313
+ else if (imgFile.publicUrl) dataUrl = await this._fetchImageAsDataUrl(imgFile.publicUrl);
1314
+ if (!dataUrl) continue;
1315
+ checkPage(30);
1316
+ sectionHeading('Attachment: ' + imgFile.name);
1317
+ const props = doc.getImageProperties(dataUrl);
1318
+ const ratio = Math.min(contentW / props.width, 80 / props.height);
1319
+ const iw = props.width * ratio, ih = props.height * ratio;
1320
+ checkPage(ih + 5);
1321
+ doc.addImage(dataUrl, imgFile.type?.split('/')[1]?.toUpperCase() || 'JPEG', margin, y, iw, ih);
1322
+ y += ih + 8;
1323
+ } catch (e) { console.warn('Could not embed image', imgFile.name, e); }
1324
+ }
1325
+
1326
+ // ── Signature block ───────────────────────────────────────────────────
1327
+
1328
+
1329
+ // ── Footer ────────────────────────────────────────────────────────────
1330
+ const totalPages = doc.getNumberOfPages();
1331
+ for (let p = 1; p <= totalPages; p++) {
1332
+ doc.setPage(p);
1333
+ doc.setFillColor(26, 54, 107);
1334
+ doc.rect(0, 287, pageW, 10, 'F');
1335
+ doc.setTextColor(180, 200, 240); doc.setFontSize(7.5); doc.setFont('helvetica', 'normal');
1336
+ doc.text(`Emergency Drill Report · Generated ${new Date().toLocaleDateString('en-GB')} · Page ${p} of ${totalPages}`, margin, 293);
1337
+ if (report.entityRef) doc.text(report.entityRef, pageW - margin, 293, { align: 'right' });
571
1338
  }
1339
+
1340
+ return doc.output('arraybuffer');
572
1341
  },
573
1342
 
574
- deleteFolder(folderId) {
575
- if (confirm('Delete this folder and all its files?')) {
576
- const i = this.folders.findIndex(f => f.id === folderId);
577
- if (i > -1) {
578
- const deleted = this.folders.splice(i, 1)[0];
579
- this.$emit('folder-deleted', deleted);
580
- this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData(), linkedReports: this.linkedReports });
1343
+ // ─── INCIDENT PDF ────────────────────────────────────────────────────────
1344
+
1345
+ async _buildIncidentPdf(report, imageFiles = []) {
1346
+ const { jsPDF } = window.jspdf;
1347
+ const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
1348
+ const pageW = 210, margin = 15, contentW = pageW - margin * 2;
1349
+ let y = 0;
1350
+
1351
+ const checkPage = (needed = 10) => {
1352
+ if (y + needed > 275) { doc.addPage(); y = 20; }
1353
+ };
1354
+
1355
+ /**
1356
+ * Bordered labelled box — mirrors the Near Miss Report form style.
1357
+ * label — small-caps field label shown inside the top of the box
1358
+ * text — content (may be null/empty — leaves a blank writable area)
1359
+ * minHeight — minimum box height in mm
1360
+ */
1361
+ const labelledBox = (label, text, minHeight = 18) => {
1362
+ const bodyText = text ? doc.splitTextToSize(String(text), contentW - 6) : [];
1363
+ const textH = bodyText.length * 5.5;
1364
+ const boxH = Math.max(minHeight, textH + 14);
1365
+ checkPage(boxH + 4);
1366
+
1367
+ doc.setDrawColor(140, 150, 165);
1368
+ doc.setLineWidth(0.35);
1369
+ doc.rect(margin, y, contentW, boxH, 'S');
1370
+
1371
+ // Label
1372
+ doc.setFontSize(7.5); doc.setFont('helvetica', 'bold'); doc.setTextColor(70, 80, 100);
1373
+ const labelLines = doc.splitTextToSize(label.toUpperCase(), contentW - 6);
1374
+ labelLines.forEach((ll, i) => doc.text(ll, margin + 3, y + 5 + i * 4));
1375
+ const labelH = labelLines.length * 4 + 2;
1376
+
1377
+ // Thin rule under label
1378
+ doc.setDrawColor(200, 208, 220); doc.setLineWidth(0.2);
1379
+ doc.line(margin + 1, y + labelH + 2, margin + contentW - 1, y + labelH + 2);
1380
+
1381
+ if (bodyText.length > 0) {
1382
+ doc.setFontSize(9.5); doc.setFont('helvetica', 'normal'); doc.setTextColor(20, 20, 20);
1383
+ let ty = y + labelH + 7;
1384
+ bodyText.forEach(line => { doc.text(line, margin + 3, ty); ty += 5.5; });
581
1385
  }
1386
+
1387
+ y += boxH + 3;
1388
+ };
1389
+
1390
+ // ── Header ────────────────────────────────────────────────────────────
1391
+ doc.setFillColor(26, 54, 107);
1392
+ doc.rect(0, 0, pageW, 16, 'F');
1393
+ doc.setTextColor(255, 255, 255);
1394
+ doc.setFontSize(12); doc.setFont('helvetica', 'bold');
1395
+ const incLabel = (report.entity_subtype || report.entityType || 'INCIDENT').toUpperCase() + ' REPORT';
1396
+ doc.text(incLabel, margin, 10);
1397
+ doc.setFontSize(7.5); doc.setFont('helvetica', 'normal'); doc.setTextColor(200, 215, 240);
1398
+ doc.text(`Ref: ${report.entityRef || '—'}`, pageW - margin, 8, { align: 'right' });
1399
+ doc.text(`Date: ${report.date || '—'}`, pageW - margin, 13, { align: 'right' });
1400
+ y = 20;
1401
+
1402
+ // Form number + submitted by row
1403
+ doc.setFontSize(8); doc.setFont('helvetica', 'bold'); doc.setTextColor(80, 90, 110);
1404
+ doc.text('FORM NUMBER:', margin, y + 4);
1405
+ doc.setFont('helvetica', 'normal'); doc.setTextColor(20, 20, 20);
1406
+ doc.text(report.entityRef || '—', margin + 30, y + 4);
1407
+ doc.setFont('helvetica', 'bold'); doc.setTextColor(80, 90, 110);
1408
+ doc.text('SUBMITTED BY:', pageW / 2, y + 4);
1409
+ doc.setFont('helvetica', 'normal'); doc.setTextColor(20, 20, 20);
1410
+ doc.text(report.submittedBy || '—', pageW / 2 + 28, y + 4);
1411
+ y += 10;
1412
+
1413
+ // ── 2-col meta: vessel | severity/type ───────────────────────────────
1414
+ const halfW = (contentW - 2) / 2;
1415
+ doc.setDrawColor(140, 150, 165); doc.setLineWidth(0.35);
1416
+ doc.rect(margin, y, halfW, 14, 'S');
1417
+ doc.rect(margin + halfW + 2, y, halfW, 14, 'S');
1418
+
1419
+ doc.setFontSize(7.5); doc.setFont('helvetica', 'bold'); doc.setTextColor(80, 90, 110);
1420
+ doc.text('VESSEL NAME', margin + 3, y + 5);
1421
+ doc.text('SEVERITY / TYPE', margin + halfW + 5, y + 5);
1422
+ doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.setTextColor(20, 20, 20);
1423
+ doc.text(String(report.vessel || '—'), margin + 3, y + 12);
1424
+ doc.text(
1425
+ `${report.entity_subtype || '—'} · ${report.severity || report.entity_subtype || '—'}`,
1426
+ margin + halfW + 5, y + 12
1427
+ );
1428
+ y += 18;
1429
+
1430
+ // Management office + date
1431
+ doc.setDrawColor(140, 150, 165); doc.setLineWidth(0.35);
1432
+ doc.rect(margin, y, halfW, 10, 'S');
1433
+ doc.rect(margin + halfW + 2, y, halfW, 10, 'S');
1434
+ doc.setFontSize(7.5); doc.setFont('helvetica', 'bold'); doc.setTextColor(80, 90, 110);
1435
+ doc.text('MANAGEMENT OFFICE', margin + 3, y + 4);
1436
+ doc.text('DATE', margin + halfW + 5, y + 4);
1437
+ doc.setFontSize(9); doc.setFont('helvetica', 'normal'); doc.setTextColor(20, 20, 20);
1438
+ doc.text('—', margin + 3, y + 9);
1439
+ doc.text(report.date || '—', margin + halfW + 5, y + 9);
1440
+ y += 14;
1441
+
1442
+ // ── Labelled content boxes ────────────────────────────────────────────
1443
+
1444
+ labelledBox('Description of Event', report.findings, 24);
1445
+
1446
+ labelledBox(
1447
+ 'Possible Consequences\n(e.g. Personal injury, damage, collision, grounding, fire, pollution etc.)',
1448
+ report.inc_consequences || null,
1449
+ 18
1450
+ );
1451
+
1452
+ labelledBox(
1453
+ 'Relevant Factors / Conditions Surrounding the Event\n(e.g. weather, lighting etc.)',
1454
+ report.inc_factors || null,
1455
+ 18
1456
+ );
1457
+
1458
+ labelledBox('Immediate Action Taken', report.inc_immediate_action || null, 16);
1459
+
1460
+ labelledBox(
1461
+ 'Direct Cause\n(e.g. failure to follow procedures, inadequate or defective equipment etc.)',
1462
+ report.inc_direct_cause || null,
1463
+ 18
1464
+ );
1465
+
1466
+ labelledBox(
1467
+ 'Root Cause\n(e.g. lack of training, personal factors, job factors, control management factors, instructions not clear etc.)',
1468
+ report.inc_root_cause || null,
1469
+ 18
1470
+ );
1471
+
1472
+ labelledBox('Action Taken on Board to Avoid Re-occurrence', report.correctiveActions || null, 18);
1473
+
1474
+ labelledBox('Any Other Remarks', report.inc_remarks || null, 16);
1475
+
1476
+ // ── Embedded images ───────────────────────────────────────────────────
1477
+ for (const imgFile of imageFiles) {
1478
+ try {
1479
+ let dataUrl;
1480
+ if (imgFile.file instanceof File) dataUrl = await this._fileToDataUrl(imgFile.file);
1481
+ else if (imgFile.publicUrl) dataUrl = await this._fetchImageAsDataUrl(imgFile.publicUrl);
1482
+ if (!dataUrl) continue;
1483
+ checkPage(30);
1484
+ doc.setFillColor(235, 240, 250);
1485
+ doc.rect(margin, y, contentW, 7, 'F');
1486
+ doc.setFontSize(8); doc.setFont('helvetica', 'bold'); doc.setTextColor(26, 54, 107);
1487
+ doc.text('ATTACHMENT: ' + imgFile.name, margin + 3, y + 5);
1488
+ y += 10;
1489
+ const props = doc.getImageProperties(dataUrl);
1490
+ const ratio = Math.min(contentW / props.width, 80 / props.height);
1491
+ const iw = props.width * ratio, ih = props.height * ratio;
1492
+ checkPage(ih + 5);
1493
+ doc.addImage(dataUrl, imgFile.type?.split('/')[1]?.toUpperCase() || 'JPEG', margin, y, iw, ih);
1494
+ y += ih + 8;
1495
+ } catch (e) { console.warn('Could not embed image', imgFile.name, e); }
1496
+ }
1497
+
1498
+ // ── Closed-out note ───────────────────────────────────────────────────
1499
+ checkPage(14);
1500
+ y += 4;
1501
+ doc.setFontSize(8.5); doc.setFont('helvetica', 'italic'); doc.setTextColor(100, 110, 130);
1502
+ doc.text('Closed out on board / Office support required (delete as applicable)', margin, y);
1503
+ y += 8;
1504
+
1505
+ // ── Footer ────────────────────────────────────────────────────────────
1506
+ const totalPages = doc.getNumberOfPages();
1507
+ for (let p = 1; p <= totalPages; p++) {
1508
+ doc.setPage(p);
1509
+ doc.setFillColor(26, 54, 107);
1510
+ doc.rect(0, 287, pageW, 10, 'F');
1511
+ doc.setTextColor(180, 200, 240); doc.setFontSize(7.5); doc.setFont('helvetica', 'normal');
1512
+ doc.text(`Incident Report · Generated ${new Date().toLocaleDateString('en-GB')} · Page ${p} of ${totalPages}`, margin, 293);
1513
+ if (report.entityRef) doc.text(report.entityRef, pageW - margin, 293, { align: 'right' });
582
1514
  }
1515
+
1516
+ return doc.output('arraybuffer');
583
1517
  },
584
1518
 
585
- toggleFolder(folderId) {
586
- const i = this.expandedFolders.indexOf(folderId);
587
- this.expandedFolders = i > -1 ? [] : [folderId];
1519
+ // ─── MANUAL PDF ──────────────────────────────────────────────────────────
1520
+
1521
+ async _buildManualPdf(report, imageFiles = []) {
1522
+ const { jsPDF } = window.jspdf;
1523
+ const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
1524
+ const margin = 18, pageW = 210, contentW = pageW - margin * 2;
1525
+ let y = 0;
1526
+
1527
+ doc.setFillColor(30, 60, 114);
1528
+ doc.rect(0, 0, pageW, 28, 'F');
1529
+ doc.setTextColor(255, 255, 255);
1530
+ doc.setFontSize(16); doc.setFont('helvetica', 'bold');
1531
+ doc.text('QHSE REPORT', margin, 11);
1532
+ doc.setFontSize(9); doc.setFont('helvetica', 'normal');
1533
+ doc.text(this.reportTypeLabel(report), margin, 18);
1534
+ if (report.entityRef) {
1535
+ doc.setFontSize(10); doc.setFont('helvetica', 'bold');
1536
+ doc.text(report.entityRef, pageW - margin, 18, { align: 'right' });
1537
+ }
1538
+ y = 36;
1539
+
1540
+ doc.setTextColor(30, 60, 114); doc.setFontSize(13); doc.setFont('helvetica', 'bold');
1541
+ const titleLines = doc.splitTextToSize(report.title, contentW);
1542
+ doc.text(titleLines, margin, y);
1543
+ y += titleLines.length * 6 + 4;
1544
+
1545
+ doc.setDrawColor(200, 210, 230); doc.line(margin, y, pageW - margin, y); y += 6;
1546
+
1547
+ const meta = [
1548
+ ['Submitted By', report.submittedBy || '—'],
1549
+ ['Date', report.date || '—'],
1550
+ ['Vessel / Location', report.vessel || '—'],
1551
+ ['Submitted At', this.formatDate(report.submittedAt)],
1552
+ ];
1553
+ if (report.manualCategory) meta.splice(2, 0, ['Category', report.manualCategory]);
1554
+
1555
+ const colW = contentW / 2;
1556
+ meta.forEach((item, idx) => {
1557
+ const col = idx % 2, row = Math.floor(idx / 2);
1558
+ const xo = margin + col * colW, yo = y + row * 10;
1559
+ doc.setFontSize(9); doc.setFont('helvetica', 'bold'); doc.setTextColor(100, 120, 160);
1560
+ doc.text(item[0].toUpperCase(), xo, yo);
1561
+ doc.setFont('helvetica', 'normal'); doc.setTextColor(40, 40, 40);
1562
+ doc.text(String(item[1]), xo, yo + 4.5);
1563
+ });
1564
+ y += Math.ceil(meta.length / 2) * 10 + 6;
1565
+
1566
+ const addSection = (heading, text) => {
1567
+ if (!text) return;
1568
+ if (y > 250) { doc.addPage(); y = 20; }
1569
+ doc.setFillColor(240, 245, 255);
1570
+ doc.roundedRect(margin, y, contentW, 7, 1, 1, 'F');
1571
+ doc.setFont('helvetica', 'bold'); doc.setFontSize(9); doc.setTextColor(30, 60, 114);
1572
+ doc.text(heading.toUpperCase(), margin + 3, y + 5); y += 10;
1573
+ doc.setFont('helvetica', 'normal'); doc.setFontSize(10); doc.setTextColor(40, 40, 40);
1574
+ doc.splitTextToSize(text, contentW).forEach(line => {
1575
+ if (y > 272) { doc.addPage(); y = 20; }
1576
+ doc.text(line, margin, y); y += 5.5;
1577
+ });
1578
+ y += 4;
1579
+ };
1580
+ addSection('Findings / Summary', report.findings);
1581
+ addSection('Corrective Actions', report.correctiveActions);
1582
+
1583
+ for (const imgFile of imageFiles) {
1584
+ try {
1585
+ let dataUrl;
1586
+ if (imgFile.file instanceof File) dataUrl = await this._fileToDataUrl(imgFile.file);
1587
+ else if (imgFile.publicUrl) dataUrl = await this._fetchImageAsDataUrl(imgFile.publicUrl);
1588
+ if (!dataUrl) continue;
1589
+ if (y > 220) { doc.addPage(); y = 20; }
1590
+ doc.setFillColor(240, 245, 255);
1591
+ doc.roundedRect(margin, y, contentW, 7, 1, 1, 'F');
1592
+ doc.setFont('helvetica', 'bold'); doc.setFontSize(9); doc.setTextColor(30, 60, 114);
1593
+ doc.text('ATTACHMENT: ' + imgFile.name, margin + 3, y + 5); y += 10;
1594
+ const props = doc.getImageProperties(dataUrl);
1595
+ const ratio = Math.min(contentW / props.width, 80 / props.height);
1596
+ const iw = props.width * ratio, ih = props.height * ratio;
1597
+ if (y + ih > 272) { doc.addPage(); y = 20; }
1598
+ doc.addImage(dataUrl, imgFile.type?.split('/')[1]?.toUpperCase() || 'JPEG', margin, y, iw, ih);
1599
+ y += ih + 8;
1600
+ } catch (e) { console.warn('Could not embed image', imgFile.name, e); }
1601
+ }
1602
+
1603
+ const totalPages = doc.getNumberOfPages();
1604
+ for (let p = 1; p <= totalPages; p++) {
1605
+ doc.setPage(p);
1606
+ doc.setFillColor(30, 60, 114); doc.rect(0, 287, pageW, 10, 'F');
1607
+ doc.setTextColor(180, 200, 240); doc.setFontSize(8); doc.setFont('helvetica', 'normal');
1608
+ doc.text(`Generated ${new Date().toLocaleDateString('en-GB')} · Page ${p} of ${totalPages}`, margin, 293);
1609
+ if (report.entityRef) doc.text(report.entityRef, pageW - margin, 293, { align: 'right' });
1610
+ }
1611
+
1612
+ return doc.output('arraybuffer');
588
1613
  },
589
- closeAllFolders() { this.expandedFolders = []; },
590
- getExpandedFolder() {
591
- return this.expandedFolders.length > 0
592
- ? this.folders.find(f => f.id === this.expandedFolders[0]) || { files: [], name: '' }
593
- : { files: [], name: '' };
1614
+
1615
+ // ─── ZIP builder ─────────────────────────────────────────────────────────
1616
+
1617
+ async _buildAndDownloadZip(report, reportPdfBytes, allAttachments) {
1618
+ const zip = new window.JSZip();
1619
+ const safeName = this.safeRef(report);
1620
+ const folder = zip.folder(safeName);
1621
+
1622
+ folder.file(`${safeName}_report.pdf`, reportPdfBytes);
1623
+
1624
+ for (const att of allAttachments) {
1625
+ try {
1626
+ let bytes;
1627
+ if (att.file instanceof File) {
1628
+ bytes = await att.file.arrayBuffer();
1629
+ } else if (att.publicUrl) {
1630
+ const resp = await fetch(att.publicUrl);
1631
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
1632
+ bytes = await resp.arrayBuffer();
1633
+ }
1634
+ if (bytes) folder.file(att.name, bytes);
1635
+ } catch (e) {
1636
+ console.warn(`Could not add ${att.name} to ZIP:`, e);
1637
+ }
1638
+ }
1639
+
1640
+ const blob = await zip.generateAsync({
1641
+ type: 'blob',
1642
+ compression: 'DEFLATE',
1643
+ compressionOptions: { level: 6 },
1644
+ });
1645
+ this._triggerDownload(blob, `${safeName}.zip`);
594
1646
  },
595
- showPreview(id) { this.previewFolder = id; },
596
- hidePreview() { this.previewFolder = null; },
597
1647
 
598
- formatFileSize(bytes) {
599
- if (!bytes) return '0 Bytes';
600
- const k = 1024, sizes = ['Bytes', 'KB', 'MB', 'GB'];
601
- const i = Math.floor(Math.log(bytes) / Math.log(k));
602
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
1648
+ _triggerDownload(blob, filename) {
1649
+ const url = URL.createObjectURL(blob);
1650
+ const a = document.createElement('a');
1651
+ a.href = url; a.download = filename;
1652
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
1653
+ setTimeout(() => URL.revokeObjectURL(url), 5000);
603
1654
  },
604
- formatDate(d) { return d ? new Date(d).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) : '—'; },
605
- getAllFilesData() {
606
- return this.folders.flatMap(folder =>
607
- folder.files.map(file => ({ ...file, folderName: folder.name, folderId: folder.id }))
608
- );
1655
+
1656
+ _fileToDataUrl(file) {
1657
+ return new Promise((resolve, reject) => {
1658
+ const r = new FileReader();
1659
+ r.onload = () => resolve(r.result); r.onerror = reject;
1660
+ r.readAsDataURL(file);
1661
+ });
609
1662
  },
610
- }
1663
+
1664
+ async _fetchImageAsDataUrl(url) {
1665
+ try {
1666
+ const resp = await fetch(url);
1667
+ return this._fileToDataUrl(await resp.blob());
1668
+ } catch { return null; }
1669
+ },
1670
+
1671
+ // ─── File input (report form) ─────────────────────────────────────────────
1672
+ handleReportFileAttach(e) { this.reportForm.files.push(...Array.from(e.target.files)); e.target.value = ''; },
1673
+ handleReportFileDrop(e) { this.reportForm.files.push(...Array.from(e.dataTransfer.files)); },
1674
+ removeReportFile(i) { this.reportForm.files.splice(i, 1); },
1675
+
1676
+ // ─── User-folder UI ───────────────────────────────────────────────────────
1677
+ requestCreateFolder() {
1678
+ if (!this.newFolderName.trim()) return;
1679
+ this.$emit('folder-create-requested', this.newFolderName.trim());
1680
+ this.newFolderName = '';
1681
+ this.showNewFolderModal = false;
1682
+ this.activeTab = 'folders';
1683
+ },
1684
+
1685
+ selectFolder(folder) { this.selectedFolder = folder; this.$refs.fileInput.click(); },
1686
+
1687
+ handleFileSelection(event) {
1688
+ const files = Array.from(event.target.files);
1689
+ if (!this.selectedFolder || files.length === 0) return;
1690
+ this.$emit('files-add-requested', {
1691
+ folderId: this.selectedFolder.id,
1692
+ files: files.map(file => ({
1693
+ id: Date.now() + Math.random(),
1694
+ name: file.name,
1695
+ size: file.size,
1696
+ type: file.type,
1697
+ file,
1698
+ addedAt: new Date(),
1699
+ })),
1700
+ });
1701
+ event.target.value = ''; this.selectedFolder = null;
1702
+ },
1703
+
1704
+ toggleFolder(key) {
1705
+ const i = this.expandedFolders.indexOf(key);
1706
+ if (i > -1) this.expandedFolders.splice(i, 1); else this.expandedFolders.push(key);
1707
+ },
1708
+
1709
+ // ─── Formatters ───────────────────────────────────────────────────────────
1710
+ formatFileSize(bytes) {
1711
+ if (!bytes) return '0 B';
1712
+ const k = 1024, sizes = ['B', 'KB', 'MB', 'GB'];
1713
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1714
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
1715
+ },
1716
+ formatDate(d) {
1717
+ return d ? new Date(d).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) : '—';
1718
+ },
1719
+ },
611
1720
  };
612
1721
  </script>
613
1722
 
614
1723
  <style scoped>
1724
+ /* ─── Base ─── */
615
1725
  .reports-container {
616
1726
  padding: 24px;
617
1727
  }
618
1728
 
619
- /* ── Banner ── */
1729
+ /* ─── Banner ─── */
620
1730
  .missing-banner {
621
1731
  background: linear-gradient(135deg, #fff3cd, #ffe8a1);
622
1732
  border: 1.5px solid #f0ad4e;
@@ -692,9 +1802,9 @@ export default {
692
1802
  transform: translateY(-12px);
693
1803
  }
694
1804
 
695
- /* ── Page Header ── */
1805
+ /* ─── Page Header ─── */
696
1806
  .page-header {
697
- margin-bottom: 24px;
1807
+ margin-bottom: 18px;
698
1808
  border-bottom: 1px solid #e0e0e0;
699
1809
  padding-bottom: 16px;
700
1810
  }
@@ -712,25 +1822,72 @@ export default {
712
1822
  margin: 2px 0 0;
713
1823
  }
714
1824
 
715
- .header-actions {
1825
+ .btn-group-split {
716
1826
  display: flex;
717
1827
  gap: 8px;
1828
+ flex-wrap: wrap;
718
1829
  }
719
1830
 
720
- /* ── Section Label ── */
721
- .section-label {
722
- font-size: 12px;
723
- font-weight: 700;
724
- text-transform: uppercase;
725
- letter-spacing: .06em;
726
- color: #495057;
727
- margin-bottom: 14px;
1831
+ /* ─── Tabs ─── */
1832
+ .tab-nav {
1833
+ display: flex;
1834
+ gap: 4px;
1835
+ margin-bottom: 20px;
1836
+ border-bottom: 2px solid #e9ecef;
1837
+ overflow-x: auto;
1838
+ }
1839
+
1840
+ .tab-btn {
1841
+ background: none;
1842
+ border: none;
1843
+ border-bottom: 3px solid transparent;
1844
+ padding: 9px 16px;
1845
+ font-size: 13px;
1846
+ font-weight: 600;
1847
+ color: #6c757d;
1848
+ cursor: pointer;
1849
+ white-space: nowrap;
1850
+ margin-bottom: -2px;
1851
+ transition: all .2s;
728
1852
  display: flex;
729
1853
  align-items: center;
730
- gap: 7px;
1854
+ gap: 5px;
1855
+ }
1856
+
1857
+ .tab-btn:hover {
1858
+ color: #2a5298;
1859
+ }
1860
+
1861
+ .tab-btn.active {
1862
+ color: #2a5298;
1863
+ border-bottom-color: #2a5298;
1864
+ }
1865
+
1866
+ .tab-count {
1867
+ font-size: 10px;
1868
+ background: #e9ecef;
1869
+ color: #495057;
1870
+ border-radius: 10px;
1871
+ padding: 1px 7px;
1872
+ font-weight: 700;
1873
+ }
1874
+
1875
+ .tab-count.drill {
1876
+ background: #d1ecf1;
1877
+ color: #0c5460;
1878
+ }
1879
+
1880
+ .tab-count.incident {
1881
+ background: #f8d7da;
1882
+ color: #721c24;
731
1883
  }
732
1884
 
733
- /* ── Report Cards ── */
1885
+ .tab-count.manual {
1886
+ background: #e2e3f0;
1887
+ color: #383d72;
1888
+ }
1889
+
1890
+ /* ─── Report Cards ─── */
734
1891
  .report-cards {
735
1892
  display: flex;
736
1893
  flex-direction: column;
@@ -741,13 +1898,14 @@ export default {
741
1898
  .report-card {
742
1899
  display: flex;
743
1900
  align-items: center;
744
- gap: 14px;
1901
+ gap: 12px;
745
1902
  background: #fff;
746
1903
  border: 1.5px solid #e9ecef;
747
1904
  border-radius: 10px;
748
- padding: 14px 18px;
749
- cursor: pointer;
1905
+ padding: 12px 16px;
750
1906
  transition: all .2s;
1907
+ position: relative;
1908
+ overflow: hidden;
751
1909
  }
752
1910
 
753
1911
  .report-card:hover {
@@ -766,18 +1924,38 @@ export default {
766
1924
 
767
1925
  0%,
768
1926
  100% {
769
- background: #fffbf0;
1927
+ background: #fffbf0
770
1928
  }
771
1929
 
772
1930
  50% {
773
- background: #fff3cd;
1931
+ background: #fff3cd
774
1932
  }
775
1933
  }
776
1934
 
1935
+ .rc-type-bar {
1936
+ position: absolute;
1937
+ left: 0;
1938
+ top: 0;
1939
+ bottom: 0;
1940
+ width: 4px;
1941
+ }
1942
+
1943
+ .rc-type-bar.drill {
1944
+ background: #17a2b8;
1945
+ }
1946
+
1947
+ .rc-type-bar.incident {
1948
+ background: #dc3545;
1949
+ }
1950
+
1951
+ .rc-type-bar.manual {
1952
+ background: #6f42c1;
1953
+ }
1954
+
777
1955
  .rc-badge {
778
- font-size: 10px;
1956
+ font-size: 9px;
779
1957
  font-weight: 700;
780
- padding: 3px 8px;
1958
+ padding: 2px 7px;
781
1959
  border-radius: 4px;
782
1960
  text-transform: uppercase;
783
1961
  white-space: nowrap;
@@ -794,16 +1972,22 @@ export default {
794
1972
  color: #721c24;
795
1973
  }
796
1974
 
1975
+ .rc-badge.manual {
1976
+ background: #ede9f6;
1977
+ color: #383d72;
1978
+ }
1979
+
797
1980
  .rc-body {
798
1981
  display: flex;
799
1982
  flex-direction: column;
800
1983
  flex: 1;
801
1984
  min-width: 0;
1985
+ cursor: pointer;
802
1986
  }
803
1987
 
804
1988
  .rc-ref {
805
1989
  font-family: monospace;
806
- font-size: 13px;
1990
+ font-size: 12px;
807
1991
  color: #2a5298;
808
1992
  font-weight: 700;
809
1993
  }
@@ -822,192 +2006,449 @@ export default {
822
2006
  margin-top: 2px;
823
2007
  }
824
2008
 
825
- .rc-folder {
2009
+ .rc-files {
826
2010
  font-size: 11px;
827
2011
  color: #6c757d;
828
- background: #f8f9fa;
829
- border-radius: 4px;
830
- padding: 2px 8px;
2012
+ display: flex;
2013
+ align-items: center;
2014
+ gap: 4px;
831
2015
  flex-shrink: 0;
832
2016
  }
833
2017
 
834
- .rc-arrow {
835
- color: #ced4da;
836
- font-size: 14px;
2018
+ .rc-folder-pill {
2019
+ display: inline-flex;
2020
+ align-items: center;
2021
+ font-size: 10px;
2022
+ font-weight: 700;
2023
+ padding: 3px 10px;
2024
+ border-radius: 12px;
2025
+ cursor: pointer;
2026
+ border: 1.5px solid;
2027
+ transition: all .18s;
2028
+ white-space: nowrap;
2029
+ flex-shrink: 0;
2030
+ max-width: 170px;
2031
+ overflow: hidden;
2032
+ text-overflow: ellipsis;
837
2033
  }
838
2034
 
839
- /* ── Folders ── */
840
- .folders-section {
841
- display: grid;
842
- grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
843
- gap: 18px;
844
- padding: 4px 0;
2035
+ .rc-folder-pill.drill {
2036
+ background: #e8f7fa;
2037
+ border-color: #17a2b8;
2038
+ color: #0c5460;
845
2039
  }
846
2040
 
847
- .folder-card {
848
- position: relative;
849
- border: 2px solid #e0e0e0;
850
- border-radius: 10px;
851
- padding: 15px;
2041
+ .rc-folder-pill.incident {
2042
+ background: #fce8ea;
2043
+ border-color: #dc3545;
2044
+ color: #721c24;
2045
+ }
2046
+
2047
+ .rc-folder-pill.manual {
2048
+ background: #f0ebfa;
2049
+ border-color: #6f42c1;
2050
+ color: #383d72;
2051
+ }
2052
+
2053
+ .rc-folder-pill:hover {
2054
+ filter: brightness(.92);
2055
+ }
2056
+
2057
+ .rc-actions {
2058
+ display: flex;
2059
+ gap: 5px;
2060
+ flex-shrink: 0;
2061
+ }
2062
+
2063
+ .rc-btn {
2064
+ width: 30px;
2065
+ height: 30px;
2066
+ border: 1.5px solid #dee2e6;
2067
+ border-radius: 6px;
852
2068
  background: #fff;
853
2069
  cursor: pointer;
854
- aspect-ratio: 1;
855
2070
  display: flex;
856
- flex-direction: column;
857
2071
  align-items: center;
858
2072
  justify-content: center;
859
- transition: all .2s;
2073
+ font-size: 13px;
2074
+ transition: all .18s;
2075
+ text-decoration: none;
2076
+ color: #495057;
2077
+ }
2078
+
2079
+ .rc-btn:disabled {
2080
+ opacity: .5;
2081
+ cursor: not-allowed;
860
2082
  }
861
2083
 
862
- .folder-card:hover {
2084
+ .rc-btn--view:hover:not(:disabled) {
2085
+ background: #2a5298;
2086
+ color: #fff;
863
2087
  border-color: #2a5298;
864
- box-shadow: 0 4px 12px rgba(0, 0, 0, .1);
865
- transform: translateY(-2px);
866
2088
  }
867
2089
 
868
- .folder-content {
2090
+ .rc-btn--download {
2091
+ color: #28a745;
2092
+ }
2093
+
2094
+ .rc-btn--download:hover:not(:disabled) {
2095
+ background: #28a745;
2096
+ color: #fff;
2097
+ border-color: #28a745;
2098
+ }
2099
+
2100
+ .rc-btn--danger {
2101
+ color: #dc3545;
2102
+ }
2103
+
2104
+ .rc-btn--danger:hover:not(:disabled) {
2105
+ background: #dc3545;
2106
+ color: #fff;
2107
+ border-color: #dc3545;
2108
+ }
2109
+
2110
+ /* ─── Crew hint ─── */
2111
+ .crew-hint {
2112
+ background: #eef7ff;
2113
+ border: 1px solid #b8deff;
2114
+ border-radius: 7px;
2115
+ padding: 10px 14px;
2116
+ font-size: 12px;
2117
+ color: #0c5460;
2118
+ display: flex;
2119
+ align-items: flex-start;
2120
+ gap: 8px;
2121
+ margin-bottom: 4px;
2122
+ }
2123
+
2124
+ .crew-hint code {
2125
+ background: #d1ecf1;
2126
+ border-radius: 3px;
2127
+ padding: 1px 5px;
2128
+ font-size: 11px;
2129
+ color: #0c5460;
2130
+ }
2131
+
2132
+ /* ─── Section Heading ─── */
2133
+ .section-heading {
2134
+ display: flex;
2135
+ align-items: center;
2136
+ font-size: 13px;
2137
+ font-weight: 700;
2138
+ color: #495057;
2139
+ margin-bottom: 12px;
2140
+ gap: 5px;
2141
+ }
2142
+
2143
+ .section-count {
2144
+ background: #e9ecef;
2145
+ color: #495057;
2146
+ font-size: 10px;
2147
+ font-weight: 700;
2148
+ border-radius: 10px;
2149
+ padding: 1px 8px;
2150
+ }
2151
+
2152
+ .section-hint {
2153
+ font-size: 11px;
2154
+ font-weight: 400;
2155
+ color: #adb5bd;
2156
+ margin-left: 2px;
2157
+ }
2158
+
2159
+ /* ─── Folders list ─── */
2160
+ .folders-list {
869
2161
  display: flex;
870
2162
  flex-direction: column;
2163
+ gap: 10px;
2164
+ }
2165
+
2166
+ .auto-folder-card {
2167
+ border: 1.5px solid #e9ecef;
2168
+ border-radius: 12px;
2169
+ background: #fff;
2170
+ overflow: hidden;
2171
+ transition: border-color .18s;
2172
+ }
2173
+
2174
+ .auto-folder-card.drill {
2175
+ border-left: 4px solid #17a2b8;
2176
+ }
2177
+
2178
+ .auto-folder-card.incident {
2179
+ border-left: 4px solid #dc3545;
2180
+ }
2181
+
2182
+ .auto-folder-card.manual {
2183
+ border-left: 4px solid #6f42c1;
2184
+ }
2185
+
2186
+ .auto-folder-card:hover,
2187
+ .auto-folder-card.is-open {
2188
+ border-color: #adb5bd;
2189
+ }
2190
+
2191
+ .user-folder-card {
2192
+ border: 1.5px solid #e9ecef;
2193
+ border-left: 4px solid #f0ad4e;
2194
+ border-radius: 12px;
2195
+ background: #fff;
2196
+ overflow: hidden;
2197
+ transition: border-color .18s;
2198
+ }
2199
+
2200
+ .user-folder-card:hover,
2201
+ .user-folder-card.is-open {
2202
+ border-color: #adb5bd;
2203
+ }
2204
+
2205
+ .af-header {
2206
+ display: flex;
871
2207
  align-items: center;
2208
+ gap: 14px;
2209
+ padding: 14px 18px;
2210
+ cursor: pointer;
2211
+ user-select: none;
2212
+ transition: background .15s;
2213
+ }
2214
+
2215
+ .af-header:hover {
2216
+ background: #f8f9fa;
2217
+ }
2218
+
2219
+ .af-icon-wrap {
2220
+ position: relative;
2221
+ flex-shrink: 0;
2222
+ }
2223
+
2224
+ .af-folder-icon {
2225
+ font-size: 2.1rem;
2226
+ }
2227
+
2228
+ .color--drill {
2229
+ color: #17a2b8;
2230
+ }
2231
+
2232
+ .color--incident {
2233
+ color: #dc3545;
2234
+ }
2235
+
2236
+ .color--manual {
2237
+ color: #6f42c1;
2238
+ }
2239
+
2240
+ .color--user {
2241
+ color: #f0ad4e;
2242
+ }
2243
+
2244
+ .af-count-badge {
2245
+ position: absolute;
2246
+ top: -5px;
2247
+ right: -8px;
2248
+ background: #2a5298;
2249
+ color: #fff;
2250
+ font-size: 9px;
2251
+ font-weight: 700;
2252
+ border-radius: 10px;
2253
+ padding: 1px 6px;
2254
+ min-width: 18px;
872
2255
  text-align: center;
873
- width: 100%;
874
- height: 100%;
875
- justify-content: center;
2256
+ line-height: 16px;
876
2257
  }
877
2258
 
878
- .folder-name {
879
- margin-top: 8px;
880
- font-weight: 600;
881
- font-size: .85rem;
882
- word-break: break-word;
2259
+ .af-meta {
2260
+ flex: 1;
2261
+ min-width: 0;
2262
+ display: flex;
2263
+ flex-direction: column;
2264
+ gap: 2px;
2265
+ }
2266
+
2267
+ .af-ref {
2268
+ font-family: monospace;
2269
+ font-size: 13px;
2270
+ font-weight: 700;
2271
+ color: #2a5298;
2272
+ }
2273
+
2274
+ .af-title {
2275
+ font-size: 13px;
2276
+ color: #212529;
2277
+ white-space: nowrap;
883
2278
  overflow: hidden;
884
2279
  text-overflow: ellipsis;
885
- display: -webkit-box;
886
- -webkit-line-clamp: 2;
887
- -webkit-box-orient: vertical;
888
2280
  }
889
2281
 
890
- .folder-actions-overlay {
891
- position: absolute;
892
- top: 5px;
893
- right: 5px;
2282
+ .af-sub {
894
2283
  display: flex;
895
- gap: 4px;
896
- opacity: 0;
897
- transition: opacity .2s;
2284
+ align-items: center;
2285
+ gap: 8px;
2286
+ flex-wrap: wrap;
2287
+ margin-top: 1px;
898
2288
  }
899
2289
 
900
- .folder-card:hover .folder-actions-overlay {
901
- opacity: 1;
2290
+ .af-actions {
2291
+ display: flex;
2292
+ align-items: center;
2293
+ gap: 8px;
2294
+ flex-shrink: 0;
902
2295
  }
903
2296
 
904
- .btn-icon {
905
- width: 28px;
906
- height: 28px;
907
- padding: 0;
2297
+ .chevron-icon {
2298
+ font-size: .85rem;
2299
+ color: #6c757d;
2300
+ }
2301
+
2302
+ .af-contents {
2303
+ border-top: 1px solid #e9ecef;
2304
+ padding: 12px 16px;
2305
+ background: #fafbff;
2306
+ display: flex;
2307
+ flex-direction: column;
2308
+ gap: 6px;
2309
+ }
2310
+
2311
+ .folder-expand-enter-active,
2312
+ .folder-expand-leave-active {
2313
+ transition: all .2s ease;
2314
+ overflow: hidden;
2315
+ }
2316
+
2317
+ .folder-expand-enter-from,
2318
+ .folder-expand-leave-to {
2319
+ opacity: 0;
2320
+ max-height: 0 !important;
2321
+ padding-top: 0 !important;
2322
+ padding-bottom: 0 !important;
2323
+ }
2324
+
2325
+ .ffi {
908
2326
  display: flex;
909
2327
  align-items: center;
910
- justify-content: center;
2328
+ gap: 10px;
2329
+ padding: 9px 12px;
911
2330
  background: #fff;
912
- border: 1px solid #dee2e6;
913
- border-radius: 4px;
2331
+ border: 1px solid #e9ecef;
2332
+ border-radius: 8px;
2333
+ transition: border-color .15s;
2334
+ }
2335
+
2336
+ .ffi:hover {
2337
+ border-color: #adb5bd;
2338
+ }
2339
+
2340
+ .ffi--report {
2341
+ border-color: #c5d0ee;
2342
+ background: #f5f7ff;
914
2343
  cursor: pointer;
915
2344
  }
916
2345
 
917
- .btn-icon:hover {
918
- background: #f8f9fa;
2346
+ .ffi--report:hover {
919
2347
  border-color: #2a5298;
920
2348
  }
921
2349
 
922
- .btn-icon-danger:hover {
923
- border-color: #dc3545;
2350
+ .ffi-icon {
2351
+ width: 34px;
2352
+ height: 34px;
2353
+ border-radius: 7px;
2354
+ display: flex;
2355
+ align-items: center;
2356
+ justify-content: center;
2357
+ font-size: 1.2rem;
2358
+ flex-shrink: 0;
2359
+ }
2360
+
2361
+ .ffi-icon--pdf {
2362
+ background: #ffeaea;
924
2363
  color: #dc3545;
925
2364
  }
926
2365
 
927
- .btn-icon i {
928
- font-size: .82rem;
2366
+ .ffi-icon--img {
2367
+ background: #e8f5e9;
2368
+ color: #28a745;
2369
+ }
2370
+
2371
+ .ffi-icon--generic {
2372
+ background: #e9ecef;
2373
+ color: #6c757d;
2374
+ }
2375
+
2376
+ .ffi-meta {
2377
+ flex: 1;
2378
+ min-width: 0;
2379
+ display: flex;
2380
+ flex-direction: column;
2381
+ gap: 1px;
2382
+ }
2383
+
2384
+ .ffi-name {
2385
+ font-size: 13px;
2386
+ font-weight: 600;
2387
+ color: #212529;
2388
+ white-space: nowrap;
2389
+ overflow: hidden;
2390
+ text-overflow: ellipsis;
2391
+ }
2392
+
2393
+ .ffi-sub {
2394
+ font-size: 11px;
2395
+ color: #6c757d;
929
2396
  }
930
2397
 
931
- /* ── Expanded Folder ── */
932
- .folder-modal-overlay {
933
- position: fixed;
934
- inset: 0;
935
- background: rgba(0, 0, 0, .5);
936
- z-index: 1000;
2398
+ .ffi-acts {
937
2399
  display: flex;
938
- align-items: center;
939
- justify-content: center;
2400
+ gap: 5px;
2401
+ flex-shrink: 0;
940
2402
  }
941
2403
 
942
- .folder-expanded {
943
- background: #fff;
944
- border-radius: 12px;
945
- width: 90%;
946
- max-width: 700px;
947
- max-height: 80vh;
948
- overflow: hidden;
2404
+ .ffi-empty {
2405
+ padding: 8px 2px;
2406
+ font-size: 12px;
2407
+ color: #6c757d;
949
2408
  display: flex;
950
- flex-direction: column;
951
- box-shadow: 0 10px 40px rgba(0, 0, 0, .3);
2409
+ align-items: center;
952
2410
  }
953
2411
 
954
- .expanded-header {
2412
+ .af-zip-row {
955
2413
  display: flex;
956
- justify-content: space-between;
957
2414
  align-items: center;
958
- padding: 18px 24px;
959
- border-bottom: 2px solid #f0f0f0;
960
- background: #f8f9fa;
2415
+ gap: 12px;
2416
+ padding-top: 8px;
2417
+ border-top: 1px dashed #dee2e6;
2418
+ margin-top: 2px;
961
2419
  }
962
2420
 
963
- .expanded-header strong {
964
- font-size: 1.1rem;
2421
+ .af-zip-hint {
2422
+ font-size: 11px;
2423
+ color: #6c757d;
2424
+ font-family: monospace;
965
2425
  }
966
2426
 
967
- .btn-close-expanded {
968
- background: transparent;
969
- border: none;
970
- font-size: 1.1rem;
971
- cursor: pointer;
972
- padding: 5px 8px;
973
- color: #666;
2427
+ .mini-badge {
2428
+ font-size: 9px;
2429
+ font-weight: 700;
2430
+ padding: 2px 7px;
974
2431
  border-radius: 4px;
975
- transition: color .2s;
976
- }
977
-
978
- .btn-close-expanded:hover {
979
- color: #dc3545;
980
- }
981
-
982
- .files-list {
983
- padding: 18px;
984
- max-height: 55vh;
985
- overflow-y: auto;
986
- }
987
-
988
- .file-item {
989
- padding: 10px 14px;
990
- background: #f8f9fa;
991
- border-radius: 6px;
992
- margin-bottom: 8px;
2432
+ text-transform: uppercase;
2433
+ flex-shrink: 0;
993
2434
  }
994
2435
 
995
- .empty-folder-message {
996
- padding: 40px 20px;
997
- text-align: center;
998
- color: #6c757d;
2436
+ .mini-badge.drill {
2437
+ background: #d1ecf1;
2438
+ color: #0c5460;
999
2439
  }
1000
2440
 
1001
- .empty-folder-message i {
1002
- font-size: 2.5rem;
1003
- color: #dee2e6;
2441
+ .mini-badge.incident {
2442
+ background: #f8d7da;
2443
+ color: #721c24;
1004
2444
  }
1005
2445
 
1006
- .empty-folder-message p {
1007
- margin: 12px 0;
2446
+ .mini-badge.manual {
2447
+ background: #ede9f6;
2448
+ color: #383d72;
1008
2449
  }
1009
2450
 
1010
- /* ── Modals ── */
2451
+ /* ─── Modals ─── */
1011
2452
  .modal {
1012
2453
  display: none;
1013
2454
  position: fixed;
@@ -1040,11 +2481,12 @@ export default {
1040
2481
  }
1041
2482
 
1042
2483
  .modal-header {
1043
- padding: 18px 22px;
2484
+ padding: 16px 22px;
1044
2485
  border-bottom: 1px solid #dee2e6;
1045
2486
  display: flex;
1046
2487
  justify-content: space-between;
1047
2488
  align-items: center;
2489
+ gap: 10px;
1048
2490
  }
1049
2491
 
1050
2492
  .modal-body {
@@ -1059,6 +2501,7 @@ export default {
1059
2501
  display: flex;
1060
2502
  justify-content: flex-end;
1061
2503
  gap: 10px;
2504
+ align-items: center;
1062
2505
  }
1063
2506
 
1064
2507
  .rpt-modal-header {
@@ -1066,19 +2509,52 @@ export default {
1066
2509
  color: #fff;
1067
2510
  }
1068
2511
 
2512
+ .modal-type-toggle {
2513
+ display: flex;
2514
+ background: rgba(255, 255, 255, .15);
2515
+ border-radius: 6px;
2516
+ padding: 3px;
2517
+ gap: 2px;
2518
+ margin-left: auto;
2519
+ margin-right: 10px;
2520
+ }
2521
+
2522
+ .type-toggle-btn {
2523
+ background: transparent;
2524
+ border: none;
2525
+ color: rgba(255, 255, 255, .7);
2526
+ font-size: 12px;
2527
+ font-weight: 600;
2528
+ padding: 4px 12px;
2529
+ border-radius: 4px;
2530
+ cursor: pointer;
2531
+ transition: all .2s;
2532
+ }
2533
+
2534
+ .type-toggle-btn.active {
2535
+ background: #fff;
2536
+ color: #2a5298;
2537
+ }
2538
+
2539
+ .type-toggle-btn:hover:not(.active) {
2540
+ color: #fff;
2541
+ background: rgba(255, 255, 255, .2);
2542
+ }
2543
+
1069
2544
  .btn-close {
1070
2545
  background: transparent;
1071
2546
  border: none;
1072
2547
  font-size: 1.4rem;
1073
2548
  cursor: pointer;
1074
2549
  opacity: .6;
2550
+ color: inherit;
1075
2551
  }
1076
2552
 
1077
2553
  .btn-close:hover {
1078
2554
  opacity: 1;
1079
2555
  }
1080
2556
 
1081
- /* ── Report Form ── */
2557
+ /* ─── Report Form ─── */
1082
2558
  .rpt-entity-box {
1083
2559
  background: #f8f9fa;
1084
2560
  border: 1px solid #e0e0e0;
@@ -1095,11 +2571,12 @@ export default {
1095
2571
  }
1096
2572
 
1097
2573
  .entity-chip {
1098
- font-size: 10px;
2574
+ font-size: 9px;
1099
2575
  font-weight: 700;
1100
- padding: 2px 8px;
2576
+ padding: 2px 7px;
1101
2577
  border-radius: 4px;
1102
2578
  text-transform: uppercase;
2579
+ flex-shrink: 0;
1103
2580
  }
1104
2581
 
1105
2582
  .entity-chip.drill {
@@ -1112,6 +2589,11 @@ export default {
1112
2589
  color: #721c24;
1113
2590
  }
1114
2591
 
2592
+ .entity-chip.manual {
2593
+ background: #ede9f6;
2594
+ color: #383d72;
2595
+ }
2596
+
1115
2597
  .input-row {
1116
2598
  display: grid;
1117
2599
  grid-template-columns: 1fr 1fr;
@@ -1131,6 +2613,13 @@ export default {
1131
2613
  color: #495057;
1132
2614
  }
1133
2615
 
2616
+ .form-hint {
2617
+ font-size: 10px;
2618
+ font-weight: 400;
2619
+ color: #adb5bd;
2620
+ margin-left: 8px;
2621
+ }
2622
+
1134
2623
  .form-control {
1135
2624
  padding: 9px 12px;
1136
2625
  border: 1px solid #ced4da;
@@ -1138,6 +2627,7 @@ export default {
1138
2627
  font-size: 14px;
1139
2628
  transition: border-color .2s;
1140
2629
  width: 100%;
2630
+ box-sizing: border-box;
1141
2631
  }
1142
2632
 
1143
2633
  .form-control:focus {
@@ -1155,11 +2645,10 @@ select.form-control {
1155
2645
  cursor: pointer;
1156
2646
  }
1157
2647
 
1158
- /* File drop zone */
1159
2648
  .file-drop-zone {
1160
2649
  border: 2px dashed #ced4da;
1161
2650
  border-radius: 8px;
1162
- padding: 20px;
2651
+ padding: 18px;
1163
2652
  text-align: center;
1164
2653
  cursor: pointer;
1165
2654
  transition: border-color .2s;
@@ -1183,11 +2672,23 @@ select.form-control {
1183
2672
  border-radius: 5px;
1184
2673
  padding: 3px 10px;
1185
2674
  font-size: 12px;
1186
- display: flex;
2675
+ display: inline-flex;
1187
2676
  align-items: center;
1188
2677
  gap: 5px;
1189
2678
  }
1190
2679
 
2680
+ .attached-chip--link {
2681
+ text-decoration: none;
2682
+ color: #2a5298;
2683
+ background: #eef2ff;
2684
+ border: 1px solid #c5d0ee;
2685
+ transition: background .15s;
2686
+ }
2687
+
2688
+ .attached-chip--link:hover {
2689
+ background: #dce4f8;
2690
+ }
2691
+
1191
2692
  .attached-chip button {
1192
2693
  background: none;
1193
2694
  border: none;
@@ -1197,7 +2698,19 @@ select.form-control {
1197
2698
  font-size: 11px;
1198
2699
  }
1199
2700
 
1200
- /* ── Report Viewer ── */
2701
+ .zip-hint {
2702
+ background: #eef2ff;
2703
+ border: 1px solid #c5d0ee;
2704
+ border-radius: 7px;
2705
+ padding: 8px 14px;
2706
+ font-size: 12px;
2707
+ color: #2a5298;
2708
+ display: flex;
2709
+ align-items: center;
2710
+ margin-top: -6px;
2711
+ }
2712
+
2713
+ /* ─── Report Viewer ─── */
1201
2714
  .view-grid {
1202
2715
  display: grid;
1203
2716
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
@@ -1255,7 +2768,7 @@ select.form-control {
1255
2768
  white-space: pre-wrap;
1256
2769
  }
1257
2770
 
1258
- /* ── Buttons ── */
2771
+ /* ─── Buttons ─── */
1259
2772
  .btn {
1260
2773
  padding: 9px 18px;
1261
2774
  border: none;
@@ -1301,39 +2814,46 @@ select.form-control {
1301
2814
  background: #5a6268;
1302
2815
  }
1303
2816
 
2817
+ .btn-danger {
2818
+ background: #dc3545;
2819
+ color: #fff;
2820
+ }
2821
+
2822
+ .btn-danger:hover:not(:disabled) {
2823
+ background: #c82333;
2824
+ }
2825
+
2826
+ .btn-warning {
2827
+ background: #f0ad4e;
2828
+ color: #664d03;
2829
+ }
2830
+
2831
+ .btn-warning:hover {
2832
+ background: #e0972e;
2833
+ }
2834
+
1304
2835
  .btn-outline-primary {
1305
2836
  background: transparent;
1306
2837
  border: 1.5px solid #2a5298;
1307
2838
  color: #2a5298;
1308
2839
  }
1309
2840
 
1310
- .btn-outline-primary:hover {
2841
+ .btn-outline-primary:hover:not(:disabled) {
1311
2842
  background: #2a5298;
1312
2843
  color: #fff;
1313
2844
  }
1314
2845
 
1315
- .btn-outline-danger {
2846
+ .btn-outline-secondary {
1316
2847
  background: transparent;
1317
- border: 1.5px solid #dc3545;
1318
- color: #dc3545;
1319
- padding: 4px 10px;
1320
- font-size: 12px;
2848
+ border: 1.5px solid #6c757d;
2849
+ color: #6c757d;
1321
2850
  }
1322
2851
 
1323
- .btn-outline-danger:hover {
1324
- background: #dc3545;
2852
+ .btn-outline-secondary:hover:not(:disabled) {
2853
+ background: #6c757d;
1325
2854
  color: #fff;
1326
2855
  }
1327
2856
 
1328
- .btn-warning {
1329
- background: #f0ad4e;
1330
- color: #664d03;
1331
- }
1332
-
1333
- .btn-warning:hover {
1334
- background: #e0972e;
1335
- }
1336
-
1337
2857
  .btn-link {
1338
2858
  background: none;
1339
2859
  border: none;
@@ -1348,6 +2868,51 @@ select.form-control {
1348
2868
  font-size: 12px;
1349
2869
  }
1350
2870
 
2871
+ /* ─── Loading / Spinners ─── */
2872
+ .loading-container {
2873
+ display: flex;
2874
+ flex-direction: column;
2875
+ align-items: center;
2876
+ padding: 40px 20px;
2877
+ }
2878
+
2879
+ .spinner {
2880
+ border: 4px solid #f3f3f3;
2881
+ border-top: 4px solid #2a5298;
2882
+ border-radius: 50%;
2883
+ width: 40px;
2884
+ height: 40px;
2885
+ animation: spin 1s linear infinite;
2886
+ margin-bottom: 14px;
2887
+ }
2888
+
2889
+ .spinner-sm {
2890
+ display: inline-block;
2891
+ width: 14px;
2892
+ height: 14px;
2893
+ border: 2px solid rgba(255, 255, 255, .4);
2894
+ border-top-color: #fff;
2895
+ border-radius: 50%;
2896
+ animation: spin .7s linear infinite;
2897
+ }
2898
+
2899
+ .spinner-xs {
2900
+ display: inline-block;
2901
+ width: 11px;
2902
+ height: 11px;
2903
+ border: 2px solid rgba(0, 0, 0, .15);
2904
+ border-top-color: #333;
2905
+ border-radius: 50%;
2906
+ animation: spin .7s linear infinite;
2907
+ }
2908
+
2909
+ @keyframes spin {
2910
+ to {
2911
+ transform: rotate(360deg);
2912
+ }
2913
+ }
2914
+
2915
+ /* ─── Utilities ─── */
1351
2916
  .fw-bold {
1352
2917
  font-weight: 700;
1353
2918
  }
@@ -1364,12 +2929,20 @@ select.form-control {
1364
2929
  margin-right: 8px;
1365
2930
  }
1366
2931
 
2932
+ .me-auto {
2933
+ margin-right: auto;
2934
+ }
2935
+
2936
+ .ms-1 {
2937
+ margin-left: 4px;
2938
+ }
2939
+
1367
2940
  .ms-2 {
1368
2941
  margin-left: 8px;
1369
2942
  }
1370
2943
 
1371
- .ms-auto {
1372
- margin-left: auto;
2944
+ .mt-2 {
2945
+ margin-top: 8px;
1373
2946
  }
1374
2947
 
1375
2948
  .mt-3 {
@@ -1380,18 +2953,10 @@ select.form-control {
1380
2953
  margin-top: 24px;
1381
2954
  }
1382
2955
 
1383
- .mt-1 {
1384
- margin-top: 4px;
1385
- }
1386
-
1387
2956
  .mb-0 {
1388
2957
  margin-bottom: 0;
1389
2958
  }
1390
2959
 
1391
- .mb-3 {
1392
- margin-bottom: 16px;
1393
- }
1394
-
1395
2960
  .text-muted {
1396
2961
  color: #6c757d !important;
1397
2962
  }
@@ -1404,6 +2969,10 @@ select.form-control {
1404
2969
  color: #28a745 !important;
1405
2970
  }
1406
2971
 
2972
+ .text-primary {
2973
+ color: #2a5298 !important;
2974
+ }
2975
+
1407
2976
  .text-warning {
1408
2977
  color: #f0ad4e !important;
1409
2978
  }
@@ -1412,8 +2981,8 @@ select.form-control {
1412
2981
  display: flex;
1413
2982
  }
1414
2983
 
1415
- .align-items-center {
1416
- align-items: center;
2984
+ .gap-2 {
2985
+ gap: 8px;
1417
2986
  }
1418
2987
 
1419
2988
  .justify-content-between {
@@ -1428,8 +2997,141 @@ select.form-control {
1428
2997
  padding: 60px 20px;
1429
2998
  }
1430
2999
 
1431
- .empty-state h5 {
1432
- margin-top: 14px;
3000
+ /* ─── Incident Fields Box ─── */
3001
+ .incident-fields-box {
3002
+ background: #fff8f5;
3003
+ border: 1.5px solid #f5d0c0;
3004
+ border-radius: 10px;
3005
+ padding: 14px 16px;
3006
+ margin-bottom: 14px;
3007
+ display: flex;
3008
+ flex-direction: column;
3009
+ gap: 0;
3010
+ }
3011
+
3012
+ .incident-fields-title {
3013
+ font-size: 13px;
3014
+ font-weight: 700;
3015
+ color: #7b2d1a;
3016
+ margin-bottom: 14px;
3017
+ display: flex;
3018
+ align-items: center;
3019
+ gap: 6px;
3020
+ }
3021
+
3022
+ .field-hint {
3023
+ font-size: 11px;
3024
+ color: #adb5bd;
3025
+ margin-bottom: 4px;
3026
+ margin-top: -2px;
3027
+ }
3028
+
3029
+ /* ─── Drill Checklist ─── */
3030
+ .drill-checklist-box {
3031
+ background: #f8faff;
3032
+ border: 1.5px solid #d0daf0;
3033
+ border-radius: 10px;
3034
+ padding: 14px 16px;
3035
+ margin-bottom: 14px;
3036
+ }
3037
+
3038
+ .drill-checklist-title {
3039
+ font-size: 13px;
3040
+ font-weight: 700;
3041
+ color: #1e3c72;
3042
+ margin-bottom: 10px;
3043
+ display: flex;
3044
+ align-items: center;
3045
+ gap: 6px;
3046
+ }
3047
+
3048
+ .checklist-hint {
3049
+ font-size: 11px;
3050
+ font-weight: 400;
3051
+ color: #adb5bd;
3052
+ margin-left: 4px;
3053
+ }
3054
+
3055
+ .checklist-grid {
3056
+ display: flex;
3057
+ flex-direction: column;
3058
+ gap: 6px;
3059
+ }
3060
+
3061
+ .checklist-item {
3062
+ display: flex;
3063
+ align-items: center;
3064
+ gap: 10px;
3065
+ padding: 8px 12px;
3066
+ border-radius: 7px;
3067
+ border: 1.5px solid #e2e8f0;
3068
+ background: #fff;
3069
+ cursor: pointer;
3070
+ transition: all .18s;
3071
+ user-select: none;
3072
+ }
3073
+
3074
+ .checklist-item:hover {
3075
+ border-color: #2a5298;
3076
+ background: #f5f8ff;
3077
+ }
3078
+
3079
+ .checklist-item.checked {
3080
+ border-color: #28a745;
3081
+ background: #f0fff4;
3082
+ }
3083
+
3084
+ .checklist-checkbox input[type="checkbox"] {
3085
+ display: none;
3086
+ }
3087
+
3088
+ .custom-check {
3089
+ width: 22px;
3090
+ height: 22px;
3091
+ border-radius: 5px;
3092
+ border: 2px solid #ced4da;
3093
+ display: flex;
3094
+ align-items: center;
3095
+ justify-content: center;
3096
+ flex-shrink: 0;
3097
+ font-size: 12px;
3098
+ transition: all .18s;
3099
+ }
3100
+
3101
+ .checklist-item.checked .custom-check {
3102
+ background: #28a745;
3103
+ border-color: #28a745;
3104
+ color: #fff;
3105
+ }
3106
+
3107
+ .checklist-item:not(.checked) .custom-check {
3108
+ background: #fff8f8;
3109
+ border-color: #dc3545;
3110
+ color: #dc3545;
3111
+ }
3112
+
3113
+ .checklist-q {
3114
+ flex: 1;
3115
+ font-size: 13px;
3116
+ color: #212529;
3117
+ }
3118
+
3119
+ .checklist-answer {
3120
+ font-size: 10px;
3121
+ font-weight: 700;
3122
+ padding: 2px 8px;
3123
+ border-radius: 4px;
3124
+ flex-shrink: 0;
3125
+ }
3126
+
3127
+ .answer-yes {
3128
+ background: #d4edda;
3129
+ color: #155724;
3130
+ }
3131
+
3132
+ .answer-no {
3133
+ background: #f8d7da;
3134
+ color: #721c24;
1433
3135
  }
1434
3136
 
1435
3137
  @media (max-width: 640px) {
@@ -1437,13 +3139,31 @@ select.form-control {
1437
3139
  grid-template-columns: 1fr;
1438
3140
  }
1439
3141
 
1440
- .header-actions {
1441
- gap: 6px;
3142
+ .btn-group-split {
3143
+ gap: 5px;
1442
3144
  }
1443
3145
 
1444
3146
  .btn {
1445
3147
  padding: 8px 12px;
1446
3148
  font-size: 12px;
1447
3149
  }
3150
+
3151
+ .tab-btn {
3152
+ padding: 8px 10px;
3153
+ font-size: 12px;
3154
+ }
3155
+
3156
+ .modal-type-toggle {
3157
+ display: none;
3158
+ }
3159
+
3160
+ .rc-folder-pill {
3161
+ display: none;
3162
+ }
3163
+
3164
+ .af-header {
3165
+ padding: 12px 14px;
3166
+ gap: 10px;
3167
+ }
1448
3168
  }
1449
3169
  </style>