oceanhelm 0.0.1
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/LICENSE +35 -0
- package/README.md +121 -0
- package/dist/favico.ico +0 -0
- package/dist/oceanhelm.es.js +2134 -0
- package/dist/oceanhelm.es.js.map +1 -0
- package/dist/oceanhelm.umd.js +2 -0
- package/dist/oceanhelm.umd.js.map +1 -0
- package/dist/style.css +1 -0
- package/package.json +55 -0
- package/src/App.vue +10 -0
- package/src/assets/logo.svg +1 -0
- package/src/assets/main.css +0 -0
- package/src/components/ActivityLogs.vue +483 -0
- package/src/components/ConfigurableSidebar.vue +239 -0
- package/src/components/CrewManagement.vue +746 -0
- package/src/components/DashHead.vue +54 -0
- package/src/components/InventoryManagement.vue +1615 -0
- package/src/components/OceanHelmMaintenance.vue +1778 -0
- package/src/components/VesselList.vue +348 -0
- package/src/index.js +33 -0
- package/src/main.js +11 -0
- package/src/router/index.js +8 -0
- package/src/types/index.js +33 -0
- package/src/utils/permissions.js +10 -0
- package/src/utils/sidebarConfig.js +87 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="crew-management">
|
|
3
|
+
<!-- Wave background -->
|
|
4
|
+
<div class="wave-bg" v-if="config.showWaveBackground"></div>
|
|
5
|
+
|
|
6
|
+
<div class="crew-section">
|
|
7
|
+
<div class="section-header">
|
|
8
|
+
<h2>{{ sectionTitle }}</h2>
|
|
9
|
+
<button
|
|
10
|
+
class="btn btn-primary"
|
|
11
|
+
@click="handleToggleAddForm"
|
|
12
|
+
v-if="canAddCrew"
|
|
13
|
+
>
|
|
14
|
+
{{ showAddForm ? 'Cancel' : '+ Add Crew Member' }}
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Search and Filter -->
|
|
19
|
+
<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
|
+
>
|
|
26
|
+
<select v-model="filterStatus" @change="handleFilter">
|
|
27
|
+
<option value="all">All Statuses</option>
|
|
28
|
+
<option value="available">Available</option>
|
|
29
|
+
<option value="onduty">On Duty</option>
|
|
30
|
+
<option value="unavailable">Unavailable</option>
|
|
31
|
+
</select>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Loading State -->
|
|
35
|
+
<div v-if="loading" class="loading-state">
|
|
36
|
+
<div class="spinner-border text-primary" role="status">
|
|
37
|
+
<span class="visually-hidden">Loading crew...</span>
|
|
38
|
+
</div>
|
|
39
|
+
<p>Loading crew members...</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Crew Grid -->
|
|
43
|
+
<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
|
+
>
|
|
50
|
+
<div :class="['status-badge', getStatusClass(member.status)]">
|
|
51
|
+
{{ formatStatus(member.status) }}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="crew-name">{{ member.name }}</div>
|
|
55
|
+
<div class="crew-role">{{ member.role }}</div>
|
|
56
|
+
|
|
57
|
+
<!-- Certifications -->
|
|
58
|
+
<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
|
+
>
|
|
66
|
+
{{ cert.name }}
|
|
67
|
+
</div>
|
|
68
|
+
<i
|
|
69
|
+
class="bi bi-patch-plus-fill icon"
|
|
70
|
+
@click="handleAddCertification(member)"
|
|
71
|
+
v-if="canEditCrew"
|
|
72
|
+
></i>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- Crew Details -->
|
|
76
|
+
<div class="crew-availability">
|
|
77
|
+
<strong>Embarkation Date:</strong> {{ member.nextShift || 'Not Scheduled' }}
|
|
78
|
+
</div>
|
|
79
|
+
<div class="crew-availability">
|
|
80
|
+
<strong>Expected Days Onboard (in days):</strong> {{ member.onBoard || 'Not Scheduled' }}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Action Buttons -->
|
|
84
|
+
<div class="action-buttons">
|
|
85
|
+
<div v-if="member.vessel" class="status-available crew-availability vcard">
|
|
86
|
+
Vessel: {{ member.vessel }}
|
|
87
|
+
</div>
|
|
88
|
+
<div v-else class="status-unavailable crew-availability vcard">
|
|
89
|
+
Vessel: Unassigned
|
|
90
|
+
</div>
|
|
91
|
+
<button
|
|
92
|
+
class="btn btn-primary"
|
|
93
|
+
@click="handleAssignShift(member)"
|
|
94
|
+
v-if="canAssignShift"
|
|
95
|
+
>
|
|
96
|
+
Assign Shift
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<!-- Delete Button -->
|
|
101
|
+
<i
|
|
102
|
+
class="bi bi-trash icon delete-icon"
|
|
103
|
+
@click="handleDeleteCrew(member)"
|
|
104
|
+
v-if="canDeleteCrew"
|
|
105
|
+
></i>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- No Results -->
|
|
110
|
+
<div v-else-if="!loading" class="no-results">
|
|
111
|
+
{{ crew.length === 0 ? 'No crew members found.' : 'No crew members found matching your search criteria.' }}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<!-- Add Crew Form -->
|
|
115
|
+
<div class="add-crew-form" v-if="showAddForm">
|
|
116
|
+
<h2>Add New Crew Member</h2>
|
|
117
|
+
|
|
118
|
+
<!-- Basic Information -->
|
|
119
|
+
<div class="form-row">
|
|
120
|
+
<div class="form-group">
|
|
121
|
+
<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
|
+
>
|
|
128
|
+
<div v-if="formErrors.name" class="error-message">{{ formErrors.name }}</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="form-group">
|
|
131
|
+
<label for="crew-role">Role/Position *</label>
|
|
132
|
+
<select id="crew-role" v-model="newCrew.role">
|
|
133
|
+
<option v-for="role in availableRoles" :key="role" :value="role">
|
|
134
|
+
{{ role }}
|
|
135
|
+
</option>
|
|
136
|
+
<option value="Other">Other</option>
|
|
137
|
+
</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
|
+
>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div class="form-row">
|
|
149
|
+
<div class="form-group">
|
|
150
|
+
<label for="crew-status">Status *</label>
|
|
151
|
+
<select id="crew-status" v-model="newCrew.status">
|
|
152
|
+
<option value="available">Available</option>
|
|
153
|
+
<option value="onduty">On Duty</option>
|
|
154
|
+
<option value="unavailable">Unavailable</option>
|
|
155
|
+
</select>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="form-group">
|
|
158
|
+
<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
|
+
>
|
|
165
|
+
<div v-if="formErrors.email" class="error-message">{{ formErrors.email }}</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<!-- Certifications Section -->
|
|
170
|
+
<div class="certification-section">
|
|
171
|
+
<h3>Certifications</h3>
|
|
172
|
+
<div v-for="(cert, index) in newCrew.certifications" :key="index" class="certification-entry">
|
|
173
|
+
<div class="form-row">
|
|
174
|
+
<div class="form-group">
|
|
175
|
+
<label>Certification Name</label>
|
|
176
|
+
<input type="text" v-model="cert.name" placeholder="Enter certification name">
|
|
177
|
+
</div>
|
|
178
|
+
<div class="form-group">
|
|
179
|
+
<label>Expiry Date</label>
|
|
180
|
+
<input type="date" v-model="cert.expiryDate">
|
|
181
|
+
</div>
|
|
182
|
+
<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
|
+
>
|
|
189
|
+
Remove
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<button type="button" class="btn btn-secondary btn-sm" @click="addCertificationEntry">
|
|
196
|
+
+ Add More Certification
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<!-- Notes -->
|
|
201
|
+
<div class="form-row">
|
|
202
|
+
<div class="form-group">
|
|
203
|
+
<label for="crew-notes">Notes</label>
|
|
204
|
+
<textarea id="crew-notes" rows="3" v-model="newCrew.notes"></textarea>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- Form Actions -->
|
|
209
|
+
<div class="form-actions">
|
|
210
|
+
<button class="btn btn-secondary" @click="handleCancelForm">Cancel</button>
|
|
211
|
+
<button class="btn btn-primary" @click="handleAddCrewMember">Add Crew Member</button>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</template>
|
|
217
|
+
|
|
218
|
+
<script>
|
|
219
|
+
export default {
|
|
220
|
+
name: 'CrewManagement',
|
|
221
|
+
|
|
222
|
+
props: {
|
|
223
|
+
// Required props
|
|
224
|
+
crew: {
|
|
225
|
+
type: Array,
|
|
226
|
+
required: true,
|
|
227
|
+
default: () => []
|
|
228
|
+
},
|
|
229
|
+
vesselName: {
|
|
230
|
+
type: String,
|
|
231
|
+
default: ''
|
|
232
|
+
},
|
|
233
|
+
userProfile: {
|
|
234
|
+
type: Object,
|
|
235
|
+
default: () => ({ role: 'viewer' })
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
// Optional props
|
|
239
|
+
loading: {
|
|
240
|
+
type: Boolean,
|
|
241
|
+
default: false
|
|
242
|
+
},
|
|
243
|
+
availableRoles: {
|
|
244
|
+
type: Array,
|
|
245
|
+
default: () => ['Captain', 'First Officer', 'Engineer', 'Deckhand', 'Mechanic', 'Cook']
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
// Configuration props
|
|
249
|
+
config: {
|
|
250
|
+
type: Object,
|
|
251
|
+
default: () => ({
|
|
252
|
+
showWaveBackground: true,
|
|
253
|
+
enableAdd: true,
|
|
254
|
+
enableEdit: true,
|
|
255
|
+
enableDelete: true,
|
|
256
|
+
enableAssignShift: true,
|
|
257
|
+
enableCertificationManagement: true
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
emits: [
|
|
263
|
+
'crew-add',
|
|
264
|
+
'crew-edit',
|
|
265
|
+
'crew-delete',
|
|
266
|
+
'crew-assign-shift',
|
|
267
|
+
'crew-add-certification',
|
|
268
|
+
'crew-view-certification',
|
|
269
|
+
'search-changed',
|
|
270
|
+
'filter-changed',
|
|
271
|
+
'access-denied'
|
|
272
|
+
],
|
|
273
|
+
|
|
274
|
+
data() {
|
|
275
|
+
return {
|
|
276
|
+
searchQuery: '',
|
|
277
|
+
filterStatus: 'all',
|
|
278
|
+
showAddForm: false,
|
|
279
|
+
formErrors: {},
|
|
280
|
+
newCrew: {
|
|
281
|
+
name: '',
|
|
282
|
+
role: 'Deckhand',
|
|
283
|
+
customRole: '',
|
|
284
|
+
status: 'available',
|
|
285
|
+
nextShift: '',
|
|
286
|
+
certifications: [{ name: '', expiryDate: '' }],
|
|
287
|
+
notes: '',
|
|
288
|
+
email: '',
|
|
289
|
+
onBoard: ''
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
computed: {
|
|
295
|
+
sectionTitle() {
|
|
296
|
+
return this.vesselName ? `Current Crew for ${this.vesselName}` : 'All Fleet Crew'
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
filteredCrew() {
|
|
300
|
+
return this.crew.filter(member => {
|
|
301
|
+
const matchesSearch = this.searchQuery === '' ||
|
|
302
|
+
member.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
|
|
303
|
+
member.role.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
|
|
304
|
+
(member.vessel && member.vessel.toLowerCase().includes(this.searchQuery.toLowerCase()))
|
|
305
|
+
|
|
306
|
+
const matchesStatus = this.filterStatus === 'all' || member.status === this.filterStatus
|
|
307
|
+
|
|
308
|
+
return matchesSearch && matchesStatus
|
|
309
|
+
})
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
canAddCrew() {
|
|
313
|
+
return this.config.enableAdd && this.hasPermission('add')
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
canEditCrew() {
|
|
317
|
+
return this.config.enableEdit && this.hasPermission('edit')
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
canDeleteCrew() {
|
|
321
|
+
return this.config.enableDelete && this.hasPermission('delete')
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
canAssignShift() {
|
|
325
|
+
return this.config.enableAssignShift && this.hasPermission('assign')
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
methods: {
|
|
330
|
+
// Permission checking
|
|
331
|
+
hasPermission(action) {
|
|
332
|
+
const { role } = this.userProfile
|
|
333
|
+
const permissions = {
|
|
334
|
+
'owner': ['add', 'edit', 'delete', 'assign', 'view'],
|
|
335
|
+
'staff': ['add', 'edit', 'assign', 'view'],
|
|
336
|
+
'captain': ['assign', 'view'],
|
|
337
|
+
'viewer': ['view']
|
|
338
|
+
}
|
|
339
|
+
return permissions[role]?.includes(action) || false
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
// Event handlers
|
|
343
|
+
handleToggleAddForm() {
|
|
344
|
+
if (!this.canAddCrew) {
|
|
345
|
+
this.$emit('access-denied', { action: 'add crew', userProfile: this.userProfile })
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
this.showAddForm = !this.showAddForm
|
|
349
|
+
if (!this.showAddForm) {
|
|
350
|
+
this.resetForm()
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
handleSearch() {
|
|
355
|
+
this.$emit('search-changed', this.searchQuery)
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
handleFilter() {
|
|
359
|
+
this.$emit('filter-changed', this.filterStatus)
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
handleAddCrewMember() {
|
|
363
|
+
if (!this.validateForm()) {
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const validCertifications = this.newCrew.certifications.filter(cert =>
|
|
368
|
+
cert.name.trim() !== '' && cert.expiryDate !== ''
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
const finalRole = this.newCrew.role === 'Other' ? this.newCrew.customRole : this.newCrew.role
|
|
372
|
+
|
|
373
|
+
const newMember = {
|
|
374
|
+
name: this.newCrew.name,
|
|
375
|
+
role: finalRole,
|
|
376
|
+
status: this.newCrew.status,
|
|
377
|
+
certifications: validCertifications,
|
|
378
|
+
notes: this.newCrew.notes,
|
|
379
|
+
vessel: this.vesselName,
|
|
380
|
+
email: this.newCrew.email,
|
|
381
|
+
onBoard: this.newCrew.onBoard,
|
|
382
|
+
nextShift: this.newCrew.nextShift
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
this.$emit('crew-add', newMember)
|
|
386
|
+
this.resetForm()
|
|
387
|
+
this.showAddForm = false
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
handleCancelForm() {
|
|
391
|
+
this.showAddForm = false
|
|
392
|
+
this.resetForm()
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
handleDeleteCrew(member) {
|
|
396
|
+
if (!this.canDeleteCrew) {
|
|
397
|
+
this.$emit('access-denied', { action: 'delete crew', userProfile: this.userProfile })
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
this.$emit('crew-delete', member)
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
handleAssignShift(member) {
|
|
405
|
+
if (!this.canAssignShift) {
|
|
406
|
+
this.$emit('access-denied', { action: 'assign shift', userProfile: this.userProfile })
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.$emit('crew-assign-shift', member)
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
handleAddCertification(member) {
|
|
414
|
+
if (!this.canEditCrew) {
|
|
415
|
+
this.$emit('access-denied', { action: 'add certification', userProfile: this.userProfile })
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
this.$emit('crew-add-certification', member)
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
handleViewCertification(certification, member) {
|
|
423
|
+
this.$emit('crew-view-certification', { certification, member })
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
// Form management
|
|
427
|
+
resetForm() {
|
|
428
|
+
this.newCrew = {
|
|
429
|
+
name: '',
|
|
430
|
+
role: 'Deckhand',
|
|
431
|
+
customRole: '',
|
|
432
|
+
status: 'available',
|
|
433
|
+
nextShift: '',
|
|
434
|
+
certifications: [{ name: '', expiryDate: '' }],
|
|
435
|
+
notes: '',
|
|
436
|
+
email: '',
|
|
437
|
+
onBoard: ''
|
|
438
|
+
}
|
|
439
|
+
this.formErrors = {}
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
validateForm() {
|
|
443
|
+
this.formErrors = {}
|
|
444
|
+
|
|
445
|
+
const requiredFields = {
|
|
446
|
+
name: 'Full Name',
|
|
447
|
+
email: 'Email Address'
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Check required fields
|
|
451
|
+
Object.keys(requiredFields).forEach(field => {
|
|
452
|
+
if (!this.newCrew[field] || this.newCrew[field].trim() === '') {
|
|
453
|
+
this.formErrors[field] = `${requiredFields[field]} is required`
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
// Validate email format
|
|
458
|
+
if (this.newCrew.email && !this.isValidEmail(this.newCrew.email)) {
|
|
459
|
+
this.formErrors.email = 'Please enter a valid email address'
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Validate custom role
|
|
463
|
+
if (this.newCrew.role === 'Other' && (!this.newCrew.customRole || this.newCrew.customRole.trim() === '')) {
|
|
464
|
+
this.formErrors.customRole = 'Custom role is required when "Other" is selected'
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return Object.keys(this.formErrors).length === 0
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
isValidEmail(email) {
|
|
471
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
472
|
+
return emailRegex.test(email)
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
addCertificationEntry() {
|
|
476
|
+
this.newCrew.certifications.push({ name: '', expiryDate: '' })
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
removeCertification(index) {
|
|
480
|
+
this.newCrew.certifications.splice(index, 1)
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
// Utility methods
|
|
484
|
+
formatStatus(status) {
|
|
485
|
+
return status ? status.charAt(0).toUpperCase() + status.slice(1) : ''
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
getStatusClass(status) {
|
|
489
|
+
const statusMap = {
|
|
490
|
+
'available': 'status-available',
|
|
491
|
+
'onduty': 'status-onduty',
|
|
492
|
+
'unavailable': 'status-unavailable'
|
|
493
|
+
}
|
|
494
|
+
return statusMap[status] || ''
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
getCertificationClass(expiryDate) {
|
|
498
|
+
const status = this.getExpiryStatus(expiryDate)
|
|
499
|
+
const classMap = {
|
|
500
|
+
'expired': 'text-danger',
|
|
501
|
+
'expiringSoon': 'text-warning',
|
|
502
|
+
'valid': 'text-success'
|
|
503
|
+
}
|
|
504
|
+
return classMap[status] || ''
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
getExpiryStatus(dateStr) {
|
|
508
|
+
if (!dateStr) return 'none'
|
|
509
|
+
|
|
510
|
+
const expiry = new Date(dateStr)
|
|
511
|
+
const today = new Date()
|
|
512
|
+
const oneMonthFromNow = new Date()
|
|
513
|
+
oneMonthFromNow.setMonth(today.getMonth() + 1)
|
|
514
|
+
|
|
515
|
+
if (expiry <= today) return 'expired'
|
|
516
|
+
if (expiry <= oneMonthFromNow) return 'expiringSoon'
|
|
517
|
+
return 'valid'
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
</script>
|
|
522
|
+
|
|
523
|
+
<style>
|
|
524
|
+
h1,
|
|
525
|
+
h2 {
|
|
526
|
+
color: #005792;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.crew-section {
|
|
530
|
+
margin-bottom: 30px;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.crew-grid {
|
|
534
|
+
display: grid;
|
|
535
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
536
|
+
gap: 20px;
|
|
537
|
+
margin-bottom: 30px;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.crew-card {
|
|
541
|
+
background-color: #f8f9fa;
|
|
542
|
+
border-radius: 6px;
|
|
543
|
+
padding: 15px;
|
|
544
|
+
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.08);
|
|
545
|
+
border-left: 4px solid #00a8e8;
|
|
546
|
+
position: relative;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.crew-card.unavailable {
|
|
550
|
+
border-left-color: #dc3545;
|
|
551
|
+
opacity: 0.8;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.crew-name {
|
|
555
|
+
font-weight: bold;
|
|
556
|
+
font-size: 1.1em;
|
|
557
|
+
margin-bottom: 5px;
|
|
558
|
+
color: #005792;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.crew-role {
|
|
562
|
+
color: #6c757d;
|
|
563
|
+
font-size: 0.9em;
|
|
564
|
+
margin-bottom: 10px;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.crew-certifications,
|
|
568
|
+
.crew-availability {
|
|
569
|
+
font-size: 0.85em;
|
|
570
|
+
margin: 5px 0;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.certification-tag {
|
|
574
|
+
display: inline-block;
|
|
575
|
+
background-color: #e6f7ff;
|
|
576
|
+
color: #005792;
|
|
577
|
+
border-radius: 4px;
|
|
578
|
+
padding: 2px 8px;
|
|
579
|
+
margin-right: 5px;
|
|
580
|
+
margin-bottom: 5px;
|
|
581
|
+
font-size: 0.85em;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.status-badge {
|
|
585
|
+
position: absolute;
|
|
586
|
+
top: 15px;
|
|
587
|
+
right: 15px;
|
|
588
|
+
font-size: 0.75em;
|
|
589
|
+
padding: 3px 8px;
|
|
590
|
+
border-radius: 12px;
|
|
591
|
+
font-weight: bold;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.status-available {
|
|
595
|
+
background-color: #d4edda;
|
|
596
|
+
color: #155724;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.status-unavailable {
|
|
600
|
+
background-color: #f8d7da;
|
|
601
|
+
color: #721c24;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.status-onduty {
|
|
605
|
+
background-color: #cce5ff;
|
|
606
|
+
color: #004085;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.action-buttons {
|
|
610
|
+
display: flex;
|
|
611
|
+
justify-content: space-between;
|
|
612
|
+
margin-top: 15px;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.btn {
|
|
616
|
+
padding: 6px 12px;
|
|
617
|
+
border-radius: 4px;
|
|
618
|
+
border: none;
|
|
619
|
+
cursor: pointer;
|
|
620
|
+
font-size: 0.9em;
|
|
621
|
+
transition: background-color 0.2s;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.btn-primary {
|
|
625
|
+
background-color: #005792;
|
|
626
|
+
color: white;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.btn-primary:hover {
|
|
630
|
+
background-color: #004675;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.btn-secondary {
|
|
634
|
+
background-color: #e9ecef;
|
|
635
|
+
color: #495057;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.btn-secondary:hover {
|
|
639
|
+
background-color: #dde2e6;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.search-filter {
|
|
643
|
+
display: flex;
|
|
644
|
+
margin-bottom: 20px;
|
|
645
|
+
gap: 10px;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.search-filter input,
|
|
649
|
+
.search-filter select {
|
|
650
|
+
padding: 8px 12px;
|
|
651
|
+
border: 1px solid #ced4da;
|
|
652
|
+
border-radius: 4px;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.search-filter input {
|
|
656
|
+
flex-grow: 1;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.search-filter select {
|
|
660
|
+
width: 30%;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.add-crew-form {
|
|
664
|
+
background-color: #f8f9fa;
|
|
665
|
+
padding: 20px;
|
|
666
|
+
border-radius: 6px;
|
|
667
|
+
margin-top: 30px;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.form-row {
|
|
671
|
+
display: flex;
|
|
672
|
+
flex-wrap: wrap;
|
|
673
|
+
gap: 15px;
|
|
674
|
+
margin-bottom: 15px;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.form-group {
|
|
678
|
+
flex: 1;
|
|
679
|
+
min-width: 200px;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.form-group label {
|
|
683
|
+
display: block;
|
|
684
|
+
margin-bottom: 5px;
|
|
685
|
+
font-weight: bold;
|
|
686
|
+
color: #495057;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.text-danger {
|
|
690
|
+
color: red;
|
|
691
|
+
font-weight: bolder;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.text-warning {
|
|
695
|
+
color: orange;
|
|
696
|
+
font-weight: bolder;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.text-success {
|
|
700
|
+
color: green;
|
|
701
|
+
font-weight: bolder;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.icon {
|
|
705
|
+
font-size: 20px;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.form-group input,
|
|
709
|
+
.form-group select,
|
|
710
|
+
.form-group textarea {
|
|
711
|
+
width: 100%;
|
|
712
|
+
padding: 8px;
|
|
713
|
+
border: 1px solid #ced4da;
|
|
714
|
+
border-radius: 4px;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.form-actions {
|
|
718
|
+
display: flex;
|
|
719
|
+
justify-content: flex-end;
|
|
720
|
+
gap: 10px;
|
|
721
|
+
margin-top: 20px;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.no-results {
|
|
725
|
+
text-align: center;
|
|
726
|
+
padding: 20px;
|
|
727
|
+
color: #6c757d;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.section-header {
|
|
731
|
+
display: flex;
|
|
732
|
+
justify-content: space-between;
|
|
733
|
+
align-items: center;
|
|
734
|
+
margin-bottom: 15px;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.vcard {
|
|
738
|
+
border-radius: 6px;
|
|
739
|
+
padding: 5px;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
#content.active {
|
|
743
|
+
margin-left: var(--sidebar-width);
|
|
744
|
+
width: calc(100% - var(--sidebar-width));
|
|
745
|
+
}
|
|
746
|
+
</style>
|