@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.
- package/README.md +8 -8
- package/base44/agents/commerce/StoreAdmin.jsonc +7 -7
- package/base44/entities/commerce.Order.jsonc +1 -1
- package/base44/entities/commerce.PaymentGateway.jsonc +3 -3
- package/base44/entities/commerce.Product.jsonc +9 -45
- package/base44/entities/commerce.ProductAttribute.jsonc +7 -19
- package/base44/entities/commerce.ProductAttributeTerm.jsonc +4 -10
- package/base44/entities/commerce.ProductTag.jsonc +1 -8
- package/base44/entities/commerce.ProductVariation.jsonc +1 -1
- package/base44/entities/commerce.StoreSettings.jsonc +1 -1
- package/base44/functions/commerce/admin-products/entry.ts +107 -35
- package/base44/functions/commerce/admin-refunds/entry.ts +1 -1
- package/base44/functions/commerce/admin-reports/entry.ts +1 -1
- package/base44/functions/commerce/admin-tools/entry.ts +4 -3
- package/base44/functions/commerce/payments/entry.ts +0 -2
- package/base44/functions/commerce/seed-store/defaults.ts +13 -39
- package/base44/functions/commerce/seed-store/entry.ts +47 -35
- package/base44/functions/commerce/seed-store/sample-data.ts +28 -66
- package/base44/functions/commerce/storefront-cart/entry.ts +0 -7
- package/base44/functions/commerce/storefront-catalog/entry.ts +12 -21
- package/base44/functions/commerce/storefront-checkout/entry.ts +5 -12
- package/base44/shared/commerce/coupons.ts +4 -7
- package/base44/shared/commerce/email-templates.ts +15 -13
- package/base44/shared/commerce/emails.ts +4 -3
- package/base44/shared/commerce/payments.ts +8 -20
- package/base44/shared/commerce/products.ts +24 -0
- package/base44/shared/commerce/settings.ts +2 -3
- package/base44/shared/commerce/stock.ts +2 -4
- package/base44/shared/commerce/totals.ts +37 -42
- package/package.json +1 -1
- package/scripts/install.js +21 -4
- package/skills/commerce/SKILL.md +6 -5
- package/skills/commerce/docs/api-admin.md +16 -14
- package/skills/commerce/docs/api-storefront.md +39 -35
- package/skills/commerce/installation-guidelines.md +4 -3
- package/skills/commerce/post-installation.md +22 -25
- package/skills/commerce/references/admin-product-form.md +56 -0
- package/skills/commerce/references/emails.md +4 -3
- package/skills/commerce/references/guest-access-security.md +2 -2
- package/skills/commerce/references/limits-and-performance.md +1 -1
- package/skills/commerce/references/online-payments.md +8 -8
- package/skills/commerce/references/product-render.md +25 -23
- package/skills/commerce/references/storefront-product-page.md +9 -9
- package/skills/commerce/references/webhooks.md +3 -1
- package/src/commerce/admin/README.md +4 -4
- package/src/commerce/admin/context/BasePathContext.jsx +5 -5
- package/src/commerce/admin/context/SettingsContext.jsx +4 -4
- package/src/commerce/admin/index.jsx +3 -3
- package/src/commerce/admin/layout/Sidebar.jsx +1 -9
- package/src/commerce/admin/lib/constants.js +0 -7
- package/src/commerce/admin/lib/paths.js +6 -6
- package/src/commerce/admin/lib/product-utils.js +18 -15
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +8 -8
- package/src/commerce/admin/pages/products/ProductEditor.jsx +10 -12
- package/src/commerce/admin/pages/products/ProductsList.jsx +3 -15
- package/src/commerce/admin/pages/products/components/AttributesSection.jsx +426 -0
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -69
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +1 -1
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +1 -14
- package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +19 -0
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +728 -0
- package/src/commerce/admin/pages/reports/Reports.jsx +3 -3
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +12 -14
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +6 -116
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +101 -62
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -6
- package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -1
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +3 -3
- package/src/commerce/admin/pages/status/Webhooks.jsx +2 -2
- package/src/commerce/admin/routes.jsx +10 -19
- package/src/commerce/utils/index.js +2 -2
- package/src/commerce/utils/shipping-promos.js +9 -6
- package/src/commerce/utils/variants.js +21 -21
- package/src/commerce/admin/pages/products/AttributeTerms.jsx +0 -180
- package/src/commerce/admin/pages/products/Attributes.jsx +0 -183
- package/src/commerce/admin/pages/products/Tags.jsx +0 -150
- package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +0 -48
- package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +0 -208
- package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +0 -41
- package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +0 -103
- package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +0 -93
- package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +0 -86
- 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
|
-
}
|