@godxjp/ui 19.2.0 → 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 +26 -2
- package/dist/components/data-display/badge.js +20 -5
- 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 +72 -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 +113 -2
- package/dist/props/registry.js +155 -1
- 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/badge-layout.css +15 -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/badge.css +8 -0
- 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 +37 -1
- package/docs/data-display/card/index.tsx +6 -4
- package/docs/data-display/carousel.tsx +8 -4
- package/docs/data-display/data-table/examples/approval-queue.tsx +1 -1
- package/docs/data-display/data-table/examples/grid-features.tsx +1 -1
- package/docs/data-display/data-table/index.tsx +1 -1
- package/docs/data-display/permission-matrix.tsx +15 -11
- package/docs/data-display/popover.tsx +2 -1
- package/docs/data-display/scroll-area.tsx +108 -76
- package/docs/data-display/table.tsx +2 -2
- package/docs/data-display/timeline-grid.tsx +119 -0
- package/docs/data-entry/branch-scope-picker.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/input-otp.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 +3 -3
- 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/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 +83 -66
- package/docs/foundation/radius-shadow.tsx +13 -11
- package/docs/general/activity.tsx +1 -1
- package/docs/layout/app-shell.tsx +2 -2
- package/docs/layout/aspect-ratio.tsx +30 -14
- 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/flex.tsx +6 -2
- package/docs/layout/master-detail.tsx +2 -2
- package/docs/layout/mobile-shell.tsx +101 -0
- package/docs/layout/page-container.tsx +15 -7
- package/docs/layout/responsive-grid.tsx +12 -4
- package/docs/layout/separator.tsx +2 -2
- package/docs/layout/service-role-panel.tsx +1 -1
- package/docs/layout/sidebar.tsx +175 -152
- package/docs/layout/split-pane.tsx +45 -28
- package/docs/layout/topbar.tsx +19 -23
- 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/filter-bar.tsx +2 -2
- package/docs/navigation/navigation-menu.tsx +89 -96
- package/docs/navigation/pagination.tsx +2 -2
- package/docs/navigation/steps.tsx +13 -11
- package/docs/navigation/tabs.tsx +27 -21
- package/docs/navigation/toolbar.tsx +15 -15
- package/docs/providers/format-date.tsx +2 -2
- package/docs/showcase/acme-portal.tsx +41 -28
- package/docs/showcase/acme-website.tsx +67 -61
- package/docs/showcase/case1-warehouse-dashboard.tsx +46 -31
- package/docs/showcase/case2-employee-me.tsx +33 -36
- package/docs/showcase/case3-approval-workflow.tsx +34 -32
- package/docs/showcase/case4-login.tsx +95 -101
- package/docs/showcase/case5-shift-calendar.tsx +182 -281
- package/docs/showcase/case6-agency-handy.tsx +514 -470
- package/docs/showcase/futurelastic-web.tsx +32 -29
- package/docs/showcase/org-switcher.tsx +20 -13
- package/docs/showcase/permission-matrix.tsx +20 -17
- 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 +9 -9
- package/docs/showcase/table-crud-list.tsx +4 -4
- package/docs/showcase/table-density.tsx +49 -42
- package/docs/showcase/table-expandable-rows.tsx +112 -100
- package/docs/showcase/table-filter-chips.tsx +78 -71
- package/docs/showcase/table-footer-totals.tsx +30 -23
- package/docs/showcase/table-grouped-subtotals.tsx +31 -25
- package/docs/showcase/table-master-detail.tsx +27 -24
- package/docs/showcase/table-pagination.tsx +34 -27
- package/docs/showcase/table-states.tsx +42 -34
- package/docs/showcase/table-sticky-columns.tsx +11 -7
- package/docs/showcase/table-tree-rows.tsx +22 -15
- package/docs/showcase/table-view-tabs.tsx +12 -14
- package/package.json +7 -7
|
@@ -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, 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,
|
|
@@ -249,32 +242,36 @@ function ItemListCard({
|
|
|
249
242
|
: "hover:border-primary")
|
|
250
243
|
}
|
|
251
244
|
>
|
|
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
|
-
|
|
245
|
+
<CardContent solo>
|
|
246
|
+
<Flex align="start" gap="md">
|
|
247
|
+
<Flex direction="col" gap="xs">
|
|
248
|
+
{selectMode ? (
|
|
249
|
+
<Checkbox
|
|
250
|
+
checked={selected}
|
|
251
|
+
onCheckedChange={onToggle}
|
|
252
|
+
aria-label={`選択 ${item.name}`}
|
|
253
|
+
className="size-5"
|
|
254
|
+
/>
|
|
255
|
+
) : null}
|
|
256
|
+
<Flex direction="col" gap="sm" className="min-w-0 flex-1">
|
|
257
|
+
<Text as="div" size="sm" weight="bold" truncate>
|
|
258
|
+
{item.name}
|
|
259
|
+
</Text>
|
|
260
|
+
<Flex wrap align="center" gap="sm">
|
|
261
|
+
<MonoCode>{item.rc}</MonoCode>
|
|
262
|
+
<MonoCode>JAN {item.jan}</MonoCode>
|
|
263
|
+
</Flex>
|
|
264
|
+
<Flex align="center" justify="between" gap="sm">
|
|
265
|
+
<Badge tone={st.tone} variant="outline" className="rounded-full">
|
|
266
|
+
{st.label}
|
|
267
|
+
</Badge>
|
|
268
|
+
<Text size="xs" tone="muted" tabular className="shrink-0">
|
|
269
|
+
×{item.qty} · {item.receivedAt}
|
|
270
|
+
</Text>
|
|
271
|
+
</Flex>
|
|
272
|
+
</Flex>
|
|
276
273
|
</Flex>
|
|
277
|
-
</
|
|
274
|
+
</Flex>
|
|
278
275
|
</CardContent>
|
|
279
276
|
</Card>
|
|
280
277
|
);
|
|
@@ -293,22 +290,24 @@ function PackingListCard({ packing, onTap }: { packing: Packing; onTap?: () => v
|
|
|
293
290
|
}
|
|
294
291
|
>
|
|
295
292
|
<CardContent solo>
|
|
296
|
-
<Flex
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
{st.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
<
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
293
|
+
<Flex direction="col" gap="xs">
|
|
294
|
+
<Flex align="center" justify="between" gap="sm">
|
|
295
|
+
<Text as="code" size="sm" weight="bold" tabular>
|
|
296
|
+
{packing.code}
|
|
297
|
+
</Text>
|
|
298
|
+
<Badge tone={st.tone} variant="outline" className="rounded-full">
|
|
299
|
+
{st.label}
|
|
300
|
+
</Badge>
|
|
301
|
+
</Flex>
|
|
302
|
+
<Flex align="center" justify="between" gap="sm">
|
|
303
|
+
<Text size="xs" tone="muted" truncate>
|
|
304
|
+
{packing.customer} · {packing.city}
|
|
305
|
+
</Text>
|
|
306
|
+
<Text size="xs" tone="muted" tabular className="shrink-0">
|
|
307
|
+
×{packing.items}
|
|
308
|
+
{packing.slot ? ` · ${packing.slot}` : ""}
|
|
309
|
+
</Text>
|
|
310
|
+
</Flex>
|
|
312
311
|
</Flex>
|
|
313
312
|
</CardContent>
|
|
314
313
|
</Card>
|
|
@@ -318,7 +317,7 @@ function PackingListCard({ packing, onTap }: { packing: Packing; onTap?: () => v
|
|
|
318
317
|
/** Three uppercase muted section header + right-aligned mono count. */
|
|
319
318
|
function SectionHeader({ children, count }: { children: React.ReactNode; count?: number }) {
|
|
320
319
|
return (
|
|
321
|
-
<Flex align="center" justify="between"
|
|
320
|
+
<Flex align="center" justify="between">
|
|
322
321
|
<Text size="2xs" weight="medium" tone="muted" className="tracking-wider uppercase">
|
|
323
322
|
{children}
|
|
324
323
|
</Text>
|
|
@@ -333,35 +332,32 @@ function SectionHeader({ children, count }: { children: React.ReactNode; count?:
|
|
|
333
332
|
|
|
334
333
|
// ── Phone shell ──────────────────────────────────────────────────────────────
|
|
335
334
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
{children}
|
|
344
|
-
</Flex>
|
|
345
|
-
</div>
|
|
346
|
-
);
|
|
347
|
-
}
|
|
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
|
+
*/
|
|
348
342
|
|
|
349
343
|
function StatusBar() {
|
|
350
344
|
return (
|
|
351
|
-
|
|
345
|
+
<>
|
|
352
346
|
<Text size="sm" weight="medium" tabular>
|
|
353
347
|
9:41
|
|
354
348
|
</Text>
|
|
355
349
|
<Text size="sm" weight="medium" tabular tone="muted">
|
|
356
350
|
Acme Handy
|
|
357
351
|
</Text>
|
|
358
|
-
|
|
352
|
+
</>
|
|
359
353
|
);
|
|
360
354
|
}
|
|
361
355
|
|
|
362
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.
|
|
363
359
|
return (
|
|
364
|
-
|
|
360
|
+
<>
|
|
365
361
|
{TABS.map((t) => {
|
|
366
362
|
const isActive = t.id === active;
|
|
367
363
|
const Icon = t.icon;
|
|
@@ -372,7 +368,7 @@ function TabBar({ active, onChange }: { active: string; onChange: (id: string) =
|
|
|
372
368
|
onClick={() => onChange(t.id)}
|
|
373
369
|
aria-current={isActive ? "page" : undefined}
|
|
374
370
|
className={
|
|
375
|
-
"h-
|
|
371
|
+
"h-full flex-col rounded-none font-medium text-[var(--font-size-2xs)] " +
|
|
376
372
|
(isActive ? "text-primary" : "text-muted-foreground")
|
|
377
373
|
}
|
|
378
374
|
>
|
|
@@ -381,7 +377,7 @@ function TabBar({ active, onChange }: { active: string; onChange: (id: string) =
|
|
|
381
377
|
</Button>
|
|
382
378
|
);
|
|
383
379
|
})}
|
|
384
|
-
|
|
380
|
+
</>
|
|
385
381
|
);
|
|
386
382
|
}
|
|
387
383
|
|
|
@@ -401,48 +397,48 @@ function ItemLookupSheet({
|
|
|
401
397
|
<SheetHeader>
|
|
402
398
|
<SheetTitle>Quét hoặc nhập mã</SheetTitle>
|
|
403
399
|
</SheetHeader>
|
|
404
|
-
<SheetBody
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
400
|
+
<SheetBody>
|
|
401
|
+
<Flex direction="col" gap="md">
|
|
402
|
+
{/* Viewfinder placeholder — a Card surface, not a hand-rolled illustration */}
|
|
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>
|
|
417
|
+
<Flex direction="col" gap="sm">
|
|
418
|
+
<Text size="sm" weight="medium">
|
|
419
|
+
Hoặc nhập mã thủ công
|
|
419
420
|
</Text>
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
disabled={code.trim() === ""}
|
|
442
|
-
>
|
|
443
|
-
<Search aria-hidden="true" />
|
|
444
|
-
Tìm
|
|
445
|
-
</Button>
|
|
421
|
+
<Flex gap="sm">
|
|
422
|
+
<Input
|
|
423
|
+
value={code}
|
|
424
|
+
onChange={(e) => setCode(e.target.value)}
|
|
425
|
+
placeholder="RC- / PKG- / JAN"
|
|
426
|
+
className="font-mono"
|
|
427
|
+
inputMode="text"
|
|
428
|
+
autoComplete="off"
|
|
429
|
+
/>
|
|
430
|
+
<Button
|
|
431
|
+
onClick={() => {
|
|
432
|
+
onOpenChange(false);
|
|
433
|
+
setCode("");
|
|
434
|
+
toast.success("Đã tìm thấy RC-204881 · Sữa rửa mặt Hada Labo");
|
|
435
|
+
}}
|
|
436
|
+
disabled={code.trim() === ""}
|
|
437
|
+
>
|
|
438
|
+
<Search aria-hidden="true" />
|
|
439
|
+
Tìm
|
|
440
|
+
</Button>
|
|
441
|
+
</Flex>
|
|
446
442
|
</Flex>
|
|
447
443
|
</Flex>
|
|
448
444
|
</SheetBody>
|
|
@@ -471,29 +467,31 @@ function PackagePickerSheet({
|
|
|
471
467
|
<SheetHeader>
|
|
472
468
|
<SheetTitle>Gán vào kiện</SheetTitle>
|
|
473
469
|
</SheetHeader>
|
|
474
|
-
<SheetBody
|
|
475
|
-
<
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
470
|
+
<SheetBody>
|
|
471
|
+
<Flex direction="col" gap="sm">
|
|
472
|
+
<SectionHeader count={PACKINGS.length}>Kiện đang mở</SectionHeader>
|
|
473
|
+
{PACKINGS.map((p) => (
|
|
474
|
+
<PackingListCard
|
|
475
|
+
key={p.id}
|
|
476
|
+
packing={p}
|
|
477
|
+
onTap={() => {
|
|
478
|
+
onOpenChange(false);
|
|
479
|
+
toast.success(`Đã gán ${count} item vào ${p.code}`);
|
|
480
|
+
}}
|
|
481
|
+
/>
|
|
482
|
+
))}
|
|
483
|
+
<Button
|
|
484
|
+
variant="outline"
|
|
485
|
+
onClick={() => {
|
|
481
486
|
onOpenChange(false);
|
|
482
|
-
toast.success(`Đã
|
|
487
|
+
toast.success(`Đã tạo kiện mới với ${count} item`);
|
|
483
488
|
}}
|
|
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>
|
|
489
|
+
className="text-primary hover:border-primary h-11 w-full border-2 border-dashed"
|
|
490
|
+
>
|
|
491
|
+
<PackagePlus aria-hidden="true" strokeWidth={1.5} />
|
|
492
|
+
Tạo kiện mới với {count} item này
|
|
493
|
+
</Button>
|
|
494
|
+
</Flex>
|
|
497
495
|
</SheetBody>
|
|
498
496
|
<SheetFooter>
|
|
499
497
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
@@ -527,66 +525,51 @@ function ItemFormSheet({
|
|
|
527
525
|
<SheetHeader>
|
|
528
526
|
<SheetTitle>Thêm hàng mới</SheetTitle>
|
|
529
527
|
</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
|
-
<RadioItem id={rowId} value={d.id} className="mt-0.5" />
|
|
575
|
-
<span className="min-w-0">
|
|
576
|
-
<Text as="span" size="sm" weight="medium" className="block">
|
|
577
|
-
{d.label}
|
|
578
|
-
</Text>
|
|
579
|
-
<Text as="span" size="xs" tone="muted" className="block">
|
|
580
|
-
{d.hint}
|
|
581
|
-
</Text>
|
|
582
|
-
</span>
|
|
583
|
-
</label>
|
|
584
|
-
);
|
|
585
|
-
})}
|
|
586
|
-
</RadioGroupRoot>
|
|
528
|
+
<SheetBody>
|
|
529
|
+
<Flex direction="col" gap="md">
|
|
530
|
+
<Flex direction="col" gap="xs">
|
|
531
|
+
<Text size="sm" weight="medium">
|
|
532
|
+
Mã vạch
|
|
533
|
+
</Text>
|
|
534
|
+
<Input defaultValue="JAN 4987241135219" readOnly className="font-mono" />
|
|
535
|
+
</Flex>
|
|
536
|
+
<Flex direction="col" gap="xs">
|
|
537
|
+
<Text size="sm" weight="medium">
|
|
538
|
+
Tên hàng (tùy chọn)
|
|
539
|
+
</Text>
|
|
540
|
+
<Input placeholder="Nhập tên hàng" autoComplete="off" />
|
|
541
|
+
</Flex>
|
|
542
|
+
<Flex direction="col" gap="xs">
|
|
543
|
+
<Text size="sm" weight="medium">
|
|
544
|
+
Số lượng
|
|
545
|
+
</Text>
|
|
546
|
+
<Input
|
|
547
|
+
type="number"
|
|
548
|
+
value={qty}
|
|
549
|
+
min={1}
|
|
550
|
+
onChange={(e) => setQty(e.target.value)}
|
|
551
|
+
className="w-24 tabular-nums"
|
|
552
|
+
/>
|
|
553
|
+
</Flex>
|
|
554
|
+
<Flex direction="col" gap="sm">
|
|
555
|
+
<Text size="sm" weight="medium">
|
|
556
|
+
Đích đến
|
|
557
|
+
</Text>
|
|
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
|
+
/>
|
|
569
|
+
</Flex>
|
|
587
570
|
</Flex>
|
|
588
571
|
</SheetBody>
|
|
589
|
-
<SheetFooter className="flex-row
|
|
572
|
+
<SheetFooter className="flex-row">
|
|
590
573
|
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>
|
|
591
574
|
Huỷ
|
|
592
575
|
</Button>
|
|
@@ -609,26 +592,24 @@ function ItemFormSheet({
|
|
|
609
592
|
|
|
610
593
|
type ListState = "ready" | "loading" | "empty" | "error";
|
|
611
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
|
+
*/
|
|
612
599
|
function InboundTab({
|
|
613
600
|
state,
|
|
614
601
|
onRetry,
|
|
615
602
|
selectMode,
|
|
616
|
-
setSelectMode,
|
|
617
603
|
selected,
|
|
618
604
|
setSelected,
|
|
619
605
|
onScan,
|
|
620
|
-
onAdd,
|
|
621
|
-
onAssign,
|
|
622
606
|
}: {
|
|
623
607
|
state: ListState;
|
|
624
608
|
onRetry: () => void;
|
|
625
609
|
selectMode: boolean;
|
|
626
|
-
setSelectMode: (v: boolean) => void;
|
|
627
610
|
selected: Set<string>;
|
|
628
611
|
setSelected: React.Dispatch<React.SetStateAction<Set<string>>>;
|
|
629
612
|
onScan: () => void;
|
|
630
|
-
onAdd: () => void;
|
|
631
|
-
onAssign: () => void;
|
|
632
613
|
}) {
|
|
633
614
|
const [filter, setFilter] = React.useState<string>("all");
|
|
634
615
|
|
|
@@ -641,22 +622,22 @@ function InboundTab({
|
|
|
641
622
|
);
|
|
642
623
|
|
|
643
624
|
return (
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
>
|
|
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">
|
|
655
636
|
{FILTERS.map((f) => (
|
|
656
637
|
<ToggleGroupItem
|
|
657
638
|
key={f.id}
|
|
658
639
|
value={f.id}
|
|
659
|
-
className="h-9 shrink-0
|
|
640
|
+
className="h-9 shrink-0 rounded-full whitespace-nowrap text-[var(--font-size-xs)]"
|
|
660
641
|
>
|
|
661
642
|
{f.label}
|
|
662
643
|
<Text size="xs" mono tabular className="opacity-70">
|
|
@@ -664,125 +645,146 @@ function InboundTab({
|
|
|
664
645
|
</Text>
|
|
665
646
|
</ToggleGroupItem>
|
|
666
647
|
))}
|
|
667
|
-
</
|
|
648
|
+
</Flex>
|
|
649
|
+
</ToggleGroup>
|
|
668
650
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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">
|
|
674
657
|
<Skeleton className="h-4 w-3/5" />
|
|
675
|
-
<Skeleton className="
|
|
676
|
-
<Skeleton className="
|
|
677
|
-
</
|
|
678
|
-
</
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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">
|
|
685
670
|
Kiểm tra kết nối rồi thử lại.
|
|
686
671
|
<Button variant="outline" size="sm" onClick={onRetry}>
|
|
687
672
|
<RefreshCw aria-hidden="true" />
|
|
688
673
|
Thử lại
|
|
689
674
|
</Button>
|
|
690
|
-
</
|
|
691
|
-
</
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
/>
|
|
704
|
-
) : (
|
|
705
|
-
<Flex direction="col" gap="md">
|
|
706
|
-
{visible.map((item) => (
|
|
707
|
-
<ItemListCard
|
|
708
|
-
key={item.id}
|
|
709
|
-
item={item}
|
|
710
|
-
selectMode={selectMode}
|
|
711
|
-
selected={selected.has(item.id)}
|
|
712
|
-
onToggle={() =>
|
|
713
|
-
setSelected((prev) => {
|
|
714
|
-
const next = new Set(prev);
|
|
715
|
-
if (next.has(item.id)) next.delete(item.id);
|
|
716
|
-
else next.add(item.id);
|
|
717
|
-
return next;
|
|
718
|
-
})
|
|
719
|
-
}
|
|
720
|
-
/>
|
|
721
|
-
))}
|
|
722
|
-
</Flex>
|
|
723
|
-
)}
|
|
724
|
-
</div>
|
|
725
|
-
|
|
726
|
-
{/* Sticky action bar (scan-first) OR select-mode contextual bar */}
|
|
727
|
-
{selectMode ? (
|
|
728
|
-
<div className="shrink-0 border-t bg-[color-mix(in_oklch,var(--primary)_5%,var(--background))] p-3">
|
|
729
|
-
<Flex align="center" justify="between" className="mb-2">
|
|
730
|
-
<Text size="sm" tabular>
|
|
731
|
-
<Text as="span" weight="bold">
|
|
732
|
-
{selected.size}
|
|
733
|
-
</Text>{" "}
|
|
734
|
-
item đã chọn
|
|
735
|
-
</Text>
|
|
736
|
-
<Button
|
|
737
|
-
variant="ghost"
|
|
738
|
-
size="sm"
|
|
739
|
-
onClick={() => {
|
|
740
|
-
setSelectMode(false);
|
|
741
|
-
setSelected(new Set());
|
|
742
|
-
}}
|
|
743
|
-
>
|
|
744
|
-
Xong
|
|
745
|
-
</Button>
|
|
746
|
-
</Flex>
|
|
747
|
-
<Flex gap="sm">
|
|
748
|
-
<Button className="flex-[2]" disabled={selected.size === 0} onClick={onAssign}>
|
|
749
|
-
<Package aria-hidden="true" />
|
|
750
|
-
Gán vào kiện…
|
|
751
|
-
</Button>
|
|
752
|
-
<Button variant="outline" className="flex-1" disabled={selected.size === 0}>
|
|
753
|
-
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ã
|
|
754
687
|
</Button>
|
|
755
|
-
|
|
756
|
-
|
|
688
|
+
}
|
|
689
|
+
/>
|
|
757
690
|
) : (
|
|
758
|
-
<
|
|
759
|
-
|
|
760
|
-
<
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
+
))}
|
|
708
|
+
</Flex>
|
|
768
709
|
)}
|
|
769
|
-
|
|
710
|
+
</Flex>
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
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>
|
|
770
772
|
);
|
|
771
773
|
}
|
|
772
774
|
|
|
773
|
-
function PackingTab(
|
|
775
|
+
function PackingTab() {
|
|
774
776
|
const active = PACKINGS.find((p) => p.status === "active");
|
|
775
777
|
const others = PACKINGS.filter((p) => p.status !== "active");
|
|
776
778
|
return (
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
>
|
|
785
|
-
<
|
|
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">
|
|
786
788
|
<Flex align="center" justify="between">
|
|
787
789
|
<Text size="2xs" weight="medium" tone="muted" className="tracking-wider uppercase">
|
|
788
790
|
Kiện đang làm
|
|
@@ -791,51 +793,59 @@ function PackingTab({ onScan }: { onScan: () => void }) {
|
|
|
791
793
|
{PACKING_STATUS.active.label}
|
|
792
794
|
</Badge>
|
|
793
795
|
</Flex>
|
|
794
|
-
<Text as="div" size="lg" weight="bold" mono tabular
|
|
796
|
+
<Text as="div" size="lg" weight="bold" mono tabular>
|
|
795
797
|
{active.code}
|
|
796
798
|
</Text>
|
|
797
|
-
<Text size="xs" tone="muted" tabular as="div"
|
|
799
|
+
<Text size="xs" tone="muted" tabular as="div">
|
|
798
800
|
{active.customer} · {active.city} · ×{active.items} · {active.slot}
|
|
799
801
|
</Text>
|
|
800
|
-
</
|
|
801
|
-
</
|
|
802
|
-
|
|
802
|
+
</Flex>
|
|
803
|
+
</CardContent>
|
|
804
|
+
</Card>
|
|
805
|
+
) : null}
|
|
803
806
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
<div className="flex shrink-0 flex-col gap-2 border-t p-3">
|
|
812
|
-
<Button onClick={onScan}>
|
|
813
|
-
<ScanLine aria-hidden="true" />
|
|
814
|
-
Quét item vào kiện
|
|
815
|
-
</Button>
|
|
816
|
-
<Button variant="outline">
|
|
817
|
-
<Plus aria-hidden="true" />
|
|
818
|
-
Tạo kiện trống
|
|
819
|
-
</Button>
|
|
820
|
-
</div>
|
|
821
|
-
</>
|
|
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>
|
|
822
814
|
);
|
|
823
815
|
}
|
|
824
816
|
|
|
825
|
-
function
|
|
826
|
-
const [seg, setSeg] = React.useState<string>("seal");
|
|
817
|
+
function PackingActions({ onScan }: { onScan: () => void }) {
|
|
827
818
|
return (
|
|
828
|
-
|
|
829
|
-
<
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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">
|
|
839
849
|
<ToggleGroupItem
|
|
840
850
|
value="seal"
|
|
841
851
|
className="h-9 rounded-xl whitespace-nowrap text-[var(--font-size-xs)]"
|
|
@@ -854,19 +864,21 @@ function OutboundTab({ onSeal, onHandoff }: { onSeal: () => void; onHandoff: ()
|
|
|
854
864
|
>
|
|
855
865
|
Đã bàn giao
|
|
856
866
|
</ToggleGroupItem>
|
|
857
|
-
</
|
|
867
|
+
</ResponsiveGrid>
|
|
868
|
+
</ToggleGroup>
|
|
858
869
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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">
|
|
880
|
+
<CardContent solo>
|
|
881
|
+
<Flex direction="col" gap="md">
|
|
870
882
|
<Flex align="center" justify="between" gap="sm">
|
|
871
883
|
<Text as="code" size="sm" weight="bold" tabular>
|
|
872
884
|
{p.code}
|
|
@@ -875,7 +887,7 @@ function OutboundTab({ onSeal, onHandoff }: { onSeal: () => void; onHandoff: ()
|
|
|
875
887
|
Sẵn sàng niêm phong
|
|
876
888
|
</Badge>
|
|
877
889
|
</Flex>
|
|
878
|
-
<Flex align="center" justify="between" gap="sm"
|
|
890
|
+
<Flex align="center" justify="between" gap="sm">
|
|
879
891
|
<Text size="xs" tone="muted" tabular truncate>
|
|
880
892
|
{p.customer} · {p.city}
|
|
881
893
|
</Text>
|
|
@@ -883,8 +895,8 @@ function OutboundTab({ onSeal, onHandoff }: { onSeal: () => void; onHandoff: ()
|
|
|
883
895
|
×{p.items} · {p.slot}
|
|
884
896
|
</Text>
|
|
885
897
|
</Flex>
|
|
886
|
-
<div
|
|
887
|
-
<Descriptions columns={1}
|
|
898
|
+
<div>
|
|
899
|
+
<Descriptions columns={1}>
|
|
888
900
|
<Descriptions.Item label="Vị trí" mono>
|
|
889
901
|
{p.slot}
|
|
890
902
|
</Descriptions.Item>
|
|
@@ -893,25 +905,34 @@ function OutboundTab({ onSeal, onHandoff }: { onSeal: () => void; onHandoff: ()
|
|
|
893
905
|
</Descriptions.Item>
|
|
894
906
|
</Descriptions>
|
|
895
907
|
</div>
|
|
896
|
-
</
|
|
897
|
-
</
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
</
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
908
|
+
</Flex>
|
|
909
|
+
</CardContent>
|
|
910
|
+
</Card>
|
|
911
|
+
))}
|
|
912
|
+
</Flex>
|
|
913
|
+
</Flex>
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
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>
|
|
915
936
|
);
|
|
916
937
|
}
|
|
917
938
|
|
|
@@ -923,6 +944,7 @@ export default function AgencyHandyShowcase() {
|
|
|
923
944
|
|
|
924
945
|
const [selectMode, setSelectMode] = React.useState(false);
|
|
925
946
|
const [selected, setSelected] = React.useState<Set<string>>(new Set());
|
|
947
|
+
const [seg, setSeg] = React.useState<string>("seal");
|
|
926
948
|
|
|
927
949
|
const [lookupOpen, setLookupOpen] = React.useState(false);
|
|
928
950
|
const [formOpen, setFormOpen] = React.useState(false);
|
|
@@ -933,111 +955,133 @@ export default function AgencyHandyShowcase() {
|
|
|
933
955
|
|
|
934
956
|
const headerTitle = tab === "inbound" ? "Nhập kho" : tab === "packing" ? "Đóng gói" : "Xuất kho";
|
|
935
957
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
<StatusBar />
|
|
958
|
+
const exitSelect = () => {
|
|
959
|
+
setSelectMode(false);
|
|
960
|
+
setSelected(new Set());
|
|
961
|
+
};
|
|
941
962
|
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
</ToggleGroupItem>
|
|
981
|
-
))}
|
|
982
|
-
</ToggleGroup>
|
|
983
|
-
) : null}
|
|
984
|
-
</Flex>
|
|
985
|
-
</header>
|
|
986
|
-
|
|
987
|
-
{/* Select-mode header strip — replaces the standard header context (inbound) */}
|
|
988
|
-
{selectMode ? (
|
|
989
|
-
<div className="flex h-10 shrink-0 items-center justify-between border-b bg-[color-mix(in_oklch,var(--primary)_5%,var(--background))] px-3">
|
|
990
|
-
<Button
|
|
991
|
-
variant="ghost"
|
|
992
|
-
size="icon-sm"
|
|
993
|
-
aria-label="閉じる"
|
|
994
|
-
onClick={() => {
|
|
995
|
-
setSelectMode(false);
|
|
996
|
-
setSelected(new Set());
|
|
997
|
-
}}
|
|
998
|
-
>
|
|
999
|
-
<X className="size-4" aria-hidden="true" strokeWidth={1.5} />
|
|
1000
|
-
</Button>
|
|
1001
|
-
<Text size="sm" mono tabular>
|
|
1002
|
-
{selected.size} đã chọn
|
|
1003
|
-
</Text>
|
|
1004
|
-
<Button
|
|
1005
|
-
variant="ghost"
|
|
1006
|
-
size="sm"
|
|
1007
|
-
onClick={() => setSelected(new Set(ITEMS.map((i) => i.id)))}
|
|
1008
|
-
>
|
|
1009
|
-
Chọn tất cả
|
|
1010
|
-
</Button>
|
|
1011
|
-
</div>
|
|
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>
|
|
1012
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}
|
|
1019
|
+
>
|
|
1020
|
+
{s[0]}
|
|
1021
|
+
</ToggleGroupItem>
|
|
1022
|
+
))}
|
|
1023
|
+
</Flex>
|
|
1024
|
+
</ToggleGroup>
|
|
1025
|
+
) : null}
|
|
1026
|
+
</Flex>
|
|
1027
|
+
</Flex>
|
|
1028
|
+
);
|
|
1013
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
|
+
);
|
|
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
|
+
>
|
|
1014
1070
|
{tab === "inbound" ? (
|
|
1015
1071
|
<InboundTab
|
|
1016
1072
|
state={listState}
|
|
1017
1073
|
onRetry={() => setListState("ready")}
|
|
1018
1074
|
selectMode={selectMode}
|
|
1019
|
-
setSelectMode={setSelectMode}
|
|
1020
1075
|
selected={selected}
|
|
1021
1076
|
setSelected={setSelected}
|
|
1022
1077
|
onScan={() => setLookupOpen(true)}
|
|
1023
|
-
onAdd={() => setFormOpen(true)}
|
|
1024
|
-
onAssign={() => setPickerOpen(true)}
|
|
1025
1078
|
/>
|
|
1026
1079
|
) : tab === "packing" ? (
|
|
1027
|
-
<PackingTab
|
|
1080
|
+
<PackingTab />
|
|
1028
1081
|
) : (
|
|
1029
|
-
<OutboundTab
|
|
1082
|
+
<OutboundTab seg={seg} setSeg={setSeg} />
|
|
1030
1083
|
)}
|
|
1031
|
-
|
|
1032
|
-
<TabBar
|
|
1033
|
-
active={tab}
|
|
1034
|
-
onChange={(id) => {
|
|
1035
|
-
setTab(id);
|
|
1036
|
-
setSelectMode(false);
|
|
1037
|
-
setSelected(new Set());
|
|
1038
|
-
}}
|
|
1039
|
-
/>
|
|
1040
|
-
</PhoneFrame>
|
|
1084
|
+
</MobileShell>
|
|
1041
1085
|
|
|
1042
1086
|
{/* Sheets */}
|
|
1043
1087
|
<ItemLookupSheet open={lookupOpen} onOpenChange={setLookupOpen} />
|
|
@@ -1067,6 +1111,6 @@ export default function AgencyHandyShowcase() {
|
|
|
1067
1111
|
toast.success("Đã bàn giao PKG-000037 · 16:24");
|
|
1068
1112
|
}}
|
|
1069
1113
|
/>
|
|
1070
|
-
|
|
1114
|
+
</>
|
|
1071
1115
|
);
|
|
1072
1116
|
}
|