@base44/app-plugin-commerce 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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/base44/agents/commerce/StoreAdmin.jsonc +64 -0
  4. package/base44/entities/commerce.Cart.jsonc +73 -0
  5. package/base44/entities/commerce.Coupon.jsonc +113 -0
  6. package/base44/entities/commerce.Customer.jsonc +96 -0
  7. package/base44/entities/commerce.DownloadPermission.jsonc +54 -0
  8. package/base44/entities/commerce.EmailLog.jsonc +43 -0
  9. package/base44/entities/commerce.Order.jsonc +287 -0
  10. package/base44/entities/commerce.OrderNote.jsonc +31 -0
  11. package/base44/entities/commerce.OrderRefund.jsonc +64 -0
  12. package/base44/entities/commerce.PaymentGateway.jsonc +48 -0
  13. package/base44/entities/commerce.Product.jsonc +291 -0
  14. package/base44/entities/commerce.ProductAttribute.jsonc +39 -0
  15. package/base44/entities/commerce.ProductAttributeTerm.jsonc +38 -0
  16. package/base44/entities/commerce.ProductCategory.jsonc +51 -0
  17. package/base44/entities/commerce.ProductReview.jsonc +48 -0
  18. package/base44/entities/commerce.ProductTag.jsonc +30 -0
  19. package/base44/entities/commerce.ProductVariation.jsonc +167 -0
  20. package/base44/entities/commerce.ShippingClass.jsonc +30 -0
  21. package/base44/entities/commerce.ShippingZone.jsonc +41 -0
  22. package/base44/entities/commerce.ShippingZoneMethod.jsonc +84 -0
  23. package/base44/entities/commerce.StoreSettings.jsonc +23 -0
  24. package/base44/entities/commerce.TaxClass.jsonc +23 -0
  25. package/base44/entities/commerce.TaxRate.jsonc +68 -0
  26. package/base44/entities/commerce.Webhook.jsonc +57 -0
  27. package/base44/entities/commerce.WebhookDelivery.jsonc +45 -0
  28. package/base44/functions/commerce/admin-coupons/entry.ts +100 -0
  29. package/base44/functions/commerce/admin-customers/entry.ts +141 -0
  30. package/base44/functions/commerce/admin-orders/entry.ts +396 -0
  31. package/base44/functions/commerce/admin-orders/helpers.ts +246 -0
  32. package/base44/functions/commerce/admin-products/entry.ts +506 -0
  33. package/base44/functions/commerce/admin-refunds/entry.ts +158 -0
  34. package/base44/functions/commerce/admin-reports/entry.ts +283 -0
  35. package/base44/functions/commerce/admin-reviews/entry.ts +66 -0
  36. package/base44/functions/commerce/admin-tools/entry.ts +261 -0
  37. package/base44/functions/commerce/admin-webhooks/entry.ts +52 -0
  38. package/base44/functions/commerce/payment-webhook/entry.ts +135 -0
  39. package/base44/functions/commerce/payments/entry.ts +238 -0
  40. package/base44/functions/commerce/seed-store/defaults.ts +162 -0
  41. package/base44/functions/commerce/seed-store/entry.ts +310 -0
  42. package/base44/functions/commerce/seed-store/sample-data.ts +349 -0
  43. package/base44/functions/commerce/storefront-account/entry.ts +207 -0
  44. package/base44/functions/commerce/storefront-cart/cart-pricing.ts +258 -0
  45. package/base44/functions/commerce/storefront-cart/entry.ts +283 -0
  46. package/base44/functions/commerce/storefront-catalog/entry.ts +459 -0
  47. package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +258 -0
  48. package/base44/functions/commerce/storefront-checkout/entry.ts +485 -0
  49. package/base44/shared/commerce/auth.ts +60 -0
  50. package/base44/shared/commerce/coupons.ts +257 -0
  51. package/base44/shared/commerce/data/continents.ts +75 -0
  52. package/base44/shared/commerce/data/countries.ts +307 -0
  53. package/base44/shared/commerce/data/currencies.ts +46 -0
  54. package/base44/shared/commerce/email-templates.ts +240 -0
  55. package/base44/shared/commerce/emails.ts +225 -0
  56. package/base44/shared/commerce/money.ts +66 -0
  57. package/base44/shared/commerce/orders.ts +251 -0
  58. package/base44/shared/commerce/payments.ts +495 -0
  59. package/base44/shared/commerce/reviews.ts +36 -0
  60. package/base44/shared/commerce/scan.ts +57 -0
  61. package/base44/shared/commerce/sequence.ts +35 -0
  62. package/base44/shared/commerce/settings.ts +57 -0
  63. package/base44/shared/commerce/shipping.ts +215 -0
  64. package/base44/shared/commerce/stock.ts +227 -0
  65. package/base44/shared/commerce/stripe.ts +463 -0
  66. package/base44/shared/commerce/tax.ts +136 -0
  67. package/base44/shared/commerce/totals.ts +314 -0
  68. package/base44/shared/commerce/webhooks.ts +116 -0
  69. package/package.json +37 -0
  70. package/scripts/install.js +156 -0
  71. package/skills/commerce/SKILL.md +62 -0
  72. package/skills/commerce/docs/api-admin.md +186 -0
  73. package/skills/commerce/docs/api-storefront.md +408 -0
  74. package/skills/commerce/installation-guidelines.md +91 -0
  75. package/skills/commerce/post-installation.md +157 -0
  76. package/skills/commerce/references/emails.md +13 -0
  77. package/skills/commerce/references/guest-access-security.md +18 -0
  78. package/skills/commerce/references/limits-and-performance.md +16 -0
  79. package/skills/commerce/references/media-and-downloads.md +4 -0
  80. package/skills/commerce/references/online-payments.md +201 -0
  81. package/skills/commerce/references/product-render.md +87 -0
  82. package/skills/commerce/references/scheduled-work.md +19 -0
  83. package/skills/commerce/references/storefront-product-page.md +83 -0
  84. package/skills/commerce/references/webhooks.md +8 -0
  85. package/src/commerce/admin/README.md +107 -0
  86. package/src/commerce/admin/bot/Markdown.jsx +138 -0
  87. package/src/commerce/admin/bot/StoreAdminBot.jsx +249 -0
  88. package/src/commerce/admin/bot/pipe-tables.js +116 -0
  89. package/src/commerce/admin/components/AddressForm.jsx +78 -0
  90. package/src/commerce/admin/components/ConfirmDialog.jsx +52 -0
  91. package/src/commerce/admin/components/CountrySelect.jsx +81 -0
  92. package/src/commerce/admin/components/DataTable.jsx +192 -0
  93. package/src/commerce/admin/components/DateRangePicker.jsx +91 -0
  94. package/src/commerce/admin/components/EmptyState.jsx +17 -0
  95. package/src/commerce/admin/components/MediaUploader.jsx +116 -0
  96. package/src/commerce/admin/components/MetaDataEditor.jsx +45 -0
  97. package/src/commerce/admin/components/MoneyInput.jsx +50 -0
  98. package/src/commerce/admin/components/PageHeader.jsx +29 -0
  99. package/src/commerce/admin/components/RichTextarea.jsx +21 -0
  100. package/src/commerce/admin/components/SearchSelect.jsx +142 -0
  101. package/src/commerce/admin/components/StatusBadge.jsx +17 -0
  102. package/src/commerce/admin/context/BasePathContext.jsx +26 -0
  103. package/src/commerce/admin/context/SettingsContext.jsx +207 -0
  104. package/src/commerce/admin/hooks/useAsync.js +46 -0
  105. package/src/commerce/admin/hooks/useDebounce.js +11 -0
  106. package/src/commerce/admin/hooks/useMoney.js +52 -0
  107. package/src/commerce/admin/hooks/usePagedList.js +83 -0
  108. package/src/commerce/admin/hooks/usePaymentProvider.js +27 -0
  109. package/src/commerce/admin/hooks/useRealtime.js +129 -0
  110. package/src/commerce/admin/index.jsx +34 -0
  111. package/src/commerce/admin/layout/AccessDenied.jsx +54 -0
  112. package/src/commerce/admin/layout/AdminLayout.jsx +33 -0
  113. package/src/commerce/admin/layout/AuthGuard.jsx +84 -0
  114. package/src/commerce/admin/layout/Sidebar.jsx +130 -0
  115. package/src/commerce/admin/layout/Topbar.jsx +94 -0
  116. package/src/commerce/admin/lib/api.js +55 -0
  117. package/src/commerce/admin/lib/constants.js +157 -0
  118. package/src/commerce/admin/lib/format.js +27 -0
  119. package/src/commerce/admin/lib/geo-data.js +125 -0
  120. package/src/commerce/admin/lib/order-utils.js +147 -0
  121. package/src/commerce/admin/lib/paths.js +35 -0
  122. package/src/commerce/admin/lib/product-utils.js +55 -0
  123. package/src/commerce/admin/pages/Dashboard.jsx +245 -0
  124. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +565 -0
  125. package/src/commerce/admin/pages/coupons/CouponsList.jsx +172 -0
  126. package/src/commerce/admin/pages/customers/CustomerEditor.jsx +318 -0
  127. package/src/commerce/admin/pages/customers/CustomersList.jsx +169 -0
  128. package/src/commerce/admin/pages/orders/OrderEditor.jsx +952 -0
  129. package/src/commerce/admin/pages/orders/OrdersList.jsx +227 -0
  130. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +149 -0
  131. package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +119 -0
  132. package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +208 -0
  133. package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +123 -0
  134. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +199 -0
  135. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +239 -0
  136. package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +52 -0
  137. package/src/commerce/admin/pages/products/AttributeTerms.jsx +180 -0
  138. package/src/commerce/admin/pages/products/Attributes.jsx +183 -0
  139. package/src/commerce/admin/pages/products/Categories.jsx +236 -0
  140. package/src/commerce/admin/pages/products/ProductEditor.jsx +267 -0
  141. package/src/commerce/admin/pages/products/ProductsList.jsx +391 -0
  142. package/src/commerce/admin/pages/products/Reviews.jsx +255 -0
  143. package/src/commerce/admin/pages/products/Tags.jsx +150 -0
  144. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +132 -0
  145. package/src/commerce/admin/pages/products/components/PublishBox.jsx +101 -0
  146. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +243 -0
  147. package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +48 -0
  148. package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +208 -0
  149. package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +91 -0
  150. package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +41 -0
  151. package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +103 -0
  152. package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +93 -0
  153. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +102 -0
  154. package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +86 -0
  155. package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +377 -0
  156. package/src/commerce/admin/pages/reports/Reports.jsx +416 -0
  157. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +240 -0
  158. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +232 -0
  159. package/src/commerce/admin/pages/settings/InventorySettings.jsx +146 -0
  160. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +260 -0
  161. package/src/commerce/admin/pages/settings/ProductsSettings.jsx +118 -0
  162. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +53 -0
  163. package/src/commerce/admin/pages/settings/ShippingSettings.jsx +304 -0
  164. package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +514 -0
  165. package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +231 -0
  166. package/src/commerce/admin/pages/settings/TaxSettings.jsx +281 -0
  167. package/src/commerce/admin/pages/settings/useGroupForm.jsx +76 -0
  168. package/src/commerce/admin/pages/status/WebhookEditor.jsx +296 -0
  169. package/src/commerce/admin/pages/status/Webhooks.jsx +53 -0
  170. package/src/commerce/admin/routes.jsx +151 -0
  171. package/src/commerce/utils/index.js +19 -0
  172. package/src/commerce/utils/shipping-promos.js +99 -0
  173. package/src/commerce/utils/variants.js +411 -0
@@ -0,0 +1,147 @@
1
+ /** Client-side helpers for order display and refund math. */
2
+
3
+ export const round2 = (n) => Math.round((Number(n) || 0) * 100) / 100;
4
+
5
+ /** Statuses in which line items are editable (intended behavior). */
6
+ export function canEditLines(order) {
7
+ return ["pending", "on-hold"].includes(order?.status);
8
+ }
9
+
10
+ /** Aggregate display totals for the TotalsBox. */
11
+ export function orderTotals(order) {
12
+ if (!order) return null;
13
+ const fees = (order.fee_lines || []).reduce((s, f) => s + (f.total || 0), 0);
14
+ const refunded = order.total_refunded || 0;
15
+ return {
16
+ itemsSubtotal: order.subtotal || 0,
17
+ discount: order.discount_total || 0,
18
+ fees: round2(fees),
19
+ shipping: order.shipping_total || 0,
20
+ taxLines: order.tax_lines || [],
21
+ taxTotal: order.total_tax || 0,
22
+ total: order.total || 0,
23
+ refunded,
24
+ net: round2((order.total || 0) - refunded),
25
+ };
26
+ }
27
+
28
+ /** Quantity of a line still refundable, given existing refunds. */
29
+ export function refundableQty(order, refunds, lineId) {
30
+ const line = (order?.line_items || []).find((l) => l.line_id === lineId);
31
+ if (!line) return 0;
32
+ const refundedQty = (refunds || []).reduce(
33
+ (sum, r) =>
34
+ sum +
35
+ (r.line_items || [])
36
+ .filter((rl) => rl.line_id === lineId)
37
+ .reduce((s, rl) => s + (rl.quantity || 0), 0),
38
+ 0
39
+ );
40
+ return Math.max(0, (line.quantity || 0) - refundedQty);
41
+ }
42
+
43
+ /** Max amount still refundable on the order. */
44
+ export function refundableAmount(order) {
45
+ return round2((order?.total || 0) - (order?.total_refunded || 0));
46
+ }
47
+
48
+ /** Human line for a shipping/billing address. */
49
+ export function shortAddress(addr) {
50
+ if (!addr) return "—";
51
+ return [addr.city, addr.state, addr.country].filter(Boolean).join(", ") || "—";
52
+ }
53
+
54
+ /**
55
+ * Patch keys whose value the backend has to re-price (tax, coupons, shipping,
56
+ * totals). The order editor sends exactly these to `admin-orders` `preview` to
57
+ * show live totals for unsaved edits.
58
+ */
59
+ export const PRICED_PATCH_KEYS = ["items", "fees", "shipping_lines", "billing", "shipping"];
60
+
61
+ /** An address with nothing filled in — safe to overwrite from a customer record. */
62
+ export function isBlankAddress(address) {
63
+ return !Object.values(address || {}).some((v) => String(v ?? "").trim() !== "");
64
+ }
65
+
66
+ /**
67
+ * Build the `admin-orders` `update` patch for an edited order.
68
+ *
69
+ * @param {object} draft the editor's working copy
70
+ * @param {object} before the same shape taken from the loaded order
71
+ * @returns {object} patch containing only what actually changed
72
+ *
73
+ * Line-level edits go out as **intent specs** (`items`/`fees`/`shipping_lines`),
74
+ * never raw line arrays: the server re-prices them through the totals engine so
75
+ * tax and coupons stay authoritative. Shared by save and the totals preview, so
76
+ * a preview is priced from exactly what saving would send.
77
+ */
78
+ export function buildOrderPatch(draft, before) {
79
+ const changed = (key) => JSON.stringify(draft[key]) !== JSON.stringify(before[key]);
80
+ const patch = {};
81
+
82
+ // Passthrough fields the server stores verbatim.
83
+ for (const key of ["status", "customer_id", "billing", "shipping", "meta_data"]) {
84
+ if (changed(key)) patch[key] = draft[key];
85
+ }
86
+ if (changed("line_items")) {
87
+ patch.items = (draft.line_items || []).map((l) => ({
88
+ product_id: l.product_id,
89
+ variation_id: l.variation_id || undefined,
90
+ quantity: l.quantity,
91
+ // pre-discount unit price so the engine re-applies coupons exactly once
92
+ price_override: l.quantity ? round2((l.subtotal ?? l.price * l.quantity) / l.quantity) : l.price,
93
+ attributes: l.attributes,
94
+ meta_data: l.meta_data,
95
+ }));
96
+ }
97
+ if (changed("fee_lines")) {
98
+ patch.fees = (draft.fee_lines || []).map((f) => ({
99
+ name: f.name,
100
+ amount: f.total,
101
+ tax_class: f.tax_class,
102
+ tax_status: f.tax_status,
103
+ }));
104
+ }
105
+ if (changed("shipping_lines")) {
106
+ // Manual admin shipping lines (title + cost); server folds them into totals.
107
+ patch.shipping_lines = (draft.shipping_lines || []).map((s) => ({
108
+ method_title: s.method_title,
109
+ total: s.total,
110
+ total_tax: s.total_tax,
111
+ }));
112
+ }
113
+ return patch;
114
+ }
115
+
116
+ /** The subset of a patch that changes pricing — `{}` when nothing does. */
117
+ export function pricedPatchOf(patch) {
118
+ const priced = {};
119
+ for (const key of PRICED_PATCH_KEYS) {
120
+ if (patch?.[key] !== undefined) priced[key] = patch[key];
121
+ }
122
+ return priced;
123
+ }
124
+
125
+ /**
126
+ * Fold a server-priced preview's per-line figures onto the draft's lines, so the
127
+ * items table's Tax/Total columns agree with the totals box before saving.
128
+ * Keeps the draft's own `line_id`s (the preview mints new ones) and copies only
129
+ * display figures; falls back to the draft lines if the shapes don't line up.
130
+ */
131
+ export function mergePricedLines(draftLines, pricedLines) {
132
+ const lines = draftLines || [];
133
+ if (!pricedLines || pricedLines.length !== lines.length) return lines;
134
+ return lines.map((l, i) => ({
135
+ ...l,
136
+ subtotal: pricedLines[i].subtotal ?? l.subtotal,
137
+ total: pricedLines[i].total ?? l.total,
138
+ total_tax: pricedLines[i].total_tax ?? l.total_tax,
139
+ taxes: pricedLines[i].taxes ?? l.taxes,
140
+ }));
141
+ }
142
+
143
+ export function customerName(order) {
144
+ const b = order?.billing || {};
145
+ const name = [b.first_name, b.last_name].filter(Boolean).join(" ");
146
+ return name || b.email || "Guest";
147
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Mount-path handling for the admin app.
3
+ *
4
+ * The admin is mounted with a splat route — `<Route path="/admin/*">` — and both
5
+ * halves of that pattern leak into places they shouldn't: the `basePath` prop
6
+ * gets the pattern pasted in verbatim, and the literal URL `/admin/*` gets
7
+ * opened (pattern copied into the address bar, or a nav link built from the
8
+ * route table). Neither is a real page, so both are normalized here rather than
9
+ * dead-ending the admin.
10
+ */
11
+
12
+ /** Strip a trailing route splat and slashes: "/admin/*" → "/admin". */
13
+ export function normalizeBasePath(value, fallback = "/admin") {
14
+ const clean = String(value ?? "")
15
+ .replace(/\/+\*+$/, "")
16
+ .replace(/\/+$/, "");
17
+ return clean || fallback;
18
+ }
19
+
20
+ /**
21
+ * Is this leftover path (the `*` param of the catch-all route) nothing but
22
+ * route-*pattern* segments — `*`, its percent-encoded form, or `:param`?
23
+ *
24
+ * `/admin/*` → true (send them to the dashboard)
25
+ * `/admin/ordrs` → false (a genuine 404)
26
+ */
27
+ export function isMountPatternPath(rest) {
28
+ const segments = String(rest ?? "")
29
+ .split("/")
30
+ .filter(Boolean);
31
+ return (
32
+ segments.length > 0 &&
33
+ segments.every((s) => s === "*" || s.toLowerCase() === "%2a" || s.startsWith(":"))
34
+ );
35
+ }
@@ -0,0 +1,55 @@
1
+ /** Client-side helpers for products and variations. */
2
+
3
+ export function slugify(name) {
4
+ return String(name || "")
5
+ .normalize("NFKD")
6
+ .replace(/[̀-ͯ]/g, "")
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9]+/g, "-")
9
+ .replace(/^-+|-+$/g, "");
10
+ }
11
+
12
+ export const isSimple = (p) => p?.type === "simple";
13
+ export const isGrouped = (p) => p?.type === "grouped";
14
+ export const isExternal = (p) => p?.type === "external";
15
+ export const isVariable = (p) => p?.type === "variable";
16
+
17
+ /**
18
+ * Cartesian product of all attributes flagged `variation: true`.
19
+ * Returns [{attributes: [{attribute_id, name, option}]}] — one entry per combo.
20
+ * Returns [] when no variation attributes with options exist.
21
+ */
22
+ export function generateVariationCombos(attributes) {
23
+ const varAttrs = (attributes || []).filter(
24
+ (a) => a.variation && (a.options || []).length > 0
25
+ );
26
+ if (!varAttrs.length) return [];
27
+ const combos = varAttrs.reduce(
28
+ (acc, attr) =>
29
+ acc.flatMap((combo) =>
30
+ attr.options.map((option) => [
31
+ ...combo,
32
+ { attribute_id: attr.attribute_id || "", name: attr.name, option },
33
+ ])
34
+ ),
35
+ [[]]
36
+ );
37
+ return combos.map((attrs) => ({ attributes: attrs }));
38
+ }
39
+
40
+ /** Display label for a variation, e.g. "Color: Red / Size: M". */
41
+ export function variationLabel(variation) {
42
+ return (variation?.attributes || [])
43
+ .map((a) => `${a.name}: ${a.option}`)
44
+ .join(" / ") || "Any";
45
+ }
46
+
47
+ /** True when two variations have the same attribute combo. */
48
+ export function sameCombo(a, b) {
49
+ const key = (v) =>
50
+ (v?.attributes || [])
51
+ .map((x) => `${(x.name || "").toLowerCase()}=${(x.option || "").toLowerCase()}`)
52
+ .sort()
53
+ .join("|");
54
+ return key(a) === key(b);
55
+ }
@@ -0,0 +1,245 @@
1
+ import React from "react";
2
+ import { Link, useNavigate } from "react-router-dom";
3
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Skeleton } from "@/components/ui/skeleton";
7
+ import {
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableHead,
12
+ TableHeader,
13
+ TableRow,
14
+ } from "@/components/ui/table";
15
+ import { BarChart3, PackagePlus, Plus, ShoppingCart, TicketPercent, TrendingUp, AlertTriangle } from "lucide-react";
16
+
17
+ import { call, base44 } from "../lib/api";
18
+ import useAsync from "../hooks/useAsync";
19
+ import useRealtime from "../hooks/useRealtime";
20
+ import useMoney from "../hooks/useMoney";
21
+ import { useAdminHref } from "../context/BasePathContext";
22
+ import { formatDate } from "../lib/format";
23
+ import { ORDER_STATUSES } from "../lib/constants";
24
+ import { customerName } from "../lib/order-utils";
25
+ import StatusBadge from "../components/StatusBadge";
26
+ import EmptyState from "../components/EmptyState";
27
+
28
+ /** Pull a number out of a summary value that may be a number or {net, gross, …}. */
29
+ function asMoneyNumber(v) {
30
+ if (v == null) return 0;
31
+ if (typeof v === "number") return v;
32
+ return v.net_sales ?? v.net ?? v.total ?? 0;
33
+ }
34
+
35
+ function StatCard({ icon: Icon, label, value, hint, loading }) {
36
+ return (
37
+ <Card>
38
+ <CardContent className="flex items-center gap-4 p-5">
39
+ <div className="rounded-lg bg-muted p-2.5">
40
+ <Icon className="h-5 w-5 text-muted-foreground" />
41
+ </div>
42
+ <div className="min-w-0">
43
+ <p className="text-xs text-muted-foreground">{label}</p>
44
+ {loading ? (
45
+ <Skeleton className="mt-1 h-6 w-20" />
46
+ ) : (
47
+ <p className="truncate text-xl font-semibold">{value}</p>
48
+ )}
49
+ {hint && !loading && <p className="text-xs text-muted-foreground">{hint}</p>}
50
+ </div>
51
+ </CardContent>
52
+ </Card>
53
+ );
54
+ }
55
+
56
+ export default function Dashboard() {
57
+ const href = useAdminHref();
58
+ const navigate = useNavigate();
59
+ const { format } = useMoney();
60
+
61
+ const summary = useAsync(() => call("admin-reports", "summary", {}, { silent: true }), []);
62
+ const stock = useAsync(() => call("admin-reports", "stock", {}, { silent: true }), []);
63
+ const latest = useAsync(() => base44.entities["commerce.Order"].list("-created_date", 8), []);
64
+
65
+ // A dashboard that needs a manual refresh to show a new order isn't much of a
66
+ // dashboard. Orders/refunds move the sales + status tiles and the latest-orders
67
+ // table; products move the low-stock list.
68
+ // The summary/stock actions scan orders, so the no-push fallback polls slowly.
69
+ useRealtime(
70
+ ["commerce.Order", "commerce.OrderRefund", "commerce.Product"],
71
+ () => {
72
+ summary.refetchQuiet();
73
+ stock.refetchQuiet();
74
+ latest.refetchQuiet();
75
+ },
76
+ { fallbackPollMs: 60000 },
77
+ );
78
+
79
+ const s = summary.data || {};
80
+ const ordersByStatus = s.orders_by_status || {};
81
+ const lowStock = (stock.data?.low_stock || []).slice(0, 6);
82
+ const processingCount = ordersByStatus.processing || 0;
83
+
84
+ return (
85
+ <div className="space-y-6">
86
+ <div className="flex flex-wrap items-center justify-between gap-3">
87
+ <div>
88
+ <h1 className="text-xl font-semibold tracking-tight">Dashboard</h1>
89
+ <p className="text-sm text-muted-foreground">Your store at a glance.</p>
90
+ </div>
91
+ <div className="flex gap-2">
92
+ <Button variant="outline" size="sm" asChild>
93
+ <Link to={href("products/new")}>
94
+ <PackagePlus className="mr-1.5 h-4 w-4" /> Add product
95
+ </Link>
96
+ </Button>
97
+ <Button variant="outline" size="sm" asChild>
98
+ <Link to={href("coupons/new")}>
99
+ <TicketPercent className="mr-1.5 h-4 w-4" /> Create coupon
100
+ </Link>
101
+ </Button>
102
+ <Button variant="outline" size="sm" asChild>
103
+ <Link to={href("reports")}>
104
+ <BarChart3 className="mr-1.5 h-4 w-4" /> View reports
105
+ </Link>
106
+ </Button>
107
+ </div>
108
+ </div>
109
+
110
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
111
+ <StatCard
112
+ icon={TrendingUp}
113
+ label="Net sales today"
114
+ value={format(asMoneyNumber(s.sales_today))}
115
+ loading={summary.loading}
116
+ />
117
+ <StatCard
118
+ icon={BarChart3}
119
+ label="Net sales this month"
120
+ value={format(asMoneyNumber(s.sales_month))}
121
+ loading={summary.loading}
122
+ />
123
+ <StatCard
124
+ icon={ShoppingCart}
125
+ label="Awaiting processing"
126
+ value={processingCount}
127
+ hint="orders to fulfill"
128
+ loading={summary.loading}
129
+ />
130
+ <StatCard
131
+ icon={AlertTriangle}
132
+ label="Low stock"
133
+ value={s.low_stock_count ?? lowStock.length}
134
+ hint="products at or below threshold"
135
+ loading={summary.loading && stock.loading}
136
+ />
137
+ </div>
138
+
139
+ {/* Orders by status */}
140
+ <div className="flex flex-wrap gap-2">
141
+ {ORDER_STATUSES.map((st) => (
142
+ <Link key={st.value} to={href(`orders?status=${st.value}`)}>
143
+ <Badge variant="outline" className={`${st.color} cursor-pointer`}>
144
+ {st.label}: {ordersByStatus[st.value] || 0}
145
+ </Badge>
146
+ </Link>
147
+ ))}
148
+ </div>
149
+
150
+ <div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
151
+ {/* Latest orders */}
152
+ <Card className="xl:col-span-2">
153
+ <CardHeader className="pb-2">
154
+ <CardTitle className="text-base">Latest orders</CardTitle>
155
+ </CardHeader>
156
+ <CardContent className="p-0">
157
+ {latest.loading ? (
158
+ <div className="space-y-2 p-4">
159
+ {Array.from({ length: 4 }).map((_, i) => (
160
+ <Skeleton key={i} className="h-8 w-full" />
161
+ ))}
162
+ </div>
163
+ ) : (latest.data || []).length === 0 ? (
164
+ <div className="py-10">
165
+ <EmptyState
166
+ icon={ShoppingCart}
167
+ title="No orders yet"
168
+ description="Orders will appear here as soon as they come in."
169
+ />
170
+ </div>
171
+ ) : (
172
+ <Table>
173
+ <TableHeader>
174
+ <TableRow>
175
+ <TableHead>Order</TableHead>
176
+ <TableHead>Date</TableHead>
177
+ <TableHead>Status</TableHead>
178
+ <TableHead>Customer</TableHead>
179
+ <TableHead className="text-right">Total</TableHead>
180
+ </TableRow>
181
+ </TableHeader>
182
+ <TableBody>
183
+ {(latest.data || []).map((o) => (
184
+ <TableRow
185
+ key={o.id}
186
+ className="cursor-pointer"
187
+ onClick={() => navigate(href(`orders/${o.id}`))}
188
+ >
189
+ <TableCell className="font-medium">#{o.order_number || o.id}</TableCell>
190
+ <TableCell>{formatDate(o.created_date)}</TableCell>
191
+ <TableCell>
192
+ <StatusBadge status={o.status} />
193
+ </TableCell>
194
+ <TableCell>{customerName(o)}</TableCell>
195
+ <TableCell className="text-right">{format(o.total || 0)}</TableCell>
196
+ </TableRow>
197
+ ))}
198
+ </TableBody>
199
+ </Table>
200
+ )}
201
+ </CardContent>
202
+ </Card>
203
+
204
+ {/* Low stock */}
205
+ <Card>
206
+ <CardHeader className="pb-2">
207
+ <CardTitle className="text-base">Low on stock</CardTitle>
208
+ </CardHeader>
209
+ <CardContent className="p-0">
210
+ {stock.loading ? (
211
+ <div className="space-y-2 p-4">
212
+ {Array.from({ length: 3 }).map((_, i) => (
213
+ <Skeleton key={i} className="h-8 w-full" />
214
+ ))}
215
+ </div>
216
+ ) : lowStock.length === 0 ? (
217
+ <div className="py-10">
218
+ <EmptyState icon={Plus} title="All stocked up" description="No products are low on stock." />
219
+ </div>
220
+ ) : (
221
+ <ul className="divide-y">
222
+ {lowStock.map((p) => (
223
+ <li key={p.id}>
224
+ <Link
225
+ to={href(`products/${p.product_id || p.id}`)}
226
+ className="flex items-center justify-between px-4 py-2.5 hover:bg-muted/50"
227
+ >
228
+ <span className="min-w-0">
229
+ <span className="block truncate text-sm font-medium">{p.name}</span>
230
+ {p.sku && <span className="text-xs text-muted-foreground">SKU: {p.sku}</span>}
231
+ </span>
232
+ <Badge variant="outline" className="ml-2 shrink-0 bg-amber-100 text-amber-800 border-amber-200">
233
+ {p.stock_quantity ?? 0} left
234
+ </Badge>
235
+ </Link>
236
+ </li>
237
+ ))}
238
+ </ul>
239
+ )}
240
+ </CardContent>
241
+ </Card>
242
+ </div>
243
+ </div>
244
+ );
245
+ }