@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,215 @@
1
+ <template>
2
+ <InvCard title="Locations" :loading="loading" no-padding>
3
+ <template v-if="!loading && locations.length === 0">
4
+ <InvEmptyState
5
+ title="No locations tracked"
6
+ description="This product is not currently tracked at any location."
7
+ />
8
+ </template>
9
+
10
+ <template v-else-if="!loading">
11
+ <InvTable
12
+ :columns="columns"
13
+ :data="locations"
14
+ empty-message="No locations found."
15
+ >
16
+ <template #cell-path="{ row }">
17
+ <button
18
+ type="button"
19
+ class="pll-location-link"
20
+ :aria-label="`Navigate to ${row.location?.name ?? 'location'}`"
21
+ @click="handleNavigate(row)"
22
+ >
23
+ <span class="pll-location-path">
24
+ {{ getLocationPath(row) }}
25
+ </span>
26
+ </button>
27
+ </template>
28
+
29
+ <template #cell-code="{ row }">
30
+ <InvBadge v-if="row.location?.full_code" variant="default" size="sm">
31
+ {{ row.location.full_code }}
32
+ </InvBadge>
33
+ <InvBadge v-else-if="row.location?.code" variant="default" size="sm">
34
+ {{ row.location.code }}
35
+ </InvBadge>
36
+ </template>
37
+
38
+ <template #cell-quantity="{ row }">
39
+ <span class="pll-quantity">{{ formatQuantity(row.quantity) }}</span>
40
+ </template>
41
+
42
+ <template #cell-is_primary="{ row }">
43
+ <svg
44
+ v-if="row.is_primary"
45
+ class="pll-primary-star"
46
+ width="18"
47
+ height="18"
48
+ viewBox="0 0 18 18"
49
+ fill="none"
50
+ role="img"
51
+ aria-label="Primary location"
52
+ >
53
+ <path
54
+ d="M9 1.5L11.3 6.2L16.5 6.9L12.75 10.55L13.6 15.75L9 13.3L4.4 15.75L5.25 10.55L1.5 6.9L6.7 6.2L9 1.5Z"
55
+ fill="currentColor"
56
+ stroke="currentColor"
57
+ stroke-width="1.5"
58
+ stroke-linecap="round"
59
+ stroke-linejoin="round"
60
+ />
61
+ </svg>
62
+ <span v-else class="pll-no-primary" aria-label="Not primary location" />
63
+ </template>
64
+ </InvTable>
65
+
66
+ <div class="pll-footer">
67
+ <span class="pll-total-label">Total:</span>
68
+ <span class="pll-total-value">
69
+ {{ formatQuantity(totalQuantity) }}
70
+ across {{ locations.length }} location{{ locations.length !== 1 ? 's' : '' }}
71
+ </span>
72
+ </div>
73
+ </template>
74
+ </InvCard>
75
+ </template>
76
+
77
+ <script setup lang="ts">
78
+ import { ref, computed, watch } from 'vue'
79
+ import type { LocationProduct, Location, Column } from '../../types'
80
+ import { useLocationProducts } from '../../composables/useLocationProducts'
81
+ import InvCard from '../shared/InvCard.vue'
82
+ import InvTable from '../shared/InvTable.vue'
83
+ import InvBadge from '../shared/InvBadge.vue'
84
+ import InvEmptyState from '../shared/InvEmptyState.vue'
85
+
86
+ const props = defineProps<{
87
+ productId: number | string
88
+ }>()
89
+
90
+ const emit = defineEmits<{
91
+ navigate: [location: Location]
92
+ }>()
93
+
94
+ const { loading, fetchProductLocations } = useLocationProducts()
95
+
96
+ const locations = ref<LocationProduct[]>([])
97
+
98
+ const columns: Column[] = [
99
+ { key: 'path', label: 'Location', sortable: false },
100
+ { key: 'code', label: 'Code', sortable: false, width: '120px' },
101
+ { key: 'quantity', label: 'Qty', sortable: false, width: '80px', align: 'right' },
102
+ { key: 'is_primary', label: 'Primary', sortable: false, width: '80px', align: 'center' },
103
+ ]
104
+
105
+ const totalQuantity = computed(() => {
106
+ return locations.value.reduce((sum, lp) => sum + lp.quantity, 0)
107
+ })
108
+
109
+ function formatQuantity(qty: number): string {
110
+ return Number.isInteger(qty) ? qty.toFixed(0) : qty.toFixed(2)
111
+ }
112
+
113
+ function getLocationPath(row: LocationProduct): string {
114
+ if (row.location?.path) {
115
+ return row.location.path
116
+ .map((segment) => segment.name)
117
+ .concat([row.location.name])
118
+ .join(' > ')
119
+ }
120
+ return row.location?.name ?? 'Unknown location'
121
+ }
122
+
123
+ function handleNavigate(row: LocationProduct) {
124
+ if (row.location) {
125
+ emit('navigate', row.location)
126
+ }
127
+ }
128
+
129
+ async function loadLocations() {
130
+ try {
131
+ locations.value = await fetchProductLocations(props.productId)
132
+ } catch {
133
+ // error displayed by composable
134
+ locations.value = []
135
+ }
136
+ }
137
+
138
+ watch(
139
+ () => props.productId,
140
+ (id) => {
141
+ if (id) {
142
+ loadLocations()
143
+ }
144
+ },
145
+ { immediate: true },
146
+ )
147
+ </script>
148
+
149
+ <style scoped>
150
+ .pll-location-link {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ padding: var(--space-1, 0.25rem) 0;
154
+ min-height: 44px;
155
+ border: none;
156
+ background: transparent;
157
+ color: var(--color-primary, #2563eb);
158
+ font-size: var(--text-sm, 0.875rem);
159
+ font-family: inherit;
160
+ font-weight: 500;
161
+ cursor: pointer;
162
+ text-align: left;
163
+ border-radius: 4px;
164
+ transition: color 0.15s ease;
165
+ }
166
+
167
+ .pll-location-link:hover {
168
+ text-decoration: underline;
169
+ }
170
+
171
+ .pll-location-link:focus-visible {
172
+ outline: 2px solid var(--admin-focus-ring, #2563eb);
173
+ outline-offset: 2px;
174
+ }
175
+
176
+ .pll-location-path {
177
+ line-height: 1.4;
178
+ }
179
+
180
+ .pll-quantity {
181
+ font-variant-numeric: tabular-nums;
182
+ font-weight: 500;
183
+ color: var(--admin-text-primary);
184
+ }
185
+
186
+ .pll-primary-star {
187
+ color: var(--color-warning, #d97706);
188
+ }
189
+
190
+ .pll-no-primary {
191
+ display: block;
192
+ width: 18px;
193
+ height: 18px;
194
+ }
195
+
196
+ .pll-footer {
197
+ display: flex;
198
+ align-items: center;
199
+ gap: var(--space-2, 0.5rem);
200
+ padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
201
+ border-top: 1px solid var(--admin-border);
202
+ background: var(--admin-content-bg);
203
+ font-size: var(--text-sm, 0.875rem);
204
+ }
205
+
206
+ .pll-total-label {
207
+ font-weight: 600;
208
+ color: var(--admin-text-primary);
209
+ }
210
+
211
+ .pll-total-value {
212
+ color: var(--admin-text-secondary);
213
+ font-variant-numeric: tabular-nums;
214
+ }
215
+ </style>