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,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>
|