@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.
Files changed (65) hide show
  1. package/README.md +38 -0
  2. package/dist/components/CartSidebar.js +222 -0
  3. package/dist/components/PosTopBar.js +156 -0
  4. package/dist/components/PrinterSettingsDialog.js +163 -0
  5. package/dist/components/ReceiptReprintDialog.js +182 -0
  6. package/dist/dashboard/components/CustomerLookupDialog.js +120 -0
  7. package/dist/dashboard/components/CustomerQuickAddDialog.js +101 -0
  8. package/dist/dashboard/components/ManagerAuthDialog.js +77 -0
  9. package/dist/dashboard/components/ProductCard.js +98 -0
  10. package/dist/dashboard/components/ProductsPanel.js +119 -0
  11. package/dist/dashboard/components/SplitPaymentPanel.js +131 -0
  12. package/dist/dashboard/components/VariantSelectorDialog.js +150 -0
  13. package/dist/dashboard/components/cart/AddChargeDialog.js +99 -0
  14. package/dist/dashboard/components/cart/CartItems.js +103 -0
  15. package/dist/dashboard/components/cart/CartSummary.js +73 -0
  16. package/dist/dashboard/components/cart/CustomerSection.js +127 -0
  17. package/dist/dashboard/components/cart/DiscountSection.js +50 -0
  18. package/dist/dashboard/components/cart/PointsRedemptionSection.js +58 -0
  19. package/dist/hardware/context.d.ts +15 -0
  20. package/dist/hardware/context.js +33 -0
  21. package/dist/hardware/index.d.ts +7 -0
  22. package/dist/hardware/index.js +7 -0
  23. package/dist/hardware/ports.d.ts +116 -0
  24. package/dist/hardware/tauri-adapter.d.ts +7 -0
  25. package/dist/hardware/tauri-adapter.js +77 -0
  26. package/dist/hardware/use-printer-availability.d.ts +12 -0
  27. package/dist/hardware/use-printer-availability.js +50 -0
  28. package/dist/hardware/use-printer-config.d.ts +16 -0
  29. package/dist/hardware/use-printer-config.js +62 -0
  30. package/dist/hardware/web-adapter.d.ts +15 -0
  31. package/dist/hardware/web-adapter.js +80 -0
  32. package/dist/hooks/useManagerAuth.js +83 -0
  33. package/dist/hooks/usePosCart.js +142 -0
  34. package/dist/hooks/usePosCustomer.js +192 -0
  35. package/dist/hooks/usePosMultiOrder.js +48 -0
  36. package/dist/hooks/usePosPayment.js +194 -0
  37. package/dist/lib/cn.js +12 -0
  38. package/dist/lib/loyalty.js +30 -0
  39. package/dist/lib/money.js +41 -0
  40. package/dist/node_modules/react-hook-form/dist/index.esm.js +1661 -0
  41. package/dist/runtime/auth-port.d.ts +46 -0
  42. package/dist/runtime/auth-port.js +21 -0
  43. package/dist/runtime/branch-port.d.ts +38 -0
  44. package/dist/runtime/branch-port.js +26 -0
  45. package/dist/runtime/config.d.ts +24 -0
  46. package/dist/runtime/config.js +21 -0
  47. package/dist/runtime/index.d.ts +4 -0
  48. package/dist/runtime/index.js +5 -0
  49. package/dist/screens/OrderHistoryDrawer.js +245 -0
  50. package/dist/screens/ParkedOrdersDrawer.js +128 -0
  51. package/dist/screens/PaymentScreen.js +397 -0
  52. package/dist/screens/ProductScreen.js +322 -0
  53. package/dist/screens/ReceiptScreen.js +288 -0
  54. package/dist/screens/ShiftCloseScreen.js +365 -0
  55. package/dist/screens/ShiftOpenScreen.js +176 -0
  56. package/dist/shell/index.d.ts +8 -0
  57. package/dist/shell/index.js +5 -0
  58. package/dist/shell/pos-shell.d.ts +23 -0
  59. package/dist/shell/pos-shell.js +132 -0
  60. package/dist/state/pos-context.js +33 -0
  61. package/dist/state/pos-state.js +70 -0
  62. package/dist/utils/customer-display.js +35 -0
  63. package/dist/utils/pos-helpers.js +242 -0
  64. package/package.json +92 -0
  65. package/styles.css +19 -0
@@ -0,0 +1,182 @@
1
+ "use client";
2
+
3
+ import { usePosBranch } from "../runtime/branch-port.js";
4
+ import { formatPaisa } from "../lib/money.js";
5
+ import { transformOrderToReceipt } from "../utils/pos-helpers.js";
6
+ import { useMemo } from "react";
7
+ import { PrintableView, printDocument } from "@classytic/fluid/client/core";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { Loader2, Printer } from "lucide-react";
10
+ import { Button } from "@/components/ui/button";
11
+ import { getReceiptLocale, paymentMethodLabel } from "@classytic/commerce-sdk/client";
12
+ import { DocumentFooter, DocumentHeader, DocumentLineTable, DocumentPage, DocumentParty, DocumentSummary } from "@classytic/fluid/document";
13
+ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
14
+
15
+ //#region src/components/ReceiptReprintDialog.tsx
16
+ /**
17
+ * ReceiptReprintDialog — Reprint a historical POS order's receipt.
18
+ *
19
+ * Takes an already-fetched raw order document (from `usePosOrderDetail`)
20
+ * and transforms it client-side via `transformOrderToReceipt`. This is the
21
+ * same helper the live ReceiptScreen uses after checkout, so a reprint
22
+ * matches the original visually.
23
+ *
24
+ * Receipt rendering is intentionally client-side. The Order doc has every
25
+ * field a receipt needs; running it through `transformOrderToReceipt`
26
+ * locally is faster, requires no extra round-trip, and gives the host
27
+ * full control over branding/format without involving a backend schema.
28
+ *
29
+ * Print path: `printDocument("pos-receipt-reprint")` isolates the rendered
30
+ * `<PrintableView>` and triggers `window.print()` (or ESC/POS via Tauri).
31
+ * The DOM id is intentionally *different* from the live ReceiptScreen's
32
+ * `pos-receipt` so a reprint never clobbers an in-flight sale.
33
+ */
34
+ const REPRINT_DOM_ID = "pos-receipt-reprint";
35
+ function isShownPhone(phone) {
36
+ return Boolean(phone && !phone.startsWith("pending_"));
37
+ }
38
+ function buildLines(receipt) {
39
+ return (receipt.items ?? []).map((item, idx) => {
40
+ const subtotal = (item.unitPrice ?? 0) * item.quantity;
41
+ return {
42
+ sequence: idx + 1,
43
+ description: item.name + (item.variant ? ` (${item.variant})` : ""),
44
+ quantity: item.quantity,
45
+ uom: "pcs",
46
+ unitPrice: item.unitPrice ?? 0,
47
+ discountType: "flat",
48
+ discountValue: 0,
49
+ subtotal,
50
+ taxRate: item.vatRate ?? 0,
51
+ taxAmount: item.vatAmount ?? 0,
52
+ lineTotal: item.total,
53
+ formatted: {
54
+ unitPrice: formatPaisa(item.unitPrice ?? 0),
55
+ subtotal: formatPaisa(subtotal),
56
+ taxAmount: formatPaisa(item.vatAmount ?? 0),
57
+ lineTotal: formatPaisa(item.total)
58
+ }
59
+ };
60
+ });
61
+ }
62
+ function ReceiptReprintDialog({ order, open, onOpenChange }) {
63
+ const selectedBranch = usePosBranch();
64
+ const receipt = useMemo(() => {
65
+ if (!order) return null;
66
+ return transformOrderToReceipt(order, {
67
+ name: selectedBranch?.name ?? "Store",
68
+ ...selectedBranch?.phone ? { phone: selectedBranch.phone } : {}
69
+ });
70
+ }, [
71
+ order,
72
+ selectedBranch?.name,
73
+ selectedBranch?.phone
74
+ ]);
75
+ const handlePrint = () => {
76
+ if (!receipt) return;
77
+ printDocument(REPRINT_DOM_ID);
78
+ };
79
+ const customerName = receipt?.customer?.name?.trim() ?? "";
80
+ const showCustomer = customerName.length > 0 && customerName !== "Walk-in Customer";
81
+ const customerPhone = isShownPhone(receipt?.customer?.phone) ? receipt.customer.phone : null;
82
+ const locale = getReceiptLocale();
83
+ const summaryExtra = [];
84
+ if (receipt?.payment) summaryExtra.push({
85
+ label: `Paid (${paymentMethodLabel(receipt.payment.method)})`,
86
+ value: formatPaisa(receipt.payment.amount ?? receipt.total ?? 0),
87
+ muted: true,
88
+ separator: true
89
+ });
90
+ if (receipt?.membership?.pointsEarned && receipt.membership.pointsEarned > 0) summaryExtra.push({
91
+ label: "Points earned",
92
+ value: `+${receipt.membership.pointsEarned}`,
93
+ muted: true,
94
+ separator: false
95
+ });
96
+ return /* @__PURE__ */ jsx(Dialog, {
97
+ open,
98
+ onOpenChange,
99
+ children: /* @__PURE__ */ jsxs(DialogContent, {
100
+ className: "max-w-md p-0 gap-0",
101
+ children: [
102
+ /* @__PURE__ */ jsx(DialogHeader, {
103
+ className: "px-4 py-3 border-b",
104
+ children: /* @__PURE__ */ jsx(DialogTitle, {
105
+ className: "text-base",
106
+ children: "Reprint Receipt"
107
+ })
108
+ }),
109
+ /* @__PURE__ */ jsx("div", {
110
+ className: "max-h-[70vh] overflow-y-auto p-4 bg-muted/30 flex justify-center",
111
+ children: !receipt ? /* @__PURE__ */ jsx("div", {
112
+ className: "flex items-center justify-center py-12 w-full",
113
+ children: /* @__PURE__ */ jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" })
114
+ }) : /* @__PURE__ */ jsx(PrintableView, {
115
+ id: REPRINT_DOM_ID,
116
+ className: "bg-white dark:bg-card shadow-sm",
117
+ children: /* @__PURE__ */ jsxs(DocumentPage, {
118
+ size: "receipt",
119
+ children: [
120
+ /* @__PURE__ */ jsx(DocumentHeader, {
121
+ companyName: receipt.branch?.name ?? "Store",
122
+ companyContact: receipt.branch?.phone,
123
+ ...receipt.vat?.sellerBin ? { companyTaxId: `${locale.taxIdLabel}: ${receipt.vat.sellerBin}` } : {},
124
+ documentTitle: "Receipt (Reprint)",
125
+ documentNumber: receipt.orderNumber,
126
+ date: receipt.date ?? ""
127
+ }),
128
+ showCustomer && /* @__PURE__ */ jsx(DocumentParty, {
129
+ billTo: {
130
+ name: customerName,
131
+ ...customerPhone ? { contact: customerPhone } : {},
132
+ ...receipt.membership?.cardId ? { extra: { Loyalty: receipt.membership.cardId } } : {}
133
+ },
134
+ billToLabel: "Customer"
135
+ }),
136
+ /* @__PURE__ */ jsx(DocumentLineTable, {
137
+ lines: buildLines(receipt),
138
+ columns: [
139
+ "description",
140
+ "quantity",
141
+ "lineTotal"
142
+ ],
143
+ showSequence: false,
144
+ compact: true
145
+ }),
146
+ /* @__PURE__ */ jsx(DocumentSummary, {
147
+ subtotal: formatPaisa(receipt.subtotal ?? 0),
148
+ ...receipt.discount && receipt.discount > 0 ? { discount: formatPaisa(receipt.discount) } : {},
149
+ ...receipt.vat?.amount && receipt.vat.amount > 0 ? { tax: formatPaisa(receipt.vat.amount) } : {},
150
+ total: formatPaisa(receipt.total ?? 0),
151
+ extraRows: summaryExtra,
152
+ currency: locale.currency,
153
+ labels: { tax: locale.taxLabel },
154
+ width: "w-full"
155
+ }),
156
+ /* @__PURE__ */ jsx(DocumentFooter, { notes: /* @__PURE__ */ jsx("p", {
157
+ className: "text-center text-xs text-muted-foreground",
158
+ children: "Duplicate copy"
159
+ }) })
160
+ ]
161
+ })
162
+ })
163
+ }),
164
+ /* @__PURE__ */ jsxs(DialogFooter, {
165
+ className: "px-4 py-3 border-t",
166
+ children: [/* @__PURE__ */ jsx(Button, {
167
+ variant: "outline",
168
+ onClick: () => onOpenChange(false),
169
+ children: "Close"
170
+ }), /* @__PURE__ */ jsxs(Button, {
171
+ onClick: handlePrint,
172
+ disabled: !receipt,
173
+ children: [/* @__PURE__ */ jsx(Printer, { className: "h-4 w-4 mr-2" }), "Print"]
174
+ })]
175
+ })
176
+ ]
177
+ })
178
+ });
179
+ }
180
+
181
+ //#endregion
182
+ export { ReceiptReprintDialog };
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import { customerDisplayName, customerDisplayPhone } from "../../utils/customer-display.js";
4
+ import { getReadableTextColor, getTierColor } from "../../lib/loyalty.js";
5
+ import { useMemo } from "react";
6
+ import { DialogWrapper } from "@classytic/fluid/client/core";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { Search, UserPlus } from "lucide-react";
9
+ import { Button } from "@/components/ui/button";
10
+ import { Badge } from "@/components/ui/badge";
11
+ import { Input } from "@/components/ui/input";
12
+ import { Skeleton } from "@/components/ui/skeleton";
13
+
14
+ //#region src/dashboard/components/CustomerLookupDialog.tsx
15
+ function CustomerLookupDialog({ open, onOpenChange, searchValue, onSearchValueChange, onSearch, results, isLoading, onSelect, onCreate }) {
16
+ const showEmpty = searchValue.trim().length < 2;
17
+ const content = useMemo(() => {
18
+ if (showEmpty) return /* @__PURE__ */ jsx("div", {
19
+ className: "rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground",
20
+ children: "Type at least 2 characters to search customers."
21
+ });
22
+ if (isLoading) return /* @__PURE__ */ jsx("div", {
23
+ className: "space-y-2",
24
+ children: [
25
+ 1,
26
+ 2,
27
+ 3
28
+ ].map((i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-full" }, i))
29
+ });
30
+ if (results.length === 0) return /* @__PURE__ */ jsx("div", {
31
+ className: "rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground",
32
+ children: "No customer found."
33
+ });
34
+ return /* @__PURE__ */ jsx("div", {
35
+ className: "space-y-2",
36
+ children: results.map((customer) => /* @__PURE__ */ jsxs(Button, {
37
+ type: "button",
38
+ variant: "outline",
39
+ className: "w-full h-auto justify-between px-3 py-2",
40
+ onClick: () => onSelect(customer),
41
+ children: [/* @__PURE__ */ jsxs("div", {
42
+ className: "text-left",
43
+ children: [/* @__PURE__ */ jsx("p", {
44
+ className: "text-sm font-medium",
45
+ children: customerDisplayName(customer)
46
+ }), (() => {
47
+ const phone = customerDisplayPhone(customer);
48
+ const email = customer.contact?.email;
49
+ return /* @__PURE__ */ jsx("p", {
50
+ className: "text-xs text-muted-foreground",
51
+ children: phone ?? email ?? "No contact info"
52
+ });
53
+ })()]
54
+ }), (customer.membership?.tier || customer.revenueTier) && (() => {
55
+ const tierName = customer.membership?.tier || customer.revenueTier;
56
+ const tierColor = getTierColor(tierName);
57
+ const tierTextColor = getReadableTextColor(tierColor);
58
+ return /* @__PURE__ */ jsx(Badge, {
59
+ variant: "secondary",
60
+ className: "capitalize",
61
+ style: tierColor ? {
62
+ backgroundColor: tierColor,
63
+ color: tierTextColor,
64
+ borderColor: "transparent"
65
+ } : void 0,
66
+ children: tierName
67
+ });
68
+ })()]
69
+ }, customer._id))
70
+ });
71
+ }, [
72
+ isLoading,
73
+ results,
74
+ showEmpty
75
+ ]);
76
+ return /* @__PURE__ */ jsxs(DialogWrapper, {
77
+ open,
78
+ onOpenChange,
79
+ title: "Find customer",
80
+ description: "Search by phone or name and attach loyalty info.",
81
+ size: "lg",
82
+ footer: /* @__PURE__ */ jsxs("div", {
83
+ className: "flex justify-end gap-2",
84
+ children: [/* @__PURE__ */ jsxs(Button, {
85
+ variant: "outline",
86
+ onClick: onCreate,
87
+ children: [/* @__PURE__ */ jsx(UserPlus, { className: "h-4 w-4 mr-2" }), "Add new"]
88
+ }), /* @__PURE__ */ jsx(Button, {
89
+ variant: "ghost",
90
+ onClick: () => onOpenChange(false),
91
+ children: "Close"
92
+ })]
93
+ }),
94
+ children: [/* @__PURE__ */ jsxs("div", {
95
+ className: "flex gap-2",
96
+ children: [/* @__PURE__ */ jsx(Input, {
97
+ placeholder: "Phone or name",
98
+ value: searchValue,
99
+ onChange: (e) => onSearchValueChange(e.target.value),
100
+ onKeyDown: (e) => {
101
+ if (e.key === "Enter") {
102
+ e.preventDefault();
103
+ onSearch();
104
+ }
105
+ }
106
+ }), /* @__PURE__ */ jsxs(Button, {
107
+ type: "button",
108
+ onClick: onSearch,
109
+ className: "shrink-0",
110
+ children: [/* @__PURE__ */ jsx(Search, { className: "h-4 w-4 mr-2" }), "Search"]
111
+ })]
112
+ }), /* @__PURE__ */ jsx("div", {
113
+ className: "pt-3",
114
+ children: content
115
+ })]
116
+ });
117
+ }
118
+
119
+ //#endregion
120
+ export { CustomerLookupDialog };
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ import { usePosAuth } from "../../runtime/auth-port.js";
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { FormDialog } from "@classytic/fluid/client/core";
6
+ import { customerApi } from "@classytic/commerce-sdk/sales";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { getReceiptLocale, isValidPhone } from "@classytic/commerce-sdk/client";
9
+ import { toast } from "sonner";
10
+ import { Input } from "@/components/ui/input";
11
+
12
+ //#region src/dashboard/components/CustomerQuickAddDialog.tsx
13
+ function CustomerQuickAddDialog({ token, open, onOpenChange, defaultName, defaultPhone, onCreated }) {
14
+ const authToken = usePosAuth().getToken();
15
+ const resolvedToken = token || authToken || "";
16
+ const [name, setName] = useState("");
17
+ const [phone, setPhone] = useState("");
18
+ const [isSaving, setIsSaving] = useState(false);
19
+ useEffect(() => {
20
+ if (!open) return;
21
+ setName(defaultName || "");
22
+ setPhone(defaultPhone || "");
23
+ }, [
24
+ open,
25
+ defaultName,
26
+ defaultPhone
27
+ ]);
28
+ const canSubmit = useMemo(() => {
29
+ return name.trim().length > 1 && isValidPhone(phone.trim());
30
+ }, [name, phone]);
31
+ const handleSubmit = async () => {
32
+ const trimmedName = name.trim();
33
+ const trimmedPhone = phone.trim();
34
+ if (!trimmedName) {
35
+ toast.error("Customer name is required");
36
+ return;
37
+ }
38
+ if (!isValidPhone(trimmedPhone)) {
39
+ toast.error(`Enter a valid phone number (e.g. ${getReceiptLocale().phonePlaceholder})`);
40
+ return;
41
+ }
42
+ try {
43
+ setIsSaving(true);
44
+ const parts = trimmedName.split(/\s+/);
45
+ const given = parts[0] ?? trimmedName;
46
+ const family = parts.length > 1 ? parts.slice(1).join(" ") : "";
47
+ const created = await customerApi.create({
48
+ token: resolvedToken,
49
+ data: {
50
+ name: {
51
+ given,
52
+ family
53
+ },
54
+ contact: { phone: trimmedPhone }
55
+ }
56
+ });
57
+ if (!created) {
58
+ toast.error("Customer saved, but response was empty");
59
+ return;
60
+ }
61
+ onCreated(created);
62
+ onOpenChange(false);
63
+ } catch (error) {
64
+ toast.error(error instanceof Error ? error.message : "Failed to create customer");
65
+ } finally {
66
+ setIsSaving(false);
67
+ }
68
+ };
69
+ return /* @__PURE__ */ jsx(FormDialog, {
70
+ open,
71
+ onOpenChange,
72
+ title: "Add customer",
73
+ description: "Quickly add a customer for POS loyalty tracking.",
74
+ size: "sm",
75
+ onSubmit: (e) => {
76
+ e.preventDefault();
77
+ handleSubmit();
78
+ },
79
+ onCancel: () => onOpenChange(false),
80
+ submitLabel: "Save Customer",
81
+ cancelLabel: "Cancel",
82
+ submitLoading: isSaving,
83
+ submitDisabled: !canSubmit,
84
+ children: /* @__PURE__ */ jsxs("div", {
85
+ className: "space-y-3",
86
+ children: [/* @__PURE__ */ jsx(Input, {
87
+ placeholder: "Full name",
88
+ value: name,
89
+ onChange: (e) => setName(e.target.value)
90
+ }), /* @__PURE__ */ jsx(Input, {
91
+ placeholder: `Phone (${getReceiptLocale().phonePlaceholder})`,
92
+ inputMode: "numeric",
93
+ value: phone,
94
+ onChange: (e) => setPhone(e.target.value)
95
+ })]
96
+ })
97
+ });
98
+ }
99
+
100
+ //#endregion
101
+ export { CustomerQuickAddDialog };
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ import { memo, useCallback, useState } from "react";
4
+ import { FormDialog, StatusBanner } from "@classytic/fluid/client/core";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { FormInput } from "@classytic/fluid/forms";
7
+
8
+ //#region src/dashboard/components/ManagerAuthDialog.tsx
9
+ const ManagerAuthDialog = memo(function ManagerAuthDialog({ open, onOpenChange, onAuthorize, isPending, error, title = "Manager Authorization Required", description = "Enter manager or admin credentials to authorize this action." }) {
10
+ const [email, setEmail] = useState("");
11
+ const [password, setPassword] = useState("");
12
+ const handleSubmit = useCallback(async (e) => {
13
+ e.preventDefault();
14
+ if (!email.trim() || !password.trim()) return;
15
+ try {
16
+ await onAuthorize(email.trim(), password);
17
+ setEmail("");
18
+ setPassword("");
19
+ onOpenChange(false);
20
+ } catch {}
21
+ }, [
22
+ email,
23
+ password,
24
+ onAuthorize,
25
+ onOpenChange
26
+ ]);
27
+ const handleOpenChange = useCallback((nextOpen) => {
28
+ if (!nextOpen) {
29
+ setEmail("");
30
+ setPassword("");
31
+ }
32
+ onOpenChange(nextOpen);
33
+ }, [onOpenChange]);
34
+ return /* @__PURE__ */ jsxs(FormDialog, {
35
+ open,
36
+ onOpenChange: handleOpenChange,
37
+ title,
38
+ description,
39
+ size: "sm",
40
+ onSubmit: handleSubmit,
41
+ onCancel: () => handleOpenChange(false),
42
+ submitLabel: isPending ? "Verifying..." : "Authorize",
43
+ cancelLabel: "Cancel",
44
+ submitLoading: isPending,
45
+ submitDisabled: !email.trim() || !password.trim(),
46
+ children: [
47
+ error && /* @__PURE__ */ jsx(StatusBanner, {
48
+ variant: "error",
49
+ children: error
50
+ }),
51
+ /* @__PURE__ */ jsx(FormInput, {
52
+ name: "manager-email",
53
+ label: "Email",
54
+ type: "email",
55
+ placeholder: "manager@example.com",
56
+ value: email,
57
+ onChange: (value) => setEmail(value),
58
+ disabled: isPending,
59
+ autoComplete: "email",
60
+ autoFocus: true
61
+ }),
62
+ /* @__PURE__ */ jsx(FormInput, {
63
+ name: "manager-password",
64
+ label: "Password",
65
+ type: "password",
66
+ placeholder: "Enter password",
67
+ value: password,
68
+ onChange: (value) => setPassword(value),
69
+ disabled: isPending,
70
+ autoComplete: "current-password"
71
+ })
72
+ ]
73
+ });
74
+ });
75
+
76
+ //#endregion
77
+ export { ManagerAuthDialog };
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import { formatBdt } from "../../lib/money.js";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { calculateVariantPrice, getAvailableVariants, getPosPrice, getPosProductImage, isVariantProduct } from "@classytic/commerce-sdk/sales";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { Badge } from "@/components/ui/badge";
8
+ import { Card, CardContent } from "@/components/ui/card";
9
+
10
+ //#region src/dashboard/components/ProductCard.tsx
11
+ function ProductCard({ product, onAddToCart, onOpenVariantSelector }) {
12
+ const hasVariants = isVariantProduct(product);
13
+ const inStock = product.branchStock?.inStock ?? false;
14
+ const lowStock = product.branchStock?.lowStock ?? false;
15
+ const stockQty = product.branchStock?.quantity ?? 0;
16
+ const getPriceDisplay = () => {
17
+ if (!hasVariants) return {
18
+ price: getPosPrice(product),
19
+ showFrom: false
20
+ };
21
+ const variants = getAvailableVariants(product);
22
+ if (variants.length === 0) return {
23
+ price: getPosPrice(product),
24
+ showFrom: false
25
+ };
26
+ const prices = variants.map((v) => calculateVariantPrice(getPosPrice(product), v.priceModifier || 0));
27
+ const minPrice = Math.min(...prices);
28
+ return {
29
+ price: minPrice,
30
+ showFrom: minPrice !== Math.max(...prices)
31
+ };
32
+ };
33
+ const { price: displayPrice, showFrom } = getPriceDisplay();
34
+ const handleClick = () => {
35
+ if (!inStock) return;
36
+ if (hasVariants) {
37
+ const variants = getAvailableVariants(product);
38
+ if (variants.length === 1) onAddToCart(product, variants[0].sku);
39
+ else onOpenVariantSelector(product);
40
+ } else onAddToCart(product);
41
+ };
42
+ return /* @__PURE__ */ jsx(Card, {
43
+ className: cn("cursor-pointer transition-all hover:shadow-lg hover:scale-[1.02] group relative overflow-hidden", !inStock && "opacity-50 cursor-not-allowed hover:scale-100"),
44
+ onClick: handleClick,
45
+ children: /* @__PURE__ */ jsxs(CardContent, {
46
+ className: "p-0",
47
+ children: [
48
+ /* @__PURE__ */ jsxs("div", {
49
+ className: "absolute top-2 right-2 z-10 flex flex-col gap-1 items-end",
50
+ children: [!inStock ? /* @__PURE__ */ jsx(Badge, {
51
+ variant: "destructive",
52
+ className: "text-xs shadow-md",
53
+ children: "Out of Stock"
54
+ }) : lowStock ? /* @__PURE__ */ jsx(Badge, {
55
+ variant: "secondary",
56
+ className: "text-xs shadow-md",
57
+ children: "Low Stock"
58
+ }) : null, hasVariants && /* @__PURE__ */ jsxs(Badge, {
59
+ variant: "outline",
60
+ className: "text-xs bg-background/90 backdrop-blur-sm shadow-md",
61
+ children: [getAvailableVariants(product).length, " variants"]
62
+ })]
63
+ }),
64
+ /* @__PURE__ */ jsxs("div", {
65
+ className: "aspect-square bg-muted overflow-hidden relative",
66
+ children: [/* @__PURE__ */ jsx("img", {
67
+ src: getPosProductImage(product),
68
+ alt: product.name,
69
+ className: "w-full h-full object-cover transition-transform group-hover:scale-105"
70
+ }), /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity" })]
71
+ }),
72
+ /* @__PURE__ */ jsxs("div", {
73
+ className: "p-3 space-y-1.5",
74
+ children: [/* @__PURE__ */ jsx("h3", {
75
+ className: "font-semibold text-sm leading-tight line-clamp-2 min-h-10",
76
+ children: product.name
77
+ }), /* @__PURE__ */ jsxs("div", {
78
+ className: "flex items-center justify-between pt-1",
79
+ children: [/* @__PURE__ */ jsxs("span", {
80
+ className: "text-base font-bold text-primary",
81
+ children: [showFrom && /* @__PURE__ */ jsx("span", {
82
+ className: "text-xs font-normal text-muted-foreground mr-1",
83
+ children: "From"
84
+ }), formatBdt(displayPrice)]
85
+ }), inStock && /* @__PURE__ */ jsxs("span", {
86
+ className: "text-xs text-muted-foreground",
87
+ children: ["Qty: ", stockQty]
88
+ })]
89
+ })]
90
+ }),
91
+ inStock && /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0 bottom-0 h-1 bg-primary transform scale-x-0 group-hover:scale-x-100 transition-transform origin-left" })
92
+ ]
93
+ })
94
+ });
95
+ }
96
+
97
+ //#endregion
98
+ export { ProductCard };