@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,528 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="label-batch">
|
|
3
|
+
<div class="label-batch__header">
|
|
4
|
+
<h1 class="label-batch__title">Print Labels</h1>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<!-- Format selector -->
|
|
8
|
+
<div class="label-batch__format">
|
|
9
|
+
<InvSelect
|
|
10
|
+
v-model="selectedFormat"
|
|
11
|
+
:options="formatOptions"
|
|
12
|
+
label="Format"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- Quick selectors -->
|
|
17
|
+
<InvCard title="Select locations to print" class="label-batch__selectors">
|
|
18
|
+
<div class="label-batch__quick-selectors">
|
|
19
|
+
<button
|
|
20
|
+
v-for="selector in quickSelectors"
|
|
21
|
+
:key="selector.label"
|
|
22
|
+
type="button"
|
|
23
|
+
class="label-batch__quick-btn"
|
|
24
|
+
@click="selector.action"
|
|
25
|
+
>
|
|
26
|
+
{{ selector.label }}
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- By building/type dropdowns -->
|
|
31
|
+
<div class="label-batch__filter-row">
|
|
32
|
+
<InvSelect
|
|
33
|
+
v-model="filterBuilding"
|
|
34
|
+
:options="buildingOptions"
|
|
35
|
+
placeholder="By Building"
|
|
36
|
+
/>
|
|
37
|
+
<InvSelect
|
|
38
|
+
v-model="filterType"
|
|
39
|
+
:options="typeFilterOptions"
|
|
40
|
+
placeholder="By Type"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</InvCard>
|
|
44
|
+
|
|
45
|
+
<!-- Loading -->
|
|
46
|
+
<InvCard v-if="loadingLocations" loading />
|
|
47
|
+
|
|
48
|
+
<!-- Location list with checkboxes -->
|
|
49
|
+
<InvCard
|
|
50
|
+
v-else-if="groupedLocations.length > 0"
|
|
51
|
+
class="label-batch__locations"
|
|
52
|
+
no-padding
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
v-for="group in groupedLocations"
|
|
56
|
+
:key="group.parentName"
|
|
57
|
+
class="label-batch__group"
|
|
58
|
+
>
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
class="label-batch__group-header"
|
|
62
|
+
@click="toggleGroup(group.parentName)"
|
|
63
|
+
>
|
|
64
|
+
<input
|
|
65
|
+
type="checkbox"
|
|
66
|
+
:checked="isGroupFullySelected(group)"
|
|
67
|
+
:indeterminate="isGroupPartiallySelected(group)"
|
|
68
|
+
class="label-batch__checkbox"
|
|
69
|
+
:aria-label="`Select all in ${group.parentName}`"
|
|
70
|
+
@click.stop="toggleGroupSelection(group)"
|
|
71
|
+
/>
|
|
72
|
+
<span class="label-batch__group-name">{{ group.parentName }}</span>
|
|
73
|
+
<span class="label-batch__group-count">{{ group.locations.length }}</span>
|
|
74
|
+
<svg
|
|
75
|
+
class="label-batch__group-chevron"
|
|
76
|
+
:class="{ 'label-batch__group-chevron--collapsed': !expandedGroups.has(group.parentName) }"
|
|
77
|
+
width="16"
|
|
78
|
+
height="16"
|
|
79
|
+
viewBox="0 0 16 16"
|
|
80
|
+
fill="none"
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
>
|
|
83
|
+
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
84
|
+
</svg>
|
|
85
|
+
</button>
|
|
86
|
+
|
|
87
|
+
<div
|
|
88
|
+
v-show="expandedGroups.has(group.parentName)"
|
|
89
|
+
class="label-batch__group-items"
|
|
90
|
+
>
|
|
91
|
+
<label
|
|
92
|
+
v-for="loc in group.locations"
|
|
93
|
+
:key="loc.id"
|
|
94
|
+
class="label-batch__location-row"
|
|
95
|
+
>
|
|
96
|
+
<input
|
|
97
|
+
type="checkbox"
|
|
98
|
+
:checked="selectedIds.has(loc.id)"
|
|
99
|
+
class="label-batch__checkbox"
|
|
100
|
+
@change="toggleLocation(loc.id)"
|
|
101
|
+
/>
|
|
102
|
+
<LocationCodeBadge :code="loc.full_code" />
|
|
103
|
+
<span class="label-batch__location-name">{{ loc.name }}</span>
|
|
104
|
+
<InvBadge variant="muted" size="sm">{{ loc.type_label }}</InvBadge>
|
|
105
|
+
</label>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</InvCard>
|
|
109
|
+
|
|
110
|
+
<!-- Empty -->
|
|
111
|
+
<InvEmptyState
|
|
112
|
+
v-else-if="!loadingLocations"
|
|
113
|
+
title="No locations found"
|
|
114
|
+
description="Create locations first to generate labels."
|
|
115
|
+
/>
|
|
116
|
+
|
|
117
|
+
<!-- Footer / print controls -->
|
|
118
|
+
<div v-if="selectedIds.size > 0" class="label-batch__footer">
|
|
119
|
+
<span class="label-batch__selected-count">
|
|
120
|
+
Selected: {{ selectedIds.size }} label{{ selectedIds.size !== 1 ? 's' : '' }}
|
|
121
|
+
</span>
|
|
122
|
+
<div class="label-batch__footer-actions">
|
|
123
|
+
<InvButton
|
|
124
|
+
variant="secondary"
|
|
125
|
+
size="md"
|
|
126
|
+
@click="handlePreview"
|
|
127
|
+
>
|
|
128
|
+
Preview All
|
|
129
|
+
</InvButton>
|
|
130
|
+
<InvButton
|
|
131
|
+
variant="primary"
|
|
132
|
+
size="md"
|
|
133
|
+
:loading="printing"
|
|
134
|
+
@click="handlePrint"
|
|
135
|
+
>
|
|
136
|
+
<template #icon-left>
|
|
137
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
138
|
+
<rect x="4" y="1" width="8" height="5" rx="1" stroke="currentColor" stroke-width="1.5" />
|
|
139
|
+
<rect x="2" y="6" width="12" height="6" rx="1" stroke="currentColor" stroke-width="1.5" />
|
|
140
|
+
<rect x="5" y="10" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.5" />
|
|
141
|
+
</svg>
|
|
142
|
+
</template>
|
|
143
|
+
Print {{ selectedIds.size }} Labels
|
|
144
|
+
</InvButton>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</template>
|
|
149
|
+
|
|
150
|
+
<script setup lang="ts">
|
|
151
|
+
import { ref, computed, onMounted, reactive } from 'vue'
|
|
152
|
+
import { useLocations } from '../../composables/useLocations'
|
|
153
|
+
import { useLabelPrinting } from '../../composables/useLabelPrinting'
|
|
154
|
+
import type { Location, LabelFormat, LocationType } from '../../types'
|
|
155
|
+
import InvCard from '../shared/InvCard.vue'
|
|
156
|
+
import InvButton from '../shared/InvButton.vue'
|
|
157
|
+
import InvSelect from '../shared/InvSelect.vue'
|
|
158
|
+
import InvBadge from '../shared/InvBadge.vue'
|
|
159
|
+
import InvEmptyState from '../shared/InvEmptyState.vue'
|
|
160
|
+
import LocationCodeBadge from '../locations/LocationCodeBadge.vue'
|
|
161
|
+
|
|
162
|
+
const { fetchRoots, locations } = useLocations()
|
|
163
|
+
const { printBatch } = useLabelPrinting()
|
|
164
|
+
|
|
165
|
+
const selectedFormat = ref<string>('standard')
|
|
166
|
+
const selectedIds = reactive(new Set<number>())
|
|
167
|
+
const expandedGroups = reactive(new Set<string>())
|
|
168
|
+
const loadingLocations = ref(true)
|
|
169
|
+
const printing = ref(false)
|
|
170
|
+
const filterBuilding = ref<string>('')
|
|
171
|
+
const filterType = ref<string>('')
|
|
172
|
+
const allLeafLocations = ref<Location[]>([])
|
|
173
|
+
|
|
174
|
+
const formatOptions = [
|
|
175
|
+
{ value: 'standard', label: 'Standard' },
|
|
176
|
+
{ value: 'small', label: 'Small' },
|
|
177
|
+
{ value: 'pallet', label: 'Pallet' },
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
const typeFilterOptions = computed(() => {
|
|
181
|
+
const types = new Set(allLeafLocations.value.map(l => l.type))
|
|
182
|
+
const options = [{ value: '', label: 'All Types' }]
|
|
183
|
+
types.forEach(t => {
|
|
184
|
+
options.push({ value: t, label: t.charAt(0).toUpperCase() + t.slice(1) })
|
|
185
|
+
})
|
|
186
|
+
return options
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const buildingOptions = computed(() => {
|
|
190
|
+
const buildings = new Set<string>()
|
|
191
|
+
allLeafLocations.value.forEach(l => {
|
|
192
|
+
if (l.path && l.path.length > 0) {
|
|
193
|
+
buildings.add(l.path[0].name)
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
const options = [{ value: '', label: 'All Buildings' }]
|
|
197
|
+
buildings.forEach(b => {
|
|
198
|
+
options.push({ value: b, label: b })
|
|
199
|
+
})
|
|
200
|
+
return options
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
interface LocationGroup {
|
|
204
|
+
parentName: string
|
|
205
|
+
locations: Location[]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const filteredLocations = computed(() => {
|
|
209
|
+
let locs = allLeafLocations.value
|
|
210
|
+
if (filterType.value) {
|
|
211
|
+
locs = locs.filter(l => l.type === filterType.value)
|
|
212
|
+
}
|
|
213
|
+
if (filterBuilding.value) {
|
|
214
|
+
locs = locs.filter(l =>
|
|
215
|
+
l.path && l.path.length > 0 && l.path[0].name === filterBuilding.value
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
return locs
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const groupedLocations = computed<LocationGroup[]>(() => {
|
|
222
|
+
const groups = new Map<string, Location[]>()
|
|
223
|
+
|
|
224
|
+
for (const loc of filteredLocations.value) {
|
|
225
|
+
const parentName = loc.path && loc.path.length >= 2
|
|
226
|
+
? loc.path[loc.path.length - 2].name
|
|
227
|
+
: loc.path && loc.path.length === 1
|
|
228
|
+
? loc.path[0].name
|
|
229
|
+
: 'Ungrouped'
|
|
230
|
+
if (!groups.has(parentName)) {
|
|
231
|
+
groups.set(parentName, [])
|
|
232
|
+
}
|
|
233
|
+
groups.get(parentName)!.push(loc)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return Array.from(groups.entries())
|
|
237
|
+
.map(([parentName, locs]) => ({ parentName, locations: locs }))
|
|
238
|
+
.sort((a, b) => a.parentName.localeCompare(b.parentName))
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const quickSelectors = computed(() => [
|
|
242
|
+
{ label: 'All Shelves', action: () => selectByType('shelf') },
|
|
243
|
+
{ label: 'All Racks', action: () => selectByType('rack') },
|
|
244
|
+
{ label: 'All Bins', action: () => selectByType('bin') },
|
|
245
|
+
{ label: 'Select All', action: selectAll },
|
|
246
|
+
{ label: 'Deselect All', action: deselectAll },
|
|
247
|
+
])
|
|
248
|
+
|
|
249
|
+
onMounted(async () => {
|
|
250
|
+
loadingLocations.value = true
|
|
251
|
+
try {
|
|
252
|
+
await fetchRoots()
|
|
253
|
+
allLeafLocations.value = flattenLeafLocations(locations.value)
|
|
254
|
+
// Expand all groups by default
|
|
255
|
+
groupedLocations.value.forEach(g => expandedGroups.add(g.parentName))
|
|
256
|
+
} finally {
|
|
257
|
+
loadingLocations.value = false
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
function flattenLeafLocations(locs: Location[]): Location[] {
|
|
262
|
+
const result: Location[] = []
|
|
263
|
+
function walk(nodes: Location[], path: Location['path']) {
|
|
264
|
+
for (const node of nodes) {
|
|
265
|
+
const currentPath = [...(path || []), {
|
|
266
|
+
id: node.id,
|
|
267
|
+
name: node.name,
|
|
268
|
+
code: node.code,
|
|
269
|
+
full_code: node.full_code,
|
|
270
|
+
type: node.type,
|
|
271
|
+
}]
|
|
272
|
+
if (node.children && node.children.length > 0) {
|
|
273
|
+
walk(node.children, currentPath)
|
|
274
|
+
} else {
|
|
275
|
+
result.push({ ...node, path: currentPath })
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
walk(locs, [])
|
|
280
|
+
return result
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function toggleGroup(name: string) {
|
|
284
|
+
if (expandedGroups.has(name)) {
|
|
285
|
+
expandedGroups.delete(name)
|
|
286
|
+
} else {
|
|
287
|
+
expandedGroups.add(name)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function toggleLocation(id: number) {
|
|
292
|
+
if (selectedIds.has(id)) {
|
|
293
|
+
selectedIds.delete(id)
|
|
294
|
+
} else {
|
|
295
|
+
selectedIds.add(id)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isGroupFullySelected(group: LocationGroup): boolean {
|
|
300
|
+
return group.locations.every(l => selectedIds.has(l.id))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isGroupPartiallySelected(group: LocationGroup): boolean {
|
|
304
|
+
const count = group.locations.filter(l => selectedIds.has(l.id)).length
|
|
305
|
+
return count > 0 && count < group.locations.length
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function toggleGroupSelection(group: LocationGroup) {
|
|
309
|
+
if (isGroupFullySelected(group)) {
|
|
310
|
+
group.locations.forEach(l => selectedIds.delete(l.id))
|
|
311
|
+
} else {
|
|
312
|
+
group.locations.forEach(l => selectedIds.add(l.id))
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function selectByType(type: LocationType) {
|
|
317
|
+
filteredLocations.value
|
|
318
|
+
.filter(l => l.type === type)
|
|
319
|
+
.forEach(l => selectedIds.add(l.id))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function selectAll() {
|
|
323
|
+
filteredLocations.value.forEach(l => selectedIds.add(l.id))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function deselectAll() {
|
|
327
|
+
selectedIds.clear()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function handlePrint() {
|
|
331
|
+
if (selectedIds.size === 0) return
|
|
332
|
+
printing.value = true
|
|
333
|
+
try {
|
|
334
|
+
await printBatch(Array.from(selectedIds), selectedFormat.value as LabelFormat)
|
|
335
|
+
} finally {
|
|
336
|
+
printing.value = false
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function handlePreview() {
|
|
341
|
+
// Preview is the same as print but without recording
|
|
342
|
+
handlePrint()
|
|
343
|
+
}
|
|
344
|
+
</script>
|
|
345
|
+
|
|
346
|
+
<style scoped>
|
|
347
|
+
.label-batch {
|
|
348
|
+
max-width: 800px;
|
|
349
|
+
display: flex;
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
gap: var(--space-5, 1.25rem);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.label-batch__header {
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
justify-content: space-between;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.label-batch__title {
|
|
361
|
+
margin: 0;
|
|
362
|
+
font-size: var(--text-xl, 1.25rem);
|
|
363
|
+
font-weight: 700;
|
|
364
|
+
color: var(--admin-text-primary);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.label-batch__format {
|
|
368
|
+
max-width: 200px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.label-batch__quick-selectors {
|
|
372
|
+
display: flex;
|
|
373
|
+
flex-wrap: wrap;
|
|
374
|
+
gap: var(--space-2, 0.5rem);
|
|
375
|
+
margin-bottom: var(--space-4, 1rem);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.label-batch__quick-btn {
|
|
379
|
+
padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
|
|
380
|
+
border: 1px solid var(--admin-border);
|
|
381
|
+
border-radius: 6px;
|
|
382
|
+
background: var(--admin-card-bg);
|
|
383
|
+
color: var(--admin-text-primary);
|
|
384
|
+
font-size: var(--text-sm, 0.875rem);
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
transition: background-color 0.15s ease, border-color 0.15s ease;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.label-batch__quick-btn:hover {
|
|
390
|
+
background: var(--admin-content-bg);
|
|
391
|
+
border-color: var(--admin-text-tertiary);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.label-batch__quick-btn:focus-visible {
|
|
395
|
+
outline: 2px solid var(--admin-focus-ring, #2563eb);
|
|
396
|
+
outline-offset: 2px;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.label-batch__filter-row {
|
|
400
|
+
display: flex;
|
|
401
|
+
gap: var(--space-3, 0.75rem);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.label-batch__filter-row > * {
|
|
405
|
+
flex: 1;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.label-batch__group {
|
|
409
|
+
border-bottom: 1px solid var(--admin-border);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.label-batch__group:last-child {
|
|
413
|
+
border-bottom: none;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.label-batch__group-header {
|
|
417
|
+
display: flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
gap: var(--space-3, 0.75rem);
|
|
420
|
+
width: 100%;
|
|
421
|
+
padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
|
|
422
|
+
border: none;
|
|
423
|
+
background: var(--admin-table-header-bg);
|
|
424
|
+
color: var(--admin-text-primary);
|
|
425
|
+
font-size: var(--text-sm, 0.875rem);
|
|
426
|
+
font-weight: 600;
|
|
427
|
+
cursor: pointer;
|
|
428
|
+
text-align: left;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.label-batch__group-header:hover {
|
|
432
|
+
background: var(--admin-content-bg);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.label-batch__group-header:focus-visible {
|
|
436
|
+
outline: 2px solid var(--admin-focus-ring, #2563eb);
|
|
437
|
+
outline-offset: -2px;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.label-batch__group-name {
|
|
441
|
+
flex: 1;
|
|
442
|
+
min-width: 0;
|
|
443
|
+
overflow: hidden;
|
|
444
|
+
text-overflow: ellipsis;
|
|
445
|
+
white-space: nowrap;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.label-batch__group-count {
|
|
449
|
+
font-size: var(--text-xs, 0.75rem);
|
|
450
|
+
color: var(--admin-text-tertiary);
|
|
451
|
+
font-weight: 400;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.label-batch__group-chevron {
|
|
455
|
+
color: var(--admin-text-tertiary);
|
|
456
|
+
transition: transform 0.2s ease;
|
|
457
|
+
flex-shrink: 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.label-batch__group-chevron--collapsed {
|
|
461
|
+
transform: rotate(-90deg);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.label-batch__group-items {
|
|
465
|
+
padding: 0;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.label-batch__location-row {
|
|
469
|
+
display: flex;
|
|
470
|
+
align-items: center;
|
|
471
|
+
gap: var(--space-3, 0.75rem);
|
|
472
|
+
padding: var(--space-2, 0.5rem) var(--space-4, 1rem) var(--space-2, 0.5rem) var(--space-8, 2rem);
|
|
473
|
+
cursor: pointer;
|
|
474
|
+
transition: background-color 0.1s ease;
|
|
475
|
+
border-bottom: 1px solid var(--admin-border);
|
|
476
|
+
font-size: var(--text-sm, 0.875rem);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.label-batch__location-row:last-child {
|
|
480
|
+
border-bottom: none;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.label-batch__location-row:hover {
|
|
484
|
+
background: var(--admin-content-bg);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.label-batch__checkbox {
|
|
488
|
+
width: 18px;
|
|
489
|
+
height: 18px;
|
|
490
|
+
accent-color: var(--color-primary, #2563eb);
|
|
491
|
+
cursor: pointer;
|
|
492
|
+
flex-shrink: 0;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.label-batch__location-name {
|
|
496
|
+
flex: 1;
|
|
497
|
+
min-width: 0;
|
|
498
|
+
overflow: hidden;
|
|
499
|
+
text-overflow: ellipsis;
|
|
500
|
+
white-space: nowrap;
|
|
501
|
+
color: var(--admin-text-primary);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.label-batch__footer {
|
|
505
|
+
position: sticky;
|
|
506
|
+
bottom: 0;
|
|
507
|
+
display: flex;
|
|
508
|
+
align-items: center;
|
|
509
|
+
justify-content: space-between;
|
|
510
|
+
gap: var(--space-3, 0.75rem);
|
|
511
|
+
padding: var(--space-4, 1rem);
|
|
512
|
+
background: var(--admin-card-bg);
|
|
513
|
+
border: 1px solid var(--admin-border);
|
|
514
|
+
border-radius: 8px;
|
|
515
|
+
box-shadow: var(--shadow-md);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.label-batch__selected-count {
|
|
519
|
+
font-size: var(--text-sm, 0.875rem);
|
|
520
|
+
font-weight: 600;
|
|
521
|
+
color: var(--admin-text-primary);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.label-batch__footer-actions {
|
|
525
|
+
display: flex;
|
|
526
|
+
gap: var(--space-2, 0.5rem);
|
|
527
|
+
}
|
|
528
|
+
</style>
|