@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,119 @@
1
+ "use client";
2
+
3
+ import { ProductCard } from "./ProductCard.js";
4
+ import { memo } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { Barcode, Loader2, Package, Search } from "lucide-react";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Input } from "@/components/ui/input";
9
+ import { Skeleton } from "@/components/ui/skeleton";
10
+
11
+ //#region src/dashboard/components/ProductsPanel.tsx
12
+ const ProductsPanel = memo(function ProductsPanel({ searchQuery, selectedCategory, categories, products, productsLoading, barcodeInput, isLookingUp, onSearchChange, onSearchSubmit, onCategoryChange, onBarcodeInputChange, onBarcodeSubmit, onProductAdd, onOpenVariantSelector }) {
13
+ return /* @__PURE__ */ jsxs("div", {
14
+ className: "h-full flex flex-col p-6 bg-background",
15
+ children: [
16
+ /* @__PURE__ */ jsxs("div", {
17
+ className: "grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4",
18
+ children: [/* @__PURE__ */ jsxs("form", {
19
+ className: "flex items-center gap-2",
20
+ onSubmit: (e) => {
21
+ e.preventDefault();
22
+ onSearchSubmit();
23
+ },
24
+ children: [
25
+ /* @__PURE__ */ jsxs("div", {
26
+ className: "relative flex-1",
27
+ 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, {
28
+ placeholder: "Search products...",
29
+ value: searchQuery,
30
+ onChange: (e) => onSearchChange(e.target.value),
31
+ className: "pl-9"
32
+ })]
33
+ }),
34
+ /* @__PURE__ */ jsx(Button, {
35
+ type: "submit",
36
+ variant: "outline",
37
+ size: "icon",
38
+ className: "shrink-0",
39
+ children: /* @__PURE__ */ jsx(Search, { className: "h-4 w-4" })
40
+ }),
41
+ searchQuery.trim().length > 0 && /* @__PURE__ */ jsx(Button, {
42
+ type: "button",
43
+ variant: "ghost",
44
+ size: "sm",
45
+ className: "shrink-0",
46
+ onClick: () => onSearchChange(""),
47
+ children: "Clear"
48
+ })
49
+ ]
50
+ }), /* @__PURE__ */ jsxs("form", {
51
+ onSubmit: onBarcodeSubmit,
52
+ className: "flex items-center gap-2",
53
+ children: [/* @__PURE__ */ jsxs("div", {
54
+ className: "relative flex-1",
55
+ children: [/* @__PURE__ */ jsx(Barcode, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ jsx(Input, {
56
+ placeholder: "Scan barcode or SKU...",
57
+ value: barcodeInput,
58
+ onChange: (e) => onBarcodeInputChange(e.target.value),
59
+ className: "pl-9",
60
+ disabled: isLookingUp
61
+ })]
62
+ }), /* @__PURE__ */ jsx(Button, {
63
+ type: "submit",
64
+ variant: "outline",
65
+ size: "icon",
66
+ className: "shrink-0",
67
+ disabled: isLookingUp || barcodeInput.trim().length === 0,
68
+ "aria-label": "Lookup barcode",
69
+ children: isLookingUp ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Barcode, { className: "h-4 w-4" })
70
+ })]
71
+ })]
72
+ }),
73
+ /* @__PURE__ */ jsxs("div", {
74
+ className: "flex gap-2 mb-4 overflow-x-auto pb-2",
75
+ children: [/* @__PURE__ */ jsx(Button, {
76
+ variant: selectedCategory === "all" ? "default" : "outline",
77
+ size: "sm",
78
+ onClick: () => onCategoryChange("all"),
79
+ children: "All"
80
+ }), categories.map((cat) => /* @__PURE__ */ jsx(Button, {
81
+ variant: selectedCategory === cat.slug ? "default" : "outline",
82
+ size: "sm",
83
+ onClick: () => onCategoryChange(cat.slug),
84
+ className: "capitalize",
85
+ children: cat.label
86
+ }, cat.slug))]
87
+ }),
88
+ /* @__PURE__ */ jsx("div", {
89
+ className: "flex-1 overflow-y-auto min-h-0",
90
+ children: productsLoading ? /* @__PURE__ */ jsx("div", {
91
+ className: "grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4",
92
+ children: [...Array(8)].map((_, i) => /* @__PURE__ */ jsxs("div", {
93
+ className: "rounded-lg border bg-card overflow-hidden",
94
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "aspect-square" }), /* @__PURE__ */ jsxs("div", {
95
+ className: "p-3 space-y-2",
96
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-20" })]
97
+ })]
98
+ }, i))
99
+ }) : products.length === 0 ? /* @__PURE__ */ jsxs("div", {
100
+ className: "flex flex-col items-center justify-center h-64 text-center",
101
+ children: [/* @__PURE__ */ jsx(Package, { className: "h-12 w-12 text-muted-foreground/30 mb-2" }), /* @__PURE__ */ jsx("p", {
102
+ className: "text-muted-foreground",
103
+ children: "No products found"
104
+ })]
105
+ }) : /* @__PURE__ */ jsx("div", {
106
+ className: "grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4",
107
+ children: products.map((product) => /* @__PURE__ */ jsx(ProductCard, {
108
+ product,
109
+ onAddToCart: onProductAdd,
110
+ onOpenVariantSelector
111
+ }, product._id))
112
+ })
113
+ })
114
+ ]
115
+ });
116
+ });
117
+
118
+ //#endregion
119
+ export { ProductsPanel };
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import { formatBdt } from "../../lib/money.js";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { useMemo } from "react";
6
+ import { SelectDropdown } from "@classytic/fluid/client/core";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { Plus, Trash2 } from "lucide-react";
9
+ import { Button } from "@/components/ui/button";
10
+ import { Input } from "@/components/ui/input";
11
+
12
+ //#region src/dashboard/components/SplitPaymentPanel.tsx
13
+ function parseAmount(value) {
14
+ const n = Number(value);
15
+ return Number.isFinite(n) ? n : 0;
16
+ }
17
+ function SplitPaymentPanel({ options, entries, total, remaining, onAdd, onRemove, onUpdate }) {
18
+ const totals = useMemo(() => entries.map((e) => parseAmount(e.amount)), [entries]);
19
+ const sum = useMemo(() => totals.reduce((acc, val) => acc + val, 0), [totals]);
20
+ const getRemainingForRow = (id) => {
21
+ const idx = entries.findIndex((e) => e.id === id);
22
+ const rowAmount = totals[idx] || 0;
23
+ return Math.max(0, total - (sum - rowAmount));
24
+ };
25
+ if (options.length === 0) return /* @__PURE__ */ jsx("div", {
26
+ className: "text-xs text-muted-foreground",
27
+ children: "No active payment methods configured."
28
+ });
29
+ return /* @__PURE__ */ jsxs("div", {
30
+ className: "space-y-3 rounded-lg border bg-muted/20 p-3",
31
+ children: [
32
+ /* @__PURE__ */ jsxs("div", {
33
+ className: "flex items-center justify-between",
34
+ children: [/* @__PURE__ */ jsx("p", {
35
+ className: "text-sm font-medium",
36
+ children: "Split payments"
37
+ }), /* @__PURE__ */ jsxs(Button, {
38
+ type: "button",
39
+ variant: "outline",
40
+ size: "sm",
41
+ className: "h-8",
42
+ onClick: () => onAdd(),
43
+ children: [/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5 mr-1" }), "Add"]
44
+ })]
45
+ }),
46
+ /* @__PURE__ */ jsx("div", {
47
+ className: "space-y-2",
48
+ children: entries.map((entry) => {
49
+ const option = options.find((o) => o.key === entry.paymentKey);
50
+ const needsRef = option?.needsReference ?? false;
51
+ const remainingForRow = getRemainingForRow(entry.id);
52
+ const hasError = Boolean(entry.error);
53
+ return /* @__PURE__ */ jsxs("div", {
54
+ className: cn("rounded-md border bg-card p-2 space-y-2", hasError && "border-destructive"),
55
+ children: [
56
+ /* @__PURE__ */ jsxs("div", {
57
+ className: "flex gap-2 items-center",
58
+ children: [
59
+ /* @__PURE__ */ jsx(SelectDropdown, {
60
+ value: entry.paymentKey,
61
+ onValueChange: (value) => value && onUpdate(entry.id, { paymentKey: value }),
62
+ placeholder: "Method",
63
+ options: options.map((opt) => ({
64
+ value: opt.key,
65
+ label: opt.label
66
+ })),
67
+ triggerClassName: cn("h-9 w-[168px] shrink-0", hasError && "border-destructive")
68
+ }),
69
+ /* @__PURE__ */ jsx(Input, {
70
+ className: cn("h-9 flex-1", hasError && !entry.amount && "border-destructive"),
71
+ placeholder: "Amount",
72
+ inputMode: "numeric",
73
+ value: entry.amount,
74
+ onChange: (e) => onUpdate(entry.id, { amount: e.target.value })
75
+ }),
76
+ /* @__PURE__ */ jsx(Button, {
77
+ type: "button",
78
+ variant: "ghost",
79
+ size: "icon",
80
+ className: "h-9 w-9",
81
+ onClick: () => onRemove(entry.id),
82
+ disabled: entries.length <= 1,
83
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
84
+ })
85
+ ]
86
+ }),
87
+ /* @__PURE__ */ jsxs("div", {
88
+ className: "flex items-center justify-between text-xs text-muted-foreground",
89
+ children: [/* @__PURE__ */ jsx("span", { children: option?.note || "Select method and amount" }), /* @__PURE__ */ jsxs(Button, {
90
+ type: "button",
91
+ variant: "ghost",
92
+ size: "sm",
93
+ className: "h-6 px-2",
94
+ onClick: () => onUpdate(entry.id, { amount: String(remainingForRow) }),
95
+ children: ["Fill ", formatBdt(remainingForRow)]
96
+ })]
97
+ }),
98
+ needsRef && /* @__PURE__ */ jsx(Input, {
99
+ className: cn("h-9", hasError && !entry.reference.trim() && "border-destructive"),
100
+ placeholder: "Reference (Txn ID)",
101
+ value: entry.reference,
102
+ onChange: (e) => onUpdate(entry.id, { reference: e.target.value })
103
+ }),
104
+ entry.error && /* @__PURE__ */ jsx("p", {
105
+ className: "text-xs text-destructive",
106
+ children: entry.error
107
+ })
108
+ ]
109
+ }, entry.id);
110
+ })
111
+ }),
112
+ /* @__PURE__ */ jsxs("div", {
113
+ className: "flex items-center justify-between text-xs text-muted-foreground",
114
+ children: [/* @__PURE__ */ jsxs("span", { children: ["Total: ", /* @__PURE__ */ jsx("span", {
115
+ className: "font-medium text-foreground",
116
+ children: formatBdt(total)
117
+ })] }), /* @__PURE__ */ jsxs("span", { children: [
118
+ "Remaining:",
119
+ " ",
120
+ /* @__PURE__ */ jsx("span", {
121
+ className: remaining === 0 ? "font-medium text-foreground" : "text-destructive font-medium",
122
+ children: formatBdt(remaining)
123
+ })
124
+ ] })]
125
+ })
126
+ ]
127
+ });
128
+ }
129
+
130
+ //#endregion
131
+ export { SplitPaymentPanel };
@@ -0,0 +1,150 @@
1
+ "use client";
2
+
3
+ import { formatBdt } from "../../lib/money.js";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { useMemo, useState } from "react";
6
+ import { DialogWrapper } from "@classytic/fluid/client/core";
7
+ import { calculateVariantPrice, formatVariantLabel, getPosPrice, getPosProductImage, getVariantStock } from "@classytic/commerce-sdk/sales";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { Check, Package } from "lucide-react";
10
+ import { Button } from "@/components/ui/button";
11
+ import { Badge } from "@/components/ui/badge";
12
+
13
+ //#region src/dashboard/components/VariantSelectorDialog.tsx
14
+ function VariantSelectorDialog({ product, open, onOpenChange, onSelect }) {
15
+ const [selectedSku, setSelectedSku] = useState(null);
16
+ const variantOptions = useMemo(() => {
17
+ if (!product?.variants) return [];
18
+ return product.variants.filter((v) => v.isActive).map((variant) => {
19
+ const stock = getVariantStock(product, variant.sku);
20
+ const inStock = stock > 0;
21
+ return {
22
+ sku: variant.sku,
23
+ attributes: variant.attributes,
24
+ label: formatVariantLabel(variant.attributes),
25
+ price: calculateVariantPrice(getPosPrice(product), variant.priceModifier),
26
+ priceModifier: variant.priceModifier || 0,
27
+ stock,
28
+ inStock,
29
+ isActive: variant.isActive
30
+ };
31
+ });
32
+ }, [product]);
33
+ const attributeKeys = useMemo(() => {
34
+ if (!product?.variants?.[0]?.attributes) return [];
35
+ return Object.keys(product.variants[0].attributes);
36
+ }, [product]);
37
+ const handleConfirm = () => {
38
+ if (selectedSku) {
39
+ onSelect(selectedSku);
40
+ onOpenChange(false);
41
+ setSelectedSku(null);
42
+ }
43
+ };
44
+ const handleCancel = () => {
45
+ onOpenChange(false);
46
+ setSelectedSku(null);
47
+ };
48
+ if (!product) return null;
49
+ return /* @__PURE__ */ jsx(DialogWrapper, {
50
+ open,
51
+ onOpenChange,
52
+ title: "Select Variant",
53
+ description: `Choose a variant for ${product.name}`,
54
+ size: "xl",
55
+ footer: /* @__PURE__ */ jsxs("div", {
56
+ className: "flex justify-end gap-2 pt-4 border-t",
57
+ children: [/* @__PURE__ */ jsx(Button, {
58
+ variant: "outline",
59
+ onClick: handleCancel,
60
+ children: "Cancel"
61
+ }), /* @__PURE__ */ jsx(Button, {
62
+ onClick: handleConfirm,
63
+ disabled: !selectedSku,
64
+ children: "Add to Cart"
65
+ })]
66
+ }),
67
+ children: /* @__PURE__ */ jsxs("div", {
68
+ className: "flex-1 overflow-y-auto",
69
+ children: [/* @__PURE__ */ jsxs("div", {
70
+ className: "flex items-center gap-4 p-4 border rounded-lg mb-4 bg-muted/30",
71
+ children: [/* @__PURE__ */ jsx("div", {
72
+ className: "w-20 h-20 rounded bg-muted overflow-hidden shrink-0",
73
+ children: /* @__PURE__ */ jsx("img", {
74
+ src: getPosProductImage(product),
75
+ alt: product.name,
76
+ className: "w-full h-full object-cover"
77
+ })
78
+ }), /* @__PURE__ */ jsxs("div", {
79
+ className: "flex-1 min-w-0",
80
+ children: [
81
+ /* @__PURE__ */ jsx("h3", {
82
+ className: "font-semibold truncate",
83
+ children: product.name
84
+ }),
85
+ /* @__PURE__ */ jsxs("p", {
86
+ className: "text-sm text-muted-foreground",
87
+ children: ["Base Price: ", formatBdt(getPosPrice(product))]
88
+ }),
89
+ /* @__PURE__ */ jsx("div", {
90
+ className: "flex flex-wrap gap-1 mt-1",
91
+ children: attributeKeys.map((key) => /* @__PURE__ */ jsx(Badge, {
92
+ variant: "outline",
93
+ className: "text-xs capitalize",
94
+ children: key
95
+ }, key))
96
+ })
97
+ ]
98
+ })]
99
+ }), /* @__PURE__ */ jsx("div", {
100
+ className: "space-y-2",
101
+ children: variantOptions.length === 0 ? /* @__PURE__ */ jsxs("div", {
102
+ className: "flex flex-col items-center justify-center py-8 text-center",
103
+ children: [/* @__PURE__ */ jsx(Package, { className: "h-12 w-12 text-muted-foreground/30 mb-2" }), /* @__PURE__ */ jsx("p", {
104
+ className: "text-sm text-muted-foreground",
105
+ children: "No variants available"
106
+ })]
107
+ }) : variantOptions.map((variant) => /* @__PURE__ */ jsxs("button", {
108
+ onClick: () => variant.inStock && setSelectedSku(variant.sku),
109
+ disabled: !variant.inStock,
110
+ className: cn("w-full flex items-center justify-between p-4 border rounded-lg transition-all", "hover:shadow-md", selectedSku === variant.sku && "border-primary bg-primary/5 ring-2 ring-primary", !variant.inStock && "opacity-50 cursor-not-allowed hover:shadow-none"),
111
+ children: [/* @__PURE__ */ jsxs("div", {
112
+ className: "flex items-center gap-3",
113
+ children: [/* @__PURE__ */ jsx("div", {
114
+ className: cn("w-5 h-5 rounded-full border-2 flex items-center justify-center", selectedSku === variant.sku ? "border-primary bg-primary" : "border-muted-foreground/30"),
115
+ children: selectedSku === variant.sku && /* @__PURE__ */ jsx(Check, { className: "h-3 w-3 text-primary-foreground" })
116
+ }), /* @__PURE__ */ jsxs("div", {
117
+ className: "text-left",
118
+ children: [/* @__PURE__ */ jsx("p", {
119
+ className: "font-medium",
120
+ children: variant.label
121
+ }), /* @__PURE__ */ jsxs("p", {
122
+ className: "text-xs text-muted-foreground font-mono",
123
+ children: ["SKU: ", variant.sku]
124
+ })]
125
+ })]
126
+ }), /* @__PURE__ */ jsxs("div", {
127
+ className: "flex items-center gap-4",
128
+ children: [/* @__PURE__ */ jsx(Badge, {
129
+ variant: variant.inStock ? "secondary" : "destructive",
130
+ className: "text-xs",
131
+ children: variant.inStock ? `Stock: ${variant.stock}` : "Out of Stock"
132
+ }), /* @__PURE__ */ jsxs("div", {
133
+ className: "text-right",
134
+ children: [/* @__PURE__ */ jsx("p", {
135
+ className: "font-bold",
136
+ children: formatBdt(variant.price)
137
+ }), variant.priceModifier !== 0 && /* @__PURE__ */ jsxs("p", {
138
+ className: "text-xs text-muted-foreground",
139
+ children: [variant.priceModifier > 0 ? "+" : "", formatBdt(variant.priceModifier)]
140
+ })]
141
+ })]
142
+ })]
143
+ }, variant.sku))
144
+ })]
145
+ })
146
+ });
147
+ }
148
+
149
+ //#endregion
150
+ export { VariantSelectorDialog };
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { FormDialog } from "@classytic/fluid/client/core";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { Plus, Sparkles } from "lucide-react";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Input } from "@/components/ui/input";
9
+ import { Label } from "@/components/ui/label";
10
+
11
+ //#region src/dashboard/components/cart/AddChargeDialog.tsx
12
+ function AddChargeDialog({ disabled, onAddCharge }) {
13
+ const [open, setOpen] = useState(false);
14
+ const [label, setLabel] = useState("");
15
+ const [amount, setAmount] = useState("");
16
+ const reset = () => {
17
+ setLabel("");
18
+ setAmount("");
19
+ };
20
+ const trimmedLabel = label.trim();
21
+ const parsedAmount = Number(amount);
22
+ const isValid = trimmedLabel.length > 0 && Number.isFinite(parsedAmount) && parsedAmount >= 0;
23
+ const handleSubmit = () => {
24
+ if (!isValid) return;
25
+ onAddCharge(trimmedLabel, parsedAmount);
26
+ reset();
27
+ setOpen(false);
28
+ };
29
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
30
+ type: "button",
31
+ variant: "outline",
32
+ size: "sm",
33
+ className: "w-full",
34
+ disabled,
35
+ onClick: () => setOpen(true),
36
+ children: [/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 mr-1.5" }), "Add Charge"]
37
+ }), /* @__PURE__ */ jsx(FormDialog, {
38
+ open,
39
+ onOpenChange: (o) => {
40
+ setOpen(o);
41
+ if (!o) reset();
42
+ },
43
+ title: /* @__PURE__ */ jsxs("span", {
44
+ className: "flex items-center gap-2",
45
+ children: [/* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4" }), "Add Service Charge"]
46
+ }),
47
+ description: "Append an ad-hoc charge to this sale (gift wrap, damaged-product reimbursement, custom handling).",
48
+ onSubmit: handleSubmit,
49
+ onCancel: () => {
50
+ setOpen(false);
51
+ reset();
52
+ },
53
+ submitLabel: "Add to cart",
54
+ cancelLabel: "Cancel",
55
+ submitDisabled: !isValid,
56
+ children: /* @__PURE__ */ jsxs("div", {
57
+ className: "space-y-3",
58
+ children: [/* @__PURE__ */ jsxs("div", {
59
+ className: "space-y-1.5",
60
+ children: [
61
+ /* @__PURE__ */ jsx(Label, {
62
+ htmlFor: "add-charge-label",
63
+ children: "Label"
64
+ }),
65
+ /* @__PURE__ */ jsx(Input, {
66
+ id: "add-charge-label",
67
+ autoFocus: true,
68
+ placeholder: "Gift wrap, paper bag, damaged product…",
69
+ value: label,
70
+ onChange: (e) => setLabel(e.target.value),
71
+ maxLength: 120
72
+ }),
73
+ /* @__PURE__ */ jsx("p", {
74
+ className: "text-xs text-muted-foreground",
75
+ children: "Free-form. Reports group by a slug derived from this label, so similar entries (\"Gift wrap\" / \"gift wrap\") aggregate together."
76
+ })
77
+ ]
78
+ }), /* @__PURE__ */ jsxs("div", {
79
+ className: "space-y-1.5",
80
+ children: [/* @__PURE__ */ jsx(Label, {
81
+ htmlFor: "add-charge-amount",
82
+ children: "Amount (BDT)"
83
+ }), /* @__PURE__ */ jsx(Input, {
84
+ id: "add-charge-amount",
85
+ type: "number",
86
+ inputMode: "decimal",
87
+ min: 0,
88
+ step: "0.01",
89
+ placeholder: "20",
90
+ value: amount,
91
+ onChange: (e) => setAmount(e.target.value)
92
+ })]
93
+ })]
94
+ })
95
+ })] });
96
+ }
97
+
98
+ //#endregion
99
+ export { AddChargeDialog };
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ import { formatBdt } from "../../../lib/money.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { ShoppingCart, Sparkles } from "lucide-react";
6
+ import { Button } from "@/components/ui/button";
7
+
8
+ //#region src/dashboard/components/cart/CartItems.tsx
9
+ function CartItems({ items, onUpdateQuantity, onRemoveItem }) {
10
+ if (items.length === 0) return /* @__PURE__ */ jsxs("div", {
11
+ className: "flex flex-col items-center justify-center h-full text-center py-12",
12
+ children: [/* @__PURE__ */ jsx(ShoppingCart, { className: "h-12 w-12 text-muted-foreground/30 mb-2" }), /* @__PURE__ */ jsx("p", {
13
+ className: "text-sm text-muted-foreground",
14
+ children: "Cart is empty"
15
+ })]
16
+ });
17
+ return /* @__PURE__ */ jsx("div", {
18
+ className: "space-y-2",
19
+ children: items.map((item, index) => /* @__PURE__ */ jsx(CartItemRow, {
20
+ item,
21
+ onQuantityChange: (delta) => onUpdateQuantity(index, delta),
22
+ onRemove: () => onRemoveItem(index)
23
+ }, `${item.productId}-${item.variantSku || "simple"}`))
24
+ });
25
+ }
26
+ function CartItemRow({ item, onQuantityChange, onRemove }) {
27
+ const isServiceFee = item.kind === "service-fee";
28
+ return /* @__PURE__ */ jsxs("div", {
29
+ className: "flex gap-3 p-3 rounded-lg border bg-card",
30
+ children: [
31
+ /* @__PURE__ */ jsx("div", {
32
+ className: "w-16 h-16 rounded bg-muted overflow-hidden shrink-0 flex items-center justify-center",
33
+ children: isServiceFee ? /* @__PURE__ */ jsx(Sparkles, {
34
+ className: "h-5 w-5 text-muted-foreground",
35
+ "aria-label": "Service charge"
36
+ }) : /* @__PURE__ */ jsx("img", {
37
+ src: item.image,
38
+ alt: item.productName,
39
+ className: "w-full h-full object-cover"
40
+ })
41
+ }),
42
+ /* @__PURE__ */ jsxs("div", {
43
+ className: "flex-1 min-w-0",
44
+ children: [
45
+ /* @__PURE__ */ jsx("h4", {
46
+ className: "text-sm font-medium truncate",
47
+ children: item.productName
48
+ }),
49
+ isServiceFee ? /* @__PURE__ */ jsx("p", {
50
+ className: "text-xs text-muted-foreground italic",
51
+ children: "Service charge"
52
+ }) : item.variantLabel && /* @__PURE__ */ jsx("p", {
53
+ className: "text-xs text-muted-foreground",
54
+ children: item.variantLabel
55
+ }),
56
+ /* @__PURE__ */ jsxs("div", {
57
+ className: "flex items-center gap-2 mt-2",
58
+ children: [
59
+ /* @__PURE__ */ jsx(Button, {
60
+ variant: "outline",
61
+ size: "icon",
62
+ className: "h-6 w-6",
63
+ onClick: () => onQuantityChange(-1),
64
+ children: "-"
65
+ }),
66
+ /* @__PURE__ */ jsx("span", {
67
+ className: "text-sm font-medium w-8 text-center",
68
+ children: item.quantity
69
+ }),
70
+ /* @__PURE__ */ jsx(Button, {
71
+ variant: "outline",
72
+ size: "icon",
73
+ className: "h-6 w-6",
74
+ onClick: () => onQuantityChange(1),
75
+ children: "+"
76
+ }),
77
+ /* @__PURE__ */ jsx(Button, {
78
+ variant: "ghost",
79
+ size: "sm",
80
+ className: "h-6 ml-auto text-xs",
81
+ onClick: onRemove,
82
+ children: "Remove"
83
+ })
84
+ ]
85
+ })
86
+ ]
87
+ }),
88
+ /* @__PURE__ */ jsxs("div", {
89
+ className: "text-right",
90
+ children: [/* @__PURE__ */ jsx("p", {
91
+ className: "font-bold",
92
+ children: formatBdt(item.lineTotal)
93
+ }), /* @__PURE__ */ jsxs("p", {
94
+ className: "text-xs text-muted-foreground",
95
+ children: [formatBdt(item.unitPrice), " each"]
96
+ })]
97
+ })
98
+ ]
99
+ });
100
+ }
101
+
102
+ //#endregion
103
+ export { CartItems };
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ import { formatBdt } from "../../../lib/money.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+
6
+ //#region src/dashboard/components/cart/CartSummary.tsx
7
+ function CartSummary({ subtotal, discount, redemptionDiscount = 0, tierDiscount = 0, tierName, tierColor, total, pointsToEarn = 0 }) {
8
+ return /* @__PURE__ */ jsxs("div", {
9
+ className: "space-y-2 text-sm",
10
+ children: [
11
+ (discount > 0 || tierDiscount > 0 || redemptionDiscount > 0) && /* @__PURE__ */ jsxs("div", {
12
+ className: "flex justify-between",
13
+ children: [/* @__PURE__ */ jsx("span", {
14
+ className: "text-muted-foreground",
15
+ children: "Subtotal"
16
+ }), /* @__PURE__ */ jsx("span", {
17
+ className: "font-medium",
18
+ children: formatBdt(subtotal)
19
+ })]
20
+ }),
21
+ discount > 0 && /* @__PURE__ */ jsxs("div", {
22
+ className: "flex justify-between",
23
+ children: [/* @__PURE__ */ jsx("span", {
24
+ className: "text-muted-foreground",
25
+ children: "Discount"
26
+ }), /* @__PURE__ */ jsxs("span", {
27
+ className: "font-medium",
28
+ children: ["-", formatBdt(discount)]
29
+ })]
30
+ }),
31
+ tierDiscount > 0 && /* @__PURE__ */ jsxs("div", {
32
+ className: "flex justify-between",
33
+ children: [/* @__PURE__ */ jsxs("span", {
34
+ className: "text-muted-foreground",
35
+ children: ["Tier Discount", tierName ? ` (${tierName})` : ""]
36
+ }), /* @__PURE__ */ jsxs("span", {
37
+ className: "font-medium",
38
+ style: tierColor ? { color: tierColor } : void 0,
39
+ children: ["-", formatBdt(tierDiscount)]
40
+ })]
41
+ }),
42
+ redemptionDiscount > 0 && /* @__PURE__ */ jsxs("div", {
43
+ className: "flex justify-between",
44
+ children: [/* @__PURE__ */ jsx("span", {
45
+ className: "text-muted-foreground",
46
+ children: "Points Redeemed"
47
+ }), /* @__PURE__ */ jsxs("span", {
48
+ className: "font-medium",
49
+ children: ["-", formatBdt(redemptionDiscount)]
50
+ })]
51
+ }),
52
+ /* @__PURE__ */ jsxs("div", {
53
+ className: "flex justify-between text-lg font-bold",
54
+ children: [/* @__PURE__ */ jsx("span", { children: "Total" }), /* @__PURE__ */ jsx("span", { children: formatBdt(total) })]
55
+ }),
56
+ pointsToEarn > 0 && /* @__PURE__ */ jsxs("div", {
57
+ className: "flex justify-between text-xs text-muted-foreground",
58
+ children: [/* @__PURE__ */ jsx("span", { children: "Points to Earn" }), /* @__PURE__ */ jsxs("span", {
59
+ className: "font-medium",
60
+ style: tierColor ? { color: tierColor } : void 0,
61
+ children: [
62
+ "+",
63
+ pointsToEarn,
64
+ " pts"
65
+ ]
66
+ })]
67
+ })
68
+ ]
69
+ });
70
+ }
71
+
72
+ //#endregion
73
+ export { CartSummary };