@base44/app-plugin-commerce 0.1.3 → 0.1.4

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 (83) hide show
  1. package/README.md +8 -8
  2. package/base44/agents/commerce/StoreAdmin.jsonc +7 -7
  3. package/base44/entities/commerce.Order.jsonc +1 -1
  4. package/base44/entities/commerce.PaymentGateway.jsonc +3 -3
  5. package/base44/entities/commerce.Product.jsonc +9 -45
  6. package/base44/entities/commerce.ProductAttribute.jsonc +7 -19
  7. package/base44/entities/commerce.ProductAttributeTerm.jsonc +4 -10
  8. package/base44/entities/commerce.ProductTag.jsonc +1 -8
  9. package/base44/entities/commerce.ProductVariation.jsonc +1 -1
  10. package/base44/entities/commerce.StoreSettings.jsonc +1 -1
  11. package/base44/functions/commerce/admin-products/entry.ts +107 -35
  12. package/base44/functions/commerce/admin-refunds/entry.ts +1 -1
  13. package/base44/functions/commerce/admin-reports/entry.ts +1 -1
  14. package/base44/functions/commerce/admin-tools/entry.ts +4 -3
  15. package/base44/functions/commerce/payments/entry.ts +0 -2
  16. package/base44/functions/commerce/seed-store/defaults.ts +13 -39
  17. package/base44/functions/commerce/seed-store/entry.ts +47 -35
  18. package/base44/functions/commerce/seed-store/sample-data.ts +28 -66
  19. package/base44/functions/commerce/storefront-cart/entry.ts +0 -7
  20. package/base44/functions/commerce/storefront-catalog/entry.ts +12 -21
  21. package/base44/functions/commerce/storefront-checkout/entry.ts +5 -12
  22. package/base44/shared/commerce/coupons.ts +4 -7
  23. package/base44/shared/commerce/email-templates.ts +15 -13
  24. package/base44/shared/commerce/emails.ts +4 -3
  25. package/base44/shared/commerce/payments.ts +8 -20
  26. package/base44/shared/commerce/products.ts +24 -0
  27. package/base44/shared/commerce/settings.ts +2 -3
  28. package/base44/shared/commerce/stock.ts +2 -4
  29. package/base44/shared/commerce/totals.ts +37 -42
  30. package/package.json +1 -1
  31. package/scripts/install.js +21 -4
  32. package/skills/commerce/SKILL.md +6 -5
  33. package/skills/commerce/docs/api-admin.md +16 -14
  34. package/skills/commerce/docs/api-storefront.md +39 -35
  35. package/skills/commerce/installation-guidelines.md +4 -3
  36. package/skills/commerce/post-installation.md +22 -25
  37. package/skills/commerce/references/admin-product-form.md +56 -0
  38. package/skills/commerce/references/emails.md +4 -3
  39. package/skills/commerce/references/guest-access-security.md +2 -2
  40. package/skills/commerce/references/limits-and-performance.md +1 -1
  41. package/skills/commerce/references/online-payments.md +8 -8
  42. package/skills/commerce/references/product-render.md +25 -23
  43. package/skills/commerce/references/storefront-product-page.md +9 -9
  44. package/skills/commerce/references/webhooks.md +3 -1
  45. package/src/commerce/admin/README.md +4 -4
  46. package/src/commerce/admin/context/BasePathContext.jsx +5 -5
  47. package/src/commerce/admin/context/SettingsContext.jsx +4 -4
  48. package/src/commerce/admin/index.jsx +3 -3
  49. package/src/commerce/admin/layout/Sidebar.jsx +1 -9
  50. package/src/commerce/admin/lib/constants.js +0 -7
  51. package/src/commerce/admin/lib/paths.js +6 -6
  52. package/src/commerce/admin/lib/product-utils.js +18 -15
  53. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +8 -8
  54. package/src/commerce/admin/pages/products/ProductEditor.jsx +10 -12
  55. package/src/commerce/admin/pages/products/ProductsList.jsx +3 -15
  56. package/src/commerce/admin/pages/products/components/AttributesSection.jsx +426 -0
  57. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -69
  58. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +1 -1
  59. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +1 -14
  60. package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +19 -0
  61. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +728 -0
  62. package/src/commerce/admin/pages/reports/Reports.jsx +3 -3
  63. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +12 -14
  64. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +6 -116
  65. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +101 -62
  66. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -6
  67. package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -1
  68. package/src/commerce/admin/pages/status/WebhookEditor.jsx +3 -3
  69. package/src/commerce/admin/pages/status/Webhooks.jsx +2 -2
  70. package/src/commerce/admin/routes.jsx +10 -19
  71. package/src/commerce/utils/index.js +2 -2
  72. package/src/commerce/utils/shipping-promos.js +9 -6
  73. package/src/commerce/utils/variants.js +21 -21
  74. package/src/commerce/admin/pages/products/AttributeTerms.jsx +0 -180
  75. package/src/commerce/admin/pages/products/Attributes.jsx +0 -183
  76. package/src/commerce/admin/pages/products/Tags.jsx +0 -150
  77. package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +0 -48
  78. package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +0 -208
  79. package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +0 -41
  80. package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +0 -103
  81. package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +0 -93
  82. package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +0 -86
  83. package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +0 -377
@@ -1,208 +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 { Label } from "@/components/ui/label";
5
- import { Badge } from "@/components/ui/badge";
6
- import { Checkbox } from "@/components/ui/checkbox";
7
- import { Card } from "@/components/ui/card";
8
- import {
9
- Select,
10
- SelectContent,
11
- SelectItem,
12
- SelectTrigger,
13
- SelectValue,
14
- } from "@/components/ui/select";
15
- import { Globe, Plus, Trash2 } from "lucide-react";
16
-
17
- import { base44 } from "../../../../lib/api";
18
- import SearchSelect from "../../../../components/SearchSelect";
19
- import { isVariable } from "../../../../lib/product-utils";
20
-
21
- const CUSTOM = "__custom__";
22
-
23
- /**
24
- * Assign global attributes (with term multi-select) or ad-hoc custom
25
- * attributes ("Red | Blue | Green"). `variation: true` rows drive the
26
- * Variations tab for variable products.
27
- */
28
- export default function AttributesTab({ product, up, refs }) {
29
- const globalAttributes = refs.attributes || [];
30
- const attributes = product.attributes || [];
31
- const [addValue, setAddValue] = useState("");
32
- const [termsCache, setTermsCache] = useState({}); // attribute_id -> [{id, name}]
33
-
34
- // Load terms for every global attribute in use.
35
- useEffect(() => {
36
- const wanted = attributes.map((a) => a.attribute_id).filter(Boolean);
37
- const missing = wanted.filter((id) => !termsCache[id]);
38
- if (!missing.length) return;
39
- let cancelled = false;
40
- Promise.all(
41
- missing.map((id) =>
42
- base44.entities["commerce.ProductAttributeTerm"].filter({ attribute_id: id }, "menu_order", 500)
43
- .then((terms) => [id, terms || []])
44
- .catch(() => [id, []])
45
- )
46
- ).then((entries) => {
47
- if (!cancelled) setTermsCache((c) => ({ ...c, ...Object.fromEntries(entries) }));
48
- });
49
- return () => {
50
- cancelled = true;
51
- };
52
- // eslint-disable-next-line -- deps intentionally partial
53
- }, [JSON.stringify(attributes.map((a) => a.attribute_id))]);
54
-
55
- const setRow = (index, patch) => {
56
- const next = attributes.map((a, i) => (i === index ? { ...a, ...patch } : a));
57
- up({ attributes: next });
58
- };
59
-
60
- const removeRow = (index) => {
61
- up({ attributes: attributes.filter((_, i) => i !== index) });
62
- };
63
-
64
- const addAttribute = () => {
65
- if (!addValue) return;
66
- if (addValue === CUSTOM) {
67
- up({
68
- attributes: [
69
- ...attributes,
70
- { attribute_id: "", name: "", position: attributes.length, visible: true, variation: false, options: [] },
71
- ],
72
- });
73
- } else {
74
- const attr = globalAttributes.find((a) => a.id === addValue);
75
- if (!attr || attributes.some((a) => a.attribute_id === attr.id)) return;
76
- up({
77
- attributes: [
78
- ...attributes,
79
- {
80
- attribute_id: attr.id,
81
- name: attr.name,
82
- position: attributes.length,
83
- visible: true,
84
- variation: false,
85
- options: [],
86
- },
87
- ],
88
- });
89
- }
90
- setAddValue("");
91
- };
92
-
93
- return (
94
- <div className="space-y-4">
95
- <div className="flex items-end gap-2">
96
- <div className="space-y-1.5">
97
- <Label>Add attribute</Label>
98
- <Select value={addValue} onValueChange={setAddValue}>
99
- <SelectTrigger className="w-64">
100
- <SelectValue placeholder="Select an attribute…" />
101
- </SelectTrigger>
102
- <SelectContent>
103
- <SelectItem value={CUSTOM}>Custom product attribute</SelectItem>
104
- {globalAttributes
105
- .filter((a) => !attributes.some((row) => row.attribute_id === a.id))
106
- .map((a) => (
107
- <SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>
108
- ))}
109
- </SelectContent>
110
- </Select>
111
- </div>
112
- <Button type="button" variant="outline" onClick={addAttribute} disabled={!addValue}>
113
- <Plus className="mr-1 h-4 w-4" /> Add
114
- </Button>
115
- </div>
116
-
117
- {attributes.length === 0 && (
118
- <p className="text-sm text-muted-foreground">
119
- No attributes yet. Attributes describe the product (e.g. Color, Size) and can power variations.
120
- </p>
121
- )}
122
-
123
- <div className="space-y-3">
124
- {attributes.map((row, i) => {
125
- const isGlobal = Boolean(row.attribute_id);
126
- const terms = termsCache[row.attribute_id] || [];
127
- return (
128
- <Card key={`${row.attribute_id || "custom"}-${i}`} className="p-3">
129
- <div className="flex items-start justify-between gap-2">
130
- <div className="min-w-0 flex-1 space-y-3">
131
- <div className="flex items-center gap-2">
132
- {isGlobal ? (
133
- <>
134
- <span className="font-medium">{row.name}</span>
135
- <Badge variant="secondary" className="gap-1">
136
- <Globe className="h-3 w-3" /> Global
137
- </Badge>
138
- </>
139
- ) : (
140
- <Input
141
- className="h-8 w-56"
142
- placeholder="Attribute name (e.g. Material)"
143
- value={row.name}
144
- onChange={(e) => setRow(i, { name: e.target.value })}
145
- />
146
- )}
147
- </div>
148
-
149
- {isGlobal ? (
150
- <SearchSelect
151
- multiple
152
- placeholder="Select terms…"
153
- value={(row.options || []).map((o) => ({ value: o, label: o }))}
154
- onChange={(next) => setRow(i, { options: next.map((o) => o.value) })}
155
- search={async (q) =>
156
- terms
157
- .filter((t) => !q || t.name.toLowerCase().includes(q.toLowerCase()))
158
- .map((t) => ({ value: t.name, label: t.name }))
159
- }
160
- />
161
- ) : (
162
- <div className="space-y-1">
163
- <Input
164
- placeholder="Values separated by | (e.g. Cotton | Wool | Silk)"
165
- value={(row.options || []).join(" | ")}
166
- onChange={(e) =>
167
- setRow(i, {
168
- options: e.target.value
169
- .split("|")
170
- .map((s) => s.trim())
171
- .filter(Boolean),
172
- })
173
- }
174
- />
175
- <p className="text-xs text-muted-foreground">Separate values with “|”.</p>
176
- </div>
177
- )}
178
-
179
- <div className="flex items-center gap-4 text-sm">
180
- <label className="flex items-center gap-1.5">
181
- <Checkbox
182
- checked={row.visible !== false}
183
- onCheckedChange={(v) => setRow(i, { visible: Boolean(v) })}
184
- />
185
- Visible on the product page
186
- </label>
187
- {isVariable(product) && (
188
- <label className="flex items-center gap-1.5">
189
- <Checkbox
190
- checked={Boolean(row.variation)}
191
- onCheckedChange={(v) => setRow(i, { variation: Boolean(v) })}
192
- />
193
- Used for variations
194
- </label>
195
- )}
196
- </div>
197
- </div>
198
- <Button type="button" variant="ghost" size="icon" onClick={() => removeRow(i)}>
199
- <Trash2 className="h-4 w-4 text-muted-foreground" />
200
- </Button>
201
- </div>
202
- </Card>
203
- );
204
- })}
205
- </div>
206
- </div>
207
- );
208
- }
@@ -1,41 +0,0 @@
1
- import React from "react";
2
- import { Input } from "@/components/ui/input";
3
- import { Label } from "@/components/ui/label";
4
-
5
- import MoneyInput from "../../../../components/MoneyInput";
6
-
7
- /** External/affiliate product: outbound URL + button text + pricing. */
8
- export default function ExternalTab({ product, up }) {
9
- return (
10
- <div className="max-w-md space-y-4">
11
- <div className="space-y-1.5">
12
- <Label>Product URL</Label>
13
- <Input
14
- type="url"
15
- placeholder="https://example.com/product"
16
- value={product.external_url || ""}
17
- onChange={(e) => up({ external_url: e.target.value })}
18
- />
19
- <p className="text-xs text-muted-foreground">Where customers are sent to buy this product.</p>
20
- </div>
21
- <div className="space-y-1.5">
22
- <Label>Button text</Label>
23
- <Input
24
- placeholder="Buy product"
25
- value={product.button_text || ""}
26
- onChange={(e) => up({ button_text: e.target.value })}
27
- />
28
- </div>
29
- <div className="grid grid-cols-2 gap-2">
30
- <div className="space-y-1.5">
31
- <Label>Regular price</Label>
32
- <MoneyInput value={product.regular_price} onChange={(v) => up({ regular_price: v })} />
33
- </div>
34
- <div className="space-y-1.5">
35
- <Label>Sale price</Label>
36
- <MoneyInput value={product.sale_price} onChange={(v) => up({ sale_price: v })} />
37
- </div>
38
- </div>
39
- </div>
40
- );
41
- }
@@ -1,103 +0,0 @@
1
- import React from "react";
2
- import { Input } from "@/components/ui/input";
3
- import { Label } from "@/components/ui/label";
4
- import { Switch } from "@/components/ui/switch";
5
- import {
6
- Select,
7
- SelectContent,
8
- SelectItem,
9
- SelectTrigger,
10
- SelectValue,
11
- } from "@/components/ui/select";
12
-
13
- import MoneyInput from "../../../../components/MoneyInput";
14
- import { TAX_STATUSES } from "../../../../lib/constants";
15
- import { isVariable } from "../../../../lib/product-utils";
16
-
17
- /** Convert an ISO datetime to the value a date input expects (yyyy-mm-dd). */
18
- const toDateInput = (iso) => (iso ? String(iso).slice(0, 10) : "");
19
- const fromDateInput = (v) => (v ? new Date(`${v}T00:00:00Z`).toISOString() : null);
20
-
21
- /** Pricing + tax. For variable products only tax fields apply (prices per variation). */
22
- export default function GeneralTab({ product, up, refs }) {
23
- const scheduled = Boolean(product.date_on_sale_from || product.date_on_sale_to);
24
- const taxClasses = refs.taxClasses || [];
25
-
26
- return (
27
- <div className="max-w-md space-y-4">
28
- {isVariable(product) ? (
29
- <p className="text-sm text-muted-foreground">
30
- Prices for variable products are set per variation on the Variations tab.
31
- </p>
32
- ) : (
33
- <>
34
- <div className="space-y-1.5">
35
- <Label>Regular price</Label>
36
- <MoneyInput
37
- value={product.regular_price}
38
- onChange={(v) => up({ regular_price: v })}
39
- />
40
- </div>
41
- <div className="space-y-1.5">
42
- <Label>Sale price</Label>
43
- <MoneyInput value={product.sale_price} onChange={(v) => up({ sale_price: v })} />
44
- </div>
45
- <div className="flex items-center gap-2">
46
- <Switch
47
- checked={scheduled}
48
- onCheckedChange={(v) =>
49
- up(v ? { date_on_sale_from: new Date().toISOString() } : { date_on_sale_from: null, date_on_sale_to: null })
50
- }
51
- />
52
- <Label>Schedule sale</Label>
53
- </div>
54
- {scheduled && (
55
- <div className="grid grid-cols-2 gap-2">
56
- <div className="space-y-1.5">
57
- <Label className="text-xs">Sale start</Label>
58
- <Input
59
- type="date"
60
- value={toDateInput(product.date_on_sale_from)}
61
- onChange={(e) => up({ date_on_sale_from: fromDateInput(e.target.value) })}
62
- />
63
- </div>
64
- <div className="space-y-1.5">
65
- <Label className="text-xs">Sale end</Label>
66
- <Input
67
- type="date"
68
- value={toDateInput(product.date_on_sale_to)}
69
- onChange={(e) => up({ date_on_sale_to: fromDateInput(e.target.value) })}
70
- />
71
- </div>
72
- </div>
73
- )}
74
- </>
75
- )}
76
-
77
- <div className="grid grid-cols-2 gap-2">
78
- <div className="space-y-1.5">
79
- <Label>Tax status</Label>
80
- <Select value={product.tax_status} onValueChange={(v) => up({ tax_status: v })}>
81
- <SelectTrigger><SelectValue /></SelectTrigger>
82
- <SelectContent>
83
- {TAX_STATUSES.map((t) => (
84
- <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
85
- ))}
86
- </SelectContent>
87
- </Select>
88
- </div>
89
- <div className="space-y-1.5">
90
- <Label>Tax class</Label>
91
- <Select value={product.tax_class || "standard"} onValueChange={(v) => up({ tax_class: v })}>
92
- <SelectTrigger><SelectValue /></SelectTrigger>
93
- <SelectContent>
94
- {(taxClasses.length ? taxClasses : [{ slug: "standard", name: "Standard" }]).map((c) => (
95
- <SelectItem key={c.slug} value={c.slug}>{c.name}</SelectItem>
96
- ))}
97
- </SelectContent>
98
- </Select>
99
- </div>
100
- </div>
101
- </div>
102
- );
103
- }
@@ -1,93 +0,0 @@
1
- import React from "react";
2
- import { Input } from "@/components/ui/input";
3
- import { Label } from "@/components/ui/label";
4
- import { Switch } from "@/components/ui/switch";
5
- import { Checkbox } from "@/components/ui/checkbox";
6
- import {
7
- Select,
8
- SelectContent,
9
- SelectItem,
10
- SelectTrigger,
11
- SelectValue,
12
- } from "@/components/ui/select";
13
-
14
- import { BACKORDER_OPTIONS, STOCK_STATUSES } from "../../../../lib/constants";
15
-
16
- const num = (v) => (v === "" ? null : Number(v));
17
-
18
- /** SKU + stock management. */
19
- export default function InventoryTab({ product, up }) {
20
- return (
21
- <div className="max-w-md space-y-4">
22
- <div className="space-y-1.5">
23
- <Label>SKU</Label>
24
- <Input
25
- placeholder="Unique stock keeping unit"
26
- value={product.sku || ""}
27
- onChange={(e) => up({ sku: e.target.value })}
28
- />
29
- </div>
30
-
31
- <div className="flex items-center gap-2">
32
- <Switch
33
- checked={product.manage_stock}
34
- onCheckedChange={(v) => up({ manage_stock: Boolean(v) })}
35
- />
36
- <Label>Track stock quantity for this product</Label>
37
- </div>
38
-
39
- {product.manage_stock ? (
40
- <>
41
- <div className="space-y-1.5">
42
- <Label>Stock quantity</Label>
43
- <Input
44
- type="number"
45
- value={product.stock_quantity ?? ""}
46
- onChange={(e) => up({ stock_quantity: num(e.target.value) })}
47
- />
48
- </div>
49
- <div className="space-y-1.5">
50
- <Label>Allow backorders?</Label>
51
- <Select value={product.backorders || "no"} onValueChange={(v) => up({ backorders: v })}>
52
- <SelectTrigger><SelectValue /></SelectTrigger>
53
- <SelectContent>
54
- {BACKORDER_OPTIONS.map((o) => (
55
- <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
56
- ))}
57
- </SelectContent>
58
- </Select>
59
- </div>
60
- <div className="space-y-1.5">
61
- <Label>Low stock threshold</Label>
62
- <Input
63
- type="number"
64
- placeholder="Store default"
65
- value={product.low_stock_amount ?? ""}
66
- onChange={(e) => up({ low_stock_amount: num(e.target.value) })}
67
- />
68
- </div>
69
- </>
70
- ) : (
71
- <div className="space-y-1.5">
72
- <Label>Stock status</Label>
73
- <Select value={product.stock_status || "instock"} onValueChange={(v) => up({ stock_status: v })}>
74
- <SelectTrigger><SelectValue /></SelectTrigger>
75
- <SelectContent>
76
- {STOCK_STATUSES.map((s) => (
77
- <SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
78
- ))}
79
- </SelectContent>
80
- </Select>
81
- </div>
82
- )}
83
-
84
- <label className="flex items-center gap-2 text-sm">
85
- <Checkbox
86
- checked={product.sold_individually}
87
- onCheckedChange={(v) => up({ sold_individually: Boolean(v) })}
88
- />
89
- Limit purchases to 1 item per order
90
- </label>
91
- </div>
92
- );
93
- }
@@ -1,86 +0,0 @@
1
- import React from "react";
2
- import { Input } from "@/components/ui/input";
3
- import { Label } from "@/components/ui/label";
4
- import {
5
- Select,
6
- SelectContent,
7
- SelectItem,
8
- SelectTrigger,
9
- SelectValue,
10
- } from "@/components/ui/select";
11
-
12
- import { useSettings } from "../../../../context/SettingsContext";
13
-
14
- const num = (v) => (v === "" ? null : Number(v));
15
- const NONE = "__none__";
16
-
17
- /** Weight, dimensions and shipping class. Hidden for virtual/external products. */
18
- export default function ShippingTab({ product, up, refs }) {
19
- const settings = useSettings();
20
- const weightUnit = settings?.get("products", "weight_unit", "kg");
21
- const dimensionUnit = settings?.get("products", "dimension_unit", "cm");
22
- const dims = product.dimensions || {};
23
- const classes = refs.shippingClasses || [];
24
-
25
- const setDim = (key, value) => up({ dimensions: { ...dims, [key]: num(value) } });
26
-
27
- return (
28
- <div className="max-w-md space-y-4">
29
- <div className="space-y-1.5">
30
- <Label>Weight ({weightUnit})</Label>
31
- <Input
32
- type="number"
33
- step="any"
34
- value={product.weight ?? ""}
35
- onChange={(e) => up({ weight: num(e.target.value) })}
36
- />
37
- </div>
38
-
39
- <div className="space-y-1.5">
40
- <Label>Dimensions ({dimensionUnit})</Label>
41
- <div className="grid grid-cols-3 gap-2">
42
- <Input
43
- type="number"
44
- step="any"
45
- placeholder="Length"
46
- value={dims.length ?? ""}
47
- onChange={(e) => setDim("length", e.target.value)}
48
- />
49
- <Input
50
- type="number"
51
- step="any"
52
- placeholder="Width"
53
- value={dims.width ?? ""}
54
- onChange={(e) => setDim("width", e.target.value)}
55
- />
56
- <Input
57
- type="number"
58
- step="any"
59
- placeholder="Height"
60
- value={dims.height ?? ""}
61
- onChange={(e) => setDim("height", e.target.value)}
62
- />
63
- </div>
64
- </div>
65
-
66
- <div className="space-y-1.5">
67
- <Label>Shipping class</Label>
68
- <Select
69
- value={product.shipping_class_id || NONE}
70
- onValueChange={(v) => up({ shipping_class_id: v === NONE ? "" : v })}
71
- >
72
- <SelectTrigger><SelectValue /></SelectTrigger>
73
- <SelectContent>
74
- <SelectItem value={NONE}>No shipping class</SelectItem>
75
- {classes.map((c) => (
76
- <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
77
- ))}
78
- </SelectContent>
79
- </Select>
80
- <p className="text-xs text-muted-foreground">
81
- Shipping classes are used by some shipping methods to group similar products.
82
- </p>
83
- </div>
84
- </div>
85
- );
86
- }