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,483 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="activity-logs">
|
|
3
|
+
<!-- Header -->
|
|
4
|
+
<div class="header">
|
|
5
|
+
<h1>Activity Logs</h1>
|
|
6
|
+
<p>Monitor and track all system activities in real-time</p>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<!-- Controls -->
|
|
10
|
+
<div class="controls">
|
|
11
|
+
<div class="search-box">
|
|
12
|
+
<input
|
|
13
|
+
type="text"
|
|
14
|
+
placeholder="Search activities, users, or actions..."
|
|
15
|
+
:value="searchTerm"
|
|
16
|
+
@input="$emit('update:searchTerm', $event.target.value)"
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
<select
|
|
20
|
+
class="filter-select"
|
|
21
|
+
:value="selectedFilter"
|
|
22
|
+
@change="$emit('update:selectedFilter', $event.target.value)"
|
|
23
|
+
>
|
|
24
|
+
<option value="all">All Activities</option>
|
|
25
|
+
<option value="login">Login</option>
|
|
26
|
+
<option value="logout">Logout</option>
|
|
27
|
+
<option value="create">Create</option>
|
|
28
|
+
<option value="update">Update</option>
|
|
29
|
+
<option value="delete">Delete</option>
|
|
30
|
+
<option value="view">View</option>
|
|
31
|
+
</select>
|
|
32
|
+
<button class="btn btn-primary" @click="$emit('refresh')">🔄 Refresh</button>
|
|
33
|
+
<button class="btn btn-secondary" @click="$emit('download')">📥 Download Report</button>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Stats -->
|
|
37
|
+
<div class="stats-grid">
|
|
38
|
+
<div class="stat-card">
|
|
39
|
+
<h3>Total Activities</h3>
|
|
40
|
+
<div class="value">{{ totalActivities }}</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="stat-card">
|
|
43
|
+
<h3>Today's Activities</h3>
|
|
44
|
+
<div class="value">{{ todayActivities }}</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="stat-card">
|
|
47
|
+
<h3>Active Users</h3>
|
|
48
|
+
<div class="value">{{ activeUsers }}</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="stat-card">
|
|
51
|
+
<h3>Important</h3>
|
|
52
|
+
<div class="badge-danger">
|
|
53
|
+
Logs are deleted at the end of every month, please download a copy
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Logs Table -->
|
|
59
|
+
<div class="logs-container">
|
|
60
|
+
<div v-if="loading" class="loading">
|
|
61
|
+
<div class="spinner"></div>
|
|
62
|
+
<p>Loading activity logs...</p>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div v-else-if="logs.length === 0" class="no-logs">
|
|
66
|
+
<h3>No activities found</h3>
|
|
67
|
+
<p>Try adjusting your search or filter criteria</p>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div v-else>
|
|
71
|
+
<table class="logs-table">
|
|
72
|
+
<thead>
|
|
73
|
+
<tr>
|
|
74
|
+
<th>Timestamp</th>
|
|
75
|
+
<th>User Name</th>
|
|
76
|
+
<th>Action</th>
|
|
77
|
+
<th>Details</th>
|
|
78
|
+
<th>Section</th>
|
|
79
|
+
</tr>
|
|
80
|
+
</thead>
|
|
81
|
+
<tbody>
|
|
82
|
+
<tr v-for="log in paginatedLogs" :key="log.id">
|
|
83
|
+
<td>{{ formatDate(log.timestamp) }}</td>
|
|
84
|
+
<td>
|
|
85
|
+
<div>{{ log.user_name }}</div>
|
|
86
|
+
<small style="color: gray">{{ log.email }}</small>
|
|
87
|
+
</td>
|
|
88
|
+
<td>
|
|
89
|
+
<span class="activity-badge" :class="getBadgeClass(log.action)">
|
|
90
|
+
{{ log.action }}
|
|
91
|
+
</span>
|
|
92
|
+
</td>
|
|
93
|
+
<td>
|
|
94
|
+
{{ log.details.status }}
|
|
95
|
+
<div v-for="(change, key) in log.details.information" :key="key">
|
|
96
|
+
<strong>{{ key }}: </strong>
|
|
97
|
+
<small style="color: gray">
|
|
98
|
+
{{ change.from || "" }} → {{ change.to || change }}
|
|
99
|
+
</small>
|
|
100
|
+
</div>
|
|
101
|
+
</td>
|
|
102
|
+
<td>{{ log.table_name }}</td>
|
|
103
|
+
</tr>
|
|
104
|
+
</tbody>
|
|
105
|
+
</table>
|
|
106
|
+
|
|
107
|
+
<!-- Pagination -->
|
|
108
|
+
<div class="pagination">
|
|
109
|
+
<button @click="$emit('change-page', currentPage - 1)" :disabled="currentPage === 1">
|
|
110
|
+
Previous
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
v-for="page in totalPages"
|
|
114
|
+
:key="page"
|
|
115
|
+
@click="$emit('change-page', page)"
|
|
116
|
+
:class="{ active: currentPage === page }"
|
|
117
|
+
>
|
|
118
|
+
{{ page }}
|
|
119
|
+
</button>
|
|
120
|
+
<button @click="$emit('change-page', currentPage + 1)" :disabled="currentPage === totalPages">
|
|
121
|
+
Next
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<script>
|
|
130
|
+
export default {
|
|
131
|
+
name: "ActivityLogs",
|
|
132
|
+
props: {
|
|
133
|
+
loading: Boolean,
|
|
134
|
+
searchTerm: String,
|
|
135
|
+
selectedFilter: String,
|
|
136
|
+
logs: Array,
|
|
137
|
+
paginatedLogs: Array,
|
|
138
|
+
totalActivities: Number,
|
|
139
|
+
todayActivities: Number,
|
|
140
|
+
activeUsers: Number,
|
|
141
|
+
totalPages: Number,
|
|
142
|
+
currentPage: Number,
|
|
143
|
+
},
|
|
144
|
+
emits: ["update:searchTerm", "update:selectedFilter", "refresh", "download", "change-page"],
|
|
145
|
+
methods: {
|
|
146
|
+
formatDate(timestamp) {
|
|
147
|
+
return new Date(timestamp).toLocaleString();
|
|
148
|
+
},
|
|
149
|
+
getBadgeClass(action) {
|
|
150
|
+
const classes = {
|
|
151
|
+
login: "badge-login",
|
|
152
|
+
logout: "badge-logout",
|
|
153
|
+
create: "badge-create",
|
|
154
|
+
update: "badge-update",
|
|
155
|
+
delete: "badge-delete",
|
|
156
|
+
view: "badge-view",
|
|
157
|
+
};
|
|
158
|
+
return classes[action] || "badge-view";
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<style>
|
|
165
|
+
.header {
|
|
166
|
+
background: linear-gradient(135deg, var(--dashprimary-color), var(--dashsecondary-color));
|
|
167
|
+
color: white;
|
|
168
|
+
padding: 2rem 0;
|
|
169
|
+
margin-bottom: 2rem;
|
|
170
|
+
border-radius: 12px;
|
|
171
|
+
box-shadow: 0 10px 30px rgba(52, 153, 64, 0.3);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.header h1 {
|
|
175
|
+
font-size: 2.5rem;
|
|
176
|
+
font-weight: 700;
|
|
177
|
+
margin-bottom: 0.5rem;
|
|
178
|
+
text-align: center;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.header p {
|
|
182
|
+
text-align: center;
|
|
183
|
+
opacity: 0.9;
|
|
184
|
+
font-size: 1.1rem;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.controls {
|
|
188
|
+
display: flex;
|
|
189
|
+
gap: 1rem;
|
|
190
|
+
margin-bottom: 2rem;
|
|
191
|
+
flex-wrap: wrap;
|
|
192
|
+
align-items: center;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.search-box {
|
|
196
|
+
flex: 1;
|
|
197
|
+
min-width: 250px;
|
|
198
|
+
position: relative;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.search-box input {
|
|
202
|
+
width: 100%;
|
|
203
|
+
padding: 12px 16px;
|
|
204
|
+
border: 2px solid #e0e0e0;
|
|
205
|
+
border-radius: 8px;
|
|
206
|
+
font-size: 1rem;
|
|
207
|
+
transition: all 0.3s ease;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.search-box input:focus {
|
|
211
|
+
outline: none;
|
|
212
|
+
border-color: var(--dashsecondary-color);
|
|
213
|
+
box-shadow: 0 0 0 3px rgba(52, 153, 64, 0.1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.filter-select {
|
|
217
|
+
padding: 12px 16px;
|
|
218
|
+
border: 2px solid #e0e0e0;
|
|
219
|
+
border-radius: 8px;
|
|
220
|
+
font-size: 1rem;
|
|
221
|
+
background: white;
|
|
222
|
+
cursor: pointer;
|
|
223
|
+
transition: all 0.3s ease;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.filter-select:focus {
|
|
227
|
+
outline: none;
|
|
228
|
+
border-color: var(--dashsecondary-color);
|
|
229
|
+
box-shadow: 0 0 0 3px rgba(52, 153, 64, 0.1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.btn {
|
|
233
|
+
padding: 12px 24px;
|
|
234
|
+
border: none;
|
|
235
|
+
border-radius: 8px;
|
|
236
|
+
font-size: 1rem;
|
|
237
|
+
font-weight: 600;
|
|
238
|
+
cursor: pointer;
|
|
239
|
+
transition: all 0.3s ease;
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: 8px;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.btn-primary {
|
|
246
|
+
background: var(--primary);
|
|
247
|
+
color: white;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.btn-primary:hover {
|
|
251
|
+
background: var(--primary-dark);
|
|
252
|
+
transform: translateY(-1px);
|
|
253
|
+
box-shadow: 0 4px 12px rgba(52, 153, 64, 0.3);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.btn-secondary {
|
|
257
|
+
background: var(--secondary);
|
|
258
|
+
color: var(--dark);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.btn-secondary:hover {
|
|
262
|
+
background: #e6a200;
|
|
263
|
+
transform: translateY(-1px);
|
|
264
|
+
box-shadow: 0 4px 12px rgba(244, 180, 0, 0.3);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.stats-grid {
|
|
268
|
+
display: grid;
|
|
269
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
270
|
+
gap: 1.5rem;
|
|
271
|
+
margin-bottom: 2rem;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.stat-card {
|
|
275
|
+
background: white;
|
|
276
|
+
padding: 1.5rem;
|
|
277
|
+
border-radius: 12px;
|
|
278
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
|
279
|
+
transition: transform 0.3s ease;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.stat-card:hover {
|
|
283
|
+
transform: translateY(-2px);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.stat-card h3 {
|
|
287
|
+
color: var(--gray);
|
|
288
|
+
font-size: 0.9rem;
|
|
289
|
+
font-weight: 600;
|
|
290
|
+
text-transform: uppercase;
|
|
291
|
+
letter-spacing: 0.5px;
|
|
292
|
+
margin-bottom: 0.5rem;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.stat-card .value {
|
|
296
|
+
font-size: 2rem;
|
|
297
|
+
font-weight: 700;
|
|
298
|
+
color: var(--dashsecondary-color);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.logs-container {
|
|
302
|
+
background: white;
|
|
303
|
+
border-radius: 12px;
|
|
304
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
|
305
|
+
overflow: hidden;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.logs-header {
|
|
309
|
+
background: var(--light);
|
|
310
|
+
padding: 1.5rem;
|
|
311
|
+
border-bottom: 1px solid #e0e0e0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.logs-header h2 {
|
|
315
|
+
color: var(--dark);
|
|
316
|
+
font-size: 1.3rem;
|
|
317
|
+
font-weight: 600;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.logs-table {
|
|
321
|
+
width: 100%;
|
|
322
|
+
border-collapse: collapse;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.logs-table th,
|
|
326
|
+
.logs-table td {
|
|
327
|
+
padding: 1rem;
|
|
328
|
+
text-align: left;
|
|
329
|
+
border-bottom: 1px solid #f0f0f0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.logs-table th {
|
|
333
|
+
background: var(--light);
|
|
334
|
+
font-weight: 600;
|
|
335
|
+
color: var(--dark);
|
|
336
|
+
font-size: 0.9rem;
|
|
337
|
+
text-transform: uppercase;
|
|
338
|
+
letter-spacing: 0.5px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.logs-table tr:hover {
|
|
342
|
+
background: #f8f9fa;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.activity-badge {
|
|
346
|
+
padding: 4px 12px;
|
|
347
|
+
border-radius: 20px;
|
|
348
|
+
font-size: 0.8rem;
|
|
349
|
+
font-weight: 600;
|
|
350
|
+
text-transform: uppercase;
|
|
351
|
+
letter-spacing: 0.5px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.badge-login {
|
|
355
|
+
background: var(--success);
|
|
356
|
+
color: white;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.badge-logout {
|
|
360
|
+
background: var(--gray);
|
|
361
|
+
color: white;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.badge-create {
|
|
365
|
+
background: var(--dashprimary-color);
|
|
366
|
+
color: white;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.badge-update {
|
|
370
|
+
background: var(--warning);
|
|
371
|
+
color: white;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.badge-delete {
|
|
375
|
+
background: var(--danger);
|
|
376
|
+
color: white;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.badge-danger {
|
|
380
|
+
color: var(--danger);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.badge-view {
|
|
384
|
+
background: var(--maitsecondary);
|
|
385
|
+
color: white;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.pagination {
|
|
389
|
+
display: flex;
|
|
390
|
+
justify-content: center;
|
|
391
|
+
align-items: center;
|
|
392
|
+
gap: 0.5rem;
|
|
393
|
+
padding: 1.5rem;
|
|
394
|
+
background: var(--light);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.pagination button {
|
|
398
|
+
padding: 8px 12px;
|
|
399
|
+
border: 1px solid #e0e0e0;
|
|
400
|
+
background: white;
|
|
401
|
+
border-radius: 6px;
|
|
402
|
+
cursor: pointer;
|
|
403
|
+
transition: all 0.3s ease;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.pagination button:hover {
|
|
407
|
+
background: var(--primary);
|
|
408
|
+
color: white;
|
|
409
|
+
border-color: var(--primary);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.pagination button.active {
|
|
413
|
+
background: var(--primary);
|
|
414
|
+
color: white;
|
|
415
|
+
border-color: var(--primary);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.pagination button:disabled {
|
|
419
|
+
opacity: 0.5;
|
|
420
|
+
cursor: not-allowed;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.no-logs {
|
|
424
|
+
text-align: center;
|
|
425
|
+
padding: 3rem;
|
|
426
|
+
color: var(--gray);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.no-logs h3 {
|
|
430
|
+
font-size: 1.5rem;
|
|
431
|
+
margin-bottom: 1rem;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.loading {
|
|
435
|
+
text-align: center;
|
|
436
|
+
padding: 3rem;
|
|
437
|
+
color: var(--gray);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.spinner {
|
|
441
|
+
border: 3px solid #f3f3f3;
|
|
442
|
+
border-top: 3px solid var(--primary);
|
|
443
|
+
border-radius: 50%;
|
|
444
|
+
width: 40px;
|
|
445
|
+
height: 40px;
|
|
446
|
+
animation: spin 1s linear infinite;
|
|
447
|
+
margin: 0 auto 1rem;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
@keyframes spin {
|
|
451
|
+
0% {
|
|
452
|
+
transform: rotate(0deg);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
100% {
|
|
456
|
+
transform: rotate(360deg);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
@media (max-width: 768px) {
|
|
461
|
+
.controls {
|
|
462
|
+
flex-direction: column;
|
|
463
|
+
align-items: stretch;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.search-box {
|
|
467
|
+
min-width: 100%;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.stats-grid {
|
|
471
|
+
grid-template-columns: 1fr;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.logs-table {
|
|
475
|
+
font-size: 0.9rem;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.logs-table th,
|
|
479
|
+
.logs-table td {
|
|
480
|
+
padding: 0.5rem;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
</style>
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav id="sidebar" :class="sidebarClasses">
|
|
3
|
+
<!-- Logo Section -->
|
|
4
|
+
<div class="logo d-flex align-items-center left" v-if="showLogo">
|
|
5
|
+
<i :class="logoIcon" class="me-2"></i>
|
|
6
|
+
<span>{{ brandName }}</span>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<!-- Navigation Items -->
|
|
10
|
+
<ul class="list-unstyled components mt-4">
|
|
11
|
+
<li
|
|
12
|
+
v-for="(item, index) in filteredMenuItems"
|
|
13
|
+
:key="index"
|
|
14
|
+
:class="{ 'active': item.active, 'dropdown': item.type === 'dropdown' }"
|
|
15
|
+
@click="handleItemClick(item)"
|
|
16
|
+
>
|
|
17
|
+
<!-- Regular Link -->
|
|
18
|
+
<a
|
|
19
|
+
v-if="item.type === 'link'"
|
|
20
|
+
:href="item.href || '#'"
|
|
21
|
+
@click.prevent="handleNavigation(item)"
|
|
22
|
+
>
|
|
23
|
+
<i :class="item.icon" v-if="item.icon"></i>
|
|
24
|
+
{{ item.label }}
|
|
25
|
+
</a>
|
|
26
|
+
|
|
27
|
+
<!-- Button/Action -->
|
|
28
|
+
<a
|
|
29
|
+
v-else-if="item.type === 'button'"
|
|
30
|
+
@click.prevent="handleAction(item)"
|
|
31
|
+
style="cursor: pointer;"
|
|
32
|
+
>
|
|
33
|
+
<i :class="item.icon" v-if="item.icon"></i>
|
|
34
|
+
{{ item.label }}
|
|
35
|
+
</a>
|
|
36
|
+
|
|
37
|
+
<!-- Dropdown -->
|
|
38
|
+
<template v-else-if="item.type === 'dropdown'">
|
|
39
|
+
<a
|
|
40
|
+
class="dropdown-toggle"
|
|
41
|
+
type="button"
|
|
42
|
+
data-bs-toggle="dropdown"
|
|
43
|
+
aria-expanded="false"
|
|
44
|
+
>
|
|
45
|
+
<i :class="item.icon" v-if="item.icon"></i>
|
|
46
|
+
{{ item.label }}
|
|
47
|
+
</a>
|
|
48
|
+
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
|
|
49
|
+
<a
|
|
50
|
+
v-for="(subItem, subIndex) in item.children"
|
|
51
|
+
:key="subIndex"
|
|
52
|
+
class="dropdown-item black"
|
|
53
|
+
@click.prevent="handleAction(subItem)"
|
|
54
|
+
style="cursor: pointer;"
|
|
55
|
+
>
|
|
56
|
+
{{ subItem.label }}
|
|
57
|
+
</a>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<!-- Separator -->
|
|
62
|
+
<div
|
|
63
|
+
v-else-if="item.type === 'separator'"
|
|
64
|
+
class="dropdown-divider"
|
|
65
|
+
></div>
|
|
66
|
+
|
|
67
|
+
<!-- Static Text -->
|
|
68
|
+
<a v-else-if="item.type === 'text'">
|
|
69
|
+
{{ item.label }}
|
|
70
|
+
</a>
|
|
71
|
+
</li>
|
|
72
|
+
</ul>
|
|
73
|
+
|
|
74
|
+
<!-- Custom Slot for Additional Content -->
|
|
75
|
+
<slot name="footer"></slot>
|
|
76
|
+
</nav>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<script>
|
|
80
|
+
export default {
|
|
81
|
+
name: 'ConfigurableSidebar',
|
|
82
|
+
|
|
83
|
+
props: {
|
|
84
|
+
// Brand configuration
|
|
85
|
+
brandName: {
|
|
86
|
+
type: String,
|
|
87
|
+
default: 'OceanHelm'
|
|
88
|
+
},
|
|
89
|
+
logoIcon: {
|
|
90
|
+
type: String,
|
|
91
|
+
default: 'bi bi-water'
|
|
92
|
+
},
|
|
93
|
+
showLogo: {
|
|
94
|
+
type: Boolean,
|
|
95
|
+
default: true
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Menu items configuration
|
|
99
|
+
menuItems: {
|
|
100
|
+
type: Array,
|
|
101
|
+
required: true,
|
|
102
|
+
default: () => []
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// User profile for role-based filtering
|
|
106
|
+
userProfile: {
|
|
107
|
+
type: Object,
|
|
108
|
+
default: () => ({})
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// Sidebar behavior
|
|
112
|
+
responsive: {
|
|
113
|
+
type: Boolean,
|
|
114
|
+
default: true
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// Custom classes
|
|
118
|
+
customClasses: {
|
|
119
|
+
type: String,
|
|
120
|
+
default: ''
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// Permission checker function
|
|
124
|
+
permissionChecker: {
|
|
125
|
+
type: Function,
|
|
126
|
+
default: null
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
computed: {
|
|
131
|
+
sidebarClasses() {
|
|
132
|
+
return this.customClasses;
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
filteredMenuItems() {
|
|
136
|
+
return this.menuItems.filter(item => this.hasPermission(item));
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
mounted() {
|
|
141
|
+
if (this.responsive) {
|
|
142
|
+
this.initializeResponsiveBehavior();
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
methods: {
|
|
147
|
+
// Permission checking
|
|
148
|
+
hasPermission(item) {
|
|
149
|
+
// Use custom permission checker if provided
|
|
150
|
+
if (this.permissionChecker) {
|
|
151
|
+
return this.permissionChecker(item, this.userProfile);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Default role-based checking
|
|
155
|
+
if (!item.roles || item.roles.length === 0) {
|
|
156
|
+
return true; // No role restriction
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return item.roles.includes(this.userProfile.role);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
// Navigation handling
|
|
163
|
+
handleNavigation(item) {
|
|
164
|
+
this.$emit('navigate', item);
|
|
165
|
+
|
|
166
|
+
// Default behavior if no custom handler
|
|
167
|
+
if (item.href && !item.preventDefault) {
|
|
168
|
+
if (item.external) {
|
|
169
|
+
window.open(item.href, '_blank');
|
|
170
|
+
} else {
|
|
171
|
+
this.$router?.push(item.href);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// Action handling
|
|
177
|
+
handleAction(item) {
|
|
178
|
+
this.$emit('action', item);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// Item click handling
|
|
182
|
+
handleItemClick(item) {
|
|
183
|
+
this.$emit('item-click', item);
|
|
184
|
+
|
|
185
|
+
if (item.type === 'dropdown') {
|
|
186
|
+
// Handle dropdown specific logic if needed
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// Responsive behavior
|
|
191
|
+
initializeResponsiveBehavior() {
|
|
192
|
+
const sidebarToggle = document.getElementById('sidebarToggle');
|
|
193
|
+
const sidebar = document.getElementById('sidebar');
|
|
194
|
+
const content = document.getElementById('content');
|
|
195
|
+
|
|
196
|
+
if (!sidebarToggle || !sidebar || !content) return;
|
|
197
|
+
|
|
198
|
+
// Initial state for desktop
|
|
199
|
+
if (window.innerWidth >= 768) {
|
|
200
|
+
sidebar.classList.toggle('active');
|
|
201
|
+
content.classList.toggle('active');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Toggle functionality
|
|
205
|
+
sidebarToggle.addEventListener('click', () => {
|
|
206
|
+
sidebar.classList.toggle('active');
|
|
207
|
+
content.classList.toggle('active');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Close sidebar on outside click (mobile)
|
|
211
|
+
document.addEventListener('click', (event) => {
|
|
212
|
+
const isClickInsideSidebar = sidebar.contains(event.target);
|
|
213
|
+
const isClickOnToggleBtn = sidebarToggle.contains(event.target);
|
|
214
|
+
|
|
215
|
+
if (!isClickInsideSidebar && !isClickOnToggleBtn &&
|
|
216
|
+
window.innerWidth < 768 &&
|
|
217
|
+
sidebar.classList.contains('active')) {
|
|
218
|
+
sidebar.classList.remove('active');
|
|
219
|
+
content.classList.remove('active');
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
emits: ['navigate', 'action', 'item-click']
|
|
226
|
+
}
|
|
227
|
+
</script>
|
|
228
|
+
|
|
229
|
+
<style scoped>
|
|
230
|
+
.left {
|
|
231
|
+
margin-left: 20px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.black {
|
|
235
|
+
color: black !important;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Add any other necessary styles */
|
|
239
|
+
</style>
|