oceanhelm 0.0.11 → 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.
@@ -1,612 +1,3169 @@
1
1
  <template>
2
2
  <div class="reports-container">
3
- <div class="page-header d-flex justify-content-between align-items-center">
4
- <h4 style="margin-left: 20px;">Reports</h4>
5
- <div class="d-flex">
6
- <button class="btn btn-primary" @click="showNewFolderModal = true">
7
- <i class="bi bi-folder-plus"></i> New Folder
8
- </button>
9
- </div>
10
- </div>
11
3
 
12
- <!-- Folders List -->
13
- <div class="folders-section" v-if="folders.length > 0">
14
- <div class="folder-card"
15
- v-for="folder in folders"
16
- :key="folder.id"
17
- @mouseenter="showPreview(folder.id)"
18
- @mouseleave="hidePreview">
19
- <div class="folder-content" @click="toggleFolder(folder.id)">
20
- <i class="bi bi-folder-fill text-warning" style="font-size: 2.5rem;"></i>
21
- <div class="folder-name">{{ folder.name }}</div>
22
- <small class="text-muted">{{ folder.files.length }} file(s)</small>
4
+ <!-- ── Missing Reports Alert Banner ── -->
5
+ <transition name="slide-down">
6
+ <div class="missing-banner" v-if="!bannerDismissed && (pendingContext || missingReports.length > 0)">
7
+ <div class="missing-banner-inner">
8
+ <div class="missing-icon"><i class="bi bi-exclamation-triangle-fill"></i></div>
9
+ <div class="missing-content">
10
+ <strong v-if="pendingContext">
11
+ Action Required — Report for
12
+ <span class="ref-chip">{{ pendingContext.entity_ref }}</span> must be submitted
13
+ </strong>
14
+ <strong v-else>
15
+ {{ missingReports.length }} report{{ missingReports.length > 1 ? 's' : '' }} awaiting
16
+ submission
17
+ </strong>
18
+ <p v-if="pendingContext">{{ pendingContext.title }}</p>
19
+ </div>
20
+ <button class="btn btn-warning btn-sm fw-bold" @click="openReportForm('qhse')">
21
+ <i class="bi bi-pencil-square me-1"></i> Submit Report
22
+ </button>
23
+ <button class="btn-dismiss" @click="dismissBanner" title="Dismiss">
24
+ <i class="bi bi-x-lg"></i>
25
+ </button>
23
26
  </div>
24
-
25
- <div class="folder-actions-overlay">
26
- <button class="btn btn-sm btn-icon" @click.stop="selectFolder(folder)" title="Add Files">
27
- <i class="bi bi-file-earmark-plus"></i>
27
+ </div>
28
+ </transition>
29
+
30
+ <!-- ── Page Header ── -->
31
+ <div class="page-header d-flex justify-content-between align-items-center">
32
+ <div>
33
+ <h4 class="page-title">Reports</h4>
34
+ <p class="page-subtitle">Document Management &amp; QHSE Reports</p>
35
+ </div>
36
+ <div class="header-actions">
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
28
40
  </button>
29
- <button class="btn btn-sm btn-icon" @click.stop="deleteFolder(folder.id)" title="Delete Folder">
30
- <i class="bi bi-trash"></i>
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
31
46
  </button>
32
47
  </div>
33
48
  </div>
34
49
  </div>
35
50
 
36
- <!-- Empty State -->
37
- <div class="empty-state text-center" v-else>
38
- <i class="bi bi-folder2-open" style="font-size: 4rem; color: #ccc;"></i>
39
- <h5 class="mt-3 text-muted">No folders yet</h5>
40
- <p class="text-muted">Create a folder to start organizing your reports</p>
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>
41
72
  </div>
42
73
 
43
- <!-- Expanded Folder Modal (Click to Open) -->
44
- <div class="folder-modal-overlay" v-if="expandedFolders.length > 0" @click="closeAllFolders">
45
- <div class="folder-expanded" @click.stop>
46
- <div class="expanded-header">
47
- <strong>{{ getExpandedFolder().name }}</strong>
48
- <button class="btn btn-sm btn-close-expanded" @click="closeAllFolders">
49
- <i class="bi bi-x-lg"></i>
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>
92
+ <span class="rc-title">{{ report.title }}</span>
93
+ <span class="rc-meta">
94
+ <i class="bi bi-person-fill me-1"></i>{{ report.submittedBy }}
95
+ &nbsp;·&nbsp;
96
+ <i class="bi bi-calendar3 me-1"></i>{{ formatDate(report.submittedAt) }}
97
+ </span>
98
+ </div>
99
+
100
+ <div class="rc-files" v-if="getFilesForReport(report.id).length > 0">
101
+ <i class="bi bi-paperclip"></i> {{ getFilesForReport(report.id).length }}
102
+ </div>
103
+
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 }}
50
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>
51
121
  </div>
52
-
53
- <div class="files-list" v-if="getExpandedFolder().files.length > 0">
54
- <div class="file-item d-flex justify-content-between align-items-center"
55
- v-for="file in getExpandedFolder().files" :key="file.id">
56
- <div class="file-info d-flex align-items-center">
57
- <i class="bi bi-file-earmark-text me-2"></i>
58
- <span>{{ file.name }}</span>
59
- <small class="text-muted ms-2">({{ formatFileSize(file.size) }})</small>
122
+ </div>
123
+
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>
134
+ </div>
135
+
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>
148
+ </div>
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>
60
183
  </div>
61
- <button class="btn btn-sm btn-outline-danger" @click="removeFile(getExpandedFolder().id, file.id)">
62
- <i class="bi bi-x-circle"></i>
63
- </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>
64
246
  </div>
65
247
  </div>
66
- <div v-else class="empty-folder-message">
67
- <i class="bi bi-inbox"></i>
68
- <p>No files in this folder</p>
69
- <button class="btn btn-sm btn-primary" @click="selectFolder(getExpandedFolder())">
70
- <i class="bi bi-file-earmark-plus"></i> Add Files
71
- </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>
72
314
  </div>
73
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>
74
327
  </div>
75
328
 
76
- <!-- New Folder Modal -->
329
+ <!-- ════════════════════════════════════════
330
+ MODALS
331
+ ════════════════════════════════════════ -->
332
+
333
+ <!-- New Folder -->
77
334
  <div class="modal" :class="{ 'show': showNewFolderModal }" @click.self="showNewFolderModal = false">
78
335
  <div class="modal-dialog modal-dialog-centered">
79
336
  <div class="modal-content">
80
337
  <div class="modal-header">
81
- <h5 class="modal-title">Create New Folder</h5>
338
+ <h5 class="modal-title"><i class="bi bi-folder-plus me-2"></i>Create New Folder</h5>
82
339
  <button type="button" class="btn-close" @click="showNewFolderModal = false"></button>
83
340
  </div>
84
341
  <div class="modal-body">
85
- <div class="mb-3">
86
- <label class="form-label">Folder Name</label>
87
- <input
88
- type="text"
89
- class="form-control"
90
- v-model="newFolderName"
91
- @keyup.enter="createFolder"
92
- placeholder="Enter folder name"
93
- />
342
+ <label class="form-label">Folder Name</label>
343
+ <input type="text" class="form-control" v-model="newFolderName"
344
+ @keyup.enter="requestCreateFolder" placeholder="e.g., Q1 2025 Drills" />
345
+ </div>
346
+ <div class="modal-footer">
347
+ <button type="button" class="btn btn-secondary"
348
+ @click="showNewFolderModal = false">Cancel</button>
349
+ <button type="button" class="btn btn-primary" @click="requestCreateFolder"
350
+ :disabled="!newFolderName.trim()">Create Folder</button>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ </div>
355
+
356
+ <!-- Submit Report -->
357
+ <div class="modal" :class="{ 'show': showReportModal }" @click.self="closeReportForm">
358
+ <div class="modal-dialog modal-dialog-centered modal-lg">
359
+ <div class="modal-content">
360
+ <div class="modal-header rpt-modal-header">
361
+ <h5 class="modal-title">
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' }}
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>
376
+ <button type="button" class="btn-close btn-close-white" @click="closeReportForm"></button>
377
+ </div>
378
+ <div class="modal-body">
379
+
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>
384
+ <div class="prefilled-entity" v-if="reportForm.entity_ref && reportForm.entity_type">
385
+ <span class="entity-chip" :class="reportForm.entity_type">
386
+ {{ reportForm.entity_type === 'drill' ? 'DRILL' : 'INCIDENT' }}
387
+ </span>
388
+ <strong>{{ reportForm.entity_ref }}</strong>
389
+ <span class="text-muted ms-2">{{ reportForm.entity_label }}</span>
390
+ <button class="btn btn-link btn-sm text-danger ms-auto" @click="clearEntityLink"><i
391
+ class="bi bi-x-circle"></i> Clear</button>
392
+ </div>
393
+ <div v-else>
394
+ <select class="form-control" v-model="reportForm.selectedMissing"
395
+ @change="applyMissingSelection">
396
+ <option value="">— Select a missing report (optional) —</option>
397
+ <optgroup label="Missing Drill Reports">
398
+ <option v-for="r in missingReports.filter(x => x.entity_type === 'drill')"
399
+ :key="r.entity_id" :value="JSON.stringify(r)">
400
+ {{ r.entity_ref }} — {{ r.label }}
401
+ </option>
402
+ </optgroup>
403
+ <optgroup label="Missing Incident Reports">
404
+ <option v-for="r in missingReports.filter(x => x.entity_type === 'incident')"
405
+ :key="r.entity_id" :value="JSON.stringify(r)">
406
+ {{ r.entity_ref }} — {{ r.label }}
407
+ </option>
408
+ </optgroup>
409
+ </select>
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>
471
+ </div>
472
+ </div>
473
+
474
+ <!-- Common fields -->
475
+ <div class="input-row mt-3">
476
+ <div class="form-group">
477
+ <label class="form-label fw-semibold">Report Title *</label>
478
+ <input type="text" class="form-control" v-model="reportForm.title"
479
+ :placeholder="reportForm.entity_ref ? `Report — ${reportForm.entity_ref}` : 'Enter report title'" />
480
+ </div>
481
+ <div class="form-group">
482
+ <label class="form-label fw-semibold">Submitted By *</label>
483
+ <input type="text" class="form-control" v-model="reportForm.submittedBy"
484
+ placeholder="Your name" />
485
+ </div>
486
+ </div>
487
+ <div class="input-row">
488
+ <div class="form-group">
489
+ <label class="form-label fw-semibold">Date</label>
490
+ <input type="date" class="form-control" v-model="reportForm.date" />
491
+ </div>
492
+ </div>
493
+ <div class="form-group">
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>
506
+ </div>
507
+
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>
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 -->
573
+ <div class="form-group">
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>
578
+ <div class="file-drop-zone" @click="$refs.reportFileInput.click()" @dragover.prevent
579
+ @drop.prevent="handleReportFileDrop">
580
+ <i class="bi bi-cloud-upload" style="font-size:2rem;color:#6c757d;"></i>
581
+ <p class="mb-0 mt-2 text-muted">Click or drag files here (PNG, JPG, PDF…)</p>
582
+ <div class="attached-files" v-if="reportForm.files.length > 0">
583
+ <span class="attached-chip" v-for="(f, i) in reportForm.files" :key="i">
584
+ <i class="bi me-1" :class="fileIcon(f.type)"></i>{{ f.name }}
585
+ <button @click.stop="removeReportFile(i)"><i class="bi bi-x"></i></button>
586
+ </span>
587
+ </div>
588
+ </div>
589
+ <input ref="reportFileInput" type="file" multiple style="display:none"
590
+ @change="handleReportFileAttach" />
591
+ </div>
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>
602
+ </div>
603
+ <div class="modal-footer">
604
+ <button type="button" class="btn btn-secondary" @click="closeReportForm">Cancel</button>
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>
609
+ </button>
610
+ </div>
611
+ </div>
612
+ </div>
613
+ </div>
614
+
615
+ <!-- Report Viewer -->
616
+ <div class="modal" :class="{ 'show': showViewModal }" @click.self="closeViewer">
617
+ <div class="modal-dialog modal-dialog-centered modal-lg">
618
+ <div class="modal-content" v-if="viewingReport">
619
+ <div class="modal-header rpt-modal-header">
620
+ <div class="d-flex align-items-center gap-2">
621
+ <span class="entity-chip me-2" :class="reportTypeClass(viewingReport)">{{
622
+ reportTypeLabel(viewingReport) }}</span>
623
+ <strong style="color:#fff">{{ viewingReport.title }}</strong>
624
+ </div>
625
+ <button type="button" class="btn-close btn-close-white" @click="closeViewer"></button>
626
+ </div>
627
+ <div class="modal-body">
628
+ <div class="view-grid">
629
+ <div class="view-item"><label>Reference</label><span class="ref-mono">{{
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>
636
+ <div class="view-item"><label>Submitted By</label><span>{{ viewingReport.submittedBy
637
+ }}</span></div>
638
+ <div class="view-item"><label>Date</label><span>{{ viewingReport.date }}</span></div>
639
+ <div class="view-item" v-if="viewingReport.vessel"><label>Vessel / Location</label><span>{{
640
+ viewingReport.vessel }}</span></div>
641
+ <div class="view-item"><label>Submitted At</label><span>{{
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>
655
+ </div>
656
+ <div class="view-section">
657
+ <label>Findings / Summary</label>
658
+ <div class="view-text">{{ viewingReport.findings }}</div>
659
+ </div>
660
+ <div class="view-section" v-if="viewingReport.correctiveActions">
661
+ <label>Corrective Actions</label>
662
+ <div class="view-text">{{ viewingReport.correctiveActions }}</div>
663
+ </div>
664
+ <div class="view-section" v-if="getFilesForReport(viewingReport.id).length > 0">
665
+ <label>Attachments</label>
666
+ <div class="attached-files mt-1">
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>
673
+ </div>
94
674
  </div>
95
675
  </div>
96
676
  <div class="modal-footer">
97
- <button type="button" class="btn btn-secondary" @click="showNewFolderModal = false">Cancel</button>
98
- <button type="button" class="btn btn-primary" @click="createFolder" :disabled="!newFolderName.trim()">
99
- Create Folder
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>
100
686
  </button>
101
687
  </div>
102
688
  </div>
103
689
  </div>
104
690
  </div>
105
691
 
106
- <!-- File Input (Hidden) -->
107
- <input
108
- ref="fileInput"
109
- type="file"
110
- multiple
111
- style="display: none;"
112
- @change="handleFileSelection"
113
- />
692
+ <!-- Hidden file input for user folders -->
693
+ <input ref="fileInput" type="file" multiple style="display:none;" @change="handleFileSelection" />
114
694
  </div>
115
695
  </template>
116
696
 
117
697
  <script>
118
698
  export default {
119
699
  name: 'Reports',
120
-
700
+
701
+ props: {
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 },
710
+ },
711
+
712
+ emits: [
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',
720
+ ],
721
+
121
722
  data() {
122
723
  return {
123
- folders: [],
724
+ activeTab: 'all',
124
725
  showNewFolderModal: false,
125
726
  newFolderName: '',
126
727
  selectedFolder: null,
127
728
  expandedFolders: [],
128
- previewFolder: null
129
- }
729
+
730
+ showReportModal: false,
731
+ showViewModal: false,
732
+ viewingReport: null,
733
+ highlightedRef: null,
734
+ bannerDismissed: false,
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(),
746
+ };
130
747
  },
131
748
 
132
- emits: [
133
- 'folder-created',
134
- 'files-added',
135
- 'file-removed',
136
- 'folder-deleted',
137
- 'data-updated'
138
- ],
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
+
772
+ watch: {
773
+ pendingContext: {
774
+ immediate: true,
775
+ handler(ctx) {
776
+ if (ctx) {
777
+ this.reportForm.entity_type = ctx.entity_type;
778
+ this.reportForm.entity_id = ctx.entity_id;
779
+ this.reportForm.entity_ref = ctx.entity_ref;
780
+ this.reportForm.entity_label = ctx.title;
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
+ }
792
+ }
793
+ },
794
+ },
795
+ highlightRef: {
796
+ immediate: true,
797
+ handler(ref) {
798
+ if (ref) {
799
+ this.highlightedRef = ref;
800
+ this.$nextTick(() => this.scrollToReport(ref));
801
+ }
802
+ },
803
+ },
804
+ isSubmitting(val, prev) {
805
+ if (prev === true && val === false) {
806
+ this.showReportModal = false;
807
+ this.resetForm();
808
+ }
809
+ },
810
+ },
139
811
 
140
812
  methods: {
141
- createFolder() {
142
- if (!this.newFolderName.trim()) return;
143
813
 
144
- const newFolder = {
145
- id: Date.now(),
146
- name: this.newFolderName.trim(),
147
- files: [],
148
- createdAt: new Date()
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 ─────────────────────────────────────────────────────────────
877
+ dismissBanner() { this.bannerDismissed = true; },
878
+
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;
890
+ this.reportForm.entity_type = this.pendingContext.entity_type;
891
+ this.reportForm.entity_id = this.pendingContext.entity_id;
892
+ this.reportForm.entity_ref = this.pendingContext.entity_ref;
893
+ this.reportForm.entity_label = this.pendingContext.title;
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
+ }
898
+ }
899
+ this.showReportModal = true;
900
+ },
901
+
902
+ closeReportForm() { this.showReportModal = false; },
903
+
904
+ clearEntityLink() {
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
+ });
910
+ },
911
+
912
+ applyMissingSelection() {
913
+ if (!this.reportForm.selectedMissing) return;
914
+ const r = JSON.parse(this.reportForm.selectedMissing);
915
+ this.reportForm.entity_type = r.entity_type;
916
+ this.reportForm.entity_id = r.entity_id;
917
+ this.reportForm.entity_ref = r.entity_ref;
918
+ this.reportForm.entity_label = r.label;
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
+ }
928
+ },
929
+
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,
938
+ title: this.reportForm.title,
939
+ submittedBy: this.reportForm.submittedBy,
940
+ vessel: this.reportForm.vessel || null,
941
+ date: this.reportForm.date,
942
+ findings: this.reportForm.findings,
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,
962
+ });
963
+ },
964
+
965
+ resetForm() { this.reportForm = this.emptyForm(); },
966
+
967
+ highlightReport(entityRef) {
968
+ this.highlightedRef = entityRef;
969
+ this.$nextTick(() => this.scrollToReport(entityRef));
970
+ setTimeout(() => { this.highlightedRef = null; }, 3000);
971
+ },
972
+
973
+ // ─── View ────────────────────────────────────────────────────────────────
974
+
975
+ viewReport(report) {
976
+ this.viewingReport = report;
977
+ this.showViewModal = true;
978
+ this.$emit('report-viewed', report);
979
+ },
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
+
992
+ scrollToReport(ref) {
993
+ this.$nextTick(() => {
994
+ const el = this.$refs['report-' + ref];
995
+ if (el) (Array.isArray(el) ? el[0] : el)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
996
+ });
997
+ },
998
+
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
+ });
1008
+ },
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
+ }
1026
+ },
1027
+
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
+ }
1041
+ },
1042
+
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
+ },
1047
+
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);
1064
+ },
1065
+
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;
149
1102
  };
150
1103
 
151
- this.folders.push(newFolder);
152
- this.$emit('folder-created', newFolder);
153
- this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData() });
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();
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 ───────────────────────────────────────────────────
154
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' });
1338
+ }
1339
+
1340
+ return doc.output('arraybuffer');
1341
+ },
1342
+
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; });
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' });
1514
+ }
1515
+
1516
+ return doc.output('arraybuffer');
1517
+ },
1518
+
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');
1613
+ },
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`);
1646
+ },
1647
+
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);
1654
+ },
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
+ });
1662
+ },
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());
155
1680
  this.newFolderName = '';
156
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);
157
1707
  },
158
1708
 
159
- selectFolder(folder) {
160
- this.selectedFolder = folder;
161
- this.$refs.fileInput.click();
162
- },
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
+ },
1720
+ };
1721
+ </script>
1722
+
1723
+ <style scoped>
1724
+ /* ─── Base ─── */
1725
+ .reports-container {
1726
+ padding: 24px;
1727
+ }
1728
+
1729
+ /* ─── Banner ─── */
1730
+ .missing-banner {
1731
+ background: linear-gradient(135deg, #fff3cd, #ffe8a1);
1732
+ border: 1.5px solid #f0ad4e;
1733
+ border-radius: 10px;
1734
+ margin-bottom: 22px;
1735
+ overflow: hidden;
1736
+ }
1737
+
1738
+ .missing-banner-inner {
1739
+ display: flex;
1740
+ align-items: center;
1741
+ gap: 14px;
1742
+ padding: 14px 18px;
1743
+ flex-wrap: wrap;
1744
+ }
1745
+
1746
+ .missing-icon {
1747
+ font-size: 22px;
1748
+ color: #856404;
1749
+ flex-shrink: 0;
1750
+ }
1751
+
1752
+ .missing-content {
1753
+ flex: 1;
1754
+ min-width: 0;
1755
+ }
1756
+
1757
+ .missing-content strong {
1758
+ font-size: 14px;
1759
+ color: #664d03;
1760
+ display: block;
1761
+ }
1762
+
1763
+ .missing-content p {
1764
+ margin: 2px 0 0;
1765
+ font-size: 12px;
1766
+ color: #856404;
1767
+ }
1768
+
1769
+ .ref-chip {
1770
+ background: #856404;
1771
+ color: #fff;
1772
+ border-radius: 4px;
1773
+ padding: 1px 8px;
1774
+ font-family: monospace;
1775
+ font-size: 12px;
1776
+ font-weight: 700;
1777
+ }
1778
+
1779
+ .btn-dismiss {
1780
+ background: none;
1781
+ border: none;
1782
+ color: #856404;
1783
+ cursor: pointer;
1784
+ font-size: 15px;
1785
+ padding: 4px 6px;
1786
+ border-radius: 4px;
1787
+ transition: background .15s;
1788
+ }
1789
+
1790
+ .btn-dismiss:hover {
1791
+ background: rgba(0, 0, 0, .08);
1792
+ }
1793
+
1794
+ .slide-down-enter-active,
1795
+ .slide-down-leave-active {
1796
+ transition: all .25s;
1797
+ }
1798
+
1799
+ .slide-down-enter-from,
1800
+ .slide-down-leave-to {
1801
+ opacity: 0;
1802
+ transform: translateY(-12px);
1803
+ }
1804
+
1805
+ /* ─── Page Header ─── */
1806
+ .page-header {
1807
+ margin-bottom: 18px;
1808
+ border-bottom: 1px solid #e0e0e0;
1809
+ padding-bottom: 16px;
1810
+ }
1811
+
1812
+ .page-title {
1813
+ font-size: 22px;
1814
+ font-weight: 700;
1815
+ color: #1e3c72;
1816
+ margin: 0;
1817
+ }
1818
+
1819
+ .page-subtitle {
1820
+ font-size: 13px;
1821
+ color: #6c757d;
1822
+ margin: 2px 0 0;
1823
+ }
1824
+
1825
+ .btn-group-split {
1826
+ display: flex;
1827
+ gap: 8px;
1828
+ flex-wrap: wrap;
1829
+ }
1830
+
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;
1852
+ display: flex;
1853
+ align-items: center;
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;
1883
+ }
1884
+
1885
+ .tab-count.manual {
1886
+ background: #e2e3f0;
1887
+ color: #383d72;
1888
+ }
1889
+
1890
+ /* ─── Report Cards ─── */
1891
+ .report-cards {
1892
+ display: flex;
1893
+ flex-direction: column;
1894
+ gap: 10px;
1895
+ margin-bottom: 24px;
1896
+ }
1897
+
1898
+ .report-card {
1899
+ display: flex;
1900
+ align-items: center;
1901
+ gap: 12px;
1902
+ background: #fff;
1903
+ border: 1.5px solid #e9ecef;
1904
+ border-radius: 10px;
1905
+ padding: 12px 16px;
1906
+ transition: all .2s;
1907
+ position: relative;
1908
+ overflow: hidden;
1909
+ }
1910
+
1911
+ .report-card:hover {
1912
+ border-color: #2a5298;
1913
+ box-shadow: 0 2px 10px rgba(42, 82, 152, .1);
1914
+ }
1915
+
1916
+ .report-card--highlighted {
1917
+ border-color: #f0ad4e !important;
1918
+ background: #fffbf0 !important;
1919
+ box-shadow: 0 0 0 3px rgba(240, 173, 78, .25) !important;
1920
+ animation: flash-highlight 1.5s ease;
1921
+ }
1922
+
1923
+ @keyframes flash-highlight {
1924
+
1925
+ 0%,
1926
+ 100% {
1927
+ background: #fffbf0
1928
+ }
1929
+
1930
+ 50% {
1931
+ background: #fff3cd
1932
+ }
1933
+ }
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
+
1955
+ .rc-badge {
1956
+ font-size: 9px;
1957
+ font-weight: 700;
1958
+ padding: 2px 7px;
1959
+ border-radius: 4px;
1960
+ text-transform: uppercase;
1961
+ white-space: nowrap;
1962
+ flex-shrink: 0;
1963
+ }
1964
+
1965
+ .rc-badge.drill {
1966
+ background: #d1ecf1;
1967
+ color: #0c5460;
1968
+ }
1969
+
1970
+ .rc-badge.incident {
1971
+ background: #f8d7da;
1972
+ color: #721c24;
1973
+ }
1974
+
1975
+ .rc-badge.manual {
1976
+ background: #ede9f6;
1977
+ color: #383d72;
1978
+ }
1979
+
1980
+ .rc-body {
1981
+ display: flex;
1982
+ flex-direction: column;
1983
+ flex: 1;
1984
+ min-width: 0;
1985
+ cursor: pointer;
1986
+ }
1987
+
1988
+ .rc-ref {
1989
+ font-family: monospace;
1990
+ font-size: 12px;
1991
+ color: #2a5298;
1992
+ font-weight: 700;
1993
+ }
1994
+
1995
+ .rc-title {
1996
+ font-size: 14px;
1997
+ color: #212529;
1998
+ white-space: nowrap;
1999
+ overflow: hidden;
2000
+ text-overflow: ellipsis;
2001
+ }
2002
+
2003
+ .rc-meta {
2004
+ font-size: 11px;
2005
+ color: #6c757d;
2006
+ margin-top: 2px;
2007
+ }
2008
+
2009
+ .rc-files {
2010
+ font-size: 11px;
2011
+ color: #6c757d;
2012
+ display: flex;
2013
+ align-items: center;
2014
+ gap: 4px;
2015
+ flex-shrink: 0;
2016
+ }
2017
+
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;
2033
+ }
2034
+
2035
+ .rc-folder-pill.drill {
2036
+ background: #e8f7fa;
2037
+ border-color: #17a2b8;
2038
+ color: #0c5460;
2039
+ }
2040
+
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;
2068
+ background: #fff;
2069
+ cursor: pointer;
2070
+ display: flex;
2071
+ align-items: center;
2072
+ justify-content: center;
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;
2082
+ }
2083
+
2084
+ .rc-btn--view:hover:not(:disabled) {
2085
+ background: #2a5298;
2086
+ color: #fff;
2087
+ border-color: #2a5298;
2088
+ }
2089
+
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 {
2161
+ display: flex;
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;
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;
2255
+ text-align: center;
2256
+ line-height: 16px;
2257
+ }
2258
+
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;
2278
+ overflow: hidden;
2279
+ text-overflow: ellipsis;
2280
+ }
2281
+
2282
+ .af-sub {
2283
+ display: flex;
2284
+ align-items: center;
2285
+ gap: 8px;
2286
+ flex-wrap: wrap;
2287
+ margin-top: 1px;
2288
+ }
2289
+
2290
+ .af-actions {
2291
+ display: flex;
2292
+ align-items: center;
2293
+ gap: 8px;
2294
+ flex-shrink: 0;
2295
+ }
2296
+
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 {
2326
+ display: flex;
2327
+ align-items: center;
2328
+ gap: 10px;
2329
+ padding: 9px 12px;
2330
+ background: #fff;
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;
2343
+ cursor: pointer;
2344
+ }
2345
+
2346
+ .ffi--report:hover {
2347
+ border-color: #2a5298;
2348
+ }
2349
+
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;
2363
+ color: #dc3545;
2364
+ }
2365
+
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;
2396
+ }
2397
+
2398
+ .ffi-acts {
2399
+ display: flex;
2400
+ gap: 5px;
2401
+ flex-shrink: 0;
2402
+ }
2403
+
2404
+ .ffi-empty {
2405
+ padding: 8px 2px;
2406
+ font-size: 12px;
2407
+ color: #6c757d;
2408
+ display: flex;
2409
+ align-items: center;
2410
+ }
2411
+
2412
+ .af-zip-row {
2413
+ display: flex;
2414
+ align-items: center;
2415
+ gap: 12px;
2416
+ padding-top: 8px;
2417
+ border-top: 1px dashed #dee2e6;
2418
+ margin-top: 2px;
2419
+ }
2420
+
2421
+ .af-zip-hint {
2422
+ font-size: 11px;
2423
+ color: #6c757d;
2424
+ font-family: monospace;
2425
+ }
2426
+
2427
+ .mini-badge {
2428
+ font-size: 9px;
2429
+ font-weight: 700;
2430
+ padding: 2px 7px;
2431
+ border-radius: 4px;
2432
+ text-transform: uppercase;
2433
+ flex-shrink: 0;
2434
+ }
2435
+
2436
+ .mini-badge.drill {
2437
+ background: #d1ecf1;
2438
+ color: #0c5460;
2439
+ }
2440
+
2441
+ .mini-badge.incident {
2442
+ background: #f8d7da;
2443
+ color: #721c24;
2444
+ }
2445
+
2446
+ .mini-badge.manual {
2447
+ background: #ede9f6;
2448
+ color: #383d72;
2449
+ }
2450
+
2451
+ /* ─── Modals ─── */
2452
+ .modal {
2453
+ display: none;
2454
+ position: fixed;
2455
+ z-index: 1050;
2456
+ inset: 0;
2457
+ background: rgba(0, 0, 0, .5);
2458
+ align-items: center;
2459
+ justify-content: center;
2460
+ }
2461
+
2462
+ .modal.show {
2463
+ display: flex;
2464
+ }
2465
+
2466
+ .modal-dialog {
2467
+ max-width: 500px;
2468
+ width: 95%;
2469
+ margin: 1.5rem auto;
2470
+ }
2471
+
2472
+ .modal-dialog.modal-lg {
2473
+ max-width: 780px;
2474
+ }
2475
+
2476
+ .modal-content {
2477
+ background: #fff;
2478
+ border-radius: 12px;
2479
+ box-shadow: 0 10px 40px rgba(0, 0, 0, .25);
2480
+ overflow: hidden;
2481
+ }
2482
+
2483
+ .modal-header {
2484
+ padding: 16px 22px;
2485
+ border-bottom: 1px solid #dee2e6;
2486
+ display: flex;
2487
+ justify-content: space-between;
2488
+ align-items: center;
2489
+ gap: 10px;
2490
+ }
2491
+
2492
+ .modal-body {
2493
+ padding: 22px;
2494
+ max-height: 70vh;
2495
+ overflow-y: auto;
2496
+ }
2497
+
2498
+ .modal-footer {
2499
+ padding: 14px 22px;
2500
+ border-top: 1px solid #dee2e6;
2501
+ display: flex;
2502
+ justify-content: flex-end;
2503
+ gap: 10px;
2504
+ align-items: center;
2505
+ }
2506
+
2507
+ .rpt-modal-header {
2508
+ background: linear-gradient(135deg, #1e3c72, #2a5298);
2509
+ color: #fff;
2510
+ }
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
+
2544
+ .btn-close {
2545
+ background: transparent;
2546
+ border: none;
2547
+ font-size: 1.4rem;
2548
+ cursor: pointer;
2549
+ opacity: .6;
2550
+ color: inherit;
2551
+ }
2552
+
2553
+ .btn-close:hover {
2554
+ opacity: 1;
2555
+ }
2556
+
2557
+ /* ─── Report Form ─── */
2558
+ .rpt-entity-box {
2559
+ background: #f8f9fa;
2560
+ border: 1px solid #e0e0e0;
2561
+ border-radius: 8px;
2562
+ padding: 14px 16px;
2563
+ margin-bottom: 4px;
2564
+ }
2565
+
2566
+ .prefilled-entity {
2567
+ display: flex;
2568
+ align-items: center;
2569
+ gap: 10px;
2570
+ flex-wrap: wrap;
2571
+ }
2572
+
2573
+ .entity-chip {
2574
+ font-size: 9px;
2575
+ font-weight: 700;
2576
+ padding: 2px 7px;
2577
+ border-radius: 4px;
2578
+ text-transform: uppercase;
2579
+ flex-shrink: 0;
2580
+ }
2581
+
2582
+ .entity-chip.drill {
2583
+ background: #d1ecf1;
2584
+ color: #0c5460;
2585
+ }
2586
+
2587
+ .entity-chip.incident {
2588
+ background: #f8d7da;
2589
+ color: #721c24;
2590
+ }
2591
+
2592
+ .entity-chip.manual {
2593
+ background: #ede9f6;
2594
+ color: #383d72;
2595
+ }
2596
+
2597
+ .input-row {
2598
+ display: grid;
2599
+ grid-template-columns: 1fr 1fr;
2600
+ gap: 14px;
2601
+ margin-bottom: 14px;
2602
+ }
2603
+
2604
+ .form-group {
2605
+ display: flex;
2606
+ flex-direction: column;
2607
+ margin-bottom: 14px;
2608
+ }
2609
+
2610
+ .form-label {
2611
+ font-size: 13px;
2612
+ margin-bottom: 5px;
2613
+ color: #495057;
2614
+ }
2615
+
2616
+ .form-hint {
2617
+ font-size: 10px;
2618
+ font-weight: 400;
2619
+ color: #adb5bd;
2620
+ margin-left: 8px;
2621
+ }
2622
+
2623
+ .form-control {
2624
+ padding: 9px 12px;
2625
+ border: 1px solid #ced4da;
2626
+ border-radius: 6px;
2627
+ font-size: 14px;
2628
+ transition: border-color .2s;
2629
+ width: 100%;
2630
+ box-sizing: border-box;
2631
+ }
2632
+
2633
+ .form-control:focus {
2634
+ outline: none;
2635
+ border-color: #2a5298;
2636
+ box-shadow: 0 0 0 3px rgba(42, 82, 152, .1);
2637
+ }
2638
+
2639
+ textarea.form-control {
2640
+ resize: vertical;
2641
+ min-height: 80px;
2642
+ }
163
2643
 
164
- handleFileSelection(event) {
165
- const files = Array.from(event.target.files);
166
-
167
- if (!this.selectedFolder || files.length === 0) return;
2644
+ select.form-control {
2645
+ cursor: pointer;
2646
+ }
168
2647
 
169
- const newFiles = files.map(file => ({
170
- id: Date.now() + Math.random(),
171
- name: file.name,
172
- size: file.size,
173
- type: file.type,
174
- file: file,
175
- addedAt: new Date()
176
- }));
2648
+ .file-drop-zone {
2649
+ border: 2px dashed #ced4da;
2650
+ border-radius: 8px;
2651
+ padding: 18px;
2652
+ text-align: center;
2653
+ cursor: pointer;
2654
+ transition: border-color .2s;
2655
+ min-height: 80px;
2656
+ }
177
2657
 
178
- this.selectedFolder.files.push(...newFiles);
179
-
180
- this.$emit('files-added', {
181
- folderId: this.selectedFolder.id,
182
- files: newFiles
183
- });
184
- this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData() });
185
-
186
- // Reset file input
187
- event.target.value = '';
188
- this.selectedFolder = null;
189
- },
190
-
191
- removeFile(folderId, fileId) {
192
- const folder = this.folders.find(f => f.id === folderId);
193
- if (folder) {
194
- const fileIndex = folder.files.findIndex(f => f.id === fileId);
195
- if (fileIndex > -1) {
196
- const removedFile = folder.files.splice(fileIndex, 1)[0];
197
- this.$emit('file-removed', {
198
- folderId,
199
- file: removedFile
200
- });
201
- this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData() });
202
- }
203
- }
204
- },
2658
+ .file-drop-zone:hover {
2659
+ border-color: #2a5298;
2660
+ }
205
2661
 
206
- deleteFolder(folderId) {
207
- if (confirm('Are you sure you want to delete this folder and all its files?')) {
208
- const folderIndex = this.folders.findIndex(f => f.id === folderId);
209
- if (folderIndex > -1) {
210
- const deletedFolder = this.folders.splice(folderIndex, 1)[0];
211
- this.$emit('folder-deleted', deletedFolder);
212
- this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData() });
213
- }
214
- }
215
- },
2662
+ .attached-files {
2663
+ display: flex;
2664
+ flex-wrap: wrap;
2665
+ gap: 6px;
2666
+ margin-top: 10px;
2667
+ justify-content: center;
2668
+ }
216
2669
 
217
- formatFileSize(bytes) {
218
- if (bytes === 0) return '0 Bytes';
219
- const k = 1024;
220
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
221
- const i = Math.floor(Math.log(bytes) / Math.log(k));
222
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
223
- },
2670
+ .attached-chip {
2671
+ background: #e9ecef;
2672
+ border-radius: 5px;
2673
+ padding: 3px 10px;
2674
+ font-size: 12px;
2675
+ display: inline-flex;
2676
+ align-items: center;
2677
+ gap: 5px;
2678
+ }
224
2679
 
225
- getAllFilesData() {
226
- return this.folders.flatMap(folder =>
227
- folder.files.map(file => ({
228
- ...file,
229
- folderName: folder.name,
230
- folderId: folder.id
231
- }))
232
- );
233
- },
2680
+ .attached-chip--link {
2681
+ text-decoration: none;
2682
+ color: #2a5298;
2683
+ background: #eef2ff;
2684
+ border: 1px solid #c5d0ee;
2685
+ transition: background .15s;
2686
+ }
234
2687
 
235
- toggleFolder(folderId) {
236
- const index = this.expandedFolders.indexOf(folderId);
237
- if (index > -1) {
238
- this.expandedFolders.splice(index, 1);
239
- } else {
240
- this.expandedFolders = [folderId]; // Only one folder expanded at a time
241
- }
242
- },
2688
+ .attached-chip--link:hover {
2689
+ background: #dce4f8;
2690
+ }
243
2691
 
244
- closeAllFolders() {
245
- this.expandedFolders = [];
246
- },
2692
+ .attached-chip button {
2693
+ background: none;
2694
+ border: none;
2695
+ cursor: pointer;
2696
+ color: #dc3545;
2697
+ padding: 0;
2698
+ font-size: 11px;
2699
+ }
247
2700
 
248
- getExpandedFolder() {
249
- if (this.expandedFolders.length > 0) {
250
- return this.folders.find(f => f.id === this.expandedFolders[0]) || { files: [], name: '' };
251
- }
252
- return { files: [], name: '' };
253
- },
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
+ }
254
2712
 
255
- showPreview(folderId) {
256
- this.previewFolder = folderId;
257
- },
2713
+ /* ─── Report Viewer ─── */
2714
+ .view-grid {
2715
+ display: grid;
2716
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
2717
+ gap: 14px;
2718
+ margin-bottom: 20px;
2719
+ }
258
2720
 
259
- hidePreview() {
260
- this.previewFolder = null;
261
- }
262
- }
2721
+ .view-item {
2722
+ display: flex;
2723
+ flex-direction: column;
2724
+ gap: 3px;
263
2725
  }
264
- </script>
265
2726
 
266
- <style scoped>
267
- .reports-container {
268
- padding: 20px;
2727
+ .view-item label {
2728
+ font-size: 11px;
2729
+ font-weight: 700;
2730
+ text-transform: uppercase;
2731
+ letter-spacing: .05em;
2732
+ color: #6c757d;
269
2733
  }
270
2734
 
271
- .page-header {
272
- margin-bottom: 30px;
273
- border-bottom: 1px solid #e0e0e0;
274
- padding-bottom: 15px;
2735
+ .view-item span {
2736
+ font-size: 14px;
2737
+ color: #212529;
275
2738
  }
276
2739
 
277
- .folders-section {
278
- display: grid;
279
- grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
280
- gap: 20px;
281
- padding: 10px;
2740
+ .ref-mono {
2741
+ font-family: monospace;
2742
+ font-size: 13px;
2743
+ color: #2a5298;
2744
+ font-weight: 700;
282
2745
  }
283
2746
 
284
- @media (max-width: 768px) {
285
- .folders-section {
286
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
287
- gap: 15px;
288
- }
2747
+ .view-section {
2748
+ margin-bottom: 18px;
289
2749
  }
290
2750
 
291
- @media (min-width: 1400px) {
292
- .folders-section {
293
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
294
- }
2751
+ .view-section label {
2752
+ font-size: 12px;
2753
+ font-weight: 700;
2754
+ text-transform: uppercase;
2755
+ letter-spacing: .05em;
2756
+ color: #6c757d;
2757
+ display: block;
2758
+ margin-bottom: 6px;
295
2759
  }
296
2760
 
297
- .folder-card {
298
- position: relative;
299
- border: 2px solid #e0e0e0;
2761
+ .view-text {
2762
+ background: #f8f9fa;
2763
+ border-radius: 6px;
2764
+ padding: 12px 14px;
2765
+ font-size: 14px;
2766
+ line-height: 1.6;
2767
+ color: #212529;
2768
+ white-space: pre-wrap;
2769
+ }
2770
+
2771
+ /* ─── Buttons ─── */
2772
+ .btn {
2773
+ padding: 9px 18px;
2774
+ border: none;
300
2775
  border-radius: 8px;
301
- padding: 15px;
302
- background-color: #fff;
303
- transition: all 0.2s;
2776
+ font-size: 13px;
2777
+ font-weight: 600;
304
2778
  cursor: pointer;
305
- aspect-ratio: 1;
306
- display: flex;
307
- flex-direction: column;
2779
+ transition: all .2s;
2780
+ display: inline-flex;
308
2781
  align-items: center;
309
- justify-content: center;
2782
+ gap: 6px;
310
2783
  }
311
2784
 
312
- .folder-card:hover {
313
- border-color: #0d6efd;
314
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
315
- transform: translateY(-2px);
2785
+ .btn:disabled {
2786
+ opacity: .6;
2787
+ cursor: not-allowed;
316
2788
  }
317
2789
 
318
- /* Hover Preview Popup */
319
- .folder-preview {
320
- position: absolute;
321
- top: 50%;
322
- left: 50%;
323
- transform: translate(-50%, -50%);
324
- background: white;
325
- border: 2px solid #0d6efd;
326
- border-radius: 8px;
327
- box-shadow: 0 8px 24px rgba(0,0,0,0.15);
328
- padding: 0;
329
- z-index: 100;
330
- min-width: 280px;
331
- max-width: 320px;
332
- pointer-events: none;
2790
+ .btn-primary {
2791
+ background: #2a5298;
2792
+ color: #fff;
333
2793
  }
334
2794
 
335
- .preview-header {
336
- background: #f8f9fa;
337
- padding: 12px 15px;
338
- border-bottom: 1px solid #e0e0e0;
339
- border-radius: 6px 6px 0 0;
2795
+ .btn-primary:hover:not(:disabled) {
2796
+ background: #1e3c72;
340
2797
  }
341
2798
 
342
- .preview-header strong {
343
- font-size: 0.95rem;
344
- color: #333;
2799
+ .btn-success {
2800
+ background: #28a745;
2801
+ color: #fff;
345
2802
  }
346
2803
 
347
- .preview-content {
348
- padding: 10px;
349
- max-height: 200px;
350
- overflow-y: auto;
2804
+ .btn-success:hover:not(:disabled) {
2805
+ background: #218838;
351
2806
  }
352
2807
 
353
- .preview-file {
354
- display: flex;
355
- align-items: center;
356
- gap: 8px;
357
- padding: 6px 8px;
358
- font-size: 0.85rem;
359
- color: #495057;
360
- border-bottom: 1px solid #f0f0f0;
2808
+ .btn-secondary {
2809
+ background: #6c757d;
2810
+ color: #fff;
361
2811
  }
362
2812
 
363
- .preview-file:last-child {
364
- border-bottom: none;
2813
+ .btn-secondary:hover:not(:disabled) {
2814
+ background: #5a6268;
365
2815
  }
366
2816
 
367
- .preview-file i {
368
- color: #6c757d;
369
- font-size: 0.9rem;
2817
+ .btn-danger {
2818
+ background: #dc3545;
2819
+ color: #fff;
370
2820
  }
371
2821
 
372
- .preview-file span {
373
- white-space: nowrap;
374
- overflow: hidden;
375
- text-overflow: ellipsis;
2822
+ .btn-danger:hover:not(:disabled) {
2823
+ background: #c82333;
376
2824
  }
377
2825
 
378
- .preview-more {
379
- text-align: center;
380
- padding: 8px;
381
- font-size: 0.8rem;
2826
+ .btn-warning {
2827
+ background: #f0ad4e;
2828
+ color: #664d03;
2829
+ }
2830
+
2831
+ .btn-warning:hover {
2832
+ background: #e0972e;
2833
+ }
2834
+
2835
+ .btn-outline-primary {
2836
+ background: transparent;
2837
+ border: 1.5px solid #2a5298;
2838
+ color: #2a5298;
2839
+ }
2840
+
2841
+ .btn-outline-primary:hover:not(:disabled) {
2842
+ background: #2a5298;
2843
+ color: #fff;
2844
+ }
2845
+
2846
+ .btn-outline-secondary {
2847
+ background: transparent;
2848
+ border: 1.5px solid #6c757d;
382
2849
  color: #6c757d;
383
- font-style: italic;
384
2850
  }
385
2851
 
386
- .preview-empty {
387
- padding: 30px 20px;
388
- text-align: center;
389
- color: #adb5bd;
2852
+ .btn-outline-secondary:hover:not(:disabled) {
2853
+ background: #6c757d;
2854
+ color: #fff;
390
2855
  }
391
2856
 
392
- .preview-empty i {
393
- font-size: 2rem;
394
- display: block;
395
- margin-bottom: 8px;
2857
+ .btn-link {
2858
+ background: none;
2859
+ border: none;
2860
+ padding: 0;
2861
+ font-size: 13px;
2862
+ text-decoration: none;
2863
+ cursor: pointer;
2864
+ }
2865
+
2866
+ .btn-sm {
2867
+ padding: 5px 12px;
2868
+ font-size: 12px;
396
2869
  }
397
2870
 
398
- .folder-content {
2871
+ /* ─── Loading / Spinners ─── */
2872
+ .loading-container {
399
2873
  display: flex;
400
2874
  flex-direction: column;
401
2875
  align-items: center;
402
- justify-content: center;
403
- text-align: center;
404
- width: 100%;
405
- height: 100%;
2876
+ padding: 40px 20px;
406
2877
  }
407
2878
 
408
- .folder-name {
409
- margin-top: 10px;
410
- font-weight: 600;
411
- font-size: 0.9rem;
412
- word-break: break-word;
413
- max-width: 100%;
414
- overflow: hidden;
415
- text-overflow: ellipsis;
416
- display: -webkit-box;
417
- -webkit-line-clamp: 2;
418
- -webkit-box-orient: vertical;
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;
419
2887
  }
420
2888
 
421
- .folder-actions-overlay {
422
- position: absolute;
423
- top: 5px;
424
- right: 5px;
425
- display: flex;
426
- gap: 5px;
427
- opacity: 0;
428
- transition: opacity 0.2s;
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;
429
2897
  }
430
2898
 
431
- .folder-card:hover .folder-actions-overlay {
432
- opacity: 1;
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;
433
2907
  }
434
2908
 
435
- .btn-icon {
436
- width: 28px;
437
- height: 28px;
438
- padding: 0;
439
- display: flex;
440
- align-items: center;
441
- justify-content: center;
442
- background: white;
443
- border: 1px solid #dee2e6;
444
- border-radius: 4px;
2909
+ @keyframes spin {
2910
+ to {
2911
+ transform: rotate(360deg);
2912
+ }
445
2913
  }
446
2914
 
447
- .btn-icon:hover {
448
- background: #f8f9fa;
449
- border-color: #0d6efd;
2915
+ /* ─── Utilities ─── */
2916
+ .fw-bold {
2917
+ font-weight: 700;
450
2918
  }
451
2919
 
452
- .btn-icon i {
453
- font-size: 0.85rem;
2920
+ .fw-semibold {
2921
+ font-weight: 600;
454
2922
  }
455
2923
 
456
- /* Expanded Folder Modal (Click to Open) */
457
- .folder-modal-overlay {
458
- position: fixed;
459
- top: 0;
460
- left: 0;
461
- right: 0;
462
- bottom: 0;
463
- background: rgba(0,0,0,0.5);
464
- z-index: 1000;
465
- display: flex;
466
- align-items: center;
467
- justify-content: center;
2924
+ .me-1 {
2925
+ margin-right: 4px;
468
2926
  }
469
2927
 
470
- .folder-expanded {
471
- background: white;
472
- border-radius: 12px;
473
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
474
- width: 90%;
475
- max-width: 700px;
476
- max-height: 80vh;
477
- overflow: hidden;
478
- display: flex;
479
- flex-direction: column;
2928
+ .me-2 {
2929
+ margin-right: 8px;
480
2930
  }
481
2931
 
482
- .expanded-header {
483
- display: flex;
484
- justify-content: space-between;
485
- align-items: center;
486
- padding: 20px 25px;
487
- border-bottom: 2px solid #f0f0f0;
488
- background: #f8f9fa;
2932
+ .me-auto {
2933
+ margin-right: auto;
489
2934
  }
490
2935
 
491
- .expanded-header strong {
492
- font-size: 1.3rem;
493
- color: #333;
2936
+ .ms-1 {
2937
+ margin-left: 4px;
494
2938
  }
495
2939
 
496
- .btn-close-expanded {
497
- background: transparent;
498
- border: none;
499
- font-size: 1.2rem;
500
- cursor: pointer;
501
- padding: 5px 10px;
502
- display: flex;
503
- align-items: center;
504
- justify-content: center;
505
- transition: color 0.2s;
2940
+ .ms-2 {
2941
+ margin-left: 8px;
506
2942
  }
507
2943
 
508
- .btn-close-expanded:hover {
509
- color: #dc3545;
2944
+ .mt-2 {
2945
+ margin-top: 8px;
510
2946
  }
511
2947
 
512
- .files-list {
513
- padding: 20px;
514
- max-height: 60vh;
515
- overflow-y: auto;
2948
+ .mt-3 {
2949
+ margin-top: 16px;
516
2950
  }
517
2951
 
518
- .file-item {
519
- padding: 12px;
520
- background-color: #f8f9fa;
521
- border-radius: 4px;
522
- margin-bottom: 8px;
2952
+ .mt-4 {
2953
+ margin-top: 24px;
523
2954
  }
524
2955
 
525
- .file-item:last-child {
2956
+ .mb-0 {
526
2957
  margin-bottom: 0;
527
2958
  }
528
2959
 
529
- .empty-folder-message {
530
- padding: 40px 20px;
531
- text-align: center;
532
- color: #6c757d;
2960
+ .text-muted {
2961
+ color: #6c757d !important;
2962
+ }
2963
+
2964
+ .text-danger {
2965
+ color: #dc3545 !important;
2966
+ }
2967
+
2968
+ .text-success {
2969
+ color: #28a745 !important;
2970
+ }
2971
+
2972
+ .text-primary {
2973
+ color: #2a5298 !important;
2974
+ }
2975
+
2976
+ .text-warning {
2977
+ color: #f0ad4e !important;
2978
+ }
2979
+
2980
+ .d-flex {
2981
+ display: flex;
2982
+ }
2983
+
2984
+ .gap-2 {
2985
+ gap: 8px;
533
2986
  }
534
2987
 
535
- .empty-folder-message i {
536
- font-size: 3rem;
537
- color: #dee2e6;
2988
+ .justify-content-between {
2989
+ justify-content: space-between;
538
2990
  }
539
2991
 
540
- .empty-folder-message p {
541
- margin: 15px 0;
2992
+ .text-center {
2993
+ text-align: center;
542
2994
  }
543
2995
 
544
2996
  .empty-state {
545
2997
  padding: 60px 20px;
546
2998
  }
547
2999
 
548
- /* Modal Styles */
549
- .modal {
550
- display: none;
551
- position: fixed;
552
- z-index: 1050;
553
- left: 0;
554
- top: 0;
555
- width: 100%;
556
- height: 100%;
557
- overflow: hidden;
558
- background-color: rgba(0,0,0,0.5);
559
- align-items: center;
560
- justify-content: center;
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;
561
3010
  }
562
3011
 
563
- .modal.show {
3012
+ .incident-fields-title {
3013
+ font-size: 13px;
3014
+ font-weight: 700;
3015
+ color: #7b2d1a;
3016
+ margin-bottom: 14px;
564
3017
  display: flex;
3018
+ align-items: center;
3019
+ gap: 6px;
565
3020
  }
566
3021
 
567
- .modal-dialog {
568
- max-width: 500px;
569
- margin: 1.75rem auto;
3022
+ .field-hint {
3023
+ font-size: 11px;
3024
+ color: #adb5bd;
3025
+ margin-bottom: 4px;
3026
+ margin-top: -2px;
570
3027
  }
571
3028
 
572
- .modal-content {
573
- background-color: #fff;
574
- border-radius: 8px;
575
- box-shadow: 0 5px 15px rgba(0,0,0,0.3);
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;
576
3036
  }
577
3037
 
578
- .modal-header {
579
- padding: 1rem;
580
- border-bottom: 1px solid #dee2e6;
3038
+ .drill-checklist-title {
3039
+ font-size: 13px;
3040
+ font-weight: 700;
3041
+ color: #1e3c72;
3042
+ margin-bottom: 10px;
581
3043
  display: flex;
582
- justify-content: space-between;
583
3044
  align-items: center;
3045
+ gap: 6px;
584
3046
  }
585
3047
 
586
- .modal-body {
587
- padding: 1rem;
3048
+ .checklist-hint {
3049
+ font-size: 11px;
3050
+ font-weight: 400;
3051
+ color: #adb5bd;
3052
+ margin-left: 4px;
588
3053
  }
589
3054
 
590
- .modal-footer {
591
- padding: 1rem;
592
- border-top: 1px solid #dee2e6;
3055
+ .checklist-grid {
593
3056
  display: flex;
594
- justify-content: flex-end;
3057
+ flex-direction: column;
3058
+ gap: 6px;
3059
+ }
3060
+
3061
+ .checklist-item {
3062
+ display: flex;
3063
+ align-items: center;
595
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;
596
3072
  }
597
3073
 
598
- .btn-close {
599
- background: transparent;
600
- border: none;
601
- font-size: 1.5rem;
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;
602
3121
  font-weight: 700;
603
- line-height: 1;
604
- color: #000;
605
- opacity: 0.5;
606
- cursor: pointer;
3122
+ padding: 2px 8px;
3123
+ border-radius: 4px;
3124
+ flex-shrink: 0;
607
3125
  }
608
3126
 
609
- .btn-close:hover {
610
- opacity: 0.75;
3127
+ .answer-yes {
3128
+ background: #d4edda;
3129
+ color: #155724;
3130
+ }
3131
+
3132
+ .answer-no {
3133
+ background: #f8d7da;
3134
+ color: #721c24;
3135
+ }
3136
+
3137
+ @media (max-width: 640px) {
3138
+ .input-row {
3139
+ grid-template-columns: 1fr;
3140
+ }
3141
+
3142
+ .btn-group-split {
3143
+ gap: 5px;
3144
+ }
3145
+
3146
+ .btn {
3147
+ padding: 8px 12px;
3148
+ font-size: 12px;
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
+ }
611
3168
  }
612
3169
  </style>