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.
- package/dist/oceanhelm.es.js +3211 -1416
- package/dist/oceanhelm.es.js.map +1 -1
- package/dist/oceanhelm.umd.js +9 -1
- package/dist/oceanhelm.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ActivityLogs.vue +319 -330
- package/src/components/ConfigurableSidebar.vue +55 -8
- package/src/components/CrewManagement.vue +686 -36
- package/src/components/Reports.vue +2985 -428
- package/src/components/RequisitionSystem.vue +97 -67
- package/src/utils/sidebarConfig.js +62 -48
|
@@ -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
|
-
<!--
|
|
13
|
-
<
|
|
14
|
-
<div class="
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 & 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-
|
|
30
|
-
<i class="bi bi-
|
|
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
|
-
<!--
|
|
37
|
-
<div class="
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
<!--
|
|
44
|
-
<div
|
|
45
|
-
<div class="
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
·
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
<!--
|
|
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
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
<!--
|
|
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
|
-
|
|
724
|
+
activeTab: 'all',
|
|
124
725
|
showNewFolderModal: false,
|
|
125
726
|
newFolderName: '',
|
|
126
727
|
selectedFolder: null,
|
|
127
728
|
expandedFolders: [],
|
|
128
|
-
|
|
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
|
-
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (!this.selectedFolder || files.length === 0) return;
|
|
2644
|
+
select.form-control {
|
|
2645
|
+
cursor: pointer;
|
|
2646
|
+
}
|
|
168
2647
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
2721
|
+
.view-item {
|
|
2722
|
+
display: flex;
|
|
2723
|
+
flex-direction: column;
|
|
2724
|
+
gap: 3px;
|
|
263
2725
|
}
|
|
264
|
-
</script>
|
|
265
2726
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
padding-bottom: 15px;
|
|
2735
|
+
.view-item span {
|
|
2736
|
+
font-size: 14px;
|
|
2737
|
+
color: #212529;
|
|
275
2738
|
}
|
|
276
2739
|
|
|
277
|
-
.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
2740
|
+
.ref-mono {
|
|
2741
|
+
font-family: monospace;
|
|
2742
|
+
font-size: 13px;
|
|
2743
|
+
color: #2a5298;
|
|
2744
|
+
font-weight: 700;
|
|
282
2745
|
}
|
|
283
2746
|
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
.
|
|
298
|
-
|
|
299
|
-
border:
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
transition: all 0.2s;
|
|
2776
|
+
font-size: 13px;
|
|
2777
|
+
font-weight: 600;
|
|
304
2778
|
cursor: pointer;
|
|
305
|
-
|
|
306
|
-
display: flex;
|
|
307
|
-
flex-direction: column;
|
|
2779
|
+
transition: all .2s;
|
|
2780
|
+
display: inline-flex;
|
|
308
2781
|
align-items: center;
|
|
309
|
-
|
|
2782
|
+
gap: 6px;
|
|
310
2783
|
}
|
|
311
2784
|
|
|
312
|
-
.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
transform: translateY(-2px);
|
|
2785
|
+
.btn:disabled {
|
|
2786
|
+
opacity: .6;
|
|
2787
|
+
cursor: not-allowed;
|
|
316
2788
|
}
|
|
317
2789
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
.
|
|
336
|
-
background: #
|
|
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
|
-
.
|
|
343
|
-
|
|
344
|
-
color: #
|
|
2799
|
+
.btn-success {
|
|
2800
|
+
background: #28a745;
|
|
2801
|
+
color: #fff;
|
|
345
2802
|
}
|
|
346
2803
|
|
|
347
|
-
.
|
|
348
|
-
|
|
349
|
-
max-height: 200px;
|
|
350
|
-
overflow-y: auto;
|
|
2804
|
+
.btn-success:hover:not(:disabled) {
|
|
2805
|
+
background: #218838;
|
|
351
2806
|
}
|
|
352
2807
|
|
|
353
|
-
.
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
.
|
|
364
|
-
|
|
2813
|
+
.btn-secondary:hover:not(:disabled) {
|
|
2814
|
+
background: #5a6268;
|
|
365
2815
|
}
|
|
366
2816
|
|
|
367
|
-
.
|
|
368
|
-
|
|
369
|
-
|
|
2817
|
+
.btn-danger {
|
|
2818
|
+
background: #dc3545;
|
|
2819
|
+
color: #fff;
|
|
370
2820
|
}
|
|
371
2821
|
|
|
372
|
-
.
|
|
373
|
-
|
|
374
|
-
overflow: hidden;
|
|
375
|
-
text-overflow: ellipsis;
|
|
2822
|
+
.btn-danger:hover:not(:disabled) {
|
|
2823
|
+
background: #c82333;
|
|
376
2824
|
}
|
|
377
2825
|
|
|
378
|
-
.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
color: #adb5bd;
|
|
2852
|
+
.btn-outline-secondary:hover:not(:disabled) {
|
|
2853
|
+
background: #6c757d;
|
|
2854
|
+
color: #fff;
|
|
390
2855
|
}
|
|
391
2856
|
|
|
392
|
-
.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
2871
|
+
/* ─── Loading / Spinners ─── */
|
|
2872
|
+
.loading-container {
|
|
399
2873
|
display: flex;
|
|
400
2874
|
flex-direction: column;
|
|
401
2875
|
align-items: center;
|
|
402
|
-
|
|
403
|
-
text-align: center;
|
|
404
|
-
width: 100%;
|
|
405
|
-
height: 100%;
|
|
2876
|
+
padding: 40px 20px;
|
|
406
2877
|
}
|
|
407
2878
|
|
|
408
|
-
.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
.
|
|
432
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
2915
|
+
/* ─── Utilities ─── */
|
|
2916
|
+
.fw-bold {
|
|
2917
|
+
font-weight: 700;
|
|
450
2918
|
}
|
|
451
2919
|
|
|
452
|
-
.
|
|
453
|
-
font-
|
|
2920
|
+
.fw-semibold {
|
|
2921
|
+
font-weight: 600;
|
|
454
2922
|
}
|
|
455
2923
|
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
.
|
|
471
|
-
|
|
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
|
-
.
|
|
483
|
-
|
|
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
|
-
.
|
|
492
|
-
|
|
493
|
-
color: #333;
|
|
2936
|
+
.ms-1 {
|
|
2937
|
+
margin-left: 4px;
|
|
494
2938
|
}
|
|
495
2939
|
|
|
496
|
-
.
|
|
497
|
-
|
|
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
|
-
.
|
|
509
|
-
|
|
2944
|
+
.mt-2 {
|
|
2945
|
+
margin-top: 8px;
|
|
510
2946
|
}
|
|
511
2947
|
|
|
512
|
-
.
|
|
513
|
-
|
|
514
|
-
max-height: 60vh;
|
|
515
|
-
overflow-y: auto;
|
|
2948
|
+
.mt-3 {
|
|
2949
|
+
margin-top: 16px;
|
|
516
2950
|
}
|
|
517
2951
|
|
|
518
|
-
.
|
|
519
|
-
|
|
520
|
-
background-color: #f8f9fa;
|
|
521
|
-
border-radius: 4px;
|
|
522
|
-
margin-bottom: 8px;
|
|
2952
|
+
.mt-4 {
|
|
2953
|
+
margin-top: 24px;
|
|
523
2954
|
}
|
|
524
2955
|
|
|
525
|
-
.
|
|
2956
|
+
.mb-0 {
|
|
526
2957
|
margin-bottom: 0;
|
|
527
2958
|
}
|
|
528
2959
|
|
|
529
|
-
.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
.
|
|
536
|
-
|
|
537
|
-
color: #dee2e6;
|
|
2988
|
+
.justify-content-between {
|
|
2989
|
+
justify-content: space-between;
|
|
538
2990
|
}
|
|
539
2991
|
|
|
540
|
-
.
|
|
541
|
-
|
|
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
|
-
/*
|
|
549
|
-
.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
568
|
-
|
|
569
|
-
|
|
3022
|
+
.field-hint {
|
|
3023
|
+
font-size: 11px;
|
|
3024
|
+
color: #adb5bd;
|
|
3025
|
+
margin-bottom: 4px;
|
|
3026
|
+
margin-top: -2px;
|
|
570
3027
|
}
|
|
571
3028
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
.
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
.
|
|
587
|
-
|
|
3048
|
+
.checklist-hint {
|
|
3049
|
+
font-size: 11px;
|
|
3050
|
+
font-weight: 400;
|
|
3051
|
+
color: #adb5bd;
|
|
3052
|
+
margin-left: 4px;
|
|
588
3053
|
}
|
|
589
3054
|
|
|
590
|
-
.
|
|
591
|
-
padding: 1rem;
|
|
592
|
-
border-top: 1px solid #dee2e6;
|
|
3055
|
+
.checklist-grid {
|
|
593
3056
|
display: flex;
|
|
594
|
-
|
|
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
|
-
.
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
cursor: pointer;
|
|
3122
|
+
padding: 2px 8px;
|
|
3123
|
+
border-radius: 4px;
|
|
3124
|
+
flex-shrink: 0;
|
|
607
3125
|
}
|
|
608
3126
|
|
|
609
|
-
.
|
|
610
|
-
|
|
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>
|