@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.
Files changed (95) hide show
  1. package/README.md +11 -11
  2. package/base44/agents/commerce/StoreAdmin.jsonc +2 -2
  3. package/base44/entities/commerce.Cart.jsonc +1 -1
  4. package/base44/entities/commerce.Coupon.jsonc +5 -0
  5. package/base44/entities/commerce.Order.jsonc +6 -7
  6. package/base44/entities/commerce.OrderRefund.jsonc +1 -1
  7. package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
  8. package/base44/entities/commerce.Product.jsonc +6 -16
  9. package/base44/entities/{commerce.ProductTag.jsonc → commerce.ProductRibbon.jsonc} +2 -2
  10. package/base44/entities/commerce.ProductVariation.jsonc +1 -9
  11. package/base44/entities/commerce.ShippingTaxLocation.jsonc +85 -0
  12. package/base44/entities/commerce.Webhook.jsonc +1 -1
  13. package/base44/functions/commerce/admin-orders/helpers.ts +7 -13
  14. package/base44/functions/commerce/admin-products/entry.ts +11 -17
  15. package/base44/functions/commerce/admin-refunds/entry.ts +10 -9
  16. package/base44/functions/commerce/admin-reports/entry.ts +3 -3
  17. package/base44/functions/commerce/admin-tools/entry.ts +9 -36
  18. package/base44/functions/commerce/payment-webhook/entry.ts +50 -89
  19. package/base44/functions/commerce/payments/entry.ts +46 -42
  20. package/base44/functions/commerce/seed-store/defaults.ts +35 -42
  21. package/base44/functions/commerce/seed-store/entry.ts +68 -46
  22. package/base44/functions/commerce/seed-store/sample-data.ts +2 -15
  23. package/base44/functions/commerce/seed-store/seed-catalog.ts +105 -55
  24. package/base44/functions/commerce/storefront-cart/cart-pricing.ts +6 -11
  25. package/base44/functions/commerce/storefront-cart/entry.ts +36 -2
  26. package/base44/functions/commerce/storefront-catalog/entry.ts +55 -72
  27. package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +6 -11
  28. package/base44/functions/commerce/storefront-checkout/entry.ts +43 -56
  29. package/base44/shared/commerce/card-payment.ts +80 -0
  30. package/base44/shared/commerce/coupons.ts +16 -10
  31. package/base44/shared/commerce/emails.ts +30 -18
  32. package/base44/shared/commerce/money.ts +11 -24
  33. package/base44/shared/commerce/payments.ts +55 -286
  34. package/base44/shared/commerce/scan.ts +1 -1
  35. package/base44/shared/commerce/sequence.ts +1 -1
  36. package/base44/shared/commerce/settings.ts +4 -9
  37. package/base44/shared/commerce/shipping.ts +65 -133
  38. package/base44/shared/commerce/tax.ts +48 -96
  39. package/base44/shared/commerce/totals.ts +77 -91
  40. package/package.json +1 -1
  41. package/scripts/install.js +28 -7
  42. package/skills/commerce/SKILL.md +14 -14
  43. package/skills/commerce/docs/api-admin.md +23 -26
  44. package/skills/commerce/docs/api-storefront.md +67 -61
  45. package/skills/commerce/installation-guidelines.md +8 -8
  46. package/skills/commerce/post-installation.md +60 -39
  47. package/skills/commerce/references/admin-product-form.md +15 -12
  48. package/skills/commerce/references/emails.md +2 -2
  49. package/skills/commerce/references/guest-access-security.md +2 -2
  50. package/skills/commerce/references/online-payments.md +24 -166
  51. package/skills/commerce/references/product-render.md +18 -18
  52. package/skills/commerce/references/reviews.md +14 -8
  53. package/skills/commerce/references/storefront-product-page.md +1 -1
  54. package/src/commerce/admin/README.md +4 -5
  55. package/src/commerce/admin/bot/Markdown.jsx +1 -1
  56. package/src/commerce/admin/hooks/useMoney.js +13 -22
  57. package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
  58. package/src/commerce/admin/lib/constants.js +2 -29
  59. package/src/commerce/admin/lib/order-utils.js +1 -1
  60. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +131 -140
  61. package/src/commerce/admin/pages/coupons/CouponsList.jsx +14 -7
  62. package/src/commerce/admin/pages/orders/OrderEditor.jsx +3 -3
  63. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +17 -28
  64. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +5 -11
  65. package/src/commerce/admin/pages/products/Categories.jsx +147 -177
  66. package/src/commerce/admin/pages/products/ProductEditor.jsx +23 -18
  67. package/src/commerce/admin/pages/products/Reviews.jsx +37 -1
  68. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -47
  69. package/src/commerce/admin/pages/products/components/PublishBox.jsx +13 -34
  70. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +29 -29
  71. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +14 -44
  72. package/src/commerce/admin/pages/reports/Reports.jsx +2 -2
  73. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +101 -68
  74. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +40 -38
  75. package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -41
  76. package/src/commerce/admin/pages/settings/LocationEditor.jsx +377 -0
  77. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +137 -119
  78. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -4
  79. package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +191 -0
  80. package/src/commerce/admin/routes.jsx +6 -12
  81. package/src/commerce/utils/index.js +2 -2
  82. package/src/commerce/utils/shipping-promos.js +45 -49
  83. package/src/commerce/utils/variants.js +1 -1
  84. package/base44/entities/commerce.ShippingClass.jsonc +0 -30
  85. package/base44/entities/commerce.ShippingZone.jsonc +0 -41
  86. package/base44/entities/commerce.ShippingZoneMethod.jsonc +0 -84
  87. package/base44/entities/commerce.TaxClass.jsonc +0 -23
  88. package/base44/entities/commerce.TaxRate.jsonc +0 -68
  89. package/base44/shared/commerce/stripe.ts +0 -463
  90. package/src/commerce/admin/hooks/usePaymentProvider.js +0 -27
  91. package/src/commerce/admin/pages/settings/ProductsSettings.jsx +0 -118
  92. package/src/commerce/admin/pages/settings/ShippingSettings.jsx +0 -304
  93. package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +0 -514
  94. package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +0 -231
  95. package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -280
@@ -1,514 +0,0 @@
1
- import React, { useEffect, useMemo, useState } from "react";
2
- import { useNavigate, useParams } from "react-router-dom";
3
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4
- import { Button } from "@/components/ui/button";
5
- import { Input } from "@/components/ui/input";
6
- import { Label } from "@/components/ui/label";
7
- import { Textarea } from "@/components/ui/textarea";
8
- import { Switch } from "@/components/ui/switch";
9
- import { Checkbox } from "@/components/ui/checkbox";
10
- import {
11
- Select,
12
- SelectContent,
13
- SelectItem,
14
- SelectTrigger,
15
- SelectValue,
16
- } from "@/components/ui/select";
17
- import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
18
- import {
19
- Dialog,
20
- DialogContent,
21
- DialogFooter,
22
- DialogHeader,
23
- DialogTitle,
24
- } from "@/components/ui/dialog";
25
- import { Loader2, Plus, Trash2 } from "lucide-react";
26
- import { toast } from "sonner";
27
- import PageHeader from "../../components/PageHeader";
28
- import DataTable from "../../components/DataTable";
29
- import MoneyInput from "../../components/MoneyInput";
30
- import ConfirmDialog from "../../components/ConfirmDialog";
31
- import SearchSelect from "../../components/SearchSelect";
32
- import { base44 } from "../../lib/api";
33
- import useAsync from "../../hooks/useAsync";
34
- import { useAdminHref } from "../../context/BasePathContext";
35
- import { CONTINENTS, COUNTRIES } from "../../lib/geo-data";
36
- import { SHIPPING_METHOD_TYPES, FREE_SHIPPING_REQUIRES, TAX_STATUSES } from "../../lib/constants";
37
-
38
- // Combined region options: continents, countries, states (code style "US:CA").
39
- const GEO_OPTIONS = [
40
- ...CONTINENTS.map((ct) => ({ value: `continent:${ct.code}`, label: `${ct.name} — continent` })),
41
- ...COUNTRIES.map((c) => ({ value: `country:${c.code}`, label: c.name })),
42
- ...COUNTRIES.filter((c) => c.states).flatMap((c) =>
43
- c.states.map((st) => ({ value: `state:${c.code}:${st.code}`, label: `${st.name}, ${c.name}` }))
44
- ),
45
- ];
46
- const GEO_LABEL = Object.fromEntries(GEO_OPTIONS.map((o) => [o.value, o.label]));
47
-
48
- const valueToLocation = (v) => {
49
- const i = v.indexOf(":");
50
- return { type: v.slice(0, i), code: v.slice(i + 1) };
51
- };
52
- const locationToValue = (l) => `${l.type}:${l.code}`;
53
-
54
- const METHOD_DEFAULTS = {
55
- flat_rate: { cost: 0, tax_status: "taxable", class_costs: [], no_class_cost: 0, calculation_type: "order" },
56
- free_shipping: { requires: "", min_amount: 0, ignore_discounts: false },
57
- local_pickup: { cost: 0, tax_status: "taxable" },
58
- };
59
-
60
- export default function ShippingZoneEditor() {
61
- const { zoneId } = useParams();
62
- const navigate = useNavigate();
63
- const href = useAdminHref();
64
-
65
- const [name, setName] = useState("");
66
- const [regions, setRegions] = useState([]); // [{value,label}]
67
- const [postcodesText, setPostcodesText] = useState("");
68
- const [loading, setLoading] = useState(true);
69
- const [savingZone, setSavingZone] = useState(false);
70
- const [dirty, setDirty] = useState(false);
71
-
72
- const methods = useAsync(() => base44.entities["commerce.ShippingZoneMethod"].filter({ zone_id: zoneId }, "order", 100), [zoneId]);
73
- const classes = useAsync(() => base44.entities["commerce.ShippingClass"].list("name", 200), []);
74
-
75
- const [dialog, setDialog] = useState(null); // { method, isNew }
76
- const [savingMethod, setSavingMethod] = useState(false);
77
- const [toDelete, setToDelete] = useState(null);
78
- const [deleting, setDeleting] = useState(false);
79
- const [adding, setAdding] = useState(false);
80
- const [confirmZoneDelete, setConfirmZoneDelete] = useState(false);
81
- const [deletingZone, setDeletingZone] = useState(false);
82
- const [locationCount, setLocationCount] = useState(0);
83
-
84
- useEffect(() => {
85
- let cancelled = false;
86
- setLoading(true);
87
- base44.entities["commerce.ShippingZone"].get(zoneId)
88
- .then((z) => {
89
- if (cancelled || !z) return;
90
- setName(z.name || "");
91
- const locs = z.locations || [];
92
- setRegions(
93
- locs
94
- .filter((l) => l.type !== "postcode")
95
- .map((l) => ({ value: locationToValue(l), label: GEO_LABEL[locationToValue(l)] || l.code }))
96
- );
97
- setPostcodesText(locs.filter((l) => l.type === "postcode").map((l) => l.code).join("\n"));
98
- setLocationCount(locs.length);
99
- setDirty(false);
100
- })
101
- .catch((err) => toast.error(err.message || "Failed to load zone"))
102
- .finally(() => !cancelled && setLoading(false));
103
- return () => {
104
- cancelled = true;
105
- };
106
- }, [zoneId]);
107
-
108
- const searchRegions = async (q) => {
109
- const needle = (q || "").toLowerCase();
110
- return GEO_OPTIONS.filter((o) => !needle || o.label.toLowerCase().includes(needle)).slice(0, 60);
111
- };
112
-
113
- const saveZone = async () => {
114
- setSavingZone(true);
115
- try {
116
- const locations = [
117
- ...regions.map((r) => valueToLocation(r.value)),
118
- ...postcodesText
119
- .split(/[\n,]/)
120
- .map((s) => s.trim())
121
- .filter(Boolean)
122
- .map((code) => ({ type: "postcode", code })),
123
- ];
124
- await base44.entities["commerce.ShippingZone"].update(zoneId, { name, locations });
125
- toast.success("Zone saved");
126
- setDirty(false);
127
- } catch (err) {
128
- toast.error(err.message || "Failed to save zone");
129
- } finally {
130
- setSavingZone(false);
131
- }
132
- };
133
-
134
- const addMethod = async (methodId) => {
135
- setAdding(true);
136
- try {
137
- const type = SHIPPING_METHOD_TYPES.find((t) => t.value === methodId);
138
- const order = (methods.data || []).reduce((m, x) => Math.max(m, x.order ?? 0), 0) + 1;
139
- const created = await base44.entities["commerce.ShippingZoneMethod"].create({
140
- zone_id: zoneId,
141
- method_id: methodId,
142
- title: type?.label || methodId,
143
- enabled: true,
144
- order,
145
- settings: { ...METHOD_DEFAULTS[methodId] },
146
- });
147
- await methods.refetch();
148
- setDialog({ method: created, isNew: true });
149
- } catch (err) {
150
- toast.error(err.message || "Failed to add method");
151
- } finally {
152
- setAdding(false);
153
- }
154
- };
155
-
156
- const toggleMethod = async (m, enabled) => {
157
- try {
158
- await base44.entities["commerce.ShippingZoneMethod"].update(m.id, { enabled });
159
- methods.refetch();
160
- } catch (err) {
161
- toast.error(err.message || "Failed to update method");
162
- }
163
- };
164
-
165
- const saveMethod = async () => {
166
- setSavingMethod(true);
167
- try {
168
- const { method } = dialog;
169
- await base44.entities["commerce.ShippingZoneMethod"].update(method.id, {
170
- title: method.title,
171
- settings: method.settings,
172
- });
173
- toast.success("Method saved");
174
- setDialog(null);
175
- methods.refetch();
176
- } catch (err) {
177
- toast.error(err.message || "Failed to save method");
178
- } finally {
179
- setSavingMethod(false);
180
- }
181
- };
182
-
183
- const removeMethod = async () => {
184
- setDeleting(true);
185
- try {
186
- await base44.entities["commerce.ShippingZoneMethod"].delete(toDelete.id);
187
- toast.success("Method removed");
188
- setToDelete(null);
189
- methods.refetch();
190
- } catch (err) {
191
- toast.error(err.message || "Failed to remove method");
192
- } finally {
193
- setDeleting(false);
194
- }
195
- };
196
-
197
- const removeZone = async () => {
198
- setDeletingZone(true);
199
- try {
200
- for (const m of methods.data || []) {
201
- await base44.entities["commerce.ShippingZoneMethod"].delete(m.id);
202
- }
203
- await base44.entities["commerce.ShippingZone"].delete(zoneId);
204
- toast.success("Shipping zone deleted");
205
- navigate(href("settings/shipping"));
206
- } catch (err) {
207
- toast.error(err.message || "Failed to delete zone");
208
- setDeletingZone(false);
209
- setConfirmZoneDelete(false);
210
- }
211
- };
212
-
213
- const methodColumns = [
214
- { key: "title", label: "Title", render: (m) => <span className="font-medium">{m.title || m.method_id}</span> },
215
- {
216
- key: "method_id",
217
- label: "Type",
218
- render: (m) => SHIPPING_METHOD_TYPES.find((t) => t.value === m.method_id)?.label || m.method_id,
219
- },
220
- {
221
- key: "enabled",
222
- label: "Enabled",
223
- render: (m) => <Switch checked={!!m.enabled} onCheckedChange={(v) => toggleMethod(m, v)} />,
224
- },
225
- ];
226
-
227
- if (loading) {
228
- return (
229
- <div className="flex justify-center py-12">
230
- <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
231
- </div>
232
- );
233
- }
234
-
235
- return (
236
- <div className="max-w-3xl space-y-4">
237
- <PageHeader
238
- title="Edit shipping zone"
239
- backHref={href("settings/shipping")}
240
- actions={
241
- <Button variant="outline" className="text-destructive" onClick={() => setConfirmZoneDelete(true)}>
242
- <Trash2 className="mr-2 h-4 w-4" />
243
- Delete zone
244
- </Button>
245
- }
246
- />
247
-
248
- <Card>
249
- <CardHeader>
250
- <CardTitle>Zone details</CardTitle>
251
- </CardHeader>
252
- <CardContent className="space-y-4">
253
- <div className="space-y-1.5">
254
- <Label>Zone name</Label>
255
- <Input
256
- value={name}
257
- onChange={(e) => {
258
- setName(e.target.value);
259
- setDirty(true);
260
- }}
261
- />
262
- </div>
263
- <div className="space-y-1.5">
264
- <Label>Zone regions</Label>
265
- <SearchSelect
266
- multiple
267
- placeholder="Add regions…"
268
- value={regions}
269
- onChange={(v) => {
270
- setRegions(v);
271
- setDirty(true);
272
- }}
273
- search={searchRegions}
274
- />
275
- <p className="text-xs text-muted-foreground">
276
- Continents, countries and states this zone applies to. Leave empty for a
277
- "rest of the world" catch-all zone.
278
- </p>
279
- </div>
280
- <div className="space-y-1.5">
281
- <Label>Limit to specific postcodes (optional)</Label>
282
- <Textarea
283
- rows={3}
284
- placeholder={"One per line. Wildcards (90*) and ranges (1000...2000) supported."}
285
- value={postcodesText}
286
- onChange={(e) => {
287
- setPostcodesText(e.target.value);
288
- setDirty(true);
289
- }}
290
- />
291
- </div>
292
- <Button onClick={saveZone} disabled={!dirty || savingZone}>
293
- {savingZone && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
294
- Save zone
295
- </Button>
296
- </CardContent>
297
- </Card>
298
-
299
- <Card>
300
- <CardHeader className="flex-row items-center justify-between space-y-0">
301
- <CardTitle>Shipping methods</CardTitle>
302
- <AddMethodSelect onAdd={addMethod} disabled={adding} />
303
- </CardHeader>
304
- <CardContent>
305
- <DataTable
306
- columns={methodColumns}
307
- rows={methods.data || []}
308
- loading={methods.loading}
309
- rowActions={(m) => [
310
- { label: "Edit settings", onClick: () => setDialog({ method: { ...m, settings: { ...METHOD_DEFAULTS[m.method_id], ...(m.settings || {}) } }, isNew: false }) },
311
- { label: "Delete", destructive: true, onClick: () => setToDelete(m) },
312
- ]}
313
- empty={{ title: "No shipping methods", description: "Add a method so customers in this zone can check out." }}
314
- />
315
- </CardContent>
316
- </Card>
317
-
318
- {dialog && (
319
- <MethodSettingsDialog
320
- dialog={dialog}
321
- setDialog={setDialog}
322
- classes={classes.data || []}
323
- saving={savingMethod}
324
- onSave={saveMethod}
325
- />
326
- )}
327
-
328
- <ConfirmDialog
329
- open={!!toDelete}
330
- onOpenChange={(o) => !o && setToDelete(null)}
331
- title="Remove shipping method?"
332
- confirmLabel="Remove"
333
- loading={deleting}
334
- onConfirm={removeMethod}
335
- />
336
-
337
- <ConfirmDialog
338
- open={confirmZoneDelete}
339
- onOpenChange={(o) => !o && setConfirmZoneDelete(false)}
340
- title={`Delete ${name || "shipping zone"}?`}
341
- description={
342
- locationCount === 0
343
- ? "This is the catch-all zone. Customers outside your other zones will have no shipping methods until you add another catch-all."
344
- : "The zone and its shipping methods are deleted. Existing orders keep the shipping they were charged."
345
- }
346
- confirmLabel="Delete"
347
- loading={deletingZone}
348
- onConfirm={removeZone}
349
- />
350
- </div>
351
- );
352
- }
353
-
354
- function AddMethodSelect({ onAdd, disabled }) {
355
- const [value, setValue] = useState("");
356
- return (
357
- <div className="flex items-center gap-2">
358
- <Select
359
- value={value}
360
- onValueChange={(v) => {
361
- setValue("");
362
- onAdd(v);
363
- }}
364
- >
365
- <SelectTrigger className="w-44">
366
- <SelectValue placeholder="Add shipping method" />
367
- </SelectTrigger>
368
- <SelectContent>
369
- {SHIPPING_METHOD_TYPES.map((t) => (
370
- <SelectItem key={t.value} value={t.value}>
371
- {t.label}
372
- </SelectItem>
373
- ))}
374
- </SelectContent>
375
- </Select>
376
- {disabled && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
377
- {!disabled && <Plus className="h-4 w-4 text-muted-foreground" />}
378
- </div>
379
- );
380
- }
381
-
382
- function MethodSettingsDialog({ dialog, setDialog, classes, saving, onSave }) {
383
- const { method } = dialog;
384
- const s = method.settings || {};
385
- const setTitle = (title) => setDialog({ ...dialog, method: { ...method, title } });
386
- const setSetting = (key, val) => setDialog({ ...dialog, method: { ...method, settings: { ...s, [key]: val } } });
387
-
388
- const setClassCost = (classId, cost) => {
389
- const existing = (s.class_costs || []).filter((c) => c.shipping_class_id !== classId);
390
- setSetting("class_costs", [...existing, { shipping_class_id: classId, cost: cost ?? 0 }]);
391
- };
392
- const classCost = (classId) => (s.class_costs || []).find((c) => c.shipping_class_id === classId)?.cost ?? null;
393
-
394
- return (
395
- <Dialog open onOpenChange={(o) => !o && setDialog(null)}>
396
- <DialogContent className="max-h-[85vh] overflow-y-auto">
397
- <DialogHeader>
398
- <DialogTitle>{SHIPPING_METHOD_TYPES.find((t) => t.value === method.method_id)?.label || method.method_id} settings</DialogTitle>
399
- </DialogHeader>
400
- <div className="space-y-4">
401
- <div className="space-y-1.5">
402
- <Label>Method title</Label>
403
- <Input value={method.title || ""} onChange={(e) => setTitle(e.target.value)} />
404
- </div>
405
-
406
- {method.method_id === "flat_rate" && (
407
- <>
408
- <div className="space-y-1.5">
409
- <Label>Tax status</Label>
410
- <Select value={s.tax_status || "taxable"} onValueChange={(v) => setSetting("tax_status", v)}>
411
- <SelectTrigger><SelectValue /></SelectTrigger>
412
- <SelectContent>
413
- {TAX_STATUSES.filter((t) => t.value !== "shipping").map((t) => (
414
- <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
415
- ))}
416
- </SelectContent>
417
- </Select>
418
- </div>
419
- <div className="space-y-1.5">
420
- <Label>Base cost</Label>
421
- <MoneyInput value={s.cost ?? 0} onChange={(v) => setSetting("cost", v ?? 0)} />
422
- </div>
423
- <div className="space-y-1.5">
424
- <Label>Calculation type</Label>
425
- <RadioGroup value={s.calculation_type || "order"} onValueChange={(v) => setSetting("calculation_type", v)}>
426
- <label className="flex items-center gap-2 text-sm">
427
- <RadioGroupItem value="order" /> Per order — charge the most expensive shipping class
428
- </label>
429
- <label className="flex items-center gap-2 text-sm">
430
- <RadioGroupItem value="class" /> Per class — charge each shipping class in the cart
431
- </label>
432
- </RadioGroup>
433
- </div>
434
- {classes.length > 0 && (
435
- <div className="space-y-2 rounded-md border p-3">
436
- <p className="text-sm font-medium">Per shipping-class costs</p>
437
- {classes.map((c) => (
438
- <div key={c.id} className="flex items-center justify-between gap-3">
439
- <span className="text-sm">{c.name}</span>
440
- <div className="w-40">
441
- <MoneyInput value={classCost(c.id)} onChange={(v) => setClassCost(c.id, v)} />
442
- </div>
443
- </div>
444
- ))}
445
- <div className="flex items-center justify-between gap-3 border-t pt-2">
446
- <span className="text-sm">No shipping class</span>
447
- <div className="w-40">
448
- <MoneyInput value={s.no_class_cost ?? 0} onChange={(v) => setSetting("no_class_cost", v ?? 0)} />
449
- </div>
450
- </div>
451
- </div>
452
- )}
453
- </>
454
- )}
455
-
456
- {method.method_id === "free_shipping" && (
457
- <>
458
- <div className="space-y-1.5">
459
- <Label>Free shipping requires</Label>
460
- <Select value={s.requires || ""} onValueChange={(v) => setSetting("requires", v)}>
461
- <SelectTrigger><SelectValue /></SelectTrigger>
462
- <SelectContent>
463
- {FREE_SHIPPING_REQUIRES.map((r) => (
464
- <SelectItem key={r.value || "none"} value={r.value || "none"}>{r.label}</SelectItem>
465
- ))}
466
- </SelectContent>
467
- </Select>
468
- </div>
469
- {["min_amount", "either", "both"].includes(s.requires) && (
470
- <>
471
- <div className="space-y-1.5">
472
- <Label>Minimum order amount</Label>
473
- <MoneyInput value={s.min_amount ?? 0} onChange={(v) => setSetting("min_amount", v ?? 0)} />
474
- </div>
475
- <label className="flex items-center gap-2 text-sm">
476
- <Checkbox checked={!!s.ignore_discounts} onCheckedChange={(v) => setSetting("ignore_discounts", !!v)} />
477
- Apply minimum order rule before coupon discounts
478
- </label>
479
- </>
480
- )}
481
- </>
482
- )}
483
-
484
- {method.method_id === "local_pickup" && (
485
- <>
486
- <div className="space-y-1.5">
487
- <Label>Tax status</Label>
488
- <Select value={s.tax_status || "taxable"} onValueChange={(v) => setSetting("tax_status", v)}>
489
- <SelectTrigger><SelectValue /></SelectTrigger>
490
- <SelectContent>
491
- {TAX_STATUSES.filter((t) => t.value !== "shipping").map((t) => (
492
- <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
493
- ))}
494
- </SelectContent>
495
- </Select>
496
- </div>
497
- <div className="space-y-1.5">
498
- <Label>Cost</Label>
499
- <MoneyInput value={s.cost ?? 0} onChange={(v) => setSetting("cost", v ?? 0)} />
500
- </div>
501
- </>
502
- )}
503
- </div>
504
- <DialogFooter>
505
- <Button variant="outline" onClick={() => setDialog(null)}>Cancel</Button>
506
- <Button onClick={onSave} disabled={saving}>
507
- {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
508
- Save
509
- </Button>
510
- </DialogFooter>
511
- </DialogContent>
512
- </Dialog>
513
- );
514
- }