@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.
Files changed (53) hide show
  1. package/package.json +47 -0
  2. package/src/api/client.ts +31 -0
  3. package/src/components/counting/CountEntryForm.vue +833 -0
  4. package/src/components/counting/CountReport.vue +385 -0
  5. package/src/components/counting/CountSessionDetail.vue +650 -0
  6. package/src/components/counting/CountSessionList.vue +683 -0
  7. package/src/components/dashboard/InventoryDashboard.vue +670 -0
  8. package/src/components/dashboard/UnlocatedProducts.vue +468 -0
  9. package/src/components/labels/LabelBatchPrint.vue +528 -0
  10. package/src/components/labels/LabelPreview.vue +293 -0
  11. package/src/components/locations/InventoryLocatorShell.vue +408 -0
  12. package/src/components/locations/LocationBreadcrumb.vue +144 -0
  13. package/src/components/locations/LocationCodeBadge.vue +46 -0
  14. package/src/components/locations/LocationDetail.vue +884 -0
  15. package/src/components/locations/LocationForm.vue +360 -0
  16. package/src/components/locations/LocationSearchInput.vue +428 -0
  17. package/src/components/locations/LocationTree.vue +156 -0
  18. package/src/components/locations/LocationTreeNode.vue +280 -0
  19. package/src/components/locations/LocationTypeIcon.vue +58 -0
  20. package/src/components/products/LocationProductAdd.vue +637 -0
  21. package/src/components/products/LocationProductList.vue +547 -0
  22. package/src/components/products/ProductLocationList.vue +215 -0
  23. package/src/components/products/QuickMoveModal.vue +592 -0
  24. package/src/components/scanning/ScanHistory.vue +146 -0
  25. package/src/components/scanning/ScanResult.vue +350 -0
  26. package/src/components/scanning/ScannerOverlay.vue +696 -0
  27. package/src/components/shared/InvBadge.vue +71 -0
  28. package/src/components/shared/InvButton.vue +206 -0
  29. package/src/components/shared/InvCard.vue +254 -0
  30. package/src/components/shared/InvEmptyState.vue +132 -0
  31. package/src/components/shared/InvInput.vue +125 -0
  32. package/src/components/shared/InvModal.vue +296 -0
  33. package/src/components/shared/InvSelect.vue +155 -0
  34. package/src/components/shared/InvTable.vue +288 -0
  35. package/src/composables/useCountSessions.ts +184 -0
  36. package/src/composables/useLabelPrinting.ts +71 -0
  37. package/src/composables/useLocationBreadcrumbs.ts +19 -0
  38. package/src/composables/useLocationProducts.ts +125 -0
  39. package/src/composables/useLocationSearch.ts +46 -0
  40. package/src/composables/useLocations.ts +159 -0
  41. package/src/composables/useMovements.ts +71 -0
  42. package/src/composables/useScanner.ts +83 -0
  43. package/src/env.d.ts +7 -0
  44. package/src/index.ts +46 -0
  45. package/src/plugin.ts +14 -0
  46. package/src/stores/countStore.ts +95 -0
  47. package/src/stores/locationStore.ts +113 -0
  48. package/src/stores/scannerStore.ts +51 -0
  49. package/src/types/index.ts +216 -0
  50. package/src/utils/codeFormatter.ts +29 -0
  51. package/src/utils/locationIcons.ts +64 -0
  52. package/tsconfig.json +21 -0
  53. package/vite.config.ts +37 -0
@@ -0,0 +1,385 @@
1
+ <template>
2
+ <div class="count-report">
3
+ <!-- Loading -->
4
+ <InvCard v-if="loading && !report" loading />
5
+
6
+ <!-- Error -->
7
+ <InvEmptyState
8
+ v-else-if="error && !report"
9
+ title="Could not load report"
10
+ :description="error || 'An unknown error occurred'"
11
+ action-label="Retry"
12
+ @action="loadReport"
13
+ />
14
+
15
+ <template v-if="report">
16
+ <!-- Report header -->
17
+ <div class="count-report__header">
18
+ <h1 class="count-report__title">
19
+ Count Report: {{ report.session.name }}
20
+ </h1>
21
+ <div class="count-report__actions">
22
+ <InvButton variant="secondary" size="sm" @click="handleExportCsv">
23
+ <template #icon-left>
24
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
25
+ <path d="M4 10L8 14L12 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
26
+ <path d="M8 14V2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
27
+ </svg>
28
+ </template>
29
+ Export CSV
30
+ </InvButton>
31
+ <InvButton variant="secondary" size="sm" @click="handlePrint">
32
+ <template #icon-left>
33
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
34
+ <rect x="4" y="1" width="8" height="5" rx="1" stroke="currentColor" stroke-width="1.5" />
35
+ <rect x="2" y="6" width="12" height="6" rx="1" stroke="currentColor" stroke-width="1.5" />
36
+ <rect x="5" y="10" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.5" />
37
+ </svg>
38
+ </template>
39
+ Print Report
40
+ </InvButton>
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Summary card -->
45
+ <InvCard title="Summary" class="count-report__summary">
46
+ <div class="count-report__summary-grid">
47
+ <div class="count-report__summary-stat">
48
+ <span class="count-report__summary-value">{{ report.summary.total_entries }}</span>
49
+ <span class="count-report__summary-label">Total Items</span>
50
+ </div>
51
+ <div class="count-report__summary-stat">
52
+ <span class="count-report__summary-value">{{ report.summary.counted }}</span>
53
+ <span class="count-report__summary-label">Counted</span>
54
+ </div>
55
+ <div class="count-report__summary-stat">
56
+ <span class="count-report__summary-value">{{ report.summary.verified }}</span>
57
+ <span class="count-report__summary-label">Verified</span>
58
+ </div>
59
+ <div class="count-report__summary-stat">
60
+ <span class="count-report__summary-value count-report__summary-value--muted">
61
+ {{ report.summary.pending }}
62
+ </span>
63
+ <span class="count-report__summary-label">Pending</span>
64
+ </div>
65
+ <div class="count-report__summary-stat">
66
+ <span
67
+ class="count-report__summary-value"
68
+ :class="report.summary.with_discrepancies > 0
69
+ ? 'count-report__summary-value--warning'
70
+ : 'count-report__summary-value--success'"
71
+ >
72
+ {{ report.summary.with_discrepancies }}
73
+ </span>
74
+ <span class="count-report__summary-label">Discrepancies</span>
75
+ </div>
76
+ <div class="count-report__summary-stat">
77
+ <span
78
+ class="count-report__summary-value"
79
+ :class="report.summary.accuracy_percent >= 95
80
+ ? 'count-report__summary-value--success'
81
+ : report.summary.accuracy_percent >= 90
82
+ ? 'count-report__summary-value--warning'
83
+ : 'count-report__summary-value--error'"
84
+ >
85
+ {{ report.summary.accuracy_percent.toFixed(1) }}%
86
+ </span>
87
+ <span class="count-report__summary-label">Accuracy</span>
88
+ </div>
89
+ </div>
90
+
91
+ <div class="count-report__totals">
92
+ <div class="count-report__total-row">
93
+ <span class="count-report__total-label">Total Expected</span>
94
+ <span class="count-report__total-value">
95
+ {{ formatNumber(report.summary.total_expected) }}
96
+ </span>
97
+ </div>
98
+ <div class="count-report__total-row">
99
+ <span class="count-report__total-label">Total Counted</span>
100
+ <span class="count-report__total-value">
101
+ {{ formatNumber(report.summary.total_counted) }}
102
+ </span>
103
+ </div>
104
+ <div class="count-report__total-row count-report__total-row--net">
105
+ <span class="count-report__total-label">Net Discrepancy</span>
106
+ <span
107
+ class="count-report__total-value"
108
+ :class="{
109
+ 'count-report__total-value--positive': report.summary.net_discrepancy > 0,
110
+ 'count-report__total-value--negative': report.summary.net_discrepancy < 0,
111
+ }"
112
+ >
113
+ {{ report.summary.net_discrepancy > 0 ? '+' : '' }}{{ formatNumber(report.summary.net_discrepancy) }}
114
+ </span>
115
+ </div>
116
+ </div>
117
+ </InvCard>
118
+
119
+ <!-- Top discrepancies table -->
120
+ <InvCard
121
+ title="Top Discrepancies"
122
+ class="count-report__discrepancies"
123
+ no-padding
124
+ >
125
+ <InvTable
126
+ :columns="discrepancyColumns"
127
+ :data="report.top_discrepancies"
128
+ empty-message="No discrepancies found."
129
+ >
130
+ <template #cell-location="{ value }">
131
+ <span class="count-report__disc-location">{{ value }}</span>
132
+ </template>
133
+ <template #cell-product="{ value }">
134
+ <span class="count-report__disc-product">{{ value }}</span>
135
+ </template>
136
+ <template #cell-expected="{ value }">
137
+ <span class="count-report__disc-qty">{{ value }}</span>
138
+ </template>
139
+ <template #cell-counted="{ value }">
140
+ <span class="count-report__disc-qty">{{ value }}</span>
141
+ </template>
142
+ <template #cell-discrepancy="{ value }">
143
+ <span
144
+ class="count-report__disc-delta"
145
+ :class="{
146
+ 'count-report__disc-delta--positive': value > 0,
147
+ 'count-report__disc-delta--negative': value < 0,
148
+ }"
149
+ >
150
+ {{ value > 0 ? '+' : '' }}{{ value }}
151
+ </span>
152
+ </template>
153
+ </InvTable>
154
+ </InvCard>
155
+ </template>
156
+ </div>
157
+ </template>
158
+
159
+ <script setup lang="ts">
160
+ import { onMounted } from 'vue'
161
+ import { useCountSessions } from '../../composables/useCountSessions'
162
+ import type { Column } from '../../types'
163
+ import InvCard from '../shared/InvCard.vue'
164
+ import InvButton from '../shared/InvButton.vue'
165
+ import InvTable from '../shared/InvTable.vue'
166
+ import InvEmptyState from '../shared/InvEmptyState.vue'
167
+
168
+ const props = defineProps<{
169
+ sessionId: number
170
+ }>()
171
+
172
+ const {
173
+ report,
174
+ loading,
175
+ error,
176
+ fetchReport,
177
+ } = useCountSessions()
178
+
179
+ const discrepancyColumns: Column[] = [
180
+ { key: 'location', label: 'Location' },
181
+ { key: 'product', label: 'Product' },
182
+ { key: 'expected', label: 'Expected', align: 'right', width: '100px' },
183
+ { key: 'counted', label: 'Counted', align: 'right', width: '100px' },
184
+ { key: 'discrepancy', label: '\u0394', align: 'right', width: '80px', sortable: true },
185
+ ]
186
+
187
+ onMounted(() => loadReport())
188
+
189
+ async function loadReport() {
190
+ try {
191
+ await fetchReport(props.sessionId)
192
+ } catch {
193
+ // Error handled in template
194
+ }
195
+ }
196
+
197
+ function formatNumber(value: number): string {
198
+ return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
199
+ }
200
+
201
+ function handleExportCsv() {
202
+ if (!report.value) return
203
+
204
+ const rows: string[][] = [
205
+ ['Location', 'Product', 'Expected', 'Counted', 'Discrepancy'],
206
+ ...report.value.top_discrepancies.map(d => [
207
+ d.location,
208
+ d.product,
209
+ String(d.expected),
210
+ String(d.counted),
211
+ String(d.discrepancy),
212
+ ]),
213
+ ]
214
+
215
+ const csvContent = rows
216
+ .map(row => row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(','))
217
+ .join('\n')
218
+
219
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
220
+ const url = URL.createObjectURL(blob)
221
+ const link = document.createElement('a')
222
+ link.href = url
223
+ link.download = `count-report-${props.sessionId}.csv`
224
+ document.body.appendChild(link)
225
+ link.click()
226
+ document.body.removeChild(link)
227
+ URL.revokeObjectURL(url)
228
+ }
229
+
230
+ function handlePrint() {
231
+ window.print()
232
+ }
233
+ </script>
234
+
235
+ <style scoped>
236
+ .count-report {
237
+ max-width: 900px;
238
+ display: flex;
239
+ flex-direction: column;
240
+ gap: var(--space-5, 1.25rem);
241
+ }
242
+
243
+ .count-report__header {
244
+ display: flex;
245
+ align-items: flex-start;
246
+ justify-content: space-between;
247
+ gap: var(--space-3, 0.75rem);
248
+ flex-wrap: wrap;
249
+ }
250
+
251
+ .count-report__title {
252
+ margin: 0;
253
+ font-size: var(--text-xl, 1.25rem);
254
+ font-weight: 700;
255
+ color: var(--admin-text-primary);
256
+ }
257
+
258
+ .count-report__actions {
259
+ display: flex;
260
+ gap: var(--space-2, 0.5rem);
261
+ flex-shrink: 0;
262
+ }
263
+
264
+ .count-report__summary-grid {
265
+ display: grid;
266
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
267
+ gap: var(--space-4, 1rem);
268
+ margin-bottom: var(--space-5, 1.25rem);
269
+ }
270
+
271
+ .count-report__summary-stat {
272
+ display: flex;
273
+ flex-direction: column;
274
+ align-items: center;
275
+ gap: var(--space-1, 0.25rem);
276
+ padding: var(--space-3, 0.75rem);
277
+ background: var(--admin-content-bg);
278
+ border-radius: 8px;
279
+ }
280
+
281
+ .count-report__summary-value {
282
+ font-size: var(--text-xl, 1.25rem);
283
+ font-weight: 700;
284
+ font-variant-numeric: tabular-nums;
285
+ color: var(--admin-text-primary);
286
+ }
287
+
288
+ .count-report__summary-value--success {
289
+ color: var(--color-success, #059669);
290
+ }
291
+
292
+ .count-report__summary-value--warning {
293
+ color: var(--color-warning, #d97706);
294
+ }
295
+
296
+ .count-report__summary-value--error {
297
+ color: var(--color-error, #dc2626);
298
+ }
299
+
300
+ .count-report__summary-value--muted {
301
+ color: var(--admin-text-tertiary);
302
+ }
303
+
304
+ .count-report__summary-label {
305
+ font-size: var(--text-xs, 0.75rem);
306
+ color: var(--admin-text-secondary);
307
+ text-transform: uppercase;
308
+ letter-spacing: 0.05em;
309
+ }
310
+
311
+ .count-report__totals {
312
+ border-top: 1px solid var(--admin-border);
313
+ padding-top: var(--space-4, 1rem);
314
+ display: flex;
315
+ flex-direction: column;
316
+ gap: var(--space-2, 0.5rem);
317
+ }
318
+
319
+ .count-report__total-row {
320
+ display: flex;
321
+ justify-content: space-between;
322
+ align-items: center;
323
+ font-size: var(--text-sm, 0.875rem);
324
+ }
325
+
326
+ .count-report__total-row--net {
327
+ padding-top: var(--space-2, 0.5rem);
328
+ border-top: 1px solid var(--admin-border);
329
+ font-weight: 600;
330
+ }
331
+
332
+ .count-report__total-label {
333
+ color: var(--admin-text-secondary);
334
+ }
335
+
336
+ .count-report__total-value {
337
+ font-variant-numeric: tabular-nums;
338
+ color: var(--admin-text-primary);
339
+ }
340
+
341
+ .count-report__total-value--positive {
342
+ color: var(--color-success, #059669);
343
+ }
344
+
345
+ .count-report__total-value--negative {
346
+ color: var(--color-error, #dc2626);
347
+ }
348
+
349
+ .count-report__disc-location {
350
+ font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
351
+ font-size: var(--text-xs, 0.75rem);
352
+ color: var(--admin-text-secondary);
353
+ }
354
+
355
+ .count-report__disc-product {
356
+ font-weight: 500;
357
+ }
358
+
359
+ .count-report__disc-qty {
360
+ font-variant-numeric: tabular-nums;
361
+ }
362
+
363
+ .count-report__disc-delta {
364
+ font-weight: 700;
365
+ font-variant-numeric: tabular-nums;
366
+ }
367
+
368
+ .count-report__disc-delta--positive {
369
+ color: var(--color-success, #059669);
370
+ }
371
+
372
+ .count-report__disc-delta--negative {
373
+ color: var(--color-error, #dc2626);
374
+ }
375
+
376
+ @media print {
377
+ .count-report__actions {
378
+ display: none;
379
+ }
380
+
381
+ .count-report {
382
+ gap: 12pt;
383
+ }
384
+ }
385
+ </style>