oceanhelm 0.0.10 → 0.0.12
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 +2268 -1081
- 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/ActivityLogs.vue +319 -330
- package/src/components/ConfigurableSidebar.vue +55 -8
- package/src/components/CrewManagement.vue +748 -47
- package/src/components/DashHead.vue +10 -0
- package/src/components/Reports.vue +1449 -0
- package/src/components/RequisitionSystem.vue +97 -67
- package/src/index.js +3 -1
- package/src/utils/sidebarConfig.js +65 -17
|
@@ -6,18 +6,92 @@
|
|
|
6
6
|
<div class="crew-section">
|
|
7
7
|
<div class="crew-section-header">
|
|
8
8
|
<h4 class="crew-subhead">{{ sectionTitle }}</h4>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
<div class="header-actions">
|
|
10
|
+
<button class="btn btn-secondary" @click="handleToggleGuestForm" v-if="canAddCrew">
|
|
11
|
+
{{ showGuestForm ? 'Cancel' : '+ Check-in Guest' }}
|
|
12
|
+
</button>
|
|
13
|
+
<button class="btn btn-primary" @click="handleToggleAddForm" v-if="canAddCrew">
|
|
14
|
+
{{ showAddForm ? 'Cancel' : '+ Add Crew Member' }}
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<!-- Personnel Onboard Summary -->
|
|
20
|
+
<div class="personnel-summary" v-if="!showAddForm && !showGuestForm && !showTimesheet">
|
|
21
|
+
<div class="summary-header">
|
|
22
|
+
<h3><i class="bi bi-people-fill"></i> Personnel Onboard Summary</h3>
|
|
23
|
+
<div class="summary-date">{{ currentDate }}</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="summary-cards">
|
|
27
|
+
<!-- Total Onboard -->
|
|
28
|
+
<div class="summary-card total-card">
|
|
29
|
+
<div class="card-icon">
|
|
30
|
+
<i class="bi bi-people"></i>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="card-content">
|
|
33
|
+
<div class="card-value">{{ totalOnboard }}</div>
|
|
34
|
+
<div class="card-label">Total Onboard</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Crew Onboard -->
|
|
39
|
+
<div class="summary-card crew-card">
|
|
40
|
+
<div class="card-icon">
|
|
41
|
+
<i class="bi bi-person-badge"></i>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="card-content">
|
|
44
|
+
<div class="card-value">{{ crewOnboard }}</div>
|
|
45
|
+
<div class="card-label">Crew Onboard</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Guests Onboard -->
|
|
50
|
+
<div class="summary-card guest-card-summary">
|
|
51
|
+
<div class="card-icon">
|
|
52
|
+
<i class="bi bi-person-check"></i>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="card-content">
|
|
55
|
+
<div class="card-value">{{ guestsOnboard }}</div>
|
|
56
|
+
<div class="card-label">Guests Onboard</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Breakdown by Vessel -->
|
|
62
|
+
<div class="vessel-breakdown" v-if="Object.keys(personnelByVessel).length > 0">
|
|
63
|
+
<h4>Breakdown by Vessel</h4>
|
|
64
|
+
<div class="vessel-cards">
|
|
65
|
+
<div v-for="(data, vessel) in personnelByVessel" :key="vessel" class="vessel-card">
|
|
66
|
+
<div class="vessel-name">{{ vessel }}</div>
|
|
67
|
+
<div class="vessel-stats">
|
|
68
|
+
<div class="stat-item">
|
|
69
|
+
<span class="stat-label">Crew:</span>
|
|
70
|
+
<span class="stat-value">{{ data.crew }}</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="stat-item">
|
|
73
|
+
<span class="stat-label">Guests:</span>
|
|
74
|
+
<span class="stat-value">{{ data.guests }}</span>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="stat-item total-stat">
|
|
77
|
+
<span class="stat-label">Total:</span>
|
|
78
|
+
<span class="stat-value">{{ data.total }}</span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
12
84
|
</div>
|
|
13
85
|
|
|
14
86
|
<!-- Search and Filter - Hide when form is shown -->
|
|
15
|
-
<div class="search-filter" v-if="!showAddForm">
|
|
87
|
+
<div class="search-filter" v-if="!showAddForm && !showGuestForm">
|
|
16
88
|
<input type="text" placeholder="Search crew by name or role..." v-model="searchQuery" @input="handleSearch">
|
|
17
89
|
<select v-model="filterStatus" @change="handleFilter">
|
|
18
90
|
<option value="all">All Statuses</option>
|
|
19
91
|
<option value="available">Available</option>
|
|
20
92
|
<option value="onduty">On Duty</option>
|
|
93
|
+
<option value="onboard">On Board</option>
|
|
94
|
+
<option value="assigned">Assigned</option>
|
|
21
95
|
<option value="unavailable">Unavailable</option>
|
|
22
96
|
</select>
|
|
23
97
|
<button class="btn btn-secondary" @click="showTimesheet = !showTimesheet">
|
|
@@ -27,7 +101,7 @@
|
|
|
27
101
|
</div>
|
|
28
102
|
|
|
29
103
|
<!-- Loading State - Hide when form is shown -->
|
|
30
|
-
<div v-if="loading && !showAddForm" class="loading-state">
|
|
104
|
+
<div v-if="loading && !showAddForm && !showGuestForm" class="loading-state">
|
|
31
105
|
<div class="spinner-border text-primary" role="status">
|
|
32
106
|
<span class="visually-hidden">Loading crew...</span>
|
|
33
107
|
</div>
|
|
@@ -35,7 +109,7 @@
|
|
|
35
109
|
</div>
|
|
36
110
|
|
|
37
111
|
<!-- Consolidated Timesheet View - Hide when form is shown -->
|
|
38
|
-
<div v-else-if="showTimesheet && !showAddForm" class="timesheet-view">
|
|
112
|
+
<div v-else-if="showTimesheet && !showAddForm && !showGuestForm" class="timesheet-view">
|
|
39
113
|
<div class="timesheet-header">
|
|
40
114
|
<h3><i class="bi bi-table"></i> Crew Activity Timesheet</h3>
|
|
41
115
|
<div class="timesheet-controls">
|
|
@@ -119,32 +193,52 @@
|
|
|
119
193
|
</div>
|
|
120
194
|
|
|
121
195
|
<!-- Crew Grid - Hide when form is shown -->
|
|
122
|
-
<div class="crew-grid" v-else-if="filteredCrew.length > 0 && !showAddForm">
|
|
196
|
+
<div class="crew-grid" v-else-if="filteredCrew.length > 0 && !showAddForm && !showGuestForm">
|
|
123
197
|
<div v-for="member in filteredCrew" :key="member.id" class="crew-card"
|
|
124
|
-
:class="{ 'unavailable': member.status === 'unavailable' }">
|
|
198
|
+
:class="{ 'unavailable': member.status === 'unavailable', 'guest-card': member.role === 'guest' }">
|
|
125
199
|
<div :class="['crew-status-badge', getStatusClass(member.status)]">
|
|
126
200
|
{{ formatStatus(member.status) }}
|
|
127
201
|
</div>
|
|
128
202
|
|
|
129
203
|
<div class="crew-name">{{ member.name }}</div>
|
|
130
|
-
<div class="crew-role">{{ member.role }}</div>
|
|
204
|
+
<div class="crew-role">{{ member.role === 'guest' ? 'Guest' : member.role }}</div>
|
|
131
205
|
|
|
132
|
-
<!-- Certifications -->
|
|
133
|
-
<div class="crew-certifications">
|
|
134
|
-
<div v-for="cert in member
|
|
206
|
+
<!-- Certifications - Only show for non-guests -->
|
|
207
|
+
<div class="crew-certifications" v-if="member.role !== 'guest'">
|
|
208
|
+
<div v-for="cert in getApprovedCertifications(member)" :key="cert.name" class="certification-tag"
|
|
135
209
|
:class="getCertificationClass(cert.expiryDate)" @click="handleViewCertification(cert, member)">
|
|
136
210
|
{{ cert.name }}
|
|
137
211
|
</div>
|
|
212
|
+
<!-- Optional: Show pending certifications count -->
|
|
213
|
+
<div v-if="getPendingCertificationsCount(member) > 0" class="certification-tag text-muted">
|
|
214
|
+
<i class="bi bi-clock-history"></i> {{ getPendingCertificationsCount(member) }} pending
|
|
215
|
+
</div>
|
|
138
216
|
<i class="bi bi-patch-plus-fill icon" @click="handleAddCertification(member)"
|
|
139
217
|
v-if="canEditCrew"></i>
|
|
140
218
|
</div>
|
|
141
219
|
|
|
142
|
-
<!-- Crew Details -->
|
|
143
|
-
<div
|
|
144
|
-
<
|
|
220
|
+
<!-- Crew Details - Different for guests -->
|
|
221
|
+
<div v-if="member.role === 'guest'">
|
|
222
|
+
<div class="crew-availability">
|
|
223
|
+
<strong>Arrival Date:</strong> {{ member.nextShift || 'Not Recorded' }}
|
|
224
|
+
</div>
|
|
225
|
+
<div class="crew-availability">
|
|
226
|
+
<strong>Expected Days Onboard:</strong> {{ member.onBoard || 'Not Specified' }}
|
|
227
|
+
</div>
|
|
228
|
+
<div class="crew-availability">
|
|
229
|
+
<strong>Days Onboard:</strong> {{ getGuestDaysOnboard(member) }}
|
|
230
|
+
</div>
|
|
145
231
|
</div>
|
|
146
|
-
<div
|
|
147
|
-
<
|
|
232
|
+
<div v-else>
|
|
233
|
+
<div class="crew-availability">
|
|
234
|
+
<strong>Embarkation Date:</strong> {{ getEmbarkationDate(member) }}
|
|
235
|
+
</div>
|
|
236
|
+
<div class="crew-availability">
|
|
237
|
+
<strong>Expected Days Onboard:</strong> {{ member.onBoard || 'Not Scheduled' }}
|
|
238
|
+
</div>
|
|
239
|
+
<div class="crew-availability">
|
|
240
|
+
<strong>Days Onboard:</strong> {{ getDaysOnboard(member) }}
|
|
241
|
+
</div>
|
|
148
242
|
</div>
|
|
149
243
|
|
|
150
244
|
<!-- Action Buttons -->
|
|
@@ -155,14 +249,32 @@
|
|
|
155
249
|
<div v-else class="status-unavailable crew-availability vcard">
|
|
156
250
|
Vessel: Unassigned
|
|
157
251
|
</div>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
v-if="canDeboardCrew && member.status === 'onduty'">
|
|
164
|
-
Deboard
|
|
252
|
+
|
|
253
|
+
<!-- Guest-specific actions -->
|
|
254
|
+
<button class="btn btn-warning" @click="handleCheckoutGuest(member)"
|
|
255
|
+
v-if="member.role === 'guest' && member.status === 'onboard'">
|
|
256
|
+
Check Out
|
|
165
257
|
</button>
|
|
258
|
+
|
|
259
|
+
<!-- Crew-specific actions -->
|
|
260
|
+
<template v-if="member.role !== 'guest'">
|
|
261
|
+
<!-- Available: Show Assign Shift button -->
|
|
262
|
+
<button class="btn btn-primary" @click="handleAssignShift(member)"
|
|
263
|
+
v-if="canAssignShift && member.status === 'available'">
|
|
264
|
+
Assign Shift
|
|
265
|
+
</button>
|
|
266
|
+
<!-- On Duty: Show Check In button -->
|
|
267
|
+
<button class="btn btn-success" @click="handleCheckinCrew(member)"
|
|
268
|
+
v-if="canDeboardCrew && member.status === 'onduty'">
|
|
269
|
+
Check In
|
|
270
|
+
</button>
|
|
271
|
+
<!-- Onboard: Show Deboard button -->
|
|
272
|
+
<button class="btn btn-warning" @click="handleDeboardCrew(member)"
|
|
273
|
+
v-if="canDeboardCrew && member.status === 'onboard'">
|
|
274
|
+
Deboard
|
|
275
|
+
</button>
|
|
276
|
+
</template>
|
|
277
|
+
<!-- Unavailable & Assigned: No buttons -->
|
|
166
278
|
</div>
|
|
167
279
|
|
|
168
280
|
<!-- Crew Log Button and Delete Icon -->
|
|
@@ -196,7 +308,7 @@
|
|
|
196
308
|
</div>
|
|
197
309
|
|
|
198
310
|
<!-- No Results - Hide when form is shown -->
|
|
199
|
-
<div v-else-if="!loading && !showTimesheet && !showAddForm" class="no-results">
|
|
311
|
+
<div v-else-if="!loading && !showTimesheet && !showAddForm && !showGuestForm" class="no-results">
|
|
200
312
|
{{ crew.length === 0 ? 'No crew members found.' : 'No crew members found matching your search criteria.' }}
|
|
201
313
|
</div>
|
|
202
314
|
|
|
@@ -225,14 +337,6 @@
|
|
|
225
337
|
</div>
|
|
226
338
|
|
|
227
339
|
<div class="form-row">
|
|
228
|
-
<div class="form-group">
|
|
229
|
-
<label for="crew-status">Status *</label>
|
|
230
|
-
<select id="crew-status" v-model="newCrew.status">
|
|
231
|
-
<option value="available">Available</option>
|
|
232
|
-
<option value="onduty">On Duty</option>
|
|
233
|
-
<option value="unavailable">Unavailable</option>
|
|
234
|
-
</select>
|
|
235
|
-
</div>
|
|
236
340
|
<div class="form-group">
|
|
237
341
|
<label for="crew-email">Email Address *</label>
|
|
238
342
|
<input type="email" id="crew-email" v-model="newCrew.email" :class="{ 'error': formErrors.email }">
|
|
@@ -240,7 +344,7 @@
|
|
|
240
344
|
</div>
|
|
241
345
|
</div>
|
|
242
346
|
|
|
243
|
-
<!-- Certifications Section
|
|
347
|
+
<!-- Certifications Section
|
|
244
348
|
<div class="certification-section">
|
|
245
349
|
<h3>Certifications</h3>
|
|
246
350
|
<div v-for="(cert, index) in newCrew.certifications" :key="index" class="certification-entry">
|
|
@@ -290,6 +394,7 @@
|
|
|
290
394
|
+ Add More Certification
|
|
291
395
|
</button>
|
|
292
396
|
</div>
|
|
397
|
+
-->
|
|
293
398
|
|
|
294
399
|
<!-- Notes -->
|
|
295
400
|
<div class="form-row">
|
|
@@ -305,6 +410,71 @@
|
|
|
305
410
|
<button class="btn btn-primary" @click="handleAddCrewMember">Add Crew Member</button>
|
|
306
411
|
</div>
|
|
307
412
|
</div>
|
|
413
|
+
|
|
414
|
+
<!-- Guest Check-in Form -->
|
|
415
|
+
<div class="add-crew-form" v-if="showGuestForm">
|
|
416
|
+
<h2>Check-in Guest</h2>
|
|
417
|
+
|
|
418
|
+
<!-- Basic Information -->
|
|
419
|
+
<div class="form-row">
|
|
420
|
+
<div class="form-group">
|
|
421
|
+
<label for="guest-name">Full Name *</label>
|
|
422
|
+
<input type="text" id="guest-name" v-model="newGuest.name"
|
|
423
|
+
:class="{ 'error': guestFormErrors.name }">
|
|
424
|
+
<div v-if="guestFormErrors.name" class="error-message">{{ guestFormErrors.name }}</div>
|
|
425
|
+
</div>
|
|
426
|
+
<div class="form-group">
|
|
427
|
+
<label for="guest-email">Email Address</label>
|
|
428
|
+
<input type="email" id="guest-email" v-model="newGuest.email">
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<!-- Vessel Selection - Only show when not on a specific vessel page -->
|
|
433
|
+
<div class="form-row" v-if="!vesselName">
|
|
434
|
+
<div class="form-group">
|
|
435
|
+
<label for="guest-vessel">Vessel *</label>
|
|
436
|
+
<select id="guest-vessel" v-model="newGuest.vessel" :class="{ 'error': guestFormErrors.vessel }">
|
|
437
|
+
<option value="">Select a vessel</option>
|
|
438
|
+
<option v-for="vessel in availableVessels" :key="vessel" :value="vessel">
|
|
439
|
+
{{ vessel }}
|
|
440
|
+
</option>
|
|
441
|
+
</select>
|
|
442
|
+
<div v-if="guestFormErrors.vessel" class="error-message">{{ guestFormErrors.vessel }}</div>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<div class="form-row">
|
|
447
|
+
<div class="form-group">
|
|
448
|
+
<label for="guest-arrival">Arrival Date *</label>
|
|
449
|
+
<input type="date" id="guest-arrival" v-model="newGuest.arrivalDate"
|
|
450
|
+
:class="{ 'error': guestFormErrors.arrivalDate }">
|
|
451
|
+
<div v-if="guestFormErrors.arrivalDate" class="error-message">{{ guestFormErrors.arrivalDate }}
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
<div class="form-group">
|
|
455
|
+
<label for="guest-expected-days">Expected Days Onboard *</label>
|
|
456
|
+
<input type="number" id="guest-expected-days" v-model="newGuest.expectedDays" min="1"
|
|
457
|
+
:class="{ 'error': guestFormErrors.expectedDays }">
|
|
458
|
+
<div v-if="guestFormErrors.expectedDays" class="error-message">{{ guestFormErrors.expectedDays }}
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<!-- Notes -->
|
|
464
|
+
<div class="form-row">
|
|
465
|
+
<div class="form-group">
|
|
466
|
+
<label for="guest-notes">Notes</label>
|
|
467
|
+
<textarea id="guest-notes" rows="3" v-model="newGuest.notes"
|
|
468
|
+
placeholder="Purpose of visit, company affiliation, etc."></textarea>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<!-- Form Actions -->
|
|
473
|
+
<div class="form-actions">
|
|
474
|
+
<button class="btn btn-secondary" @click="handleCancelGuestForm">Cancel</button>
|
|
475
|
+
<button class="btn btn-primary" @click="handleCheckinGuest">Check-in Guest</button>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
308
478
|
</div>
|
|
309
479
|
</div>
|
|
310
480
|
</template>
|
|
@@ -323,6 +493,11 @@ export default {
|
|
|
323
493
|
type: String,
|
|
324
494
|
default: ''
|
|
325
495
|
},
|
|
496
|
+
currentCompanyId: {
|
|
497
|
+
type: String,
|
|
498
|
+
required: true,
|
|
499
|
+
default: ''
|
|
500
|
+
},
|
|
326
501
|
userProfile: {
|
|
327
502
|
type: Object,
|
|
328
503
|
default: () => ({ role: 'viewer' })
|
|
@@ -352,14 +527,18 @@ export default {
|
|
|
352
527
|
'crew-add',
|
|
353
528
|
'crew-edit',
|
|
354
529
|
'crew-delete',
|
|
530
|
+
'view-pending-certifications',
|
|
355
531
|
'crew-assign-shift',
|
|
356
532
|
'crew-deboard',
|
|
533
|
+
'crew-checkin',
|
|
357
534
|
'crew-add-certification',
|
|
358
535
|
'crew-view-certification',
|
|
359
536
|
'search-changed',
|
|
360
537
|
'filter-changed',
|
|
361
538
|
'access-denied',
|
|
362
|
-
'upload-cert-image'
|
|
539
|
+
'upload-cert-image',
|
|
540
|
+
'guest-checkin',
|
|
541
|
+
'guest-checkout'
|
|
363
542
|
],
|
|
364
543
|
|
|
365
544
|
data() {
|
|
@@ -367,7 +546,9 @@ export default {
|
|
|
367
546
|
searchQuery: '',
|
|
368
547
|
filterStatus: 'all',
|
|
369
548
|
showAddForm: false,
|
|
549
|
+
showGuestForm: false,
|
|
370
550
|
formErrors: {},
|
|
551
|
+
guestFormErrors: {},
|
|
371
552
|
expandedLogs: [],
|
|
372
553
|
showTimesheet: false,
|
|
373
554
|
timesheetFilter: 'all',
|
|
@@ -391,6 +572,14 @@ export default {
|
|
|
391
572
|
notes: '',
|
|
392
573
|
email: '',
|
|
393
574
|
onBoard: ''
|
|
575
|
+
},
|
|
576
|
+
newGuest: {
|
|
577
|
+
name: '',
|
|
578
|
+
email: '',
|
|
579
|
+
vessel: '',
|
|
580
|
+
arrivalDate: '',
|
|
581
|
+
expectedDays: '',
|
|
582
|
+
notes: ''
|
|
394
583
|
}
|
|
395
584
|
}
|
|
396
585
|
},
|
|
@@ -400,6 +589,29 @@ export default {
|
|
|
400
589
|
return this.vesselName ? `Current Crew for ${this.vesselName}` : 'All Fleet Crew'
|
|
401
590
|
},
|
|
402
591
|
|
|
592
|
+
getApprovedCertifications() {
|
|
593
|
+
return (member) => {
|
|
594
|
+
if (!member.certifications || member.certifications.length === 0) {
|
|
595
|
+
return []
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return member.certifications.filter(cert => {
|
|
599
|
+
// If no verifications exist, certificate is not approved by anyone
|
|
600
|
+
if (!cert.verifications || cert.verifications.length === 0) {
|
|
601
|
+
return false
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Find verification for current company
|
|
605
|
+
const companyVerification = cert.verifications.find(
|
|
606
|
+
v => v.companyId === this.currentCompanyId
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
// Only show if approved by this company
|
|
610
|
+
return companyVerification && companyVerification.status === 'approved'
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
|
|
403
615
|
filteredCrew() {
|
|
404
616
|
return this.crew.filter(member => {
|
|
405
617
|
const matchesSearch = this.searchQuery === '' ||
|
|
@@ -493,6 +705,72 @@ export default {
|
|
|
493
705
|
|
|
494
706
|
canDeboardCrew() {
|
|
495
707
|
return this.config.enableAssignShift && this.hasPermission('assign')
|
|
708
|
+
},
|
|
709
|
+
|
|
710
|
+
// Current date for display
|
|
711
|
+
currentDate() {
|
|
712
|
+
return new Date().toLocaleDateString('en-US', {
|
|
713
|
+
weekday: 'long',
|
|
714
|
+
year: 'numeric',
|
|
715
|
+
month: 'long',
|
|
716
|
+
day: 'numeric'
|
|
717
|
+
})
|
|
718
|
+
},
|
|
719
|
+
|
|
720
|
+
// Total personnel onboard
|
|
721
|
+
totalOnboard() {
|
|
722
|
+
return this.crew.filter(member => member.status === 'onboard').length
|
|
723
|
+
},
|
|
724
|
+
|
|
725
|
+
// Crew members onboard (excluding guests)
|
|
726
|
+
crewOnboard() {
|
|
727
|
+
return this.crew.filter(member =>
|
|
728
|
+
member.status === 'onboard' && member.role !== 'guest'
|
|
729
|
+
).length
|
|
730
|
+
},
|
|
731
|
+
|
|
732
|
+
// Guests onboard
|
|
733
|
+
guestsOnboard() {
|
|
734
|
+
return this.crew.filter(member =>
|
|
735
|
+
member.status === 'onboard' && member.role === 'guest'
|
|
736
|
+
).length
|
|
737
|
+
},
|
|
738
|
+
|
|
739
|
+
// Personnel breakdown by vessel
|
|
740
|
+
personnelByVessel() {
|
|
741
|
+
const breakdown = {}
|
|
742
|
+
|
|
743
|
+
this.crew.forEach(member => {
|
|
744
|
+
if (member.status === 'onboard' && member.vessel) {
|
|
745
|
+
if (!breakdown[member.vessel]) {
|
|
746
|
+
breakdown[member.vessel] = {
|
|
747
|
+
crew: 0,
|
|
748
|
+
guests: 0,
|
|
749
|
+
total: 0
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (member.role === 'guest') {
|
|
754
|
+
breakdown[member.vessel].guests++
|
|
755
|
+
} else {
|
|
756
|
+
breakdown[member.vessel].crew++
|
|
757
|
+
}
|
|
758
|
+
breakdown[member.vessel].total++
|
|
759
|
+
}
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
return breakdown
|
|
763
|
+
},
|
|
764
|
+
|
|
765
|
+
// Get unique vessels from crew data for vessel selector
|
|
766
|
+
availableVessels() {
|
|
767
|
+
const vessels = new Set()
|
|
768
|
+
this.crew.forEach(member => {
|
|
769
|
+
if (member.vessel && member.vessel.trim() !== '') {
|
|
770
|
+
vessels.add(member.vessel)
|
|
771
|
+
}
|
|
772
|
+
})
|
|
773
|
+
return Array.from(vessels).sort()
|
|
496
774
|
}
|
|
497
775
|
},
|
|
498
776
|
|
|
@@ -517,6 +795,124 @@ export default {
|
|
|
517
795
|
return rolePermissions.includes(action)
|
|
518
796
|
},
|
|
519
797
|
|
|
798
|
+
/**
|
|
799
|
+
* Get embarkation date based on crew member status
|
|
800
|
+
* Returns N/A if status is not onboard, onduty, or assigned
|
|
801
|
+
* @param {Object} member - The crew member
|
|
802
|
+
* @returns {String} - Embarkation date or 'N/A'
|
|
803
|
+
*/
|
|
804
|
+
getEmbarkationDate(member) {
|
|
805
|
+
const validStatuses = ['onboard', 'onduty', 'assigned']
|
|
806
|
+
if (!validStatuses.includes(member.status)) {
|
|
807
|
+
return 'N/A'
|
|
808
|
+
}
|
|
809
|
+
return member.nextShift || 'Not Scheduled'
|
|
810
|
+
},
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Calculate days onboard from logs
|
|
814
|
+
* Finds the most recent "Onboard" action and counts days from that timestamp
|
|
815
|
+
* @param {Object} member - The crew member
|
|
816
|
+
* @returns {String|Number} - Days onboard or 'N/A'
|
|
817
|
+
*/
|
|
818
|
+
getDaysOnboard(member) {
|
|
819
|
+
// Only calculate if member is currently onboard
|
|
820
|
+
if (member.status !== 'onboard') {
|
|
821
|
+
return 'N/A'
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Check if logs exist
|
|
825
|
+
if (!member.log || member.log.length === 0) {
|
|
826
|
+
return 'N/A'
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Find the most recent "Onboard" action
|
|
830
|
+
// Logs are typically sorted by timestamp, but we'll search from the end to be safe
|
|
831
|
+
let mostRecentOnboard = null
|
|
832
|
+
|
|
833
|
+
for (let i = member.log.length - 1; i >= 0; i--) {
|
|
834
|
+
if (member.log[i].action === 'Onboard') {
|
|
835
|
+
mostRecentOnboard = member.log[i]
|
|
836
|
+
break
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// If no "Onboard" action found, return N/A
|
|
841
|
+
if (!mostRecentOnboard || !mostRecentOnboard.timestamp) {
|
|
842
|
+
return 'N/A'
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Calculate days between onboard timestamp and now
|
|
846
|
+
const onboardDate = new Date(mostRecentOnboard.timestamp)
|
|
847
|
+
const today = new Date()
|
|
848
|
+
const diffTime = Math.abs(today - onboardDate)
|
|
849
|
+
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
|
850
|
+
|
|
851
|
+
return diffDays
|
|
852
|
+
},
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Calculate days onboard for guests from arrival date
|
|
856
|
+
* @param {Object} member - The guest member
|
|
857
|
+
* @returns {String|Number} - Days onboard or 'N/A'
|
|
858
|
+
*/
|
|
859
|
+
getGuestDaysOnboard(member) {
|
|
860
|
+
// Check if arrival date (nextShift) exists
|
|
861
|
+
if (!member.nextShift) {
|
|
862
|
+
return 'N/A'
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Only calculate if guest is currently onboard
|
|
866
|
+
if (member.status !== 'onboard') {
|
|
867
|
+
return 'N/A'
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Calculate days between arrival date and now
|
|
871
|
+
const arrivalDate = new Date(member.nextShift)
|
|
872
|
+
const today = new Date()
|
|
873
|
+
const diffTime = Math.abs(today - arrivalDate)
|
|
874
|
+
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
|
875
|
+
|
|
876
|
+
return diffDays
|
|
877
|
+
},
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Get count of pending certifications for a member
|
|
881
|
+
* @param {Object} member - The crew member
|
|
882
|
+
* @returns {Number} - Count of pending certifications
|
|
883
|
+
*/
|
|
884
|
+
getPendingCertificationsCount(member) {
|
|
885
|
+
if (!member.certifications || member.certifications.length === 0) {
|
|
886
|
+
return 0
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return member.certifications.filter(cert => {
|
|
890
|
+
if (!cert.verifications || cert.verifications.length === 0) {
|
|
891
|
+
return true // No verifications = pending
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const companyVerification = cert.verifications.find(
|
|
895
|
+
v => v.companyId === this.currentCompanyId
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
// Pending if: no verification OR verification is pending/rejected
|
|
899
|
+
return !companyVerification ||
|
|
900
|
+
companyVerification.status === 'pending'
|
|
901
|
+
}).length
|
|
902
|
+
},
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Emit event when user clicks on pending certifications
|
|
906
|
+
* This allows parent component to navigate to verification page
|
|
907
|
+
*/
|
|
908
|
+
handleViewPendingCertifications(member) {
|
|
909
|
+
this.$emit('view-pending-certifications', {
|
|
910
|
+
memberId: member.id,
|
|
911
|
+
memberName: member.name,
|
|
912
|
+
companyId: this.currentCompanyId
|
|
913
|
+
})
|
|
914
|
+
},
|
|
915
|
+
|
|
520
916
|
handleToggleAddForm() {
|
|
521
917
|
if (!this.canAddCrew) {
|
|
522
918
|
this.$emit('access-denied', { action: 'add crew', userProfile: this.userProfile })
|
|
@@ -528,6 +924,101 @@ export default {
|
|
|
528
924
|
}
|
|
529
925
|
},
|
|
530
926
|
|
|
927
|
+
handleToggleGuestForm() {
|
|
928
|
+
if (!this.canAddCrew) {
|
|
929
|
+
this.$emit('access-denied', { action: 'check-in guest', userProfile: this.userProfile })
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
this.showGuestForm = !this.showGuestForm
|
|
933
|
+
if (!this.showGuestForm) {
|
|
934
|
+
this.resetGuestForm()
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
|
|
938
|
+
handleCheckinGuest() {
|
|
939
|
+
if (!this.validateGuestForm()) {
|
|
940
|
+
return
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const newGuest = {
|
|
944
|
+
name: this.newGuest.name,
|
|
945
|
+
role: 'guest',
|
|
946
|
+
status: 'onboard',
|
|
947
|
+
nextShift: this.newGuest.arrivalDate, // Store arrival date in nextShift
|
|
948
|
+
onBoard: this.newGuest.expectedDays,
|
|
949
|
+
email: this.newGuest.email || '',
|
|
950
|
+
notes: this.newGuest.notes,
|
|
951
|
+
vessel: this.vesselName || this.newGuest.vessel,
|
|
952
|
+
certifications: []
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
this.$emit('guest-checkin', newGuest)
|
|
956
|
+
this.resetGuestForm()
|
|
957
|
+
this.showGuestForm = false
|
|
958
|
+
},
|
|
959
|
+
|
|
960
|
+
handleCheckoutGuest(member) {
|
|
961
|
+
if (!this.canDeboardCrew) {
|
|
962
|
+
this.$emit('access-denied', { action: 'check out guest', userProfile: this.userProfile })
|
|
963
|
+
return
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
let duration = null
|
|
967
|
+
if (member.nextShift) {
|
|
968
|
+
const arrivalDate = new Date(member.nextShift)
|
|
969
|
+
const today = new Date()
|
|
970
|
+
const diffTime = Math.abs(today - arrivalDate)
|
|
971
|
+
duration = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
this.$emit('guest-checkout', { member, duration })
|
|
975
|
+
},
|
|
976
|
+
|
|
977
|
+
handleCancelGuestForm() {
|
|
978
|
+
this.showGuestForm = false
|
|
979
|
+
this.resetGuestForm()
|
|
980
|
+
},
|
|
981
|
+
|
|
982
|
+
resetGuestForm() {
|
|
983
|
+
this.newGuest = {
|
|
984
|
+
name: '',
|
|
985
|
+
email: '',
|
|
986
|
+
vessel: '',
|
|
987
|
+
arrivalDate: '',
|
|
988
|
+
expectedDays: '',
|
|
989
|
+
notes: ''
|
|
990
|
+
}
|
|
991
|
+
this.guestFormErrors = {}
|
|
992
|
+
},
|
|
993
|
+
|
|
994
|
+
validateGuestForm() {
|
|
995
|
+
this.guestFormErrors = {}
|
|
996
|
+
|
|
997
|
+
if (!this.newGuest.name || this.newGuest.name.trim() === '') {
|
|
998
|
+
this.guestFormErrors.name = 'Guest name is required'
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Require vessel selection if not on a specific vessel page
|
|
1002
|
+
if (!this.vesselName && (!this.newGuest.vessel || this.newGuest.vessel.trim() === '')) {
|
|
1003
|
+
this.guestFormErrors.vessel = 'Please select a vessel'
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (!this.newGuest.arrivalDate) {
|
|
1007
|
+
this.guestFormErrors.arrivalDate = 'Arrival date is required'
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (!this.newGuest.expectedDays || this.newGuest.expectedDays <= 0) {
|
|
1011
|
+
this.guestFormErrors.expectedDays = 'Expected days onboard must be at least 1'
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (Object.keys(this.guestFormErrors).length > 0) {
|
|
1015
|
+
this.$emit('validation-error', this.guestFormErrors)
|
|
1016
|
+
return false
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
return true
|
|
1020
|
+
},
|
|
1021
|
+
|
|
531
1022
|
handleSearch() {
|
|
532
1023
|
this.$emit('search-changed', this.searchQuery)
|
|
533
1024
|
},
|
|
@@ -593,15 +1084,26 @@ export default {
|
|
|
593
1084
|
return
|
|
594
1085
|
}
|
|
595
1086
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1087
|
+
// Check for incomplete certifications
|
|
1088
|
+
const hasIncompleteCert = this.newCrew.certifications.some(cert => {
|
|
1089
|
+
const hasName = cert.name.trim() !== ''
|
|
1090
|
+
const hasDate = cert.expiryDate !== ''
|
|
1091
|
+
const hasImage = cert.imageFile !== null
|
|
1092
|
+
|
|
1093
|
+
// If any field is filled but not all, it's incomplete
|
|
1094
|
+
return (hasName || hasDate || hasImage) && !(hasName && hasDate && hasImage)
|
|
1095
|
+
})
|
|
599
1096
|
|
|
600
|
-
if (
|
|
601
|
-
|
|
1097
|
+
if (hasIncompleteCert) {
|
|
1098
|
+
this.$emit('incomplete-certification')
|
|
602
1099
|
return
|
|
603
1100
|
}
|
|
604
1101
|
|
|
1102
|
+
// Filter only complete certifications (all three fields filled)
|
|
1103
|
+
const validCertifications = this.newCrew.certifications.filter(cert => {
|
|
1104
|
+
return cert.name.trim() !== '' && cert.expiryDate !== '' && cert.imageFile !== null
|
|
1105
|
+
})
|
|
1106
|
+
|
|
605
1107
|
const finalRole = this.newCrew.role === 'Other' ? this.newCrew.customRole : this.newCrew.role
|
|
606
1108
|
|
|
607
1109
|
// Upload all certification images first and get their URLs
|
|
@@ -623,7 +1125,7 @@ export default {
|
|
|
623
1125
|
const newMember = {
|
|
624
1126
|
name: this.newCrew.name,
|
|
625
1127
|
role: finalRole,
|
|
626
|
-
status:
|
|
1128
|
+
status: 'unavailable',
|
|
627
1129
|
certifications: certificationsWithUrls,
|
|
628
1130
|
notes: this.newCrew.notes,
|
|
629
1131
|
vessel: this.vesselName,
|
|
@@ -695,6 +1197,15 @@ export default {
|
|
|
695
1197
|
this.$emit('crew-deboard', { member, duration })
|
|
696
1198
|
},
|
|
697
1199
|
|
|
1200
|
+
handleCheckinCrew(member) {
|
|
1201
|
+
if (!this.canDeboardCrew) {
|
|
1202
|
+
this.$emit('access-denied', { action: 'check in crew', userProfile: this.userProfile })
|
|
1203
|
+
return
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
this.$emit('crew-checkin', member)
|
|
1207
|
+
},
|
|
1208
|
+
|
|
698
1209
|
toggleCrewLog(memberId) {
|
|
699
1210
|
const index = this.expandedLogs.indexOf(memberId)
|
|
700
1211
|
if (index > -1) {
|
|
@@ -722,7 +1233,7 @@ export default {
|
|
|
722
1233
|
name: '',
|
|
723
1234
|
role: 'Deckhand',
|
|
724
1235
|
customRole: '',
|
|
725
|
-
status: '
|
|
1236
|
+
status: 'Unavailable',
|
|
726
1237
|
nextShift: '',
|
|
727
1238
|
certifications: [{
|
|
728
1239
|
name: '',
|
|
@@ -761,7 +1272,13 @@ export default {
|
|
|
761
1272
|
this.formErrors.customRole = 'Custom role is required when "Other" is selected'
|
|
762
1273
|
}
|
|
763
1274
|
|
|
764
|
-
|
|
1275
|
+
// Emit validation errors if any exist
|
|
1276
|
+
if (Object.keys(this.formErrors).length > 0) {
|
|
1277
|
+
this.$emit('validation-error', this.formErrors)
|
|
1278
|
+
return false
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return true
|
|
765
1282
|
},
|
|
766
1283
|
|
|
767
1284
|
isValidEmail(email) {
|
|
@@ -827,7 +1344,9 @@ export default {
|
|
|
827
1344
|
const statusMap = {
|
|
828
1345
|
'available': 'status-available',
|
|
829
1346
|
'onduty': 'status-onduty',
|
|
830
|
-
'
|
|
1347
|
+
'onboard': 'status-onboard',
|
|
1348
|
+
'unavailable': 'status-unavailable',
|
|
1349
|
+
'assigned': 'status-assigned'
|
|
831
1350
|
}
|
|
832
1351
|
return statusMap[status] || ''
|
|
833
1352
|
},
|
|
@@ -899,6 +1418,152 @@ export default {
|
|
|
899
1418
|
margin-bottom: 30px;
|
|
900
1419
|
}
|
|
901
1420
|
|
|
1421
|
+
/* Personnel Summary Styles */
|
|
1422
|
+
.personnel-summary {
|
|
1423
|
+
background: linear-gradient(135deg, #005792 0%, #00a8e8 100%);
|
|
1424
|
+
border-radius: 8px;
|
|
1425
|
+
padding: 20px;
|
|
1426
|
+
margin-bottom: 25px;
|
|
1427
|
+
color: white;
|
|
1428
|
+
box-shadow: 0 4px 12px rgba(0, 87, 146, 0.2);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
.summary-header {
|
|
1432
|
+
display: flex;
|
|
1433
|
+
justify-content: space-between;
|
|
1434
|
+
align-items: center;
|
|
1435
|
+
margin-bottom: 20px;
|
|
1436
|
+
padding-bottom: 15px;
|
|
1437
|
+
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
.summary-header h3 {
|
|
1441
|
+
margin: 0;
|
|
1442
|
+
font-size: 1.4em;
|
|
1443
|
+
display: flex;
|
|
1444
|
+
align-items: center;
|
|
1445
|
+
gap: 10px;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
.summary-date {
|
|
1449
|
+
font-size: 0.9em;
|
|
1450
|
+
opacity: 0.9;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
.summary-cards {
|
|
1454
|
+
display: grid;
|
|
1455
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
1456
|
+
gap: 15px;
|
|
1457
|
+
margin-bottom: 25px;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
.summary-card {
|
|
1461
|
+
background: linear-gradient(135deg, #005792 0%, #00a8e8 100%);
|
|
1462
|
+
backdrop-filter: blur(10px);
|
|
1463
|
+
border-radius: 8px;
|
|
1464
|
+
padding: 20px;
|
|
1465
|
+
display: flex;
|
|
1466
|
+
align-items: center;
|
|
1467
|
+
gap: 15px;
|
|
1468
|
+
transition: transform 0.2s, background 0.2s;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
.summary-card:hover {
|
|
1472
|
+
transform: translateY(-3px);
|
|
1473
|
+
background: rgba(255, 255, 255, 0.2);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
.card-icon {
|
|
1477
|
+
font-size: 2.5em;
|
|
1478
|
+
opacity: 0.9;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.card-content {
|
|
1482
|
+
flex: 1;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
.card-value {
|
|
1486
|
+
font-size: 2.5em;
|
|
1487
|
+
font-weight: bold;
|
|
1488
|
+
line-height: 1;
|
|
1489
|
+
margin-bottom: 5px;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
.card-label {
|
|
1493
|
+
font-size: 0.9em;
|
|
1494
|
+
opacity: 0.9;
|
|
1495
|
+
text-transform: uppercase;
|
|
1496
|
+
letter-spacing: 0.5px;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.total-card {
|
|
1500
|
+
background: linear-gradient(135deg, #005792 0%, #00a8e8 100%);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
.vessel-breakdown {
|
|
1504
|
+
margin-top: 20px;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.vessel-breakdown h4 {
|
|
1508
|
+
margin: 0 0 15px 0;
|
|
1509
|
+
font-size: 1.1em;
|
|
1510
|
+
opacity: 0.95;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
.vessel-cards {
|
|
1514
|
+
display: grid;
|
|
1515
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
1516
|
+
gap: 12px;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
.vessel-card {
|
|
1520
|
+
background: rgba(255, 255, 255, 0.15);
|
|
1521
|
+
backdrop-filter: blur(10px);
|
|
1522
|
+
border-radius: 6px;
|
|
1523
|
+
padding: 15px;
|
|
1524
|
+
border-left: 4px solid rgba(255, 255, 255, 0.5);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
.vessel-name {
|
|
1528
|
+
font-weight: bold;
|
|
1529
|
+
font-size: 1.1em;
|
|
1530
|
+
margin-bottom: 10px;
|
|
1531
|
+
padding-bottom: 8px;
|
|
1532
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
.vessel-stats {
|
|
1536
|
+
display: flex;
|
|
1537
|
+
justify-content: space-between;
|
|
1538
|
+
gap: 10px;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
.stat-item {
|
|
1542
|
+
display: flex;
|
|
1543
|
+
flex-direction: column;
|
|
1544
|
+
gap: 3px;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
.stat-label {
|
|
1548
|
+
font-size: 0.8em;
|
|
1549
|
+
opacity: 0.8;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
.stat-value {
|
|
1553
|
+
font-size: 1.5em;
|
|
1554
|
+
font-weight: bold;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
.total-stat {
|
|
1558
|
+
border-left: 1px solid rgba(255, 255, 255, 0.3);
|
|
1559
|
+
padding-left: 10px;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
.total-stat .stat-value {
|
|
1563
|
+
color: #ffd700;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
|
|
902
1567
|
.crew-grid {
|
|
903
1568
|
display: grid;
|
|
904
1569
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
@@ -976,11 +1641,21 @@ export default {
|
|
|
976
1641
|
color: #721c24;
|
|
977
1642
|
}
|
|
978
1643
|
|
|
1644
|
+
.status-assigned {
|
|
1645
|
+
background-color: #ffc107;
|
|
1646
|
+
color: #212529;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
979
1649
|
.status-onduty {
|
|
980
1650
|
background-color: #cce5ff;
|
|
981
1651
|
color: #004085;
|
|
982
1652
|
}
|
|
983
1653
|
|
|
1654
|
+
.status-onboard {
|
|
1655
|
+
background-color: #d1ecf1;
|
|
1656
|
+
color: #0c5460;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
984
1659
|
.action-buttons {
|
|
985
1660
|
display: flex;
|
|
986
1661
|
justify-content: space-between;
|
|
@@ -1025,6 +1700,15 @@ export default {
|
|
|
1025
1700
|
background-color: #e0a800;
|
|
1026
1701
|
}
|
|
1027
1702
|
|
|
1703
|
+
.btn-success {
|
|
1704
|
+
background-color: #28a745;
|
|
1705
|
+
color: white;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
.btn-success:hover {
|
|
1709
|
+
background-color: #218838;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1028
1712
|
.btn-danger {
|
|
1029
1713
|
background-color: #dc3545;
|
|
1030
1714
|
color: white;
|
|
@@ -1275,6 +1959,23 @@ export default {
|
|
|
1275
1959
|
margin: 0;
|
|
1276
1960
|
}
|
|
1277
1961
|
|
|
1962
|
+
.header-actions {
|
|
1963
|
+
display: flex;
|
|
1964
|
+
gap: 10px;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
.guest-card {
|
|
1968
|
+
border-left-color: #ffc107 !important;
|
|
1969
|
+
background: linear-gradient(135deg, #f8f9fa 0%, #e3f2fd 100%);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
.guest-card .crew-role {
|
|
1973
|
+
color: #17a2b8;
|
|
1974
|
+
font-weight: 600;
|
|
1975
|
+
text-transform: uppercase;
|
|
1976
|
+
font-size: 0.85em;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1278
1979
|
.vcard {
|
|
1279
1980
|
border-radius: 6px;
|
|
1280
1981
|
padding: 5px;
|
|
@@ -1655,7 +2356,7 @@ export default {
|
|
|
1655
2356
|
padding: 8px 4px;
|
|
1656
2357
|
}
|
|
1657
2358
|
}
|
|
1658
|
-
|
|
2359
|
+
|
|
2360
|
+
.crew-subhead {
|
|
1659
2361
|
color: #000;
|
|
1660
|
-
}
|
|
1661
|
-
</style>
|
|
2362
|
+
}</style>
|