@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,231 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from "react";
|
|
2
|
-
import { Button } from "@/components/ui/button";
|
|
3
|
-
import { Input } from "@/components/ui/input";
|
|
4
|
-
import { Checkbox } from "@/components/ui/checkbox";
|
|
5
|
-
import {
|
|
6
|
-
Table,
|
|
7
|
-
TableBody,
|
|
8
|
-
TableCell,
|
|
9
|
-
TableHead,
|
|
10
|
-
TableHeader,
|
|
11
|
-
TableRow,
|
|
12
|
-
} from "@/components/ui/table";
|
|
13
|
-
import { Loader2, Plus, Trash2, Undo2 } from "lucide-react";
|
|
14
|
-
import { toast } from "sonner";
|
|
15
|
-
import { base44 } from "../../lib/api";
|
|
16
|
-
import useAsync from "../../hooks/useAsync";
|
|
17
|
-
|
|
18
|
-
let tmpSeq = 0;
|
|
19
|
-
const tmpId = () => `tmp_${++tmpSeq}`;
|
|
20
|
-
|
|
21
|
-
const joinList = (arr) => (arr || []).join(", ");
|
|
22
|
-
const splitList = (text) =>
|
|
23
|
-
text
|
|
24
|
-
.split(",")
|
|
25
|
-
.map((s) => s.trim())
|
|
26
|
-
.filter(Boolean);
|
|
27
|
-
|
|
28
|
-
const newRow = (taxClass, order) => ({
|
|
29
|
-
_tmpId: tmpId(),
|
|
30
|
-
_state: "new",
|
|
31
|
-
country: "",
|
|
32
|
-
state: "",
|
|
33
|
-
postcodes: [],
|
|
34
|
-
cities: [],
|
|
35
|
-
rate: 0,
|
|
36
|
-
name: "Tax",
|
|
37
|
-
priority: 1,
|
|
38
|
-
compound: false,
|
|
39
|
-
shipping: true,
|
|
40
|
-
tax_class: taxClass,
|
|
41
|
-
menu_order: order,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Editable tax-rate grid for one tax class ("Standard rates" table).
|
|
46
|
-
* Tracks new / modified / deleted rows locally; "Save changes" batches the
|
|
47
|
-
* TaxRate create/update/delete calls.
|
|
48
|
-
*/
|
|
49
|
-
export default function TaxRatesTable({ taxClass }) {
|
|
50
|
-
const { data, loading, refetch } = useAsync(
|
|
51
|
-
() => base44.entities["commerce.TaxRate"].filter({ tax_class: taxClass }, "menu_order", 500),
|
|
52
|
-
[taxClass]
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const [rows, setRows] = useState([]);
|
|
56
|
-
const [saving, setSaving] = useState(false);
|
|
57
|
-
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
setRows((data || []).map((r) => ({ ...r, _tmpId: r.id, _state: "clean" })));
|
|
60
|
-
}, [data]);
|
|
61
|
-
|
|
62
|
-
const setCell = (id, key, value) =>
|
|
63
|
-
setRows((rs) =>
|
|
64
|
-
rs.map((r) =>
|
|
65
|
-
r._tmpId === id
|
|
66
|
-
? { ...r, [key]: value, _state: r._state === "new" ? "new" : "modified" }
|
|
67
|
-
: r
|
|
68
|
-
)
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const removeRow = (id) =>
|
|
72
|
-
setRows((rs) =>
|
|
73
|
-
rs
|
|
74
|
-
.map((r) => {
|
|
75
|
-
if (r._tmpId !== id) return r;
|
|
76
|
-
if (r._state === "new") return null;
|
|
77
|
-
return { ...r, _state: r._state === "deleted" ? "modified" : "deleted" };
|
|
78
|
-
})
|
|
79
|
-
.filter(Boolean)
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const insertRow = () => setRows((rs) => [...rs, newRow(taxClass, rs.length)]);
|
|
83
|
-
|
|
84
|
-
const dirty = rows.some((r) => r._state !== "clean");
|
|
85
|
-
|
|
86
|
-
const saveAll = async () => {
|
|
87
|
-
setSaving(true);
|
|
88
|
-
try {
|
|
89
|
-
for (const r of rows) {
|
|
90
|
-
const payload = {
|
|
91
|
-
country: (r.country || "").toUpperCase(),
|
|
92
|
-
state: (r.state || "").toUpperCase(),
|
|
93
|
-
postcodes: r.postcodes || [],
|
|
94
|
-
cities: r.cities || [],
|
|
95
|
-
rate: Number(r.rate) || 0,
|
|
96
|
-
name: r.name || "Tax",
|
|
97
|
-
priority: Number(r.priority) || 1,
|
|
98
|
-
compound: !!r.compound,
|
|
99
|
-
shipping: !!r.shipping,
|
|
100
|
-
tax_class: taxClass,
|
|
101
|
-
menu_order: rows.indexOf(r),
|
|
102
|
-
};
|
|
103
|
-
if (r._state === "new") await base44.entities["commerce.TaxRate"].create(payload);
|
|
104
|
-
else if (r._state === "modified") await base44.entities["commerce.TaxRate"].update(r.id, payload);
|
|
105
|
-
else if (r._state === "deleted") await base44.entities["commerce.TaxRate"].delete(r.id);
|
|
106
|
-
}
|
|
107
|
-
toast.success("Tax rates saved");
|
|
108
|
-
await refetch();
|
|
109
|
-
} catch (err) {
|
|
110
|
-
toast.error(err.message || "Failed to save tax rates");
|
|
111
|
-
} finally {
|
|
112
|
-
setSaving(false);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const cell = (r, key, props = {}) => (
|
|
117
|
-
<Input
|
|
118
|
-
className="h-8"
|
|
119
|
-
disabled={r._state === "deleted"}
|
|
120
|
-
value={r[key] ?? ""}
|
|
121
|
-
onChange={(e) => setCell(r._tmpId, key, e.target.value)}
|
|
122
|
-
{...props}
|
|
123
|
-
/>
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<div className="space-y-3">
|
|
128
|
-
<div className="overflow-x-auto rounded-lg border bg-background">
|
|
129
|
-
<Table>
|
|
130
|
-
<TableHeader>
|
|
131
|
-
<TableRow>
|
|
132
|
-
<TableHead className="min-w-20">Country code</TableHead>
|
|
133
|
-
<TableHead className="min-w-20">State code</TableHead>
|
|
134
|
-
<TableHead className="min-w-32">Postcode / ZIP</TableHead>
|
|
135
|
-
<TableHead className="min-w-32">City</TableHead>
|
|
136
|
-
<TableHead className="min-w-24">Rate %</TableHead>
|
|
137
|
-
<TableHead className="min-w-28">Tax name</TableHead>
|
|
138
|
-
<TableHead className="w-20">Priority</TableHead>
|
|
139
|
-
<TableHead className="w-20 text-center">Compound</TableHead>
|
|
140
|
-
<TableHead className="w-20 text-center">Shipping</TableHead>
|
|
141
|
-
<TableHead className="w-12" />
|
|
142
|
-
</TableRow>
|
|
143
|
-
</TableHeader>
|
|
144
|
-
<TableBody>
|
|
145
|
-
{loading ? (
|
|
146
|
-
<TableRow>
|
|
147
|
-
<TableCell colSpan={10} className="py-8 text-center text-muted-foreground">
|
|
148
|
-
<Loader2 className="mx-auto h-5 w-5 animate-spin" />
|
|
149
|
-
</TableCell>
|
|
150
|
-
</TableRow>
|
|
151
|
-
) : rows.length === 0 ? (
|
|
152
|
-
<TableRow>
|
|
153
|
-
<TableCell colSpan={10} className="py-8 text-center text-sm text-muted-foreground">
|
|
154
|
-
No rates for this class yet. Insert a row to get started. Empty country/state
|
|
155
|
-
matches everywhere; postcodes support wildcards (90*) and ranges (1000...2000).
|
|
156
|
-
</TableCell>
|
|
157
|
-
</TableRow>
|
|
158
|
-
) : (
|
|
159
|
-
rows.map((r) => (
|
|
160
|
-
<TableRow key={r._tmpId} className={r._state === "deleted" ? "opacity-50 line-through" : ""}>
|
|
161
|
-
<TableCell>{cell(r, "country", { placeholder: "*", maxLength: 2 })}</TableCell>
|
|
162
|
-
<TableCell>{cell(r, "state", { placeholder: "*" })}</TableCell>
|
|
163
|
-
<TableCell>
|
|
164
|
-
<Input
|
|
165
|
-
className="h-8"
|
|
166
|
-
placeholder="*"
|
|
167
|
-
disabled={r._state === "deleted"}
|
|
168
|
-
value={joinList(r.postcodes)}
|
|
169
|
-
onChange={(e) => setCell(r._tmpId, "postcodes", splitList(e.target.value))}
|
|
170
|
-
/>
|
|
171
|
-
</TableCell>
|
|
172
|
-
<TableCell>
|
|
173
|
-
<Input
|
|
174
|
-
className="h-8"
|
|
175
|
-
placeholder="*"
|
|
176
|
-
disabled={r._state === "deleted"}
|
|
177
|
-
value={joinList(r.cities)}
|
|
178
|
-
onChange={(e) => setCell(r._tmpId, "cities", splitList(e.target.value))}
|
|
179
|
-
/>
|
|
180
|
-
</TableCell>
|
|
181
|
-
<TableCell>{cell(r, "rate", { type: "number", step: "any" })}</TableCell>
|
|
182
|
-
<TableCell>{cell(r, "name")}</TableCell>
|
|
183
|
-
<TableCell>{cell(r, "priority", { type: "number", min: 1 })}</TableCell>
|
|
184
|
-
<TableCell className="text-center">
|
|
185
|
-
<Checkbox
|
|
186
|
-
disabled={r._state === "deleted"}
|
|
187
|
-
checked={!!r.compound}
|
|
188
|
-
onCheckedChange={(v) => setCell(r._tmpId, "compound", !!v)}
|
|
189
|
-
/>
|
|
190
|
-
</TableCell>
|
|
191
|
-
<TableCell className="text-center">
|
|
192
|
-
<Checkbox
|
|
193
|
-
disabled={r._state === "deleted"}
|
|
194
|
-
checked={!!r.shipping}
|
|
195
|
-
onCheckedChange={(v) => setCell(r._tmpId, "shipping", !!v)}
|
|
196
|
-
/>
|
|
197
|
-
</TableCell>
|
|
198
|
-
<TableCell>
|
|
199
|
-
<Button
|
|
200
|
-
variant="ghost"
|
|
201
|
-
size="icon"
|
|
202
|
-
className="h-8 w-8"
|
|
203
|
-
title={r._state === "deleted" ? "Undo delete" : "Delete row"}
|
|
204
|
-
onClick={() => removeRow(r._tmpId)}
|
|
205
|
-
>
|
|
206
|
-
{r._state === "deleted" ? (
|
|
207
|
-
<Undo2 className="h-4 w-4" />
|
|
208
|
-
) : (
|
|
209
|
-
<Trash2 className="h-4 w-4 text-destructive" />
|
|
210
|
-
)}
|
|
211
|
-
</Button>
|
|
212
|
-
</TableCell>
|
|
213
|
-
</TableRow>
|
|
214
|
-
))
|
|
215
|
-
)}
|
|
216
|
-
</TableBody>
|
|
217
|
-
</Table>
|
|
218
|
-
</div>
|
|
219
|
-
<div className="flex items-center gap-2">
|
|
220
|
-
<Button variant="outline" size="sm" onClick={insertRow}>
|
|
221
|
-
<Plus className="mr-1 h-4 w-4" />
|
|
222
|
-
Insert row
|
|
223
|
-
</Button>
|
|
224
|
-
<Button size="sm" onClick={saveAll} disabled={!dirty || saving}>
|
|
225
|
-
{saving && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
|
|
226
|
-
Save changes
|
|
227
|
-
</Button>
|
|
228
|
-
</div>
|
|
229
|
-
</div>
|
|
230
|
-
);
|
|
231
|
-
}
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
|
-
import { Button } from "@/components/ui/button";
|
|
4
|
-
import { Input } from "@/components/ui/input";
|
|
5
|
-
import { Label } from "@/components/ui/label";
|
|
6
|
-
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
7
|
-
import {
|
|
8
|
-
Dialog,
|
|
9
|
-
DialogContent,
|
|
10
|
-
DialogFooter,
|
|
11
|
-
DialogHeader,
|
|
12
|
-
DialogTitle,
|
|
13
|
-
} from "@/components/ui/dialog";
|
|
14
|
-
import {
|
|
15
|
-
Select,
|
|
16
|
-
SelectContent,
|
|
17
|
-
SelectItem,
|
|
18
|
-
SelectTrigger,
|
|
19
|
-
SelectValue,
|
|
20
|
-
} from "@/components/ui/select";
|
|
21
|
-
import { Loader2, Plus, Trash2 } from "lucide-react";
|
|
22
|
-
import { toast } from "sonner";
|
|
23
|
-
import { base44 } from "../../lib/api";
|
|
24
|
-
import useAsync from "../../hooks/useAsync";
|
|
25
|
-
import ConfirmDialog from "../../components/ConfirmDialog";
|
|
26
|
-
import { slugify } from "../../lib/product-utils";
|
|
27
|
-
import useGroupForm from "./useGroupForm";
|
|
28
|
-
import TaxRatesTable from "./TaxRatesTable";
|
|
29
|
-
|
|
30
|
-
const DEFAULTS = {
|
|
31
|
-
prices_include_tax: false,
|
|
32
|
-
tax_based_on: "shipping",
|
|
33
|
-
shipping_tax_class: "inherit",
|
|
34
|
-
display_prices_shop: "excl",
|
|
35
|
-
display_prices_cart: "excl",
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
function TaxOptions({ classes }) {
|
|
39
|
-
const { form, setField, SaveButton } = useGroupForm("tax", DEFAULTS);
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<Card className="max-w-3xl">
|
|
43
|
-
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
44
|
-
<div className="space-y-1.5">
|
|
45
|
-
<CardTitle>Tax options</CardTitle>
|
|
46
|
-
</div>
|
|
47
|
-
<SaveButton />
|
|
48
|
-
</CardHeader>
|
|
49
|
-
<CardContent className="space-y-4">
|
|
50
|
-
<div className="space-y-1.5">
|
|
51
|
-
<Label>Prices entered with tax</Label>
|
|
52
|
-
<RadioGroup
|
|
53
|
-
value={form.prices_include_tax ? "yes" : "no"}
|
|
54
|
-
onValueChange={(v) => setField("prices_include_tax", v === "yes")}
|
|
55
|
-
className="gap-2"
|
|
56
|
-
>
|
|
57
|
-
<label className="flex items-center gap-2 text-sm">
|
|
58
|
-
<RadioGroupItem value="no" />
|
|
59
|
-
No, I will enter prices exclusive of tax
|
|
60
|
-
</label>
|
|
61
|
-
<label className="flex items-center gap-2 text-sm">
|
|
62
|
-
<RadioGroupItem value="yes" />
|
|
63
|
-
Yes, I will enter prices inclusive of tax
|
|
64
|
-
</label>
|
|
65
|
-
</RadioGroup>
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<div className="grid gap-4 sm:grid-cols-2">
|
|
69
|
-
<div className="space-y-1.5">
|
|
70
|
-
<Label>Calculate tax based on</Label>
|
|
71
|
-
<Select value={form.tax_based_on} onValueChange={(v) => setField("tax_based_on", v)}>
|
|
72
|
-
<SelectTrigger>
|
|
73
|
-
<SelectValue />
|
|
74
|
-
</SelectTrigger>
|
|
75
|
-
<SelectContent>
|
|
76
|
-
<SelectItem value="shipping">Customer shipping address</SelectItem>
|
|
77
|
-
<SelectItem value="billing">Customer billing address</SelectItem>
|
|
78
|
-
</SelectContent>
|
|
79
|
-
</Select>
|
|
80
|
-
</div>
|
|
81
|
-
<div className="space-y-1.5">
|
|
82
|
-
<Label>Shipping tax class</Label>
|
|
83
|
-
<Select
|
|
84
|
-
value={form.shipping_tax_class}
|
|
85
|
-
onValueChange={(v) => setField("shipping_tax_class", v)}
|
|
86
|
-
>
|
|
87
|
-
<SelectTrigger>
|
|
88
|
-
<SelectValue />
|
|
89
|
-
</SelectTrigger>
|
|
90
|
-
<SelectContent>
|
|
91
|
-
<SelectItem value="inherit">Shipping tax class based on cart items</SelectItem>
|
|
92
|
-
{(classes || []).map((c) => (
|
|
93
|
-
<SelectItem key={c.slug} value={c.slug}>
|
|
94
|
-
{c.name}
|
|
95
|
-
</SelectItem>
|
|
96
|
-
))}
|
|
97
|
-
</SelectContent>
|
|
98
|
-
</Select>
|
|
99
|
-
</div>
|
|
100
|
-
<div className="space-y-1.5">
|
|
101
|
-
<Label>Display prices in the shop</Label>
|
|
102
|
-
<Select
|
|
103
|
-
value={form.display_prices_shop}
|
|
104
|
-
onValueChange={(v) => setField("display_prices_shop", v)}
|
|
105
|
-
>
|
|
106
|
-
<SelectTrigger>
|
|
107
|
-
<SelectValue />
|
|
108
|
-
</SelectTrigger>
|
|
109
|
-
<SelectContent>
|
|
110
|
-
<SelectItem value="excl">Excluding tax</SelectItem>
|
|
111
|
-
<SelectItem value="incl">Including tax</SelectItem>
|
|
112
|
-
</SelectContent>
|
|
113
|
-
</Select>
|
|
114
|
-
</div>
|
|
115
|
-
<div className="space-y-1.5">
|
|
116
|
-
<Label>Display prices during cart and checkout</Label>
|
|
117
|
-
<Select
|
|
118
|
-
value={form.display_prices_cart}
|
|
119
|
-
onValueChange={(v) => setField("display_prices_cart", v)}
|
|
120
|
-
>
|
|
121
|
-
<SelectTrigger>
|
|
122
|
-
<SelectValue />
|
|
123
|
-
</SelectTrigger>
|
|
124
|
-
<SelectContent>
|
|
125
|
-
<SelectItem value="excl">Excluding tax</SelectItem>
|
|
126
|
-
<SelectItem value="incl">Including tax</SelectItem>
|
|
127
|
-
</SelectContent>
|
|
128
|
-
</Select>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
</CardContent>
|
|
132
|
-
</Card>
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export default function TaxSettings() {
|
|
137
|
-
const { data: classes, loading, refetch } = useAsync(
|
|
138
|
-
() => base44.entities["commerce.TaxClass"].list("created_date", 100),
|
|
139
|
-
[]
|
|
140
|
-
);
|
|
141
|
-
const [tab, setTab] = useState("options");
|
|
142
|
-
const [addOpen, setAddOpen] = useState(false);
|
|
143
|
-
const [newName, setNewName] = useState("");
|
|
144
|
-
const [creating, setCreating] = useState(false);
|
|
145
|
-
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
146
|
-
const [deleting, setDeleting] = useState(false);
|
|
147
|
-
|
|
148
|
-
const createClass = async () => {
|
|
149
|
-
const name = newName.trim();
|
|
150
|
-
if (!name) return;
|
|
151
|
-
setCreating(true);
|
|
152
|
-
try {
|
|
153
|
-
const slug = slugify(name);
|
|
154
|
-
await base44.entities["commerce.TaxClass"].create({ slug, name });
|
|
155
|
-
toast.success(`Tax class "${name}" added`);
|
|
156
|
-
setAddOpen(false);
|
|
157
|
-
setNewName("");
|
|
158
|
-
await refetch();
|
|
159
|
-
setTab(slug);
|
|
160
|
-
} catch (err) {
|
|
161
|
-
toast.error(err.message || "Failed to add tax class");
|
|
162
|
-
} finally {
|
|
163
|
-
setCreating(false);
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const deleteClass = async () => {
|
|
168
|
-
setDeleting(true);
|
|
169
|
-
try {
|
|
170
|
-
await base44.entities["commerce.TaxClass"].delete(deleteTarget.id);
|
|
171
|
-
toast.success(`Tax class "${deleteTarget.name}" removed. Its tax rates were kept.`);
|
|
172
|
-
setDeleteTarget(null);
|
|
173
|
-
if (tab === deleteTarget.slug) setTab("options");
|
|
174
|
-
await refetch();
|
|
175
|
-
} catch (err) {
|
|
176
|
-
toast.error(err.message || "Failed to delete tax class");
|
|
177
|
-
} finally {
|
|
178
|
-
setDeleting(false);
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const current = (classes || []).find((c) => c.slug === tab);
|
|
183
|
-
|
|
184
|
-
return (
|
|
185
|
-
<div className="space-y-4">
|
|
186
|
-
<div className="flex flex-wrap items-center gap-1 text-sm">
|
|
187
|
-
<button
|
|
188
|
-
type="button"
|
|
189
|
-
onClick={() => setTab("options")}
|
|
190
|
-
className={`rounded-md px-3 py-1.5 ${
|
|
191
|
-
tab === "options" ? "bg-muted font-medium" : "text-muted-foreground hover:text-foreground"
|
|
192
|
-
}`}
|
|
193
|
-
>
|
|
194
|
-
Tax options
|
|
195
|
-
</button>
|
|
196
|
-
<span className="text-muted-foreground">|</span>
|
|
197
|
-
{loading ? (
|
|
198
|
-
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
199
|
-
) : (
|
|
200
|
-
(classes || []).map((c) => (
|
|
201
|
-
<button
|
|
202
|
-
key={c.slug}
|
|
203
|
-
type="button"
|
|
204
|
-
onClick={() => setTab(c.slug)}
|
|
205
|
-
className={`rounded-md px-3 py-1.5 ${
|
|
206
|
-
tab === c.slug ? "bg-muted font-medium" : "text-muted-foreground hover:text-foreground"
|
|
207
|
-
}`}
|
|
208
|
-
>
|
|
209
|
-
{c.name} rates
|
|
210
|
-
</button>
|
|
211
|
-
))
|
|
212
|
-
)}
|
|
213
|
-
<Button variant="ghost" size="sm" className="text-muted-foreground" onClick={() => setAddOpen(true)}>
|
|
214
|
-
<Plus className="mr-1 h-4 w-4" />
|
|
215
|
-
Add class
|
|
216
|
-
</Button>
|
|
217
|
-
</div>
|
|
218
|
-
|
|
219
|
-
{tab === "options" ? (
|
|
220
|
-
<TaxOptions classes={classes} />
|
|
221
|
-
) : (
|
|
222
|
-
<div className="space-y-3">
|
|
223
|
-
{current && current.slug !== "standard" && (
|
|
224
|
-
<div className="flex justify-end">
|
|
225
|
-
<Button
|
|
226
|
-
variant="outline"
|
|
227
|
-
size="sm"
|
|
228
|
-
className="text-destructive"
|
|
229
|
-
onClick={() => setDeleteTarget(current)}
|
|
230
|
-
>
|
|
231
|
-
<Trash2 className="mr-1 h-4 w-4" />
|
|
232
|
-
Remove tax class
|
|
233
|
-
</Button>
|
|
234
|
-
</div>
|
|
235
|
-
)}
|
|
236
|
-
<TaxRatesTable taxClass={tab} />
|
|
237
|
-
</div>
|
|
238
|
-
)}
|
|
239
|
-
|
|
240
|
-
<Dialog open={addOpen} onOpenChange={setAddOpen}>
|
|
241
|
-
<DialogContent className="sm:max-w-sm">
|
|
242
|
-
<DialogHeader>
|
|
243
|
-
<DialogTitle>Add tax class</DialogTitle>
|
|
244
|
-
</DialogHeader>
|
|
245
|
-
<div className="space-y-1.5">
|
|
246
|
-
<Label>Class name</Label>
|
|
247
|
-
<Input
|
|
248
|
-
value={newName}
|
|
249
|
-
onChange={(e) => setNewName(e.target.value)}
|
|
250
|
-
placeholder="e.g. Reduced rate"
|
|
251
|
-
onKeyDown={(e) => e.key === "Enter" && createClass()}
|
|
252
|
-
/>
|
|
253
|
-
{newName.trim() && (
|
|
254
|
-
<p className="text-xs text-muted-foreground">Slug: {slugify(newName)}</p>
|
|
255
|
-
)}
|
|
256
|
-
</div>
|
|
257
|
-
<DialogFooter>
|
|
258
|
-
<Button variant="outline" onClick={() => setAddOpen(false)}>
|
|
259
|
-
Cancel
|
|
260
|
-
</Button>
|
|
261
|
-
<Button onClick={createClass} disabled={creating || !newName.trim()}>
|
|
262
|
-
{creating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
263
|
-
Add class
|
|
264
|
-
</Button>
|
|
265
|
-
</DialogFooter>
|
|
266
|
-
</DialogContent>
|
|
267
|
-
</Dialog>
|
|
268
|
-
|
|
269
|
-
<ConfirmDialog
|
|
270
|
-
open={!!deleteTarget}
|
|
271
|
-
onOpenChange={(v) => !v && setDeleteTarget(null)}
|
|
272
|
-
title={`Remove tax class "${deleteTarget?.name}"?`}
|
|
273
|
-
description="Products assigned to this class fall back to Standard. Existing tax rates for the class are not deleted."
|
|
274
|
-
confirmLabel="Remove class"
|
|
275
|
-
onConfirm={deleteClass}
|
|
276
|
-
loading={deleting}
|
|
277
|
-
/>
|
|
278
|
-
</div>
|
|
279
|
-
);
|
|
280
|
-
}
|