@classytic/pos-ui 0.1.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/README.md +38 -0
- package/dist/components/CartSidebar.js +222 -0
- package/dist/components/PosTopBar.js +156 -0
- package/dist/components/PrinterSettingsDialog.js +163 -0
- package/dist/components/ReceiptReprintDialog.js +182 -0
- package/dist/dashboard/components/CustomerLookupDialog.js +120 -0
- package/dist/dashboard/components/CustomerQuickAddDialog.js +101 -0
- package/dist/dashboard/components/ManagerAuthDialog.js +77 -0
- package/dist/dashboard/components/ProductCard.js +98 -0
- package/dist/dashboard/components/ProductsPanel.js +119 -0
- package/dist/dashboard/components/SplitPaymentPanel.js +131 -0
- package/dist/dashboard/components/VariantSelectorDialog.js +150 -0
- package/dist/dashboard/components/cart/AddChargeDialog.js +99 -0
- package/dist/dashboard/components/cart/CartItems.js +103 -0
- package/dist/dashboard/components/cart/CartSummary.js +73 -0
- package/dist/dashboard/components/cart/CustomerSection.js +127 -0
- package/dist/dashboard/components/cart/DiscountSection.js +50 -0
- package/dist/dashboard/components/cart/PointsRedemptionSection.js +58 -0
- package/dist/hardware/context.d.ts +15 -0
- package/dist/hardware/context.js +33 -0
- package/dist/hardware/index.d.ts +7 -0
- package/dist/hardware/index.js +7 -0
- package/dist/hardware/ports.d.ts +116 -0
- package/dist/hardware/tauri-adapter.d.ts +7 -0
- package/dist/hardware/tauri-adapter.js +77 -0
- package/dist/hardware/use-printer-availability.d.ts +12 -0
- package/dist/hardware/use-printer-availability.js +50 -0
- package/dist/hardware/use-printer-config.d.ts +16 -0
- package/dist/hardware/use-printer-config.js +62 -0
- package/dist/hardware/web-adapter.d.ts +15 -0
- package/dist/hardware/web-adapter.js +80 -0
- package/dist/hooks/useManagerAuth.js +83 -0
- package/dist/hooks/usePosCart.js +142 -0
- package/dist/hooks/usePosCustomer.js +192 -0
- package/dist/hooks/usePosMultiOrder.js +48 -0
- package/dist/hooks/usePosPayment.js +194 -0
- package/dist/lib/cn.js +12 -0
- package/dist/lib/loyalty.js +30 -0
- package/dist/lib/money.js +41 -0
- package/dist/node_modules/react-hook-form/dist/index.esm.js +1661 -0
- package/dist/runtime/auth-port.d.ts +46 -0
- package/dist/runtime/auth-port.js +21 -0
- package/dist/runtime/branch-port.d.ts +38 -0
- package/dist/runtime/branch-port.js +26 -0
- package/dist/runtime/config.d.ts +24 -0
- package/dist/runtime/config.js +21 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +5 -0
- package/dist/screens/OrderHistoryDrawer.js +245 -0
- package/dist/screens/ParkedOrdersDrawer.js +128 -0
- package/dist/screens/PaymentScreen.js +397 -0
- package/dist/screens/ProductScreen.js +322 -0
- package/dist/screens/ReceiptScreen.js +288 -0
- package/dist/screens/ShiftCloseScreen.js +365 -0
- package/dist/screens/ShiftOpenScreen.js +176 -0
- package/dist/shell/index.d.ts +8 -0
- package/dist/shell/index.js +5 -0
- package/dist/shell/pos-shell.d.ts +23 -0
- package/dist/shell/pos-shell.js +132 -0
- package/dist/state/pos-context.js +33 -0
- package/dist/state/pos-state.js +70 -0
- package/dist/utils/customer-display.js +35 -0
- package/dist/utils/pos-helpers.js +242 -0
- package/package.json +92 -0
- package/styles.css +19 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/runtime/auth-port.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `PosAuthPort` — the auth seam the POS app needs, injected by the host.
|
|
6
|
+
*
|
|
7
|
+
* POS itself is auth-agnostic: it needs a bearer token for a few manual calls,
|
|
8
|
+
* the current session user, and a *manager override* sign-in (for discount /
|
|
9
|
+
* variance approvals). The host wires these to its own auth client (Better Auth
|
|
10
|
+
* on the web; whatever a desktop shell uses). The package declares the contract,
|
|
11
|
+
* the host supplies the implementation. Types are structural (no better-auth
|
|
12
|
+
* import), so a Tauri / RN shell can implement it against a different client.
|
|
13
|
+
*/
|
|
14
|
+
interface PosSessionUser {
|
|
15
|
+
id?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
email?: string;
|
|
18
|
+
role?: unknown;
|
|
19
|
+
}
|
|
20
|
+
interface PosAuthPort {
|
|
21
|
+
/** Current bearer token (or null). Used for the few manual POS API calls. */
|
|
22
|
+
getToken: () => string | null;
|
|
23
|
+
/** Reactive current session user (null when signed out). */
|
|
24
|
+
useSessionUser: () => PosSessionUser | null;
|
|
25
|
+
/**
|
|
26
|
+
* Manager-override sign-in — validates a manager's credentials WITHOUT
|
|
27
|
+
* replacing the cashier's session (temporary elevation for approvals).
|
|
28
|
+
* Resolves with the authorizing user, rejects on bad credentials / role.
|
|
29
|
+
*/
|
|
30
|
+
signInManager: (input: {
|
|
31
|
+
email: string;
|
|
32
|
+
password: string;
|
|
33
|
+
}) => Promise<{
|
|
34
|
+
user: PosSessionUser;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
declare function PosAuthProvider({
|
|
38
|
+
auth,
|
|
39
|
+
children
|
|
40
|
+
}: {
|
|
41
|
+
auth: PosAuthPort;
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
}): import("react").JSX.Element;
|
|
44
|
+
declare function usePosAuth(): PosAuthPort;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { PosAuthPort, PosAuthProvider, PosSessionUser, usePosAuth };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/runtime/auth-port.tsx
|
|
7
|
+
const PosAuthContext = createContext(null);
|
|
8
|
+
function PosAuthProvider({ auth, children }) {
|
|
9
|
+
return /* @__PURE__ */ jsx(PosAuthContext.Provider, {
|
|
10
|
+
value: auth,
|
|
11
|
+
children
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function usePosAuth() {
|
|
15
|
+
const ctx = useContext(PosAuthContext);
|
|
16
|
+
if (!ctx) throw new Error("usePosAuth() must be used within <PosShell> / <PosAuthProvider>. Inject a PosAuthPort (host wires it to its auth client).");
|
|
17
|
+
return ctx;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { PosAuthProvider, usePosAuth };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/runtime/branch-port.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `PosBranchPort` — the active-branch seam, injected by the host. POS is
|
|
6
|
+
* branch-scoped (products, shifts, receipts) but must NOT depend on the
|
|
7
|
+
* dashboard's branch context (`@classytic/erp-shell/branch`) — that would drag
|
|
8
|
+
* the dashboard stack into a desktop register. So the host provides the active
|
|
9
|
+
* branch: on the web it wires this to `erp-shell`'s `useBranch`; a Tauri/RN
|
|
10
|
+
* register wires its own branch selection.
|
|
11
|
+
*/
|
|
12
|
+
interface PosBranch {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
/** Branch role (head_office / sub_branch), if the host distinguishes. */
|
|
16
|
+
role?: string;
|
|
17
|
+
/** Receipt header fields. */
|
|
18
|
+
address?: string;
|
|
19
|
+
bin?: string;
|
|
20
|
+
phone?: string;
|
|
21
|
+
}
|
|
22
|
+
interface PosBranchPort {
|
|
23
|
+
/** Reactive active branch (null before one is selected). */
|
|
24
|
+
useActiveBranch: () => PosBranch | null;
|
|
25
|
+
}
|
|
26
|
+
declare function PosBranchProvider({
|
|
27
|
+
branch,
|
|
28
|
+
children
|
|
29
|
+
}: {
|
|
30
|
+
branch: PosBranchPort;
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
33
|
+
/** Read the injected active-branch port. */
|
|
34
|
+
declare function usePosBranchPort(): PosBranchPort;
|
|
35
|
+
/** Convenience: the active branch directly. */
|
|
36
|
+
declare function usePosBranch(): PosBranch | null;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { PosBranch, PosBranchPort, PosBranchProvider, usePosBranch, usePosBranchPort };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/runtime/branch-port.tsx
|
|
7
|
+
const PosBranchContext = createContext(null);
|
|
8
|
+
function PosBranchProvider({ branch, children }) {
|
|
9
|
+
return /* @__PURE__ */ jsx(PosBranchContext.Provider, {
|
|
10
|
+
value: branch,
|
|
11
|
+
children
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/** Read the injected active-branch port. */
|
|
15
|
+
function usePosBranchPort() {
|
|
16
|
+
const ctx = useContext(PosBranchContext);
|
|
17
|
+
if (!ctx) throw new Error("usePosBranchPort() must be used within <PosShell> / <PosBranchProvider>. Inject a PosBranchPort (host wires it to its branch context).");
|
|
18
|
+
return ctx;
|
|
19
|
+
}
|
|
20
|
+
/** Convenience: the active branch directly. */
|
|
21
|
+
function usePosBranch() {
|
|
22
|
+
return usePosBranchPort().useActiveBranch();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { PosBranchProvider, usePosBranch, usePosBranchPort };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/runtime/config.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `PosRuntime` — host-provided runtime services that are genuinely app/
|
|
6
|
+
* deployment-specific (not auth, not hardware): navigation and money
|
|
7
|
+
* formatting. Injected once by `<PosShell>`; screens read it via
|
|
8
|
+
* `usePosRuntime()`. Keeps the package free of hardcoded routes and currency
|
|
9
|
+
* conventions — the host owns those (same rule as branding elsewhere).
|
|
10
|
+
*/
|
|
11
|
+
interface PosRuntime {
|
|
12
|
+
/** Navigate to a host route (e.g. back to `/dashboard`, out to `/sign-in`). */
|
|
13
|
+
onNavigate: (path: string) => void;
|
|
14
|
+
}
|
|
15
|
+
declare function PosRuntimeProvider({
|
|
16
|
+
runtime,
|
|
17
|
+
children
|
|
18
|
+
}: {
|
|
19
|
+
runtime: PosRuntime;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
22
|
+
declare function usePosRuntime(): PosRuntime;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { PosRuntime, PosRuntimeProvider, usePosRuntime };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/runtime/config.tsx
|
|
7
|
+
const PosRuntimeContext = createContext(null);
|
|
8
|
+
function PosRuntimeProvider({ runtime, children }) {
|
|
9
|
+
return /* @__PURE__ */ jsx(PosRuntimeContext.Provider, {
|
|
10
|
+
value: runtime,
|
|
11
|
+
children
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function usePosRuntime() {
|
|
15
|
+
const ctx = useContext(PosRuntimeContext);
|
|
16
|
+
if (!ctx) throw new Error("usePosRuntime() must be used within <PosShell> / <PosRuntimeProvider>. Inject a PosRuntime (onNavigate + formatMoney).");
|
|
17
|
+
return ctx;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { PosRuntimeProvider, usePosRuntime };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { PosAuthPort, PosAuthProvider, PosSessionUser, usePosAuth } from "./auth-port.js";
|
|
2
|
+
import { PosRuntime, PosRuntimeProvider, usePosRuntime } from "./config.js";
|
|
3
|
+
import { PosBranch, PosBranchPort, PosBranchProvider, usePosBranch, usePosBranchPort } from "./branch-port.js";
|
|
4
|
+
export { type PosAuthPort, PosAuthProvider, type PosBranch, type PosBranchPort, PosBranchProvider, type PosRuntime, PosRuntimeProvider, type PosSessionUser, usePosAuth, usePosBranch, usePosBranchPort, usePosRuntime };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { PosAuthProvider, usePosAuth } from "./auth-port.js";
|
|
2
|
+
import { PosRuntimeProvider, usePosRuntime } from "./config.js";
|
|
3
|
+
import { PosBranchProvider, usePosBranch, usePosBranchPort } from "./branch-port.js";
|
|
4
|
+
|
|
5
|
+
export { PosAuthProvider, PosBranchProvider, PosRuntimeProvider, usePosAuth, usePosBranch, usePosBranchPort, usePosRuntime };
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { formatPaisa } from "../lib/money.js";
|
|
4
|
+
import { ReceiptReprintDialog } from "../components/ReceiptReprintDialog.js";
|
|
5
|
+
import { useCallback, useState } from "react";
|
|
6
|
+
import { SheetWrapper } from "@classytic/fluid/client/core";
|
|
7
|
+
import { usePosOrderDetail, usePosOrderHistory, usePosRefundOrder, usePosVoidOrder } from "@classytic/commerce-sdk/sales";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { Ban, ChevronRight, Loader2, Package, Printer, RotateCcw, Search } from "lucide-react";
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import { StatusBadge } from "@classytic/fluid";
|
|
12
|
+
import { Input } from "@/components/ui/input";
|
|
13
|
+
|
|
14
|
+
//#region src/screens/OrderHistoryDrawer.tsx
|
|
15
|
+
/**
|
|
16
|
+
* OrderHistoryDrawer — Slide-out panel with recent POS orders.
|
|
17
|
+
*
|
|
18
|
+
* Powered by SDK hooks (`usePosOrderHistory`, `usePosOrderDetail`,
|
|
19
|
+
* `usePosVoidOrder`, `usePosRefundOrder`) — no inline `handleApiRequest`
|
|
20
|
+
* calls or hand-rolled query keys. Cache invalidation + mutation toasts
|
|
21
|
+
* are handled inside the SDK hooks; this component just composes them
|
|
22
|
+
* with fluid's `SheetWrapper`.
|
|
23
|
+
*/
|
|
24
|
+
const STATUS_VARIANT = {
|
|
25
|
+
pending: "warning",
|
|
26
|
+
confirmed: "info",
|
|
27
|
+
processing: "info",
|
|
28
|
+
completed: "success",
|
|
29
|
+
cancelled: "error",
|
|
30
|
+
refunded: "error"
|
|
31
|
+
};
|
|
32
|
+
function OrderHistoryDrawer({ open, onOpenChange }) {
|
|
33
|
+
const [search, setSearch] = useState("");
|
|
34
|
+
const [selectedOrderId, setSelectedOrderId] = useState(null);
|
|
35
|
+
const [reprintOrderId, setReprintOrderId] = useState(null);
|
|
36
|
+
const trimmedSearch = search.trim();
|
|
37
|
+
const { orders, isLoading } = usePosOrderHistory({
|
|
38
|
+
limit: 30,
|
|
39
|
+
...trimmedSearch ? { search: trimmedSearch } : {}
|
|
40
|
+
}, open);
|
|
41
|
+
const { order: selectedOrder, isLoading: detailLoading } = usePosOrderDetail(selectedOrderId, open);
|
|
42
|
+
const { voidOrder, isPending: isVoiding } = usePosVoidOrder();
|
|
43
|
+
const { refundOrder, isPending: isRefunding } = usePosRefundOrder();
|
|
44
|
+
const handleVoid = useCallback(async () => {
|
|
45
|
+
if (!selectedOrderId || !window.confirm("Void this order?")) return;
|
|
46
|
+
try {
|
|
47
|
+
await voidOrder({
|
|
48
|
+
orderId: selectedOrderId,
|
|
49
|
+
reason: "Voided from POS"
|
|
50
|
+
});
|
|
51
|
+
setSelectedOrderId(null);
|
|
52
|
+
} catch {}
|
|
53
|
+
}, [selectedOrderId, voidOrder]);
|
|
54
|
+
const handleRefund = useCallback(async () => {
|
|
55
|
+
if (!selectedOrderId || !window.confirm("Process full refund?")) return;
|
|
56
|
+
try {
|
|
57
|
+
await refundOrder({
|
|
58
|
+
orderId: selectedOrderId,
|
|
59
|
+
reason: "Full refund from POS"
|
|
60
|
+
});
|
|
61
|
+
setSelectedOrderId(null);
|
|
62
|
+
} catch {}
|
|
63
|
+
}, [selectedOrderId, refundOrder]);
|
|
64
|
+
return /* @__PURE__ */ jsxs(SheetWrapper, {
|
|
65
|
+
open,
|
|
66
|
+
onOpenChange,
|
|
67
|
+
title: "Order History",
|
|
68
|
+
side: "right",
|
|
69
|
+
size: "lg",
|
|
70
|
+
contentClassName: "p-0",
|
|
71
|
+
children: [
|
|
72
|
+
/* @__PURE__ */ jsx("div", {
|
|
73
|
+
className: "px-4 py-2 border-b",
|
|
74
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
75
|
+
className: "relative",
|
|
76
|
+
children: [/* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ jsx(Input, {
|
|
77
|
+
placeholder: "Search by order number...",
|
|
78
|
+
value: search,
|
|
79
|
+
onChange: (e) => setSearch(e.target.value),
|
|
80
|
+
className: "pl-9 h-9"
|
|
81
|
+
})]
|
|
82
|
+
})
|
|
83
|
+
}),
|
|
84
|
+
selectedOrderId && selectedOrder ? /* @__PURE__ */ jsxs("div", {
|
|
85
|
+
className: "flex-1 overflow-y-auto",
|
|
86
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
87
|
+
className: "px-4 py-3 border-b",
|
|
88
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
89
|
+
variant: "ghost",
|
|
90
|
+
size: "sm",
|
|
91
|
+
onClick: () => setSelectedOrderId(null),
|
|
92
|
+
className: "h-7 text-xs -ml-2",
|
|
93
|
+
children: "Back to list"
|
|
94
|
+
})
|
|
95
|
+
}), detailLoading ? /* @__PURE__ */ jsx("div", {
|
|
96
|
+
className: "flex items-center justify-center py-12",
|
|
97
|
+
children: /* @__PURE__ */ jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" })
|
|
98
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
99
|
+
className: "px-4 py-4 space-y-4",
|
|
100
|
+
children: [
|
|
101
|
+
/* @__PURE__ */ jsxs("div", {
|
|
102
|
+
className: "flex items-center justify-between",
|
|
103
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("h3", {
|
|
104
|
+
className: "font-semibold",
|
|
105
|
+
children: ["#", String(selectedOrder.orderNumber ?? "N/A")]
|
|
106
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
107
|
+
className: "text-xs text-muted-foreground",
|
|
108
|
+
children: new Date(String(selectedOrder.createdAt)).toLocaleString()
|
|
109
|
+
})] }), /* @__PURE__ */ jsx(StatusBadge, {
|
|
110
|
+
variant: STATUS_VARIANT[String(selectedOrder.status)] ?? "neutral",
|
|
111
|
+
children: String(selectedOrder.status)
|
|
112
|
+
})]
|
|
113
|
+
}),
|
|
114
|
+
selectedOrder.customerSnapshot ? /* @__PURE__ */ jsxs("div", {
|
|
115
|
+
className: "text-sm",
|
|
116
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
117
|
+
className: "text-muted-foreground",
|
|
118
|
+
children: "Customer: "
|
|
119
|
+
}), /* @__PURE__ */ jsx("span", { children: String(selectedOrder.customerSnapshot?.name ?? "Walk-in") })]
|
|
120
|
+
}) : null,
|
|
121
|
+
/* @__PURE__ */ jsxs("div", {
|
|
122
|
+
className: "space-y-2",
|
|
123
|
+
children: [/* @__PURE__ */ jsx("h4", {
|
|
124
|
+
className: "text-sm font-medium",
|
|
125
|
+
children: "Items"
|
|
126
|
+
}), (selectedOrder.lines ?? []).map((line, i) => {
|
|
127
|
+
const lineTotalMoney = line.lineTotal;
|
|
128
|
+
const snapshot = line.snapshot;
|
|
129
|
+
const unitPriceMoney = snapshot?.unitPrice;
|
|
130
|
+
const qty = Number(line.quantity ?? 0);
|
|
131
|
+
const unitPaisa = typeof unitPriceMoney === "number" ? unitPriceMoney : unitPriceMoney?.amount ?? 0;
|
|
132
|
+
const lineTotalPaisa = typeof lineTotalMoney?.amount === "number" ? lineTotalMoney.amount : unitPaisa * qty;
|
|
133
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
134
|
+
className: "flex justify-between text-sm",
|
|
135
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [String(snapshot?.name ?? "Item"), /* @__PURE__ */ jsxs("span", {
|
|
136
|
+
className: "text-muted-foreground ml-1",
|
|
137
|
+
children: ["x", qty]
|
|
138
|
+
})] }), /* @__PURE__ */ jsx("span", {
|
|
139
|
+
className: "tabular-nums",
|
|
140
|
+
children: formatPaisa(lineTotalPaisa)
|
|
141
|
+
})]
|
|
142
|
+
}, i);
|
|
143
|
+
})]
|
|
144
|
+
}),
|
|
145
|
+
/* @__PURE__ */ jsxs("div", {
|
|
146
|
+
className: "flex justify-between font-semibold text-base pt-2 border-t",
|
|
147
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Total" }), /* @__PURE__ */ jsx("span", {
|
|
148
|
+
className: "tabular-nums",
|
|
149
|
+
children: formatPaisa(Number((selectedOrder.totals?.grandTotal)?.amount ?? 0))
|
|
150
|
+
})]
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ jsxs("div", {
|
|
153
|
+
className: "flex flex-col gap-2 pt-4 border-t",
|
|
154
|
+
children: [
|
|
155
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
156
|
+
variant: "outline",
|
|
157
|
+
size: "sm",
|
|
158
|
+
onClick: () => selectedOrderId && setReprintOrderId(selectedOrderId),
|
|
159
|
+
children: [/* @__PURE__ */ jsx(Printer, { className: "h-4 w-4 mr-2" }), "Reprint Receipt"]
|
|
160
|
+
}),
|
|
161
|
+
(selectedOrder.status === "pending" || selectedOrder.status === "confirmed") && /* @__PURE__ */ jsxs(Button, {
|
|
162
|
+
variant: "outline",
|
|
163
|
+
size: "sm",
|
|
164
|
+
className: "text-destructive",
|
|
165
|
+
onClick: handleVoid,
|
|
166
|
+
disabled: isVoiding,
|
|
167
|
+
children: [/* @__PURE__ */ jsx(Ban, { className: "h-4 w-4 mr-2" }), isVoiding ? "Voiding..." : "Void Order"]
|
|
168
|
+
}),
|
|
169
|
+
selectedOrder.status === "completed" && /* @__PURE__ */ jsxs(Button, {
|
|
170
|
+
variant: "outline",
|
|
171
|
+
size: "sm",
|
|
172
|
+
className: "text-orange-600",
|
|
173
|
+
onClick: handleRefund,
|
|
174
|
+
disabled: isRefunding,
|
|
175
|
+
children: [/* @__PURE__ */ jsx(RotateCcw, { className: "h-4 w-4 mr-2" }), isRefunding ? "Processing..." : "Full Refund"]
|
|
176
|
+
})
|
|
177
|
+
]
|
|
178
|
+
})
|
|
179
|
+
]
|
|
180
|
+
})]
|
|
181
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
182
|
+
className: "flex-1 overflow-y-auto",
|
|
183
|
+
children: isLoading ? /* @__PURE__ */ jsx("div", {
|
|
184
|
+
className: "flex items-center justify-center py-12",
|
|
185
|
+
children: /* @__PURE__ */ jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" })
|
|
186
|
+
}) : orders.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
187
|
+
className: "flex flex-col items-center justify-center py-12 text-muted-foreground",
|
|
188
|
+
children: [/* @__PURE__ */ jsx(Package, { className: "h-10 w-10 mb-3 opacity-30" }), /* @__PURE__ */ jsx("p", {
|
|
189
|
+
className: "text-sm",
|
|
190
|
+
children: "No POS orders found"
|
|
191
|
+
})]
|
|
192
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
193
|
+
className: "divide-y",
|
|
194
|
+
children: orders.map((order) => /* @__PURE__ */ jsxs("button", {
|
|
195
|
+
type: "button",
|
|
196
|
+
className: "w-full px-4 py-3 text-left hover:bg-muted/50 transition-colors flex items-center gap-3",
|
|
197
|
+
onClick: () => setSelectedOrderId(String(order.orderNumber ?? order._id)),
|
|
198
|
+
children: [
|
|
199
|
+
/* @__PURE__ */ jsxs("div", {
|
|
200
|
+
className: "flex-1 min-w-0",
|
|
201
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
202
|
+
className: "flex items-center gap-2",
|
|
203
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
204
|
+
className: "font-medium text-sm",
|
|
205
|
+
children: ["#", String(order.orderNumber ?? "N/A")]
|
|
206
|
+
}), /* @__PURE__ */ jsx(StatusBadge, {
|
|
207
|
+
variant: STATUS_VARIANT[String(order.status)] ?? "neutral",
|
|
208
|
+
size: "sm",
|
|
209
|
+
children: String(order.status)
|
|
210
|
+
})]
|
|
211
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
212
|
+
className: "flex items-center gap-2 text-xs text-muted-foreground mt-0.5",
|
|
213
|
+
children: [/* @__PURE__ */ jsx("span", { children: new Date(String(order.createdAt)).toLocaleString("en-US", {
|
|
214
|
+
month: "short",
|
|
215
|
+
day: "numeric",
|
|
216
|
+
hour: "2-digit",
|
|
217
|
+
minute: "2-digit"
|
|
218
|
+
}) }), order.customerSnapshot?.name ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children: "-" }), /* @__PURE__ */ jsx("span", {
|
|
219
|
+
className: "truncate",
|
|
220
|
+
children: String(order.customerSnapshot.name)
|
|
221
|
+
})] }) : null]
|
|
222
|
+
})]
|
|
223
|
+
}),
|
|
224
|
+
/* @__PURE__ */ jsx("span", {
|
|
225
|
+
className: "font-semibold text-sm tabular-nums shrink-0",
|
|
226
|
+
children: formatPaisa(Number((order.totals?.grandTotal)?.amount ?? 0))
|
|
227
|
+
}),
|
|
228
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 text-muted-foreground shrink-0" })
|
|
229
|
+
]
|
|
230
|
+
}, String(order.orderNumber ?? order._id)))
|
|
231
|
+
})
|
|
232
|
+
}),
|
|
233
|
+
/* @__PURE__ */ jsx(ReceiptReprintDialog, {
|
|
234
|
+
order: reprintOrderId && selectedOrder ? selectedOrder : null,
|
|
235
|
+
open: !!reprintOrderId,
|
|
236
|
+
onOpenChange: (o) => {
|
|
237
|
+
if (!o) setReprintOrderId(null);
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
]
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
//#endregion
|
|
245
|
+
export { OrderHistoryDrawer };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { formatBdt } from "../lib/money.js";
|
|
4
|
+
import { calculateCartTotals } from "../utils/pos-helpers.js";
|
|
5
|
+
import { useCallback } from "react";
|
|
6
|
+
import { SheetWrapper } from "@classytic/fluid/client/core";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { Inbox, PlayCircle, Trash2 } from "lucide-react";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { Badge } from "@/components/ui/badge";
|
|
11
|
+
|
|
12
|
+
//#region src/screens/ParkedOrdersDrawer.tsx
|
|
13
|
+
/**
|
|
14
|
+
* ParkedOrdersDrawer — slide-out list of parked sales.
|
|
15
|
+
*
|
|
16
|
+
* Backed by `usePosMultiOrder` (localStorage, max 5 slots, 8h TTL per
|
|
17
|
+
* branch). Resume drops the parked cart back into the active session and
|
|
18
|
+
* removes the slot. Delete discards without resuming.
|
|
19
|
+
*
|
|
20
|
+
* Use case: cashier mid-sale, customer steps away to grab one more item;
|
|
21
|
+
* cashier parks the sale, serves the next customer, resumes when the
|
|
22
|
+
* first returns. Same pattern as Square "Save sale" / Lightspeed "On hold".
|
|
23
|
+
*/
|
|
24
|
+
function formatRelative(timestamp) {
|
|
25
|
+
const diffMs = Date.now() - timestamp;
|
|
26
|
+
const diffMin = Math.floor(diffMs / 6e4);
|
|
27
|
+
if (diffMin < 1) return "just now";
|
|
28
|
+
if (diffMin < 60) return `${diffMin} min ago`;
|
|
29
|
+
return `${Math.floor(diffMin / 60)}h ${diffMin % 60}m ago`;
|
|
30
|
+
}
|
|
31
|
+
function ParkedOrdersDrawer({ open, onOpenChange, orders, onResume, onDelete }) {
|
|
32
|
+
const handleDelete = useCallback((id) => {
|
|
33
|
+
if (window.confirm("Discard this parked sale?")) onDelete(id);
|
|
34
|
+
}, [onDelete]);
|
|
35
|
+
return /* @__PURE__ */ jsx(SheetWrapper, {
|
|
36
|
+
open,
|
|
37
|
+
onOpenChange,
|
|
38
|
+
title: "Parked sales",
|
|
39
|
+
description: "Hold an in-progress sale and resume later. Max 5 per branch.",
|
|
40
|
+
side: "right",
|
|
41
|
+
size: "default",
|
|
42
|
+
contentClassName: "p-0",
|
|
43
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
44
|
+
className: "flex-1 overflow-y-auto",
|
|
45
|
+
children: orders.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
46
|
+
className: "flex flex-col items-center justify-center py-12 text-muted-foreground",
|
|
47
|
+
children: [
|
|
48
|
+
/* @__PURE__ */ jsx(Inbox, { className: "h-10 w-10 mb-3 opacity-30" }),
|
|
49
|
+
/* @__PURE__ */ jsx("p", {
|
|
50
|
+
className: "text-sm",
|
|
51
|
+
children: "No parked sales"
|
|
52
|
+
}),
|
|
53
|
+
/* @__PURE__ */ jsxs("p", {
|
|
54
|
+
className: "text-xs mt-1",
|
|
55
|
+
children: [
|
|
56
|
+
"Press ",
|
|
57
|
+
/* @__PURE__ */ jsx("kbd", {
|
|
58
|
+
className: "px-1.5 py-0.5 rounded border text-[10px]",
|
|
59
|
+
children: "F6"
|
|
60
|
+
}),
|
|
61
|
+
" on a sale to park it"
|
|
62
|
+
]
|
|
63
|
+
})
|
|
64
|
+
]
|
|
65
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
66
|
+
className: "divide-y",
|
|
67
|
+
children: orders.map((order) => {
|
|
68
|
+
const totals = calculateCartTotals(order.cart, order.discountInput);
|
|
69
|
+
const itemCount = order.cart.reduce((sum, item) => sum + item.quantity, 0);
|
|
70
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
71
|
+
className: "px-4 py-3",
|
|
72
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
73
|
+
className: "flex items-start justify-between gap-2 mb-2",
|
|
74
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
75
|
+
className: "min-w-0 flex-1",
|
|
76
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
77
|
+
className: "flex items-center gap-2 flex-wrap",
|
|
78
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
79
|
+
className: "font-medium text-sm truncate",
|
|
80
|
+
children: order.label
|
|
81
|
+
}), /* @__PURE__ */ jsxs(Badge, {
|
|
82
|
+
variant: "secondary",
|
|
83
|
+
className: "text-[10px]",
|
|
84
|
+
children: [
|
|
85
|
+
itemCount,
|
|
86
|
+
" ",
|
|
87
|
+
itemCount === 1 ? "item" : "items"
|
|
88
|
+
]
|
|
89
|
+
})]
|
|
90
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
91
|
+
className: "text-xs text-muted-foreground mt-0.5",
|
|
92
|
+
children: [formatRelative(order.parkedAt), order.customerName && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
93
|
+
className: "mx-1",
|
|
94
|
+
children: "·"
|
|
95
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
96
|
+
className: "truncate",
|
|
97
|
+
children: order.customerName
|
|
98
|
+
})] })]
|
|
99
|
+
})]
|
|
100
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
101
|
+
className: "text-sm font-semibold tabular-nums shrink-0",
|
|
102
|
+
children: formatBdt(totals.total)
|
|
103
|
+
})]
|
|
104
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
105
|
+
className: "flex gap-2",
|
|
106
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
107
|
+
size: "sm",
|
|
108
|
+
className: "h-8 flex-1",
|
|
109
|
+
onClick: () => onResume(order.id),
|
|
110
|
+
children: [/* @__PURE__ */ jsx(PlayCircle, { className: "h-3.5 w-3.5 mr-1.5" }), "Resume"]
|
|
111
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
112
|
+
variant: "outline",
|
|
113
|
+
size: "sm",
|
|
114
|
+
className: "h-8 text-destructive",
|
|
115
|
+
onClick: () => handleDelete(order.id),
|
|
116
|
+
"aria-label": "Discard parked sale",
|
|
117
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
|
|
118
|
+
})]
|
|
119
|
+
})]
|
|
120
|
+
}, order.id);
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
export { ParkedOrdersDrawer };
|