@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
|
@@ -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
|
-
}
|