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.
@@ -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
- <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>
@@ -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.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"
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 class="crew-availability">
146
- <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>
147
231
  </div>
148
- <div class="crew-availability">
149
- <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>
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
- <!-- Available: Show Assign Shift button -->
161
- <button class="btn btn-primary" @click="handleAssignShift(member)"
162
- v-if="canAssignShift && member.status === 'available'">
163
- Assign Shift
164
- </button>
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>