@tallyui/components 3.0.0-next.0 → 3.0.0-next.2
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 +1 -1
- package/dist/index.js +18 -7
- package/package.json +9 -9
- package/src/customer/customer-picker.tsx +4 -2
- package/src/product/product-stock-badge.tsx +1 -1
- package/src/sale/catalogue.tsx +1 -1
- package/src/sale/orders-list.tsx +2 -1
- package/src/sale/since-text.ts +7 -0
- package/src/sale/sync-status.tsx +11 -8
package/dist/index.d.ts
CHANGED
|
@@ -737,7 +737,7 @@ interface CustomerPickerProps {
|
|
|
737
737
|
create?: (input: CustomerInput) => Promise<Customer>;
|
|
738
738
|
selected: Customer | null;
|
|
739
739
|
onSelect: (customer: Customer | null) => void;
|
|
740
|
-
/** Called for errors the picker can't explain to the cashier, including ConnectorUnauthorizedError
|
|
740
|
+
/** Called for errors the picker can't explain to the cashier, including `ConnectorUnauthorizedError`: sign in again only when its `code` is `'unauthorized'` (401). `'forbidden'` (403) means signed in but not allowed; never sign out for it. */
|
|
741
741
|
onError?: (error: unknown) => void;
|
|
742
742
|
/** false disables search and create (online only in v1). */
|
|
743
743
|
online: boolean;
|
package/dist/index.js
CHANGED
|
@@ -1734,7 +1734,7 @@ var STATUS_STYLES = {
|
|
|
1734
1734
|
};
|
|
1735
1735
|
function formatAsOf(iso) {
|
|
1736
1736
|
const date = new Date(iso);
|
|
1737
|
-
return date.toDateString() === (/* @__PURE__ */ new Date()).toDateString() ? date.toLocaleTimeString(void 0, { hour: "
|
|
1737
|
+
return date.toDateString() === (/* @__PURE__ */ new Date()).toDateString() ? date.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" }) : date.toLocaleDateString(void 0, { day: "numeric", month: "short" });
|
|
1738
1738
|
}
|
|
1739
1739
|
function ProductStockBadge({ doc, showQuantity = false, showAsOf = true, className, ...props }) {
|
|
1740
1740
|
const { status, quantity, asOf } = useProductStock(doc);
|
|
@@ -2445,7 +2445,7 @@ function CustomerPicker({ search, create, selected, onSelect, onError, online, d
|
|
|
2445
2445
|
if (error2 instanceof CustomerServiceError) {
|
|
2446
2446
|
setError(error2.code === "network" ? "Couldn't reach the store. Try again." : error2.code === "server" ? "The store couldn't do that. Try again." : error2.message);
|
|
2447
2447
|
} else {
|
|
2448
|
-
setError(error2 instanceof ConnectorUnauthorizedError ? "Sign in again to continue." : "Something went wrong. Try again.");
|
|
2448
|
+
setError(error2 instanceof ConnectorUnauthorizedError && error2.code === "unauthorized" ? "Sign in again to continue." : error2 instanceof ConnectorUnauthorizedError && error2.code === "forbidden" ? "Your account isn't allowed to do this on this store. Ask the store owner." : "Something went wrong. Try again.");
|
|
2449
2449
|
onError?.(error2);
|
|
2450
2450
|
}
|
|
2451
2451
|
}
|
|
@@ -4229,7 +4229,7 @@ var STOCK_LABEL = {
|
|
|
4229
4229
|
unknown: "Unknown"
|
|
4230
4230
|
};
|
|
4231
4231
|
function formatStockSyncTime(time, locale, hour12, now = /* @__PURE__ */ new Date()) {
|
|
4232
|
-
const timeLabel = time.toLocaleTimeString(locale, { hour: "
|
|
4232
|
+
const timeLabel = time.toLocaleTimeString(locale, { hour: "numeric", minute: "2-digit", hour12 });
|
|
4233
4233
|
if (time.toDateString() === now.toDateString()) return timeLabel;
|
|
4234
4234
|
const dateLabel = time.toLocaleDateString(locale, { day: "numeric", month: "short" });
|
|
4235
4235
|
return `${dateLabel}, ${timeLabel}`;
|
|
@@ -4428,12 +4428,24 @@ function Receipt({
|
|
|
4428
4428
|
// src/sale/sync-status.tsx
|
|
4429
4429
|
import { useEffect as useEffect7, useRef as useRef6, useState as useState18 } from "react";
|
|
4430
4430
|
import { AccessibilityInfo as AccessibilityInfo2, Platform as Platform3, Text as Text38, View as View59 } from "react-native";
|
|
4431
|
+
|
|
4432
|
+
// src/sale/since-text.ts
|
|
4433
|
+
function sinceText(clock) {
|
|
4434
|
+
const time = (t) => new Date(t).toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
|
|
4435
|
+
return clock.firstFailedAt === void 0 ? "about " + time(clock.since) : time(clock.firstFailedAt);
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4438
|
+
// src/sale/sync-status.tsx
|
|
4431
4439
|
import { Fragment as Fragment7, jsx as jsx93, jsxs as jsxs52 } from "react/jsx-runtime";
|
|
4432
4440
|
var PULL_NOTICE_TEXT = {
|
|
4433
4441
|
unauthorized: {
|
|
4434
4442
|
line: () => "Products aren't updating: this till needs to sign in to the online store again.",
|
|
4435
4443
|
detail: () => "You can keep selling. Products, prices and stock stay as they were until someone signs in again."
|
|
4436
4444
|
},
|
|
4445
|
+
forbidden: {
|
|
4446
|
+
line: () => "Products aren't updating: your account isn't allowed to do this on this store.",
|
|
4447
|
+
detail: () => "You can keep selling. Ask the store owner."
|
|
4448
|
+
},
|
|
4437
4449
|
till_update_required: {
|
|
4438
4450
|
line: () => "Products aren't updating: this till needs updating.",
|
|
4439
4451
|
detail: () => "You can keep selling. Products, prices and stock stay as they were until this till is updated."
|
|
@@ -4463,7 +4475,7 @@ var BACKEND_MISSING_DETAIL = "This till couldn't find {pluginName} on the online
|
|
|
4463
4475
|
var REFUSED = (n, waiting) => n === 1 ? `1 sale needs attention${waiting} \xB7 The online store refused it. Ask the store owner to look at the till's sync log.` : `${n} sales need attention${waiting} \xB7 The online store refused them. Ask the store owner to look at the till's sync log.`;
|
|
4464
4476
|
var REFUSED_DETAIL = "Refused sales stay on this till under Needs attention, each with what to do next.";
|
|
4465
4477
|
var SALES_REFUSED = "The online store refused the last send. This till will try again with the next sale, or when the app is reopened.";
|
|
4466
|
-
var TILL_UPDATES_REFUSED = "The online store refused the last send. This till will try again with the next till update.";
|
|
4478
|
+
var TILL_UPDATES_REFUSED = "The online store refused the last send. This till will try again with the next till update, or when the app is reopened.";
|
|
4467
4479
|
var count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
4468
4480
|
function useAnnounceOnIos(...texts) {
|
|
4469
4481
|
const announced = useRef6(texts);
|
|
@@ -4490,8 +4502,7 @@ function SyncStatus({ state, registerState, pluginName = "the POS plugin", pullN
|
|
|
4490
4502
|
].filter(Boolean).join(" and ") + " waiting to sync";
|
|
4491
4503
|
const { stuck } = state;
|
|
4492
4504
|
const backendMissing = state.backendMissing ?? registerState?.backendMissing;
|
|
4493
|
-
const
|
|
4494
|
-
const backendMissingText = state.pending === 0 && updates > 0 ? registerState?.stuck ? TILL_UPDATES_MISSING_SINCE.replace("{time}", () => at(registerState.stuck.since)) : TILL_UPDATES_MISSING : stuck ? BACKEND_MISSING_SINCE.replace("{time}", () => at(stuck.since)) : BACKEND_MISSING;
|
|
4505
|
+
const backendMissingText = state.pending === 0 && updates > 0 ? registerState?.stuck ? TILL_UPDATES_MISSING_SINCE.replace("{time}", () => sinceText(registerState.stuck)) : TILL_UPDATES_MISSING : stuck ? BACKEND_MISSING_SINCE.replace("{time}", () => sinceText(stuck)) : BACKEND_MISSING;
|
|
4495
4506
|
const upToDate = state.pending === 0 && updates === 0;
|
|
4496
4507
|
const sentence = outbox.refused ? ` \xB7 ${outbox === state ? SALES_REFUSED : TILL_UPDATES_REFUSED}` : backendMissing || (state.pending === 0 && updates > 0 ? registerState?.stuck : stuck) ? ` \xB7 ${backendMissingText}` : "";
|
|
4497
4508
|
const refused = state.rejected ? REFUSED(state.rejected, upToDate ? "" : `, ${updates ? label : `${state.pending} waiting to sync`}`) : "";
|
|
@@ -4569,7 +4580,7 @@ function OrdersList({ orders, onRetry, formatDate = defaultFormatDate2, footer,
|
|
|
4569
4580
|
" \xB7 ",
|
|
4570
4581
|
STATUS_LABEL[order.syncStatus]
|
|
4571
4582
|
] }),
|
|
4572
|
-
stuckEntry ? /* @__PURE__ */ jsx94(Text39, { className: "text-destructive", children: `Hasn't reached the online store since ${
|
|
4583
|
+
stuckEntry ? /* @__PURE__ */ jsx94(Text39, { className: "text-destructive", children: `Hasn't reached the online store since ${sinceText(stuckEntry)}.` }) : null,
|
|
4573
4584
|
order.syncStatus === "rejected" ? /* @__PURE__ */ jsx94(Text39, { className: "text-destructive", children: refusalSentence(order.error?.code) }) : null,
|
|
4574
4585
|
order.lateSessionId !== void 0 ? /* @__PURE__ */ jsx94(Text39, { className: "text-foreground", children: "Taken after the register closed. It is not in that register's closure." }) : null,
|
|
4575
4586
|
order.localWarnings?.map((warning, index) => /* @__PURE__ */ jsx94(Text39, { className: "text-foreground", children: warning.code === "customer_omitted" ? `The customer's ${warning.field} couldn't be sent to the store, so the order isn't linked to them.` : "The terminal's payment reference couldn't be kept; the payment is recorded without it." }, index)),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tallyui/components",
|
|
3
|
-
"version": "3.0.0-next.
|
|
3
|
+
"version": "3.0.0-next.2",
|
|
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.0-next.
|
|
31
|
-
"@tallyui/primitives": "^3.0.0-next.
|
|
32
|
-
"@tallyui/theme": "^3.0.0-next.
|
|
30
|
+
"@tallyui/core": "^3.0.0-next.2",
|
|
31
|
+
"@tallyui/primitives": "^3.0.0-next.2",
|
|
32
|
+
"@tallyui/theme": "^3.0.0-next.2",
|
|
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.0-next.
|
|
39
|
+
"@tallyui/pos": "3.0.0-next.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@tallyui/connector-medusa": "3.0.0-next.
|
|
43
|
-
"@tallyui/connector-vendure": "3.0.0-next.
|
|
44
|
-
"@tallyui/connector-woocommerce": "3.0.0-next.
|
|
45
|
-
"@tallyui/mock-api": "3.0.0-next.
|
|
42
|
+
"@tallyui/connector-medusa": "3.0.0-next.2",
|
|
43
|
+
"@tallyui/connector-vendure": "3.0.0-next.2",
|
|
44
|
+
"@tallyui/connector-woocommerce": "3.0.0-next.2",
|
|
45
|
+
"@tallyui/mock-api": "3.0.0-next.2",
|
|
46
46
|
"@types/react": ">=18",
|
|
47
47
|
"react": "^19.2.4",
|
|
48
48
|
"react-native-svg": "15.12.1",
|
|
@@ -13,7 +13,7 @@ export interface CustomerPickerProps {
|
|
|
13
13
|
create?: (input: CustomerInput) => Promise<Customer>;
|
|
14
14
|
selected: Customer | null;
|
|
15
15
|
onSelect: (customer: Customer | null) => void;
|
|
16
|
-
/** Called for errors the picker can't explain to the cashier, including ConnectorUnauthorizedError
|
|
16
|
+
/** Called for errors the picker can't explain to the cashier, including `ConnectorUnauthorizedError`: sign in again only when its `code` is `'unauthorized'` (401). `'forbidden'` (403) means signed in but not allowed; never sign out for it. */
|
|
17
17
|
onError?: (error: unknown) => void;
|
|
18
18
|
/** false disables search and create (online only in v1). */
|
|
19
19
|
online: boolean;
|
|
@@ -44,7 +44,9 @@ export function CustomerPicker({ search, create, selected, onSelect, onError, on
|
|
|
44
44
|
setError(error.code === 'network' ? "Couldn't reach the store. Try again."
|
|
45
45
|
: error.code === 'server' ? "The store couldn't do that. Try again." : error.message);
|
|
46
46
|
} else {
|
|
47
|
-
setError(error instanceof ConnectorUnauthorizedError ? 'Sign in again to continue.'
|
|
47
|
+
setError(error instanceof ConnectorUnauthorizedError && error.code === 'unauthorized' ? 'Sign in again to continue.'
|
|
48
|
+
: error instanceof ConnectorUnauthorizedError && error.code === 'forbidden' ? "Your account isn't allowed to do this on this store. Ask the store owner."
|
|
49
|
+
: 'Something went wrong. Try again.');
|
|
48
50
|
onError?.(error);
|
|
49
51
|
}
|
|
50
52
|
}
|
|
@@ -49,7 +49,7 @@ const STATUS_STYLES: Record<StockStatus, { badge: string; dot: string; text: str
|
|
|
49
49
|
function formatAsOf(iso: string): string {
|
|
50
50
|
const date = new Date(iso);
|
|
51
51
|
return date.toDateString() === new Date().toDateString()
|
|
52
|
-
? date.toLocaleTimeString(undefined, { hour: '
|
|
52
|
+
? date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })
|
|
53
53
|
: date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
|
|
54
54
|
}
|
|
55
55
|
|
package/src/sale/catalogue.tsx
CHANGED
|
@@ -16,7 +16,7 @@ const STOCK_LABEL = {
|
|
|
16
16
|
// function) so the formatting is testable without the device; callers default them to the
|
|
17
17
|
// device's own settings and the current moment.
|
|
18
18
|
export function formatStockSyncTime(time: Date, locale?: string, hour12?: boolean, now: Date = new Date()): string {
|
|
19
|
-
const timeLabel = time.toLocaleTimeString(locale, { hour: '
|
|
19
|
+
const timeLabel = time.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit', hour12 });
|
|
20
20
|
// "Today" is decided in the device's own timezone: toDateString() reads the Date in local time,
|
|
21
21
|
// the same timezone the device clock and toLocaleTimeString above already use.
|
|
22
22
|
if (time.toDateString() === now.toDateString()) return timeLabel;
|
package/src/sale/orders-list.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
|
2
2
|
import { Pressable, ScrollView, Text, View } from 'react-native';
|
|
3
3
|
import { formatMoney, knownWarnings } from '@tallyui/core';
|
|
4
4
|
import { needsAttention, type OutboxState, type PosOrder } from '@tallyui/pos';
|
|
5
|
+
import { sinceText } from './since-text';
|
|
5
6
|
|
|
6
7
|
const STATUS_LABEL = { pending: 'Waiting to sync', applied: 'Synced', rejected: 'Not accepted' };
|
|
7
8
|
/** `figures_mismatch`'s labels; a field a newer store sends that isn't here is shown by its raw name. */
|
|
@@ -61,7 +62,7 @@ export function OrdersList({ orders, onRetry, formatDate = defaultFormatDate, fo
|
|
|
61
62
|
<Text className="text-muted-foreground">{formatDate(order.createdAt)} · {formatMoney({ amount: order.totalMinor, currency: order.currency })} · {STATUS_LABEL[order.syncStatus]}</Text>
|
|
62
63
|
{stuckEntry ? <Text className="text-destructive">
|
|
63
64
|
{/* The same words for any reason, with no reason code; the hour as the status line has it (numeric, #245). */}
|
|
64
|
-
{`Hasn't reached the online store since ${
|
|
65
|
+
{`Hasn't reached the online store since ${sinceText(stuckEntry)}.`}
|
|
65
66
|
</Text> : null}
|
|
66
67
|
{/* The code's sentence alone: the cashier sees no error code and never the store's message. */}
|
|
67
68
|
{order.syncStatus === 'rejected' ? <Text className="text-destructive">{refusalSentence(order.error?.code)}</Text> : null}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The time a stuck line names: the real first failure when this run has one, else "about" the virtual since (after a restart
|
|
2
|
+
// firstFailedAt is gone; see OutboxState.stuck).
|
|
3
|
+
export function sinceText(clock: { since: number; firstFailedAt?: number }): string {
|
|
4
|
+
// The device's own 12/24-hour format, with no forced leading zero on the hour.
|
|
5
|
+
const time = (t: number) => new Date(t).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
|
|
6
|
+
return clock.firstFailedAt === undefined ? 'about ' + time(clock.since) : time(clock.firstFailedAt);
|
|
7
|
+
}
|
package/src/sale/sync-status.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { AccessibilityInfo, Platform, Text, View } from 'react-native';
|
|
3
3
|
import type { SyncNotice } from '@tallyui/core';
|
|
4
4
|
import type { OutboxState } from '@tallyui/pos';
|
|
5
|
+
import { sinceText } from './since-text';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* What the cashier reads when the product pull stops, keyed by the notice's code; the code itself is never shown.
|
|
@@ -14,6 +15,10 @@ const PULL_NOTICE_TEXT: Record<string, PullNoticeText> = {
|
|
|
14
15
|
line: () => "Products aren't updating: this till needs to sign in to the online store again.",
|
|
15
16
|
detail: () => 'You can keep selling. Products, prices and stock stay as they were until someone signs in again.',
|
|
16
17
|
},
|
|
18
|
+
forbidden: {
|
|
19
|
+
line: () => "Products aren't updating: your account isn't allowed to do this on this store.",
|
|
20
|
+
detail: () => 'You can keep selling. Ask the store owner.',
|
|
21
|
+
},
|
|
17
22
|
till_update_required: {
|
|
18
23
|
line: () => "Products aren't updating: this till needs updating.",
|
|
19
24
|
detail: () => 'You can keep selling. Products, prices and stock stay as they were until this till is updated.',
|
|
@@ -43,7 +48,7 @@ const PULL_NOTICE_FALLBACK: PullNoticeText = {
|
|
|
43
48
|
|
|
44
49
|
// The line the cashier reads while the store keeps answering 404 (OutboxState.backendMissing) and no order is stuck.
|
|
45
50
|
const BACKEND_MISSING = "Sales aren't reaching the online store. Keep selling: they're saved on this till and will send by themselves.";
|
|
46
|
-
// The same line once an order is stuck, with the store missing or not; {time}
|
|
51
|
+
// The same line once an order is stuck, with the store missing or not; {time} is the real first failure or about the virtual since.
|
|
47
52
|
const BACKEND_MISSING_SINCE = "Sales haven't reached the online store since {time}. Keep selling: they're saved on this till and will send by themselves.";
|
|
48
53
|
// The line instead when no sale waits but till updates do, and its "since" variant once registerState.stuck is set.
|
|
49
54
|
const TILL_UPDATES_MISSING = "Till updates aren't reaching the online store. Keep selling: they're saved on this till and will send by themselves.";
|
|
@@ -62,10 +67,10 @@ const REFUSED = (n: number, waiting: string) => n === 1
|
|
|
62
67
|
const REFUSED_DETAIL = 'Refused sales stay on this till under Needs attention, each with what to do next.';
|
|
63
68
|
// The sentence after the waiting count once the store refused a whole batch (OutboxState.refused) from the outbox shown, when
|
|
64
69
|
// no command carries a code. Neither outbox sets a timer then: a new pending command flushes its outbox. For sales,
|
|
65
|
-
// useOrderOutbox
|
|
66
|
-
//
|
|
70
|
+
// useOrderOutbox calls start() when its store opens and useRegisterOutbox when its collection opens, so both flush
|
|
71
|
+
// on reopen and both sentences name a reopen.
|
|
67
72
|
const SALES_REFUSED = 'The online store refused the last send. This till will try again with the next sale, or when the app is reopened.';
|
|
68
|
-
const TILL_UPDATES_REFUSED = 'The online store refused the last send. This till will try again with the next till update.';
|
|
73
|
+
const TILL_UPDATES_REFUSED = 'The online store refused the last send. This till will try again with the next till update, or when the app is reopened.';
|
|
69
74
|
|
|
70
75
|
const count = (n: number, noun: string) => `${n} ${noun}${n === 1 ? '' : 's'}`;
|
|
71
76
|
// iOS has no live region (accessibilityLiveRegion is Android-only), so there each text is announced when it changes: never on
|
|
@@ -99,11 +104,9 @@ export function SyncStatus({ state, registerState, pluginName = 'the POS plugin'
|
|
|
99
104
|
updates && count(updates, 'till update')].filter(Boolean).join(' and ') + ' waiting to sync';
|
|
100
105
|
const { stuck } = state;
|
|
101
106
|
const backendMissing = state.backendMissing ?? registerState?.backendMissing;
|
|
102
|
-
// The device's own 12/24-hour format, with no forced leading zero on the hour.
|
|
103
|
-
const at = (time: number) => new Date(time).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
|
|
104
107
|
const backendMissingText = state.pending === 0 && updates > 0 ? (registerState?.stuck
|
|
105
|
-
? TILL_UPDATES_MISSING_SINCE.replace('{time}', () =>
|
|
106
|
-
: stuck ? BACKEND_MISSING_SINCE.replace('{time}', () =>
|
|
108
|
+
? TILL_UPDATES_MISSING_SINCE.replace('{time}', () => sinceText(registerState.stuck!)) : TILL_UPDATES_MISSING)
|
|
109
|
+
: stuck ? BACKEND_MISSING_SINCE.replace('{time}', () => sinceText(stuck)) : BACKEND_MISSING;
|
|
107
110
|
const upToDate = state.pending === 0 && updates === 0;
|
|
108
111
|
// The sentence shows with the store missing, or once the waiting sales (with none waiting, the till updates) are stuck, then as
|
|
109
112
|
// its "since" variant: the same words for any reason, and no reason code.
|