oceanhelm 0.0.6 → 0.0.8

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,23 +6,14 @@
6
6
  <div class="crew-section">
7
7
  <div class="crew-section-header">
8
8
  <h2>{{ sectionTitle }}</h2>
9
- <button
10
- class="btn btn-primary"
11
- @click="handleToggleAddForm"
12
- v-if="canAddCrew"
13
- >
9
+ <button class="btn btn-primary" @click="handleToggleAddForm" v-if="canAddCrew">
14
10
  {{ showAddForm ? 'Cancel' : '+ Add Crew Member' }}
15
11
  </button>
16
12
  </div>
17
13
 
18
14
  <!-- Search and Filter -->
19
15
  <div class="search-filter">
20
- <input
21
- type="text"
22
- placeholder="Search crew by name or role..."
23
- v-model="searchQuery"
24
- @input="handleSearch"
25
- >
16
+ <input type="text" placeholder="Search crew by name or role..." v-model="searchQuery" @input="handleSearch">
26
17
  <select v-model="filterStatus" @change="handleFilter">
27
18
  <option value="all">All Statuses</option>
28
19
  <option value="available">Available</option>
@@ -41,37 +32,25 @@
41
32
 
42
33
  <!-- Crew Grid -->
43
34
  <div class="crew-grid" v-else-if="filteredCrew.length > 0">
44
- <div
45
- v-for="member in filteredCrew"
46
- :key="member.id"
47
- class="crew-card"
48
- :class="{ 'unavailable': member.status === 'unavailable' }"
49
- >
35
+ <div v-for="member in filteredCrew" :key="member.id" class="crew-card"
36
+ :class="{ 'unavailable': member.status === 'unavailable' }">
50
37
  <div :class="['crew-status-badge', getStatusClass(member.status)]">
51
38
  {{ formatStatus(member.status) }}
52
39
  </div>
53
-
40
+
54
41
  <div class="crew-name">{{ member.name }}</div>
55
42
  <div class="crew-role">{{ member.role }}</div>
56
-
43
+
57
44
  <!-- Certifications -->
58
45
  <div class="crew-certifications">
59
- <div
60
- v-for="cert in member.certifications"
61
- :key="cert.name"
62
- class="certification-tag"
63
- :class="getCertificationClass(cert.expiryDate)"
64
- @click="handleViewCertification(cert, member)"
65
- >
46
+ <div v-for="cert in member.certifications" :key="cert.name" class="certification-tag"
47
+ :class="getCertificationClass(cert.expiryDate)" @click="handleViewCertification(cert, member)">
66
48
  {{ cert.name }}
67
49
  </div>
68
- <i
69
- class="bi bi-patch-plus-fill icon"
70
- @click="handleAddCertification(member)"
71
- v-if="canEditCrew"
72
- ></i>
50
+ <i class="bi bi-patch-plus-fill icon" @click="handleAddCertification(member)"
51
+ v-if="canEditCrew"></i>
73
52
  </div>
74
-
53
+
75
54
  <!-- Crew Details -->
76
55
  <div class="crew-availability">
77
56
  <strong>Embarkation Date:</strong> {{ member.nextShift || 'Not Scheduled' }}
@@ -79,7 +58,7 @@
79
58
  <div class="crew-availability">
80
59
  <strong>Expected Days Onboard (in days):</strong> {{ member.onBoard || 'Not Scheduled' }}
81
60
  </div>
82
-
61
+
83
62
  <!-- Action Buttons -->
84
63
  <div class="action-buttons">
85
64
  <div v-if="member.vessel" class="status-available crew-availability vcard">
@@ -88,21 +67,13 @@
88
67
  <div v-else class="status-unavailable crew-availability vcard">
89
68
  Vessel: Unassigned
90
69
  </div>
91
- <button
92
- class="btn btn-primary"
93
- @click="handleAssignShift(member)"
94
- v-if="canAssignShift"
95
- >
70
+ <button class="btn btn-primary" @click="handleAssignShift(member)" v-if="canAssignShift">
96
71
  Assign Shift
97
72
  </button>
98
73
  </div>
99
-
74
+
100
75
  <!-- Delete Button -->
101
- <i
102
- class="bi bi-trash icon delete-icon"
103
- @click="handleDeleteCrew(member)"
104
- v-if="canDeleteCrew"
105
- ></i>
76
+ <i class="bi bi-trash icon delete-icon" @click="handleDeleteCrew(member)" v-if="canDeleteCrew"></i>
106
77
  </div>
107
78
  </div>
108
79
 
@@ -114,17 +85,12 @@
114
85
  <!-- Add Crew Form -->
115
86
  <div class="add-crew-form" v-if="showAddForm">
116
87
  <h2>Add New Crew Member</h2>
117
-
88
+
118
89
  <!-- Basic Information -->
119
90
  <div class="form-row">
120
91
  <div class="form-group">
121
92
  <label for="crew-name">Full Name *</label>
122
- <input
123
- type="text"
124
- id="crew-name"
125
- v-model="newCrew.name"
126
- :class="{ 'error': formErrors.name }"
127
- >
93
+ <input type="text" id="crew-name" v-model="newCrew.name" :class="{ 'error': formErrors.name }">
128
94
  <div v-if="formErrors.name" class="error-message">{{ formErrors.name }}</div>
129
95
  </div>
130
96
  <div class="form-group">
@@ -135,13 +101,8 @@
135
101
  </option>
136
102
  <option value="Other">Other</option>
137
103
  </select>
138
- <input
139
- v-if="newCrew.role === 'Other'"
140
- type="text"
141
- placeholder="Enter custom role"
142
- v-model="newCrew.customRole"
143
- style="margin-top: 8px;"
144
- >
104
+ <input v-if="newCrew.role === 'Other'" type="text" placeholder="Enter custom role"
105
+ v-model="newCrew.customRole" style="margin-top: 8px;">
145
106
  </div>
146
107
  </div>
147
108
 
@@ -156,12 +117,7 @@
156
117
  </div>
157
118
  <div class="form-group">
158
119
  <label for="crew-email">Email Address *</label>
159
- <input
160
- type="email"
161
- id="crew-email"
162
- v-model="newCrew.email"
163
- :class="{ 'error': formErrors.email }"
164
- >
120
+ <input type="email" id="crew-email" v-model="newCrew.email" :class="{ 'error': formErrors.email }">
165
121
  <div v-if="formErrors.email" class="error-message">{{ formErrors.email }}</div>
166
122
  </div>
167
123
  </div>
@@ -180,18 +136,14 @@
180
136
  <input type="date" v-model="cert.expiryDate">
181
137
  </div>
182
138
  <div class="form-group">
183
- <button
184
- type="button"
185
- class="btn btn-danger btn-sm"
186
- @click="removeCertification(index)"
187
- v-if="newCrew.certifications.length > 1"
188
- >
139
+ <button type="button" class="btn btn-danger btn-sm" @click="removeCertification(index)"
140
+ v-if="newCrew.certifications.length > 1">
189
141
  Remove
190
142
  </button>
191
143
  </div>
192
144
  </div>
193
145
  </div>
194
-
146
+
195
147
  <button type="button" class="btn btn-secondary btn-sm" @click="addCertificationEntry">
196
148
  + Add More Certification
197
149
  </button>
@@ -218,7 +170,7 @@
218
170
  <script>
219
171
  export default {
220
172
  name: 'CrewManagement',
221
-
173
+
222
174
  props: {
223
175
  // Required props
224
176
  crew: {
@@ -234,7 +186,7 @@ export default {
234
186
  type: Object,
235
187
  default: () => ({ role: 'viewer' })
236
188
  },
237
-
189
+
238
190
  // Optional props
239
191
  loading: {
240
192
  type: Boolean,
@@ -244,7 +196,7 @@ export default {
244
196
  type: Array,
245
197
  default: () => ['Captain', 'First Officer', 'Engineer', 'Deckhand', 'Mechanic', 'Cook']
246
198
  },
247
-
199
+
248
200
  // Configuration props
249
201
  config: {
250
202
  type: Object,
@@ -258,7 +210,7 @@ export default {
258
210
  })
259
211
  }
260
212
  },
261
-
213
+
262
214
  emits: [
263
215
  'crew-add',
264
216
  'crew-edit',
@@ -270,7 +222,7 @@ export default {
270
222
  'filter-changed',
271
223
  'access-denied'
272
224
  ],
273
-
225
+
274
226
  data() {
275
227
  return {
276
228
  searchQuery: '',
@@ -290,55 +242,65 @@ export default {
290
242
  }
291
243
  }
292
244
  },
293
-
245
+
294
246
  computed: {
295
247
  sectionTitle() {
296
248
  return this.vesselName ? `Current Crew for ${this.vesselName}` : 'All Fleet Crew'
297
249
  },
298
-
250
+
299
251
  filteredCrew() {
300
252
  return this.crew.filter(member => {
301
- const matchesSearch = this.searchQuery === '' ||
253
+ const matchesSearch = this.searchQuery === '' ||
302
254
  member.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
303
255
  member.role.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
304
256
  (member.vessel && member.vessel.toLowerCase().includes(this.searchQuery.toLowerCase()))
305
-
257
+
306
258
  const matchesStatus = this.filterStatus === 'all' || member.status === this.filterStatus
307
-
259
+
308
260
  return matchesSearch && matchesStatus
309
261
  })
310
262
  },
311
-
263
+
312
264
  canAddCrew() {
313
265
  return this.config.enableAdd && this.hasPermission('add')
314
266
  },
315
-
267
+
316
268
  canEditCrew() {
317
269
  return this.config.enableEdit && this.hasPermission('edit')
318
270
  },
319
-
271
+
320
272
  canDeleteCrew() {
321
273
  return this.config.enableDelete && this.hasPermission('delete')
322
274
  },
323
-
275
+
324
276
  canAssignShift() {
325
277
  return this.config.enableAssignShift && this.hasPermission('assign')
326
278
  }
327
279
  },
328
-
280
+
329
281
  methods: {
330
282
  // Permission checking
331
283
  hasPermission(action) {
332
- const { role } = this.userProfile
284
+ const { role, vessel } = this.userProfile
285
+ const isVessel = vessel === this.vesselName
286
+
333
287
  const permissions = {
334
288
  'owner': ['add', 'edit', 'delete', 'assign', 'view'],
335
289
  'staff': ['add', 'edit', 'assign', 'view'],
336
- 'captain': ['assign', 'view'],
290
+ 'captain': ['assign', 'view'], // default for captain
337
291
  'viewer': ['view']
338
292
  }
339
- return permissions[role]?.includes(action) || false
293
+
294
+ let rolePermissions = permissions[role] || []
295
+
296
+ // Special case: captain + isVessel = true
297
+ if (role === 'captain' && isVessel) {
298
+ rolePermissions = [...rolePermissions, 'add']
299
+ }
300
+
301
+ return rolePermissions.includes(action)
340
302
  },
341
-
303
+
342
304
  // Event handlers
343
305
  handleToggleAddForm() {
344
306
  if (!this.canAddCrew) {
@@ -350,26 +312,26 @@ export default {
350
312
  this.resetForm()
351
313
  }
352
314
  },
353
-
315
+
354
316
  handleSearch() {
355
317
  this.$emit('search-changed', this.searchQuery)
356
318
  },
357
-
319
+
358
320
  handleFilter() {
359
321
  this.$emit('filter-changed', this.filterStatus)
360
322
  },
361
-
323
+
362
324
  handleAddCrewMember() {
363
325
  if (!this.validateForm()) {
364
326
  return
365
327
  }
366
-
367
- const validCertifications = this.newCrew.certifications.filter(cert =>
328
+
329
+ const validCertifications = this.newCrew.certifications.filter(cert =>
368
330
  cert.name.trim() !== '' && cert.expiryDate !== ''
369
331
  )
370
-
332
+
371
333
  const finalRole = this.newCrew.role === 'Other' ? this.newCrew.customRole : this.newCrew.role
372
-
334
+
373
335
  const newMember = {
374
336
  name: this.newCrew.name,
375
337
  role: finalRole,
@@ -381,48 +343,48 @@ export default {
381
343
  onBoard: this.newCrew.onBoard,
382
344
  nextShift: this.newCrew.nextShift
383
345
  }
384
-
346
+
385
347
  this.$emit('crew-add', newMember)
386
348
  this.resetForm()
387
349
  this.showAddForm = false
388
350
  },
389
-
351
+
390
352
  handleCancelForm() {
391
353
  this.showAddForm = false
392
354
  this.resetForm()
393
355
  },
394
-
356
+
395
357
  handleDeleteCrew(member) {
396
358
  if (!this.canDeleteCrew) {
397
359
  this.$emit('access-denied', { action: 'delete crew', userProfile: this.userProfile })
398
360
  return
399
361
  }
400
-
362
+
401
363
  this.$emit('crew-delete', member)
402
364
  },
403
-
365
+
404
366
  handleAssignShift(member) {
405
367
  if (!this.canAssignShift) {
406
368
  this.$emit('access-denied', { action: 'assign shift', userProfile: this.userProfile })
407
369
  return
408
370
  }
409
-
371
+
410
372
  this.$emit('crew-assign-shift', member)
411
373
  },
412
-
374
+
413
375
  handleAddCertification(member) {
414
376
  if (!this.canEditCrew) {
415
377
  this.$emit('access-denied', { action: 'add certification', userProfile: this.userProfile })
416
378
  return
417
379
  }
418
-
380
+
419
381
  this.$emit('crew-add-certification', member)
420
382
  },
421
-
383
+
422
384
  handleViewCertification(certification, member) {
423
385
  this.$emit('crew-view-certification', { certification, member })
424
386
  },
425
-
387
+
426
388
  // Form management
427
389
  resetForm() {
428
390
  this.newCrew = {
@@ -438,53 +400,53 @@ export default {
438
400
  }
439
401
  this.formErrors = {}
440
402
  },
441
-
403
+
442
404
  validateForm() {
443
405
  this.formErrors = {}
444
-
406
+
445
407
  const requiredFields = {
446
408
  name: 'Full Name',
447
409
  email: 'Email Address'
448
410
  }
449
-
411
+
450
412
  // Check required fields
451
413
  Object.keys(requiredFields).forEach(field => {
452
414
  if (!this.newCrew[field] || this.newCrew[field].trim() === '') {
453
415
  this.formErrors[field] = `${requiredFields[field]} is required`
454
416
  }
455
417
  })
456
-
418
+
457
419
  // Validate email format
458
420
  if (this.newCrew.email && !this.isValidEmail(this.newCrew.email)) {
459
421
  this.formErrors.email = 'Please enter a valid email address'
460
422
  }
461
-
423
+
462
424
  // Validate custom role
463
425
  if (this.newCrew.role === 'Other' && (!this.newCrew.customRole || this.newCrew.customRole.trim() === '')) {
464
426
  this.formErrors.customRole = 'Custom role is required when "Other" is selected'
465
427
  }
466
-
428
+
467
429
  return Object.keys(this.formErrors).length === 0
468
430
  },
469
-
431
+
470
432
  isValidEmail(email) {
471
433
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
472
434
  return emailRegex.test(email)
473
435
  },
474
-
436
+
475
437
  addCertificationEntry() {
476
438
  this.newCrew.certifications.push({ name: '', expiryDate: '' })
477
439
  },
478
-
440
+
479
441
  removeCertification(index) {
480
442
  this.newCrew.certifications.splice(index, 1)
481
443
  },
482
-
444
+
483
445
  // Utility methods
484
446
  formatStatus(status) {
485
447
  return status ? status.charAt(0).toUpperCase() + status.slice(1) : ''
486
448
  },
487
-
449
+
488
450
  getStatusClass(status) {
489
451
  const statusMap = {
490
452
  'available': 'status-available',
@@ -493,7 +455,7 @@ export default {
493
455
  }
494
456
  return statusMap[status] || ''
495
457
  },
496
-
458
+
497
459
  getCertificationClass(expiryDate) {
498
460
  const status = this.getExpiryStatus(expiryDate)
499
461
  const classMap = {
@@ -503,7 +465,7 @@ export default {
503
465
  }
504
466
  return classMap[status] || ''
505
467
  },
506
-
468
+
507
469
  getExpiryStatus(dateStr) {
508
470
  if (!dateStr) return 'none'
509
471