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.
@@ -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>