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.
- package/dist/oceanhelm.es.js +527 -486
- package/dist/oceanhelm.es.js.map +1 -1
- package/dist/oceanhelm.umd.js +1 -1
- package/dist/oceanhelm.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ConfigurableSidebar.vue +208 -216
- package/src/components/CrewManagement.vue +82 -120
- package/src/components/VesselList.vue +92 -41
- package/src/utils/sidebarConfig.js +6 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|