@godxjp/ui 19.3.1 → 19.4.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/dist/components/data-display/badge.d.ts +9 -1
- package/dist/components/data-display/badge.js +3 -2
- package/dist/components/data-display/index.d.ts +2 -0
- package/dist/components/data-display/index.js +2 -0
- package/dist/components/data-display/popover.d.ts +13 -1
- package/dist/components/data-display/popover.js +3 -1
- package/dist/components/data-display/table.d.ts +19 -2
- package/dist/components/data-display/table.js +4 -1
- package/dist/components/data-display/timeline-grid.d.ts +18 -0
- package/dist/components/data-display/timeline-grid.js +222 -0
- package/dist/components/data-entry/cascader.js +4 -6
- package/dist/components/data-entry/checkbox.js +1 -1
- package/dist/components/data-entry/search-select.js +5 -7
- package/dist/components/data-entry/select.js +32 -28
- package/dist/components/data-entry/tree-select.js +4 -6
- package/dist/components/general/inert-background.d.ts +5 -0
- package/dist/components/general/inert-background.js +48 -0
- package/dist/components/layout/auth-shell.d.ts +4 -2
- package/dist/components/layout/auth-shell.js +7 -1
- package/dist/components/layout/centered-shell.d.ts +2 -2
- package/dist/components/layout/flex.d.ts +1 -1
- package/dist/components/layout/flex.js +2 -1
- package/dist/components/layout/index.d.ts +3 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/components/layout/mobile-shell.d.ts +21 -0
- package/dist/components/layout/mobile-shell.js +43 -0
- package/dist/components/layout/sidebar-link.d.ts +1 -1
- package/dist/components/navigation/context-menu.js +13 -9
- package/dist/components/navigation/dropdown-menu.js +17 -13
- package/dist/inertia/index.d.ts +1 -1
- package/dist/lib/variants.d.ts +3 -3
- package/dist/lib/variants.js +1 -0
- package/dist/props/components/data-display.prop.d.ts +65 -0
- package/dist/props/components/data-entry.prop.d.ts +1 -1
- package/dist/props/components/layout.prop.d.ts +93 -6
- package/dist/props/registry.d.ts +109 -2
- package/dist/props/registry.js +143 -0
- package/dist/props/vocabulary/data.prop.d.ts +6 -0
- package/dist/props/vocabulary/index.d.ts +3 -3
- package/dist/props/vocabulary/layout.prop.d.ts +17 -2
- package/dist/props/vocabulary/shared.prop.d.ts +7 -0
- package/dist/styles/control.css +18 -1
- package/dist/styles/data-display-layout.css +167 -0
- package/dist/styles/layout.css +7 -0
- package/dist/styles/shell-layout.css +148 -0
- package/dist/styles/table-layout.css +23 -4
- package/dist/tokens/components/control.css +1 -0
- package/dist/tokens/components/data-display.css +20 -0
- package/dist/tokens/components/shell.css +22 -0
- package/dist/tokens/components/table.css +2 -0
- package/docs/SPACING.md +19 -6
- package/docs/charts/compact-bar-trend.tsx +5 -6
- package/docs/data-display/avatar.tsx +7 -9
- package/docs/data-display/badge.tsx +1 -1
- package/docs/data-display/card/index.tsx +6 -4
- package/docs/data-display/data-table/examples/approval-queue.tsx +1 -1
- package/docs/data-display/data-table/index.tsx +1 -1
- package/docs/data-display/permission-matrix.tsx +2 -2
- package/docs/data-display/scroll-area.tsx +1 -1
- package/docs/data-display/table.tsx +1 -1
- package/docs/data-display/timeline-grid.tsx +119 -0
- package/docs/data-entry/branch-scope-picker.tsx +1 -1
- package/docs/data-entry/input-otp.tsx +1 -1
- package/docs/data-entry/textarea.tsx +1 -1
- package/docs/data-entry/toggle-count.tsx +1 -1
- package/docs/feedback/alert.tsx +1 -1
- package/docs/feedback/banner.tsx +1 -1
- package/docs/foundation/email-tokens.tsx +1 -1
- package/docs/general/activity.tsx +1 -1
- package/docs/layout/auth-account-summary.tsx +1 -1
- package/docs/layout/auth-recovery/examples/mobile-390.tsx +1 -1
- package/docs/layout/auth-recovery/examples/password-recovery.tsx +0 -2
- package/docs/layout/auth-shell-context.tsx +1 -1
- package/docs/layout/auth-shell-device.tsx +2 -2
- package/docs/layout/auth-shell-registration.tsx +1 -1
- package/docs/layout/auth-shell.tsx +1 -1
- package/docs/layout/master-detail.tsx +2 -2
- package/docs/layout/mobile-shell.tsx +101 -0
- package/docs/layout/page-container.tsx +6 -6
- package/docs/layout/responsive-grid.tsx +2 -2
- package/docs/layout/separator.tsx +2 -2
- package/docs/layout/service-role-panel.tsx +1 -1
- package/docs/layout/sidebar.tsx +6 -6
- package/docs/layout/split-pane.tsx +1 -1
- package/docs/layout/topbar.tsx +2 -8
- package/docs/navigation/filter-bar.tsx +2 -2
- package/docs/navigation/pagination.tsx +2 -2
- package/docs/navigation/steps.tsx +1 -1
- package/docs/navigation/tabs.tsx +1 -1
- package/docs/showcase/acme-portal.tsx +11 -12
- package/docs/showcase/acme-website.tsx +7 -7
- package/docs/showcase/case1-warehouse-dashboard.tsx +11 -12
- package/docs/showcase/case2-employee-me.tsx +10 -16
- package/docs/showcase/case4-login.tsx +144 -156
- package/docs/showcase/case5-shift-calendar.tsx +181 -280
- package/docs/showcase/case6-agency-handy.tsx +446 -443
- package/docs/showcase/futurelastic-web.tsx +9 -8
- package/docs/showcase/org-switcher.tsx +18 -11
- package/docs/showcase/permission-matrix.tsx +13 -13
- package/docs/showcase/public-landing.tsx +1 -1
- package/docs/showcase/settings-account-sections.tsx +2 -2
- package/docs/showcase/settings-security-mfa.tsx +3 -3
- package/docs/showcase/table-approval-queue.tsx +1 -1
- package/docs/showcase/table-bulk-actions.tsx +49 -52
- package/docs/showcase/table-conditional-format.tsx +1 -1
- package/docs/showcase/table-crud-list.tsx +1 -1
- package/docs/showcase/table-expandable-rows.tsx +101 -98
- package/docs/showcase/table-filter-chips.tsx +78 -66
- package/docs/showcase/table-footer-totals.tsx +23 -19
- package/docs/showcase/table-grouped-subtotals.tsx +7 -4
- package/docs/showcase/table-master-detail.tsx +12 -14
- package/docs/showcase/table-states.tsx +34 -29
- package/docs/showcase/table-sticky-columns.tsx +3 -2
- package/docs/showcase/table-tree-rows.tsx +22 -15
- package/docs/showcase/table-view-tabs.tsx +12 -14
- package/package.json +7 -6
|
@@ -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, ResponsiveGrid } from "@godxjp/ui/layout";
|
|
46
|
+
import { Flex, MobileShell, ResponsiveGrid } from "@godxjp/ui/layout";
|
|
47
47
|
import {
|
|
48
48
|
Badge,
|
|
49
49
|
type BadgeProps,
|
|
@@ -52,14 +52,7 @@ import {
|
|
|
52
52
|
Descriptions,
|
|
53
53
|
EmptyState,
|
|
54
54
|
} from "@godxjp/ui/data-display";
|
|
55
|
-
import {
|
|
56
|
-
Checkbox,
|
|
57
|
-
Input,
|
|
58
|
-
RadioGroupRoot,
|
|
59
|
-
RadioItem,
|
|
60
|
-
ToggleGroup,
|
|
61
|
-
ToggleGroupItem,
|
|
62
|
-
} from "@godxjp/ui/data-entry";
|
|
55
|
+
import { Checkbox, Input, RadioGroup, ToggleGroup, ToggleGroupItem } from "@godxjp/ui/data-entry";
|
|
63
56
|
import {
|
|
64
57
|
AlertDialog,
|
|
65
58
|
Alert,
|
|
@@ -324,7 +317,7 @@ function PackingListCard({ packing, onTap }: { packing: Packing; onTap?: () => v
|
|
|
324
317
|
/** Three uppercase muted section header + right-aligned mono count. */
|
|
325
318
|
function SectionHeader({ children, count }: { children: React.ReactNode; count?: number }) {
|
|
326
319
|
return (
|
|
327
|
-
<Flex align="center" justify="between"
|
|
320
|
+
<Flex align="center" justify="between">
|
|
328
321
|
<Text size="2xs" weight="medium" tone="muted" className="tracking-wider uppercase">
|
|
329
322
|
{children}
|
|
330
323
|
</Text>
|
|
@@ -339,35 +332,32 @@ function SectionHeader({ children, count }: { children: React.ReactNode; count?:
|
|
|
339
332
|
|
|
340
333
|
// ── Phone shell ──────────────────────────────────────────────────────────────
|
|
341
334
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
{children}
|
|
350
|
-
</Flex>
|
|
351
|
-
</div>
|
|
352
|
-
);
|
|
353
|
-
}
|
|
335
|
+
/**
|
|
336
|
+
* gh#354 §6 — this file used to BUILD the shell: a `Card` + `CardContent flush` phone frame, a
|
|
337
|
+
* `ui-card-inset-x h-9` status row, an `h-14 border-b` header, a `flex-1 overflow-y-auto` body, a
|
|
338
|
+
* `shrink-0 border-t` action strip and a `ResponsiveGrid` tab bar. It reproduced the look and
|
|
339
|
+
* neither behaviour that matters on a device — the document still scrolled, and nothing padded out
|
|
340
|
+
* of `env(safe-area-inset-*)`. All five bands are now `MobileShell` slots.
|
|
341
|
+
*/
|
|
354
342
|
|
|
355
343
|
function StatusBar() {
|
|
356
344
|
return (
|
|
357
|
-
|
|
345
|
+
<>
|
|
358
346
|
<Text size="sm" weight="medium" tabular>
|
|
359
347
|
9:41
|
|
360
348
|
</Text>
|
|
361
349
|
<Text size="sm" weight="medium" tabular tone="muted">
|
|
362
350
|
Acme Handy
|
|
363
351
|
</Text>
|
|
364
|
-
|
|
352
|
+
</>
|
|
365
353
|
);
|
|
366
354
|
}
|
|
367
355
|
|
|
368
356
|
function TabBar({ active, onChange }: { active: string; onChange: (id: string) => void }) {
|
|
357
|
+
// The tab bar's own tiling — equal width, no seam, no gutter — is the `tabBar` slot's contract,
|
|
358
|
+
// so there is no grid and no column count here any more.
|
|
369
359
|
return (
|
|
370
|
-
|
|
360
|
+
<>
|
|
371
361
|
{TABS.map((t) => {
|
|
372
362
|
const isActive = t.id === active;
|
|
373
363
|
const Icon = t.icon;
|
|
@@ -378,7 +368,7 @@ function TabBar({ active, onChange }: { active: string; onChange: (id: string) =
|
|
|
378
368
|
onClick={() => onChange(t.id)}
|
|
379
369
|
aria-current={isActive ? "page" : undefined}
|
|
380
370
|
className={
|
|
381
|
-
"h-
|
|
371
|
+
"h-full flex-col rounded-none font-medium text-[var(--font-size-2xs)] " +
|
|
382
372
|
(isActive ? "text-primary" : "text-muted-foreground")
|
|
383
373
|
}
|
|
384
374
|
>
|
|
@@ -387,7 +377,7 @@ function TabBar({ active, onChange }: { active: string; onChange: (id: string) =
|
|
|
387
377
|
</Button>
|
|
388
378
|
);
|
|
389
379
|
})}
|
|
390
|
-
|
|
380
|
+
</>
|
|
391
381
|
);
|
|
392
382
|
}
|
|
393
383
|
|
|
@@ -410,22 +400,20 @@ function ItemLookupSheet({
|
|
|
410
400
|
<SheetBody>
|
|
411
401
|
<Flex direction="col" gap="md">
|
|
412
402
|
{/* Viewfinder placeholder — a Card surface, not a hand-rolled illustration */}
|
|
413
|
-
<
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
</Flex>
|
|
428
|
-
</Flex>
|
|
403
|
+
<Card variant="muted" className="border-2 border-dashed">
|
|
404
|
+
<CardContent solo>
|
|
405
|
+
<Flex direction="col" align="center" gap="sm" className="h-40 justify-center">
|
|
406
|
+
<ScanLine
|
|
407
|
+
className="size-7 text-[color:var(--attention,var(--warning))]"
|
|
408
|
+
aria-hidden="true"
|
|
409
|
+
strokeWidth={1.5}
|
|
410
|
+
/>
|
|
411
|
+
<Text size="sm" tone="muted">
|
|
412
|
+
Đưa mã vạch vào khung
|
|
413
|
+
</Text>
|
|
414
|
+
</Flex>
|
|
415
|
+
</CardContent>
|
|
416
|
+
</Card>
|
|
429
417
|
<Flex direction="col" gap="sm">
|
|
430
418
|
<Text size="sm" weight="medium">
|
|
431
419
|
Hoặc nhập mã thủ công
|
|
@@ -567,38 +555,21 @@ function ItemFormSheet({
|
|
|
567
555
|
<Text size="sm" weight="medium">
|
|
568
556
|
Đích đến
|
|
569
557
|
</Text>
|
|
570
|
-
<
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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>
|
|
558
|
+
<RadioGroup
|
|
559
|
+
id="handy-dest"
|
|
560
|
+
value={dest}
|
|
561
|
+
onValueChange={setDest}
|
|
562
|
+
orientation="vertical"
|
|
563
|
+
options={DESTINATIONS.map((d) => ({
|
|
564
|
+
value: d.id,
|
|
565
|
+
label: d.label,
|
|
566
|
+
description: d.hint,
|
|
567
|
+
}))}
|
|
568
|
+
/>
|
|
598
569
|
</Flex>
|
|
599
570
|
</Flex>
|
|
600
571
|
</SheetBody>
|
|
601
|
-
<SheetFooter className="flex-row
|
|
572
|
+
<SheetFooter className="flex-row">
|
|
602
573
|
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>
|
|
603
574
|
Huỷ
|
|
604
575
|
</Button>
|
|
@@ -621,26 +592,24 @@ function ItemFormSheet({
|
|
|
621
592
|
|
|
622
593
|
type ListState = "ready" | "loading" | "empty" | "error";
|
|
623
594
|
|
|
595
|
+
/**
|
|
596
|
+
* The SCROLL REGION of the inbound screen — and nothing else. The sticky bar and the tab bar are
|
|
597
|
+
* `MobileShell` bands now, so this component no longer owns a scroll box, a `flex-1`, or a border.
|
|
598
|
+
*/
|
|
624
599
|
function InboundTab({
|
|
625
600
|
state,
|
|
626
601
|
onRetry,
|
|
627
602
|
selectMode,
|
|
628
|
-
setSelectMode,
|
|
629
603
|
selected,
|
|
630
604
|
setSelected,
|
|
631
605
|
onScan,
|
|
632
|
-
onAdd,
|
|
633
|
-
onAssign,
|
|
634
606
|
}: {
|
|
635
607
|
state: ListState;
|
|
636
608
|
onRetry: () => void;
|
|
637
609
|
selectMode: boolean;
|
|
638
|
-
setSelectMode: (v: boolean) => void;
|
|
639
610
|
selected: Set<string>;
|
|
640
611
|
setSelected: React.Dispatch<React.SetStateAction<Set<string>>>;
|
|
641
612
|
onScan: () => void;
|
|
642
|
-
onAdd: () => void;
|
|
643
|
-
onAssign: () => void;
|
|
644
613
|
}) {
|
|
645
614
|
const [filter, setFilter] = React.useState<string>("all");
|
|
646
615
|
|
|
@@ -653,298 +622,317 @@ function InboundTab({
|
|
|
653
622
|
);
|
|
654
623
|
|
|
655
624
|
return (
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
>
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
</ToggleGroup>
|
|
625
|
+
<Flex direction="col" gap="md">
|
|
626
|
+
{/* Filter chips — horizontal scroll, count pills */}
|
|
627
|
+
<ToggleGroup
|
|
628
|
+
type="single"
|
|
629
|
+
value={filter}
|
|
630
|
+
onValueChange={(v) => {
|
|
631
|
+
if (v) setFilter(v);
|
|
632
|
+
}}
|
|
633
|
+
className="w-full overflow-x-auto"
|
|
634
|
+
>
|
|
635
|
+
<Flex justify="start" gap="sm">
|
|
636
|
+
{FILTERS.map((f) => (
|
|
637
|
+
<ToggleGroupItem
|
|
638
|
+
key={f.id}
|
|
639
|
+
value={f.id}
|
|
640
|
+
className="h-9 shrink-0 rounded-full whitespace-nowrap text-[var(--font-size-xs)]"
|
|
641
|
+
>
|
|
642
|
+
{f.label}
|
|
643
|
+
<Text size="xs" mono tabular className="opacity-70">
|
|
644
|
+
{f.count}
|
|
645
|
+
</Text>
|
|
646
|
+
</ToggleGroupItem>
|
|
647
|
+
))}
|
|
648
|
+
</Flex>
|
|
649
|
+
</ToggleGroup>
|
|
682
650
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
</Button>
|
|
707
|
-
</Flex>
|
|
708
|
-
</AlertDescription>
|
|
709
|
-
</Alert>
|
|
710
|
-
) : state === "empty" || visible.length === 0 ? (
|
|
711
|
-
<EmptyState
|
|
712
|
-
icon={Boxes}
|
|
713
|
-
title="Chưa có hàng trong mục này"
|
|
714
|
-
description="Quét mã để nhập hàng mới vào kho."
|
|
715
|
-
action={
|
|
716
|
-
<Button size="sm" onClick={onScan}>
|
|
717
|
-
<ScanLine aria-hidden="true" />
|
|
718
|
-
Quét / Tìm mã
|
|
651
|
+
{state === "loading" ? (
|
|
652
|
+
<Flex direction="col" gap="md" aria-busy="true">
|
|
653
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
654
|
+
<Card key={i} density="tight" className="rounded-xl">
|
|
655
|
+
<CardContent solo>
|
|
656
|
+
<Flex direction="col" gap="md">
|
|
657
|
+
<Skeleton className="h-4 w-3/5" />
|
|
658
|
+
<Skeleton className="h-3 w-2/5" />
|
|
659
|
+
<Skeleton className="h-5 w-24 rounded-full" />
|
|
660
|
+
</Flex>
|
|
661
|
+
</CardContent>
|
|
662
|
+
</Card>
|
|
663
|
+
))}
|
|
664
|
+
</Flex>
|
|
665
|
+
) : state === "error" ? (
|
|
666
|
+
<Alert tone="warning">
|
|
667
|
+
<AlertTitle>Không tải được danh sách</AlertTitle>
|
|
668
|
+
<AlertDescription>
|
|
669
|
+
<Flex direction="col" align="start" gap="sm">
|
|
670
|
+
Kiểm tra kết nối rồi thử lại.
|
|
671
|
+
<Button variant="outline" size="sm" onClick={onRetry}>
|
|
672
|
+
<RefreshCw aria-hidden="true" />
|
|
673
|
+
Thử lại
|
|
719
674
|
</Button>
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const next = new Set(prev);
|
|
733
|
-
if (next.has(item.id)) next.delete(item.id);
|
|
734
|
-
else next.add(item.id);
|
|
735
|
-
return next;
|
|
736
|
-
})
|
|
737
|
-
}
|
|
738
|
-
/>
|
|
739
|
-
))}
|
|
740
|
-
</Flex>
|
|
741
|
-
)}
|
|
742
|
-
</Flex>
|
|
743
|
-
|
|
744
|
-
{/* Sticky action bar (scan-first) OR select-mode contextual bar */}
|
|
745
|
-
{selectMode ? (
|
|
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">
|
|
752
|
-
<Text size="sm" tabular>
|
|
753
|
-
<Text as="span" weight="bold">
|
|
754
|
-
{selected.size}
|
|
755
|
-
</Text>{" "}
|
|
756
|
-
item đã chọn
|
|
757
|
-
</Text>
|
|
758
|
-
<Button
|
|
759
|
-
variant="ghost"
|
|
760
|
-
size="sm"
|
|
761
|
-
onClick={() => {
|
|
762
|
-
setSelectMode(false);
|
|
763
|
-
setSelected(new Set());
|
|
764
|
-
}}
|
|
765
|
-
>
|
|
766
|
-
Xong
|
|
767
|
-
</Button>
|
|
768
|
-
</Flex>
|
|
769
|
-
<Flex gap="sm">
|
|
770
|
-
<Button className="flex-[2]" disabled={selected.size === 0} onClick={onAssign}>
|
|
771
|
-
<Package aria-hidden="true" />
|
|
772
|
-
Gán vào kiện…
|
|
773
|
-
</Button>
|
|
774
|
-
<Button variant="outline" className="flex-1" disabled={selected.size === 0}>
|
|
775
|
-
Kiện mới
|
|
675
|
+
</Flex>
|
|
676
|
+
</AlertDescription>
|
|
677
|
+
</Alert>
|
|
678
|
+
) : state === "empty" || visible.length === 0 ? (
|
|
679
|
+
<EmptyState
|
|
680
|
+
icon={Boxes}
|
|
681
|
+
title="Chưa có hàng trong mục này"
|
|
682
|
+
description="Quét mã để nhập hàng mới vào kho."
|
|
683
|
+
action={
|
|
684
|
+
<Button size="sm" onClick={onScan}>
|
|
685
|
+
<ScanLine aria-hidden="true" />
|
|
686
|
+
Quét / Tìm mã
|
|
776
687
|
</Button>
|
|
777
|
-
|
|
778
|
-
|
|
688
|
+
}
|
|
689
|
+
/>
|
|
779
690
|
) : (
|
|
780
|
-
<Flex
|
|
781
|
-
|
|
782
|
-
<
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
691
|
+
<Flex direction="col" gap="md">
|
|
692
|
+
{visible.map((item) => (
|
|
693
|
+
<ItemListCard
|
|
694
|
+
key={item.id}
|
|
695
|
+
item={item}
|
|
696
|
+
selectMode={selectMode}
|
|
697
|
+
selected={selected.has(item.id)}
|
|
698
|
+
onToggle={() =>
|
|
699
|
+
setSelected((prev) => {
|
|
700
|
+
const next = new Set(prev);
|
|
701
|
+
if (next.has(item.id)) next.delete(item.id);
|
|
702
|
+
else next.add(item.id);
|
|
703
|
+
return next;
|
|
704
|
+
})
|
|
705
|
+
}
|
|
706
|
+
/>
|
|
707
|
+
))}
|
|
789
708
|
</Flex>
|
|
790
709
|
)}
|
|
791
|
-
|
|
710
|
+
</Flex>
|
|
792
711
|
);
|
|
793
712
|
}
|
|
794
713
|
|
|
795
|
-
|
|
714
|
+
/**
|
|
715
|
+
* The inbound screen's `actions` band: the scan-first pair at rest, the contextual pair in select
|
|
716
|
+
* mode. It lives OUTSIDE the scroll region, so it needs no `position: sticky` and no bottom padding
|
|
717
|
+
* on the list to clear it, and `MobileShell` gives it the home-indicator inset when no tab bar
|
|
718
|
+
* follows.
|
|
719
|
+
*/
|
|
720
|
+
function InboundActions({
|
|
721
|
+
selectMode,
|
|
722
|
+
selected,
|
|
723
|
+
onExitSelect,
|
|
724
|
+
onScan,
|
|
725
|
+
onAdd,
|
|
726
|
+
onAssign,
|
|
727
|
+
}: {
|
|
728
|
+
selectMode: boolean;
|
|
729
|
+
selected: Set<string>;
|
|
730
|
+
onExitSelect: () => void;
|
|
731
|
+
onScan: () => void;
|
|
732
|
+
onAdd: () => void;
|
|
733
|
+
onAssign: () => void;
|
|
734
|
+
}) {
|
|
735
|
+
if (!selectMode) {
|
|
736
|
+
return (
|
|
737
|
+
<>
|
|
738
|
+
<Button className="flex-[2]" onClick={onScan}>
|
|
739
|
+
<ScanLine aria-hidden="true" />
|
|
740
|
+
Quét / Tìm mã
|
|
741
|
+
</Button>
|
|
742
|
+
<Button variant="outline" className="flex-1" onClick={onAdd}>
|
|
743
|
+
<Plus aria-hidden="true" />
|
|
744
|
+
Thêm hàng
|
|
745
|
+
</Button>
|
|
746
|
+
</>
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
return (
|
|
750
|
+
<Flex direction="col" gap="sm" className="w-full">
|
|
751
|
+
<Flex align="center" justify="between">
|
|
752
|
+
<Text size="sm" tabular>
|
|
753
|
+
<Text as="span" weight="bold">
|
|
754
|
+
{selected.size}
|
|
755
|
+
</Text>{" "}
|
|
756
|
+
item đã chọn
|
|
757
|
+
</Text>
|
|
758
|
+
<Button variant="ghost" size="sm" onClick={onExitSelect}>
|
|
759
|
+
Xong
|
|
760
|
+
</Button>
|
|
761
|
+
</Flex>
|
|
762
|
+
<Flex gap="sm">
|
|
763
|
+
<Button className="flex-[2]" disabled={selected.size === 0} onClick={onAssign}>
|
|
764
|
+
<Package aria-hidden="true" />
|
|
765
|
+
Gán vào kiện…
|
|
766
|
+
</Button>
|
|
767
|
+
<Button variant="outline" className="flex-1" disabled={selected.size === 0}>
|
|
768
|
+
Kiện mới
|
|
769
|
+
</Button>
|
|
770
|
+
</Flex>
|
|
771
|
+
</Flex>
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function PackingTab() {
|
|
796
776
|
const active = PACKINGS.find((p) => p.status === "active");
|
|
797
777
|
const others = PACKINGS.filter((p) => p.status !== "active");
|
|
798
778
|
return (
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
779
|
+
<Flex direction="col" gap="md">
|
|
780
|
+
{active ? (
|
|
781
|
+
<Card
|
|
782
|
+
density="tight"
|
|
783
|
+
accent="primary"
|
|
784
|
+
className="border-primary bg-[color-mix(in_oklch,var(--primary)_6%,transparent)]"
|
|
785
|
+
>
|
|
786
|
+
<CardContent solo>
|
|
787
|
+
<Flex direction="col" gap="xs">
|
|
788
|
+
<Flex align="center" justify="between">
|
|
789
|
+
<Text size="2xs" weight="medium" tone="muted" className="tracking-wider uppercase">
|
|
790
|
+
Kiện đang làm
|
|
791
|
+
</Text>
|
|
792
|
+
<Badge tone="info" variant="outline" className="rounded-full">
|
|
793
|
+
{PACKING_STATUS.active.label}
|
|
794
|
+
</Badge>
|
|
795
|
+
</Flex>
|
|
796
|
+
<Text as="div" size="lg" weight="bold" mono tabular>
|
|
797
|
+
{active.code}
|
|
798
|
+
</Text>
|
|
799
|
+
<Text size="xs" tone="muted" tabular as="div">
|
|
800
|
+
{active.customer} · {active.city} · ×{active.items} · {active.slot}
|
|
801
|
+
</Text>
|
|
802
|
+
</Flex>
|
|
803
|
+
</CardContent>
|
|
804
|
+
</Card>
|
|
805
|
+
) : null}
|
|
806
|
+
|
|
807
|
+
<SectionHeader count={others.length}>Kiện đang mở</SectionHeader>
|
|
808
|
+
<Flex direction="col" gap="md">
|
|
809
|
+
{others.map((p) => (
|
|
810
|
+
<PackingListCard key={p.id} packing={p} onTap={() => undefined} />
|
|
811
|
+
))}
|
|
812
|
+
</Flex>
|
|
813
|
+
</Flex>
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function PackingActions({ onScan }: { onScan: () => void }) {
|
|
818
|
+
return (
|
|
819
|
+
<Flex direction="col" gap="sm" className="w-full">
|
|
820
|
+
<Button onClick={onScan}>
|
|
821
|
+
<ScanLine aria-hidden="true" />
|
|
822
|
+
Quét item vào kiện
|
|
823
|
+
</Button>
|
|
824
|
+
<Button variant="outline">
|
|
825
|
+
<Plus aria-hidden="true" />
|
|
826
|
+
Tạo kiện trống
|
|
827
|
+
</Button>
|
|
828
|
+
</Flex>
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* `seg` is lifted to the page: the segmented control lives in the scroll region while the verb it
|
|
834
|
+
* selects lives in the `actions` band, and the two are different MobileShell slots.
|
|
835
|
+
*/
|
|
836
|
+
function OutboundTab({ seg, setSeg }: { seg: string; setSeg: (v: string) => void }) {
|
|
837
|
+
return (
|
|
838
|
+
<Flex direction="col" gap="md">
|
|
839
|
+
{/* Segmented — outbound status */}
|
|
840
|
+
<ToggleGroup
|
|
841
|
+
type="single"
|
|
842
|
+
value={seg}
|
|
843
|
+
onValueChange={(v) => {
|
|
844
|
+
if (v) setSeg(v);
|
|
845
|
+
}}
|
|
846
|
+
className="bg-secondary/60 w-full rounded-xl"
|
|
847
|
+
>
|
|
848
|
+
<ResponsiveGrid columns={3} gap="xs">
|
|
849
|
+
<ToggleGroupItem
|
|
850
|
+
value="seal"
|
|
851
|
+
className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
|
|
852
|
+
>
|
|
853
|
+
Chờ niêm phong
|
|
854
|
+
</ToggleGroupItem>
|
|
855
|
+
<ToggleGroupItem
|
|
856
|
+
value="handoff"
|
|
857
|
+
className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
|
|
806
858
|
>
|
|
859
|
+
Chờ bàn giao
|
|
860
|
+
</ToggleGroupItem>
|
|
861
|
+
<ToggleGroupItem
|
|
862
|
+
value="done"
|
|
863
|
+
className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
|
|
864
|
+
>
|
|
865
|
+
Đã bàn giao
|
|
866
|
+
</ToggleGroupItem>
|
|
867
|
+
</ResponsiveGrid>
|
|
868
|
+
</ToggleGroup>
|
|
869
|
+
|
|
870
|
+
<SectionHeader count={OUTBOUND.length}>
|
|
871
|
+
{seg === "seal"
|
|
872
|
+
? "Sẵn sàng niêm phong"
|
|
873
|
+
: seg === "handoff"
|
|
874
|
+
? "Chờ bàn giao"
|
|
875
|
+
: "Đã bàn giao"}
|
|
876
|
+
</SectionHeader>
|
|
877
|
+
<Flex direction="col" gap="md">
|
|
878
|
+
{OUTBOUND.map((p) => (
|
|
879
|
+
<Card key={p.id} density="tight" className="rounded-xl">
|
|
807
880
|
<CardContent solo>
|
|
808
|
-
<Flex direction="col" gap="
|
|
809
|
-
<Flex align="center" justify="between">
|
|
810
|
-
<Text
|
|
811
|
-
|
|
812
|
-
weight="medium"
|
|
813
|
-
tone="muted"
|
|
814
|
-
className="tracking-wider uppercase"
|
|
815
|
-
>
|
|
816
|
-
Kiện đang làm
|
|
881
|
+
<Flex direction="col" gap="md">
|
|
882
|
+
<Flex align="center" justify="between" gap="sm">
|
|
883
|
+
<Text as="code" size="sm" weight="bold" tabular>
|
|
884
|
+
{p.code}
|
|
817
885
|
</Text>
|
|
818
|
-
<Badge tone="
|
|
819
|
-
|
|
886
|
+
<Badge tone="success" variant="outline" className="rounded-full">
|
|
887
|
+
Sẵn sàng niêm phong
|
|
820
888
|
</Badge>
|
|
821
889
|
</Flex>
|
|
822
|
-
<
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
890
|
+
<Flex align="center" justify="between" gap="sm">
|
|
891
|
+
<Text size="xs" tone="muted" tabular truncate>
|
|
892
|
+
{p.customer} · {p.city}
|
|
893
|
+
</Text>
|
|
894
|
+
<Text size="xs" tone="muted" tabular className="shrink-0">
|
|
895
|
+
×{p.items} · {p.slot}
|
|
896
|
+
</Text>
|
|
897
|
+
</Flex>
|
|
898
|
+
<div>
|
|
899
|
+
<Descriptions columns={1}>
|
|
900
|
+
<Descriptions.Item label="Vị trí" mono>
|
|
901
|
+
{p.slot}
|
|
902
|
+
</Descriptions.Item>
|
|
903
|
+
<Descriptions.Item label="Số kiện" mono>
|
|
904
|
+
×{p.items}
|
|
905
|
+
</Descriptions.Item>
|
|
906
|
+
</Descriptions>
|
|
907
|
+
</div>
|
|
828
908
|
</Flex>
|
|
829
909
|
</CardContent>
|
|
830
910
|
</Card>
|
|
831
|
-
)
|
|
832
|
-
|
|
833
|
-
<SectionHeader count={others.length}>Kiện đang mở</SectionHeader>
|
|
834
|
-
<Flex direction="col" gap="md">
|
|
835
|
-
{others.map((p) => (
|
|
836
|
-
<PackingListCard key={p.id} packing={p} onTap={() => undefined} />
|
|
837
|
-
))}
|
|
838
|
-
</Flex>
|
|
911
|
+
))}
|
|
839
912
|
</Flex>
|
|
840
|
-
|
|
841
|
-
<Button onClick={onScan}>
|
|
842
|
-
<ScanLine aria-hidden="true" />
|
|
843
|
-
Quét item vào kiện
|
|
844
|
-
</Button>
|
|
845
|
-
<Button variant="outline">
|
|
846
|
-
<Plus aria-hidden="true" />
|
|
847
|
-
Tạo kiện trống
|
|
848
|
-
</Button>
|
|
849
|
-
</Flex>
|
|
850
|
-
</>
|
|
913
|
+
</Flex>
|
|
851
914
|
);
|
|
852
915
|
}
|
|
853
916
|
|
|
854
|
-
function
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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>
|
|
888
|
-
</ToggleGroup>
|
|
889
|
-
|
|
890
|
-
<SectionHeader count={OUTBOUND.length}>
|
|
891
|
-
{seg === "seal"
|
|
892
|
-
? "Sẵn sàng niêm phong"
|
|
893
|
-
: seg === "handoff"
|
|
894
|
-
? "Chờ bàn giao"
|
|
895
|
-
: "Đã bàn giao"}
|
|
896
|
-
</SectionHeader>
|
|
897
|
-
<Flex direction="col" gap="md">
|
|
898
|
-
{OUTBOUND.map((p) => (
|
|
899
|
-
<Card key={p.id} density="tight" className="rounded-xl">
|
|
900
|
-
<CardContent solo>
|
|
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>
|
|
928
|
-
</Flex>
|
|
929
|
-
</CardContent>
|
|
930
|
-
</Card>
|
|
931
|
-
))}
|
|
932
|
-
</Flex>
|
|
933
|
-
</Flex>
|
|
934
|
-
<Flex gap="sm" className="shrink-0 border-t p-3">
|
|
935
|
-
{seg === "handoff" ? (
|
|
936
|
-
<Button className="flex-1" onClick={onHandoff}>
|
|
937
|
-
<Truck aria-hidden="true" />
|
|
938
|
-
Xác nhận bàn giao
|
|
939
|
-
</Button>
|
|
940
|
-
) : (
|
|
941
|
-
<Button className="flex-1" onClick={onSeal}>
|
|
942
|
-
<Check aria-hidden="true" />
|
|
943
|
-
Quét mã kiện · Niêm phong
|
|
944
|
-
</Button>
|
|
945
|
-
)}
|
|
946
|
-
</Flex>
|
|
947
|
-
</>
|
|
917
|
+
function OutboundActions({
|
|
918
|
+
seg,
|
|
919
|
+
onSeal,
|
|
920
|
+
onHandoff,
|
|
921
|
+
}: {
|
|
922
|
+
seg: string;
|
|
923
|
+
onSeal: () => void;
|
|
924
|
+
onHandoff: () => void;
|
|
925
|
+
}) {
|
|
926
|
+
return seg === "handoff" ? (
|
|
927
|
+
<Button className="flex-1" onClick={onHandoff}>
|
|
928
|
+
<Truck aria-hidden="true" />
|
|
929
|
+
Xác nhận bàn giao
|
|
930
|
+
</Button>
|
|
931
|
+
) : (
|
|
932
|
+
<Button className="flex-1" onClick={onSeal}>
|
|
933
|
+
<Check aria-hidden="true" />
|
|
934
|
+
Quét mã kiện · Niêm phong
|
|
935
|
+
</Button>
|
|
948
936
|
);
|
|
949
937
|
}
|
|
950
938
|
|
|
@@ -956,6 +944,7 @@ export default function AgencyHandyShowcase() {
|
|
|
956
944
|
|
|
957
945
|
const [selectMode, setSelectMode] = React.useState(false);
|
|
958
946
|
const [selected, setSelected] = React.useState<Set<string>>(new Set());
|
|
947
|
+
const [seg, setSeg] = React.useState<string>("seal");
|
|
959
948
|
|
|
960
949
|
const [lookupOpen, setLookupOpen] = React.useState(false);
|
|
961
950
|
const [formOpen, setFormOpen] = React.useState(false);
|
|
@@ -966,119 +955,133 @@ export default function AgencyHandyShowcase() {
|
|
|
966
955
|
|
|
967
956
|
const headerTitle = tab === "inbound" ? "Nhập kho" : tab === "packing" ? "Đóng gói" : "Xuất kho";
|
|
968
957
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
<StatusBar />
|
|
958
|
+
const exitSelect = () => {
|
|
959
|
+
setSelectMode(false);
|
|
960
|
+
setSelected(new Set());
|
|
961
|
+
};
|
|
974
962
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
963
|
+
// SELECT MODE REPLACES THE APP BAR — it does not stack a second strip under it. That is the
|
|
964
|
+
// platform pattern on both iOS and Android, and it is what the `header` slot is for: one bar to
|
|
965
|
+
// read at a time. Before gh#354 this file rendered the title bar AND a contextual strip, because
|
|
966
|
+
// there was no bar to swap.
|
|
967
|
+
const header = selectMode ? (
|
|
968
|
+
<Flex align="center" justify="between" gap="xs" className="w-full">
|
|
969
|
+
<Button variant="ghost" size="icon-sm" aria-label="Thoát chế độ chọn" onClick={exitSelect}>
|
|
970
|
+
<X className="size-4" aria-hidden="true" strokeWidth={1.5} />
|
|
971
|
+
</Button>
|
|
972
|
+
<Text size="sm" mono tabular>
|
|
973
|
+
{selected.size} đã chọn
|
|
974
|
+
</Text>
|
|
975
|
+
<Button
|
|
976
|
+
variant="ghost"
|
|
977
|
+
size="sm"
|
|
978
|
+
onClick={() => setSelected(new Set(ITEMS.map((i) => i.id)))}
|
|
979
|
+
>
|
|
980
|
+
Chọn tất cả
|
|
981
|
+
</Button>
|
|
982
|
+
</Flex>
|
|
983
|
+
) : (
|
|
984
|
+
<Flex align="center" justify="between" gap="xs" className="w-full">
|
|
985
|
+
<Heading level={3} as="h1" className="whitespace-nowrap">
|
|
986
|
+
{headerTitle}
|
|
987
|
+
</Heading>
|
|
988
|
+
<Flex align="center" gap="xs">
|
|
989
|
+
{tab === "inbound" ? (
|
|
990
|
+
<Button
|
|
991
|
+
variant="ghost"
|
|
992
|
+
size="sm"
|
|
993
|
+
onClick={() => {
|
|
994
|
+
setSelectMode(true);
|
|
995
|
+
setSelected(new Set());
|
|
996
|
+
}}
|
|
997
|
+
>
|
|
998
|
+
<Check aria-hidden="true" />
|
|
999
|
+
Chọn
|
|
1000
|
+
</Button>
|
|
1001
|
+
) : null}
|
|
1002
|
+
{/* State switcher — exposes the 4 list states at rest (showcase affordance) */}
|
|
1003
|
+
{tab === "inbound" ? (
|
|
1004
|
+
<ToggleGroup
|
|
1005
|
+
type="single"
|
|
1006
|
+
value={listState}
|
|
1007
|
+
onValueChange={(v) => {
|
|
1008
|
+
if (v) setListState(v as ListState);
|
|
1009
|
+
}}
|
|
1010
|
+
aria-label="List state (showcase)"
|
|
1011
|
+
>
|
|
1012
|
+
<Flex gap="xs">
|
|
1013
|
+
{(["ready", "loading", "empty", "error"] as const).map((s) => (
|
|
1014
|
+
<ToggleGroupItem
|
|
1015
|
+
key={s}
|
|
1016
|
+
value={s}
|
|
1017
|
+
className="size-7 rounded-md text-[var(--font-size-2xs)] uppercase"
|
|
1018
|
+
title={s}
|
|
1004
1019
|
>
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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}
|
|
1020
|
+
{s[0]}
|
|
1021
|
+
</ToggleGroupItem>
|
|
1022
|
+
))}
|
|
1019
1023
|
</Flex>
|
|
1020
|
-
</
|
|
1021
|
-
</header>
|
|
1022
|
-
|
|
1023
|
-
{/* Select-mode header strip — replaces the standard header context (inbound) */}
|
|
1024
|
-
{selectMode ? (
|
|
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
|
-
>
|
|
1031
|
-
<Button
|
|
1032
|
-
variant="ghost"
|
|
1033
|
-
size="icon-sm"
|
|
1034
|
-
aria-label="閉じる"
|
|
1035
|
-
onClick={() => {
|
|
1036
|
-
setSelectMode(false);
|
|
1037
|
-
setSelected(new Set());
|
|
1038
|
-
}}
|
|
1039
|
-
>
|
|
1040
|
-
<X className="size-4" aria-hidden="true" strokeWidth={1.5} />
|
|
1041
|
-
</Button>
|
|
1042
|
-
<Text size="sm" mono tabular>
|
|
1043
|
-
{selected.size} đã chọn
|
|
1044
|
-
</Text>
|
|
1045
|
-
<Button
|
|
1046
|
-
variant="ghost"
|
|
1047
|
-
size="sm"
|
|
1048
|
-
onClick={() => setSelected(new Set(ITEMS.map((i) => i.id)))}
|
|
1049
|
-
>
|
|
1050
|
-
Chọn tất cả
|
|
1051
|
-
</Button>
|
|
1052
|
-
</Flex>
|
|
1024
|
+
</ToggleGroup>
|
|
1053
1025
|
) : null}
|
|
1026
|
+
</Flex>
|
|
1027
|
+
</Flex>
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
const actions =
|
|
1031
|
+
tab === "inbound" ? (
|
|
1032
|
+
<InboundActions
|
|
1033
|
+
selectMode={selectMode}
|
|
1034
|
+
selected={selected}
|
|
1035
|
+
onExitSelect={exitSelect}
|
|
1036
|
+
onScan={() => setLookupOpen(true)}
|
|
1037
|
+
onAdd={() => setFormOpen(true)}
|
|
1038
|
+
onAssign={() => setPickerOpen(true)}
|
|
1039
|
+
/>
|
|
1040
|
+
) : tab === "packing" ? (
|
|
1041
|
+
<PackingActions onScan={() => setLookupOpen(true)} />
|
|
1042
|
+
) : (
|
|
1043
|
+
<OutboundActions
|
|
1044
|
+
seg={seg}
|
|
1045
|
+
onSeal={() => setSealOpen(true)}
|
|
1046
|
+
onHandoff={() => setHandoffOpen(true)}
|
|
1047
|
+
/>
|
|
1048
|
+
);
|
|
1054
1049
|
|
|
1050
|
+
return (
|
|
1051
|
+
<>
|
|
1052
|
+
<Toaster />
|
|
1053
|
+
{/* The whole phone is ONE primitive now: five bands, no Card frame, no hand-rolled scroll box,
|
|
1054
|
+
* no `min-h-screen` wrapper. The shell is the document's only scroll container and every band
|
|
1055
|
+
* pads itself out of the device safe areas. */}
|
|
1056
|
+
<MobileShell
|
|
1057
|
+
statusBar={<StatusBar />}
|
|
1058
|
+
header={header}
|
|
1059
|
+
actions={actions}
|
|
1060
|
+
tabBar={
|
|
1061
|
+
<TabBar
|
|
1062
|
+
active={tab}
|
|
1063
|
+
onChange={(id) => {
|
|
1064
|
+
setTab(id);
|
|
1065
|
+
exitSelect();
|
|
1066
|
+
}}
|
|
1067
|
+
/>
|
|
1068
|
+
}
|
|
1069
|
+
>
|
|
1055
1070
|
{tab === "inbound" ? (
|
|
1056
1071
|
<InboundTab
|
|
1057
1072
|
state={listState}
|
|
1058
1073
|
onRetry={() => setListState("ready")}
|
|
1059
1074
|
selectMode={selectMode}
|
|
1060
|
-
setSelectMode={setSelectMode}
|
|
1061
1075
|
selected={selected}
|
|
1062
1076
|
setSelected={setSelected}
|
|
1063
1077
|
onScan={() => setLookupOpen(true)}
|
|
1064
|
-
onAdd={() => setFormOpen(true)}
|
|
1065
|
-
onAssign={() => setPickerOpen(true)}
|
|
1066
1078
|
/>
|
|
1067
1079
|
) : tab === "packing" ? (
|
|
1068
|
-
<PackingTab
|
|
1080
|
+
<PackingTab />
|
|
1069
1081
|
) : (
|
|
1070
|
-
<OutboundTab
|
|
1082
|
+
<OutboundTab seg={seg} setSeg={setSeg} />
|
|
1071
1083
|
)}
|
|
1072
|
-
|
|
1073
|
-
<TabBar
|
|
1074
|
-
active={tab}
|
|
1075
|
-
onChange={(id) => {
|
|
1076
|
-
setTab(id);
|
|
1077
|
-
setSelectMode(false);
|
|
1078
|
-
setSelected(new Set());
|
|
1079
|
-
}}
|
|
1080
|
-
/>
|
|
1081
|
-
</PhoneFrame>
|
|
1084
|
+
</MobileShell>
|
|
1082
1085
|
|
|
1083
1086
|
{/* Sheets */}
|
|
1084
1087
|
<ItemLookupSheet open={lookupOpen} onOpenChange={setLookupOpen} />
|
|
@@ -1108,6 +1111,6 @@ export default function AgencyHandyShowcase() {
|
|
|
1108
1111
|
toast.success("Đã bàn giao PKG-000037 · 16:24");
|
|
1109
1112
|
}}
|
|
1110
1113
|
/>
|
|
1111
|
-
|
|
1114
|
+
</>
|
|
1112
1115
|
);
|
|
1113
1116
|
}
|