oceanhelm 0.0.9 → 0.0.11

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.
@@ -51,4 +51,14 @@ export default {
51
51
  border-bottom: 1px solid #e0e0e0;
52
52
  padding-bottom: 15px;
53
53
  }
54
+
55
+ .btn-outline-primary {
56
+ border: 1px solid #0d6efd !important;
57
+ }
58
+
59
+ .btn-outline-primary:hover {
60
+ background-color: #0d6efd !important;
61
+ border-color: #0d6efd !important;
62
+ color: white !important;
63
+ }
54
64
  </style>
@@ -0,0 +1,612 @@
1
+ <template>
2
+ <div class="reports-container">
3
+ <div class="page-header d-flex justify-content-between align-items-center">
4
+ <h4 style="margin-left: 20px;">Reports</h4>
5
+ <div class="d-flex">
6
+ <button class="btn btn-primary" @click="showNewFolderModal = true">
7
+ <i class="bi bi-folder-plus"></i> New Folder
8
+ </button>
9
+ </div>
10
+ </div>
11
+
12
+ <!-- Folders List -->
13
+ <div class="folders-section" v-if="folders.length > 0">
14
+ <div class="folder-card"
15
+ v-for="folder in folders"
16
+ :key="folder.id"
17
+ @mouseenter="showPreview(folder.id)"
18
+ @mouseleave="hidePreview">
19
+ <div class="folder-content" @click="toggleFolder(folder.id)">
20
+ <i class="bi bi-folder-fill text-warning" style="font-size: 2.5rem;"></i>
21
+ <div class="folder-name">{{ folder.name }}</div>
22
+ <small class="text-muted">{{ folder.files.length }} file(s)</small>
23
+ </div>
24
+
25
+ <div class="folder-actions-overlay">
26
+ <button class="btn btn-sm btn-icon" @click.stop="selectFolder(folder)" title="Add Files">
27
+ <i class="bi bi-file-earmark-plus"></i>
28
+ </button>
29
+ <button class="btn btn-sm btn-icon" @click.stop="deleteFolder(folder.id)" title="Delete Folder">
30
+ <i class="bi bi-trash"></i>
31
+ </button>
32
+ </div>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- Empty State -->
37
+ <div class="empty-state text-center" v-else>
38
+ <i class="bi bi-folder2-open" style="font-size: 4rem; color: #ccc;"></i>
39
+ <h5 class="mt-3 text-muted">No folders yet</h5>
40
+ <p class="text-muted">Create a folder to start organizing your reports</p>
41
+ </div>
42
+
43
+ <!-- Expanded Folder Modal (Click to Open) -->
44
+ <div class="folder-modal-overlay" v-if="expandedFolders.length > 0" @click="closeAllFolders">
45
+ <div class="folder-expanded" @click.stop>
46
+ <div class="expanded-header">
47
+ <strong>{{ getExpandedFolder().name }}</strong>
48
+ <button class="btn btn-sm btn-close-expanded" @click="closeAllFolders">
49
+ <i class="bi bi-x-lg"></i>
50
+ </button>
51
+ </div>
52
+
53
+ <div class="files-list" v-if="getExpandedFolder().files.length > 0">
54
+ <div class="file-item d-flex justify-content-between align-items-center"
55
+ v-for="file in getExpandedFolder().files" :key="file.id">
56
+ <div class="file-info d-flex align-items-center">
57
+ <i class="bi bi-file-earmark-text me-2"></i>
58
+ <span>{{ file.name }}</span>
59
+ <small class="text-muted ms-2">({{ formatFileSize(file.size) }})</small>
60
+ </div>
61
+ <button class="btn btn-sm btn-outline-danger" @click="removeFile(getExpandedFolder().id, file.id)">
62
+ <i class="bi bi-x-circle"></i>
63
+ </button>
64
+ </div>
65
+ </div>
66
+ <div v-else class="empty-folder-message">
67
+ <i class="bi bi-inbox"></i>
68
+ <p>No files in this folder</p>
69
+ <button class="btn btn-sm btn-primary" @click="selectFolder(getExpandedFolder())">
70
+ <i class="bi bi-file-earmark-plus"></i> Add Files
71
+ </button>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- New Folder Modal -->
77
+ <div class="modal" :class="{ 'show': showNewFolderModal }" @click.self="showNewFolderModal = false">
78
+ <div class="modal-dialog modal-dialog-centered">
79
+ <div class="modal-content">
80
+ <div class="modal-header">
81
+ <h5 class="modal-title">Create New Folder</h5>
82
+ <button type="button" class="btn-close" @click="showNewFolderModal = false"></button>
83
+ </div>
84
+ <div class="modal-body">
85
+ <div class="mb-3">
86
+ <label class="form-label">Folder Name</label>
87
+ <input
88
+ type="text"
89
+ class="form-control"
90
+ v-model="newFolderName"
91
+ @keyup.enter="createFolder"
92
+ placeholder="Enter folder name"
93
+ />
94
+ </div>
95
+ </div>
96
+ <div class="modal-footer">
97
+ <button type="button" class="btn btn-secondary" @click="showNewFolderModal = false">Cancel</button>
98
+ <button type="button" class="btn btn-primary" @click="createFolder" :disabled="!newFolderName.trim()">
99
+ Create Folder
100
+ </button>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- File Input (Hidden) -->
107
+ <input
108
+ ref="fileInput"
109
+ type="file"
110
+ multiple
111
+ style="display: none;"
112
+ @change="handleFileSelection"
113
+ />
114
+ </div>
115
+ </template>
116
+
117
+ <script>
118
+ export default {
119
+ name: 'Reports',
120
+
121
+ data() {
122
+ return {
123
+ folders: [],
124
+ showNewFolderModal: false,
125
+ newFolderName: '',
126
+ selectedFolder: null,
127
+ expandedFolders: [],
128
+ previewFolder: null
129
+ }
130
+ },
131
+
132
+ emits: [
133
+ 'folder-created',
134
+ 'files-added',
135
+ 'file-removed',
136
+ 'folder-deleted',
137
+ 'data-updated'
138
+ ],
139
+
140
+ methods: {
141
+ createFolder() {
142
+ if (!this.newFolderName.trim()) return;
143
+
144
+ const newFolder = {
145
+ id: Date.now(),
146
+ name: this.newFolderName.trim(),
147
+ files: [],
148
+ createdAt: new Date()
149
+ };
150
+
151
+ this.folders.push(newFolder);
152
+ this.$emit('folder-created', newFolder);
153
+ this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData() });
154
+
155
+ this.newFolderName = '';
156
+ this.showNewFolderModal = false;
157
+ },
158
+
159
+ selectFolder(folder) {
160
+ this.selectedFolder = folder;
161
+ this.$refs.fileInput.click();
162
+ },
163
+
164
+ handleFileSelection(event) {
165
+ const files = Array.from(event.target.files);
166
+
167
+ if (!this.selectedFolder || files.length === 0) return;
168
+
169
+ const newFiles = files.map(file => ({
170
+ id: Date.now() + Math.random(),
171
+ name: file.name,
172
+ size: file.size,
173
+ type: file.type,
174
+ file: file,
175
+ addedAt: new Date()
176
+ }));
177
+
178
+ this.selectedFolder.files.push(...newFiles);
179
+
180
+ this.$emit('files-added', {
181
+ folderId: this.selectedFolder.id,
182
+ files: newFiles
183
+ });
184
+ this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData() });
185
+
186
+ // Reset file input
187
+ event.target.value = '';
188
+ this.selectedFolder = null;
189
+ },
190
+
191
+ removeFile(folderId, fileId) {
192
+ const folder = this.folders.find(f => f.id === folderId);
193
+ if (folder) {
194
+ const fileIndex = folder.files.findIndex(f => f.id === fileId);
195
+ if (fileIndex > -1) {
196
+ const removedFile = folder.files.splice(fileIndex, 1)[0];
197
+ this.$emit('file-removed', {
198
+ folderId,
199
+ file: removedFile
200
+ });
201
+ this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData() });
202
+ }
203
+ }
204
+ },
205
+
206
+ deleteFolder(folderId) {
207
+ if (confirm('Are you sure you want to delete this folder and all its files?')) {
208
+ const folderIndex = this.folders.findIndex(f => f.id === folderId);
209
+ if (folderIndex > -1) {
210
+ const deletedFolder = this.folders.splice(folderIndex, 1)[0];
211
+ this.$emit('folder-deleted', deletedFolder);
212
+ this.$emit('data-updated', { folders: this.folders, allFiles: this.getAllFilesData() });
213
+ }
214
+ }
215
+ },
216
+
217
+ formatFileSize(bytes) {
218
+ if (bytes === 0) return '0 Bytes';
219
+ const k = 1024;
220
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
221
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
222
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
223
+ },
224
+
225
+ getAllFilesData() {
226
+ return this.folders.flatMap(folder =>
227
+ folder.files.map(file => ({
228
+ ...file,
229
+ folderName: folder.name,
230
+ folderId: folder.id
231
+ }))
232
+ );
233
+ },
234
+
235
+ toggleFolder(folderId) {
236
+ const index = this.expandedFolders.indexOf(folderId);
237
+ if (index > -1) {
238
+ this.expandedFolders.splice(index, 1);
239
+ } else {
240
+ this.expandedFolders = [folderId]; // Only one folder expanded at a time
241
+ }
242
+ },
243
+
244
+ closeAllFolders() {
245
+ this.expandedFolders = [];
246
+ },
247
+
248
+ getExpandedFolder() {
249
+ if (this.expandedFolders.length > 0) {
250
+ return this.folders.find(f => f.id === this.expandedFolders[0]) || { files: [], name: '' };
251
+ }
252
+ return { files: [], name: '' };
253
+ },
254
+
255
+ showPreview(folderId) {
256
+ this.previewFolder = folderId;
257
+ },
258
+
259
+ hidePreview() {
260
+ this.previewFolder = null;
261
+ }
262
+ }
263
+ }
264
+ </script>
265
+
266
+ <style scoped>
267
+ .reports-container {
268
+ padding: 20px;
269
+ }
270
+
271
+ .page-header {
272
+ margin-bottom: 30px;
273
+ border-bottom: 1px solid #e0e0e0;
274
+ padding-bottom: 15px;
275
+ }
276
+
277
+ .folders-section {
278
+ display: grid;
279
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
280
+ gap: 20px;
281
+ padding: 10px;
282
+ }
283
+
284
+ @media (max-width: 768px) {
285
+ .folders-section {
286
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
287
+ gap: 15px;
288
+ }
289
+ }
290
+
291
+ @media (min-width: 1400px) {
292
+ .folders-section {
293
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
294
+ }
295
+ }
296
+
297
+ .folder-card {
298
+ position: relative;
299
+ border: 2px solid #e0e0e0;
300
+ border-radius: 8px;
301
+ padding: 15px;
302
+ background-color: #fff;
303
+ transition: all 0.2s;
304
+ cursor: pointer;
305
+ aspect-ratio: 1;
306
+ display: flex;
307
+ flex-direction: column;
308
+ align-items: center;
309
+ justify-content: center;
310
+ }
311
+
312
+ .folder-card:hover {
313
+ border-color: #0d6efd;
314
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
315
+ transform: translateY(-2px);
316
+ }
317
+
318
+ /* Hover Preview Popup */
319
+ .folder-preview {
320
+ position: absolute;
321
+ top: 50%;
322
+ left: 50%;
323
+ transform: translate(-50%, -50%);
324
+ background: white;
325
+ border: 2px solid #0d6efd;
326
+ border-radius: 8px;
327
+ box-shadow: 0 8px 24px rgba(0,0,0,0.15);
328
+ padding: 0;
329
+ z-index: 100;
330
+ min-width: 280px;
331
+ max-width: 320px;
332
+ pointer-events: none;
333
+ }
334
+
335
+ .preview-header {
336
+ background: #f8f9fa;
337
+ padding: 12px 15px;
338
+ border-bottom: 1px solid #e0e0e0;
339
+ border-radius: 6px 6px 0 0;
340
+ }
341
+
342
+ .preview-header strong {
343
+ font-size: 0.95rem;
344
+ color: #333;
345
+ }
346
+
347
+ .preview-content {
348
+ padding: 10px;
349
+ max-height: 200px;
350
+ overflow-y: auto;
351
+ }
352
+
353
+ .preview-file {
354
+ display: flex;
355
+ align-items: center;
356
+ gap: 8px;
357
+ padding: 6px 8px;
358
+ font-size: 0.85rem;
359
+ color: #495057;
360
+ border-bottom: 1px solid #f0f0f0;
361
+ }
362
+
363
+ .preview-file:last-child {
364
+ border-bottom: none;
365
+ }
366
+
367
+ .preview-file i {
368
+ color: #6c757d;
369
+ font-size: 0.9rem;
370
+ }
371
+
372
+ .preview-file span {
373
+ white-space: nowrap;
374
+ overflow: hidden;
375
+ text-overflow: ellipsis;
376
+ }
377
+
378
+ .preview-more {
379
+ text-align: center;
380
+ padding: 8px;
381
+ font-size: 0.8rem;
382
+ color: #6c757d;
383
+ font-style: italic;
384
+ }
385
+
386
+ .preview-empty {
387
+ padding: 30px 20px;
388
+ text-align: center;
389
+ color: #adb5bd;
390
+ }
391
+
392
+ .preview-empty i {
393
+ font-size: 2rem;
394
+ display: block;
395
+ margin-bottom: 8px;
396
+ }
397
+
398
+ .folder-content {
399
+ display: flex;
400
+ flex-direction: column;
401
+ align-items: center;
402
+ justify-content: center;
403
+ text-align: center;
404
+ width: 100%;
405
+ height: 100%;
406
+ }
407
+
408
+ .folder-name {
409
+ margin-top: 10px;
410
+ font-weight: 600;
411
+ font-size: 0.9rem;
412
+ word-break: break-word;
413
+ max-width: 100%;
414
+ overflow: hidden;
415
+ text-overflow: ellipsis;
416
+ display: -webkit-box;
417
+ -webkit-line-clamp: 2;
418
+ -webkit-box-orient: vertical;
419
+ }
420
+
421
+ .folder-actions-overlay {
422
+ position: absolute;
423
+ top: 5px;
424
+ right: 5px;
425
+ display: flex;
426
+ gap: 5px;
427
+ opacity: 0;
428
+ transition: opacity 0.2s;
429
+ }
430
+
431
+ .folder-card:hover .folder-actions-overlay {
432
+ opacity: 1;
433
+ }
434
+
435
+ .btn-icon {
436
+ width: 28px;
437
+ height: 28px;
438
+ padding: 0;
439
+ display: flex;
440
+ align-items: center;
441
+ justify-content: center;
442
+ background: white;
443
+ border: 1px solid #dee2e6;
444
+ border-radius: 4px;
445
+ }
446
+
447
+ .btn-icon:hover {
448
+ background: #f8f9fa;
449
+ border-color: #0d6efd;
450
+ }
451
+
452
+ .btn-icon i {
453
+ font-size: 0.85rem;
454
+ }
455
+
456
+ /* Expanded Folder Modal (Click to Open) */
457
+ .folder-modal-overlay {
458
+ position: fixed;
459
+ top: 0;
460
+ left: 0;
461
+ right: 0;
462
+ bottom: 0;
463
+ background: rgba(0,0,0,0.5);
464
+ z-index: 1000;
465
+ display: flex;
466
+ align-items: center;
467
+ justify-content: center;
468
+ }
469
+
470
+ .folder-expanded {
471
+ background: white;
472
+ border-radius: 12px;
473
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
474
+ width: 90%;
475
+ max-width: 700px;
476
+ max-height: 80vh;
477
+ overflow: hidden;
478
+ display: flex;
479
+ flex-direction: column;
480
+ }
481
+
482
+ .expanded-header {
483
+ display: flex;
484
+ justify-content: space-between;
485
+ align-items: center;
486
+ padding: 20px 25px;
487
+ border-bottom: 2px solid #f0f0f0;
488
+ background: #f8f9fa;
489
+ }
490
+
491
+ .expanded-header strong {
492
+ font-size: 1.3rem;
493
+ color: #333;
494
+ }
495
+
496
+ .btn-close-expanded {
497
+ background: transparent;
498
+ border: none;
499
+ font-size: 1.2rem;
500
+ cursor: pointer;
501
+ padding: 5px 10px;
502
+ display: flex;
503
+ align-items: center;
504
+ justify-content: center;
505
+ transition: color 0.2s;
506
+ }
507
+
508
+ .btn-close-expanded:hover {
509
+ color: #dc3545;
510
+ }
511
+
512
+ .files-list {
513
+ padding: 20px;
514
+ max-height: 60vh;
515
+ overflow-y: auto;
516
+ }
517
+
518
+ .file-item {
519
+ padding: 12px;
520
+ background-color: #f8f9fa;
521
+ border-radius: 4px;
522
+ margin-bottom: 8px;
523
+ }
524
+
525
+ .file-item:last-child {
526
+ margin-bottom: 0;
527
+ }
528
+
529
+ .empty-folder-message {
530
+ padding: 40px 20px;
531
+ text-align: center;
532
+ color: #6c757d;
533
+ }
534
+
535
+ .empty-folder-message i {
536
+ font-size: 3rem;
537
+ color: #dee2e6;
538
+ }
539
+
540
+ .empty-folder-message p {
541
+ margin: 15px 0;
542
+ }
543
+
544
+ .empty-state {
545
+ padding: 60px 20px;
546
+ }
547
+
548
+ /* Modal Styles */
549
+ .modal {
550
+ display: none;
551
+ position: fixed;
552
+ z-index: 1050;
553
+ left: 0;
554
+ top: 0;
555
+ width: 100%;
556
+ height: 100%;
557
+ overflow: hidden;
558
+ background-color: rgba(0,0,0,0.5);
559
+ align-items: center;
560
+ justify-content: center;
561
+ }
562
+
563
+ .modal.show {
564
+ display: flex;
565
+ }
566
+
567
+ .modal-dialog {
568
+ max-width: 500px;
569
+ margin: 1.75rem auto;
570
+ }
571
+
572
+ .modal-content {
573
+ background-color: #fff;
574
+ border-radius: 8px;
575
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
576
+ }
577
+
578
+ .modal-header {
579
+ padding: 1rem;
580
+ border-bottom: 1px solid #dee2e6;
581
+ display: flex;
582
+ justify-content: space-between;
583
+ align-items: center;
584
+ }
585
+
586
+ .modal-body {
587
+ padding: 1rem;
588
+ }
589
+
590
+ .modal-footer {
591
+ padding: 1rem;
592
+ border-top: 1px solid #dee2e6;
593
+ display: flex;
594
+ justify-content: flex-end;
595
+ gap: 10px;
596
+ }
597
+
598
+ .btn-close {
599
+ background: transparent;
600
+ border: none;
601
+ font-size: 1.5rem;
602
+ font-weight: 700;
603
+ line-height: 1;
604
+ color: #000;
605
+ opacity: 0.5;
606
+ cursor: pointer;
607
+ }
608
+
609
+ .btn-close:hover {
610
+ opacity: 0.75;
611
+ }
612
+ </style>
package/src/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import ConfigurableSidebar from './components/ConfigurableSidebar.vue'
3
3
  import VesselList from './components/VesselList.vue'
4
4
  import DashHead from './components/DashHead.vue'
5
+ import Reports from './components/Reports.vue'
5
6
  import OceanHelmMaintenance from './components/OceanHelmMaintenance.vue'
6
7
  import ActivityLogs from './components/ActivityLogs.vue'
7
8
  import CrewManagement from './components/CrewManagement.vue'
@@ -11,7 +12,7 @@ import { createSidebarConfig, defaultMenuItems } from './utils/sidebarConfig'
11
12
  import { defaultPermissionChecker } from './utils/permissions'
12
13
 
13
14
  // Export main components
14
- export { ConfigurableSidebar, VesselList, DashHead, CrewManagement, ActivityLogs, OceanHelmMaintenance, RequisitionSystem }
15
+ export { ConfigurableSidebar, VesselList, DashHead, Reports, CrewManagement, ActivityLogs, OceanHelmMaintenance, RequisitionSystem }
15
16
 
16
17
  // Export utilities
17
18
  export { createSidebarConfig, defaultMenuItems, defaultPermissionChecker }
@@ -25,6 +26,7 @@ export default {
25
26
  app.component('ConfigurableSidebar', ConfigurableSidebar)
26
27
  app.component('VesselLists', VesselList)
27
28
  app.component('DashHead', DashHead)
29
+ app.component('Reports', Reports)
28
30
  app.component('ActivityLogs', ActivityLogs)
29
31
  app.component('CrewManagement', CrewManagement)
30
32
  app.component('RequisitionSystem', RequisitionSystem)