@base44/app-plugin-commerce 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/base44/agents/commerce/StoreAdmin.jsonc +2 -2
- package/base44/entities/commerce.Cart.jsonc +1 -1
- package/base44/entities/commerce.Coupon.jsonc +5 -0
- package/base44/entities/commerce.Order.jsonc +6 -7
- package/base44/entities/commerce.OrderRefund.jsonc +1 -1
- package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
- package/base44/entities/commerce.Product.jsonc +6 -16
- package/base44/entities/{commerce.ProductTag.jsonc → commerce.ProductRibbon.jsonc} +2 -2
- package/base44/entities/commerce.ProductVariation.jsonc +1 -9
- package/base44/entities/commerce.ShippingTaxLocation.jsonc +85 -0
- package/base44/entities/commerce.Webhook.jsonc +1 -1
- package/base44/functions/commerce/admin-orders/helpers.ts +7 -13
- package/base44/functions/commerce/admin-products/entry.ts +11 -17
- package/base44/functions/commerce/admin-refunds/entry.ts +10 -9
- package/base44/functions/commerce/admin-reports/entry.ts +3 -3
- package/base44/functions/commerce/admin-tools/entry.ts +9 -36
- package/base44/functions/commerce/payment-webhook/entry.ts +50 -89
- package/base44/functions/commerce/payments/entry.ts +46 -42
- package/base44/functions/commerce/seed-store/defaults.ts +35 -42
- package/base44/functions/commerce/seed-store/entry.ts +68 -46
- package/base44/functions/commerce/seed-store/sample-data.ts +2 -15
- package/base44/functions/commerce/seed-store/seed-catalog.ts +105 -55
- package/base44/functions/commerce/storefront-cart/cart-pricing.ts +6 -11
- package/base44/functions/commerce/storefront-cart/entry.ts +36 -2
- package/base44/functions/commerce/storefront-catalog/entry.ts +55 -72
- package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +6 -11
- package/base44/functions/commerce/storefront-checkout/entry.ts +43 -56
- package/base44/shared/commerce/card-payment.ts +80 -0
- package/base44/shared/commerce/coupons.ts +16 -10
- package/base44/shared/commerce/emails.ts +30 -18
- package/base44/shared/commerce/money.ts +11 -24
- package/base44/shared/commerce/payments.ts +55 -286
- package/base44/shared/commerce/scan.ts +1 -1
- package/base44/shared/commerce/sequence.ts +1 -1
- package/base44/shared/commerce/settings.ts +4 -9
- package/base44/shared/commerce/shipping.ts +65 -133
- package/base44/shared/commerce/tax.ts +48 -96
- package/base44/shared/commerce/totals.ts +77 -91
- package/package.json +1 -1
- package/scripts/install.js +28 -7
- package/skills/commerce/SKILL.md +14 -14
- package/skills/commerce/docs/api-admin.md +23 -26
- package/skills/commerce/docs/api-storefront.md +67 -61
- package/skills/commerce/installation-guidelines.md +8 -8
- package/skills/commerce/post-installation.md +60 -39
- package/skills/commerce/references/admin-product-form.md +15 -12
- package/skills/commerce/references/emails.md +2 -2
- package/skills/commerce/references/guest-access-security.md +2 -2
- package/skills/commerce/references/online-payments.md +24 -166
- package/skills/commerce/references/product-render.md +18 -18
- package/skills/commerce/references/reviews.md +14 -8
- package/skills/commerce/references/storefront-product-page.md +1 -1
- package/src/commerce/admin/README.md +4 -5
- package/src/commerce/admin/bot/Markdown.jsx +1 -1
- package/src/commerce/admin/hooks/useMoney.js +13 -22
- package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
- package/src/commerce/admin/lib/constants.js +2 -29
- package/src/commerce/admin/lib/order-utils.js +1 -1
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +131 -140
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +14 -7
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +3 -3
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +17 -28
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +5 -11
- package/src/commerce/admin/pages/products/Categories.jsx +147 -177
- package/src/commerce/admin/pages/products/ProductEditor.jsx +23 -18
- package/src/commerce/admin/pages/products/Reviews.jsx +37 -1
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -47
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +13 -34
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +29 -29
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +14 -44
- package/src/commerce/admin/pages/reports/Reports.jsx +2 -2
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +101 -68
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +40 -38
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -41
- package/src/commerce/admin/pages/settings/LocationEditor.jsx +377 -0
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +137 -119
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -4
- package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +191 -0
- package/src/commerce/admin/routes.jsx +6 -12
- package/src/commerce/utils/index.js +2 -2
- package/src/commerce/utils/shipping-promos.js +45 -49
- package/src/commerce/utils/variants.js +1 -1
- package/base44/entities/commerce.ShippingClass.jsonc +0 -30
- package/base44/entities/commerce.ShippingZone.jsonc +0 -41
- package/base44/entities/commerce.ShippingZoneMethod.jsonc +0 -84
- package/base44/entities/commerce.TaxClass.jsonc +0 -23
- package/base44/entities/commerce.TaxRate.jsonc +0 -68
- package/base44/shared/commerce/stripe.ts +0 -463
- package/src/commerce/admin/hooks/usePaymentProvider.js +0 -27
- package/src/commerce/admin/pages/settings/ProductsSettings.jsx +0 -118
- package/src/commerce/admin/pages/settings/ShippingSettings.jsx +0 -304
- package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +0 -514
- package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +0 -231
- package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -280
|
@@ -5,7 +5,6 @@ import { Input } from "@/components/ui/input";
|
|
|
5
5
|
import { Label } from "@/components/ui/label";
|
|
6
6
|
import { Textarea } from "@/components/ui/textarea";
|
|
7
7
|
import { Switch } from "@/components/ui/switch";
|
|
8
|
-
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
9
8
|
import {
|
|
10
9
|
Dialog,
|
|
11
10
|
DialogContent,
|
|
@@ -13,20 +12,26 @@ import {
|
|
|
13
12
|
DialogHeader,
|
|
14
13
|
DialogTitle,
|
|
15
14
|
} from "@/components/ui/dialog";
|
|
16
|
-
import { ArrowDown, ArrowUp,
|
|
15
|
+
import { ArrowDown, ArrowUp, Loader2, Plus, Settings2, Trash2 } from "lucide-react";
|
|
17
16
|
import { toast } from "sonner";
|
|
18
17
|
import { base44 } from "../../lib/api";
|
|
19
18
|
import useAsync from "../../hooks/useAsync";
|
|
20
|
-
import usePaymentProvider from "../../hooks/usePaymentProvider";
|
|
21
19
|
|
|
22
20
|
const emptyBank = () => ({ account_name: "", account_number: "", bank_name: "", sort_code: "", iban: "", bic: "" });
|
|
23
21
|
|
|
22
|
+
/** The two seeded options; everything else is operator-added and deletable. */
|
|
23
|
+
const BUILT_IN_SLUGS = new Set(["offline", "card"]);
|
|
24
|
+
|
|
25
|
+
function slugify(title, taken) {
|
|
26
|
+
const base = String(title || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "payment-method";
|
|
27
|
+
let slug = base;
|
|
28
|
+
for (let n = 2; taken.has(slug); n += 1) slug = `${base}-${n}`;
|
|
29
|
+
return slug;
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
export default function PaymentsSettings() {
|
|
25
33
|
const gateways = useAsync(() => base44.entities["commerce.PaymentGateway"].list("order", 100), []);
|
|
26
|
-
|
|
27
|
-
// payment provider, not a constant — the notice below follows it.
|
|
28
|
-
const provider = usePaymentProvider();
|
|
29
|
-
const [dialog, setDialog] = useState(null); // { gateway (local copy) }
|
|
34
|
+
const [dialog, setDialog] = useState(null); // { gateway (local copy), isNew }
|
|
30
35
|
const [saving, setSaving] = useState(false);
|
|
31
36
|
const [busy, setBusy] = useState(false);
|
|
32
37
|
|
|
@@ -60,16 +65,54 @@ export default function PaymentsSettings() {
|
|
|
60
65
|
}
|
|
61
66
|
};
|
|
62
67
|
|
|
68
|
+
const remove = async (g) => {
|
|
69
|
+
if (!window.confirm(`Delete the "${g.title || g.slug}" payment option?`)) return;
|
|
70
|
+
setBusy(true);
|
|
71
|
+
try {
|
|
72
|
+
await base44.entities["commerce.PaymentGateway"].delete(g.id);
|
|
73
|
+
toast.success("Payment method deleted");
|
|
74
|
+
gateways.refetch();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
toast.error(err.message || "Failed to delete");
|
|
77
|
+
} finally {
|
|
78
|
+
setBusy(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const openNew = () =>
|
|
83
|
+
setDialog({
|
|
84
|
+
isNew: true,
|
|
85
|
+
gateway: { title: "", description: "", settings: { account_details: [] } },
|
|
86
|
+
});
|
|
87
|
+
|
|
63
88
|
const saveGateway = async () => {
|
|
89
|
+
const g = dialog.gateway;
|
|
90
|
+
if (!String(g.title || "").trim()) {
|
|
91
|
+
toast.error("Give the payment method a title");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
64
94
|
setSaving(true);
|
|
65
95
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
if (dialog.isNew) {
|
|
97
|
+
await base44.entities["commerce.PaymentGateway"].create({
|
|
98
|
+
slug: slugify(g.title, new Set(rows.map((r) => r.slug))),
|
|
99
|
+
title: g.title,
|
|
100
|
+
description: g.description || "",
|
|
101
|
+
enabled: true,
|
|
102
|
+
order: rows.length,
|
|
103
|
+
method_title: g.title,
|
|
104
|
+
method_description: g.description || "",
|
|
105
|
+
settings: g.settings || {},
|
|
106
|
+
});
|
|
107
|
+
toast.success("Payment method added");
|
|
108
|
+
} else {
|
|
109
|
+
await base44.entities["commerce.PaymentGateway"].update(g.id, {
|
|
110
|
+
title: g.title,
|
|
111
|
+
description: g.description,
|
|
112
|
+
settings: g.settings || {},
|
|
113
|
+
});
|
|
114
|
+
toast.success("Payment method saved");
|
|
115
|
+
}
|
|
73
116
|
setDialog(null);
|
|
74
117
|
gateways.refetch();
|
|
75
118
|
} catch (err) {
|
|
@@ -90,118 +133,86 @@ export default function PaymentsSettings() {
|
|
|
90
133
|
return (
|
|
91
134
|
<div className="max-w-3xl">
|
|
92
135
|
<Card>
|
|
93
|
-
<CardHeader className="pb-3">
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
136
|
+
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0 pb-3">
|
|
137
|
+
<div className="space-y-1.5">
|
|
138
|
+
<CardTitle>Checkout options</CardTitle>
|
|
139
|
+
<CardDescription>
|
|
140
|
+
Which payment options customers can pick at checkout, in this order, and the wording
|
|
141
|
+
they see. Every option except Credit card settles outside the store: the order goes
|
|
142
|
+
on-hold with the option's description as instructions, and you move it on once paid.
|
|
143
|
+
</CardDescription>
|
|
144
|
+
</div>
|
|
145
|
+
<Button size="sm" onClick={openNew}>
|
|
146
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
147
|
+
Add payment method
|
|
148
|
+
</Button>
|
|
100
149
|
</CardHeader>
|
|
101
150
|
<CardContent className="divide-y border-t p-0">
|
|
102
|
-
{rows.map((g, i) =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
</Button>
|
|
126
|
-
</div>
|
|
151
|
+
{rows.map((g, i) => (
|
|
152
|
+
<div key={g.id} className="space-y-2 px-6 py-4">
|
|
153
|
+
<div className="flex items-start gap-4">
|
|
154
|
+
<div className="flex flex-col pt-0.5">
|
|
155
|
+
<Button
|
|
156
|
+
variant="ghost"
|
|
157
|
+
size="icon"
|
|
158
|
+
className="h-6 w-6"
|
|
159
|
+
disabled={i === 0 || busy}
|
|
160
|
+
onClick={() => move(i, -1)}
|
|
161
|
+
>
|
|
162
|
+
<ArrowUp className="h-3.5 w-3.5" />
|
|
163
|
+
</Button>
|
|
164
|
+
<Button
|
|
165
|
+
variant="ghost"
|
|
166
|
+
size="icon"
|
|
167
|
+
className="h-6 w-6"
|
|
168
|
+
disabled={i === rows.length - 1 || busy}
|
|
169
|
+
onClick={() => move(i, 1)}
|
|
170
|
+
>
|
|
171
|
+
<ArrowDown className="h-3.5 w-3.5" />
|
|
172
|
+
</Button>
|
|
173
|
+
</div>
|
|
127
174
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
</div>
|
|
175
|
+
<div className="min-w-0 flex-1">
|
|
176
|
+
<div className="font-medium">{g.method_title || g.title || g.slug}</div>
|
|
177
|
+
<div className="text-sm text-muted-foreground">
|
|
178
|
+
{g.method_description || g.description}
|
|
133
179
|
</div>
|
|
180
|
+
</div>
|
|
134
181
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
{/* Every option's customer-facing wording is editable, the card
|
|
147
|
-
one included — and a button on every row keeps the toggles
|
|
148
|
-
in one column. */}
|
|
182
|
+
<div className="flex shrink-0 items-center gap-2 pt-0.5">
|
|
183
|
+
<Switch checked={!!g.enabled} disabled={busy} onCheckedChange={(v) => toggle(g, v)} />
|
|
184
|
+
<Button
|
|
185
|
+
variant="outline"
|
|
186
|
+
size="sm"
|
|
187
|
+
onClick={() => setDialog({ gateway: { ...g, settings: { ...(g.settings || {}) } } })}
|
|
188
|
+
>
|
|
189
|
+
<Settings2 className="mr-2 h-4 w-4" />
|
|
190
|
+
Edit
|
|
191
|
+
</Button>
|
|
192
|
+
{!BUILT_IN_SLUGS.has(g.slug) && (
|
|
149
193
|
<Button
|
|
150
|
-
variant="
|
|
151
|
-
size="
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
settings: {
|
|
157
|
-
...(g.settings || {}),
|
|
158
|
-
account_details:
|
|
159
|
-
g.slug === "offline"
|
|
160
|
-
? g.settings?.account_details || []
|
|
161
|
-
: g.settings?.account_details,
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
})
|
|
165
|
-
}
|
|
194
|
+
variant="ghost"
|
|
195
|
+
size="icon"
|
|
196
|
+
className="h-8 w-8 text-muted-foreground"
|
|
197
|
+
disabled={busy}
|
|
198
|
+
title="Delete"
|
|
199
|
+
onClick={() => remove(g)}
|
|
166
200
|
>
|
|
167
|
-
<
|
|
168
|
-
Labels
|
|
201
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
169
202
|
</Button>
|
|
170
|
-
|
|
203
|
+
)}
|
|
171
204
|
</div>
|
|
172
|
-
|
|
173
|
-
{/* Full width, below the row: inside it the tall notice drags the
|
|
174
|
-
arrows and the switch down to the vertical middle. */}
|
|
175
|
-
{isOnline && !provider.loading && (
|
|
176
|
-
<Alert>
|
|
177
|
-
<Info className="h-4 w-4" />
|
|
178
|
-
{provider.connected ? (
|
|
179
|
-
<>
|
|
180
|
-
<AlertTitle>Payment provider connected</AlertTitle>
|
|
181
|
-
<AlertDescription>
|
|
182
|
-
Card payments are live: customers see this option at checkout, orders are
|
|
183
|
-
confirmed automatically once paid, and refunds can be sent back through the
|
|
184
|
-
provider{provider.providerLabel ? ` (${provider.providerLabel})` : ""}.
|
|
185
|
-
</AlertDescription>
|
|
186
|
-
</>
|
|
187
|
-
) : (
|
|
188
|
-
<>
|
|
189
|
-
<AlertTitle>No payment provider connected</AlertTitle>
|
|
190
|
-
<AlertDescription>
|
|
191
|
-
Card payments are ready to go but need a provider: set{" "}
|
|
192
|
-
{provider.providerLabel || "a payment provider"} up in this app's
|
|
193
|
-
integrations, and this option starts appearing at checkout — no code to
|
|
194
|
-
write. Until then it stays hidden from customers, so nobody can pick it and
|
|
195
|
-
pay nothing. Prefer a different provider? See{" "}
|
|
196
|
-
<code className="text-xs">skills/commerce/references/online-payments.md</code>.
|
|
197
|
-
</AlertDescription>
|
|
198
|
-
</>
|
|
199
|
-
)}
|
|
200
|
-
</Alert>
|
|
201
|
-
)}
|
|
202
205
|
</div>
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
|
|
207
|
+
{/* The "card" option redirects to a provider-hosted payment page.
|
|
208
|
+
Wiring a provider means implementing the two payment files —
|
|
209
|
+
shared/commerce/card-payment.ts + functions/commerce/payment-webhook,
|
|
210
|
+
see .agents/skills/commerce/references/online-payments.md. Until
|
|
211
|
+
then, picking it at checkout answers 503 no_card_payment_provider.
|
|
212
|
+
Deliberately not shown to the store operator — it's developer
|
|
213
|
+
guidance, not store configuration. */}
|
|
214
|
+
</div>
|
|
215
|
+
))}
|
|
205
216
|
</CardContent>
|
|
206
217
|
</Card>
|
|
207
218
|
|
|
@@ -209,7 +220,9 @@ export default function PaymentsSettings() {
|
|
|
209
220
|
<Dialog open onOpenChange={(o) => !o && setDialog(null)}>
|
|
210
221
|
<DialogContent className="max-h-[85vh] overflow-y-auto">
|
|
211
222
|
<DialogHeader>
|
|
212
|
-
<DialogTitle>
|
|
223
|
+
<DialogTitle>
|
|
224
|
+
{dialog.isNew ? "Add payment method" : dialog.gateway.method_title || dialog.gateway.title}
|
|
225
|
+
</DialogTitle>
|
|
213
226
|
</DialogHeader>
|
|
214
227
|
<div className="space-y-4">
|
|
215
228
|
<div className="space-y-1.5">
|
|
@@ -227,9 +240,14 @@ export default function PaymentsSettings() {
|
|
|
227
240
|
value={dialog.gateway.description || ""}
|
|
228
241
|
onChange={(e) => setDialog({ ...dialog, gateway: { ...dialog.gateway, description: e.target.value } })}
|
|
229
242
|
/>
|
|
243
|
+
{dialog.gateway.slug !== "card" && (
|
|
244
|
+
<p className="text-xs text-muted-foreground">
|
|
245
|
+
Also shown as the payment instructions after the order is placed.
|
|
246
|
+
</p>
|
|
247
|
+
)}
|
|
230
248
|
</div>
|
|
231
249
|
|
|
232
|
-
{dialog.gateway.slug
|
|
250
|
+
{dialog.gateway.slug !== "card" && (
|
|
233
251
|
<BankAccountsEditor
|
|
234
252
|
accounts={dialog.gateway.settings?.account_details || []}
|
|
235
253
|
onChange={(account_details) =>
|
|
@@ -242,7 +260,7 @@ export default function PaymentsSettings() {
|
|
|
242
260
|
<Button variant="outline" onClick={() => setDialog(null)}>Cancel</Button>
|
|
243
261
|
<Button onClick={saveGateway} disabled={saving}>
|
|
244
262
|
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
245
|
-
Save
|
|
263
|
+
{dialog.isNew ? "Add" : "Save"}
|
|
246
264
|
</Button>
|
|
247
265
|
</DialogFooter>
|
|
248
266
|
</DialogContent>
|
|
@@ -5,10 +5,8 @@ import PageHeader from "../../components/PageHeader";
|
|
|
5
5
|
|
|
6
6
|
const TABS = [
|
|
7
7
|
{ seg: "general", label: "General" },
|
|
8
|
-
{ seg: "products", label: "Products" },
|
|
9
8
|
{ seg: "inventory", label: "Inventory" },
|
|
10
|
-
{ seg: "tax", label: "Tax" },
|
|
11
|
-
{ seg: "shipping", label: "Shipping" },
|
|
9
|
+
{ seg: "shipping-tax", label: "Shipping & Tax" },
|
|
12
10
|
{ seg: "payments", label: "Payments" },
|
|
13
11
|
{ seg: "emails", label: "Emails" },
|
|
14
12
|
{ seg: "webhooks", label: "Webhooks" },
|
|
@@ -20,7 +18,7 @@ export default function SettingsLayout() {
|
|
|
20
18
|
const navigate = useNavigate();
|
|
21
19
|
const location = useLocation();
|
|
22
20
|
|
|
23
|
-
// Current group segment: ".../settings/<seg>[/...]" (
|
|
21
|
+
// Current group segment: ".../settings/<seg>[/...]" (location editor keeps "shipping-tax" active).
|
|
24
22
|
const afterSettings = location.pathname.split("/settings/")[1] || "general";
|
|
25
23
|
const current = afterSettings.split("/")[0] || "general";
|
|
26
24
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
5
|
+
import { Label } from "@/components/ui/label";
|
|
6
|
+
import { Switch } from "@/components/ui/switch";
|
|
7
|
+
import { Copy, Globe, MapPin, Pencil, Plus, Trash2 } from "lucide-react";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
|
|
10
|
+
import { base44 } from "../../lib/api";
|
|
11
|
+
import useAsync from "../../hooks/useAsync";
|
|
12
|
+
import { useAdminHref } from "../../context/BasePathContext";
|
|
13
|
+
import { useSettings } from "../../context/SettingsContext";
|
|
14
|
+
import DataTable from "../../components/DataTable";
|
|
15
|
+
import ConfirmDialog from "../../components/ConfirmDialog";
|
|
16
|
+
import useMoney from "../../hooks/useMoney";
|
|
17
|
+
import { COUNTRIES, CONTINENTS } from "../../lib/geo-data";
|
|
18
|
+
|
|
19
|
+
function regionLabel(region) {
|
|
20
|
+
if (region.type === "continent") return CONTINENTS.find((c) => c.code === region.code)?.name || region.code;
|
|
21
|
+
if (region.type === "state") return region.code;
|
|
22
|
+
return COUNTRIES.find((c) => c.code === region.code)?.name || region.code;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Shipping & Tax Locations — the one screen for where the store sells and what
|
|
27
|
+
* it charges there: each location carries its shipping rates, tax groups and
|
|
28
|
+
* shipping tax. Rows edit, clone or delete; the editor is its own page.
|
|
29
|
+
*/
|
|
30
|
+
export default function ShippingTaxSettings() {
|
|
31
|
+
const href = useAdminHref();
|
|
32
|
+
const navigate = useNavigate();
|
|
33
|
+
const money = useMoney();
|
|
34
|
+
const settings = useSettings();
|
|
35
|
+
const { data: locations, loading, refetch } = useAsync(
|
|
36
|
+
() => base44.entities["commerce.ShippingTaxLocation"].list("order", 500),
|
|
37
|
+
[]
|
|
38
|
+
);
|
|
39
|
+
const [confirmDelete, setConfirmDelete] = useState(null);
|
|
40
|
+
const [deleting, setDeleting] = useState(false);
|
|
41
|
+
|
|
42
|
+
const shippingEnabled = settings?.get("shipping", "enable_shipping", true) !== false;
|
|
43
|
+
const setShippingEnabled = async (v) => {
|
|
44
|
+
try {
|
|
45
|
+
await settings.saveGroup("shipping", { ...(settings.settings?.shipping || {}), enable_shipping: v });
|
|
46
|
+
toast.success(v ? "Shipping enabled" : "Shipping disabled");
|
|
47
|
+
} catch (e) {
|
|
48
|
+
toast.error(e.message || "Failed to save");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const clone = async (row) => {
|
|
53
|
+
try {
|
|
54
|
+
const { id: _id, created_date: _cd, updated_date: _ud, created_by: _cb, ...fields } = row;
|
|
55
|
+
await base44.entities["commerce.ShippingTaxLocation"].create({
|
|
56
|
+
...fields,
|
|
57
|
+
name: `${row.name} (copy)`,
|
|
58
|
+
// fresh rate ids — carts/orders reference rates by id, so a clone must not share them
|
|
59
|
+
shipping_rates: (row.shipping_rates || []).map((r) => ({ ...r, id: crypto.randomUUID() })),
|
|
60
|
+
});
|
|
61
|
+
toast.success("Location cloned");
|
|
62
|
+
refetch();
|
|
63
|
+
} catch (e) {
|
|
64
|
+
toast.error(e.message || "Failed to clone location");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const doDelete = async () => {
|
|
69
|
+
setDeleting(true);
|
|
70
|
+
try {
|
|
71
|
+
await base44.entities["commerce.ShippingTaxLocation"].delete(confirmDelete.id);
|
|
72
|
+
toast.success("Location deleted");
|
|
73
|
+
setConfirmDelete(null);
|
|
74
|
+
refetch();
|
|
75
|
+
} catch (e) {
|
|
76
|
+
toast.error(e.message || "Failed to delete location");
|
|
77
|
+
} finally {
|
|
78
|
+
setDeleting(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const columns = [
|
|
83
|
+
{
|
|
84
|
+
key: "name",
|
|
85
|
+
label: "Location",
|
|
86
|
+
render: (row) => (
|
|
87
|
+
<div className="flex items-center gap-2">
|
|
88
|
+
{(row.regions || []).length ? (
|
|
89
|
+
<MapPin className="h-4 w-4 text-muted-foreground" />
|
|
90
|
+
) : (
|
|
91
|
+
<Globe className="h-4 w-4 text-muted-foreground" />
|
|
92
|
+
)}
|
|
93
|
+
<span className="font-medium">{row.name}</span>
|
|
94
|
+
</div>
|
|
95
|
+
),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: "regions",
|
|
99
|
+
label: "Regions",
|
|
100
|
+
className: "hidden md:table-cell text-muted-foreground",
|
|
101
|
+
render: (row) => {
|
|
102
|
+
const regions = row.regions || [];
|
|
103
|
+
if (!regions.length) return "Everywhere else (fallback)";
|
|
104
|
+
const names = regions.slice(0, 3).map(regionLabel).join(", ");
|
|
105
|
+
return regions.length > 3 ? `${names} +${regions.length - 3}` : names;
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: "shipping",
|
|
110
|
+
label: "Shipping rates",
|
|
111
|
+
className: "hidden lg:table-cell text-muted-foreground",
|
|
112
|
+
render: (row) => {
|
|
113
|
+
const rates = row.shipping_rates || [];
|
|
114
|
+
if (!rates.length) return "none — nothing ships here";
|
|
115
|
+
return rates.map((r) => `${r.name || "Rate"} ${money.format(r.cost || 0)}`).join(" · ");
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: "tax",
|
|
120
|
+
label: "Tax",
|
|
121
|
+
className: "hidden lg:table-cell text-muted-foreground",
|
|
122
|
+
render: (row) => {
|
|
123
|
+
const groups = (row.tax_groups || []).filter((g) => (g.rates || []).some((r) => Number(r.rate) > 0));
|
|
124
|
+
const parts = groups.map(
|
|
125
|
+
(g) => `${g.name} ${(g.rates || []).reduce((a, r) => a + (Number(r.rate) || 0), 0)}%`
|
|
126
|
+
);
|
|
127
|
+
if (row.shipping_tax?.value > 0) {
|
|
128
|
+
parts.push(
|
|
129
|
+
row.shipping_tax.type === "fixed"
|
|
130
|
+
? `Shipping ${money.format(row.shipping_tax.value)}`
|
|
131
|
+
: `Shipping ${row.shipping_tax.value}%`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return parts.length ? parts.join(" · ") : "no tax";
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div className="max-w-4xl space-y-4">
|
|
141
|
+
<Card>
|
|
142
|
+
<CardContent className="flex items-center justify-between gap-4 py-4">
|
|
143
|
+
<div>
|
|
144
|
+
<Label>Charge shipping</Label>
|
|
145
|
+
<p className="text-xs text-muted-foreground">
|
|
146
|
+
Off means carts never carry a shipping step — for fully virtual stores.
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
149
|
+
<Switch checked={shippingEnabled} onCheckedChange={setShippingEnabled} />
|
|
150
|
+
</CardContent>
|
|
151
|
+
</Card>
|
|
152
|
+
|
|
153
|
+
<div className="flex items-center justify-between">
|
|
154
|
+
<p className="text-sm text-muted-foreground">
|
|
155
|
+
A location covers the regions it names; the location with no regions catches everyone else.
|
|
156
|
+
Each carries its own shipping rates and tax.
|
|
157
|
+
</p>
|
|
158
|
+
<Button onClick={() => navigate(href("settings/shipping-tax/new"))}>
|
|
159
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
160
|
+
Add location
|
|
161
|
+
</Button>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<DataTable
|
|
165
|
+
columns={columns}
|
|
166
|
+
rows={locations || []}
|
|
167
|
+
loading={loading}
|
|
168
|
+
rowActions={(row) => [
|
|
169
|
+
{ label: "Edit", icon: Pencil, onClick: () => navigate(href(`settings/shipping-tax/${row.id}`)) },
|
|
170
|
+
{ label: "Clone", icon: Copy, onClick: () => clone(row) },
|
|
171
|
+
{ label: "Delete", icon: Trash2, destructive: true, onClick: () => setConfirmDelete(row) },
|
|
172
|
+
]}
|
|
173
|
+
empty={{
|
|
174
|
+
icon: Globe,
|
|
175
|
+
title: "No locations yet",
|
|
176
|
+
description: "Add a location to define where you ship and what tax applies.",
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
|
|
180
|
+
<ConfirmDialog
|
|
181
|
+
open={Boolean(confirmDelete)}
|
|
182
|
+
onOpenChange={(o) => !o && setConfirmDelete(null)}
|
|
183
|
+
title={`Delete "${confirmDelete?.name}"?`}
|
|
184
|
+
description="Orders already placed keep their charges; carts shipping to this location lose their options."
|
|
185
|
+
confirmLabel="Delete"
|
|
186
|
+
onConfirm={doDelete}
|
|
187
|
+
loading={deleting}
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -19,11 +19,9 @@ import CustomerEditor from "./pages/customers/CustomerEditor";
|
|
|
19
19
|
import Reports from "./pages/reports/Reports";
|
|
20
20
|
import SettingsLayout from "./pages/settings/SettingsLayout";
|
|
21
21
|
import GeneralSettings from "./pages/settings/GeneralSettings";
|
|
22
|
-
import ProductsSettings from "./pages/settings/ProductsSettings";
|
|
23
22
|
import InventorySettings from "./pages/settings/InventorySettings";
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
import ShippingZoneEditor from "./pages/settings/ShippingZoneEditor";
|
|
23
|
+
import ShippingTaxSettings from "./pages/settings/ShippingTaxSettings";
|
|
24
|
+
import LocationEditor from "./pages/settings/LocationEditor";
|
|
27
25
|
import PaymentsSettings from "./pages/settings/PaymentsSettings";
|
|
28
26
|
import EmailsSettings from "./pages/settings/EmailsSettings";
|
|
29
27
|
import Webhooks from "./pages/status/Webhooks";
|
|
@@ -52,11 +50,9 @@ export const ADMIN_ROUTES = [
|
|
|
52
50
|
{ path: "reports", label: "Reports" },
|
|
53
51
|
{ path: "settings", label: "Settings" },
|
|
54
52
|
{ path: "settings/general", label: "General" },
|
|
55
|
-
{ path: "settings/products", label: "Products" },
|
|
56
53
|
{ path: "settings/inventory", label: "Inventory" },
|
|
57
|
-
{ path: "settings/tax", label: "Tax" },
|
|
58
|
-
{ path: "settings/shipping", label: "
|
|
59
|
-
{ path: "settings/shipping/zones/:zoneId", label: "Shipping zone" },
|
|
54
|
+
{ path: "settings/shipping-tax", label: "Shipping & Tax" },
|
|
55
|
+
{ path: "settings/shipping-tax/:locationId", label: "Location" },
|
|
60
56
|
{ path: "settings/payments", label: "Payments" },
|
|
61
57
|
{ path: "settings/emails", label: "Emails" },
|
|
62
58
|
{ path: "settings/webhooks", label: "Webhooks" },
|
|
@@ -124,11 +120,9 @@ export default function AdminRoutes() {
|
|
|
124
120
|
<Route path="settings" element={<SettingsLayout />}>
|
|
125
121
|
<Route index element={<Navigate to="general" replace />} />
|
|
126
122
|
<Route path="general" element={<GeneralSettings />} />
|
|
127
|
-
<Route path="products" element={<ProductsSettings />} />
|
|
128
123
|
<Route path="inventory" element={<InventorySettings />} />
|
|
129
|
-
<Route path="tax" element={<
|
|
130
|
-
<Route path="shipping" element={<
|
|
131
|
-
<Route path="shipping/zones/:zoneId" element={<ShippingZoneEditor />} />
|
|
124
|
+
<Route path="shipping-tax" element={<ShippingTaxSettings />} />
|
|
125
|
+
<Route path="shipping-tax/:locationId" element={<LocationEditor />} />
|
|
132
126
|
<Route path="payments" element={<PaymentsSettings />} />
|
|
133
127
|
<Route path="emails" element={<EmailsSettings />} />
|
|
134
128
|
<Route path="webhooks" element={<Webhooks />} />
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* - `variants.js` — variant selection: map attribute selections (Size, Color)
|
|
12
12
|
* to a `ProductVariation` and back, per-option availability, variant price
|
|
13
|
-
* ranges. See
|
|
13
|
+
* ranges. See `.agents/skills/commerce/references/storefront-product-page.md`.
|
|
14
14
|
* - `shipping-promos.js` — read the store's real free-shipping configuration so
|
|
15
15
|
* "Free shipping over €150" copy states a configured rule, not an invented
|
|
16
|
-
* number. See
|
|
16
|
+
* number. See `.agents/skills/commerce/docs/api-storefront.md`.
|
|
17
17
|
*/
|
|
18
18
|
export * from "./variants.js";
|
|
19
19
|
export * from "./shipping-promos.js";
|