oceanhelm 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/oceanhelm.es.js +3211 -1416
- package/dist/oceanhelm.es.js.map +1 -1
- package/dist/oceanhelm.umd.js +9 -1
- package/dist/oceanhelm.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ActivityLogs.vue +319 -330
- package/src/components/ConfigurableSidebar.vue +55 -8
- package/src/components/CrewManagement.vue +686 -36
- package/src/components/Reports.vue +2985 -428
- package/src/components/RequisitionSystem.vue +97 -67
- package/src/utils/sidebarConfig.js +62 -48
|
@@ -6,13 +6,85 @@
|
|
|
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>
|
|
@@ -29,7 +101,7 @@
|
|
|
29
101
|
</div>
|
|
30
102
|
|
|
31
103
|
<!-- Loading State - Hide when form is shown -->
|
|
32
|
-
<div v-if="loading && !showAddForm" class="loading-state">
|
|
104
|
+
<div v-if="loading && !showAddForm && !showGuestForm" class="loading-state">
|
|
33
105
|
<div class="spinner-border text-primary" role="status">
|
|
34
106
|
<span class="visually-hidden">Loading crew...</span>
|
|
35
107
|
</div>
|
|
@@ -37,7 +109,7 @@
|
|
|
37
109
|
</div>
|
|
38
110
|
|
|
39
111
|
<!-- Consolidated Timesheet View - Hide when form is shown -->
|
|
40
|
-
<div v-else-if="showTimesheet && !showAddForm" class="timesheet-view">
|
|
112
|
+
<div v-else-if="showTimesheet && !showAddForm && !showGuestForm" class="timesheet-view">
|
|
41
113
|
<div class="timesheet-header">
|
|
42
114
|
<h3><i class="bi bi-table"></i> Crew Activity Timesheet</h3>
|
|
43
115
|
<div class="timesheet-controls">
|
|
@@ -121,32 +193,52 @@
|
|
|
121
193
|
</div>
|
|
122
194
|
|
|
123
195
|
<!-- Crew Grid - Hide when form is shown -->
|
|
124
|
-
<div class="crew-grid" v-else-if="filteredCrew.length > 0 && !showAddForm">
|
|
196
|
+
<div class="crew-grid" v-else-if="filteredCrew.length > 0 && !showAddForm && !showGuestForm">
|
|
125
197
|
<div v-for="member in filteredCrew" :key="member.id" class="crew-card"
|
|
126
|
-
:class="{ 'unavailable': member.status === 'unavailable' }">
|
|
198
|
+
:class="{ 'unavailable': member.status === 'unavailable', 'guest-card': member.role === 'guest' }">
|
|
127
199
|
<div :class="['crew-status-badge', getStatusClass(member.status)]">
|
|
128
200
|
{{ formatStatus(member.status) }}
|
|
129
201
|
</div>
|
|
130
202
|
|
|
131
203
|
<div class="crew-name">{{ member.name }}</div>
|
|
132
|
-
<div class="crew-role">{{ member.role }}</div>
|
|
204
|
+
<div class="crew-role">{{ member.role === 'guest' ? 'Guest' : member.role }}</div>
|
|
133
205
|
|
|
134
|
-
<!-- Certifications -->
|
|
135
|
-
<div class="crew-certifications">
|
|
136
|
-
<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"
|
|
137
209
|
:class="getCertificationClass(cert.expiryDate)" @click="handleViewCertification(cert, member)">
|
|
138
210
|
{{ cert.name }}
|
|
139
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>
|
|
140
216
|
<i class="bi bi-patch-plus-fill icon" @click="handleAddCertification(member)"
|
|
141
217
|
v-if="canEditCrew"></i>
|
|
142
218
|
</div>
|
|
143
219
|
|
|
144
|
-
<!-- Crew Details -->
|
|
145
|
-
<div
|
|
146
|
-
<
|
|
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>
|
|
147
231
|
</div>
|
|
148
|
-
<div
|
|
149
|
-
<
|
|
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>
|
|
150
242
|
</div>
|
|
151
243
|
|
|
152
244
|
<!-- Action Buttons -->
|
|
@@ -157,21 +249,31 @@
|
|
|
157
249
|
<div v-else class="status-unavailable crew-availability vcard">
|
|
158
250
|
Vessel: Unassigned
|
|
159
251
|
</div>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<!-- On Duty: Show Check In button -->
|
|
166
|
-
<button class="btn btn-success" @click="handleCheckinCrew(member)"
|
|
167
|
-
v-if="canDeboardCrew && member.status === 'onduty'">
|
|
168
|
-
Check In
|
|
169
|
-
</button>
|
|
170
|
-
<!-- Onboard: Show Deboard button -->
|
|
171
|
-
<button class="btn btn-warning" @click="handleDeboardCrew(member)"
|
|
172
|
-
v-if="canDeboardCrew && member.status === 'onboard'">
|
|
173
|
-
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
|
|
174
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>
|
|
175
277
|
<!-- Unavailable & Assigned: No buttons -->
|
|
176
278
|
</div>
|
|
177
279
|
|
|
@@ -206,7 +308,7 @@
|
|
|
206
308
|
</div>
|
|
207
309
|
|
|
208
310
|
<!-- No Results - Hide when form is shown -->
|
|
209
|
-
<div v-else-if="!loading && !showTimesheet && !showAddForm" class="no-results">
|
|
311
|
+
<div v-else-if="!loading && !showTimesheet && !showAddForm && !showGuestForm" class="no-results">
|
|
210
312
|
{{ crew.length === 0 ? 'No crew members found.' : 'No crew members found matching your search criteria.' }}
|
|
211
313
|
</div>
|
|
212
314
|
|
|
@@ -242,7 +344,7 @@
|
|
|
242
344
|
</div>
|
|
243
345
|
</div>
|
|
244
346
|
|
|
245
|
-
<!-- Certifications Section
|
|
347
|
+
<!-- Certifications Section
|
|
246
348
|
<div class="certification-section">
|
|
247
349
|
<h3>Certifications</h3>
|
|
248
350
|
<div v-for="(cert, index) in newCrew.certifications" :key="index" class="certification-entry">
|
|
@@ -292,6 +394,7 @@
|
|
|
292
394
|
+ Add More Certification
|
|
293
395
|
</button>
|
|
294
396
|
</div>
|
|
397
|
+
-->
|
|
295
398
|
|
|
296
399
|
<!-- Notes -->
|
|
297
400
|
<div class="form-row">
|
|
@@ -307,6 +410,71 @@
|
|
|
307
410
|
<button class="btn btn-primary" @click="handleAddCrewMember">Add Crew Member</button>
|
|
308
411
|
</div>
|
|
309
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>
|
|
310
478
|
</div>
|
|
311
479
|
</div>
|
|
312
480
|
</template>
|
|
@@ -325,6 +493,11 @@ export default {
|
|
|
325
493
|
type: String,
|
|
326
494
|
default: ''
|
|
327
495
|
},
|
|
496
|
+
currentCompanyId: {
|
|
497
|
+
type: String,
|
|
498
|
+
required: true,
|
|
499
|
+
default: ''
|
|
500
|
+
},
|
|
328
501
|
userProfile: {
|
|
329
502
|
type: Object,
|
|
330
503
|
default: () => ({ role: 'viewer' })
|
|
@@ -354,6 +527,7 @@ export default {
|
|
|
354
527
|
'crew-add',
|
|
355
528
|
'crew-edit',
|
|
356
529
|
'crew-delete',
|
|
530
|
+
'view-pending-certifications',
|
|
357
531
|
'crew-assign-shift',
|
|
358
532
|
'crew-deboard',
|
|
359
533
|
'crew-checkin',
|
|
@@ -362,7 +536,9 @@ export default {
|
|
|
362
536
|
'search-changed',
|
|
363
537
|
'filter-changed',
|
|
364
538
|
'access-denied',
|
|
365
|
-
'upload-cert-image'
|
|
539
|
+
'upload-cert-image',
|
|
540
|
+
'guest-checkin',
|
|
541
|
+
'guest-checkout'
|
|
366
542
|
],
|
|
367
543
|
|
|
368
544
|
data() {
|
|
@@ -370,7 +546,9 @@ export default {
|
|
|
370
546
|
searchQuery: '',
|
|
371
547
|
filterStatus: 'all',
|
|
372
548
|
showAddForm: false,
|
|
549
|
+
showGuestForm: false,
|
|
373
550
|
formErrors: {},
|
|
551
|
+
guestFormErrors: {},
|
|
374
552
|
expandedLogs: [],
|
|
375
553
|
showTimesheet: false,
|
|
376
554
|
timesheetFilter: 'all',
|
|
@@ -394,6 +572,14 @@ export default {
|
|
|
394
572
|
notes: '',
|
|
395
573
|
email: '',
|
|
396
574
|
onBoard: ''
|
|
575
|
+
},
|
|
576
|
+
newGuest: {
|
|
577
|
+
name: '',
|
|
578
|
+
email: '',
|
|
579
|
+
vessel: '',
|
|
580
|
+
arrivalDate: '',
|
|
581
|
+
expectedDays: '',
|
|
582
|
+
notes: ''
|
|
397
583
|
}
|
|
398
584
|
}
|
|
399
585
|
},
|
|
@@ -403,6 +589,29 @@ export default {
|
|
|
403
589
|
return this.vesselName ? `Current Crew for ${this.vesselName}` : 'All Fleet Crew'
|
|
404
590
|
},
|
|
405
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
|
+
|
|
406
615
|
filteredCrew() {
|
|
407
616
|
return this.crew.filter(member => {
|
|
408
617
|
const matchesSearch = this.searchQuery === '' ||
|
|
@@ -496,6 +705,72 @@ export default {
|
|
|
496
705
|
|
|
497
706
|
canDeboardCrew() {
|
|
498
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()
|
|
499
774
|
}
|
|
500
775
|
},
|
|
501
776
|
|
|
@@ -520,6 +795,124 @@ export default {
|
|
|
520
795
|
return rolePermissions.includes(action)
|
|
521
796
|
},
|
|
522
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
|
+
|
|
523
916
|
handleToggleAddForm() {
|
|
524
917
|
if (!this.canAddCrew) {
|
|
525
918
|
this.$emit('access-denied', { action: 'add crew', userProfile: this.userProfile })
|
|
@@ -531,6 +924,101 @@ export default {
|
|
|
531
924
|
}
|
|
532
925
|
},
|
|
533
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
|
+
|
|
534
1022
|
handleSearch() {
|
|
535
1023
|
this.$emit('search-changed', this.searchQuery)
|
|
536
1024
|
},
|
|
@@ -930,6 +1418,152 @@ export default {
|
|
|
930
1418
|
margin-bottom: 30px;
|
|
931
1419
|
}
|
|
932
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
|
+
|
|
933
1567
|
.crew-grid {
|
|
934
1568
|
display: grid;
|
|
935
1569
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
@@ -1325,6 +1959,23 @@ export default {
|
|
|
1325
1959
|
margin: 0;
|
|
1326
1960
|
}
|
|
1327
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
|
+
|
|
1328
1979
|
.vcard {
|
|
1329
1980
|
border-radius: 6px;
|
|
1330
1981
|
padding: 5px;
|
|
@@ -1708,5 +2359,4 @@ export default {
|
|
|
1708
2359
|
|
|
1709
2360
|
.crew-subhead {
|
|
1710
2361
|
color: #000;
|
|
1711
|
-
}
|
|
1712
|
-
</style>
|
|
2362
|
+
}</style>
|