oceanhelm 0.0.9 → 0.0.10
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 +1218 -817
- package/dist/oceanhelm.es.js.map +1 -1
- package/dist/oceanhelm.umd.js +1 -1
- package/dist/oceanhelm.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ConfigurableSidebar.vue +1 -2
- package/src/components/CrewManagement.vue +995 -40
- package/src/utils/sidebarConfig.js +7 -0
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
<div class="crew-section">
|
|
7
7
|
<div class="crew-section-header">
|
|
8
|
-
<
|
|
8
|
+
<h4 class="crew-subhead">{{ sectionTitle }}</h4>
|
|
9
9
|
<button class="btn btn-primary" @click="handleToggleAddForm" v-if="canAddCrew">
|
|
10
10
|
{{ showAddForm ? 'Cancel' : '+ Add Crew Member' }}
|
|
11
11
|
</button>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
<!-- Search and Filter -->
|
|
15
|
-
<div class="search-filter">
|
|
14
|
+
<!-- Search and Filter - Hide when form is shown -->
|
|
15
|
+
<div class="search-filter" v-if="!showAddForm">
|
|
16
16
|
<input type="text" placeholder="Search crew by name or role..." v-model="searchQuery" @input="handleSearch">
|
|
17
17
|
<select v-model="filterStatus" @change="handleFilter">
|
|
18
18
|
<option value="all">All Statuses</option>
|
|
@@ -20,18 +20,106 @@
|
|
|
20
20
|
<option value="onduty">On Duty</option>
|
|
21
21
|
<option value="unavailable">Unavailable</option>
|
|
22
22
|
</select>
|
|
23
|
+
<button class="btn btn-secondary" @click="showTimesheet = !showTimesheet">
|
|
24
|
+
<i :class="showTimesheet ? 'bi bi-grid-3x3-gap' : 'bi bi-table'"></i>
|
|
25
|
+
{{ showTimesheet ? 'Show Crew Cards' : 'Show Timesheet' }}
|
|
26
|
+
</button>
|
|
23
27
|
</div>
|
|
24
28
|
|
|
25
|
-
<!-- Loading State -->
|
|
26
|
-
<div v-if="loading" class="loading-state">
|
|
29
|
+
<!-- Loading State - Hide when form is shown -->
|
|
30
|
+
<div v-if="loading && !showAddForm" class="loading-state">
|
|
27
31
|
<div class="spinner-border text-primary" role="status">
|
|
28
32
|
<span class="visually-hidden">Loading crew...</span>
|
|
29
33
|
</div>
|
|
30
34
|
<p>Loading crew members...</p>
|
|
31
35
|
</div>
|
|
32
36
|
|
|
33
|
-
<!--
|
|
34
|
-
<div
|
|
37
|
+
<!-- Consolidated Timesheet View - Hide when form is shown -->
|
|
38
|
+
<div v-else-if="showTimesheet && !showAddForm" class="timesheet-view">
|
|
39
|
+
<div class="timesheet-header">
|
|
40
|
+
<h3><i class="bi bi-table"></i> Crew Activity Timesheet</h3>
|
|
41
|
+
<div class="timesheet-controls">
|
|
42
|
+
<select v-model="timesheetFilter" @change="filterTimesheet">
|
|
43
|
+
<option value="all">All Activities</option>
|
|
44
|
+
<option value="Embarked">Embarked Only</option>
|
|
45
|
+
<option value="Deboarded">Deboarded Only</option>
|
|
46
|
+
<option value="Assigned">Assigned Only</option>
|
|
47
|
+
<option value="Status Changed">Status Changes Only</option>
|
|
48
|
+
</select>
|
|
49
|
+
<input type="text" v-model="timesheetSearch" placeholder="Search timesheet..."
|
|
50
|
+
class="timesheet-search">
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="timesheet-summary">
|
|
55
|
+
<div class="summary-card">
|
|
56
|
+
<span class="summary-label">Total Entries:</span>
|
|
57
|
+
<span class="summary-value">{{ filteredTimesheetEntries.length }}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="summary-card">
|
|
60
|
+
<span class="summary-label">Crew Members:</span>
|
|
61
|
+
<span class="summary-value">{{ uniqueCrewCount }}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="summary-card">
|
|
64
|
+
<span class="summary-label">Date Range:</span>
|
|
65
|
+
<span class="summary-value">{{ timesheetDateRange }}</span>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div v-if="filteredTimesheetEntries.length === 0" class="no-results">
|
|
70
|
+
No timesheet entries found.
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div v-else class="timesheet-table-container">
|
|
74
|
+
<table class="timesheet-table">
|
|
75
|
+
<thead>
|
|
76
|
+
<tr>
|
|
77
|
+
<th @click="sortTimesheet('timestamp')">
|
|
78
|
+
Date/Time
|
|
79
|
+
<i class="bi bi-arrow-down-up"></i>
|
|
80
|
+
</th>
|
|
81
|
+
<th @click="sortTimesheet('crewName')">
|
|
82
|
+
Crew Member
|
|
83
|
+
<i class="bi bi-arrow-down-up"></i>
|
|
84
|
+
</th>
|
|
85
|
+
<th @click="sortTimesheet('role')">
|
|
86
|
+
Role
|
|
87
|
+
<i class="bi bi-arrow-down-up"></i>
|
|
88
|
+
</th>
|
|
89
|
+
<th @click="sortTimesheet('action')">
|
|
90
|
+
Action
|
|
91
|
+
<i class="bi bi-arrow-down-up"></i>
|
|
92
|
+
</th>
|
|
93
|
+
<th>Vessel</th>
|
|
94
|
+
<th>Duration</th>
|
|
95
|
+
<th>Notes</th>
|
|
96
|
+
</tr>
|
|
97
|
+
</thead>
|
|
98
|
+
<tbody>
|
|
99
|
+
<tr v-for="entry in filteredTimesheetEntries" :key="entry.uniqueId"
|
|
100
|
+
:class="getTimesheetRowClass(entry.action)">
|
|
101
|
+
<td class="timestamp-cell">{{ formatLogDate(entry.timestamp) }}</td>
|
|
102
|
+
<td class="crew-name-cell">
|
|
103
|
+
<strong>{{ entry.crewName }}</strong>
|
|
104
|
+
</td>
|
|
105
|
+
<td class="role-cell">{{ entry.role }}</td>
|
|
106
|
+
<td class="action-cell">
|
|
107
|
+
<span :class="['action-badge', getLogActionClass(entry.action)]">
|
|
108
|
+
<i :class="getLogIcon(entry.action)"></i>
|
|
109
|
+
{{ entry.action }}
|
|
110
|
+
</span>
|
|
111
|
+
</td>
|
|
112
|
+
<td class="vessel-cell">{{ entry.vessel || 'N/A' }}</td>
|
|
113
|
+
<td class="duration-cell">{{ entry.duration ? entry.duration + ' days' : 'N/A' }}</td>
|
|
114
|
+
<td class="notes-cell">{{ entry.notes || '-' }}</td>
|
|
115
|
+
</tr>
|
|
116
|
+
</tbody>
|
|
117
|
+
</table>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- Crew Grid - Hide when form is shown -->
|
|
122
|
+
<div class="crew-grid" v-else-if="filteredCrew.length > 0 && !showAddForm">
|
|
35
123
|
<div v-for="member in filteredCrew" :key="member.id" class="crew-card"
|
|
36
124
|
:class="{ 'unavailable': member.status === 'unavailable' }">
|
|
37
125
|
<div :class="['crew-status-badge', getStatusClass(member.status)]">
|
|
@@ -67,18 +155,48 @@
|
|
|
67
155
|
<div v-else class="status-unavailable crew-availability vcard">
|
|
68
156
|
Vessel: Unassigned
|
|
69
157
|
</div>
|
|
70
|
-
<button class="btn btn-primary" @click="handleAssignShift(member)"
|
|
158
|
+
<button class="btn btn-primary" @click="handleAssignShift(member)"
|
|
159
|
+
v-if="canAssignShift && member.status !== 'onduty'">
|
|
71
160
|
Assign Shift
|
|
72
161
|
</button>
|
|
162
|
+
<button class="btn btn-warning" @click="handleDeboardCrew(member)"
|
|
163
|
+
v-if="canDeboardCrew && member.status === 'onduty'">
|
|
164
|
+
Deboard
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<!-- Crew Log Button and Delete Icon -->
|
|
169
|
+
<div class="crew-footer">
|
|
170
|
+
<div class="crew-log-toggle" v-if="member.log && member.log.length > 0">
|
|
171
|
+
<button class="btn btn-link btn-sm log-toggle-btn" @click="toggleCrewLog(member.id)">
|
|
172
|
+
<i class="bi bi-clock-history"></i> View Log ({{ member.log.length }})
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
<i class="bi bi-trash icon delete-icon" @click="handleDeleteCrew(member)" v-if="canDeleteCrew"></i>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- Crew Log Display -->
|
|
179
|
+
<div v-if="expandedLogs.includes(member.id)" class="crew-log">
|
|
180
|
+
<h4>Crew Activity Log</h4>
|
|
181
|
+
<div v-for="(entry, index) in member.log" :key="index" class="log-entry">
|
|
182
|
+
<div class="log-date">{{ formatLogDate(entry.timestamp) }}</div>
|
|
183
|
+
<div class="log-action" :class="getLogActionClass(entry.action)">
|
|
184
|
+
<i :class="getLogIcon(entry.action)"></i>
|
|
185
|
+
{{ entry.action }}
|
|
186
|
+
</div>
|
|
187
|
+
<div class="log-details">
|
|
188
|
+
<span v-if="entry.vessel">Vessel: {{ entry.vessel }}</span>
|
|
189
|
+
<span v-if="entry.duration"> | Duration: {{ entry.duration }} days</span>
|
|
190
|
+
</div>
|
|
191
|
+
<div v-if="entry.notes" class="log-notes">{{ entry.notes }}</div>
|
|
192
|
+
</div>
|
|
73
193
|
</div>
|
|
74
194
|
|
|
75
|
-
<!-- Delete Button -->
|
|
76
|
-
<i class="bi bi-trash icon delete-icon" @click="handleDeleteCrew(member)" v-if="canDeleteCrew"></i>
|
|
77
195
|
</div>
|
|
78
196
|
</div>
|
|
79
197
|
|
|
80
|
-
<!-- No Results -->
|
|
81
|
-
<div v-else-if="!loading" class="no-results">
|
|
198
|
+
<!-- No Results - Hide when form is shown -->
|
|
199
|
+
<div v-else-if="!loading && !showTimesheet && !showAddForm" class="no-results">
|
|
82
200
|
{{ crew.length === 0 ? 'No crew members found.' : 'No crew members found matching your search criteria.' }}
|
|
83
201
|
</div>
|
|
84
202
|
|
|
@@ -128,14 +246,38 @@
|
|
|
128
246
|
<div v-for="(cert, index) in newCrew.certifications" :key="index" class="certification-entry">
|
|
129
247
|
<div class="form-row">
|
|
130
248
|
<div class="form-group">
|
|
131
|
-
<label>Certification Name
|
|
249
|
+
<label>Certification Name *</label>
|
|
132
250
|
<input type="text" v-model="cert.name" placeholder="Enter certification name">
|
|
133
251
|
</div>
|
|
134
252
|
<div class="form-group">
|
|
135
|
-
<label>Expiry Date
|
|
253
|
+
<label>Expiry Date *</label>
|
|
136
254
|
<input type="date" v-model="cert.expiryDate">
|
|
137
255
|
</div>
|
|
138
256
|
<div class="form-group">
|
|
257
|
+
<label>Certificate Image *</label>
|
|
258
|
+
<div class="image-upload-wrapper">
|
|
259
|
+
<input type="file" :id="'cert-image-' + index"
|
|
260
|
+
@change="handleCertImageUpload($event, index)" accept="image/*,.pdf"
|
|
261
|
+
class="file-input">
|
|
262
|
+
<label :for="'cert-image-' + index" class="file-input-label">
|
|
263
|
+
<i class="bi bi-cloud-upload"></i>
|
|
264
|
+
{{ cert.imagePreview ? 'Change File' : 'Upload File' }}
|
|
265
|
+
</label>
|
|
266
|
+
<div v-if="cert.imagePreview" class="image-preview">
|
|
267
|
+
<img v-if="cert.imageType !== 'pdf'" :src="cert.imagePreview"
|
|
268
|
+
alt="Certificate preview">
|
|
269
|
+
<div v-else class="pdf-preview">
|
|
270
|
+
<i class="bi bi-file-pdf"></i>
|
|
271
|
+
<span>{{ cert.imageName }}</span>
|
|
272
|
+
</div>
|
|
273
|
+
<button type="button" class="btn-remove-image" @click="removeCertImage(index)">
|
|
274
|
+
<i class="bi bi-x-circle"></i>
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
<small v-if="cert.imageName" class="file-name">{{ cert.imageName }}</small>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="form-group" style="display: flex; align-items: flex-end;">
|
|
139
281
|
<button type="button" class="btn btn-danger btn-sm" @click="removeCertification(index)"
|
|
140
282
|
v-if="newCrew.certifications.length > 1">
|
|
141
283
|
Remove
|
|
@@ -172,7 +314,6 @@ export default {
|
|
|
172
314
|
name: 'CrewManagement',
|
|
173
315
|
|
|
174
316
|
props: {
|
|
175
|
-
// Required props
|
|
176
317
|
crew: {
|
|
177
318
|
type: Array,
|
|
178
319
|
required: true,
|
|
@@ -186,8 +327,6 @@ export default {
|
|
|
186
327
|
type: Object,
|
|
187
328
|
default: () => ({ role: 'viewer' })
|
|
188
329
|
},
|
|
189
|
-
|
|
190
|
-
// Optional props
|
|
191
330
|
loading: {
|
|
192
331
|
type: Boolean,
|
|
193
332
|
default: false
|
|
@@ -196,8 +335,6 @@ export default {
|
|
|
196
335
|
type: Array,
|
|
197
336
|
default: () => ['Captain', 'First Officer', 'Engineer', 'Deckhand', 'Mechanic', 'Cook']
|
|
198
337
|
},
|
|
199
|
-
|
|
200
|
-
// Configuration props
|
|
201
338
|
config: {
|
|
202
339
|
type: Object,
|
|
203
340
|
default: () => ({
|
|
@@ -216,11 +353,13 @@ export default {
|
|
|
216
353
|
'crew-edit',
|
|
217
354
|
'crew-delete',
|
|
218
355
|
'crew-assign-shift',
|
|
356
|
+
'crew-deboard',
|
|
219
357
|
'crew-add-certification',
|
|
220
358
|
'crew-view-certification',
|
|
221
359
|
'search-changed',
|
|
222
360
|
'filter-changed',
|
|
223
|
-
'access-denied'
|
|
361
|
+
'access-denied',
|
|
362
|
+
'upload-cert-image'
|
|
224
363
|
],
|
|
225
364
|
|
|
226
365
|
data() {
|
|
@@ -229,13 +368,26 @@ export default {
|
|
|
229
368
|
filterStatus: 'all',
|
|
230
369
|
showAddForm: false,
|
|
231
370
|
formErrors: {},
|
|
371
|
+
expandedLogs: [],
|
|
372
|
+
showTimesheet: false,
|
|
373
|
+
timesheetFilter: 'all',
|
|
374
|
+
timesheetSearch: '',
|
|
375
|
+
timesheetSortKey: 'timestamp',
|
|
376
|
+
timesheetSortOrder: 'desc',
|
|
232
377
|
newCrew: {
|
|
233
378
|
name: '',
|
|
234
379
|
role: 'Deckhand',
|
|
235
380
|
customRole: '',
|
|
236
381
|
status: 'available',
|
|
237
382
|
nextShift: '',
|
|
238
|
-
certifications: [{
|
|
383
|
+
certifications: [{
|
|
384
|
+
name: '',
|
|
385
|
+
expiryDate: '',
|
|
386
|
+
imageFile: null,
|
|
387
|
+
imagePreview: null,
|
|
388
|
+
imageName: '',
|
|
389
|
+
imageType: ''
|
|
390
|
+
}],
|
|
239
391
|
notes: '',
|
|
240
392
|
email: '',
|
|
241
393
|
onBoard: ''
|
|
@@ -261,6 +413,68 @@ export default {
|
|
|
261
413
|
})
|
|
262
414
|
},
|
|
263
415
|
|
|
416
|
+
allTimesheetEntries() {
|
|
417
|
+
const entries = []
|
|
418
|
+
|
|
419
|
+
this.crew.forEach(member => {
|
|
420
|
+
if (member.log && member.log.length > 0) {
|
|
421
|
+
member.log.forEach((logEntry, index) => {
|
|
422
|
+
entries.push({
|
|
423
|
+
...logEntry,
|
|
424
|
+
crewName: member.name,
|
|
425
|
+
role: member.role,
|
|
426
|
+
crewId: member.id,
|
|
427
|
+
uniqueId: `${member.id}-${index}`
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
return entries.sort((a, b) => {
|
|
434
|
+
const dateA = new Date(a.timestamp)
|
|
435
|
+
const dateB = new Date(b.timestamp)
|
|
436
|
+
return this.timesheetSortOrder === 'desc' ? dateB - dateA : dateA - dateB
|
|
437
|
+
})
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
filteredTimesheetEntries() {
|
|
441
|
+
let filtered = this.allTimesheetEntries
|
|
442
|
+
|
|
443
|
+
if (this.timesheetFilter !== 'all') {
|
|
444
|
+
filtered = filtered.filter(entry => entry.action === this.timesheetFilter)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (this.timesheetSearch) {
|
|
448
|
+
const search = this.timesheetSearch.toLowerCase()
|
|
449
|
+
filtered = filtered.filter(entry =>
|
|
450
|
+
entry.crewName.toLowerCase().includes(search) ||
|
|
451
|
+
entry.role.toLowerCase().includes(search) ||
|
|
452
|
+
entry.action.toLowerCase().includes(search) ||
|
|
453
|
+
(entry.vessel && entry.vessel.toLowerCase().includes(search)) ||
|
|
454
|
+
(entry.notes && entry.notes.toLowerCase().includes(search))
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return this.sortTimesheetEntries(filtered)
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
uniqueCrewCount() {
|
|
462
|
+
const uniqueCrewIds = new Set(this.filteredTimesheetEntries.map(e => e.crewId))
|
|
463
|
+
return uniqueCrewIds.size
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
timesheetDateRange() {
|
|
467
|
+
if (this.filteredTimesheetEntries.length === 0) return 'N/A'
|
|
468
|
+
|
|
469
|
+
const dates = this.filteredTimesheetEntries.map(e => new Date(e.timestamp))
|
|
470
|
+
const minDate = new Date(Math.min(...dates))
|
|
471
|
+
const maxDate = new Date(Math.max(...dates))
|
|
472
|
+
|
|
473
|
+
const formatDate = (date) => date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
474
|
+
|
|
475
|
+
return `${formatDate(minDate)} - ${formatDate(maxDate)}`
|
|
476
|
+
},
|
|
477
|
+
|
|
264
478
|
canAddCrew() {
|
|
265
479
|
return this.config.enableAdd && this.hasPermission('add')
|
|
266
480
|
},
|
|
@@ -275,11 +489,14 @@ export default {
|
|
|
275
489
|
|
|
276
490
|
canAssignShift() {
|
|
277
491
|
return this.config.enableAssignShift && this.hasPermission('assign')
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
canDeboardCrew() {
|
|
495
|
+
return this.config.enableAssignShift && this.hasPermission('assign')
|
|
278
496
|
}
|
|
279
497
|
},
|
|
280
498
|
|
|
281
499
|
methods: {
|
|
282
|
-
// Permission checking
|
|
283
500
|
hasPermission(action) {
|
|
284
501
|
const { role, vessel } = this.userProfile
|
|
285
502
|
const isVessel = vessel === this.vesselName
|
|
@@ -287,13 +504,12 @@ export default {
|
|
|
287
504
|
const permissions = {
|
|
288
505
|
'owner': ['add', 'edit', 'delete', 'assign', 'view'],
|
|
289
506
|
'staff': ['add', 'edit', 'assign', 'view'],
|
|
290
|
-
'captain': ['assign', 'view'],
|
|
507
|
+
'captain': ['assign', 'view'],
|
|
291
508
|
'viewer': ['view']
|
|
292
509
|
}
|
|
293
510
|
|
|
294
511
|
let rolePermissions = permissions[role] || []
|
|
295
512
|
|
|
296
|
-
// Special case: captain + isVessel = true
|
|
297
513
|
if (role === 'captain' && isVessel) {
|
|
298
514
|
rolePermissions = [...rolePermissions, 'add']
|
|
299
515
|
}
|
|
@@ -301,7 +517,6 @@ export default {
|
|
|
301
517
|
return rolePermissions.includes(action)
|
|
302
518
|
},
|
|
303
519
|
|
|
304
|
-
// Event handlers
|
|
305
520
|
handleToggleAddForm() {
|
|
306
521
|
if (!this.canAddCrew) {
|
|
307
522
|
this.$emit('access-denied', { action: 'add crew', userProfile: this.userProfile })
|
|
@@ -321,22 +536,95 @@ export default {
|
|
|
321
536
|
this.$emit('filter-changed', this.filterStatus)
|
|
322
537
|
},
|
|
323
538
|
|
|
324
|
-
|
|
539
|
+
filterTimesheet() {
|
|
540
|
+
// Trigger re-computation of filtered entries
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
sortTimesheet(key) {
|
|
544
|
+
if (this.timesheetSortKey === key) {
|
|
545
|
+
this.timesheetSortOrder = this.timesheetSortOrder === 'asc' ? 'desc' : 'asc'
|
|
546
|
+
} else {
|
|
547
|
+
this.timesheetSortKey = key
|
|
548
|
+
this.timesheetSortOrder = 'asc'
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
sortTimesheetEntries(entries) {
|
|
553
|
+
const sorted = [...entries]
|
|
554
|
+
|
|
555
|
+
sorted.sort((a, b) => {
|
|
556
|
+
let valA, valB
|
|
557
|
+
|
|
558
|
+
switch (this.timesheetSortKey) {
|
|
559
|
+
case 'timestamp':
|
|
560
|
+
valA = new Date(a.timestamp)
|
|
561
|
+
valB = new Date(b.timestamp)
|
|
562
|
+
break
|
|
563
|
+
case 'crewName':
|
|
564
|
+
valA = a.crewName.toLowerCase()
|
|
565
|
+
valB = b.crewName.toLowerCase()
|
|
566
|
+
break
|
|
567
|
+
case 'role':
|
|
568
|
+
valA = a.role.toLowerCase()
|
|
569
|
+
valB = b.role.toLowerCase()
|
|
570
|
+
break
|
|
571
|
+
case 'action':
|
|
572
|
+
valA = a.action.toLowerCase()
|
|
573
|
+
valB = b.action.toLowerCase()
|
|
574
|
+
break
|
|
575
|
+
default:
|
|
576
|
+
return 0
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (valA < valB) return this.timesheetSortOrder === 'asc' ? -1 : 1
|
|
580
|
+
if (valA > valB) return this.timesheetSortOrder === 'asc' ? 1 : -1
|
|
581
|
+
return 0
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
return sorted
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
getTimesheetRowClass(action) {
|
|
588
|
+
return `timesheet-row-${action.toLowerCase().replace(/\s+/g, '-')}`
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
async handleAddCrewMember() {
|
|
325
592
|
if (!this.validateForm()) {
|
|
326
593
|
return
|
|
327
594
|
}
|
|
328
595
|
|
|
329
596
|
const validCertifications = this.newCrew.certifications.filter(cert =>
|
|
330
|
-
cert.name.trim() !== '' && cert.expiryDate !== ''
|
|
597
|
+
cert.name.trim() !== '' && cert.expiryDate !== '' && cert.imageFile !== null
|
|
331
598
|
)
|
|
332
599
|
|
|
600
|
+
if (validCertifications.length === 0) {
|
|
601
|
+
alert('Please add at least one complete certification with name, expiry date, and image.')
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
|
|
333
605
|
const finalRole = this.newCrew.role === 'Other' ? this.newCrew.customRole : this.newCrew.role
|
|
334
606
|
|
|
607
|
+
// Upload all certification images first and get their URLs
|
|
608
|
+
const certificationsWithUrls = await Promise.all(
|
|
609
|
+
validCertifications.map(async (cert, index) => {
|
|
610
|
+
// Emit and wait for the upload to complete
|
|
611
|
+
const uploadResult = await this.uploadCertificationImage(cert.imageFile, this.newCrew.email)
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
name: cert.name,
|
|
615
|
+
expiryDate: cert.expiryDate,
|
|
616
|
+
imageName: cert.imageName,
|
|
617
|
+
imageType: cert.imageType,
|
|
618
|
+
imageUrl: uploadResult.publicUrl // Add the public URL here
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
)
|
|
622
|
+
|
|
335
623
|
const newMember = {
|
|
336
624
|
name: this.newCrew.name,
|
|
337
625
|
role: finalRole,
|
|
338
626
|
status: this.newCrew.status,
|
|
339
|
-
certifications:
|
|
627
|
+
certifications: certificationsWithUrls,
|
|
340
628
|
notes: this.newCrew.notes,
|
|
341
629
|
vessel: this.vesselName,
|
|
342
630
|
email: this.newCrew.email,
|
|
@@ -349,6 +637,24 @@ export default {
|
|
|
349
637
|
this.showAddForm = false
|
|
350
638
|
},
|
|
351
639
|
|
|
640
|
+
// Add this helper method to handle the upload:
|
|
641
|
+
async uploadCertificationImage(file, email) {
|
|
642
|
+
// Return a promise that resolves when upload completes
|
|
643
|
+
return new Promise((resolve, reject) => {
|
|
644
|
+
this.$emit('upload-cert-image', {
|
|
645
|
+
file,
|
|
646
|
+
email,
|
|
647
|
+
callback: (result) => {
|
|
648
|
+
if (result.error) {
|
|
649
|
+
reject(result.error)
|
|
650
|
+
} else {
|
|
651
|
+
resolve(result)
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
})
|
|
655
|
+
})
|
|
656
|
+
},
|
|
657
|
+
|
|
352
658
|
handleCancelForm() {
|
|
353
659
|
this.showAddForm = false
|
|
354
660
|
this.resetForm()
|
|
@@ -372,6 +678,32 @@ export default {
|
|
|
372
678
|
this.$emit('crew-assign-shift', member)
|
|
373
679
|
},
|
|
374
680
|
|
|
681
|
+
handleDeboardCrew(member) {
|
|
682
|
+
if (!this.canDeboardCrew) {
|
|
683
|
+
this.$emit('access-denied', { action: 'deboard crew', userProfile: this.userProfile })
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
let duration = null
|
|
688
|
+
if (member.nextShift) {
|
|
689
|
+
const embarkDate = new Date(member.nextShift)
|
|
690
|
+
const today = new Date()
|
|
691
|
+
const diffTime = Math.abs(today - embarkDate)
|
|
692
|
+
duration = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
this.$emit('crew-deboard', { member, duration })
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
toggleCrewLog(memberId) {
|
|
699
|
+
const index = this.expandedLogs.indexOf(memberId)
|
|
700
|
+
if (index > -1) {
|
|
701
|
+
this.expandedLogs.splice(index, 1)
|
|
702
|
+
} else {
|
|
703
|
+
this.expandedLogs.push(memberId)
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
|
|
375
707
|
handleAddCertification(member) {
|
|
376
708
|
if (!this.canEditCrew) {
|
|
377
709
|
this.$emit('access-denied', { action: 'add certification', userProfile: this.userProfile })
|
|
@@ -385,7 +717,6 @@ export default {
|
|
|
385
717
|
this.$emit('crew-view-certification', { certification, member })
|
|
386
718
|
},
|
|
387
719
|
|
|
388
|
-
// Form management
|
|
389
720
|
resetForm() {
|
|
390
721
|
this.newCrew = {
|
|
391
722
|
name: '',
|
|
@@ -393,7 +724,14 @@ export default {
|
|
|
393
724
|
customRole: '',
|
|
394
725
|
status: 'available',
|
|
395
726
|
nextShift: '',
|
|
396
|
-
certifications: [{
|
|
727
|
+
certifications: [{
|
|
728
|
+
name: '',
|
|
729
|
+
expiryDate: '',
|
|
730
|
+
imageFile: null,
|
|
731
|
+
imagePreview: null,
|
|
732
|
+
imageName: '',
|
|
733
|
+
imageType: ''
|
|
734
|
+
}],
|
|
397
735
|
notes: '',
|
|
398
736
|
email: '',
|
|
399
737
|
onBoard: ''
|
|
@@ -409,19 +747,16 @@ export default {
|
|
|
409
747
|
email: 'Email Address'
|
|
410
748
|
}
|
|
411
749
|
|
|
412
|
-
// Check required fields
|
|
413
750
|
Object.keys(requiredFields).forEach(field => {
|
|
414
751
|
if (!this.newCrew[field] || this.newCrew[field].trim() === '') {
|
|
415
752
|
this.formErrors[field] = `${requiredFields[field]} is required`
|
|
416
753
|
}
|
|
417
754
|
})
|
|
418
755
|
|
|
419
|
-
// Validate email format
|
|
420
756
|
if (this.newCrew.email && !this.isValidEmail(this.newCrew.email)) {
|
|
421
757
|
this.formErrors.email = 'Please enter a valid email address'
|
|
422
758
|
}
|
|
423
759
|
|
|
424
|
-
// Validate custom role
|
|
425
760
|
if (this.newCrew.role === 'Other' && (!this.newCrew.customRole || this.newCrew.customRole.trim() === '')) {
|
|
426
761
|
this.formErrors.customRole = 'Custom role is required when "Other" is selected'
|
|
427
762
|
}
|
|
@@ -435,14 +770,55 @@ export default {
|
|
|
435
770
|
},
|
|
436
771
|
|
|
437
772
|
addCertificationEntry() {
|
|
438
|
-
this.newCrew.certifications.push({
|
|
773
|
+
this.newCrew.certifications.push({
|
|
774
|
+
name: '',
|
|
775
|
+
expiryDate: '',
|
|
776
|
+
imageFile: null,
|
|
777
|
+
imagePreview: null,
|
|
778
|
+
imageName: '',
|
|
779
|
+
imageType: ''
|
|
780
|
+
})
|
|
439
781
|
},
|
|
440
782
|
|
|
441
783
|
removeCertification(index) {
|
|
442
784
|
this.newCrew.certifications.splice(index, 1)
|
|
443
785
|
},
|
|
444
786
|
|
|
445
|
-
|
|
787
|
+
handleCertImageUpload(event, index) {
|
|
788
|
+
const file = event.target.files[0]
|
|
789
|
+
if (!file) return
|
|
790
|
+
|
|
791
|
+
const cert = this.newCrew.certifications[index]
|
|
792
|
+
cert.imageFile = file
|
|
793
|
+
cert.imageName = file.name
|
|
794
|
+
cert.imageType = file.type.includes('pdf') ? 'pdf' : 'image'
|
|
795
|
+
|
|
796
|
+
// Create preview for images
|
|
797
|
+
if (cert.imageType === 'image') {
|
|
798
|
+
const reader = new FileReader()
|
|
799
|
+
reader.onload = (e) => {
|
|
800
|
+
cert.imagePreview = e.target.result
|
|
801
|
+
}
|
|
802
|
+
reader.readAsDataURL(file)
|
|
803
|
+
} else {
|
|
804
|
+
cert.imagePreview = 'pdf'
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
|
|
808
|
+
removeCertImage(index) {
|
|
809
|
+
const cert = this.newCrew.certifications[index]
|
|
810
|
+
cert.imageFile = null
|
|
811
|
+
cert.imagePreview = null
|
|
812
|
+
cert.imageName = ''
|
|
813
|
+
cert.imageType = ''
|
|
814
|
+
|
|
815
|
+
// Clear the file input
|
|
816
|
+
const fileInput = document.getElementById('cert-image-' + index)
|
|
817
|
+
if (fileInput) {
|
|
818
|
+
fileInput.value = ''
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
|
|
446
822
|
formatStatus(status) {
|
|
447
823
|
return status ? status.charAt(0).toUpperCase() + status.slice(1) : ''
|
|
448
824
|
},
|
|
@@ -477,6 +853,38 @@ export default {
|
|
|
477
853
|
if (expiry <= today) return 'expired'
|
|
478
854
|
if (expiry <= oneMonthFromNow) return 'expiringSoon'
|
|
479
855
|
return 'valid'
|
|
856
|
+
},
|
|
857
|
+
|
|
858
|
+
formatLogDate(timestamp) {
|
|
859
|
+
if (!timestamp) return ''
|
|
860
|
+
const date = new Date(timestamp)
|
|
861
|
+
return date.toLocaleDateString('en-US', {
|
|
862
|
+
year: 'numeric',
|
|
863
|
+
month: 'short',
|
|
864
|
+
day: 'numeric',
|
|
865
|
+
hour: '2-digit',
|
|
866
|
+
minute: '2-digit'
|
|
867
|
+
})
|
|
868
|
+
},
|
|
869
|
+
|
|
870
|
+
getLogActionClass(action) {
|
|
871
|
+
const actionMap = {
|
|
872
|
+
'Embarked': 'log-action-embark',
|
|
873
|
+
'Deboarded': 'log-action-deboard',
|
|
874
|
+
'Assigned': 'log-action-assign',
|
|
875
|
+
'Status Changed': 'log-action-status'
|
|
876
|
+
}
|
|
877
|
+
return actionMap[action] || ''
|
|
878
|
+
},
|
|
879
|
+
|
|
880
|
+
getLogIcon(action) {
|
|
881
|
+
const iconMap = {
|
|
882
|
+
'Embarked': 'bi bi-box-arrow-in-right',
|
|
883
|
+
'Deboarded': 'bi bi-box-arrow-left',
|
|
884
|
+
'Assigned': 'bi bi-calendar-check',
|
|
885
|
+
'Status Changed': 'bi bi-arrow-repeat'
|
|
886
|
+
}
|
|
887
|
+
return iconMap[action] || 'bi bi-circle-fill'
|
|
480
888
|
}
|
|
481
889
|
}
|
|
482
890
|
}
|
|
@@ -496,6 +904,7 @@ export default {
|
|
|
496
904
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
497
905
|
gap: 20px;
|
|
498
906
|
margin-bottom: 30px;
|
|
907
|
+
align-items: start;
|
|
499
908
|
}
|
|
500
909
|
|
|
501
910
|
.crew-card {
|
|
@@ -540,6 +949,11 @@ export default {
|
|
|
540
949
|
margin-right: 5px;
|
|
541
950
|
margin-bottom: 5px;
|
|
542
951
|
font-size: 0.85em;
|
|
952
|
+
cursor: pointer;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.certification-tag:hover {
|
|
956
|
+
background-color: #d1f0ff;
|
|
543
957
|
}
|
|
544
958
|
|
|
545
959
|
.crew-status-badge {
|
|
@@ -571,6 +985,8 @@ export default {
|
|
|
571
985
|
display: flex;
|
|
572
986
|
justify-content: space-between;
|
|
573
987
|
margin-top: 15px;
|
|
988
|
+
gap: 5px;
|
|
989
|
+
flex-wrap: wrap;
|
|
574
990
|
}
|
|
575
991
|
|
|
576
992
|
.btn {
|
|
@@ -579,6 +995,7 @@ export default {
|
|
|
579
995
|
cursor: pointer;
|
|
580
996
|
font-size: 0.9em;
|
|
581
997
|
transition: background-color 0.2s;
|
|
998
|
+
border: none;
|
|
582
999
|
}
|
|
583
1000
|
|
|
584
1001
|
.btn-primary {
|
|
@@ -599,10 +1016,140 @@ export default {
|
|
|
599
1016
|
background-color: #dde2e6;
|
|
600
1017
|
}
|
|
601
1018
|
|
|
1019
|
+
.btn-warning {
|
|
1020
|
+
background-color: #ffc107;
|
|
1021
|
+
color: #212529;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
.btn-warning:hover {
|
|
1025
|
+
background-color: #e0a800;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.btn-danger {
|
|
1029
|
+
background-color: #dc3545;
|
|
1030
|
+
color: white;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.btn-danger:hover {
|
|
1034
|
+
background-color: #c82333;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.btn-link {
|
|
1038
|
+
background: none;
|
|
1039
|
+
border: none;
|
|
1040
|
+
color: #005792;
|
|
1041
|
+
text-decoration: none;
|
|
1042
|
+
padding: 4px 8px;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.btn-link:hover {
|
|
1046
|
+
text-decoration: underline;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.btn-sm {
|
|
1050
|
+
font-size: 0.8em;
|
|
1051
|
+
padding: 4px 8px;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.crew-footer {
|
|
1055
|
+
display: flex;
|
|
1056
|
+
justify-content: space-between;
|
|
1057
|
+
align-items: center;
|
|
1058
|
+
margin-top: 10px;
|
|
1059
|
+
padding-top: 10px;
|
|
1060
|
+
border-top: 1px solid #e9ecef;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
.crew-log-toggle {
|
|
1064
|
+
flex: 1;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.log-toggle-btn {
|
|
1068
|
+
text-align: left;
|
|
1069
|
+
padding: 0;
|
|
1070
|
+
display: flex;
|
|
1071
|
+
align-items: center;
|
|
1072
|
+
gap: 5px;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
.crew-log {
|
|
1076
|
+
margin-top: 15px;
|
|
1077
|
+
padding: 10px;
|
|
1078
|
+
background-color: #ffffff;
|
|
1079
|
+
border-radius: 4px;
|
|
1080
|
+
border: 1px solid #dee2e6;
|
|
1081
|
+
max-height: 300px;
|
|
1082
|
+
overflow-y: auto;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.crew-log h4 {
|
|
1086
|
+
margin: 0 0 10px 0;
|
|
1087
|
+
font-size: 0.9em;
|
|
1088
|
+
color: #005792;
|
|
1089
|
+
border-bottom: 1px solid #dee2e6;
|
|
1090
|
+
padding-bottom: 5px;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
.log-entry {
|
|
1094
|
+
padding: 8px;
|
|
1095
|
+
margin-bottom: 8px;
|
|
1096
|
+
border-left: 3px solid #005792;
|
|
1097
|
+
background-color: #f8f9fa;
|
|
1098
|
+
border-radius: 3px;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
.log-entry:last-child {
|
|
1102
|
+
margin-bottom: 0;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
.log-date {
|
|
1106
|
+
font-size: 0.75em;
|
|
1107
|
+
color: #6c757d;
|
|
1108
|
+
margin-bottom: 3px;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
.log-action {
|
|
1112
|
+
font-weight: bold;
|
|
1113
|
+
font-size: 0.85em;
|
|
1114
|
+
margin-bottom: 3px;
|
|
1115
|
+
display: flex;
|
|
1116
|
+
align-items: center;
|
|
1117
|
+
gap: 5px;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
.log-action-embark {
|
|
1121
|
+
color: #28a745;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.log-action-deboard {
|
|
1125
|
+
color: #dc3545;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
.log-action-assign {
|
|
1129
|
+
color: #007bff;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
.log-action-status {
|
|
1133
|
+
color: #ffc107;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
.log-details {
|
|
1137
|
+
font-size: 0.8em;
|
|
1138
|
+
color: #495057;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.log-notes {
|
|
1142
|
+
font-size: 0.8em;
|
|
1143
|
+
color: #6c757d;
|
|
1144
|
+
font-style: italic;
|
|
1145
|
+
margin-top: 3px;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
602
1148
|
.search-filter {
|
|
603
1149
|
display: flex;
|
|
604
1150
|
margin-bottom: 20px;
|
|
605
1151
|
gap: 10px;
|
|
1152
|
+
align-items: center;
|
|
606
1153
|
}
|
|
607
1154
|
|
|
608
1155
|
.search-filter input,
|
|
@@ -617,7 +1164,7 @@ export default {
|
|
|
617
1164
|
}
|
|
618
1165
|
|
|
619
1166
|
.search-filter select {
|
|
620
|
-
width:
|
|
1167
|
+
min-width: 150px;
|
|
621
1168
|
}
|
|
622
1169
|
|
|
623
1170
|
.add-crew-form {
|
|
@@ -663,6 +1210,24 @@ export default {
|
|
|
663
1210
|
|
|
664
1211
|
.icon {
|
|
665
1212
|
font-size: 20px;
|
|
1213
|
+
cursor: pointer;
|
|
1214
|
+
color: #005792;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
.icon:hover {
|
|
1218
|
+
color: #003d5c;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
.delete-icon {
|
|
1222
|
+
font-size: 20px;
|
|
1223
|
+
cursor: pointer;
|
|
1224
|
+
color: #dc3545;
|
|
1225
|
+
position: static;
|
|
1226
|
+
margin: 0;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
.delete-icon:hover {
|
|
1230
|
+
color: #c82333;
|
|
666
1231
|
}
|
|
667
1232
|
|
|
668
1233
|
.form-group input,
|
|
@@ -674,6 +1239,16 @@ export default {
|
|
|
674
1239
|
border-radius: 4px;
|
|
675
1240
|
}
|
|
676
1241
|
|
|
1242
|
+
.form-group input.error {
|
|
1243
|
+
border-color: #dc3545;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
.error-message {
|
|
1247
|
+
color: #dc3545;
|
|
1248
|
+
font-size: 0.85em;
|
|
1249
|
+
margin-top: 5px;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
677
1252
|
.form-actions {
|
|
678
1253
|
display: flex;
|
|
679
1254
|
justify-content: flex-end;
|
|
@@ -683,8 +1258,9 @@ export default {
|
|
|
683
1258
|
|
|
684
1259
|
.no-results {
|
|
685
1260
|
text-align: center;
|
|
686
|
-
padding: 20px;
|
|
1261
|
+
padding: 40px 20px;
|
|
687
1262
|
color: #6c757d;
|
|
1263
|
+
font-size: 1.1em;
|
|
688
1264
|
}
|
|
689
1265
|
|
|
690
1266
|
.crew-section-header {
|
|
@@ -694,13 +1270,392 @@ export default {
|
|
|
694
1270
|
margin-bottom: 15px;
|
|
695
1271
|
}
|
|
696
1272
|
|
|
1273
|
+
.crew-section-header h2 {
|
|
1274
|
+
color: #005792;
|
|
1275
|
+
margin: 0;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
697
1278
|
.vcard {
|
|
698
1279
|
border-radius: 6px;
|
|
699
1280
|
padding: 5px;
|
|
700
1281
|
}
|
|
701
1282
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1283
|
+
.loading-state {
|
|
1284
|
+
text-align: center;
|
|
1285
|
+
padding: 40px 20px;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.spinner-border {
|
|
1289
|
+
width: 3rem;
|
|
1290
|
+
height: 3rem;
|
|
1291
|
+
border: 0.25em solid currentColor;
|
|
1292
|
+
border-right-color: transparent;
|
|
1293
|
+
border-radius: 50%;
|
|
1294
|
+
display: inline-block;
|
|
1295
|
+
animation: spinner-border 0.75s linear infinite;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
@keyframes spinner-border {
|
|
1299
|
+
to {
|
|
1300
|
+
transform: rotate(360deg);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
.text-primary {
|
|
1305
|
+
color: #005792;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
.visually-hidden {
|
|
1309
|
+
position: absolute;
|
|
1310
|
+
width: 1px;
|
|
1311
|
+
height: 1px;
|
|
1312
|
+
padding: 0;
|
|
1313
|
+
margin: -1px;
|
|
1314
|
+
overflow: hidden;
|
|
1315
|
+
clip: rect(0, 0, 0, 0);
|
|
1316
|
+
white-space: nowrap;
|
|
1317
|
+
border: 0;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
.certification-section {
|
|
1321
|
+
margin: 20px 0;
|
|
1322
|
+
padding: 15px;
|
|
1323
|
+
background-color: #ffffff;
|
|
1324
|
+
border-radius: 4px;
|
|
1325
|
+
border: 1px solid #dee2e6;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
.certification-section h3 {
|
|
1329
|
+
color: #005792;
|
|
1330
|
+
margin-top: 0;
|
|
1331
|
+
margin-bottom: 15px;
|
|
1332
|
+
font-size: 1.1em;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
.certification-entry {
|
|
1336
|
+
margin-bottom: 10px;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/* Image Upload Styles */
|
|
1340
|
+
.image-upload-wrapper {
|
|
1341
|
+
position: relative;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
.file-input {
|
|
1345
|
+
display: none;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
.file-input-label {
|
|
1349
|
+
display: inline-flex;
|
|
1350
|
+
align-items: center;
|
|
1351
|
+
gap: 8px;
|
|
1352
|
+
padding: 8px 16px;
|
|
1353
|
+
background-color: #005792;
|
|
1354
|
+
color: white !important;
|
|
1355
|
+
border-radius: 4px;
|
|
1356
|
+
cursor: pointer;
|
|
1357
|
+
font-size: 0.9em;
|
|
1358
|
+
transition: background-color 0.2s;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.file-input-label:hover {
|
|
1362
|
+
background-color: #004675;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
.file-input-label i {
|
|
1366
|
+
font-size: 1.1em;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
.file-name {
|
|
1370
|
+
display: block;
|
|
1371
|
+
margin-top: 5px;
|
|
1372
|
+
color: #6c757d;
|
|
1373
|
+
font-size: 0.85em;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
.image-preview {
|
|
1377
|
+
margin-top: 10px;
|
|
1378
|
+
position: relative;
|
|
1379
|
+
display: inline-block;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
.image-preview img {
|
|
1383
|
+
max-width: 200px;
|
|
1384
|
+
max-height: 150px;
|
|
1385
|
+
border-radius: 4px;
|
|
1386
|
+
border: 2px solid #dee2e6;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
.pdf-preview {
|
|
1390
|
+
display: flex;
|
|
1391
|
+
align-items: center;
|
|
1392
|
+
gap: 10px;
|
|
1393
|
+
padding: 15px;
|
|
1394
|
+
background-color: #f8f9fa;
|
|
1395
|
+
border: 2px solid #dee2e6;
|
|
1396
|
+
border-radius: 4px;
|
|
1397
|
+
max-width: 200px;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
.pdf-preview i {
|
|
1401
|
+
font-size: 2em;
|
|
1402
|
+
color: #dc3545;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
.pdf-preview span {
|
|
1406
|
+
font-size: 0.85em;
|
|
1407
|
+
color: #495057;
|
|
1408
|
+
word-break: break-word;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.btn-remove-image {
|
|
1412
|
+
position: absolute;
|
|
1413
|
+
top: -8px;
|
|
1414
|
+
right: -8px;
|
|
1415
|
+
background-color: #dc3545;
|
|
1416
|
+
color: white;
|
|
1417
|
+
border: none;
|
|
1418
|
+
border-radius: 50%;
|
|
1419
|
+
width: 24px;
|
|
1420
|
+
height: 24px;
|
|
1421
|
+
display: flex;
|
|
1422
|
+
align-items: center;
|
|
1423
|
+
justify-content: center;
|
|
1424
|
+
cursor: pointer;
|
|
1425
|
+
padding: 0;
|
|
1426
|
+
transition: background-color 0.2s;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
.btn-remove-image:hover {
|
|
1430
|
+
background-color: #c82333;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
.btn-remove-image i {
|
|
1434
|
+
font-size: 1em;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/* Timesheet Styles */
|
|
1438
|
+
.timesheet-view {
|
|
1439
|
+
background-color: #ffffff;
|
|
1440
|
+
border-radius: 6px;
|
|
1441
|
+
padding: 20px;
|
|
1442
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
.timesheet-header {
|
|
1446
|
+
display: flex;
|
|
1447
|
+
justify-content: space-between;
|
|
1448
|
+
align-items: center;
|
|
1449
|
+
margin-bottom: 20px;
|
|
1450
|
+
padding-bottom: 15px;
|
|
1451
|
+
border-bottom: 2px solid #005792;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
.timesheet-header h3 {
|
|
1455
|
+
color: #005792;
|
|
1456
|
+
margin: 0;
|
|
1457
|
+
display: flex;
|
|
1458
|
+
align-items: center;
|
|
1459
|
+
gap: 10px;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
.timesheet-controls {
|
|
1463
|
+
display: flex;
|
|
1464
|
+
gap: 10px;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
.timesheet-controls select,
|
|
1468
|
+
.timesheet-search {
|
|
1469
|
+
padding: 8px 12px;
|
|
1470
|
+
border: 1px solid #ced4da;
|
|
1471
|
+
border-radius: 4px;
|
|
1472
|
+
font-size: 0.9em;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
.timesheet-search {
|
|
1476
|
+
min-width: 250px;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
.timesheet-summary {
|
|
1480
|
+
display: flex;
|
|
1481
|
+
gap: 20px;
|
|
1482
|
+
margin-bottom: 20px;
|
|
1483
|
+
flex-wrap: wrap;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
.summary-card {
|
|
1487
|
+
flex: 1;
|
|
1488
|
+
min-width: 150px;
|
|
1489
|
+
background-color: #f8f9fa;
|
|
1490
|
+
padding: 15px;
|
|
1491
|
+
border-radius: 6px;
|
|
1492
|
+
border-left: 4px solid #005792;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
.summary-label {
|
|
1496
|
+
display: block;
|
|
1497
|
+
font-size: 0.85em;
|
|
1498
|
+
color: #6c757d;
|
|
1499
|
+
margin-bottom: 5px;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
.summary-value {
|
|
1503
|
+
display: block;
|
|
1504
|
+
font-size: 1.5em;
|
|
1505
|
+
font-weight: bold;
|
|
1506
|
+
color: #005792;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.timesheet-table-container {
|
|
1510
|
+
overflow-x: auto;
|
|
1511
|
+
margin-top: 20px;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
.timesheet-table {
|
|
1515
|
+
width: 100%;
|
|
1516
|
+
border-collapse: collapse;
|
|
1517
|
+
font-size: 0.9em;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
.timesheet-table thead {
|
|
1521
|
+
background-color: #005792;
|
|
1522
|
+
color: white;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
.timesheet-table th {
|
|
1526
|
+
padding: 12px 8px;
|
|
1527
|
+
text-align: left;
|
|
1528
|
+
font-weight: bold;
|
|
1529
|
+
cursor: pointer;
|
|
1530
|
+
user-select: none;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
.timesheet-table th:hover {
|
|
1534
|
+
background-color: #004675;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.timesheet-table th i {
|
|
1538
|
+
font-size: 0.8em;
|
|
1539
|
+
margin-left: 5px;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
.timesheet-table tbody tr {
|
|
1543
|
+
border-bottom: 1px solid #dee2e6;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
.timesheet-table tbody tr:hover {
|
|
1547
|
+
background-color: #f8f9fa;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
.timesheet-table td {
|
|
1551
|
+
padding: 10px 8px;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
.timestamp-cell {
|
|
1555
|
+
white-space: nowrap;
|
|
1556
|
+
color: #495057;
|
|
1557
|
+
font-size: 0.85em;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
.crew-name-cell strong {
|
|
1561
|
+
color: #005792;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
.role-cell {
|
|
1565
|
+
color: #6c757d;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
.action-cell {
|
|
1569
|
+
white-space: nowrap;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
.action-badge {
|
|
1573
|
+
display: inline-flex;
|
|
1574
|
+
align-items: center;
|
|
1575
|
+
gap: 5px;
|
|
1576
|
+
padding: 4px 10px;
|
|
1577
|
+
border-radius: 12px;
|
|
1578
|
+
font-size: 0.85em;
|
|
1579
|
+
font-weight: bold;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
.vessel-cell {
|
|
1583
|
+
color: #495057;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
.duration-cell {
|
|
1587
|
+
color: #495057;
|
|
1588
|
+
font-weight: 500;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
.notes-cell {
|
|
1592
|
+
color: #6c757d;
|
|
1593
|
+
font-style: italic;
|
|
1594
|
+
max-width: 200px;
|
|
1595
|
+
overflow: hidden;
|
|
1596
|
+
text-overflow: ellipsis;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
.timesheet-row-embarked {
|
|
1600
|
+
background-color: #d4edda;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
.timesheet-row-deboarded {
|
|
1604
|
+
background-color: #f8d7da;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
.timesheet-row-assigned {
|
|
1608
|
+
background-color: #d1ecf1;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
.timesheet-row-status-changed {
|
|
1612
|
+
background-color: #fff3cd;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
@media (max-width: 768px) {
|
|
1616
|
+
.crew-grid {
|
|
1617
|
+
grid-template-columns: 1fr;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
.search-filter {
|
|
1621
|
+
flex-direction: column;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
.search-filter input,
|
|
1625
|
+
.search-filter select {
|
|
1626
|
+
width: 100%;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
.timesheet-header {
|
|
1630
|
+
flex-direction: column;
|
|
1631
|
+
align-items: flex-start;
|
|
1632
|
+
gap: 15px;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
.timesheet-controls {
|
|
1636
|
+
flex-direction: column;
|
|
1637
|
+
width: 100%;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
.timesheet-controls select,
|
|
1641
|
+
.timesheet-search {
|
|
1642
|
+
width: 100%;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
.timesheet-summary {
|
|
1646
|
+
flex-direction: column;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
.timesheet-table {
|
|
1650
|
+
font-size: 0.8em;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
.timesheet-table th,
|
|
1654
|
+
.timesheet-table td {
|
|
1655
|
+
padding: 8px 4px;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
.crew-subhead{
|
|
1659
|
+
color: #000;
|
|
705
1660
|
}
|
|
706
1661
|
</style>
|