@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,118 @@
1
+ import React from "react";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { Label } from "@/components/ui/label";
4
+ import { Switch } from "@/components/ui/switch";
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from "@/components/ui/select";
12
+ import { WEIGHT_UNITS, DIMENSION_UNITS } from "../../lib/constants";
13
+ import useGroupForm from "./useGroupForm";
14
+
15
+ const DEFAULTS = {
16
+ weight_unit: "kg",
17
+ dimension_unit: "cm",
18
+ enable_reviews: true,
19
+ review_rating_required: true,
20
+ only_verified_reviews: false,
21
+ auto_approve_reviews: false,
22
+ };
23
+
24
+ function SwitchRow({ label, hint, checked, onChange }) {
25
+ return (
26
+ <div className="flex items-start justify-between gap-4">
27
+ <div>
28
+ <Label>{label}</Label>
29
+ {hint && <p className="text-xs text-muted-foreground">{hint}</p>}
30
+ </div>
31
+ <Switch checked={!!checked} onCheckedChange={onChange} />
32
+ </div>
33
+ );
34
+ }
35
+
36
+ export default function ProductsSettings() {
37
+ const { form, setField, SaveButton } = useGroupForm("products", DEFAULTS);
38
+
39
+ return (
40
+ <div className="max-w-3xl space-y-4">
41
+ <Card>
42
+ <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
43
+ <div className="space-y-1.5">
44
+ <CardTitle>Measurements</CardTitle>
45
+ <CardDescription>Units used for product weight and dimensions.</CardDescription>
46
+ </div>
47
+ <SaveButton />
48
+ </CardHeader>
49
+ <CardContent className="grid gap-4 sm:grid-cols-2">
50
+ <div className="space-y-1.5">
51
+ <Label>Weight unit</Label>
52
+ <Select value={form.weight_unit} onValueChange={(v) => setField("weight_unit", v)}>
53
+ <SelectTrigger>
54
+ <SelectValue />
55
+ </SelectTrigger>
56
+ <SelectContent>
57
+ {WEIGHT_UNITS.map((u) => (
58
+ <SelectItem key={u} value={u}>
59
+ {u}
60
+ </SelectItem>
61
+ ))}
62
+ </SelectContent>
63
+ </Select>
64
+ </div>
65
+ <div className="space-y-1.5">
66
+ <Label>Dimensions unit</Label>
67
+ <Select value={form.dimension_unit} onValueChange={(v) => setField("dimension_unit", v)}>
68
+ <SelectTrigger>
69
+ <SelectValue />
70
+ </SelectTrigger>
71
+ <SelectContent>
72
+ {DIMENSION_UNITS.map((u) => (
73
+ <SelectItem key={u} value={u}>
74
+ {u}
75
+ </SelectItem>
76
+ ))}
77
+ </SelectContent>
78
+ </Select>
79
+ </div>
80
+ </CardContent>
81
+ </Card>
82
+
83
+ <Card>
84
+ <CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
85
+ <div className="space-y-1.5">
86
+ <CardTitle>Reviews</CardTitle>
87
+ </div>
88
+ <SaveButton />
89
+ </CardHeader>
90
+ <CardContent className="space-y-4">
91
+ <SwitchRow
92
+ label="Enable product reviews"
93
+ checked={form.enable_reviews}
94
+ onChange={(v) => setField("enable_reviews", v)}
95
+ />
96
+ <SwitchRow
97
+ label="Star rating required"
98
+ hint="Reviews submitted without a rating are rejected."
99
+ checked={form.review_rating_required}
100
+ onChange={(v) => setField("review_rating_required", v)}
101
+ />
102
+ <SwitchRow
103
+ label="Reviews can only be left by verified owners"
104
+ hint="Requires a completed order containing the product for the reviewer's email."
105
+ checked={form.only_verified_reviews}
106
+ onChange={(v) => setField("only_verified_reviews", v)}
107
+ />
108
+ <SwitchRow
109
+ label="Auto-approve reviews"
110
+ hint="When off, new reviews are held for moderation."
111
+ checked={form.auto_approve_reviews}
112
+ onChange={(v) => setField("auto_approve_reviews", v)}
113
+ />
114
+ </CardContent>
115
+ </Card>
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,53 @@
1
+ import React from "react";
2
+ import { Outlet, useLocation, useNavigate } from "react-router-dom";
3
+ import { useSettings } from "../../context/SettingsContext";
4
+ import { useAdminHref } from "../../context/BasePathContext";
5
+ import PageHeader from "../../components/PageHeader";
6
+
7
+ const TABS = [
8
+ { seg: "general", label: "General" },
9
+ { seg: "products", label: "Products" },
10
+ { seg: "inventory", label: "Inventory" },
11
+ { seg: "tax", label: "Tax" },
12
+ { seg: "shipping", label: "Shipping" },
13
+ { seg: "payments", label: "Payments" },
14
+ { seg: "emails", label: "Emails" },
15
+ ];
16
+
17
+ /** Layout route for /settings/*: horizontal group tabs + nested page via <Outlet/>. */
18
+ export default function SettingsLayout() {
19
+ const { get } = useSettings();
20
+ const href = useAdminHref();
21
+ const navigate = useNavigate();
22
+ const location = useLocation();
23
+
24
+ // Current group segment: ".../settings/<seg>[/...]" (zone editor keeps "shipping" active).
25
+ const afterSettings = location.pathname.split("/settings/")[1] || "general";
26
+ const current = afterSettings.split("/")[0] || "general";
27
+
28
+ const enableTaxes = get("general", "enable_taxes", true);
29
+ const tabs = TABS.filter((t) => t.seg !== "tax" || enableTaxes);
30
+
31
+ return (
32
+ <div className="space-y-4">
33
+ <PageHeader title="Settings" description="Configure how your store works." />
34
+ <div className="flex flex-wrap gap-1 border-b">
35
+ {tabs.map((t) => (
36
+ <button
37
+ key={t.seg}
38
+ type="button"
39
+ onClick={() => navigate(href(`settings/${t.seg}`))}
40
+ className={`-mb-px border-b-2 px-3 py-2 text-sm font-medium transition-colors ${
41
+ current === t.seg
42
+ ? "border-primary text-foreground"
43
+ : "border-transparent text-muted-foreground hover:text-foreground"
44
+ }`}
45
+ >
46
+ {t.label}
47
+ </button>
48
+ ))}
49
+ </div>
50
+ <Outlet />
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,304 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import { Textarea } from "@/components/ui/textarea";
9
+ import { Badge } from "@/components/ui/badge";
10
+ import { Loader2, Plus, Truck } from "lucide-react";
11
+ import { toast } from "sonner";
12
+ import DataTable from "../../components/DataTable";
13
+ import ConfirmDialog from "../../components/ConfirmDialog";
14
+ import { base44 } from "../../lib/api";
15
+ import useAsync from "../../hooks/useAsync";
16
+ import { useAdminHref } from "../../context/BasePathContext";
17
+ import { slugify } from "../../lib/product-utils";
18
+ import { countryName } from "../../lib/geo-data";
19
+ import { CONTINENTS } from "../../lib/geo-data";
20
+
21
+ /** Human summary of a zone's embedded locations array. */
22
+ function regionsSummary(locations) {
23
+ const locs = locations || [];
24
+ if (locs.length === 0) return "Everywhere not covered by other zones";
25
+ const names = [];
26
+ let postcodes = 0;
27
+ for (const l of locs) {
28
+ if (l.type === "postcode") postcodes += 1;
29
+ else if (l.type === "continent")
30
+ names.push(CONTINENTS.find((c) => c.code === l.code)?.name || l.code);
31
+ else if (l.type === "country") names.push(countryName(l.code));
32
+ else if (l.type === "state") names.push(l.code);
33
+ }
34
+ const parts = [];
35
+ if (names.length) parts.push(names.slice(0, 4).join(", ") + (names.length > 4 ? `, +${names.length - 4}` : ""));
36
+ if (postcodes) parts.push(`${postcodes} postcode${postcodes > 1 ? "s" : ""}`);
37
+ return parts.join(" · ") || "—";
38
+ }
39
+
40
+ function ZonesTab() {
41
+ const navigate = useNavigate();
42
+ const href = useAdminHref();
43
+ const zones = useAsync(() => base44.entities["commerce.ShippingZone"].list("order", 200), []);
44
+ const methods = useAsync(() => base44.entities["commerce.ShippingZoneMethod"].list(undefined, 500), []);
45
+ const [creating, setCreating] = useState(false);
46
+ const [toDelete, setToDelete] = useState(null);
47
+ const [deleting, setDeleting] = useState(false);
48
+
49
+ const methodsByZone = useMemo(() => {
50
+ const map = {};
51
+ (methods.data || []).forEach((m) => {
52
+ (map[m.zone_id] = map[m.zone_id] || []).push(m);
53
+ });
54
+ return map;
55
+ }, [methods.data]);
56
+
57
+ // Sort by order, but push empty-location (rest-of-world) zones to the end.
58
+ const rows = useMemo(() => {
59
+ return [...(zones.data || [])].sort((a, b) => {
60
+ const ae = (a.locations || []).length === 0;
61
+ const be = (b.locations || []).length === 0;
62
+ if (ae !== be) return ae ? 1 : -1;
63
+ return (a.order ?? 0) - (b.order ?? 0);
64
+ });
65
+ }, [zones.data]);
66
+
67
+ const addZone = async () => {
68
+ setCreating(true);
69
+ try {
70
+ const maxOrder = (zones.data || [])
71
+ .filter((z) => (z.locations || []).length > 0)
72
+ .reduce((m, z) => Math.max(m, z.order ?? 0), 0);
73
+ const zone = await base44.entities["commerce.ShippingZone"].create({
74
+ name: "New zone",
75
+ order: maxOrder + 1,
76
+ locations: [],
77
+ });
78
+ navigate(href(`settings/shipping/zones/${zone.id}`));
79
+ } catch (err) {
80
+ toast.error(err.message || "Failed to create zone");
81
+ setCreating(false);
82
+ }
83
+ };
84
+
85
+ const removeZone = async () => {
86
+ setDeleting(true);
87
+ try {
88
+ // Methods first: a ShippingZoneMethod outlives its zone otherwise, and
89
+ // nothing else reaps them.
90
+ for (const m of methodsByZone[toDelete.id] || []) {
91
+ await base44.entities["commerce.ShippingZoneMethod"].delete(m.id);
92
+ }
93
+ await base44.entities["commerce.ShippingZone"].delete(toDelete.id);
94
+ toast.success("Shipping zone deleted");
95
+ setToDelete(null);
96
+ zones.refetch();
97
+ methods.refetch();
98
+ } catch (err) {
99
+ toast.error(err.message || "Failed to delete zone");
100
+ } finally {
101
+ setDeleting(false);
102
+ }
103
+ };
104
+
105
+ const columns = [
106
+ {
107
+ key: "name",
108
+ label: "Zone name",
109
+ render: (z) => <span className="font-medium">{z.name}</span>,
110
+ },
111
+ { key: "regions", label: "Region(s)", render: (z) => regionsSummary(z.locations) },
112
+ {
113
+ key: "methods",
114
+ label: "Shipping method(s)",
115
+ render: (z) => {
116
+ const ms = methodsByZone[z.id] || [];
117
+ if (!ms.length) return <span className="text-muted-foreground">No methods</span>;
118
+ return (
119
+ <div className="flex flex-wrap gap-1">
120
+ {ms.map((m) => (
121
+ <Badge key={m.id} variant={m.enabled ? "secondary" : "outline"} className="font-normal">
122
+ {m.title || m.method_id}
123
+ </Badge>
124
+ ))}
125
+ </div>
126
+ );
127
+ },
128
+ },
129
+ ];
130
+
131
+ return (
132
+ <div className="space-y-4">
133
+ <div className="flex justify-end">
134
+ <Button onClick={addZone} disabled={creating}>
135
+ {creating ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Plus className="mr-2 h-4 w-4" />}
136
+ Add shipping zone
137
+ </Button>
138
+ </div>
139
+ <DataTable
140
+ columns={columns}
141
+ rows={rows}
142
+ loading={zones.loading || methods.loading}
143
+ onRowClick={(z) => navigate(href(`settings/shipping/zones/${z.id}`))}
144
+ rowActions={(z) => [
145
+ { label: "Edit", onClick: () => navigate(href(`settings/shipping/zones/${z.id}`)) },
146
+ { label: "Delete", destructive: true, onClick: () => setToDelete(z) },
147
+ ]}
148
+ empty={{ icon: Truck, title: "No shipping zones", description: "Create a zone to offer shipping methods to your customers." }}
149
+ />
150
+
151
+ <ConfirmDialog
152
+ open={!!toDelete}
153
+ onOpenChange={(o) => !o && setToDelete(null)}
154
+ title={`Delete ${toDelete?.name || "shipping zone"}?`}
155
+ description={
156
+ (toDelete?.locations || []).length === 0
157
+ ? "This is the catch-all zone. Customers outside your other zones will have no shipping methods until you add another catch-all."
158
+ : "The zone and its shipping methods are deleted. Existing orders keep the shipping they were charged."
159
+ }
160
+ confirmLabel="Delete"
161
+ loading={deleting}
162
+ onConfirm={removeZone}
163
+ />
164
+ </div>
165
+ );
166
+ }
167
+
168
+ function ClassesTab() {
169
+ const classes = useAsync(() => base44.entities["commerce.ShippingClass"].list("name", 200), []);
170
+ const blank = { id: null, name: "", slug: "", description: "" };
171
+ const [form, setForm] = useState(blank);
172
+ const [slugTouched, setSlugTouched] = useState(false);
173
+ const [saving, setSaving] = useState(false);
174
+ const [toDelete, setToDelete] = useState(null);
175
+ const [deleting, setDeleting] = useState(false);
176
+
177
+ const setName = (name) =>
178
+ setForm((f) => ({ ...f, name, slug: slugTouched ? f.slug : slugify(name) }));
179
+
180
+ const reset = () => {
181
+ setForm(blank);
182
+ setSlugTouched(false);
183
+ };
184
+
185
+ const save = async () => {
186
+ if (!form.name.trim()) {
187
+ toast.error("Name is required");
188
+ return;
189
+ }
190
+ setSaving(true);
191
+ try {
192
+ const payload = { name: form.name, slug: form.slug || slugify(form.name), description: form.description };
193
+ if (form.id) await base44.entities["commerce.ShippingClass"].update(form.id, payload);
194
+ else await base44.entities["commerce.ShippingClass"].create(payload);
195
+ toast.success("Shipping class saved");
196
+ reset();
197
+ classes.refetch();
198
+ } catch (err) {
199
+ toast.error(err.message || "Failed to save");
200
+ } finally {
201
+ setSaving(false);
202
+ }
203
+ };
204
+
205
+ const remove = async () => {
206
+ setDeleting(true);
207
+ try {
208
+ await base44.entities["commerce.ShippingClass"].delete(toDelete.id);
209
+ toast.success("Shipping class deleted");
210
+ if (form.id === toDelete.id) reset();
211
+ setToDelete(null);
212
+ classes.refetch();
213
+ } catch (err) {
214
+ toast.error(err.message || "Failed to delete");
215
+ } finally {
216
+ setDeleting(false);
217
+ }
218
+ };
219
+
220
+ const columns = [
221
+ { key: "name", label: "Name", render: (c) => <span className="font-medium">{c.name}</span> },
222
+ { key: "slug", label: "Slug", render: (c) => <code className="text-xs">{c.slug}</code> },
223
+ { key: "description", label: "Description", render: (c) => c.description || "—" },
224
+ ];
225
+
226
+ return (
227
+ <div className="grid gap-4 lg:grid-cols-[320px_1fr]">
228
+ <Card className="h-fit">
229
+ <CardHeader>
230
+ <CardTitle>{form.id ? "Edit shipping class" : "Add shipping class"}</CardTitle>
231
+ </CardHeader>
232
+ <CardContent className="space-y-3">
233
+ <div className="space-y-1.5">
234
+ <Label>Name</Label>
235
+ <Input value={form.name} onChange={(e) => setName(e.target.value)} />
236
+ </div>
237
+ <div className="space-y-1.5">
238
+ <Label>Slug</Label>
239
+ <Input
240
+ value={form.slug}
241
+ onChange={(e) => {
242
+ setSlugTouched(true);
243
+ setForm((f) => ({ ...f, slug: e.target.value }));
244
+ }}
245
+ />
246
+ </div>
247
+ <div className="space-y-1.5">
248
+ <Label>Description</Label>
249
+ <Textarea rows={3} value={form.description} onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))} />
250
+ </div>
251
+ <div className="flex gap-2">
252
+ <Button onClick={save} disabled={saving}>
253
+ {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
254
+ {form.id ? "Update" : "Add"}
255
+ </Button>
256
+ {form.id && (
257
+ <Button variant="ghost" onClick={reset}>
258
+ Cancel
259
+ </Button>
260
+ )}
261
+ </div>
262
+ </CardContent>
263
+ </Card>
264
+
265
+ <DataTable
266
+ columns={columns}
267
+ rows={classes.data || []}
268
+ loading={classes.loading}
269
+ rowActions={(c) => [
270
+ { label: "Edit", onClick: () => setForm({ id: c.id, name: c.name, slug: c.slug || "", description: c.description || "" }) },
271
+ { label: "Delete", destructive: true, onClick: () => setToDelete(c) },
272
+ ]}
273
+ empty={{ title: "No shipping classes", description: "Shipping classes group products with similar shipping costs." }}
274
+ />
275
+
276
+ <ConfirmDialog
277
+ open={!!toDelete}
278
+ onOpenChange={(o) => !o && setToDelete(null)}
279
+ title="Delete shipping class?"
280
+ description="Products in this class will not be deleted, but will lose their class assignment."
281
+ confirmLabel="Delete"
282
+ loading={deleting}
283
+ onConfirm={remove}
284
+ />
285
+ </div>
286
+ );
287
+ }
288
+
289
+ export default function ShippingSettings() {
290
+ return (
291
+ <Tabs defaultValue="zones" className="space-y-4">
292
+ <TabsList>
293
+ <TabsTrigger value="zones">Shipping zones</TabsTrigger>
294
+ <TabsTrigger value="classes">Shipping classes</TabsTrigger>
295
+ </TabsList>
296
+ <TabsContent value="zones">
297
+ <ZonesTab />
298
+ </TabsContent>
299
+ <TabsContent value="classes">
300
+ <ClassesTab />
301
+ </TabsContent>
302
+ </Tabs>
303
+ );
304
+ }