@base44/app-plugin-commerce 0.1.5 → 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 +84 -31
  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 +76 -41
  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 -191
  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,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&nbsp;code</TableHead>
133
- <TableHead className="min-w-20">State&nbsp;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&nbsp;%</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
- }