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.
@@ -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
- <button class="btn btn-primary" @click="handleToggleAddForm" v-if="canAddCrew">
10
- {{ showAddForm ? 'Cancel' : '+ Add Crew Member' }}
11
- </button>
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.certifications" :key="cert.name" class="certification-tag"
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 class="crew-availability">
144
- <strong>Embarkation Date:</strong> {{ member.nextShift || 'Not Scheduled' }}
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 class="crew-availability">
147
- <strong>Expected Days Onboard (in days):</strong> {{ member.onBoard || 'Not Scheduled' }}
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
- <button class="btn btn-primary" @click="handleAssignShift(member)"
159
- v-if="canAssignShift && member.status !== 'onduty'">
160
- Assign Shift
161
- </button>
162
- <button class="btn btn-warning" @click="handleDeboardCrew(member)"
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
- const validCertifications = this.newCrew.certifications.filter(cert =>
597
- cert.name.trim() !== '' && cert.expiryDate !== '' && cert.imageFile !== null
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 (validCertifications.length === 0) {
601
- alert('Please add at least one complete certification with name, expiry date, and image.')
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: this.newCrew.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: 'available',
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
- return Object.keys(this.formErrors).length === 0
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
- 'unavailable': 'status-unavailable'
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
- .crew-subhead{
2359
+
2360
+ .crew-subhead {
1659
2361
  color: #000;
1660
- }
1661
- </style>
2362
+ }</style>