@godxjp/ui 19.2.0 → 19.3.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/dist/components/data-display/badge.d.ts +18 -2
- package/dist/components/data-display/badge.js +18 -4
- package/dist/props/components/data-display.prop.d.ts +7 -0
- package/dist/props/registry.d.ts +5 -1
- package/dist/props/registry.js +12 -1
- package/dist/styles/badge-layout.css +15 -0
- package/dist/tokens/components/badge.css +8 -0
- package/docs/data-display/badge.tsx +36 -0
- package/docs/data-display/carousel.tsx +8 -4
- package/docs/data-display/data-table/examples/grid-features.tsx +1 -1
- package/docs/data-display/permission-matrix.tsx +13 -9
- package/docs/data-display/popover.tsx +2 -1
- package/docs/data-display/scroll-area.tsx +107 -75
- package/docs/data-display/table.tsx +1 -1
- package/docs/data-entry/calendar.tsx +1 -1
- package/docs/data-entry/checkbox.tsx +1 -1
- package/docs/data-entry/country-picker-recipe.tsx +11 -6
- package/docs/data-entry/form-field/index.tsx +33 -29
- package/docs/data-entry/form-root.tsx +1 -1
- package/docs/data-entry/password-input.tsx +1 -5
- package/docs/data-entry/select.tsx +7 -7
- package/docs/data-entry/textarea.tsx +2 -2
- package/docs/feedback/sheet.tsx +112 -102
- package/docs/feedback/skeleton.tsx +2 -2
- package/docs/foundation/colors.tsx +8 -6
- package/docs/foundation/email-tokens.tsx +82 -65
- package/docs/foundation/radius-shadow.tsx +13 -11
- package/docs/layout/app-shell.tsx +2 -2
- package/docs/layout/aspect-ratio.tsx +30 -14
- package/docs/layout/flex.tsx +6 -2
- package/docs/layout/page-container.tsx +9 -1
- package/docs/layout/responsive-grid.tsx +10 -2
- package/docs/layout/sidebar.tsx +169 -146
- package/docs/layout/split-pane.tsx +44 -27
- package/docs/layout/topbar.tsx +17 -15
- package/docs/navigation/app-setting-picker.tsx +5 -4
- package/docs/navigation/breadcrumb.tsx +12 -5
- package/docs/navigation/context-menu.tsx +10 -4
- package/docs/navigation/navigation-menu.tsx +89 -96
- package/docs/navigation/steps.tsx +12 -10
- package/docs/navigation/tabs.tsx +26 -20
- package/docs/navigation/toolbar.tsx +15 -15
- package/docs/providers/format-date.tsx +2 -2
- package/docs/showcase/acme-portal.tsx +41 -27
- package/docs/showcase/acme-website.tsx +60 -54
- package/docs/showcase/case1-warehouse-dashboard.tsx +46 -30
- package/docs/showcase/case2-employee-me.tsx +23 -20
- package/docs/showcase/case3-approval-workflow.tsx +34 -32
- package/docs/showcase/case4-login.tsx +119 -113
- package/docs/showcase/case5-shift-calendar.tsx +3 -3
- package/docs/showcase/case6-agency-handy.tsx +340 -299
- package/docs/showcase/futurelastic-web.tsx +23 -21
- package/docs/showcase/org-switcher.tsx +2 -2
- package/docs/showcase/permission-matrix.tsx +7 -4
- package/docs/showcase/table-conditional-format.tsx +8 -8
- package/docs/showcase/table-crud-list.tsx +3 -3
- package/docs/showcase/table-density.tsx +49 -42
- package/docs/showcase/table-expandable-rows.tsx +24 -15
- package/docs/showcase/table-filter-chips.tsx +2 -7
- package/docs/showcase/table-footer-totals.tsx +7 -4
- package/docs/showcase/table-grouped-subtotals.tsx +25 -22
- package/docs/showcase/table-master-detail.tsx +15 -10
- package/docs/showcase/table-pagination.tsx +34 -27
- package/docs/showcase/table-states.tsx +8 -5
- package/docs/showcase/table-sticky-columns.tsx +8 -5
- package/package.json +2 -3
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
} from "lucide-react";
|
|
44
44
|
|
|
45
45
|
import { Button, Heading, Text } from "@godxjp/ui/general";
|
|
46
|
-
import { Flex } from "@godxjp/ui/layout";
|
|
46
|
+
import { Flex, ResponsiveGrid } from "@godxjp/ui/layout";
|
|
47
47
|
import {
|
|
48
48
|
Badge,
|
|
49
49
|
type BadgeProps,
|
|
@@ -249,32 +249,36 @@ function ItemListCard({
|
|
|
249
249
|
: "hover:border-primary")
|
|
250
250
|
}
|
|
251
251
|
>
|
|
252
|
-
<CardContent solo
|
|
253
|
-
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
252
|
+
<CardContent solo>
|
|
253
|
+
<Flex align="start" gap="md">
|
|
254
|
+
<Flex direction="col" gap="xs">
|
|
255
|
+
{selectMode ? (
|
|
256
|
+
<Checkbox
|
|
257
|
+
checked={selected}
|
|
258
|
+
onCheckedChange={onToggle}
|
|
259
|
+
aria-label={`選択 ${item.name}`}
|
|
260
|
+
className="size-5"
|
|
261
|
+
/>
|
|
262
|
+
) : null}
|
|
263
|
+
<Flex direction="col" gap="sm" className="min-w-0 flex-1">
|
|
264
|
+
<Text as="div" size="sm" weight="bold" truncate>
|
|
265
|
+
{item.name}
|
|
266
|
+
</Text>
|
|
267
|
+
<Flex wrap align="center" gap="sm">
|
|
268
|
+
<MonoCode>{item.rc}</MonoCode>
|
|
269
|
+
<MonoCode>JAN {item.jan}</MonoCode>
|
|
270
|
+
</Flex>
|
|
271
|
+
<Flex align="center" justify="between" gap="sm">
|
|
272
|
+
<Badge tone={st.tone} variant="outline" className="rounded-full">
|
|
273
|
+
{st.label}
|
|
274
|
+
</Badge>
|
|
275
|
+
<Text size="xs" tone="muted" tabular className="shrink-0">
|
|
276
|
+
×{item.qty} · {item.receivedAt}
|
|
277
|
+
</Text>
|
|
278
|
+
</Flex>
|
|
279
|
+
</Flex>
|
|
276
280
|
</Flex>
|
|
277
|
-
</
|
|
281
|
+
</Flex>
|
|
278
282
|
</CardContent>
|
|
279
283
|
</Card>
|
|
280
284
|
);
|
|
@@ -293,22 +297,24 @@ function PackingListCard({ packing, onTap }: { packing: Packing; onTap?: () => v
|
|
|
293
297
|
}
|
|
294
298
|
>
|
|
295
299
|
<CardContent solo>
|
|
296
|
-
<Flex
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
{st.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
<
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
300
|
+
<Flex direction="col" gap="xs">
|
|
301
|
+
<Flex align="center" justify="between" gap="sm">
|
|
302
|
+
<Text as="code" size="sm" weight="bold" tabular>
|
|
303
|
+
{packing.code}
|
|
304
|
+
</Text>
|
|
305
|
+
<Badge tone={st.tone} variant="outline" className="rounded-full">
|
|
306
|
+
{st.label}
|
|
307
|
+
</Badge>
|
|
308
|
+
</Flex>
|
|
309
|
+
<Flex align="center" justify="between" gap="sm">
|
|
310
|
+
<Text size="xs" tone="muted" truncate>
|
|
311
|
+
{packing.customer} · {packing.city}
|
|
312
|
+
</Text>
|
|
313
|
+
<Text size="xs" tone="muted" tabular className="shrink-0">
|
|
314
|
+
×{packing.items}
|
|
315
|
+
{packing.slot ? ` · ${packing.slot}` : ""}
|
|
316
|
+
</Text>
|
|
317
|
+
</Flex>
|
|
312
318
|
</Flex>
|
|
313
319
|
</CardContent>
|
|
314
320
|
</Card>
|
|
@@ -348,14 +354,14 @@ function PhoneFrame({ children }: { children: React.ReactNode }) {
|
|
|
348
354
|
|
|
349
355
|
function StatusBar() {
|
|
350
356
|
return (
|
|
351
|
-
<
|
|
357
|
+
<Flex align="center" justify="between" gap="xs" className="h-9 shrink-0 px-5">
|
|
352
358
|
<Text size="sm" weight="medium" tabular>
|
|
353
359
|
9:41
|
|
354
360
|
</Text>
|
|
355
361
|
<Text size="sm" weight="medium" tabular tone="muted">
|
|
356
362
|
Acme Handy
|
|
357
363
|
</Text>
|
|
358
|
-
</
|
|
364
|
+
</Flex>
|
|
359
365
|
);
|
|
360
366
|
}
|
|
361
367
|
|
|
@@ -401,48 +407,50 @@ function ItemLookupSheet({
|
|
|
401
407
|
<SheetHeader>
|
|
402
408
|
<SheetTitle>Quét hoặc nhập mã</SheetTitle>
|
|
403
409
|
</SheetHeader>
|
|
404
|
-
<SheetBody
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
<
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
410
|
+
<SheetBody>
|
|
411
|
+
<Flex direction="col" gap="md">
|
|
412
|
+
{/* Viewfinder placeholder — a Card surface, not a hand-rolled illustration */}
|
|
413
|
+
<Flex
|
|
414
|
+
align="center"
|
|
415
|
+
justify="center"
|
|
416
|
+
className="border-border bg-secondary/40 h-40 rounded-xl border-2 border-dashed"
|
|
417
|
+
>
|
|
418
|
+
<Flex direction="col" align="center" gap="sm">
|
|
419
|
+
<ScanLine
|
|
420
|
+
className="size-7 text-[color:var(--attention,var(--warning))]"
|
|
421
|
+
aria-hidden="true"
|
|
422
|
+
strokeWidth={1.5}
|
|
423
|
+
/>
|
|
424
|
+
<Text size="sm" tone="muted">
|
|
425
|
+
Đưa mã vạch vào khung
|
|
426
|
+
</Text>
|
|
427
|
+
</Flex>
|
|
420
428
|
</Flex>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
</
|
|
429
|
+
<Flex direction="col" gap="sm">
|
|
430
|
+
<Text size="sm" weight="medium">
|
|
431
|
+
Hoặc nhập mã thủ công
|
|
432
|
+
</Text>
|
|
433
|
+
<Flex gap="sm">
|
|
434
|
+
<Input
|
|
435
|
+
value={code}
|
|
436
|
+
onChange={(e) => setCode(e.target.value)}
|
|
437
|
+
placeholder="RC- / PKG- / JAN"
|
|
438
|
+
className="font-mono"
|
|
439
|
+
inputMode="text"
|
|
440
|
+
autoComplete="off"
|
|
441
|
+
/>
|
|
442
|
+
<Button
|
|
443
|
+
onClick={() => {
|
|
444
|
+
onOpenChange(false);
|
|
445
|
+
setCode("");
|
|
446
|
+
toast.success("Đã tìm thấy RC-204881 · Sữa rửa mặt Hada Labo");
|
|
447
|
+
}}
|
|
448
|
+
disabled={code.trim() === ""}
|
|
449
|
+
>
|
|
450
|
+
<Search aria-hidden="true" />
|
|
451
|
+
Tìm
|
|
452
|
+
</Button>
|
|
453
|
+
</Flex>
|
|
446
454
|
</Flex>
|
|
447
455
|
</Flex>
|
|
448
456
|
</SheetBody>
|
|
@@ -471,29 +479,31 @@ function PackagePickerSheet({
|
|
|
471
479
|
<SheetHeader>
|
|
472
480
|
<SheetTitle>Gán vào kiện</SheetTitle>
|
|
473
481
|
</SheetHeader>
|
|
474
|
-
<SheetBody
|
|
475
|
-
<
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
482
|
+
<SheetBody>
|
|
483
|
+
<Flex direction="col" gap="sm">
|
|
484
|
+
<SectionHeader count={PACKINGS.length}>Kiện đang mở</SectionHeader>
|
|
485
|
+
{PACKINGS.map((p) => (
|
|
486
|
+
<PackingListCard
|
|
487
|
+
key={p.id}
|
|
488
|
+
packing={p}
|
|
489
|
+
onTap={() => {
|
|
490
|
+
onOpenChange(false);
|
|
491
|
+
toast.success(`Đã gán ${count} item vào ${p.code}`);
|
|
492
|
+
}}
|
|
493
|
+
/>
|
|
494
|
+
))}
|
|
495
|
+
<Button
|
|
496
|
+
variant="outline"
|
|
497
|
+
onClick={() => {
|
|
481
498
|
onOpenChange(false);
|
|
482
|
-
toast.success(`Đã
|
|
499
|
+
toast.success(`Đã tạo kiện mới với ${count} item`);
|
|
483
500
|
}}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
toast.success(`Đã tạo kiện mới với ${count} item`);
|
|
491
|
-
}}
|
|
492
|
-
className="text-primary hover:border-primary h-11 w-full border-2 border-dashed"
|
|
493
|
-
>
|
|
494
|
-
<PackagePlus aria-hidden="true" strokeWidth={1.5} />
|
|
495
|
-
Tạo kiện mới với {count} item này
|
|
496
|
-
</Button>
|
|
501
|
+
className="text-primary hover:border-primary h-11 w-full border-2 border-dashed"
|
|
502
|
+
>
|
|
503
|
+
<PackagePlus aria-hidden="true" strokeWidth={1.5} />
|
|
504
|
+
Tạo kiện mới với {count} item này
|
|
505
|
+
</Button>
|
|
506
|
+
</Flex>
|
|
497
507
|
</SheetBody>
|
|
498
508
|
<SheetFooter>
|
|
499
509
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
@@ -527,63 +537,65 @@ function ItemFormSheet({
|
|
|
527
537
|
<SheetHeader>
|
|
528
538
|
<SheetTitle>Thêm hàng mới</SheetTitle>
|
|
529
539
|
</SheetHeader>
|
|
530
|
-
<SheetBody
|
|
531
|
-
<Flex direction="col" gap="
|
|
532
|
-
<
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
<
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
<
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
<
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
{
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
<
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
540
|
+
<SheetBody>
|
|
541
|
+
<Flex direction="col" gap="md">
|
|
542
|
+
<Flex direction="col" gap="xs">
|
|
543
|
+
<Text size="sm" weight="medium">
|
|
544
|
+
Mã vạch
|
|
545
|
+
</Text>
|
|
546
|
+
<Input defaultValue="JAN 4987241135219" readOnly className="font-mono" />
|
|
547
|
+
</Flex>
|
|
548
|
+
<Flex direction="col" gap="xs">
|
|
549
|
+
<Text size="sm" weight="medium">
|
|
550
|
+
Tên hàng (tùy chọn)
|
|
551
|
+
</Text>
|
|
552
|
+
<Input placeholder="Nhập tên hàng" autoComplete="off" />
|
|
553
|
+
</Flex>
|
|
554
|
+
<Flex direction="col" gap="xs">
|
|
555
|
+
<Text size="sm" weight="medium">
|
|
556
|
+
Số lượng
|
|
557
|
+
</Text>
|
|
558
|
+
<Input
|
|
559
|
+
type="number"
|
|
560
|
+
value={qty}
|
|
561
|
+
min={1}
|
|
562
|
+
onChange={(e) => setQty(e.target.value)}
|
|
563
|
+
className="w-24 tabular-nums"
|
|
564
|
+
/>
|
|
565
|
+
</Flex>
|
|
566
|
+
<Flex direction="col" gap="sm">
|
|
567
|
+
<Text size="sm" weight="medium">
|
|
568
|
+
Đích đến
|
|
569
|
+
</Text>
|
|
570
|
+
<RadioGroupRoot value={dest} onValueChange={setDest} className="flex flex-col gap-2">
|
|
571
|
+
{DESTINATIONS.map((d) => {
|
|
572
|
+
const checked = dest === d.id;
|
|
573
|
+
const rowId = `handy-dest-${d.id}`;
|
|
574
|
+
return (
|
|
575
|
+
<label
|
|
576
|
+
key={d.id}
|
|
577
|
+
htmlFor={rowId}
|
|
578
|
+
className={
|
|
579
|
+
"flex cursor-pointer items-center gap-3 rounded-xl border p-3 transition-colors " +
|
|
580
|
+
(checked
|
|
581
|
+
? "border-primary bg-[color-mix(in_oklch,var(--primary)_8%,transparent)]"
|
|
582
|
+
: "hover:border-primary")
|
|
583
|
+
}
|
|
584
|
+
>
|
|
585
|
+
<RadioItem id={rowId} value={d.id} className="mt-0.5" />
|
|
586
|
+
<span className="min-w-0">
|
|
587
|
+
<Text as="span" size="sm" weight="medium" className="block">
|
|
588
|
+
{d.label}
|
|
589
|
+
</Text>
|
|
590
|
+
<Text as="span" size="xs" tone="muted" className="block">
|
|
591
|
+
{d.hint}
|
|
592
|
+
</Text>
|
|
593
|
+
</span>
|
|
594
|
+
</label>
|
|
595
|
+
);
|
|
596
|
+
})}
|
|
597
|
+
</RadioGroupRoot>
|
|
598
|
+
</Flex>
|
|
587
599
|
</Flex>
|
|
588
600
|
</SheetBody>
|
|
589
601
|
<SheetFooter className="flex-row gap-2">
|
|
@@ -642,7 +654,7 @@ function InboundTab({
|
|
|
642
654
|
|
|
643
655
|
return (
|
|
644
656
|
<>
|
|
645
|
-
<
|
|
657
|
+
<Flex direction="col" gap="md" className="flex-1 overflow-y-auto p-4">
|
|
646
658
|
{/* Filter chips — horizontal scroll, count pills */}
|
|
647
659
|
<ToggleGroup
|
|
648
660
|
type="single"
|
|
@@ -650,20 +662,22 @@ function InboundTab({
|
|
|
650
662
|
onValueChange={(v) => {
|
|
651
663
|
if (v) setFilter(v);
|
|
652
664
|
}}
|
|
653
|
-
className="
|
|
665
|
+
className="w-full overflow-x-auto"
|
|
654
666
|
>
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
+
<Flex justify="start" gap="sm">
|
|
668
|
+
{FILTERS.map((f) => (
|
|
669
|
+
<ToggleGroupItem
|
|
670
|
+
key={f.id}
|
|
671
|
+
value={f.id}
|
|
672
|
+
className="h-9 shrink-0 gap-1.5 rounded-full px-3 whitespace-nowrap text-[var(--font-size-xs)]"
|
|
673
|
+
>
|
|
674
|
+
{f.label}
|
|
675
|
+
<Text size="xs" mono tabular className="opacity-70">
|
|
676
|
+
{f.count}
|
|
677
|
+
</Text>
|
|
678
|
+
</ToggleGroupItem>
|
|
679
|
+
))}
|
|
680
|
+
</Flex>
|
|
667
681
|
</ToggleGroup>
|
|
668
682
|
|
|
669
683
|
{state === "loading" ? (
|
|
@@ -671,9 +685,11 @@ function InboundTab({
|
|
|
671
685
|
{Array.from({ length: 3 }).map((_, i) => (
|
|
672
686
|
<Card key={i} density="tight" className="rounded-xl">
|
|
673
687
|
<CardContent solo>
|
|
674
|
-
<
|
|
675
|
-
|
|
676
|
-
|
|
688
|
+
<Flex direction="col" gap="md">
|
|
689
|
+
<Skeleton className="h-4 w-3/5" />
|
|
690
|
+
<Skeleton className="h-3 w-2/5" />
|
|
691
|
+
<Skeleton className="h-5 w-24 rounded-full" />
|
|
692
|
+
</Flex>
|
|
677
693
|
</CardContent>
|
|
678
694
|
</Card>
|
|
679
695
|
))}
|
|
@@ -681,12 +697,14 @@ function InboundTab({
|
|
|
681
697
|
) : state === "error" ? (
|
|
682
698
|
<Alert tone="warning">
|
|
683
699
|
<AlertTitle>Không tải được danh sách</AlertTitle>
|
|
684
|
-
<AlertDescription
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
<
|
|
688
|
-
|
|
689
|
-
|
|
700
|
+
<AlertDescription>
|
|
701
|
+
<Flex direction="col" align="start" gap="sm">
|
|
702
|
+
Kiểm tra kết nối rồi thử lại.
|
|
703
|
+
<Button variant="outline" size="sm" onClick={onRetry}>
|
|
704
|
+
<RefreshCw aria-hidden="true" />
|
|
705
|
+
Thử lại
|
|
706
|
+
</Button>
|
|
707
|
+
</Flex>
|
|
690
708
|
</AlertDescription>
|
|
691
709
|
</Alert>
|
|
692
710
|
) : state === "empty" || visible.length === 0 ? (
|
|
@@ -721,12 +739,16 @@ function InboundTab({
|
|
|
721
739
|
))}
|
|
722
740
|
</Flex>
|
|
723
741
|
)}
|
|
724
|
-
</
|
|
742
|
+
</Flex>
|
|
725
743
|
|
|
726
744
|
{/* Sticky action bar (scan-first) OR select-mode contextual bar */}
|
|
727
745
|
{selectMode ? (
|
|
728
|
-
<
|
|
729
|
-
|
|
746
|
+
<Flex
|
|
747
|
+
direction="col"
|
|
748
|
+
gap="sm"
|
|
749
|
+
className="shrink-0 border-t bg-[color-mix(in_oklch,var(--primary)_5%,var(--background))] p-3"
|
|
750
|
+
>
|
|
751
|
+
<Flex align="center" justify="between">
|
|
730
752
|
<Text size="sm" tabular>
|
|
731
753
|
<Text as="span" weight="bold">
|
|
732
754
|
{selected.size}
|
|
@@ -753,9 +775,9 @@ function InboundTab({
|
|
|
753
775
|
Kiện mới
|
|
754
776
|
</Button>
|
|
755
777
|
</Flex>
|
|
756
|
-
</
|
|
778
|
+
</Flex>
|
|
757
779
|
) : (
|
|
758
|
-
<
|
|
780
|
+
<Flex gap="sm" className="shrink-0 border-t p-3">
|
|
759
781
|
<Button className="flex-[2]" onClick={onScan}>
|
|
760
782
|
<ScanLine aria-hidden="true" />
|
|
761
783
|
Quét / Tìm mã
|
|
@@ -764,7 +786,7 @@ function InboundTab({
|
|
|
764
786
|
<Plus aria-hidden="true" />
|
|
765
787
|
Thêm hàng
|
|
766
788
|
</Button>
|
|
767
|
-
</
|
|
789
|
+
</Flex>
|
|
768
790
|
)}
|
|
769
791
|
</>
|
|
770
792
|
);
|
|
@@ -775,7 +797,7 @@ function PackingTab({ onScan }: { onScan: () => void }) {
|
|
|
775
797
|
const others = PACKINGS.filter((p) => p.status !== "active");
|
|
776
798
|
return (
|
|
777
799
|
<>
|
|
778
|
-
<
|
|
800
|
+
<Flex direction="col" gap="md" className="flex-1 overflow-y-auto p-4">
|
|
779
801
|
{active ? (
|
|
780
802
|
<Card
|
|
781
803
|
density="tight"
|
|
@@ -783,20 +805,27 @@ function PackingTab({ onScan }: { onScan: () => void }) {
|
|
|
783
805
|
className="border-primary rounded-xl bg-[color-mix(in_oklch,var(--primary)_6%,transparent)]"
|
|
784
806
|
>
|
|
785
807
|
<CardContent solo>
|
|
786
|
-
<Flex
|
|
787
|
-
<
|
|
788
|
-
|
|
808
|
+
<Flex direction="col" gap="xs">
|
|
809
|
+
<Flex align="center" justify="between">
|
|
810
|
+
<Text
|
|
811
|
+
size="2xs"
|
|
812
|
+
weight="medium"
|
|
813
|
+
tone="muted"
|
|
814
|
+
className="tracking-wider uppercase"
|
|
815
|
+
>
|
|
816
|
+
Kiện đang làm
|
|
817
|
+
</Text>
|
|
818
|
+
<Badge tone="info" variant="outline" className="rounded-full">
|
|
819
|
+
{PACKING_STATUS.active.label}
|
|
820
|
+
</Badge>
|
|
821
|
+
</Flex>
|
|
822
|
+
<Text as="div" size="lg" weight="bold" mono tabular>
|
|
823
|
+
{active.code}
|
|
824
|
+
</Text>
|
|
825
|
+
<Text size="xs" tone="muted" tabular as="div">
|
|
826
|
+
{active.customer} · {active.city} · ×{active.items} · {active.slot}
|
|
789
827
|
</Text>
|
|
790
|
-
<Badge tone="info" variant="outline" className="rounded-full">
|
|
791
|
-
{PACKING_STATUS.active.label}
|
|
792
|
-
</Badge>
|
|
793
828
|
</Flex>
|
|
794
|
-
<Text as="div" size="lg" weight="bold" mono tabular className="mt-1">
|
|
795
|
-
{active.code}
|
|
796
|
-
</Text>
|
|
797
|
-
<Text size="xs" tone="muted" tabular as="div" className="mt-0.5">
|
|
798
|
-
{active.customer} · {active.city} · ×{active.items} · {active.slot}
|
|
799
|
-
</Text>
|
|
800
829
|
</CardContent>
|
|
801
830
|
</Card>
|
|
802
831
|
) : null}
|
|
@@ -807,8 +836,8 @@ function PackingTab({ onScan }: { onScan: () => void }) {
|
|
|
807
836
|
<PackingListCard key={p.id} packing={p} onTap={() => undefined} />
|
|
808
837
|
))}
|
|
809
838
|
</Flex>
|
|
810
|
-
</
|
|
811
|
-
<
|
|
839
|
+
</Flex>
|
|
840
|
+
<Flex direction="col" gap="sm" className="shrink-0 border-t p-3">
|
|
812
841
|
<Button onClick={onScan}>
|
|
813
842
|
<ScanLine aria-hidden="true" />
|
|
814
843
|
Quét item vào kiện
|
|
@@ -817,7 +846,7 @@ function PackingTab({ onScan }: { onScan: () => void }) {
|
|
|
817
846
|
<Plus aria-hidden="true" />
|
|
818
847
|
Tạo kiện trống
|
|
819
848
|
</Button>
|
|
820
|
-
</
|
|
849
|
+
</Flex>
|
|
821
850
|
</>
|
|
822
851
|
);
|
|
823
852
|
}
|
|
@@ -826,7 +855,7 @@ function OutboundTab({ onSeal, onHandoff }: { onSeal: () => void; onHandoff: ()
|
|
|
826
855
|
const [seg, setSeg] = React.useState<string>("seal");
|
|
827
856
|
return (
|
|
828
857
|
<>
|
|
829
|
-
<
|
|
858
|
+
<Flex direction="col" gap="md" className="flex-1 overflow-y-auto p-4">
|
|
830
859
|
{/* Segmented — outbound status */}
|
|
831
860
|
<ToggleGroup
|
|
832
861
|
type="single"
|
|
@@ -834,26 +863,28 @@ function OutboundTab({ onSeal, onHandoff }: { onSeal: () => void; onHandoff: ()
|
|
|
834
863
|
onValueChange={(v) => {
|
|
835
864
|
if (v) setSeg(v);
|
|
836
865
|
}}
|
|
837
|
-
className="bg-secondary/60
|
|
866
|
+
className="bg-secondary/60 w-full rounded-xl p-1"
|
|
838
867
|
>
|
|
839
|
-
<
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
868
|
+
<ResponsiveGrid columns={3} gap="xs">
|
|
869
|
+
<ToggleGroupItem
|
|
870
|
+
value="seal"
|
|
871
|
+
className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
|
|
872
|
+
>
|
|
873
|
+
Chờ niêm phong
|
|
874
|
+
</ToggleGroupItem>
|
|
875
|
+
<ToggleGroupItem
|
|
876
|
+
value="handoff"
|
|
877
|
+
className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
|
|
878
|
+
>
|
|
879
|
+
Chờ bàn giao
|
|
880
|
+
</ToggleGroupItem>
|
|
881
|
+
<ToggleGroupItem
|
|
882
|
+
value="done"
|
|
883
|
+
className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
|
|
884
|
+
>
|
|
885
|
+
Đã bàn giao
|
|
886
|
+
</ToggleGroupItem>
|
|
887
|
+
</ResponsiveGrid>
|
|
857
888
|
</ToggleGroup>
|
|
858
889
|
|
|
859
890
|
<SectionHeader count={OUTBOUND.length}>
|
|
@@ -867,38 +898,40 @@ function OutboundTab({ onSeal, onHandoff }: { onSeal: () => void; onHandoff: ()
|
|
|
867
898
|
{OUTBOUND.map((p) => (
|
|
868
899
|
<Card key={p.id} density="tight" className="rounded-xl">
|
|
869
900
|
<CardContent solo>
|
|
870
|
-
<Flex
|
|
871
|
-
<
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
901
|
+
<Flex direction="col" gap="md">
|
|
902
|
+
<Flex align="center" justify="between" gap="sm">
|
|
903
|
+
<Text as="code" size="sm" weight="bold" tabular>
|
|
904
|
+
{p.code}
|
|
905
|
+
</Text>
|
|
906
|
+
<Badge tone="success" variant="outline" className="rounded-full">
|
|
907
|
+
Sẵn sàng niêm phong
|
|
908
|
+
</Badge>
|
|
909
|
+
</Flex>
|
|
910
|
+
<Flex align="center" justify="between" gap="sm">
|
|
911
|
+
<Text size="xs" tone="muted" tabular truncate>
|
|
912
|
+
{p.customer} · {p.city}
|
|
913
|
+
</Text>
|
|
914
|
+
<Text size="xs" tone="muted" tabular className="shrink-0">
|
|
915
|
+
×{p.items} · {p.slot}
|
|
916
|
+
</Text>
|
|
917
|
+
</Flex>
|
|
918
|
+
<div>
|
|
919
|
+
<Descriptions columns={1} className="gap-y-1">
|
|
920
|
+
<Descriptions.Item label="Vị trí" mono>
|
|
921
|
+
{p.slot}
|
|
922
|
+
</Descriptions.Item>
|
|
923
|
+
<Descriptions.Item label="Số kiện" mono>
|
|
924
|
+
×{p.items}
|
|
925
|
+
</Descriptions.Item>
|
|
926
|
+
</Descriptions>
|
|
927
|
+
</div>
|
|
877
928
|
</Flex>
|
|
878
|
-
<Flex align="center" justify="between" gap="sm" className="mt-1">
|
|
879
|
-
<Text size="xs" tone="muted" tabular truncate>
|
|
880
|
-
{p.customer} · {p.city}
|
|
881
|
-
</Text>
|
|
882
|
-
<Text size="xs" tone="muted" tabular className="shrink-0">
|
|
883
|
-
×{p.items} · {p.slot}
|
|
884
|
-
</Text>
|
|
885
|
-
</Flex>
|
|
886
|
-
<div className="mt-3">
|
|
887
|
-
<Descriptions columns={1} className="gap-y-1">
|
|
888
|
-
<Descriptions.Item label="Vị trí" mono>
|
|
889
|
-
{p.slot}
|
|
890
|
-
</Descriptions.Item>
|
|
891
|
-
<Descriptions.Item label="Số kiện" mono>
|
|
892
|
-
×{p.items}
|
|
893
|
-
</Descriptions.Item>
|
|
894
|
-
</Descriptions>
|
|
895
|
-
</div>
|
|
896
929
|
</CardContent>
|
|
897
930
|
</Card>
|
|
898
931
|
))}
|
|
899
932
|
</Flex>
|
|
900
|
-
</
|
|
901
|
-
<
|
|
933
|
+
</Flex>
|
|
934
|
+
<Flex gap="sm" className="shrink-0 border-t p-3">
|
|
902
935
|
{seg === "handoff" ? (
|
|
903
936
|
<Button className="flex-1" onClick={onHandoff}>
|
|
904
937
|
<Truck aria-hidden="true" />
|
|
@@ -910,7 +943,7 @@ function OutboundTab({ onSeal, onHandoff }: { onSeal: () => void; onHandoff: ()
|
|
|
910
943
|
Quét mã kiện · Niêm phong
|
|
911
944
|
</Button>
|
|
912
945
|
)}
|
|
913
|
-
</
|
|
946
|
+
</Flex>
|
|
914
947
|
</>
|
|
915
948
|
);
|
|
916
949
|
}
|
|
@@ -940,53 +973,61 @@ export default function AgencyHandyShowcase() {
|
|
|
940
973
|
<StatusBar />
|
|
941
974
|
|
|
942
975
|
{/* App header (52px) — title + iOS text-action select-mode entry (inbound only) */}
|
|
943
|
-
<header className="
|
|
944
|
-
<
|
|
945
|
-
{
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
976
|
+
<header className="h-14 shrink-0 border-b px-4">
|
|
977
|
+
<Flex align="center" justify="between" gap="xs">
|
|
978
|
+
<Heading level={3} as="h1" className="whitespace-nowrap">
|
|
979
|
+
{headerTitle}
|
|
980
|
+
</Heading>
|
|
981
|
+
<Flex align="center" gap="xs">
|
|
982
|
+
{tab === "inbound" && !selectMode ? (
|
|
983
|
+
<Button
|
|
984
|
+
variant="ghost"
|
|
985
|
+
size="sm"
|
|
986
|
+
onClick={() => {
|
|
987
|
+
setSelectMode(true);
|
|
988
|
+
setSelected(new Set());
|
|
989
|
+
}}
|
|
990
|
+
>
|
|
991
|
+
<Check aria-hidden="true" />
|
|
992
|
+
Chọn
|
|
993
|
+
</Button>
|
|
994
|
+
) : null}
|
|
995
|
+
{/* State switcher — exposes the 4 list states at rest (showcase affordance) */}
|
|
996
|
+
{tab === "inbound" && !selectMode ? (
|
|
997
|
+
<ToggleGroup
|
|
998
|
+
type="single"
|
|
999
|
+
value={listState}
|
|
1000
|
+
onValueChange={(v) => {
|
|
1001
|
+
if (v) setListState(v as ListState);
|
|
1002
|
+
}}
|
|
1003
|
+
aria-label="List state (showcase)"
|
|
1004
|
+
>
|
|
1005
|
+
<Flex gap="xs">
|
|
1006
|
+
{(["ready", "loading", "empty", "error"] as const).map((s) => (
|
|
1007
|
+
<ToggleGroupItem
|
|
1008
|
+
key={s}
|
|
1009
|
+
value={s}
|
|
1010
|
+
className="size-7 rounded-md p-0 text-[var(--font-size-2xs)] uppercase"
|
|
1011
|
+
title={s}
|
|
1012
|
+
>
|
|
1013
|
+
{s[0]}
|
|
1014
|
+
</ToggleGroupItem>
|
|
1015
|
+
))}
|
|
1016
|
+
</Flex>
|
|
1017
|
+
</ToggleGroup>
|
|
1018
|
+
) : null}
|
|
1019
|
+
</Flex>
|
|
984
1020
|
</Flex>
|
|
985
1021
|
</header>
|
|
986
1022
|
|
|
987
1023
|
{/* Select-mode header strip — replaces the standard header context (inbound) */}
|
|
988
1024
|
{selectMode ? (
|
|
989
|
-
<
|
|
1025
|
+
<Flex
|
|
1026
|
+
align="center"
|
|
1027
|
+
justify="between"
|
|
1028
|
+
gap="xs"
|
|
1029
|
+
className="h-10 shrink-0 border-b bg-[color-mix(in_oklch,var(--primary)_5%,var(--background))] px-3"
|
|
1030
|
+
>
|
|
990
1031
|
<Button
|
|
991
1032
|
variant="ghost"
|
|
992
1033
|
size="icon-sm"
|
|
@@ -1008,7 +1049,7 @@ export default function AgencyHandyShowcase() {
|
|
|
1008
1049
|
>
|
|
1009
1050
|
Chọn tất cả
|
|
1010
1051
|
</Button>
|
|
1011
|
-
</
|
|
1052
|
+
</Flex>
|
|
1012
1053
|
) : null}
|
|
1013
1054
|
|
|
1014
1055
|
{tab === "inbound" ? (
|