@tallyui/components 1.0.0 → 2.0.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/LICENSE +21 -0
- package/dist/index.d.ts +1076 -76
- package/dist/index.js +4662 -355
- package/package.json +27 -5
- package/src/cart/cart-line-actions.tsx +40 -0
- package/src/cart/cart-line.tsx +18 -32
- package/src/cart/cart-note-input.tsx +46 -0
- package/src/cart/cart-panel.tsx +39 -26
- package/src/cart/cart-total.tsx +43 -47
- package/src/cart/discount-badge.tsx +19 -0
- package/src/cart/index.ts +4 -1
- package/src/checkout/cash-tendered.tsx +101 -0
- package/src/checkout/change-display.tsx +19 -0
- package/src/checkout/index.ts +3 -0
- package/src/checkout/order-summary-line.tsx +7 -8
- package/src/checkout/order-summary.tsx +8 -7
- package/src/checkout/payment-method-card.tsx +10 -9
- package/src/checkout/payment-selector.tsx +5 -5
- package/src/checkout/receipt-preview.tsx +78 -0
- package/src/customer/customer-card.tsx +8 -9
- package/src/customer/customer-form.tsx +70 -0
- package/src/customer/customer-order-history.tsx +34 -0
- package/src/customer/customer-select.tsx +63 -24
- package/src/customer/index.ts +2 -0
- package/src/index.ts +112 -1
- package/src/input/filter-chip.tsx +85 -27
- package/src/input/index.ts +1 -0
- package/src/input/quantity-stepper.tsx +64 -0
- package/src/input/search-input.tsx +30 -7
- package/src/layout/checkout-layout.tsx +46 -0
- package/src/layout/index.ts +5 -0
- package/src/layout/live-tab-screen.tsx +54 -0
- package/src/layout/pos-layout.tsx +64 -0
- package/src/layout/settings-layout.tsx +58 -0
- package/src/layout/store-settings-choice-screen.tsx +142 -0
- package/src/order/index.ts +4 -0
- package/src/order/order-card.tsx +48 -0
- package/src/order/order-detail.tsx +82 -0
- package/src/order/order-list.tsx +39 -0
- package/src/order/order-status-badge.tsx +32 -0
- package/src/product/category-nav.tsx +70 -0
- package/src/product/index.ts +3 -0
- package/src/product/product-card.tsx +36 -8
- package/src/product/product-grid.tsx +25 -6
- package/src/product/product-image.tsx +28 -7
- package/src/product/product-list.tsx +44 -0
- package/src/product/product-price.tsx +77 -22
- package/src/product/product-sku.tsx +5 -5
- package/src/product/product-stock-badge.tsx +41 -28
- package/src/product/product-title.tsx +2 -3
- package/src/product/product-variant-picker.tsx +61 -0
- package/src/register/cash-count-input.tsx +85 -0
- package/src/register/closure-sheet.tsx +155 -0
- package/src/register/index.ts +12 -0
- package/src/register/movement-sheet.tsx +118 -0
- package/src/register/open-register-card.tsx +109 -0
- package/src/register/register-bar.helpers.ts +57 -0
- package/src/register/register-bar.tsx +85 -0
- package/src/register/register-column.tsx +117 -0
- package/src/register/register-count.tsx +330 -0
- package/src/register/register-open-close.tsx +52 -0
- package/src/register/register-panel.tsx +151 -0
- package/src/register/register-picker.tsx +60 -0
- package/src/register/register-summary.tsx +72 -0
- package/src/sale/cart-bar.tsx +18 -0
- package/src/sale/cart.tsx +66 -0
- package/src/sale/catalogue.tsx +131 -0
- package/src/sale/discount-form.tsx +85 -0
- package/src/sale/index.ts +9 -0
- package/src/sale/orders-list.tsx +66 -0
- package/src/sale/print-style.ts +11 -0
- package/src/sale/receipt.tsx +61 -0
- package/src/sale/sync-status.tsx +20 -0
- package/src/sale/tender.tsx +35 -0
- package/src/settings/connector-status.tsx +65 -0
- package/src/settings/index.ts +3 -0
- package/src/settings/settings-group.tsx +23 -0
- package/src/settings/settings-row.tsx +23 -0
- package/src/ui/accordion/index.tsx +59 -0
- package/src/ui/alert-dialog/index.tsx +91 -0
- package/src/ui/avatar/index.tsx +50 -0
- package/src/ui/badge/index.tsx +60 -0
- package/src/ui/button/index.tsx +90 -0
- package/src/ui/card/index.tsx +60 -0
- package/src/ui/checkbox/index.tsx +29 -0
- package/src/ui/collapsible/index.tsx +24 -0
- package/src/ui/combobox/index.tsx +98 -0
- package/src/ui/context-menu/index.tsx +159 -0
- package/src/ui/dialog/index.tsx +92 -0
- package/src/ui/dropdown-menu/index.tsx +161 -0
- package/src/ui/hover-card/index.tsx +31 -0
- package/src/ui/hstack/index.tsx +43 -0
- package/src/ui/icon/index.tsx +44 -0
- package/src/ui/icon/search-icon.tsx +9 -0
- package/src/ui/icon/svg-icon.tsx +36 -0
- package/src/ui/icon-button/index.tsx +58 -0
- package/src/ui/index.ts +99 -0
- package/src/ui/input/index.tsx +100 -0
- package/src/ui/label/index.tsx +26 -0
- package/src/ui/loader/index.tsx +37 -0
- package/src/ui/popover/index.tsx +31 -0
- package/src/ui/progress/index.tsx +28 -0
- package/src/ui/radio-group/index.tsx +38 -0
- package/src/ui/select/index.tsx +96 -0
- package/src/ui/separator/index.tsx +29 -0
- package/src/ui/slider/index.tsx +28 -0
- package/src/ui/switch/index.tsx +31 -0
- package/src/ui/table/index.tsx +59 -0
- package/src/ui/tabs/index.tsx +53 -0
- package/src/ui/text/index.tsx +36 -0
- package/src/ui/textarea/index.tsx +29 -0
- package/src/ui/toast/index.tsx +52 -0
- package/src/ui/toggle/index.tsx +69 -0
- package/src/ui/toggle-group/index.tsx +66 -0
- package/src/ui/tooltip/index.tsx +30 -0
- package/src/ui/vstack/index.tsx +43 -0
- package/src/uniwind-env.d.ts +9 -1
- package/src/__tests__/cart-line.test.tsx +0 -53
- package/src/__tests__/cart-panel.test.tsx +0 -55
- package/src/__tests__/cart-total.test.tsx +0 -60
- package/src/__tests__/customer-card.test.tsx +0 -47
- package/src/__tests__/customer-select.test.tsx +0 -33
- package/src/__tests__/filter-chip.test.tsx +0 -40
- package/src/__tests__/helpers.ts +0 -111
- package/src/__tests__/order-summary-line.test.tsx +0 -18
- package/src/__tests__/order-summary.test.tsx +0 -32
- package/src/__tests__/payment-method-card.test.tsx +0 -24
- package/src/__tests__/payment-selector.test.tsx +0 -23
- package/src/__tests__/product-card.test.tsx +0 -31
- package/src/__tests__/product-grid.test.tsx +0 -37
- package/src/__tests__/product-image.test.tsx +0 -54
- package/src/__tests__/product-price.test.tsx +0 -68
- package/src/__tests__/product-sku.test.tsx +0 -47
- package/src/__tests__/product-stock-badge.test.tsx +0 -58
- package/src/__tests__/product-title.test.tsx +0 -38
- package/src/__tests__/search-input.test.tsx +0 -22
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import type { useRegisterSession } from '@tallyui/pos';
|
|
4
|
+
import { parseMinor, validAmount } from '@tallyui/pos';
|
|
5
|
+
|
|
6
|
+
import { currencySymbol, formatMoney, minorUnitDigits } from '@tallyui/core';
|
|
7
|
+
import { cn } from '@tallyui/theme';
|
|
8
|
+
import { Button } from '../ui/button';
|
|
9
|
+
import { Input } from '../ui/input';
|
|
10
|
+
import { Label } from '../ui/label';
|
|
11
|
+
import { Text } from '../ui/text';
|
|
12
|
+
|
|
13
|
+
const AMOUNT_LABEL_ID = 'open-register-amount-label';
|
|
14
|
+
|
|
15
|
+
export interface OpenRegisterCardProps {
|
|
16
|
+
/** `useRegisterSession`'s return value: opens the session through `register.actions.openSession`. */
|
|
17
|
+
register: ReturnType<typeof useRegisterSession>;
|
|
18
|
+
/** ISO 4217 code, for parsing and formatting the typed amount. */
|
|
19
|
+
currency: string;
|
|
20
|
+
/** The store's configured opening float, minor units; preferred over the last counted cash. */
|
|
21
|
+
configuredFloatMinor?: number | null;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Opens a register session, prefilled with the configured float or the last counted cash.
|
|
27
|
+
* Port provenance (ADR-032 amendment 1): WCPOS `next` `3b5331b5c` `open-register-card.tsx`
|
|
28
|
+
* (LEDGER 52).
|
|
29
|
+
*
|
|
30
|
+
* The chip choices are already resolved once `expectedFloatMinor` is computed: a late-arriving
|
|
31
|
+
* `configuredFloatMinor` or `register.lastClosure` never overwrites what the cashier already
|
|
32
|
+
* typed, because `enteredAmount` only ever starts from it, never re-derives from it.
|
|
33
|
+
*/
|
|
34
|
+
export function OpenRegisterCard({ register, currency, configuredFloatMinor, className }: OpenRegisterCardProps) {
|
|
35
|
+
const digits = minorUnitDigits(currency);
|
|
36
|
+
const lastCountedMinor = register.lastClosure?.counted?.cash ?? null;
|
|
37
|
+
const expectedFloatMinor = configuredFloatMinor ?? lastCountedMinor ?? null;
|
|
38
|
+
const [enteredAmount, setEnteredAmount] = useState<string | null>(null);
|
|
39
|
+
const amount = enteredAmount ?? (expectedFloatMinor != null ? decimal(expectedFloatMinor, digits) : '');
|
|
40
|
+
const [busy, setBusy] = useState(false);
|
|
41
|
+
const [error, setError] = useState('');
|
|
42
|
+
const valid = validAmount(amount, digits);
|
|
43
|
+
const countedMinor = valid ? parseMinor(amount, digits) : NaN;
|
|
44
|
+
const open = async () => {
|
|
45
|
+
if (busy || !valid) return;
|
|
46
|
+
setBusy(true);
|
|
47
|
+
setError('');
|
|
48
|
+
try {
|
|
49
|
+
await register.actions.openSession({ expectedFloatMinor, countedFloatMinor: countedMinor });
|
|
50
|
+
} catch (e) {
|
|
51
|
+
setError(String(e));
|
|
52
|
+
} finally {
|
|
53
|
+
setBusy(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const amountLabel = `Cash in the drawer to start (${currencySymbol(currency)})`;
|
|
57
|
+
return (
|
|
58
|
+
<View testID="open-register-card" className={cn('bg-card gap-3 rounded-md p-4', className)}>
|
|
59
|
+
<Text className="text-lg font-semibold">Open register</Text>
|
|
60
|
+
<Label testID="open-register-amount-label" nativeID={AMOUNT_LABEL_ID}>
|
|
61
|
+
{amountLabel}
|
|
62
|
+
</Label>
|
|
63
|
+
<Input>
|
|
64
|
+
<Input.Field
|
|
65
|
+
testID="open-register-amount"
|
|
66
|
+
value={amount}
|
|
67
|
+
onChangeText={setEnteredAmount}
|
|
68
|
+
keyboardType="decimal-pad"
|
|
69
|
+
accessibilityLabel={amountLabel}
|
|
70
|
+
accessibilityLabelledBy={AMOUNT_LABEL_ID}
|
|
71
|
+
/>
|
|
72
|
+
</Input>
|
|
73
|
+
{configuredFloatMinor != null && (
|
|
74
|
+
<Button
|
|
75
|
+
testID="open-register-chip-configured"
|
|
76
|
+
variant="outline"
|
|
77
|
+
className="min-h-11"
|
|
78
|
+
onPress={() => setEnteredAmount(decimal(configuredFloatMinor, digits))}
|
|
79
|
+
>
|
|
80
|
+
<Text>Use the configured float ({formatMoney({ amount: configuredFloatMinor, currency })})</Text>
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
83
|
+
{lastCountedMinor != null && (
|
|
84
|
+
<Button
|
|
85
|
+
testID="open-register-chip-last"
|
|
86
|
+
variant="outline"
|
|
87
|
+
className="min-h-11"
|
|
88
|
+
onPress={() => setEnteredAmount(decimal(lastCountedMinor, digits))}
|
|
89
|
+
>
|
|
90
|
+
<Text>Use the last counted cash ({formatMoney({ amount: lastCountedMinor, currency })})</Text>
|
|
91
|
+
</Button>
|
|
92
|
+
)}
|
|
93
|
+
{expectedFloatMinor !== null && valid && countedMinor !== expectedFloatMinor && (
|
|
94
|
+
<Text testID="opening-variance">
|
|
95
|
+
{formatMoney({ amount: countedMinor - expectedFloatMinor, currency })} vs the expected float
|
|
96
|
+
</Text>
|
|
97
|
+
)}
|
|
98
|
+
{!!error && <Text className="text-destructive">{error}</Text>}
|
|
99
|
+
<Button testID="open-register-button" className="min-h-14" disabled={busy || !valid} onPress={open}>
|
|
100
|
+
<Text>{busy ? 'Opening…' : 'Open register'}</Text>
|
|
101
|
+
</Button>
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function decimal(minor: number, digits: number): string {
|
|
107
|
+
const scale = 10 ** digits;
|
|
108
|
+
return (minor / scale).toFixed(digits);
|
|
109
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The register bar's status pill: one pill, one place. Port provenance (ADR-032 amendment 1):
|
|
3
|
+
* WCPOS `next` `3b5331b5c` `register-bar.helpers.ts` (LEDGER 48).
|
|
4
|
+
*
|
|
5
|
+
* Neutral changes: WCPOS decides "choosing a register" from a WooCommerce binding status
|
|
6
|
+
* (`bindingStatus: 'bound' | 'choose' | 'none' | 'unknown'`) and folds the store's display name
|
|
7
|
+
* into `place`. TallyUI's binding is entirely the app's own (ADR-032 amendment 1's "Not ported"
|
|
8
|
+
* note), so this takes the plain `registerId` the app already passes to `useRegisterSession`
|
|
9
|
+
* (`null` means unbound) instead, and drops the store name — RegisterBar here shows only the
|
|
10
|
+
* register name (`multiRegister` decides whether that's shown at all) and the status pill.
|
|
11
|
+
* Copy is plain English (no i18n keys, ADR-064's neutral-copy rule).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type RegisterBarPill =
|
|
15
|
+
| 'Choose a register'
|
|
16
|
+
| 'Close not finished'
|
|
17
|
+
| 'Offline'
|
|
18
|
+
| 'Approval needed'
|
|
19
|
+
| 'Counting'
|
|
20
|
+
| 'Overdue'
|
|
21
|
+
| 'Register closed';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Priority, matching WCPOS's ordering: choosing a register outranks everything (LEDGER 48);
|
|
25
|
+
* then a closed session whose close isn't in flight (TallyUI: it blocks selling until Finish
|
|
26
|
+
* closing); then an unreachable store reads as offline, ahead of any session state; approval needed
|
|
27
|
+
* outranks plain counting; an open, overdue session outranks a session-less "closed" pill.
|
|
28
|
+
*/
|
|
29
|
+
export function describeRegisterBarPill({
|
|
30
|
+
registerId,
|
|
31
|
+
online,
|
|
32
|
+
sessionStatus,
|
|
33
|
+
overdue = false,
|
|
34
|
+
approvalRequired = false,
|
|
35
|
+
sessionsOn = false,
|
|
36
|
+
closing = false,
|
|
37
|
+
}: {
|
|
38
|
+
/** The register this till is bound to, or `null` when it isn't bound yet. */
|
|
39
|
+
registerId: string | null;
|
|
40
|
+
online: boolean;
|
|
41
|
+
sessionStatus?: 'open' | 'counting' | 'closed' | null;
|
|
42
|
+
overdue?: boolean;
|
|
43
|
+
approvalRequired?: boolean;
|
|
44
|
+
/** The store uses register sessions at all (`useRegisterSession`'s `enabled`). */
|
|
45
|
+
sessionsOn?: boolean;
|
|
46
|
+
/** A close is in flight (`useRegisterSession`'s `closing`), so a closed session isn't unfinished. */
|
|
47
|
+
closing?: boolean;
|
|
48
|
+
}): RegisterBarPill | null {
|
|
49
|
+
if (registerId === null) return 'Choose a register';
|
|
50
|
+
if (sessionStatus === 'closed' && !closing) return 'Close not finished';
|
|
51
|
+
if (!online) return 'Offline';
|
|
52
|
+
if (sessionStatus === 'counting' && !approvalRequired) return 'Counting';
|
|
53
|
+
if (approvalRequired) return 'Approval needed';
|
|
54
|
+
if (sessionStatus === 'open' && overdue) return 'Overdue';
|
|
55
|
+
if (sessionsOn && !sessionStatus) return 'Register closed';
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Pressable, View } from 'react-native';
|
|
2
|
+
import type { useRegisterSession } from '@tallyui/pos';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
import { Badge } from '../ui/badge';
|
|
6
|
+
import { Button } from '../ui/button';
|
|
7
|
+
import { HStack } from '../ui/hstack';
|
|
8
|
+
import { Text } from '../ui/text';
|
|
9
|
+
import { describeRegisterBarPill } from './register-bar.helpers';
|
|
10
|
+
|
|
11
|
+
export interface RegisterBarProps {
|
|
12
|
+
/** `useRegisterSession`'s return value: the session's status, `overdue`, `enabled` and so on. */
|
|
13
|
+
register: ReturnType<typeof useRegisterSession>;
|
|
14
|
+
/** The register this till is bound to, or `null` when no register is chosen yet. */
|
|
15
|
+
registerId: string | null;
|
|
16
|
+
/** `false` when the store, or the app itself, can't be reached — reads to the cashier as offline. */
|
|
17
|
+
online: boolean;
|
|
18
|
+
/** Hidden when `multiRegister` is `false`: a single-register store doesn't need to name it. */
|
|
19
|
+
registerName?: string;
|
|
20
|
+
multiRegister: boolean;
|
|
21
|
+
onOpenPanel: () => void;
|
|
22
|
+
/** Turns the pill into a button (opens the gate/picker or the panel) when given; the plain badge otherwise. */
|
|
23
|
+
onPressPill?: () => void;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The till's one status bar: which register (multi-register stores only), one status pill
|
|
29
|
+
* (LEDGER 48), and the button that opens the register panel. Port provenance (ADR-032 amendment
|
|
30
|
+
* 1): WCPOS `next` `3b5331b5c` `register-bar.tsx`.
|
|
31
|
+
*
|
|
32
|
+
* Dropped for this neutral port: the user avatar and user sheet, switch-store, and the
|
|
33
|
+
* WooCommerce OAuth-return rehosting — all app/backend concerns `useRegisterSession` never
|
|
34
|
+
* carries. The drawer/menu button is gone too: TallyUI has no fixed navigation shell to open.
|
|
35
|
+
*/
|
|
36
|
+
export function RegisterBar({
|
|
37
|
+
register,
|
|
38
|
+
registerId,
|
|
39
|
+
online,
|
|
40
|
+
registerName,
|
|
41
|
+
multiRegister,
|
|
42
|
+
onOpenPanel,
|
|
43
|
+
onPressPill,
|
|
44
|
+
className,
|
|
45
|
+
}: RegisterBarProps) {
|
|
46
|
+
const { session, overdue, enabled, lastClosure, closing } = register;
|
|
47
|
+
const pill = describeRegisterBarPill({
|
|
48
|
+
registerId,
|
|
49
|
+
online,
|
|
50
|
+
sessionStatus: session?.status ?? null,
|
|
51
|
+
overdue,
|
|
52
|
+
approvalRequired: session?.approval_required,
|
|
53
|
+
sessionsOn: enabled,
|
|
54
|
+
closing,
|
|
55
|
+
});
|
|
56
|
+
return (
|
|
57
|
+
<HStack testID="register-bar" className={cn('bg-card border-border h-12 border-b px-3', className)}>
|
|
58
|
+
<View className="min-w-0 flex-1 shrink">
|
|
59
|
+
{multiRegister && registerId !== null && (
|
|
60
|
+
<Text testID="register-bar-name" numberOfLines={1}>
|
|
61
|
+
{registerName}
|
|
62
|
+
</Text>
|
|
63
|
+
)}
|
|
64
|
+
</View>
|
|
65
|
+
{pill && (onPressPill ? (
|
|
66
|
+
<Pressable testID="register-bar-pill" accessibilityRole="button" accessibilityLabel={pill} onPress={onPressPill}>
|
|
67
|
+
<Badge label={pill} variant="warning" />
|
|
68
|
+
</Pressable>
|
|
69
|
+
) : (
|
|
70
|
+
<Badge testID="register-bar-pill" label={pill} variant="warning" />
|
|
71
|
+
))}
|
|
72
|
+
{(session || lastClosure) && (
|
|
73
|
+
<Button
|
|
74
|
+
variant="ghost"
|
|
75
|
+
size="sm"
|
|
76
|
+
testID="register-bar-open-panel"
|
|
77
|
+
accessibilityLabel="Open register panel"
|
|
78
|
+
onPress={onOpenPanel}
|
|
79
|
+
>
|
|
80
|
+
<Text className={overdue ? 'text-warning' : undefined}>Register ›</Text>
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
83
|
+
</HStack>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import type { useRegisterSession } from '@tallyui/pos';
|
|
4
|
+
|
|
5
|
+
import { Button } from '../ui/button';
|
|
6
|
+
import { Text } from '../ui/text';
|
|
7
|
+
import { OpenRegisterCard } from './open-register-card';
|
|
8
|
+
import { RegisterPicker, type RegisterPickerRegister } from './register-picker';
|
|
9
|
+
|
|
10
|
+
export interface RegisterColumnProps {
|
|
11
|
+
/** `useRegisterSession`'s return value: decides the picker/open/counting/selling swap. */
|
|
12
|
+
register: ReturnType<typeof useRegisterSession>;
|
|
13
|
+
/** The register this till is bound to, the same value passed into `useRegisterSession`; `null`
|
|
14
|
+
* means unbound, since the hook's own return carries no such field (it's the app's input, not
|
|
15
|
+
* its output). */
|
|
16
|
+
registerId: string | null;
|
|
17
|
+
registers: RegisterPickerRegister[];
|
|
18
|
+
onPick: (id: string) => void;
|
|
19
|
+
/** ISO 4217 code, forwarded to `OpenRegisterCard`. */
|
|
20
|
+
currency: string;
|
|
21
|
+
configuredFloatMinor?: number | null;
|
|
22
|
+
/** Rendered instead of `children` while a session is counting (Job B fills this), and while its close is in flight
|
|
23
|
+
* (`register.closing`). A closed session that isn't closing shows the Finish-closing card instead, whose button has
|
|
24
|
+
* `nativeID="register-column-finish-close-button"`, so an app's `onPressPill` can focus it on the web. */
|
|
25
|
+
countSlot?: ReactNode;
|
|
26
|
+
/** Whether the cart has no lines; gates the overdue Close register offer (LEDGER 53). */
|
|
27
|
+
cartEmpty?: boolean;
|
|
28
|
+
/** The cart, shown once a register is bound, open, and not counting. */
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Swaps the cart column's content for register selection, opening or counting when the till
|
|
34
|
+
* needs it, offering Close register when an open session runs overdue with nothing left to
|
|
35
|
+
* sell. Port provenance (ADR-032 amendment 1): WCPOS `next` `3b5331b5c`'s cart-column register
|
|
36
|
+
* swap (LEDGER 53), composed here as one small component rather than inline cart-screen logic.
|
|
37
|
+
*/
|
|
38
|
+
export function RegisterColumn({
|
|
39
|
+
register,
|
|
40
|
+
registerId,
|
|
41
|
+
registers,
|
|
42
|
+
onPick,
|
|
43
|
+
currency,
|
|
44
|
+
configuredFloatMinor,
|
|
45
|
+
countSlot,
|
|
46
|
+
cartEmpty,
|
|
47
|
+
children,
|
|
48
|
+
}: RegisterColumnProps) {
|
|
49
|
+
const [finishing, setFinishing] = useState(false); // Lifted, so the card stays up through its own close.
|
|
50
|
+
if (registerId === null) {
|
|
51
|
+
return <RegisterPicker registers={registers} onPick={onPick} className="flex-1" />;
|
|
52
|
+
}
|
|
53
|
+
if (register.session?.status === 'closed') {
|
|
54
|
+
// A close in flight stores the session closed before its closure lands: unless the card started it, not an unfinished close.
|
|
55
|
+
return register.closing && !finishing ? <>{countSlot}</> : <FinishClose register={register} busy={finishing} setBusy={setFinishing} />;
|
|
56
|
+
}
|
|
57
|
+
if (!register.session) {
|
|
58
|
+
return <OpenRegisterCard register={register} currency={currency} configuredFloatMinor={configuredFloatMinor} className="flex-1" />;
|
|
59
|
+
}
|
|
60
|
+
if (register.session.status === 'counting') {
|
|
61
|
+
return <>{countSlot}</>;
|
|
62
|
+
}
|
|
63
|
+
return (
|
|
64
|
+
<View testID="register-column" className="flex-1">
|
|
65
|
+
{register.overdue && cartEmpty && (
|
|
66
|
+
<View testID="register-column-overdue" className="bg-warning/10 flex-row items-center justify-between gap-2 px-3 py-2">
|
|
67
|
+
<Text className="flex-1">This register is overdue for closing.</Text>
|
|
68
|
+
<Button
|
|
69
|
+
testID="register-column-close-overdue"
|
|
70
|
+
variant="outline"
|
|
71
|
+
className="min-h-11"
|
|
72
|
+
onPress={() => register.actions.startCounting()}
|
|
73
|
+
>
|
|
74
|
+
<Text>Close register</Text>
|
|
75
|
+
</Button>
|
|
76
|
+
</View>
|
|
77
|
+
)}
|
|
78
|
+
{children}
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* `currentSession` (`use-register-session.ts` ~110-125) can return a closed session whose closure
|
|
85
|
+
* row was never written: an unapplied reservation's session, or a closed session with no closure
|
|
86
|
+
* row. `openSession` then refuses with `RegisterCloseIncompleteError`, so the cashier is stuck
|
|
87
|
+
* behind the cart unless this offers a way to resume the close.
|
|
88
|
+
*/
|
|
89
|
+
function FinishClose({ register, busy, setBusy }: { register: ReturnType<typeof useRegisterSession>; busy: boolean; setBusy: (busy: boolean) => void }) {
|
|
90
|
+
const [error, setError] = useState('');
|
|
91
|
+
const finish = async () => {
|
|
92
|
+
setBusy(true);
|
|
93
|
+
setError('');
|
|
94
|
+
try {
|
|
95
|
+
// A resumed close: the store keeps the count persisted on the session (a retry's count is
|
|
96
|
+
// ignored for a session already closed), and the approval gate doesn't run again.
|
|
97
|
+
await register.actions.closeSession({ counted: register.session?.counted ?? {} });
|
|
98
|
+
} catch (e) {
|
|
99
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
100
|
+
} finally {
|
|
101
|
+
setBusy(false);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
return (
|
|
105
|
+
<View testID="register-column-finish-close" className="flex-1 gap-3 p-4">
|
|
106
|
+
<Text>The last close didn't finish.</Text>
|
|
107
|
+
<Button testID="register-column-finish-close-button" nativeID="register-column-finish-close-button" disabled={busy} onPress={finish}>
|
|
108
|
+
<Text>Finish closing</Text>
|
|
109
|
+
</Button>
|
|
110
|
+
{!!error && (
|
|
111
|
+
<Text testID="register-column-finish-close-error" className="text-destructive">
|
|
112
|
+
{error}
|
|
113
|
+
</Text>
|
|
114
|
+
)}
|
|
115
|
+
</View>
|
|
116
|
+
);
|
|
117
|
+
}
|