@codingfactory/inventory-locator-client 0.1.0
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/package.json +47 -0
- package/src/api/client.ts +31 -0
- package/src/components/counting/CountEntryForm.vue +833 -0
- package/src/components/counting/CountReport.vue +385 -0
- package/src/components/counting/CountSessionDetail.vue +650 -0
- package/src/components/counting/CountSessionList.vue +683 -0
- package/src/components/dashboard/InventoryDashboard.vue +670 -0
- package/src/components/dashboard/UnlocatedProducts.vue +468 -0
- package/src/components/labels/LabelBatchPrint.vue +528 -0
- package/src/components/labels/LabelPreview.vue +293 -0
- package/src/components/locations/InventoryLocatorShell.vue +408 -0
- package/src/components/locations/LocationBreadcrumb.vue +144 -0
- package/src/components/locations/LocationCodeBadge.vue +46 -0
- package/src/components/locations/LocationDetail.vue +884 -0
- package/src/components/locations/LocationForm.vue +360 -0
- package/src/components/locations/LocationSearchInput.vue +428 -0
- package/src/components/locations/LocationTree.vue +156 -0
- package/src/components/locations/LocationTreeNode.vue +280 -0
- package/src/components/locations/LocationTypeIcon.vue +58 -0
- package/src/components/products/LocationProductAdd.vue +637 -0
- package/src/components/products/LocationProductList.vue +547 -0
- package/src/components/products/ProductLocationList.vue +215 -0
- package/src/components/products/QuickMoveModal.vue +592 -0
- package/src/components/scanning/ScanHistory.vue +146 -0
- package/src/components/scanning/ScanResult.vue +350 -0
- package/src/components/scanning/ScannerOverlay.vue +696 -0
- package/src/components/shared/InvBadge.vue +71 -0
- package/src/components/shared/InvButton.vue +206 -0
- package/src/components/shared/InvCard.vue +254 -0
- package/src/components/shared/InvEmptyState.vue +132 -0
- package/src/components/shared/InvInput.vue +125 -0
- package/src/components/shared/InvModal.vue +296 -0
- package/src/components/shared/InvSelect.vue +155 -0
- package/src/components/shared/InvTable.vue +288 -0
- package/src/composables/useCountSessions.ts +184 -0
- package/src/composables/useLabelPrinting.ts +71 -0
- package/src/composables/useLocationBreadcrumbs.ts +19 -0
- package/src/composables/useLocationProducts.ts +125 -0
- package/src/composables/useLocationSearch.ts +46 -0
- package/src/composables/useLocations.ts +159 -0
- package/src/composables/useMovements.ts +71 -0
- package/src/composables/useScanner.ts +83 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +46 -0
- package/src/plugin.ts +14 -0
- package/src/stores/countStore.ts +95 -0
- package/src/stores/locationStore.ts +113 -0
- package/src/stores/scannerStore.ts +51 -0
- package/src/types/index.ts +216 -0
- package/src/utils/codeFormatter.ts +29 -0
- package/src/utils/locationIcons.ts +64 -0
- package/tsconfig.json +21 -0
- package/vite.config.ts +37 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="count-detail">
|
|
3
|
+
<!-- Loading -->
|
|
4
|
+
<InvCard v-if="loading && !session" loading />
|
|
5
|
+
|
|
6
|
+
<!-- Error -->
|
|
7
|
+
<InvEmptyState
|
|
8
|
+
v-else-if="error && !session"
|
|
9
|
+
title="Could not load session"
|
|
10
|
+
:description="error"
|
|
11
|
+
action-label="Retry"
|
|
12
|
+
@action="loadSession"
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<template v-if="session">
|
|
16
|
+
<!-- Header -->
|
|
17
|
+
<div class="count-detail__header">
|
|
18
|
+
<div class="count-detail__header-info">
|
|
19
|
+
<h1 class="count-detail__title">{{ session.name }}</h1>
|
|
20
|
+
<div class="count-detail__meta">
|
|
21
|
+
<InvBadge :variant="statusVariant">{{ statusLabel }}</InvBadge>
|
|
22
|
+
<span v-if="session.started_at" class="count-detail__meta-item">
|
|
23
|
+
Started: {{ formatDate(session.started_at) }}
|
|
24
|
+
</span>
|
|
25
|
+
<span v-if="session.scope_location_id" class="count-detail__meta-item">
|
|
26
|
+
Scope: Location #{{ session.scope_location_id }}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Progress card -->
|
|
33
|
+
<InvCard title="Progress" class="count-detail__progress-card">
|
|
34
|
+
<div
|
|
35
|
+
class="count-detail__progress-bar"
|
|
36
|
+
role="progressbar"
|
|
37
|
+
:aria-valuenow="session.progress.percent_complete"
|
|
38
|
+
aria-valuemin="0"
|
|
39
|
+
aria-valuemax="100"
|
|
40
|
+
:aria-label="`${session.progress.percent_complete}% complete`"
|
|
41
|
+
>
|
|
42
|
+
<div class="count-detail__progress-track">
|
|
43
|
+
<div
|
|
44
|
+
class="count-detail__progress-fill"
|
|
45
|
+
:class="progressClass"
|
|
46
|
+
:style="{ width: `${session.progress.percent_complete}%` }"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
<span class="count-detail__progress-pct">
|
|
50
|
+
{{ session.progress.percent_complete }}%
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="count-detail__stats-grid">
|
|
55
|
+
<div class="count-detail__stat">
|
|
56
|
+
<span class="count-detail__stat-value">{{ session.progress.total }}</span>
|
|
57
|
+
<span class="count-detail__stat-label">Total</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="count-detail__stat">
|
|
60
|
+
<span class="count-detail__stat-value">{{ session.progress.counted }}</span>
|
|
61
|
+
<span class="count-detail__stat-label">Counted</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="count-detail__stat">
|
|
64
|
+
<span class="count-detail__stat-value">{{ session.progress.verified }}</span>
|
|
65
|
+
<span class="count-detail__stat-label">Verified</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="count-detail__stat">
|
|
68
|
+
<span class="count-detail__stat-value count-detail__stat-value--pending">
|
|
69
|
+
{{ session.progress.total - session.progress.counted }}
|
|
70
|
+
</span>
|
|
71
|
+
<span class="count-detail__stat-label">Pending</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="count-detail__stat">
|
|
74
|
+
<span class="count-detail__stat-value count-detail__stat-value--warning">
|
|
75
|
+
{{ session.progress.discrepancies }}
|
|
76
|
+
</span>
|
|
77
|
+
<span class="count-detail__stat-label">Discrepancies</span>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</InvCard>
|
|
81
|
+
|
|
82
|
+
<!-- Action buttons -->
|
|
83
|
+
<InvCard title="Actions" class="count-detail__actions-card">
|
|
84
|
+
<div class="count-detail__action-grid">
|
|
85
|
+
<InvButton
|
|
86
|
+
v-if="canGenerate"
|
|
87
|
+
variant="secondary"
|
|
88
|
+
size="md"
|
|
89
|
+
:loading="generating"
|
|
90
|
+
@click="handleGenerate"
|
|
91
|
+
>
|
|
92
|
+
Generate Entries
|
|
93
|
+
</InvButton>
|
|
94
|
+
<InvButton
|
|
95
|
+
v-if="canStartCounting"
|
|
96
|
+
variant="primary"
|
|
97
|
+
size="md"
|
|
98
|
+
@click="handleStartCounting"
|
|
99
|
+
>
|
|
100
|
+
Start Counting
|
|
101
|
+
</InvButton>
|
|
102
|
+
<InvButton
|
|
103
|
+
v-if="hasCounted"
|
|
104
|
+
variant="secondary"
|
|
105
|
+
size="md"
|
|
106
|
+
@click="toggleDiscrepancyFilter"
|
|
107
|
+
>
|
|
108
|
+
{{ showDiscrepanciesOnly ? 'Show All' : 'Review Discrepancies' }}
|
|
109
|
+
</InvButton>
|
|
110
|
+
<InvButton
|
|
111
|
+
v-if="canApply"
|
|
112
|
+
variant="primary"
|
|
113
|
+
size="md"
|
|
114
|
+
:loading="applying"
|
|
115
|
+
@click="handleApply"
|
|
116
|
+
>
|
|
117
|
+
Apply Adjustments
|
|
118
|
+
</InvButton>
|
|
119
|
+
<InvButton
|
|
120
|
+
v-if="session.status === 'completed'"
|
|
121
|
+
variant="secondary"
|
|
122
|
+
size="md"
|
|
123
|
+
@click="handleViewReport"
|
|
124
|
+
>
|
|
125
|
+
View Report
|
|
126
|
+
</InvButton>
|
|
127
|
+
<InvButton
|
|
128
|
+
v-if="session.status !== 'completed' && session.status !== 'cancelled'"
|
|
129
|
+
variant="danger"
|
|
130
|
+
size="md"
|
|
131
|
+
@click="handleCancel"
|
|
132
|
+
>
|
|
133
|
+
Cancel
|
|
134
|
+
</InvButton>
|
|
135
|
+
</div>
|
|
136
|
+
</InvCard>
|
|
137
|
+
|
|
138
|
+
<!-- Entry table -->
|
|
139
|
+
<InvCard title="Entries" class="count-detail__entries-card" no-padding>
|
|
140
|
+
<!-- Filters -->
|
|
141
|
+
<template #actions>
|
|
142
|
+
<div class="count-detail__filters">
|
|
143
|
+
<InvSelect
|
|
144
|
+
v-model="statusFilter"
|
|
145
|
+
:options="statusOptions"
|
|
146
|
+
placeholder="All Statuses"
|
|
147
|
+
/>
|
|
148
|
+
<label class="count-detail__discrepancy-toggle">
|
|
149
|
+
<input
|
|
150
|
+
v-model="showDiscrepanciesOnly"
|
|
151
|
+
type="checkbox"
|
|
152
|
+
class="count-detail__discrepancy-checkbox"
|
|
153
|
+
/>
|
|
154
|
+
<span>Discrepancies only</span>
|
|
155
|
+
</label>
|
|
156
|
+
</div>
|
|
157
|
+
</template>
|
|
158
|
+
|
|
159
|
+
<InvTable
|
|
160
|
+
:columns="entryColumns"
|
|
161
|
+
:data="filteredEntries"
|
|
162
|
+
:loading="entriesLoading"
|
|
163
|
+
clickable
|
|
164
|
+
empty-message="No entries. Generate entries to start counting."
|
|
165
|
+
@row-click="handleEntryClick"
|
|
166
|
+
>
|
|
167
|
+
<template #cell-status="{ value }">
|
|
168
|
+
<span
|
|
169
|
+
class="count-detail__entry-status"
|
|
170
|
+
:title="entryStatusLabel(value)"
|
|
171
|
+
>
|
|
172
|
+
{{ entryStatusIcon(value) }}
|
|
173
|
+
</span>
|
|
174
|
+
</template>
|
|
175
|
+
<template #cell-product_name="{ value }">
|
|
176
|
+
<span class="count-detail__entry-product">{{ value }}</span>
|
|
177
|
+
</template>
|
|
178
|
+
<template #cell-expected_quantity="{ value }">
|
|
179
|
+
<span class="count-detail__entry-qty">{{ value ?? '-' }}</span>
|
|
180
|
+
</template>
|
|
181
|
+
<template #cell-counted_quantity="{ row }">
|
|
182
|
+
<span
|
|
183
|
+
v-if="row.counted_quantity !== null"
|
|
184
|
+
class="count-detail__entry-qty"
|
|
185
|
+
:class="{
|
|
186
|
+
'count-detail__entry-qty--match': row.discrepancy === 0,
|
|
187
|
+
'count-detail__entry-qty--discrepancy': row.discrepancy !== null && row.discrepancy !== 0,
|
|
188
|
+
}"
|
|
189
|
+
>
|
|
190
|
+
{{ row.counted_quantity }}
|
|
191
|
+
<span
|
|
192
|
+
v-if="row.discrepancy !== null && row.discrepancy !== 0"
|
|
193
|
+
class="count-detail__entry-disc"
|
|
194
|
+
>
|
|
195
|
+
({{ row.discrepancy > 0 ? '+' : '' }}{{ row.discrepancy }})
|
|
196
|
+
</span>
|
|
197
|
+
</span>
|
|
198
|
+
<span v-else class="count-detail__entry-pending">pending</span>
|
|
199
|
+
</template>
|
|
200
|
+
</InvTable>
|
|
201
|
+
</InvCard>
|
|
202
|
+
</template>
|
|
203
|
+
|
|
204
|
+
<!-- Confirmation modal -->
|
|
205
|
+
<InvModal
|
|
206
|
+
:show="showConfirmModal"
|
|
207
|
+
:title="confirmTitle"
|
|
208
|
+
:description="confirmDescription"
|
|
209
|
+
size="sm"
|
|
210
|
+
@update:show="showConfirmModal = $event"
|
|
211
|
+
@close="showConfirmModal = false"
|
|
212
|
+
>
|
|
213
|
+
<template #footer>
|
|
214
|
+
<InvButton variant="ghost" size="sm" @click="showConfirmModal = false">
|
|
215
|
+
Cancel
|
|
216
|
+
</InvButton>
|
|
217
|
+
<InvButton
|
|
218
|
+
:variant="confirmDanger ? 'danger' : 'primary'"
|
|
219
|
+
size="sm"
|
|
220
|
+
:loading="confirmLoading"
|
|
221
|
+
@click="confirmAction"
|
|
222
|
+
>
|
|
223
|
+
{{ confirmLabel }}
|
|
224
|
+
</InvButton>
|
|
225
|
+
</template>
|
|
226
|
+
</InvModal>
|
|
227
|
+
</div>
|
|
228
|
+
</template>
|
|
229
|
+
|
|
230
|
+
<script setup lang="ts">
|
|
231
|
+
import { ref, computed, onMounted, watch } from 'vue'
|
|
232
|
+
import { useRouter } from 'vue-router'
|
|
233
|
+
import { useCountSessions } from '../../composables/useCountSessions'
|
|
234
|
+
import { useCountStore } from '../../stores/countStore'
|
|
235
|
+
import type { CountSession, CountEntry, CountEntryStatus, Column } from '../../types'
|
|
236
|
+
import InvCard from '../shared/InvCard.vue'
|
|
237
|
+
import InvButton from '../shared/InvButton.vue'
|
|
238
|
+
import InvBadge from '../shared/InvBadge.vue'
|
|
239
|
+
import InvTable from '../shared/InvTable.vue'
|
|
240
|
+
import InvSelect from '../shared/InvSelect.vue'
|
|
241
|
+
import InvModal from '../shared/InvModal.vue'
|
|
242
|
+
import InvEmptyState from '../shared/InvEmptyState.vue'
|
|
243
|
+
|
|
244
|
+
const props = defineProps<{
|
|
245
|
+
sessionId: number
|
|
246
|
+
}>()
|
|
247
|
+
|
|
248
|
+
const router = useRouter()
|
|
249
|
+
const countStore = useCountStore()
|
|
250
|
+
const {
|
|
251
|
+
currentSession: session,
|
|
252
|
+
entries,
|
|
253
|
+
loading,
|
|
254
|
+
error,
|
|
255
|
+
fetchSession,
|
|
256
|
+
fetchEntries,
|
|
257
|
+
generateEntries,
|
|
258
|
+
updateStatus,
|
|
259
|
+
applySession,
|
|
260
|
+
verifyEntry,
|
|
261
|
+
} = useCountSessions()
|
|
262
|
+
|
|
263
|
+
const entriesLoading = ref(false)
|
|
264
|
+
const generating = ref(false)
|
|
265
|
+
const applying = ref(false)
|
|
266
|
+
const statusFilter = ref<string>('')
|
|
267
|
+
const showDiscrepanciesOnly = ref(false)
|
|
268
|
+
|
|
269
|
+
// Confirmation modal state
|
|
270
|
+
const showConfirmModal = ref(false)
|
|
271
|
+
const confirmTitle = ref('')
|
|
272
|
+
const confirmDescription = ref('')
|
|
273
|
+
const confirmLabel = ref('')
|
|
274
|
+
const confirmDanger = ref(false)
|
|
275
|
+
const confirmLoading = ref(false)
|
|
276
|
+
let pendingConfirmAction: (() => Promise<void>) | null = null
|
|
277
|
+
|
|
278
|
+
const entryColumns: Column[] = [
|
|
279
|
+
{ key: 'status', label: 'Status', width: '60px', align: 'center' },
|
|
280
|
+
{ key: 'product_name', label: 'Product' },
|
|
281
|
+
{ key: 'expected_quantity', label: 'Expected', width: '100px', align: 'right' },
|
|
282
|
+
{ key: 'counted_quantity', label: 'Counted', width: '140px', align: 'right' },
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
const statusOptions = [
|
|
286
|
+
{ value: '', label: 'All Statuses' },
|
|
287
|
+
{ value: 'pending', label: 'Pending' },
|
|
288
|
+
{ value: 'counted', label: 'Counted' },
|
|
289
|
+
{ value: 'verified', label: 'Verified' },
|
|
290
|
+
{ value: 'adjusted', label: 'Adjusted' },
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
const statusVariant = computed(() => {
|
|
294
|
+
if (!session.value) return 'muted' as const
|
|
295
|
+
const map: Record<string, 'default' | 'success' | 'warning' | 'error' | 'muted'> = {
|
|
296
|
+
pending: 'muted',
|
|
297
|
+
in_progress: 'default',
|
|
298
|
+
completed: 'success',
|
|
299
|
+
cancelled: 'error',
|
|
300
|
+
}
|
|
301
|
+
return map[session.value.status] ?? 'muted'
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const statusLabel = computed(() => {
|
|
305
|
+
if (!session.value) return ''
|
|
306
|
+
const map: Record<string, string> = {
|
|
307
|
+
pending: 'Pending',
|
|
308
|
+
in_progress: 'In Progress',
|
|
309
|
+
completed: 'Completed',
|
|
310
|
+
cancelled: 'Cancelled',
|
|
311
|
+
}
|
|
312
|
+
return map[session.value.status] ?? session.value.status
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const progressClass = computed(() => {
|
|
316
|
+
if (!session.value) return ''
|
|
317
|
+
const pct = session.value.progress.percent_complete
|
|
318
|
+
if (pct >= 90) return 'count-detail__progress-fill--success'
|
|
319
|
+
if (pct >= 50) return 'count-detail__progress-fill--primary'
|
|
320
|
+
return 'count-detail__progress-fill--warning'
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
const canGenerate = computed(() =>
|
|
324
|
+
session.value?.status === 'pending' && entries.value.length === 0
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
const canStartCounting = computed(() =>
|
|
328
|
+
session.value &&
|
|
329
|
+
(session.value.status === 'pending' || session.value.status === 'in_progress') &&
|
|
330
|
+
entries.value.length > 0
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
const hasCounted = computed(() =>
|
|
334
|
+
entries.value.some(e => e.status !== 'pending')
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
const canApply = computed(() =>
|
|
338
|
+
entries.value.some(
|
|
339
|
+
e => e.status === 'verified' && e.discrepancy !== null && e.discrepancy !== 0
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
const filteredEntries = computed(() => {
|
|
344
|
+
let result = entries.value
|
|
345
|
+
if (statusFilter.value) {
|
|
346
|
+
result = result.filter(e => e.status === statusFilter.value)
|
|
347
|
+
}
|
|
348
|
+
if (showDiscrepanciesOnly.value) {
|
|
349
|
+
result = result.filter(e => e.discrepancy !== null && e.discrepancy !== 0)
|
|
350
|
+
}
|
|
351
|
+
return result
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
onMounted(() => loadSession())
|
|
355
|
+
|
|
356
|
+
async function loadSession() {
|
|
357
|
+
try {
|
|
358
|
+
await fetchSession(props.sessionId)
|
|
359
|
+
await loadEntries()
|
|
360
|
+
} catch {
|
|
361
|
+
// error state handled by template
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function loadEntries() {
|
|
366
|
+
entriesLoading.value = true
|
|
367
|
+
try {
|
|
368
|
+
await fetchEntries(props.sessionId)
|
|
369
|
+
} finally {
|
|
370
|
+
entriesLoading.value = false
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function handleGenerate() {
|
|
375
|
+
generating.value = true
|
|
376
|
+
try {
|
|
377
|
+
await generateEntries(props.sessionId)
|
|
378
|
+
await loadEntries()
|
|
379
|
+
await fetchSession(props.sessionId)
|
|
380
|
+
} finally {
|
|
381
|
+
generating.value = false
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function handleStartCounting() {
|
|
386
|
+
countStore.startCounting(props.sessionId)
|
|
387
|
+
router.push({ name: 'inventory-count-entry', params: { id: props.sessionId } })
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function toggleDiscrepancyFilter() {
|
|
391
|
+
showDiscrepanciesOnly.value = !showDiscrepanciesOnly.value
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function handleApply() {
|
|
395
|
+
confirmTitle.value = 'Apply Adjustments'
|
|
396
|
+
confirmDescription.value = 'This will adjust inventory quantities based on verified counts. This action cannot be undone.'
|
|
397
|
+
confirmLabel.value = 'Apply'
|
|
398
|
+
confirmDanger.value = false
|
|
399
|
+
pendingConfirmAction = async () => {
|
|
400
|
+
applying.value = true
|
|
401
|
+
try {
|
|
402
|
+
await applySession(props.sessionId)
|
|
403
|
+
await fetchSession(props.sessionId)
|
|
404
|
+
await loadEntries()
|
|
405
|
+
} finally {
|
|
406
|
+
applying.value = false
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
showConfirmModal.value = true
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function handleCancel() {
|
|
413
|
+
confirmTitle.value = 'Cancel Session'
|
|
414
|
+
confirmDescription.value = 'Are you sure you want to cancel this count session? Uncounted entries will be discarded.'
|
|
415
|
+
confirmLabel.value = 'Cancel Session'
|
|
416
|
+
confirmDanger.value = true
|
|
417
|
+
pendingConfirmAction = async () => {
|
|
418
|
+
await updateStatus(props.sessionId, 'cancelled')
|
|
419
|
+
await fetchSession(props.sessionId)
|
|
420
|
+
}
|
|
421
|
+
showConfirmModal.value = true
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function confirmAction() {
|
|
425
|
+
if (!pendingConfirmAction) return
|
|
426
|
+
confirmLoading.value = true
|
|
427
|
+
try {
|
|
428
|
+
await pendingConfirmAction()
|
|
429
|
+
showConfirmModal.value = false
|
|
430
|
+
} finally {
|
|
431
|
+
confirmLoading.value = false
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function handleViewReport() {
|
|
436
|
+
router.push({ name: 'inventory-count-report', params: { id: props.sessionId } })
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function handleEntryClick(entry: CountEntry) {
|
|
440
|
+
if (entry.status === 'counted' && entry.discrepancy !== null && entry.discrepancy !== 0) {
|
|
441
|
+
verifyEntry(props.sessionId, entry.id)
|
|
442
|
+
.then(() => loadEntries())
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function entryStatusIcon(status: CountEntryStatus): string {
|
|
447
|
+
const icons: Record<CountEntryStatus, string> = {
|
|
448
|
+
pending: '\u23F3',
|
|
449
|
+
counted: '\u2713',
|
|
450
|
+
verified: '\u2705',
|
|
451
|
+
adjusted: '\uD83D\uDD27',
|
|
452
|
+
}
|
|
453
|
+
return icons[status] ?? status
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function entryStatusLabel(status: CountEntryStatus): string {
|
|
457
|
+
const labels: Record<CountEntryStatus, string> = {
|
|
458
|
+
pending: 'Pending',
|
|
459
|
+
counted: 'Counted',
|
|
460
|
+
verified: 'Verified',
|
|
461
|
+
adjusted: 'Adjusted',
|
|
462
|
+
}
|
|
463
|
+
return labels[status] ?? status
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function formatDate(isoDate: string): string {
|
|
467
|
+
return new Date(isoDate).toLocaleDateString('en-US', {
|
|
468
|
+
month: 'short',
|
|
469
|
+
day: 'numeric',
|
|
470
|
+
year: 'numeric',
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
</script>
|
|
474
|
+
|
|
475
|
+
<style scoped>
|
|
476
|
+
.count-detail {
|
|
477
|
+
max-width: 900px;
|
|
478
|
+
display: flex;
|
|
479
|
+
flex-direction: column;
|
|
480
|
+
gap: var(--space-5, 1.25rem);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.count-detail__header {
|
|
484
|
+
display: flex;
|
|
485
|
+
align-items: flex-start;
|
|
486
|
+
justify-content: space-between;
|
|
487
|
+
gap: var(--space-3, 0.75rem);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.count-detail__header-info {
|
|
491
|
+
min-width: 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.count-detail__title {
|
|
495
|
+
margin: 0 0 var(--space-2, 0.5rem);
|
|
496
|
+
font-size: var(--text-xl, 1.25rem);
|
|
497
|
+
font-weight: 700;
|
|
498
|
+
color: var(--admin-text-primary);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.count-detail__meta {
|
|
502
|
+
display: flex;
|
|
503
|
+
align-items: center;
|
|
504
|
+
gap: var(--space-3, 0.75rem);
|
|
505
|
+
flex-wrap: wrap;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.count-detail__meta-item {
|
|
509
|
+
font-size: var(--text-sm, 0.875rem);
|
|
510
|
+
color: var(--admin-text-secondary);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.count-detail__progress-bar {
|
|
514
|
+
display: flex;
|
|
515
|
+
align-items: center;
|
|
516
|
+
gap: var(--space-3, 0.75rem);
|
|
517
|
+
margin-bottom: var(--space-4, 1rem);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.count-detail__progress-track {
|
|
521
|
+
flex: 1;
|
|
522
|
+
height: 10px;
|
|
523
|
+
border-radius: 5px;
|
|
524
|
+
background: var(--admin-border);
|
|
525
|
+
overflow: hidden;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.count-detail__progress-fill {
|
|
529
|
+
height: 100%;
|
|
530
|
+
border-radius: 5px;
|
|
531
|
+
transition: width 0.3s ease;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.count-detail__progress-fill--primary {
|
|
535
|
+
background: var(--color-primary, #2563eb);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.count-detail__progress-fill--success {
|
|
539
|
+
background: var(--color-success, #059669);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.count-detail__progress-fill--warning {
|
|
543
|
+
background: var(--color-warning, #d97706);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.count-detail__progress-pct {
|
|
547
|
+
font-size: var(--text-base, 1rem);
|
|
548
|
+
font-weight: 700;
|
|
549
|
+
font-variant-numeric: tabular-nums;
|
|
550
|
+
color: var(--admin-text-primary);
|
|
551
|
+
min-width: 42px;
|
|
552
|
+
text-align: right;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.count-detail__stats-grid {
|
|
556
|
+
display: grid;
|
|
557
|
+
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
|
558
|
+
gap: var(--space-4, 1rem);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.count-detail__stat {
|
|
562
|
+
display: flex;
|
|
563
|
+
flex-direction: column;
|
|
564
|
+
align-items: center;
|
|
565
|
+
gap: var(--space-1, 0.25rem);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.count-detail__stat-value {
|
|
569
|
+
font-size: var(--text-lg, 1.125rem);
|
|
570
|
+
font-weight: 700;
|
|
571
|
+
font-variant-numeric: tabular-nums;
|
|
572
|
+
color: var(--admin-text-primary);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.count-detail__stat-value--pending {
|
|
576
|
+
color: var(--admin-text-tertiary);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.count-detail__stat-value--warning {
|
|
580
|
+
color: var(--color-warning, #d97706);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.count-detail__stat-label {
|
|
584
|
+
font-size: var(--text-xs, 0.75rem);
|
|
585
|
+
color: var(--admin-text-secondary);
|
|
586
|
+
text-transform: uppercase;
|
|
587
|
+
letter-spacing: 0.05em;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.count-detail__action-grid {
|
|
591
|
+
display: flex;
|
|
592
|
+
flex-wrap: wrap;
|
|
593
|
+
gap: var(--space-2, 0.5rem);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.count-detail__filters {
|
|
597
|
+
display: flex;
|
|
598
|
+
align-items: center;
|
|
599
|
+
gap: var(--space-3, 0.75rem);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.count-detail__discrepancy-toggle {
|
|
603
|
+
display: flex;
|
|
604
|
+
align-items: center;
|
|
605
|
+
gap: var(--space-2, 0.5rem);
|
|
606
|
+
font-size: var(--text-sm, 0.875rem);
|
|
607
|
+
color: var(--admin-text-secondary);
|
|
608
|
+
cursor: pointer;
|
|
609
|
+
white-space: nowrap;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.count-detail__discrepancy-checkbox {
|
|
613
|
+
width: 16px;
|
|
614
|
+
height: 16px;
|
|
615
|
+
accent-color: var(--color-primary, #2563eb);
|
|
616
|
+
cursor: pointer;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.count-detail__entry-status {
|
|
620
|
+
font-size: var(--text-base, 1rem);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.count-detail__entry-product {
|
|
624
|
+
font-weight: 500;
|
|
625
|
+
color: var(--admin-text-primary);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.count-detail__entry-qty {
|
|
629
|
+
font-variant-numeric: tabular-nums;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.count-detail__entry-qty--match {
|
|
633
|
+
color: var(--color-success, #059669);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.count-detail__entry-qty--discrepancy {
|
|
637
|
+
color: var(--color-warning, #d97706);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.count-detail__entry-disc {
|
|
641
|
+
font-size: var(--text-xs, 0.75rem);
|
|
642
|
+
font-weight: 600;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.count-detail__entry-pending {
|
|
646
|
+
color: var(--admin-text-tertiary);
|
|
647
|
+
font-style: italic;
|
|
648
|
+
font-size: var(--text-sm, 0.875rem);
|
|
649
|
+
}
|
|
650
|
+
</style>
|