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