@tallyui/components 3.0.2 → 3.0.4

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/index.d.ts CHANGED
@@ -1255,7 +1255,7 @@ declare function DiscountChips({ discounts, currency, onRemove, prefix, amounts
1255
1255
  }): react_jsx_runtime.JSX.Element | null;
1256
1256
 
1257
1257
  declare function formatStockSyncTime(time: Date, locale?: string, hour12?: boolean, now?: Date): string;
1258
- declare function Catalogue<Doc>({ products, traits, currency, onSelect, statusText, statusAccessory, lastSyncedAt, loading, lastStockCheckAt, hour12, minCodeLength }: {
1258
+ declare function Catalogue<Doc>({ products, traits, currency, onSelect, statusText, statusAccessory, lastSyncedAt, loading, pullError, lastStockCheckAt, hour12, minCodeLength }: {
1259
1259
  products: Doc[];
1260
1260
  traits: ProductTraits<Doc>;
1261
1261
  currency: string;
@@ -1265,9 +1265,12 @@ declare function Catalogue<Doc>({ products, traits, currency, onSelect, statusTe
1265
1265
  /** Rendered at the end of the status line, e.g. a register control at phone width; the row renders even without `statusText`. */
1266
1266
  statusAccessory?: ReactNode;
1267
1267
  lastSyncedAt: Date | null;
1268
- /** The catalogue's first pull is still running, so an empty grid says "Loading products…" rather than "No products yet.". Defaults to lastSyncedAt === null; pass false
1268
+ /** The catalogue's first pull is still running, so an empty grid says "Loading products…" rather than "No products yet.". Defaults to lastSyncedAt === null with no pullError; pass false
1269
1269
  * when a null lastSyncedAt doesn't mean a pull is running (e.g. offline on first start). */
1270
1270
  loading?: boolean;
1271
+ /** The product pull's last error (an Error, a SyncNotice, anything the app holds), or null/undefined when none. While it is
1272
+ * set, a catalogue that has never synced isn't treated as loading, so its empty grid says "No products yet." beside the app's error. */
1273
+ pullError?: unknown;
1271
1274
  /** The last completed stock reconcile pass, persisted across restarts. The "stock as of" time shown is the
1272
1275
  * latest of this, the provider's stockOverlayAsOf (when valid) and lastSyncedAt; null and omitted alike. */
1273
1276
  lastStockCheckAt?: Date | null;
@@ -1299,6 +1302,15 @@ declare function Receipt({ order, posOrder, store, cashier, registerId, newSale,
1299
1302
  formatDate?: (iso: string) => string;
1300
1303
  }): react_jsx_runtime.JSX.Element;
1301
1304
 
1305
+ /** The order reference a visitor sees on the receipt and in Orders (front desk ruling, 2026-10-06): the finalized
1306
+ * PosOrder id's last 8 characters, then the backend's display id once the store has one. */
1307
+ declare function orderReference(order: {
1308
+ id: string;
1309
+ serverRefs?: {
1310
+ displayId?: string;
1311
+ };
1312
+ }): string;
1313
+
1302
1314
  declare function injectPrintStyle(): void;
1303
1315
 
1304
1316
  /**
@@ -1402,4 +1414,4 @@ interface StoreSettingsChoiceScreenProps extends Omit<ViewProps, 'children'> {
1402
1414
  */
1403
1415
  declare function StoreSettingsChoiceScreen({ choices, initial, onSubmit, countryName, locale, title, body, regionLabel, countryLabel, channelLabel, submitLabel, className, ...viewProps }: StoreSettingsChoiceScreenProps): react_jsx_runtime.JSX.Element;
1404
1416
 
1405
- export { APPROVAL_REQUIRED_TEXT, Accordion, AccordionContent, AccordionItem, AccordionTrigger, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, Avatar, type AvatarProps, Badge, type BadgeProps, Button, type ButtonProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Cart, type CartAction, CartBar, CartLine, CartLineActions, type CartLineActionsProps, type CartLineProps, CartNoteInput, type CartNoteInputProps, CartPanel, type CartPanelProps, CartTotal, type CartTotalProps, CashCountInput, type CashCountInputProps, CashTendered, type CashTenderedProps, Catalogue, type CategoryItem, CategoryNav, type CategoryNavProps, ChangeDisplay, type ChangeDisplayProps, Checkbox, CheckoutLayout, type CheckoutLayoutProps, type ChipItem, ClosureSheet, type ClosureSheetProps, Collapsible, CollapsibleContent, CollapsibleTrigger, Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxTrigger, type ConnectionStatus, ConnectorStatus, type ConnectorStatusProps, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CustomerCard, type CustomerCardProps, CustomerForm, type CustomerFormProps, type CustomerFormValues, CustomerOrderHistory, type CustomerOrderHistoryProps, CustomerPicker, type CustomerPickerProps, CustomerSelect, type CustomerSelectProps, type Denomination, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiscountBadge, type DiscountBadgeProps, DiscountChips, DiscountForm, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FilterChip, FilterChipGroup, type FilterChipGroupProps, type FilterChipProps, HStack, type HStackProps, HoverCard, HoverCardContent, HoverCardTrigger, Icon, IconButton, type IconButtonProps, type IconProps, Input, InputField, InputRoot, type InputRootProps, InputSlot, Label, type LabelProps, LiveTabScreen, type LiveTabScreenProps, type LiveTabScreenState, Loader, type LoaderProps, MovementSheet, type MovementSheetProps, OpenRegisterCard, type OpenRegisterCardProps, OrderCard, type OrderCardProps, OrderDetail, type OrderDetailLineItem, type OrderDetailProps, OrderList, type OrderListProps, type OrderStatus, OrderStatusBadge, type OrderStatusBadgeProps, OrderSummary, OrderSummaryLine, type OrderSummaryLineProps, type OrderSummaryPayment, type OrderSummaryProps, OrdersList, POSLayout, type POSLayoutProps, type PaymentMethod, PaymentMethodCard, type PaymentMethodCardProps, PaymentSelector, type PaymentSelectorProps, Popover, PopoverContent, PopoverTrigger, ProductCard, type ProductCardProps, ProductGrid, type ProductGridProps, ProductImage, type ProductImageProps, ProductList, type ProductListProps, ProductPrice, type ProductPriceProps, ProductSku, type ProductSkuProps, ProductStockBadge, type ProductStockBadgeProps, ProductTitle, type ProductTitleProps, ProductVariantPicker, type ProductVariantPickerProps, Progress, type ProgressProps, QuantityStepper, type QuantityStepperProps, RadioGroup, RadioGroupItem, Receipt, type ReceiptItem, ReceiptPreview, type ReceiptPreviewProps, RegisterBar, type RegisterBarPill, type RegisterBarProps, RegisterColumn, type RegisterColumnProps, RegisterCount, type RegisterCountProps, RegisterOpenClose, type RegisterOpenCloseProps, RegisterPanel, type RegisterPanelProps, RegisterPicker, type RegisterPickerProps, type RegisterPickerRegister, RegisterSummary, type RegisterSummaryProps, SearchIcon, SearchInput, type SearchInputProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeparatorProps, SettingsGroup, type SettingsGroupProps, SettingsLayout, type SettingsLayoutProps, SettingsRow, type SettingsRowProps, Slider, type SliderProps, StoreSettingsChoiceScreen, type StoreSettingsChoiceScreenProps, type SvgIconProps, Switch, SyncStatus, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tender, Text, type TextProps, Textarea, type TextareaProps, Toast, ToastClose, ToastDescription, type ToastProps, ToastTitle, Toggle, ToggleGroup, ToggleGroupItem, type ToggleGroupItemProps, type ToggleGroupProps, type ToggleProps, Tooltip, TooltipContent, TooltipTrigger, type TransactionSummary, VStack, type VStackProps, type VariantOption, XIcon, avatarVariants, badgeTextVariants, badgeVariants, buttonTextVariants, buttonVariants, createSvgIcon, describeRegisterBarPill, discountLabel, formatStockSyncTime, hstackVariants, iconButtonVariants, iconVariants, injectPrintStyle, loaderVariants, parseDiscount, textVariants, toastVariants, toggleTextVariants, toggleVariants, useInputContext, vstackVariants };
1417
+ export { APPROVAL_REQUIRED_TEXT, Accordion, AccordionContent, AccordionItem, AccordionTrigger, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, Avatar, type AvatarProps, Badge, type BadgeProps, Button, type ButtonProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Cart, type CartAction, CartBar, CartLine, CartLineActions, type CartLineActionsProps, type CartLineProps, CartNoteInput, type CartNoteInputProps, CartPanel, type CartPanelProps, CartTotal, type CartTotalProps, CashCountInput, type CashCountInputProps, CashTendered, type CashTenderedProps, Catalogue, type CategoryItem, CategoryNav, type CategoryNavProps, ChangeDisplay, type ChangeDisplayProps, Checkbox, CheckoutLayout, type CheckoutLayoutProps, type ChipItem, ClosureSheet, type ClosureSheetProps, Collapsible, CollapsibleContent, CollapsibleTrigger, Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxTrigger, type ConnectionStatus, ConnectorStatus, type ConnectorStatusProps, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CustomerCard, type CustomerCardProps, CustomerForm, type CustomerFormProps, type CustomerFormValues, CustomerOrderHistory, type CustomerOrderHistoryProps, CustomerPicker, type CustomerPickerProps, CustomerSelect, type CustomerSelectProps, type Denomination, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiscountBadge, type DiscountBadgeProps, DiscountChips, DiscountForm, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, FilterChip, FilterChipGroup, type FilterChipGroupProps, type FilterChipProps, HStack, type HStackProps, HoverCard, HoverCardContent, HoverCardTrigger, Icon, IconButton, type IconButtonProps, type IconProps, Input, InputField, InputRoot, type InputRootProps, InputSlot, Label, type LabelProps, LiveTabScreen, type LiveTabScreenProps, type LiveTabScreenState, Loader, type LoaderProps, MovementSheet, type MovementSheetProps, OpenRegisterCard, type OpenRegisterCardProps, OrderCard, type OrderCardProps, OrderDetail, type OrderDetailLineItem, type OrderDetailProps, OrderList, type OrderListProps, type OrderStatus, OrderStatusBadge, type OrderStatusBadgeProps, OrderSummary, OrderSummaryLine, type OrderSummaryLineProps, type OrderSummaryPayment, type OrderSummaryProps, OrdersList, POSLayout, type POSLayoutProps, type PaymentMethod, PaymentMethodCard, type PaymentMethodCardProps, PaymentSelector, type PaymentSelectorProps, Popover, PopoverContent, PopoverTrigger, ProductCard, type ProductCardProps, ProductGrid, type ProductGridProps, ProductImage, type ProductImageProps, ProductList, type ProductListProps, ProductPrice, type ProductPriceProps, ProductSku, type ProductSkuProps, ProductStockBadge, type ProductStockBadgeProps, ProductTitle, type ProductTitleProps, ProductVariantPicker, type ProductVariantPickerProps, Progress, type ProgressProps, QuantityStepper, type QuantityStepperProps, RadioGroup, RadioGroupItem, Receipt, type ReceiptItem, ReceiptPreview, type ReceiptPreviewProps, RegisterBar, type RegisterBarPill, type RegisterBarProps, RegisterColumn, type RegisterColumnProps, RegisterCount, type RegisterCountProps, RegisterOpenClose, type RegisterOpenCloseProps, RegisterPanel, type RegisterPanelProps, RegisterPicker, type RegisterPickerProps, type RegisterPickerRegister, RegisterSummary, type RegisterSummaryProps, SearchIcon, SearchInput, type SearchInputProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeparatorProps, SettingsGroup, type SettingsGroupProps, SettingsLayout, type SettingsLayoutProps, SettingsRow, type SettingsRowProps, Slider, type SliderProps, StoreSettingsChoiceScreen, type StoreSettingsChoiceScreenProps, type SvgIconProps, Switch, SyncStatus, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tender, Text, type TextProps, Textarea, type TextareaProps, Toast, ToastClose, ToastDescription, type ToastProps, ToastTitle, Toggle, ToggleGroup, ToggleGroupItem, type ToggleGroupItemProps, type ToggleGroupProps, type ToggleProps, Tooltip, TooltipContent, TooltipTrigger, type TransactionSummary, VStack, type VStackProps, type VariantOption, XIcon, avatarVariants, badgeTextVariants, badgeVariants, buttonTextVariants, buttonVariants, createSvgIcon, describeRegisterBarPill, discountLabel, formatStockSyncTime, hstackVariants, iconButtonVariants, iconVariants, injectPrintStyle, loaderVariants, orderReference, parseDiscount, textVariants, toastVariants, toggleTextVariants, toggleVariants, useInputContext, vstackVariants };
package/dist/index.js CHANGED
@@ -3341,194 +3341,13 @@ function MovementSheet({ register, type, currency, onDone, onOpenChange, onOpenD
3341
3341
  }
3342
3342
 
3343
3343
  // src/register/register-panel.tsx
3344
- import { useState as useState11 } from "react";
3345
- import { ScrollView as ScrollView11, useWindowDimensions, View as View47 } from "react-native";
3346
- import { formatMoney as formatMoney7 } from "@tallyui/core";
3347
- import { jsx as jsx81, jsxs as jsxs40 } from "react/jsx-runtime";
3348
- var DIALOG_VERTICAL_MARGIN = 64;
3349
- function RegisterPanel({ register, currency, registerName, open, onOpenChange, className }) {
3350
- const { session, expected, salesCount, blind, movements, lastClosure, actions } = register;
3351
- const { height: windowHeight } = useWindowDimensions();
3352
- const maxHeight = Math.max(280, windowHeight - DIALOG_VERTICAL_MARGIN);
3353
- const [movementType, setMovementType] = useState11(null);
3354
- const [expanded, setExpanded] = useState11(false);
3355
- const [error, setError] = useState11("");
3356
- const attempt = async (action) => {
3357
- try {
3358
- await action();
3359
- } catch (e) {
3360
- setError(String(e));
3361
- }
3362
- };
3363
- if (!session && !lastClosure) return null;
3364
- const activeMovements = movements.filter(
3365
- (row) => row.type !== "void" && !row.voided_by && !movements.some((entry) => entry.voids === row.id)
3366
- );
3367
- return /* @__PURE__ */ jsx81(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs40(
3368
- DialogContent,
3369
- {
3370
- testID: "register-panel",
3371
- className: `flex-col overflow-hidden ${className ?? ""}`,
3372
- style: { maxHeight },
3373
- children: [
3374
- /* @__PURE__ */ jsxs40(View47, { className: "flex-row items-center justify-between gap-2", children: [
3375
- /* @__PURE__ */ jsx81(DialogTitle, { testID: "register-panel-amount", children: registerName ?? "Register" }),
3376
- /* @__PURE__ */ jsx81(DialogClose, { asChild: true, children: /* @__PURE__ */ jsx81(IconButton, { variant: "ghost", size: "sm", testID: "register-panel-dismiss", accessibilityLabel: "Close panel", children: /* @__PURE__ */ jsx81(XIcon, { size: 16 }) }) })
3377
- ] }),
3378
- /* @__PURE__ */ jsxs40(Text, { testID: "register-panel-sales-count", children: [
3379
- salesCount,
3380
- " sale",
3381
- salesCount === 1 ? "" : "s",
3382
- " this session"
3383
- ] }),
3384
- /* @__PURE__ */ jsxs40(ScrollView11, { testID: "register-panel-body", className: "flex-1", contentContainerClassName: "gap-3", children: [
3385
- /* @__PURE__ */ jsx81(View47, { className: "flex-row gap-2", children: ["paid_in", "paid_out", "no_sale"].map((type) => /* @__PURE__ */ jsx81(
3386
- Button,
3387
- {
3388
- testID: `register-panel-${type.replace("_", "-")}`,
3389
- className: "min-h-14 flex-1",
3390
- variant: "outline",
3391
- disabled: session?.status !== "open",
3392
- onPress: () => setMovementType(type),
3393
- children: /* @__PURE__ */ jsx81(Text, { children: type === "paid_in" ? "Paid in" : type === "paid_out" ? "Paid out" : "No sale" })
3394
- },
3395
- type
3396
- )) }),
3397
- !blind && /* @__PURE__ */ jsxs40(View47, { testID: "register-panel-expected", className: "gap-1", children: [
3398
- /* @__PURE__ */ jsx81(Text, { className: "text-muted-foreground text-sm font-semibold", children: "Expected in the drawer" }),
3399
- Object.entries(expected).map(([method, amount]) => /* @__PURE__ */ jsxs40(View47, { className: "min-h-11 flex-row items-center justify-between", children: [
3400
- /* @__PURE__ */ jsx81(Text, { className: "capitalize", children: method === "cash" ? "Cash" : method }),
3401
- /* @__PURE__ */ jsx81(Text, { className: "tabular-nums", children: formatMoney7({ amount, currency }) })
3402
- ] }, method))
3403
- ] }),
3404
- /* @__PURE__ */ jsx81(
3405
- Button,
3406
- {
3407
- testID: "register-panel-movements",
3408
- variant: "ghost",
3409
- className: "min-h-11",
3410
- onPress: () => setExpanded(!expanded),
3411
- children: /* @__PURE__ */ jsx81(Text, { children: "Paid in / out" })
3412
- }
3413
- ),
3414
- expanded && activeMovements.map((row) => /* @__PURE__ */ jsxs40(View47, { className: "min-h-11 flex-row items-center gap-2", children: [
3415
- /* @__PURE__ */ jsxs40(Text, { testID: `movement-row-${row.id}`, className: "flex-1", children: [
3416
- row.type === "paid_in" ? "Paid in" : row.type === "paid_out" ? "Paid out" : "No sale",
3417
- !blind ? ` \xB7 ${formatMoney7({ amount: row.amountMinor, currency })} \xB7 ${row.reason}` : ""
3418
- ] }),
3419
- /* @__PURE__ */ jsx81(
3420
- Button,
3421
- {
3422
- variant: "ghost",
3423
- className: "min-h-11",
3424
- testID: `movement-void-${row.id}`,
3425
- disabled: session?.status !== "open",
3426
- onPress: () => attempt(() => actions.voidMovement(row.id)),
3427
- children: /* @__PURE__ */ jsx81(Text, { children: "Undo" })
3428
- }
3429
- )
3430
- ] }, row.id)),
3431
- !!error && /* @__PURE__ */ jsx81(Text, { className: "text-destructive", children: error }),
3432
- /* @__PURE__ */ jsx81(
3433
- Button,
3434
- {
3435
- testID: "register-panel-close",
3436
- disabled: !session,
3437
- variant: "outline",
3438
- className: "min-h-14",
3439
- onPress: () => attempt(async () => {
3440
- await actions.startCounting();
3441
- onOpenChange(false);
3442
- }),
3443
- children: /* @__PURE__ */ jsx81(Text, { children: "Close register" })
3444
- }
3445
- )
3446
- ] }),
3447
- movementType && /* @__PURE__ */ jsx81(
3448
- MovementSheet,
3449
- {
3450
- register,
3451
- type: movementType,
3452
- currency,
3453
- onOpenChange: (value) => {
3454
- if (!value) setMovementType(null);
3455
- },
3456
- onDone: () => setMovementType(null)
3457
- }
3458
- )
3459
- ]
3460
- }
3461
- ) });
3462
- }
3463
-
3464
- // src/register/register-column.tsx
3465
3344
  import { useState as useState12 } from "react";
3466
- import { View as View48 } from "react-native";
3467
- import { Fragment as Fragment4, jsx as jsx82, jsxs as jsxs41 } from "react/jsx-runtime";
3468
- function RegisterColumn({
3469
- register,
3470
- registerId,
3471
- registers,
3472
- onPick,
3473
- currency,
3474
- configuredFloatMinor,
3475
- countSlot,
3476
- cartEmpty,
3477
- children
3478
- }) {
3479
- const [finishing, setFinishing] = useState12(false);
3480
- if (registerId === null) {
3481
- return /* @__PURE__ */ jsx82(RegisterPicker, { registers, onPick, className: "flex-1" });
3482
- }
3483
- if (register.session?.status === "closed") {
3484
- return register.closing && !finishing ? /* @__PURE__ */ jsx82(Fragment4, { children: countSlot }) : /* @__PURE__ */ jsx82(FinishClose, { register, busy: finishing, setBusy: setFinishing });
3485
- }
3486
- if (!register.session) {
3487
- return /* @__PURE__ */ jsx82(OpenRegisterCard, { register, currency, configuredFloatMinor, className: "flex-1" });
3488
- }
3489
- if (register.session.status === "counting") {
3490
- return /* @__PURE__ */ jsx82(Fragment4, { children: countSlot });
3491
- }
3492
- return /* @__PURE__ */ jsxs41(View48, { testID: "register-column", className: "flex-1", children: [
3493
- register.overdue && cartEmpty && /* @__PURE__ */ jsxs41(View48, { testID: "register-column-overdue", className: "bg-warning/10 flex-row items-center justify-between gap-2 px-3 py-2", children: [
3494
- /* @__PURE__ */ jsx82(Text, { className: "flex-1", children: "This register is overdue for closing." }),
3495
- /* @__PURE__ */ jsx82(
3496
- Button,
3497
- {
3498
- testID: "register-column-close-overdue",
3499
- variant: "outline",
3500
- className: "min-h-11",
3501
- onPress: () => register.actions.startCounting(),
3502
- children: /* @__PURE__ */ jsx82(Text, { children: "Close register" })
3503
- }
3504
- )
3505
- ] }),
3506
- children
3507
- ] });
3508
- }
3509
- function FinishClose({ register, busy, setBusy }) {
3510
- const [error, setError] = useState12("");
3511
- const finish = async () => {
3512
- setBusy(true);
3513
- setError("");
3514
- try {
3515
- await register.actions.closeSession({ counted: register.session?.counted ?? {} });
3516
- } catch (e) {
3517
- setError(e instanceof Error ? e.message : String(e));
3518
- } finally {
3519
- setBusy(false);
3520
- }
3521
- };
3522
- return /* @__PURE__ */ jsxs41(View48, { testID: "register-column-finish-close", className: "flex-1 gap-3 p-4", children: [
3523
- /* @__PURE__ */ jsx82(Text, { children: "The last close didn't finish." }),
3524
- /* @__PURE__ */ jsx82(Button, { testID: "register-column-finish-close-button", nativeID: "register-column-finish-close-button", disabled: busy, onPress: finish, children: /* @__PURE__ */ jsx82(Text, { children: "Finish closing" }) }),
3525
- !!error && /* @__PURE__ */ jsx82(Text, { testID: "register-column-finish-close-error", className: "text-destructive", children: error })
3526
- ] });
3527
- }
3345
+ import { ScrollView as ScrollView12, useWindowDimensions as useWindowDimensions2, View as View48 } from "react-native";
3346
+ import { formatMoney as formatMoney8 } from "@tallyui/core";
3528
3347
 
3529
3348
  // src/register/register-count.tsx
3530
- import { useEffect as useEffect3, useRef as useRef4, useState as useState13 } from "react";
3531
- import { Platform, Pressable as Pressable22, ScrollView as ScrollView12, useWindowDimensions as useWindowDimensions2, View as View49 } from "react-native";
3349
+ import { useEffect as useEffect3, useRef as useRef4, useState as useState11 } from "react";
3350
+ import { Platform, Pressable as Pressable22, ScrollView as ScrollView11, useWindowDimensions, View as View47 } from "react-native";
3532
3351
  import {
3533
3352
  closeNeedsApproval,
3534
3353
  countVariance,
@@ -3539,9 +3358,9 @@ import {
3539
3358
  validAmount as validAmount2,
3540
3359
  varianceText
3541
3360
  } from "@tallyui/pos";
3542
- import { currencySymbol as currencySymbol3, formatMoney as formatMoney8, minorUnitDigits as minorUnitDigits5 } from "@tallyui/core";
3361
+ import { currencySymbol as currencySymbol3, formatMoney as formatMoney7, minorUnitDigits as minorUnitDigits5 } from "@tallyui/core";
3543
3362
  import { cn as cn76 } from "@tallyui/theme";
3544
- import { jsx as jsx83, jsxs as jsxs42 } from "react/jsx-runtime";
3363
+ import { jsx as jsx81, jsxs as jsxs40 } from "react/jsx-runtime";
3545
3364
  var AMOUNT_LABEL_ID3 = "count-amount-label";
3546
3365
  var APPROVAL_REQUIRED_TEXT = "Manager approval needed. Ask a manager to approve, or count again.";
3547
3366
  function varianceWord(key) {
@@ -3562,7 +3381,7 @@ function tileLabel(value, currency) {
3562
3381
  }
3563
3382
  return formatter.format(value / 10 ** minorUnitDigits5(currency));
3564
3383
  }
3565
- var tenderLabel = (method) => method.charAt(0).toUpperCase() + method.slice(1);
3384
+ var tenderLabel = (method) => (/* @__PURE__ */ new Map([["cash", "Cash"], ["external", "Card"]])).get(method) ?? method.charAt(0).toUpperCase() + method.slice(1);
3566
3385
  function DenominationTile({ value, count: count2, currency, onAdd }) {
3567
3386
  const label = tileLabel(value, currency);
3568
3387
  const held = useRef4(false);
@@ -3572,7 +3391,7 @@ function DenominationTile({ value, count: count2, currency, onAdd }) {
3572
3391
  timer.current = void 0;
3573
3392
  };
3574
3393
  useEffect3(() => stop, []);
3575
- return /* @__PURE__ */ jsxs42(
3394
+ return /* @__PURE__ */ jsxs40(
3576
3395
  Pressable22,
3577
3396
  {
3578
3397
  testID: `den-tile-${value}`,
@@ -3594,8 +3413,8 @@ function DenominationTile({ value, count: count2, currency, onAdd }) {
3594
3413
  if (!held.current) onAdd(1);
3595
3414
  },
3596
3415
  children: [
3597
- /* @__PURE__ */ jsx83(Text, { className: "text-sm tabular-nums", numberOfLines: 1, children: label }),
3598
- /* @__PURE__ */ jsx83(Text, { testID: `den-count-${value}`, className: "bg-background min-w-6 rounded-full px-1.5 text-center text-xs tabular-nums", children: count2 })
3416
+ /* @__PURE__ */ jsx81(Text, { className: "text-sm tabular-nums", numberOfLines: 1, children: label }),
3417
+ /* @__PURE__ */ jsx81(Text, { testID: `den-count-${value}`, className: "bg-background min-w-6 rounded-full px-1.5 text-center text-xs tabular-nums", children: count2 })
3599
3418
  ]
3600
3419
  }
3601
3420
  );
@@ -3604,20 +3423,20 @@ function RegisterCount({ register, currency, approve, className }) {
3604
3423
  const { session, expected, blind, varianceThreshold, actions } = register;
3605
3424
  const digits = minorUnitDigits5(currency);
3606
3425
  const faces = denominations[currency] ?? denominations.default;
3607
- const { width } = useWindowDimensions2();
3426
+ const { width } = useWindowDimensions();
3608
3427
  const columns = width < 480 ? 3 : 4;
3609
- const [pieces, setPieces] = useState13({});
3610
- const [cashText, setCashText] = useState13(
3428
+ const [pieces, setPieces] = useState11({});
3429
+ const [cashText, setCashText] = useState11(
3611
3430
  () => session?.counted?.cash != null ? minorToDecimal(session.counted.cash, digits) : ""
3612
3431
  );
3613
- const [others, setOthers] = useState13(
3432
+ const [others, setOthers] = useState11(
3614
3433
  () => Object.fromEntries(
3615
3434
  Object.entries(session?.counted ?? {}).filter(([method]) => method !== "cash").map(([method, minor]) => [method, minorToDecimal(minor, digits)])
3616
3435
  )
3617
3436
  );
3618
- const [busy, setBusy] = useState13(false);
3619
- const [error, setError] = useState13("");
3620
- const [refusal, setRefusal] = useState13("");
3437
+ const [busy, setBusy] = useState11(false);
3438
+ const [error, setError] = useState11("");
3439
+ const [refusal, setRefusal] = useState11("");
3621
3440
  const usingTiles = Object.keys(pieces).length > 0;
3622
3441
  const tilesTotal = denominationTotal(pieces);
3623
3442
  const displayAmount = usingTiles ? minorToDecimal(tilesTotal, digits) : cashText;
@@ -3665,11 +3484,11 @@ function RegisterCount({ register, currency, approve, className }) {
3665
3484
  void attempt(() => actions.closeSession({ counted }));
3666
3485
  };
3667
3486
  if (!session) return null;
3668
- return /* @__PURE__ */ jsxs42(View49, { testID: "register-count", className: cn76("bg-card flex-1 rounded-md", className), children: [
3669
- /* @__PURE__ */ jsxs42(ScrollView12, { className: "flex-1", contentContainerClassName: "gap-3 p-4", children: [
3670
- /* @__PURE__ */ jsx83(Text, { className: "min-h-11 text-lg font-semibold", children: "Count the drawer" }),
3671
- /* @__PURE__ */ jsx83(Label, { testID: "count-amount-label", nativeID: AMOUNT_LABEL_ID3, children: `Cash counted (${currencySymbol3(currency)})` }),
3672
- /* @__PURE__ */ jsx83(Input, { children: /* @__PURE__ */ jsx83(
3487
+ return /* @__PURE__ */ jsxs40(View47, { testID: "register-count", className: cn76("bg-card flex-1 rounded-md", className), children: [
3488
+ /* @__PURE__ */ jsxs40(ScrollView11, { className: "flex-1", contentContainerClassName: "gap-3 p-4", children: [
3489
+ /* @__PURE__ */ jsx81(Text, { className: "min-h-11 text-lg font-semibold", children: "Count the drawer" }),
3490
+ /* @__PURE__ */ jsx81(Label, { testID: "count-amount-label", nativeID: AMOUNT_LABEL_ID3, children: `Cash counted (${currencySymbol3(currency)})` }),
3491
+ /* @__PURE__ */ jsx81(Input, { children: /* @__PURE__ */ jsx81(
3673
3492
  Input.Field,
3674
3493
  {
3675
3494
  testID: "count-amount",
@@ -3683,16 +3502,16 @@ function RegisterCount({ register, currency, approve, className }) {
3683
3502
  accessibilityLabelledBy: AMOUNT_LABEL_ID3
3684
3503
  }
3685
3504
  ) }),
3686
- !blind && valid && /* @__PURE__ */ jsx83(Text, { testID: "count-variance", className: "min-h-11 tabular-nums", children: `Expected ${formatMoney8({ amount: expected.cash ?? 0, currency })} \xB7 ${varianceText(
3505
+ !blind && valid && /* @__PURE__ */ jsx81(Text, { testID: "count-variance", className: "min-h-11 tabular-nums", children: `Expected ${formatMoney7({ amount: expected.cash ?? 0, currency })} \xB7 ${varianceText(
3687
3506
  variance,
3688
3507
  digits,
3689
- (major) => formatMoney8({ amount: Math.round(major * 10 ** digits), currency }) ?? "",
3508
+ (major) => formatMoney7({ amount: Math.round(major * 10 ** digits), currency }) ?? "",
3690
3509
  varianceWord
3691
3510
  )}` }),
3692
- /* @__PURE__ */ jsxs42(View49, { testID: "count-denominations", className: "gap-2", children: [
3693
- Array.from({ length: Math.ceil(faces.length / columns) }, (_, row) => /* @__PURE__ */ jsx83(View49, { className: "flex-row gap-2", children: Array.from({ length: columns }, (_2, column) => {
3511
+ /* @__PURE__ */ jsxs40(View47, { testID: "count-denominations", className: "gap-2", children: [
3512
+ Array.from({ length: Math.ceil(faces.length / columns) }, (_, row) => /* @__PURE__ */ jsx81(View47, { className: "flex-row gap-2", children: Array.from({ length: columns }, (_2, column) => {
3694
3513
  const value = faces[row * columns + column];
3695
- return value ? /* @__PURE__ */ jsx83(
3514
+ return value ? /* @__PURE__ */ jsx81(
3696
3515
  DenominationTile,
3697
3516
  {
3698
3517
  value,
@@ -3701,9 +3520,9 @@ function RegisterCount({ register, currency, approve, className }) {
3701
3520
  onAdd: (n) => addPieces(value, n)
3702
3521
  },
3703
3522
  value
3704
- ) : /* @__PURE__ */ jsx83(View49, { className: "flex-1" }, column);
3523
+ ) : /* @__PURE__ */ jsx81(View47, { className: "flex-1" }, column);
3705
3524
  }) }, row)),
3706
- /* @__PURE__ */ jsx83(
3525
+ /* @__PURE__ */ jsx81(
3707
3526
  Button,
3708
3527
  {
3709
3528
  testID: "count-clear",
@@ -3713,16 +3532,16 @@ function RegisterCount({ register, currency, approve, className }) {
3713
3532
  setPieces({});
3714
3533
  setCashText(minorToDecimal(0, digits));
3715
3534
  },
3716
- children: /* @__PURE__ */ jsx83(Text, { children: "Clear" })
3535
+ children: /* @__PURE__ */ jsx81(Text, { children: "Clear" })
3717
3536
  }
3718
3537
  )
3719
3538
  ] }),
3720
3539
  otherMethods.map((method) => {
3721
3540
  const labelId = `count-tender-${method}-label`;
3722
3541
  const label = `${tenderLabel(method)} counted (${currencySymbol3(currency)})`;
3723
- return /* @__PURE__ */ jsxs42(View49, { className: "gap-2", children: [
3724
- /* @__PURE__ */ jsx83(Label, { testID: labelId, nativeID: labelId, children: label }),
3725
- /* @__PURE__ */ jsx83(Input, { children: /* @__PURE__ */ jsx83(
3542
+ return /* @__PURE__ */ jsxs40(View47, { className: "gap-2", children: [
3543
+ /* @__PURE__ */ jsx81(Label, { testID: labelId, nativeID: labelId, children: label }),
3544
+ /* @__PURE__ */ jsx81(Input, { children: /* @__PURE__ */ jsx81(
3726
3545
  Input.Field,
3727
3546
  {
3728
3547
  testID: `count-tender-${method}`,
@@ -3735,11 +3554,11 @@ function RegisterCount({ register, currency, approve, className }) {
3735
3554
  ) })
3736
3555
  ] }, method);
3737
3556
  }),
3738
- !!refusal && /* @__PURE__ */ jsx83(Text, { testID: "count-manager-line", className: "text-warning min-h-11", children: refusal }),
3739
- !!error && /* @__PURE__ */ jsx83(Text, { testID: "count-error", className: "text-destructive min-h-11", children: error })
3557
+ !!refusal && /* @__PURE__ */ jsx81(Text, { testID: "count-manager-line", className: "text-warning min-h-11", children: refusal }),
3558
+ !!error && /* @__PURE__ */ jsx81(Text, { testID: "count-error", className: "text-destructive min-h-11", children: error })
3740
3559
  ] }),
3741
- /* @__PURE__ */ jsxs42(View49, { className: "gap-2 border-t border-border p-4", children: [
3742
- /* @__PURE__ */ jsx83(
3560
+ /* @__PURE__ */ jsxs40(View47, { className: "gap-2 border-t border-border p-4", children: [
3561
+ /* @__PURE__ */ jsx81(
3743
3562
  Button,
3744
3563
  {
3745
3564
  testID: "count-back",
@@ -3747,23 +3566,214 @@ function RegisterCount({ register, currency, approve, className }) {
3747
3566
  className: "min-h-11",
3748
3567
  disabled: busy,
3749
3568
  onPress: () => attempt(actions.backToSelling),
3750
- children: /* @__PURE__ */ jsx83(Text, { children: "Back to selling" })
3569
+ children: /* @__PURE__ */ jsx81(Text, { children: "Back to selling" })
3751
3570
  }
3752
3571
  ),
3753
- /* @__PURE__ */ jsx83(
3572
+ /* @__PURE__ */ jsx81(
3754
3573
  Button,
3755
3574
  {
3756
3575
  testID: "count-close",
3757
3576
  className: "min-h-14",
3758
3577
  disabled: !valid || otherTendersInvalid || busy,
3759
3578
  onPress: onClose,
3760
- children: /* @__PURE__ */ jsx83(Text, { children: busy ? "Closing\u2026" : "Close register" })
3579
+ children: /* @__PURE__ */ jsx81(Text, { children: busy ? "Closing\u2026" : "Close register" })
3761
3580
  }
3762
3581
  )
3763
3582
  ] })
3764
3583
  ] });
3765
3584
  }
3766
3585
 
3586
+ // src/register/register-panel.tsx
3587
+ import { jsx as jsx82, jsxs as jsxs41 } from "react/jsx-runtime";
3588
+ var DIALOG_VERTICAL_MARGIN = 64;
3589
+ function RegisterPanel({ register, currency, registerName, open, onOpenChange, className }) {
3590
+ const { session, expected, salesCount, blind, movements, lastClosure, actions } = register;
3591
+ const otherTenders = Object.entries(expected).filter(([method, amount]) => method !== "cash" && amount !== 0);
3592
+ const { height: windowHeight } = useWindowDimensions2();
3593
+ const maxHeight = Math.max(280, windowHeight - DIALOG_VERTICAL_MARGIN);
3594
+ const [movementType, setMovementType] = useState12(null);
3595
+ const [expanded, setExpanded] = useState12(false);
3596
+ const [error, setError] = useState12("");
3597
+ const attempt = async (action) => {
3598
+ try {
3599
+ await action();
3600
+ } catch (e) {
3601
+ setError(String(e));
3602
+ }
3603
+ };
3604
+ if (!session && !lastClosure) return null;
3605
+ const activeMovements = movements.filter(
3606
+ (row) => row.type !== "void" && !row.voided_by && !movements.some((entry) => entry.voids === row.id)
3607
+ );
3608
+ return /* @__PURE__ */ jsx82(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs41(
3609
+ DialogContent,
3610
+ {
3611
+ testID: "register-panel",
3612
+ className: `flex-col overflow-hidden ${className ?? ""}`,
3613
+ style: { maxHeight },
3614
+ children: [
3615
+ /* @__PURE__ */ jsxs41(View48, { className: "flex-row items-center justify-between gap-2", children: [
3616
+ /* @__PURE__ */ jsx82(DialogTitle, { testID: "register-panel-amount", children: registerName ?? "Register" }),
3617
+ /* @__PURE__ */ jsx82(DialogClose, { asChild: true, children: /* @__PURE__ */ jsx82(IconButton, { variant: "ghost", size: "sm", testID: "register-panel-dismiss", accessibilityLabel: "Close panel", children: /* @__PURE__ */ jsx82(XIcon, { size: 16 }) }) })
3618
+ ] }),
3619
+ /* @__PURE__ */ jsxs41(Text, { testID: "register-panel-sales-count", children: [
3620
+ salesCount,
3621
+ " sale",
3622
+ salesCount === 1 ? "" : "s",
3623
+ " this session"
3624
+ ] }),
3625
+ /* @__PURE__ */ jsxs41(ScrollView12, { testID: "register-panel-body", className: "flex-1", contentContainerClassName: "gap-3", children: [
3626
+ /* @__PURE__ */ jsx82(View48, { className: "flex-row gap-2", children: ["paid_in", "paid_out", "no_sale"].map((type) => /* @__PURE__ */ jsx82(
3627
+ Button,
3628
+ {
3629
+ testID: `register-panel-${type.replace("_", "-")}`,
3630
+ className: "min-h-14 flex-1",
3631
+ variant: "outline",
3632
+ disabled: session?.status !== "open",
3633
+ onPress: () => setMovementType(type),
3634
+ children: /* @__PURE__ */ jsx82(Text, { children: type === "paid_in" ? "Paid in" : type === "paid_out" ? "Paid out" : "No sale" })
3635
+ },
3636
+ type
3637
+ )) }),
3638
+ !blind && /* @__PURE__ */ jsxs41(View48, { testID: "register-panel-expected", className: "gap-1", children: [
3639
+ /* @__PURE__ */ jsx82(Text, { className: "text-muted-foreground text-sm font-semibold", children: "Expected in the drawer" }),
3640
+ Object.entries(expected).filter(([method]) => method === "cash").map(([method, amount]) => /* @__PURE__ */ jsxs41(View48, { className: "min-h-11 flex-row items-center justify-between", children: [
3641
+ /* @__PURE__ */ jsx82(Text, { children: tenderLabel("cash") }),
3642
+ /* @__PURE__ */ jsx82(Text, { className: "tabular-nums", children: formatMoney8({ amount, currency }) })
3643
+ ] }, method))
3644
+ ] }),
3645
+ !blind && otherTenders.length > 0 && /* @__PURE__ */ jsxs41(View48, { testID: "register-panel-other-tenders", className: "gap-1", children: [
3646
+ /* @__PURE__ */ jsx82(Text, { className: "text-muted-foreground text-sm font-semibold", children: "Other tenders" }),
3647
+ otherTenders.map(([method, amount]) => /* @__PURE__ */ jsxs41(View48, { className: "min-h-11 flex-row items-center justify-between", children: [
3648
+ /* @__PURE__ */ jsx82(Text, { children: tenderLabel(method) }),
3649
+ /* @__PURE__ */ jsx82(Text, { className: "tabular-nums", children: formatMoney8({ amount, currency }) })
3650
+ ] }, method))
3651
+ ] }),
3652
+ /* @__PURE__ */ jsx82(
3653
+ Button,
3654
+ {
3655
+ testID: "register-panel-movements",
3656
+ variant: "ghost",
3657
+ className: "min-h-11",
3658
+ onPress: () => setExpanded(!expanded),
3659
+ children: /* @__PURE__ */ jsx82(Text, { children: "Paid in / out" })
3660
+ }
3661
+ ),
3662
+ expanded && activeMovements.map((row) => /* @__PURE__ */ jsxs41(View48, { className: "min-h-11 flex-row items-center gap-2", children: [
3663
+ /* @__PURE__ */ jsxs41(Text, { testID: `movement-row-${row.id}`, className: "flex-1", children: [
3664
+ row.type === "paid_in" ? "Paid in" : row.type === "paid_out" ? "Paid out" : "No sale",
3665
+ !blind ? ` \xB7 ${formatMoney8({ amount: row.amountMinor, currency })} \xB7 ${row.reason}` : ""
3666
+ ] }),
3667
+ /* @__PURE__ */ jsx82(
3668
+ Button,
3669
+ {
3670
+ variant: "ghost",
3671
+ className: "min-h-11",
3672
+ testID: `movement-void-${row.id}`,
3673
+ disabled: session?.status !== "open",
3674
+ onPress: () => attempt(() => actions.voidMovement(row.id)),
3675
+ children: /* @__PURE__ */ jsx82(Text, { children: "Undo" })
3676
+ }
3677
+ )
3678
+ ] }, row.id)),
3679
+ !!error && /* @__PURE__ */ jsx82(Text, { className: "text-destructive", children: error }),
3680
+ /* @__PURE__ */ jsx82(
3681
+ Button,
3682
+ {
3683
+ testID: "register-panel-close",
3684
+ disabled: !session,
3685
+ variant: "outline",
3686
+ className: "min-h-14",
3687
+ onPress: () => attempt(async () => {
3688
+ await actions.startCounting();
3689
+ onOpenChange(false);
3690
+ }),
3691
+ children: /* @__PURE__ */ jsx82(Text, { children: "Close register" })
3692
+ }
3693
+ )
3694
+ ] }),
3695
+ movementType && /* @__PURE__ */ jsx82(
3696
+ MovementSheet,
3697
+ {
3698
+ register,
3699
+ type: movementType,
3700
+ currency,
3701
+ onOpenChange: (value) => {
3702
+ if (!value) setMovementType(null);
3703
+ },
3704
+ onDone: () => setMovementType(null)
3705
+ }
3706
+ )
3707
+ ]
3708
+ }
3709
+ ) });
3710
+ }
3711
+
3712
+ // src/register/register-column.tsx
3713
+ import { useState as useState13 } from "react";
3714
+ import { View as View49 } from "react-native";
3715
+ import { Fragment as Fragment4, jsx as jsx83, jsxs as jsxs42 } from "react/jsx-runtime";
3716
+ function RegisterColumn({
3717
+ register,
3718
+ registerId,
3719
+ registers,
3720
+ onPick,
3721
+ currency,
3722
+ configuredFloatMinor,
3723
+ countSlot,
3724
+ cartEmpty,
3725
+ children
3726
+ }) {
3727
+ const [finishing, setFinishing] = useState13(false);
3728
+ if (registerId === null) {
3729
+ return /* @__PURE__ */ jsx83(RegisterPicker, { registers, onPick, className: "flex-1" });
3730
+ }
3731
+ if (register.session?.status === "closed") {
3732
+ return register.closing && !finishing ? /* @__PURE__ */ jsx83(Fragment4, { children: countSlot }) : /* @__PURE__ */ jsx83(FinishClose, { register, busy: finishing, setBusy: setFinishing });
3733
+ }
3734
+ if (!register.session) {
3735
+ return /* @__PURE__ */ jsx83(OpenRegisterCard, { register, currency, configuredFloatMinor, className: "flex-1" });
3736
+ }
3737
+ if (register.session.status === "counting") {
3738
+ return /* @__PURE__ */ jsx83(Fragment4, { children: countSlot });
3739
+ }
3740
+ return /* @__PURE__ */ jsxs42(View49, { testID: "register-column", className: "flex-1", children: [
3741
+ register.overdue && cartEmpty && /* @__PURE__ */ jsxs42(View49, { testID: "register-column-overdue", className: "bg-warning/10 flex-row items-center justify-between gap-2 px-3 py-2", children: [
3742
+ /* @__PURE__ */ jsx83(Text, { className: "flex-1", children: "This register is overdue for closing." }),
3743
+ /* @__PURE__ */ jsx83(
3744
+ Button,
3745
+ {
3746
+ testID: "register-column-close-overdue",
3747
+ variant: "outline",
3748
+ className: "min-h-11",
3749
+ onPress: () => register.actions.startCounting(),
3750
+ children: /* @__PURE__ */ jsx83(Text, { children: "Close register" })
3751
+ }
3752
+ )
3753
+ ] }),
3754
+ children
3755
+ ] });
3756
+ }
3757
+ function FinishClose({ register, busy, setBusy }) {
3758
+ const [error, setError] = useState13("");
3759
+ const finish = async () => {
3760
+ setBusy(true);
3761
+ setError("");
3762
+ try {
3763
+ await register.actions.closeSession({ counted: register.session?.counted ?? {} });
3764
+ } catch (e) {
3765
+ setError(e instanceof Error ? e.message : String(e));
3766
+ } finally {
3767
+ setBusy(false);
3768
+ }
3769
+ };
3770
+ return /* @__PURE__ */ jsxs42(View49, { testID: "register-column-finish-close", className: "flex-1 gap-3 p-4", children: [
3771
+ /* @__PURE__ */ jsx83(Text, { children: "The last close didn't finish." }),
3772
+ /* @__PURE__ */ jsx83(Button, { testID: "register-column-finish-close-button", nativeID: "register-column-finish-close-button", disabled: busy, onPress: finish, children: /* @__PURE__ */ jsx83(Text, { children: "Finish closing" }) }),
3773
+ !!error && /* @__PURE__ */ jsx83(Text, { testID: "register-column-finish-close-error", className: "text-destructive", children: error })
3774
+ ] });
3775
+ }
3776
+
3767
3777
  // src/register/closure-sheet.tsx
3768
3778
  import { useEffect as useEffect4, useRef as useRef5, useState as useState14 } from "react";
3769
3779
  import { AccessibilityInfo, Animated, View as View50 } from "react-native";
@@ -4250,8 +4260,8 @@ function laterOf(a, b) {
4250
4260
  if (!b) return a;
4251
4261
  return a.getTime() >= b.getTime() ? a : b;
4252
4262
  }
4253
- function Catalogue({ products, traits, currency, onSelect, statusText, statusAccessory, lastSyncedAt, loading, lastStockCheckAt, hour12, minCodeLength }) {
4254
- const isLoading = loading ?? lastSyncedAt === null;
4263
+ function Catalogue({ products, traits, currency, onSelect, statusText, statusAccessory, lastSyncedAt, loading, pullError, lastStockCheckAt, hour12, minCodeLength }) {
4264
+ const isLoading = loading ?? (lastSyncedAt === null && pullError == null);
4255
4265
  const shown = useStockOverlaid(products);
4256
4266
  const overlayAsOf = useStockOverlayAsOf();
4257
4267
  const parsedOverlayAsOf = overlayAsOf ? new Date(overlayAsOf) : void 0;
@@ -4261,7 +4271,7 @@ function Catalogue({ products, traits, currency, onSelect, statusText, statusAcc
4261
4271
  const [chooserId, setChooserId] = useState17(null);
4262
4272
  const [width, setWidth] = useState17(0);
4263
4273
  const columns = Math.max(2, Math.min(6, Math.floor((width - 8) / (MIN_TILE_WIDTH + 8))));
4264
- const entries = useMemo2(() => catalogueEntries(shown, traits), [shown, traits]);
4274
+ const entries = useMemo2(() => catalogueEntries(shown, traits, { currency }), [shown, traits, currency]);
4265
4275
  const results = useMemo2(() => searchProducts(shown, query, traits), [shown, query, traits]);
4266
4276
  const choices = useMemo2(() => chooserId === null ? [] : entries.filter((entry) => traits.getId(entry.product) === chooserId), [entries, chooserId, traits]);
4267
4277
  useEffect5(() => {
@@ -5074,6 +5084,7 @@ export {
5074
5084
  iconVariants,
5075
5085
  injectPrintStyle,
5076
5086
  loaderVariants,
5087
+ orderReference,
5077
5088
  parseDiscount,
5078
5089
  textVariants,
5079
5090
  toastVariants,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/components",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "type": "module",
5
5
  "description": "Universal POS UI components for Tally UI",
6
6
  "main": "./dist/index.js",
@@ -27,22 +27,22 @@
27
27
  "directory": "packages/components"
28
28
  },
29
29
  "peerDependencies": {
30
- "@tallyui/core": "^3.0.2",
31
- "@tallyui/primitives": "^3.0.2",
32
- "@tallyui/theme": "^3.0.2",
30
+ "@tallyui/core": "^3.0.4",
31
+ "@tallyui/primitives": "^3.0.4",
32
+ "@tallyui/theme": "^3.0.4",
33
33
  "react": ">=18",
34
34
  "react-native": ">=0.74",
35
35
  "react-native-svg": ">=15"
36
36
  },
37
37
  "dependencies": {
38
38
  "class-variance-authority": "^0.7.1",
39
- "@tallyui/pos": "3.0.2"
39
+ "@tallyui/pos": "3.0.4"
40
40
  },
41
41
  "devDependencies": {
42
- "@tallyui/connector-medusa": "3.0.2",
43
- "@tallyui/connector-vendure": "3.0.2",
44
- "@tallyui/connector-woocommerce": "3.0.2",
45
- "@tallyui/mock-api": "3.0.2",
42
+ "@tallyui/connector-medusa": "3.0.4",
43
+ "@tallyui/connector-vendure": "3.0.4",
44
+ "@tallyui/connector-woocommerce": "3.0.4",
45
+ "@tallyui/mock-api": "3.0.4",
46
46
  "@types/react": ">=18",
47
47
  "react": "^19.2.4",
48
48
  "react-native-svg": "15.12.1",
package/src/index.ts CHANGED
@@ -155,7 +155,7 @@ export {
155
155
  // Sale components
156
156
  export {
157
157
  Cart, CartBar, Tender, DiscountForm, DiscountChips, parseDiscount, discountLabel,
158
- Catalogue, formatStockSyncTime, Receipt, injectPrintStyle, SyncStatus, OrdersList,
158
+ Catalogue, formatStockSyncTime, Receipt, injectPrintStyle, SyncStatus, OrdersList, orderReference,
159
159
  } from './sale';
160
160
 
161
161
  // Layout components
@@ -52,8 +52,11 @@ function tileLabel(value: number, currency: string): string {
52
52
  return formatter.format(value / 10 ** minorUnitDigits(currency));
53
53
  }
54
54
 
55
- /** Title-cases a tender method key ('card' -> 'Card') for a plain-English label. Shared with `ClosureSheet`. */
56
- export const tenderLabel = (method: string) => method.charAt(0).toUpperCase() + method.slice(1);
55
+ /** Plain-English label for a tender method key: `external` is the till's card tender ('Card');
56
+ * other keys are title-cased. Shared with `ClosureSheet` and `RegisterPanel`. */
57
+ export const tenderLabel = (method: string) =>
58
+ new Map([['cash', 'Cash'], ['external', 'Card']]).get(method) ??
59
+ method.charAt(0).toUpperCase() + method.slice(1);
57
60
 
58
61
  interface DenominationTileProps {
59
62
  value: number;
@@ -9,6 +9,7 @@ import { IconButton } from '../ui/icon-button';
9
9
  import { XIcon } from '../ui/icon/x-icon';
10
10
  import { Text } from '../ui/text';
11
11
  import { MovementSheet } from './movement-sheet';
12
+ import { tenderLabel } from './register-count';
12
13
 
13
14
  /** Margin kept clear above and below the dialog (LEDGER-free layout fix, 2026-09-28 review):
14
15
  * a phone-height viewport must never let the panel's top go under the page header. */
@@ -37,6 +38,7 @@ export interface RegisterPanelProps {
37
38
  */
38
39
  export function RegisterPanel({ register, currency, registerName, open, onOpenChange, className }: RegisterPanelProps) {
39
40
  const { session, expected, salesCount, blind, movements, lastClosure, actions } = register;
41
+ const otherTenders = Object.entries(expected).filter(([method, amount]) => method !== 'cash' && amount !== 0);
40
42
  const { height: windowHeight } = useWindowDimensions();
41
43
  const maxHeight = Math.max(280, windowHeight - DIALOG_VERTICAL_MARGIN);
42
44
  const [movementType, setMovementType] = useState<MovementType | null>(null);
@@ -93,9 +95,20 @@ export function RegisterPanel({ register, currency, registerName, open, onOpenCh
93
95
  {!blind && (
94
96
  <View testID="register-panel-expected" className="gap-1">
95
97
  <Text className="text-muted-foreground text-sm font-semibold">Expected in the drawer</Text>
96
- {Object.entries(expected).map(([method, amount]) => (
98
+ {Object.entries(expected).filter(([method]) => method === 'cash').map(([method, amount]) => (
97
99
  <View key={method} className="min-h-11 flex-row items-center justify-between">
98
- <Text className="capitalize">{method === 'cash' ? 'Cash' : method}</Text>
100
+ <Text>{tenderLabel('cash')}</Text>
101
+ <Text className="tabular-nums">{formatMoney({ amount, currency })}</Text>
102
+ </View>
103
+ ))}
104
+ </View>
105
+ )}
106
+ {!blind && otherTenders.length > 0 && (
107
+ <View testID="register-panel-other-tenders" className="gap-1">
108
+ <Text className="text-muted-foreground text-sm font-semibold">Other tenders</Text>
109
+ {otherTenders.map(([method, amount]) => (
110
+ <View key={method} className="min-h-11 flex-row items-center justify-between">
111
+ <Text>{tenderLabel(method)}</Text>
99
112
  <Text className="tabular-nums">{formatMoney({ amount, currency })}</Text>
100
113
  </View>
101
114
  ))}
@@ -32,7 +32,7 @@ function laterOf(a: Date | null | undefined, b: Date | null | undefined): Date |
32
32
  return a.getTime() >= b.getTime() ? a : b;
33
33
  }
34
34
 
35
- export function Catalogue<Doc>({ products, traits, currency, onSelect, statusText, statusAccessory, lastSyncedAt, loading, lastStockCheckAt, hour12, minCodeLength }: {
35
+ export function Catalogue<Doc>({ products, traits, currency, onSelect, statusText, statusAccessory, lastSyncedAt, loading, pullError, lastStockCheckAt, hour12, minCodeLength }: {
36
36
  products: Doc[];
37
37
  traits: ProductTraits<Doc>;
38
38
  currency: string;
@@ -42,9 +42,12 @@ export function Catalogue<Doc>({ products, traits, currency, onSelect, statusTex
42
42
  /** Rendered at the end of the status line, e.g. a register control at phone width; the row renders even without `statusText`. */
43
43
  statusAccessory?: ReactNode;
44
44
  lastSyncedAt: Date | null;
45
- /** The catalogue's first pull is still running, so an empty grid says "Loading products…" rather than "No products yet.". Defaults to lastSyncedAt === null; pass false
45
+ /** The catalogue's first pull is still running, so an empty grid says "Loading products…" rather than "No products yet.". Defaults to lastSyncedAt === null with no pullError; pass false
46
46
  * when a null lastSyncedAt doesn't mean a pull is running (e.g. offline on first start). */
47
47
  loading?: boolean;
48
+ /** The product pull's last error (an Error, a SyncNotice, anything the app holds), or null/undefined when none. While it is
49
+ * set, a catalogue that has never synced isn't treated as loading, so its empty grid says "No products yet." beside the app's error. */
50
+ pullError?: unknown;
48
51
  /** The last completed stock reconcile pass, persisted across restarts. The "stock as of" time shown is the
49
52
  * latest of this, the provider's stockOverlayAsOf (when valid) and lastSyncedAt; null and omitted alike. */
50
53
  lastStockCheckAt?: Date | null;
@@ -56,7 +59,7 @@ export function Catalogue<Doc>({ products, traits, currency, onSelect, statusTex
56
59
  * in its unfocused wedge listener — not this prop's job. */
57
60
  minCodeLength?: number;
58
61
  }) {
59
- const isLoading = loading ?? lastSyncedAt === null;
62
+ const isLoading = loading ?? (lastSyncedAt === null && pullError == null);
60
63
  // Idempotent (the adapter's overlay returns just the fields the stock map sets), so an app that already
61
64
  // merges the overlay itself keeps working, and can drop its own merge.
62
65
  const shown = useStockOverlaid(products) as Doc[];
@@ -71,7 +74,7 @@ export function Catalogue<Doc>({ products, traits, currency, onSelect, statusTex
71
74
  const [width, setWidth] = useState(0);
72
75
  // ProductGrid has 4 px padding on each side of the content and each cell.
73
76
  const columns = Math.max(2, Math.min(6, Math.floor((width - 8) / (MIN_TILE_WIDTH + 8))));
74
- const entries = useMemo(() => catalogueEntries(shown, traits), [shown, traits]);
77
+ const entries = useMemo(() => catalogueEntries(shown, traits, { currency }), [shown, traits, currency]);
75
78
  const results = useMemo(() => searchProducts(shown, query, traits), [shown, query, traits]);
76
79
  const choices = useMemo(() => (chooserId === null ? []
77
80
  : entries.filter((entry) => traits.getId(entry.product) === chooserId)), [entries, chooserId, traits]);