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,1615 @@
1
+ <template>
2
+ <div class="inventory-management">
3
+ <!-- Wave background -->
4
+ <div class="wave-bg"></div>
5
+
6
+ <!-- Sidebar Toggle Button -->
7
+ <button class="toggle-btn" @click="$emit('toggle-sidebar')">
8
+ <i class="bi bi-list"></i>
9
+ </button>
10
+
11
+ <div class="inventory-container">
12
+ <!-- Header -->
13
+ <div class="header">
14
+ <div>
15
+ <h1><i class="fas fa-ship"></i>{{ title || 'OceanHelm Inventory Management' }}</h1>
16
+ <div class="header-stats">
17
+ <div class="stat-card">
18
+ <div class="stat-value">{{ vessels.length }}</div>
19
+ <div class="stat-label">Total Vessels</div>
20
+ </div>
21
+ <div class="stat-card" v-if="selectedTab === 'inventory'">
22
+ <div class="stat-value">{{ totalItems }}</div>
23
+ <div class="stat-label">Total Items</div>
24
+ </div>
25
+ <div class="stat-card" v-if="selectedTab === 'inventory'">
26
+ <div class="stat-value">{{ lowStockCount }}</div>
27
+ <div class="stat-label">Low Stock</div>
28
+ </div>
29
+ <div class="stat-card" v-if="selectedTab === 'inventory'">
30
+ <div class="stat-value">₦{{ totalValue.toLocaleString() }}</div>
31
+ <div class="stat-label">Total Value</div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <div class="header-actions">
36
+ <!-- Tabs -->
37
+ <div class="tabs">
38
+ <button class="tab" :class="{ active: selectedTab === 'overview' }" @click="selectedTab = 'overview'"
39
+ v-if="hasPermission(['owner', 'staff'])">
40
+ <i class="fas fa-chart-bar"></i>
41
+ Overview
42
+ </button>
43
+ <button class="tab" :class="{ active: selectedTab === 'inventory' }" @click="selectedTab = 'inventory'"
44
+ v-if="hasPermission(['owner', 'staff'])">
45
+ <i class="fas fa-boxes"></i>
46
+ Inventory
47
+ </button>
48
+ <button class="tab" :class="{ active: selectedTab === 'vessels' }" @click="selectedTab = 'vessels'">
49
+ <i class="fas fa-ship"></i>
50
+ Vessels
51
+ </button>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <!-- Inventory Table -->
57
+ <div class="inventory-table" v-if="selectedTab === 'inventory'">
58
+ <!-- Low Stock Alerts -->
59
+ <div v-if="lowStockItems.length > 0" class="low-stock-alerts">
60
+ <div class="alert-header">
61
+ <i class="fas fa-exclamation-triangle"></i>
62
+ Low Stock Alerts ({{ lowStockItems.length }} items)
63
+ </div>
64
+ <ul class="alert-list">
65
+ <li v-for="item in lowStockItems" :key="item.id">
66
+ <strong>{{ item.itemName }}</strong> - {{ item.vessel }} ({{ item.currentStock }}/{{ item.minStock }} {{ item.unit }})
67
+ </li>
68
+ </ul>
69
+ </div>
70
+
71
+ <!-- Controls -->
72
+ <div class="controls">
73
+ <div class="controls-row">
74
+ <div class="search-box">
75
+ <input type="text" placeholder="Search items, vessels, or categories..." v-model="searchTerm">
76
+ <i class="fas fa-search"></i>
77
+ </div>
78
+ <select v-model="selectedVessel" class="filter-select">
79
+ <option value="">All Vessels</option>
80
+ <option v-for="vessel in vessels" :key="vessel.id" :value="vessel.name">{{ vessel.name }}</option>
81
+ </select>
82
+ <select v-model="selectedCategory" class="filter-select">
83
+ <option value="">All Categories</option>
84
+ <option v-for="category in availableCategories" :key="category" :value="category">{{ category }}</option>
85
+ </select>
86
+ <button class="btn btn-primary" @click="addNewItem">
87
+ <i class="fas fa-plus"></i>
88
+ Add Item
89
+ </button>
90
+ <button class="btn btn-secondary" @click="generatePDF">
91
+ <i class="fas fa-download"></i>
92
+ Export
93
+ </button>
94
+ <button class="btn btn-secondary" @click="toggleImportOptions">
95
+ <i class="fas fa-upload"></i>
96
+ Import
97
+ </button>
98
+ </div>
99
+ </div>
100
+
101
+ <div class="table-responsive item-row">
102
+ <table class="table">
103
+ <thead>
104
+ <tr>
105
+ <th>Part Number</th>
106
+ <th>Item Name</th>
107
+ <th>Category</th>
108
+ <th>Vessel</th>
109
+ <th>Location</th>
110
+ <th>Stock Level</th>
111
+ <th>Status</th>
112
+ <th>Last Updated</th>
113
+ <th>Value</th>
114
+ <th>Actions</th>
115
+ </tr>
116
+ </thead>
117
+ <tbody>
118
+ <tr v-for="item in paginatedInventory" :key="item.id" :class="{ 'inactive-item': !item.active }">
119
+ <td><strong>{{ item.id }}</strong></td>
120
+ <td>{{ item.itemName }}</td>
121
+ <td>{{ item.category }}</td>
122
+ <td>{{ item.vessel }}</td>
123
+ <td>
124
+ <i class="fas fa-map-marker-alt"></i>
125
+ {{ item.location }}
126
+ </td>
127
+ <td>
128
+ <div class="stock-level">
129
+ <span>{{ item.currentStock }}</span>
130
+ <div class="stock-bar">
131
+ <div class="stock-fill" :class="getStockClass(item)"
132
+ :style="{ width: getStockPercentage(item) + '%' }"></div>
133
+ </div>
134
+ </div>
135
+ </td>
136
+ <td>
137
+ <span class="status-badge" :class="getStatusClass(item.status)">
138
+ {{ item.status }}
139
+ </span>
140
+ </td>
141
+ <td>
142
+ <i class="fas fa-calendar"></i>
143
+ {{ formatDate(item.lastUpdated) }}
144
+ </td>
145
+ <td>₦{{ item.value ? item.value.toLocaleString() : '0' }}</td>
146
+ <td>
147
+ <div class="action-btns">
148
+ <button class="action-btn btn-view" @click="stockIn(item)" title="Stock In">
149
+ <i class="fa-solid fa-arrow-trend-up"></i>
150
+ </button>
151
+ <button class="action-btn btn-view" @click="stockOut(item)" title="Stock Out">
152
+ <i class="fa-solid fa-arrow-trend-down"></i>
153
+ </button>
154
+ <button class="action-btn btn-view" @click="transferItem(item)" title="Transfer Item">
155
+ <i class="fa-solid fa-arrow-up-from-bracket"></i>
156
+ </button>
157
+ <button class="action-btn btn-edit" @click="editItem(item)" title="Edit Item">
158
+ <i class="fas fa-edit"></i>
159
+ </button>
160
+ <button class="action-btn btn-delete" @click="deleteItem(item)"
161
+ :title="item.active ? 'Archive Item' : 'Restore Item'">
162
+ <i :class="[
163
+ 'fa',
164
+ item.active ? 'fa-box-archive' : 'fa-undo',
165
+ 'icon-button'
166
+ ]" aria-hidden="true"></i>
167
+ </button>
168
+ </div>
169
+ </td>
170
+ </tr>
171
+ </tbody>
172
+ </table>
173
+ </div>
174
+
175
+ <div v-if="filteredInventory.length === 0" class="empty-state">
176
+ <i class="fas fa-box-open"></i>
177
+ <h3>No items found</h3>
178
+ <p>Try adjusting your search or filters</p>
179
+ </div>
180
+
181
+ <!-- Pagination -->
182
+ <div class="pagination" v-if="filteredInventory.length > 0">
183
+ <button @click="prevPage" :disabled="currentPage === 1">
184
+ <i class="fas fa-chevron-left"></i>
185
+ </button>
186
+ <button v-for="page in totalPages" :key="page" @click="currentPage = page"
187
+ :class="{ active: currentPage === page }">
188
+ {{ page }}
189
+ </button>
190
+ <button @click="nextPage" :disabled="currentPage === totalPages">
191
+ <i class="fas fa-chevron-right"></i>
192
+ </button>
193
+ </div>
194
+ </div>
195
+
196
+ <!-- Overview Tab Content -->
197
+ <div v-if="selectedTab === 'overview'" class="inventory-table">
198
+ <div class="controls" v-if="hasPermission(['owner', 'staff'])">
199
+ <div class="controls-row">
200
+ <div class="search-box">
201
+ <input type="text" placeholder="Search items, vessels, or categories..." v-model="searchTerm">
202
+ <i class="fas fa-search"></i>
203
+ </div>
204
+ <select v-model="selectedVessel" class="filter-select">
205
+ <option value="">All Vessels</option>
206
+ <option v-for="vessel in vessels" :key="vessel.id" :value="vessel.name">{{ vessel.name }}</option>
207
+ </select>
208
+ <select v-model="selectedCategory" class="filter-select">
209
+ <option value="">All Categories</option>
210
+ <option v-for="category in availableCategories" :key="category" :value="category">{{ category }}</option>
211
+ </select>
212
+ </div>
213
+ </div>
214
+ <div style="padding: 40px; text-align: center;">
215
+ <div class="dashboard div-responsive">
216
+ <div class="charts-grid">
217
+ <div class="chart-card">
218
+ <h3 class="chart-title">Inventory by Category</h3>
219
+ <div class="chart-container">
220
+ <canvas ref="categoryChart"></canvas>
221
+ </div>
222
+ </div>
223
+
224
+ <div class="chart-card">
225
+ <h3 class="chart-title">Stock Status Distribution</h3>
226
+ <div class="chart-container">
227
+ <canvas ref="stockChart"></canvas>
228
+ </div>
229
+ </div>
230
+
231
+ <div class="chart-card full-width">
232
+ <h3 class="chart-title">Stock In/Out/Transfer (Last 6 Months)</h3>
233
+ <div class="">
234
+ <canvas ref="activityChart"></canvas>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+
242
+ <!-- Vessels Tab Content -->
243
+ <div v-if="selectedTab === 'vessels'" class="inventory-table">
244
+ <div class="vessels-grid">
245
+ <div class="vessel-card" v-for="vessel in vessels" :key="vessel.id" @click="inventoryVessel(vessel.name)">
246
+ <div class="vessel-header">
247
+ <div class="vessel-icon">
248
+ <i class="fas fa-ship"></i>
249
+ </div>
250
+ <div class="vessel-status">
251
+ {{ vessel.status || 'Active' }}
252
+ </div>
253
+ </div>
254
+ <div class="vessel-info">
255
+ <h3 class="vessel-name">{{ vessel.name }}</h3>
256
+ <p class="vessel-reg">{{ vessel.registrationNumber }}</p>
257
+ <div class="vessel-stats">
258
+ <div class="vessel-stat">
259
+ <i class="fas fa-boxes"></i>
260
+ <span>{{ getVesselItemCount(vessel.name) }} Inventory Items</span>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ <div class="vessel-actions">
265
+ <div class="vessel-btn" :class="getVesselLowStockCount(vessel.name) === 0 ? 'green-btn' : 'red-btn'"
266
+ title="Low Stock Count">
267
+ {{ getVesselLowStockCount(vessel.name) }}
268
+ <i class="fas fa-exclamation-triangle"></i>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ </div>
273
+ </div>
274
+
275
+ <!-- Import Modal -->
276
+ <div v-if="showImportModal" class="modal-overlay" @click.self="closeImportModal">
277
+ <div class="modal-container">
278
+ <button class="modal-close" @click="closeImportModal">✕</button>
279
+ <h2 class="modal-title">📦 Import Inventory Data</h2>
280
+ <div class="import-section">
281
+ <div class="sub-buttons">
282
+ <button class="sub-btn" @click="downloadTemplate">
283
+ 📥 Download Template
284
+ </button>
285
+ <button class="sub-btn" @click="triggerFileUpload">
286
+ 📤 Upload Inventory
287
+ </button>
288
+ </div>
289
+ <input type="file" ref="fileInput" class="file-input" accept=".csv" @change="handleFileUpload">
290
+ <div class="upload-area" :class="{ dragover: isDragOver }" @drop="handleDrop"
291
+ @dragover.prevent="isDragOver = true" @dragleave="isDragOver = false" @dragenter.prevent>
292
+ <div class="upload-text">
293
+ Drop your CSV file here or click "Upload Inventory"
294
+ </div>
295
+ <small style="color: #666;">Supported format: .csv</small>
296
+ </div>
297
+ </div>
298
+ <div v-if="message" class="message show" :class="messageType">
299
+ {{ message }}
300
+ </div>
301
+ <div v-if="importedData.length > 0" class="data-preview">
302
+ <h3>📊 Imported Data Preview ({{ importedData.length }} rows)</h3>
303
+ <table class="data-table">
304
+ <thead>
305
+ <tr>
306
+ <th>Part Number</th>
307
+ <th>Item Name</th>
308
+ <th>Category</th>
309
+ <th>Vessel</th>
310
+ <th>Location</th>
311
+ <th>Stock Level</th>
312
+ <th>Min Stock</th>
313
+ <th>Max Stock</th>
314
+ <th>Unit Price</th>
315
+ </tr>
316
+ </thead>
317
+ <tbody>
318
+ <tr v-for="(row, index) in importedData.slice(0, 10)" :key="index">
319
+ <td>{{ row.partNumber }}</td>
320
+ <td>{{ row.itemName }}</td>
321
+ <td>{{ row.category }}</td>
322
+ <td>{{ row.vessel }}</td>
323
+ <td>{{ row.location }}</td>
324
+ <td>{{ row.stockLevel }}</td>
325
+ <td>{{ row.minStock }}</td>
326
+ <td>{{ row.maxStock }}</td>
327
+ <td>{{ row.unitPrice }}</td>
328
+ </tr>
329
+ </tbody>
330
+ </table>
331
+ <div v-if="importedData.length > 10" style="margin-top: 10px; color: #666; font-size: 0.9rem;">
332
+ Showing first 10 rows of {{ importedData.length }} total rows
333
+ </div>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </template>
340
+
341
+ <script>
342
+ import { ref, computed, watch, nextTick, onMounted } from 'vue'
343
+
344
+ export default {
345
+ name: 'InventoryManagement',
346
+ props: {
347
+ title: {
348
+ type: String,
349
+ default: 'OceanHelm Inventory Management'
350
+ },
351
+ vessels: {
352
+ type: Array,
353
+ default: () => []
354
+ },
355
+ inventory: {
356
+ type: Array,
357
+ default: () => []
358
+ },
359
+ userRole: {
360
+ type: String,
361
+ default: 'staff'
362
+ },
363
+ userVessel: {
364
+ type: String,
365
+ default: ''
366
+ }
367
+ },
368
+ emits: [
369
+ 'toggle-sidebar',
370
+ 'add-item',
371
+ 'update-item',
372
+ 'delete-item',
373
+ 'stock-in',
374
+ 'stock-out',
375
+ 'transfer-item',
376
+ 'import-data'
377
+ ],
378
+ setup(props, { emit }) {
379
+ // Reactive data
380
+ const selectedTab = ref('vessels')
381
+ const searchTerm = ref('')
382
+ const selectedVessel = ref('')
383
+ const selectedCategory = ref('')
384
+ const showImportModal = ref(false)
385
+ const currentPage = ref(1)
386
+ const itemsPerPage = ref(10)
387
+ const message = ref('')
388
+ const messageType = ref('')
389
+ const importedData = ref([])
390
+ const isDragOver = ref(false)
391
+
392
+ // Chart instances
393
+ const categoryChart = ref(null)
394
+ const stockChart = ref(null)
395
+ const activityChart = ref(null)
396
+
397
+ // Constants
398
+ const categories = [
399
+ 'Engine Parts',
400
+ 'Safety Equipment',
401
+ 'Electronics',
402
+ 'Deck Equipment',
403
+ 'Navigation',
404
+ 'Communication',
405
+ 'Tools',
406
+ 'Consumables',
407
+ 'Other'
408
+ ]
409
+
410
+ const locations = [
411
+ 'Engine Room',
412
+ 'Bridge',
413
+ 'Main Deck',
414
+ 'Cargo Hold',
415
+ 'Workshop',
416
+ 'Storage Room',
417
+ 'Galley',
418
+ 'Other'
419
+ ]
420
+
421
+ const statusOptions = [
422
+ 'Available',
423
+ 'Low',
424
+ 'Out',
425
+ 'Reserved',
426
+ 'Maintenance',
427
+ 'Damaged'
428
+ ]
429
+
430
+ const requiredColumns = [
431
+ 'part number',
432
+ 'item name',
433
+ 'category',
434
+ 'vessel',
435
+ 'location',
436
+ 'stock level',
437
+ 'min stock',
438
+ 'max stock',
439
+ 'unit price'
440
+ ]
441
+
442
+ // Computed properties
443
+ const filteredInventory = computed(() => {
444
+ let filtered = props.inventory
445
+
446
+ // Search filter
447
+ if (searchTerm.value) {
448
+ const search = searchTerm.value.toLowerCase()
449
+ filtered = filtered.filter(item =>
450
+ item.itemName?.toLowerCase().includes(search) ||
451
+ item.category?.toLowerCase().includes(search) ||
452
+ item.vessel?.toLowerCase().includes(search) ||
453
+ item.id?.toLowerCase().includes(search)
454
+ )
455
+ }
456
+
457
+ // Vessel filter
458
+ if (selectedVessel.value) {
459
+ filtered = filtered.filter(item => item.vessel === selectedVessel.value)
460
+ }
461
+
462
+ // Category filter
463
+ if (selectedCategory.value) {
464
+ filtered = filtered.filter(item => item.category === selectedCategory.value)
465
+ }
466
+
467
+ return filtered
468
+ })
469
+
470
+ const paginatedInventory = computed(() => {
471
+ const start = (currentPage.value - 1) * itemsPerPage.value
472
+ const end = start + itemsPerPage.value
473
+ return filteredInventory.value.slice(start, end)
474
+ })
475
+
476
+ const totalPages = computed(() => {
477
+ return Math.ceil(filteredInventory.value.length / itemsPerPage.value)
478
+ })
479
+
480
+ const availableCategories = computed(() => {
481
+ return [...new Set(props.inventory.map(item => item.category).filter(Boolean))]
482
+ })
483
+
484
+ const totalItems = computed(() => {
485
+ return props.inventory.length
486
+ })
487
+
488
+ const lowStockItems = computed(() => {
489
+ return props.inventory.filter(item =>
490
+ item.currentStock <= item.minStock || item.status === 'Low Stock' || item.status === 'Out of Stock'
491
+ )
492
+ })
493
+
494
+ const lowStockCount = computed(() => {
495
+ return lowStockItems.value.length
496
+ })
497
+
498
+ const totalValue = computed(() => {
499
+ return props.inventory.reduce((sum, item) => sum + (item.value || 0), 0)
500
+ })
501
+
502
+ // Methods
503
+ const hasPermission = (roles) => {
504
+ return roles.includes(props.userRole)
505
+ }
506
+
507
+ const getStockClass = (item) => {
508
+ const percentage = (item.currentStock / item.maxStock) * 100
509
+ if (percentage <= 25) return 'stock-danger'
510
+ if (percentage <= 50) return 'stock-warning'
511
+ return 'stock-good'
512
+ }
513
+
514
+ const getStockPercentage = (item) => {
515
+ return Math.min(100, (item.currentStock / item.maxStock) * 100)
516
+ }
517
+
518
+ const getStatusClass = (status) => {
519
+ switch (status) {
520
+ case 'In Stock':
521
+ case 'Available':
522
+ return 'status-in-stock'
523
+ case 'Low':
524
+ return 'status-low-stock'
525
+ case 'Out':
526
+ return 'status-out-of-stock'
527
+ case 'Over':
528
+ return 'status-critical'
529
+ default:
530
+ return 'status-in-stock'
531
+ }
532
+ }
533
+
534
+ const formatDate = (dateString) => {
535
+ if (!dateString) return ''
536
+ const date = new Date(dateString)
537
+ return date.toLocaleDateString('en-US', {
538
+ month: 'short',
539
+ day: 'numeric',
540
+ year: 'numeric'
541
+ })
542
+ }
543
+
544
+ const getVesselItemCount = (vesselName) => {
545
+ return props.inventory.filter(item => item.vessel === vesselName).length
546
+ }
547
+
548
+ const getVesselLowStockCount = (vesselName) => {
549
+ return props.inventory.filter(item =>
550
+ item.vessel === vesselName &&
551
+ item.currentStock <= item.minStock
552
+ ).length
553
+ }
554
+
555
+ const inventoryVessel = (vesselName) => {
556
+ if (!hasPermission(['owner', 'staff']) && props.userVessel !== vesselName) {
557
+ alert('Unauthorized: You do not have access to this vessel')
558
+ return
559
+ }
560
+ selectedVessel.value = vesselName
561
+ selectedTab.value = 'inventory'
562
+ }
563
+
564
+ const addNewItem = () => {
565
+ emit('add-item')
566
+ }
567
+
568
+ const stockIn = (item) => {
569
+ emit('stock-in', item)
570
+ }
571
+
572
+ const stockOut = (item) => {
573
+ emit('stock-out', item)
574
+ }
575
+
576
+ const transferItem = (item) => {
577
+ emit('transfer-item', item)
578
+ }
579
+
580
+ const editItem = (item) => {
581
+ emit('update-item', item)
582
+ }
583
+
584
+ const deleteItem = (item) => {
585
+ emit('delete-item', item)
586
+ }
587
+
588
+ const prevPage = () => {
589
+ if (currentPage.value > 1) {
590
+ currentPage.value--
591
+ }
592
+ }
593
+
594
+ const nextPage = () => {
595
+ if (currentPage.value < totalPages.value) {
596
+ currentPage.value++
597
+ }
598
+ }
599
+
600
+ const toggleImportOptions = () => {
601
+ showImportModal.value = true
602
+ clearMessage()
603
+ importedData.value = []
604
+ }
605
+
606
+ const closeImportModal = () => {
607
+ showImportModal.value = false
608
+ message.value = ''
609
+ isDragOver.value = false
610
+ }
611
+
612
+ const showMessage = (text, type) => {
613
+ message.value = text
614
+ messageType.value = type
615
+ setTimeout(() => {
616
+ clearMessage()
617
+ }, 5000)
618
+ }
619
+
620
+ const clearMessage = () => {
621
+ message.value = ''
622
+ messageType.value = ''
623
+ }
624
+
625
+ const downloadTemplate = () => {
626
+ const headers = [
627
+ 'part number',
628
+ 'item name',
629
+ 'category',
630
+ 'vessel',
631
+ 'location',
632
+ 'stock level',
633
+ 'min stock',
634
+ 'max stock',
635
+ 'unit price'
636
+ ]
637
+
638
+ let csvContent = headers.join(',') + '\n'
639
+ // Add 10 empty rows
640
+ for (let i = 0; i < 10; i++) {
641
+ csvContent += ','.repeat(headers.length - 1) + '\n'
642
+ }
643
+ const blob = new Blob([csvContent], { type: 'text/csv' })
644
+ const url = URL.createObjectURL(blob)
645
+
646
+ const link = document.createElement('a')
647
+ link.href = url
648
+ link.download = 'inventory_import_template.csv'
649
+ document.body.appendChild(link)
650
+ link.click()
651
+ document.body.removeChild(link)
652
+ URL.revokeObjectURL(url)
653
+
654
+ showMessage('Template downloaded successfully!', 'success')
655
+ }
656
+
657
+ const triggerFileUpload = () => {
658
+ const fileInput = document.querySelector('input[type="file"]')
659
+ fileInput?.click()
660
+ }
661
+
662
+ const handleFileUpload = (event) => {
663
+ const file = event.target.files[0]
664
+ if (file) {
665
+ processFile(file)
666
+ }
667
+ }
668
+
669
+ const handleDrop = (event) => {
670
+ event.preventDefault()
671
+ isDragOver.value = false
672
+
673
+ const files = event.dataTransfer.files
674
+ if (files.length > 0) {
675
+ const file = files[0]
676
+ if (file.type === 'text/csv' || file.name.endsWith('.csv')) {
677
+ processFile(file)
678
+ } else {
679
+ showMessage('Please upload a CSV file only.', 'error')
680
+ }
681
+ }
682
+ }
683
+
684
+ const processFile = (file) => {
685
+ if (!file.name.endsWith('.csv')) {
686
+ showMessage('Please upload a CSV file only.', 'error')
687
+ return
688
+ }
689
+
690
+ // This would normally use Papa Parse, but for now we'll emit the event
691
+ emit('import-data', file)
692
+ showMessage('File processing started...', 'info')
693
+ }
694
+
695
+ const generatePDF = () => {
696
+ // Emit event to parent to handle PDF generation
697
+ emit('export-pdf', filteredInventory.value)
698
+ }
699
+
700
+ return {
701
+ // Reactive refs
702
+ selectedTab,
703
+ searchTerm,
704
+ selectedVessel,
705
+ selectedCategory,
706
+ showImportModal,
707
+ currentPage,
708
+ itemsPerPage,
709
+ message,
710
+ messageType,
711
+ importedData,
712
+ isDragOver,
713
+ categoryChart,
714
+ stockChart,
715
+ activityChart,
716
+
717
+ // Constants
718
+ categories,
719
+ locations,
720
+ statusOptions,
721
+ requiredColumns,
722
+
723
+ // Computed
724
+ filteredInventory,
725
+ paginatedInventory,
726
+ totalPages,
727
+ availableCategories,
728
+ totalItems,
729
+ lowStockItems,
730
+ lowStockCount,
731
+ totalValue,
732
+
733
+ // Methods
734
+ hasPermission,
735
+ getStockClass,
736
+ getStockPercentage,
737
+ getStatusClass,
738
+ formatDate,
739
+ getVesselItemCount,
740
+ getVesselLowStockCount,
741
+ inventoryVessel,
742
+ addNewItem,
743
+ stockIn,
744
+ stockOut,
745
+ transferItem,
746
+ editItem,
747
+ deleteItem,
748
+ prevPage,
749
+ nextPage,
750
+ toggleImportOptions,
751
+ closeImportModal,
752
+ showMessage,
753
+ clearMessage,
754
+ downloadTemplate,
755
+ triggerFileUpload,
756
+ handleFileUpload,
757
+ handleDrop,
758
+ processFile,
759
+ generatePDF
760
+ }
761
+ }
762
+ }
763
+ </script>
764
+
765
+ <style>
766
+ .inventory-container {
767
+ max-width: 1400px;
768
+ margin: 0 auto;
769
+ padding: 20px;
770
+ }
771
+
772
+ .header {
773
+ background: linear-gradient(135deg, var(--dashprimary-color), #0f172a);
774
+ color: white;
775
+ padding: 20px;
776
+ border-radius: 12px;
777
+ margin-bottom: 30px;
778
+ display: flex;
779
+ justify-content: between;
780
+ align-items: center;
781
+ }
782
+
783
+ .header h1 {
784
+ font-size: 28px;
785
+ font-weight: 600;
786
+ display: flex;
787
+ align-items: center;
788
+ gap: 12px;
789
+ }
790
+
791
+ .header-stats {
792
+ display: flex;
793
+ gap: 20px;
794
+ margin-top: 10px;
795
+ }
796
+
797
+ .stat-card {
798
+ background: rgba(255, 255, 255, 0.1);
799
+ padding: 10px 15px;
800
+ border-radius: 8px;
801
+ text-align: center;
802
+ backdrop-filter: blur(10px);
803
+ }
804
+
805
+ .stat-card .stat-value {
806
+ font-size: 20px;
807
+ font-weight: bold;
808
+ }
809
+
810
+ .stat-card .stat-label {
811
+ font-size: 12px;
812
+ opacity: 0.9;
813
+ }
814
+
815
+ .controls {
816
+ background: white;
817
+ padding: 20px;
818
+ border-radius: 12px;
819
+ margin-bottom: 20px;
820
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
821
+ }
822
+
823
+ .controls-row {
824
+ display: flex;
825
+ gap: 15px;
826
+ align-items: center;
827
+ flex-wrap: wrap;
828
+ }
829
+
830
+ .search-box {
831
+ position: relative;
832
+ flex: 1;
833
+ min-width: 250px;
834
+ }
835
+
836
+ .search-box input {
837
+ width: 100%;
838
+ padding: 10px 40px 10px 15px;
839
+ border: 1px solid #e5e7eb;
840
+ border-radius: 8px;
841
+ font-size: 14px;
842
+ }
843
+
844
+ .search-box i {
845
+ position: absolute;
846
+ right: 12px;
847
+ top: 50%;
848
+ transform: translateY(-50%);
849
+ color: #6b7280;
850
+ }
851
+
852
+ .filter-select {
853
+ padding: 10px 15px;
854
+ border: 1px solid #e5e7eb;
855
+ border-radius: 8px;
856
+ background: white;
857
+ font-size: 14px;
858
+ min-width: 150px;
859
+ }
860
+
861
+ .btn {
862
+ padding: 10px 20px;
863
+ border: none;
864
+ border-radius: 8px;
865
+ cursor: pointer;
866
+ font-size: 14px;
867
+ font-weight: 500;
868
+ display: inline-flex;
869
+ align-items: center;
870
+ gap: 8px;
871
+ transition: all 0.3s ease;
872
+ }
873
+
874
+ .btn-primary {
875
+ background: var(--dashprimary-color);
876
+ color: white;
877
+ }
878
+
879
+ .btn-primary:hover {
880
+ background: #1d4ed8;
881
+ transform: translateY(-2px);
882
+ }
883
+
884
+ .btn-secondary {
885
+ background: #6b7280;
886
+ color: white;
887
+ }
888
+
889
+ .btn-secondary:hover {
890
+ background: #4b5563;
891
+ }
892
+
893
+ .tabs {
894
+ background: white;
895
+ border-radius: 12px;
896
+ overflow: hidden;
897
+ margin-bottom: 20px;
898
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
899
+ }
900
+
901
+ .tab {
902
+ flex: 1;
903
+ padding: 15px 20px;
904
+ text-align: center;
905
+ cursor: pointer;
906
+ transition: all 0.3s ease;
907
+ border: none;
908
+ background: transparent;
909
+ font-size: 14px;
910
+ font-weight: 500;
911
+ }
912
+
913
+ .tab.active {
914
+ background: var(--dashprimary-color);
915
+ color: white;
916
+ }
917
+
918
+ .tab:hover:not(.active) {
919
+ background: #f3f4f6;
920
+ }
921
+
922
+ .inventory-table {
923
+ background: white;
924
+ border-radius: 12px;
925
+ overflow: hidden;
926
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
927
+ }
928
+
929
+ .table {
930
+ width: 100%;
931
+ border-collapse: collapse;
932
+ }
933
+
934
+ .table th,
935
+ .table td {
936
+ padding: 12px 15px;
937
+ text-align: left;
938
+ border-bottom: 1px solid #e5e7eb;
939
+ }
940
+
941
+ .table th {
942
+ background: #f9fafb;
943
+ font-weight: 600;
944
+ color: #374151;
945
+ }
946
+
947
+ .table tr:hover {
948
+ background: #f9fafb;
949
+ }
950
+
951
+ .status-badge {
952
+ padding: 4px 12px;
953
+ border-radius: 20px;
954
+ font-size: 12px;
955
+ font-weight: 500;
956
+ }
957
+
958
+ .status-in-stock {
959
+ background: #d1fae5;
960
+ color: #065f46;
961
+ }
962
+
963
+ .status-low-stock {
964
+ background: #fed7aa;
965
+ color: #9a3412;
966
+ }
967
+
968
+ .status-out-of-stock {
969
+ background: #fecaca;
970
+ color: #991b1b;
971
+ }
972
+
973
+ .status-critical {
974
+ background: #ddd6fe;
975
+ color: #5b21b6;
976
+ }
977
+
978
+ .stock-level {
979
+ display: flex;
980
+ align-items: center;
981
+ gap: 10px;
982
+ }
983
+
984
+ .stock-bar {
985
+ width: 80px;
986
+ height: 6px;
987
+ background: #e5e7eb;
988
+ border-radius: 3px;
989
+ overflow: hidden;
990
+ }
991
+
992
+ .stock-fill {
993
+ height: 100%;
994
+ transition: width 0.3s ease;
995
+ }
996
+
997
+ .stock-good {
998
+ background: #10b981;
999
+ }
1000
+
1001
+ .stock-warning {
1002
+ background: #f59e0b;
1003
+ }
1004
+
1005
+ .stock-danger {
1006
+ background: #ef4444;
1007
+ }
1008
+
1009
+ .action-btns {
1010
+ display: flex;
1011
+ gap: 8px;
1012
+ }
1013
+
1014
+ .action-btn {
1015
+ padding: 6px 10px;
1016
+ border: none;
1017
+ border-radius: 6px;
1018
+ cursor: pointer;
1019
+ font-size: 12px;
1020
+ transition: all 0.3s ease;
1021
+ }
1022
+
1023
+ .action-btn:hover {
1024
+ transform: translateY(-1px);
1025
+ }
1026
+
1027
+ .btn-edit {
1028
+ background: #3b82f6;
1029
+ color: white;
1030
+ }
1031
+
1032
+ .btn-view {
1033
+ background: #6b7280;
1034
+ color: white;
1035
+ }
1036
+
1037
+ .btn-delete {
1038
+ background: #ef4444;
1039
+ color: white;
1040
+ }
1041
+
1042
+ .empty-state {
1043
+ text-align: center;
1044
+ padding: 60px 20px;
1045
+ color: #6b7280;
1046
+ }
1047
+
1048
+ .empty-state i {
1049
+ font-size: 48px;
1050
+ margin-bottom: 20px;
1051
+ opacity: 0.5;
1052
+ }
1053
+
1054
+ .pagination {
1055
+ display: flex;
1056
+ justify-content: center;
1057
+ align-items: center;
1058
+ gap: 10px;
1059
+ margin-top: 20px;
1060
+ }
1061
+
1062
+ .pagination button {
1063
+ padding: 8px 12px;
1064
+ border: 1px solid #e5e7eb;
1065
+ background: white;
1066
+ border-radius: 6px;
1067
+ cursor: pointer;
1068
+ font-size: 14px;
1069
+ }
1070
+
1071
+ .pagination button:hover {
1072
+ background: #f3f4f6;
1073
+ }
1074
+
1075
+ .pagination button.active {
1076
+ background: var(--dashprimary-color);
1077
+ color: white;
1078
+ border-color: var(--dashprimary-color);
1079
+ }
1080
+
1081
+ .low-stock-alerts {
1082
+ background: #fef2f2;
1083
+ border: 1px solid #fecaca;
1084
+ border-radius: 8px;
1085
+ padding: 15px;
1086
+ margin-bottom: 20px;
1087
+ }
1088
+
1089
+ .alert-header {
1090
+ display: flex;
1091
+ align-items: center;
1092
+ gap: 10px;
1093
+ margin-bottom: 10px;
1094
+ color: #991b1b;
1095
+ font-weight: 600;
1096
+ }
1097
+
1098
+ .alert-list {
1099
+ list-style: none;
1100
+ color: #7f1d1d;
1101
+ }
1102
+
1103
+ .alert-list li {
1104
+ padding: 5px 0;
1105
+ font-size: 14px;
1106
+ }
1107
+
1108
+ @media (max-width: 768px) {
1109
+ .controls-row {
1110
+ flex-direction: column;
1111
+ align-items: stretch;
1112
+ }
1113
+
1114
+ .search-box {
1115
+ min-width: auto;
1116
+ }
1117
+
1118
+ .header-stats {
1119
+ justify-content: center;
1120
+ }
1121
+
1122
+ .table-responsive {
1123
+ overflow-x: auto;
1124
+ }
1125
+ }
1126
+
1127
+ #content.active {
1128
+ margin-left: var(--sidebar-width);
1129
+ width: calc(100% - var(--sidebar-width));
1130
+ }
1131
+
1132
+ .item-row {
1133
+ display: flex;
1134
+ justify-content: space-between;
1135
+ align-items: center;
1136
+ padding: 8px;
1137
+ }
1138
+
1139
+ .inactive-item {
1140
+ color: grey;
1141
+ opacity: 0.6;
1142
+ }
1143
+
1144
+ /* Vessel Cards */
1145
+ .left {
1146
+ margin-left: 20px;
1147
+ }
1148
+
1149
+ .vessel-card {
1150
+ background: white;
1151
+ border-radius: 10px;
1152
+ box-shadow: 0 8px 16px rgba(0, 105, 192, 0.15);
1153
+ transition: all 0.3s ease;
1154
+ margin-bottom: 20px;
1155
+ overflow: hidden;
1156
+ border-left: 4px solid var(--accent-color);
1157
+ }
1158
+
1159
+ .vessel-card:hover {
1160
+ transform: translateY(-5px);
1161
+ box-shadow: 0 12px 20px rgba(0, 105, 192, 0.2);
1162
+ }
1163
+
1164
+ .vessel-icon {
1165
+ display: inline-flex;
1166
+ align-items: center;
1167
+ justify-content: center;
1168
+ width: 50px;
1169
+ height: 50px;
1170
+ background-color: #e3f2fd;
1171
+ border-radius: 10px;
1172
+ color: var(--accent-color);
1173
+ font-size: 24px;
1174
+ margin-right: 15px;
1175
+ }
1176
+
1177
+ /* Header Actions */
1178
+ .header {
1179
+ display: flex;
1180
+ justify-content: space-between;
1181
+ align-items: flex-start;
1182
+ flex-wrap: wrap;
1183
+ gap: 20px;
1184
+ }
1185
+
1186
+ .header-actions {
1187
+ display: flex;
1188
+ gap: 10px;
1189
+ align-items: center;
1190
+ }
1191
+
1192
+ .header-actions .btn {
1193
+ white-space: nowrap;
1194
+ }
1195
+
1196
+ /* Vessels Grid */
1197
+ .vessels-grid {
1198
+ display: grid;
1199
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
1200
+ gap: 20px;
1201
+ padding: 20px;
1202
+ }
1203
+
1204
+ .vessel-card {
1205
+ background: white;
1206
+ border-radius: 12px;
1207
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
1208
+ transition: all 0.3s ease;
1209
+ cursor: pointer;
1210
+ overflow: hidden;
1211
+ border-left: 4px solid var(--dashprimary-color);
1212
+ position: relative;
1213
+ }
1214
+
1215
+ .vessel-card:hover {
1216
+ transform: translateY(-5px);
1217
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
1218
+ }
1219
+
1220
+ .vessel-header {
1221
+ display: flex;
1222
+ justify-content: space-between;
1223
+ align-items: center;
1224
+ padding: 15px 20px 10px;
1225
+ background: linear-gradient(135deg, rgba(37, 99, 235, 0.1), rgba(15, 23, 42, 0.1));
1226
+ }
1227
+
1228
+ .vessel-icon {
1229
+ display: flex;
1230
+ align-items: center;
1231
+ justify-content: center;
1232
+ width: 45px;
1233
+ height: 45px;
1234
+ background: linear-gradient(135deg, var(--dashprimary-color), #1e40af);
1235
+ border-radius: 10px;
1236
+ color: white;
1237
+ font-size: 20px;
1238
+ }
1239
+
1240
+ .vessel-status {
1241
+ padding: 4px 12px;
1242
+ border-radius: 20px;
1243
+ font-size: 12px;
1244
+ font-weight: 500;
1245
+ text-transform: uppercase;
1246
+ letter-spacing: 0.5px;
1247
+ }
1248
+
1249
+ .vessel-status.active {
1250
+ background: #d1fae5;
1251
+ color: #065f46;
1252
+ }
1253
+
1254
+ .vessel-status.maintenance {
1255
+ background: #fed7aa;
1256
+ color: #9a3412;
1257
+ }
1258
+
1259
+ .vessel-status.inactive {
1260
+ background: #fecaca;
1261
+ color: #991b1b;
1262
+ }
1263
+
1264
+ .vessel-info {
1265
+ padding: 15px 20px;
1266
+ }
1267
+
1268
+ .vessel-name {
1269
+ font-size: 20px;
1270
+ font-weight: 600;
1271
+ color: #1f2937;
1272
+ margin-bottom: 5px;
1273
+ }
1274
+
1275
+ .vessel-reg {
1276
+ color: #6b7280;
1277
+ font-size: 14px;
1278
+ margin-bottom: 15px;
1279
+ }
1280
+
1281
+ .vessel-stats {
1282
+ display: flex;
1283
+ gap: 20px;
1284
+ }
1285
+
1286
+ .vessel-stat {
1287
+ display: flex;
1288
+ align-items: center;
1289
+ gap: 8px;
1290
+ font-size: 14px;
1291
+ color: #4b5563;
1292
+ }
1293
+
1294
+ .vessel-stat i {
1295
+ color: var(--dashprimary-color);
1296
+ font-size: 16px;
1297
+ }
1298
+
1299
+ .vessel-actions {
1300
+ display: flex;
1301
+ justify-content: flex-end;
1302
+ gap: 10px;
1303
+ padding: 15px 20px;
1304
+ background: #f9fafb;
1305
+ border-top: 1px solid #e5e7eb;
1306
+ }
1307
+
1308
+ .vessel-btn {
1309
+ padding: 8px 12px;
1310
+ border: none;
1311
+ border-radius: 6px;
1312
+ cursor: pointer;
1313
+ font-size: 14px;
1314
+ transition: all 0.3s ease;
1315
+ color: white;
1316
+ }
1317
+
1318
+ .red-btn {
1319
+ background: red;
1320
+ }
1321
+
1322
+ .green-btn {
1323
+ background: var(--dashprimary-color);
1324
+ }
1325
+
1326
+ /* Responsive Design */
1327
+ @media (max-width: 768px) {
1328
+ .vessels-grid {
1329
+ grid-template-columns: 1fr;
1330
+ padding: 15px;
1331
+ }
1332
+
1333
+ .header {
1334
+ flex-direction: column;
1335
+ align-items: stretch;
1336
+ }
1337
+
1338
+ .header-actions {
1339
+ justify-content: center;
1340
+ }
1341
+
1342
+ .vessel-stats {
1343
+ flex-direction: column;
1344
+ gap: 10px;
1345
+ }
1346
+ }
1347
+
1348
+
1349
+
1350
+
1351
+
1352
+ /* Modal Overlay */
1353
+ .modal-overlay {
1354
+ position: fixed;
1355
+ top: 0;
1356
+ left: 0;
1357
+ width: 100%;
1358
+ height: 100%;
1359
+ background: rgba(0, 0, 0, 0.5);
1360
+ backdrop-filter: blur(8px);
1361
+ -webkit-backdrop-filter: blur(8px);
1362
+ display: flex;
1363
+ align-items: center;
1364
+ justify-content: center;
1365
+ z-index: 1000;
1366
+ padding: 20px;
1367
+ box-sizing: border-box;
1368
+ }
1369
+
1370
+ /* Modal Container */
1371
+ .modal-container {
1372
+ background: rgba(255, 255, 255, 0.95);
1373
+ backdrop-filter: blur(20px);
1374
+ -webkit-backdrop-filter: blur(20px);
1375
+ border-radius: 20px;
1376
+ padding: 40px;
1377
+ max-width: 800px;
1378
+ width: 100%;
1379
+ max-height: 90vh;
1380
+ overflow-y: auto;
1381
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
1382
+ border: 1px solid rgba(255, 255, 255, 0.2);
1383
+ position: relative;
1384
+ animation: modalSlideIn 0.3s ease-out;
1385
+ }
1386
+
1387
+ @keyframes modalSlideIn {
1388
+ from {
1389
+ opacity: 0;
1390
+ transform: translateY(-30px) scale(0.95);
1391
+ }
1392
+
1393
+ to {
1394
+ opacity: 1;
1395
+ transform: translateY(0) scale(1);
1396
+ }
1397
+ }
1398
+
1399
+ /* Close Button */
1400
+ .modal-close {
1401
+ position: absolute;
1402
+ top: 20px;
1403
+ right: 20px;
1404
+ background: rgba(239, 68, 68, 0.1);
1405
+ color: #dc2626;
1406
+ border: none;
1407
+ width: 40px;
1408
+ height: 40px;
1409
+ border-radius: 50%;
1410
+ font-size: 1.2rem;
1411
+ cursor: pointer;
1412
+ transition: all 0.3s ease;
1413
+ display: flex;
1414
+ align-items: center;
1415
+ justify-content: center;
1416
+ }
1417
+
1418
+ .modal-close:hover {
1419
+ background: #dc2626;
1420
+ color: white;
1421
+ transform: rotate(90deg);
1422
+ }
1423
+
1424
+ /* Import Section */
1425
+ .import-section {
1426
+ text-align: center;
1427
+ }
1428
+
1429
+ /* Modal Title */
1430
+ .modal-title {
1431
+ color: #333;
1432
+ font-size: 1.8rem;
1433
+ font-weight: 700;
1434
+ margin-bottom: 30px;
1435
+ text-align: center;
1436
+ background: linear-gradient(135deg, var(--dashsecondary-color), var(--dashprimary-color));
1437
+ -webkit-background-clip: text;
1438
+ -webkit-text-fill-color: transparent;
1439
+ background-clip: text;
1440
+ }
1441
+
1442
+ .sub-buttons {
1443
+ margin-top: 20px;
1444
+ display: flex;
1445
+ gap: 15px;
1446
+ justify-content: center;
1447
+ flex-wrap: wrap;
1448
+ }
1449
+
1450
+ .sub-btn {
1451
+ background: rgba(102, 126, 234, 0.1);
1452
+ color: var(--dashprimary-color);
1453
+ border: 2px solid var(--dashprimary-color);
1454
+ padding: 12px 25px;
1455
+ border-radius: 25px;
1456
+ font-size: 0.95rem;
1457
+ font-weight: 500;
1458
+ cursor: pointer;
1459
+ transition: all 0.3s ease;
1460
+ backdrop-filter: blur(5px);
1461
+ }
1462
+
1463
+ .sub-btn:hover {
1464
+ background: var(--dashprimary-color);
1465
+ color: white;
1466
+ transform: translateY(-2px);
1467
+ box-shadow: 0 8px 15px rgba(102, 126, 234, 0.3);
1468
+ }
1469
+
1470
+ .file-input {
1471
+ display: none;
1472
+ }
1473
+
1474
+ .upload-area {
1475
+ margin-top: 20px;
1476
+ padding: 30px;
1477
+ border: 2px dashed var(--dashprimary-color);
1478
+ border-radius: 15px;
1479
+ background: rgba(102, 126, 234, 0.05);
1480
+ text-align: center;
1481
+ transition: all 0.3s ease;
1482
+ }
1483
+
1484
+ .upload-area:hover {
1485
+ background: rgba(102, 126, 234, 0.1);
1486
+ border-color: #764ba2;
1487
+ }
1488
+
1489
+ .upload-area.dragover {
1490
+ background: rgba(102, 126, 234, 0.15);
1491
+ border-color: #764ba2;
1492
+ transform: scale(1.02);
1493
+ }
1494
+
1495
+ .upload-text {
1496
+ color: var(--dashprimary-color);
1497
+ font-weight: 500;
1498
+ margin-bottom: 10px;
1499
+ }
1500
+
1501
+ .message {
1502
+ margin-top: 20px;
1503
+ padding: 15px;
1504
+ border-radius: 10px;
1505
+ font-weight: 500;
1506
+ text-align: center;
1507
+ opacity: 0;
1508
+ transform: translateY(-10px);
1509
+ transition: all 0.3s ease;
1510
+ }
1511
+
1512
+ .message.show {
1513
+ opacity: 1;
1514
+ transform: translateY(0);
1515
+ }
1516
+
1517
+ .success {
1518
+ background: rgba(34, 197, 94, 0.1);
1519
+ color: #16a34a;
1520
+ border: 1px solid rgba(34, 197, 94, 0.3);
1521
+ }
1522
+
1523
+ .error {
1524
+ background: rgba(239, 68, 68, 0.1);
1525
+ color: #dc2626;
1526
+ border: 1px solid rgba(239, 68, 68, 0.3);
1527
+ }
1528
+
1529
+ .data-preview {
1530
+ margin-top: 20px;
1531
+ background: rgba(255, 255, 255, 0.8);
1532
+ border-radius: 10px;
1533
+ padding: 20px;
1534
+ max-height: 300px;
1535
+ overflow-y: auto;
1536
+ border: 1px solid rgba(102, 126, 234, 0.2);
1537
+ }
1538
+
1539
+ .data-preview h3 {
1540
+ color: #333;
1541
+ margin-bottom: 15px;
1542
+ font-size: 1.2rem;
1543
+ }
1544
+
1545
+ .data-table {
1546
+ width: 100%;
1547
+ border-collapse: collapse;
1548
+ font-size: 0.9rem;
1549
+ }
1550
+
1551
+ .data-table th,
1552
+ .data-table td {
1553
+ padding: 8px 12px;
1554
+ text-align: left;
1555
+ border-bottom: 1px solid rgba(102, 126, 234, 0.2);
1556
+ }
1557
+
1558
+ .data-table th {
1559
+ background: rgba(102, 126, 234, 0.1);
1560
+ font-weight: 600;
1561
+ color: #333;
1562
+ }
1563
+
1564
+ .fade-enter-active,
1565
+ .fade-leave-active {
1566
+ transition: all 0.3s ease;
1567
+ }
1568
+
1569
+ .fade-enter-from,
1570
+ .fade-leave-to {
1571
+ opacity: 0;
1572
+ transform: translateY(-10px);
1573
+ }
1574
+
1575
+ /* Modal Transitions */
1576
+ .modal-enter-active,
1577
+ .modal-leave-active {
1578
+ transition: all 0.3s ease;
1579
+ }
1580
+
1581
+ .modal-enter-from,
1582
+ .modal-leave-to {
1583
+ opacity: 0;
1584
+ }
1585
+
1586
+ .modal-enter-from .modal-container,
1587
+ .modal-leave-to .modal-container {
1588
+ transform: translateY(-30px) scale(0.95);
1589
+ }
1590
+
1591
+ @media (max-width: 768px) {
1592
+ .modal-container {
1593
+ margin: 10px;
1594
+ padding: 30px 20px;
1595
+ max-height: 95vh;
1596
+ }
1597
+
1598
+ .modal-title {
1599
+ font-size: 1.5rem;
1600
+ }
1601
+
1602
+ .sub-buttons {
1603
+ flex-direction: column;
1604
+ align-items: center;
1605
+ }
1606
+
1607
+ .modal-close {
1608
+ top: 15px;
1609
+ right: 15px;
1610
+ width: 35px;
1611
+ height: 35px;
1612
+ font-size: 1rem;
1613
+ }
1614
+ }
1615
+ </style>