@base44/app-plugin-commerce 0.1.6 → 0.1.8
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 +12 -12
- 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 +5 -6
- 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,232 +1,202 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
2
|
import { Button } from "@/components/ui/button";
|
|
3
3
|
import { Input } from "@/components/ui/input";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
7
|
-
import {
|
|
8
|
-
Select,
|
|
9
|
-
SelectContent,
|
|
10
|
-
SelectItem,
|
|
11
|
-
SelectTrigger,
|
|
12
|
-
SelectValue,
|
|
13
|
-
} from "@/components/ui/select";
|
|
14
|
-
import { FolderTree, ImageIcon, Loader2, Pencil, Trash2 } from "lucide-react";
|
|
4
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
5
|
+
import { FolderTree, Loader2, Plus, Trash2 } from "lucide-react";
|
|
15
6
|
import { toast } from "sonner";
|
|
16
7
|
|
|
17
|
-
import { base44 } from "../../lib/api";
|
|
8
|
+
import { base44, call } from "../../lib/api";
|
|
18
9
|
import useAsync from "../../hooks/useAsync";
|
|
19
10
|
import PageHeader from "../../components/PageHeader";
|
|
20
|
-
import DataTable from "../../components/DataTable";
|
|
21
11
|
import ConfirmDialog from "../../components/ConfirmDialog";
|
|
22
|
-
import MediaUploader from "../../components/MediaUploader";
|
|
23
|
-
import { slugify } from "../../lib/product-utils";
|
|
24
|
-
|
|
25
|
-
const NO_PARENT = "__none__";
|
|
26
|
-
const BLANK = { id: null, name: "", slug: "", parent_id: "", description: "", image: null };
|
|
27
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Categories as an editable list of name / slug / order rows. `+ Add category`
|
|
15
|
+
* appends a fresh row, and each row can be removed (with a warning). Rows save
|
|
16
|
+
* on blur/Enter through admin-products `save-term`. A blank slug is derived
|
|
17
|
+
* from the name (and uniquified) server-side; a filled one is kept — which is
|
|
18
|
+
* why the row always sends the stored slug back: otherwise a rename would
|
|
19
|
+
* silently regenerate it and break storefront links.
|
|
20
|
+
*/
|
|
28
21
|
export default function Categories() {
|
|
29
22
|
const { data: categories, loading, refetch } = useAsync(
|
|
30
23
|
() => base44.entities["commerce.ProductCategory"].list("menu_order", 1000),
|
|
31
24
|
[]
|
|
32
25
|
);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
|
|
27
|
+
// rows: saved categories + any unsaved new lines (id: null). description and
|
|
28
|
+
// parent_id ride along untouched — save-term writes them, so omitting them
|
|
29
|
+
// would wipe what's stored.
|
|
30
|
+
const [rows, setRows] = useState([]);
|
|
31
|
+
const [savingId, setSavingId] = useState(null);
|
|
36
32
|
const [confirmDelete, setConfirmDelete] = useState(null);
|
|
37
33
|
const [deleting, setDeleting] = useState(false);
|
|
38
34
|
|
|
39
|
-
const
|
|
35
|
+
const fromCategory = (c) => ({
|
|
36
|
+
id: c.id,
|
|
37
|
+
name: c.name || "",
|
|
38
|
+
slug: c.slug || "",
|
|
39
|
+
menu_order: c.menu_order ?? 0,
|
|
40
|
+
description: c.description ?? "",
|
|
41
|
+
parent_id: c.parent_id ?? "",
|
|
42
|
+
saved: { name: c.name || "", slug: c.slug || "", menu_order: c.menu_order ?? 0 },
|
|
43
|
+
});
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const key = c.parent_id || "";
|
|
46
|
-
if (!byParent.has(key)) byParent.set(key, []);
|
|
47
|
-
byParent.get(key).push(c);
|
|
48
|
-
}
|
|
49
|
-
const out = [];
|
|
50
|
-
const walk = (parentId, depth) => {
|
|
51
|
-
for (const c of byParent.get(parentId) || []) {
|
|
52
|
-
out.push({ ...c, depth });
|
|
53
|
-
walk(c.id, depth + 1);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
walk("", 0);
|
|
57
|
-
return out;
|
|
58
|
-
}, [list]);
|
|
59
|
-
|
|
60
|
-
const edit = (c) => {
|
|
61
|
-
setForm({
|
|
62
|
-
id: c.id,
|
|
63
|
-
name: c.name || "",
|
|
64
|
-
slug: c.slug || "",
|
|
65
|
-
parent_id: c.parent_id || "",
|
|
66
|
-
description: c.description || "",
|
|
67
|
-
image: c.image || null,
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
setRows((prev) => {
|
|
47
|
+
const drafts = prev.filter((r) => !r.id);
|
|
48
|
+
return [...(categories || []).map(fromCategory), ...drafts];
|
|
68
49
|
});
|
|
69
|
-
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const reset = () => {
|
|
73
|
-
setForm({ ...BLANK });
|
|
74
|
-
setSlugTouched(false);
|
|
75
|
-
};
|
|
50
|
+
}, [categories]);
|
|
76
51
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
};
|
|
52
|
+
const setField = (index, key, value) =>
|
|
53
|
+
setRows((prev) => prev.map((r, i) => (i === index ? { ...r, [key]: value } : r)));
|
|
80
54
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
55
|
+
const commit = async (index) => {
|
|
56
|
+
const row = rows[index];
|
|
57
|
+
const name = row.name.trim();
|
|
58
|
+
const slug = String(row.slug ?? "").trim();
|
|
59
|
+
const menuOrder = Number(row.menu_order) || 0;
|
|
60
|
+
if (!name) {
|
|
61
|
+
if (!row.id) setRows((prev) => prev.filter((_, i) => i !== index)); // abandoned empty line
|
|
84
62
|
return;
|
|
85
63
|
}
|
|
86
|
-
|
|
64
|
+
const dirty =
|
|
65
|
+
!row.id ||
|
|
66
|
+
name !== row.saved.name ||
|
|
67
|
+
slug !== row.saved.slug ||
|
|
68
|
+
menuOrder !== row.saved.menu_order;
|
|
69
|
+
if (!dirty) return;
|
|
70
|
+
setSavingId(row.id || `new-${index}`);
|
|
87
71
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
72
|
+
await call("admin-products", "save-term", {
|
|
73
|
+
taxonomy: "category",
|
|
74
|
+
term: {
|
|
75
|
+
...(row.id ? { id: row.id } : {}),
|
|
76
|
+
name,
|
|
77
|
+
slug, // blank = derive from the name server-side
|
|
78
|
+
menu_order: menuOrder,
|
|
79
|
+
description: row.description,
|
|
80
|
+
parent_id: row.parent_id,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
toast.success(row.id ? "Category saved" : "Category added");
|
|
99
84
|
refetch();
|
|
100
|
-
} catch
|
|
101
|
-
toast
|
|
85
|
+
} catch {
|
|
86
|
+
/* toast handled by call() */
|
|
102
87
|
} finally {
|
|
103
|
-
|
|
88
|
+
setSavingId(null);
|
|
104
89
|
}
|
|
105
90
|
};
|
|
106
91
|
|
|
107
92
|
const doDelete = async () => {
|
|
108
93
|
setDeleting(true);
|
|
109
94
|
try {
|
|
110
|
-
await
|
|
95
|
+
await call("admin-products", "delete-term", { taxonomy: "category", id: confirmDelete.id });
|
|
111
96
|
toast.success("Category deleted");
|
|
112
|
-
if (form.id === confirmDelete.id) reset();
|
|
113
97
|
setConfirmDelete(null);
|
|
114
98
|
refetch();
|
|
115
|
-
} catch
|
|
116
|
-
toast
|
|
99
|
+
} catch {
|
|
100
|
+
/* toast handled by call() */
|
|
117
101
|
} finally {
|
|
118
102
|
setDeleting(false);
|
|
119
103
|
}
|
|
120
104
|
};
|
|
121
105
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<div className="flex h-9 w-9 items-center justify-center rounded bg-muted">
|
|
132
|
-
<ImageIcon className="h-4 w-4 text-muted-foreground" />
|
|
133
|
-
</div>
|
|
134
|
-
),
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
key: "name",
|
|
138
|
-
label: "Name",
|
|
139
|
-
render: (row) => (
|
|
140
|
-
<span style={{ paddingLeft: `${row.depth * 16}px` }} className="font-medium">
|
|
141
|
-
{row.depth > 0 && <span className="text-muted-foreground">— </span>}
|
|
142
|
-
{row.name}
|
|
143
|
-
</span>
|
|
144
|
-
),
|
|
145
|
-
},
|
|
146
|
-
{ key: "slug", label: "Slug", className: "hidden md:table-cell text-muted-foreground" },
|
|
147
|
-
];
|
|
106
|
+
const removeRow = (index) => {
|
|
107
|
+
const row = rows[index];
|
|
108
|
+
if (!row.id) setRows((prev) => prev.filter((_, i) => i !== index)); // unsaved line — just drop it
|
|
109
|
+
else setConfirmDelete(row);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const blurOnEnter = (e) => {
|
|
113
|
+
if (e.key === "Enter") e.currentTarget.blur();
|
|
114
|
+
};
|
|
148
115
|
|
|
149
116
|
return (
|
|
150
117
|
<div className="space-y-4">
|
|
151
|
-
<PageHeader title="Categories" description="Organize products into
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<div className="
|
|
159
|
-
<
|
|
160
|
-
<
|
|
118
|
+
<PageHeader title="Categories" description="Organize products into categories" />
|
|
119
|
+
|
|
120
|
+
<Card className="max-w-3xl">
|
|
121
|
+
<CardContent className="space-y-2 py-4">
|
|
122
|
+
{loading && !rows.length && <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />}
|
|
123
|
+
|
|
124
|
+
{!loading && !rows.length && (
|
|
125
|
+
<div className="flex items-center gap-3 py-6 text-muted-foreground">
|
|
126
|
+
<FolderTree className="h-5 w-5" />
|
|
127
|
+
<span className="text-sm">No categories yet — add the first one below.</span>
|
|
161
128
|
</div>
|
|
162
|
-
|
|
163
|
-
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{rows.length > 0 && (
|
|
132
|
+
<div className="grid grid-cols-[1fr_1fr_5rem_2.25rem] items-center gap-2 px-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
133
|
+
<span>Name</span>
|
|
134
|
+
<span>Slug</span>
|
|
135
|
+
<span>Order</span>
|
|
136
|
+
<span />
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{rows.map((row, i) => (
|
|
141
|
+
<div key={row.id || `new-${i}`} className="grid grid-cols-[1fr_1fr_5rem_2.25rem] items-center gap-2">
|
|
164
142
|
<Input
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
143
|
+
autoFocus={!row.id && !row.name}
|
|
144
|
+
placeholder="Category name"
|
|
145
|
+
value={row.name}
|
|
146
|
+
onChange={(e) => setField(i, "name", e.target.value)}
|
|
147
|
+
onBlur={() => commit(i)}
|
|
148
|
+
onKeyDown={blurOnEnter}
|
|
170
149
|
/>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
179
|
-
<SelectContent>
|
|
180
|
-
<SelectItem value={NO_PARENT}>— None —</SelectItem>
|
|
181
|
-
{list
|
|
182
|
-
.filter((c) => c.id !== form.id)
|
|
183
|
-
.map((c) => (
|
|
184
|
-
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
|
|
185
|
-
))}
|
|
186
|
-
</SelectContent>
|
|
187
|
-
</Select>
|
|
188
|
-
</div>
|
|
189
|
-
<div className="space-y-1.5">
|
|
190
|
-
<Label>Description</Label>
|
|
191
|
-
<Textarea
|
|
192
|
-
rows={3}
|
|
193
|
-
value={form.description}
|
|
194
|
-
onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))}
|
|
150
|
+
<Input
|
|
151
|
+
placeholder="auto from name"
|
|
152
|
+
className="font-mono text-xs lowercase"
|
|
153
|
+
value={row.slug}
|
|
154
|
+
onChange={(e) => setField(i, "slug", e.target.value)}
|
|
155
|
+
onBlur={() => commit(i)}
|
|
156
|
+
onKeyDown={blurOnEnter}
|
|
195
157
|
/>
|
|
158
|
+
<Input
|
|
159
|
+
type="number"
|
|
160
|
+
className="text-right"
|
|
161
|
+
value={row.menu_order}
|
|
162
|
+
onChange={(e) => setField(i, "menu_order", e.target.value === "" ? 0 : Number(e.target.value))}
|
|
163
|
+
onBlur={() => commit(i)}
|
|
164
|
+
onKeyDown={blurOnEnter}
|
|
165
|
+
/>
|
|
166
|
+
<span className="flex items-center justify-end">
|
|
167
|
+
{savingId === (row.id || `new-${i}`) ? (
|
|
168
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
169
|
+
) : (
|
|
170
|
+
<Button type="button" variant="ghost" size="icon" onClick={() => removeRow(i)}>
|
|
171
|
+
<Trash2 className="h-4 w-4 text-muted-foreground" />
|
|
172
|
+
</Button>
|
|
173
|
+
)}
|
|
174
|
+
</span>
|
|
196
175
|
</div>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
rows={rows}
|
|
216
|
-
loading={loading}
|
|
217
|
-
rowActions={(row) => [
|
|
218
|
-
{ label: "Edit", icon: Pencil, onClick: () => edit(row) },
|
|
219
|
-
{ label: "Delete", icon: Trash2, destructive: true, onClick: () => setConfirmDelete(row) },
|
|
220
|
-
]}
|
|
221
|
-
empty={{ icon: FolderTree, title: "No categories yet", description: "Create your first category." }}
|
|
222
|
-
/>
|
|
223
|
-
</div>
|
|
176
|
+
))}
|
|
177
|
+
|
|
178
|
+
<Button
|
|
179
|
+
type="button"
|
|
180
|
+
variant="link"
|
|
181
|
+
size="sm"
|
|
182
|
+
className="h-auto p-0"
|
|
183
|
+
onClick={() =>
|
|
184
|
+
setRows((prev) => [
|
|
185
|
+
...prev,
|
|
186
|
+
{ id: null, name: "", slug: "", menu_order: 0, description: "", parent_id: "", saved: { name: "", slug: "", menu_order: 0 } },
|
|
187
|
+
])
|
|
188
|
+
}
|
|
189
|
+
>
|
|
190
|
+
<Plus className="mr-1 h-3 w-3" /> Add category
|
|
191
|
+
</Button>
|
|
192
|
+
</CardContent>
|
|
193
|
+
</Card>
|
|
224
194
|
|
|
225
195
|
<ConfirmDialog
|
|
226
196
|
open={Boolean(confirmDelete)}
|
|
227
197
|
onOpenChange={(o) => !o && setConfirmDelete(null)}
|
|
228
198
|
title={`Delete "${confirmDelete?.name}"?`}
|
|
229
|
-
description="Products in this category are not deleted; they simply lose the category."
|
|
199
|
+
description="Products in this category are not deleted; they simply lose the category. This cannot be undone."
|
|
230
200
|
confirmLabel="Delete"
|
|
231
201
|
onConfirm={doDelete}
|
|
232
202
|
loading={deleting}
|
|
@@ -21,7 +21,6 @@ const NEW_PRODUCT = {
|
|
|
21
21
|
slug: "",
|
|
22
22
|
status: "draft",
|
|
23
23
|
featured: false,
|
|
24
|
-
catalog_visibility: "visible",
|
|
25
24
|
description: "",
|
|
26
25
|
short_description: "",
|
|
27
26
|
sku: "",
|
|
@@ -35,7 +34,7 @@ const NEW_PRODUCT = {
|
|
|
35
34
|
download_limit: -1,
|
|
36
35
|
download_expiry: -1,
|
|
37
36
|
tax_status: "taxable",
|
|
38
|
-
|
|
37
|
+
tax_group: "Products",
|
|
39
38
|
manage_stock: false,
|
|
40
39
|
stock_quantity: null,
|
|
41
40
|
stock_status: "instock",
|
|
@@ -44,11 +43,10 @@ const NEW_PRODUCT = {
|
|
|
44
43
|
sold_individually: false,
|
|
45
44
|
weight: null,
|
|
46
45
|
dimensions: { length: null, width: null, height: null },
|
|
47
|
-
shipping_class_id: "",
|
|
48
46
|
upsell_ids: [],
|
|
49
47
|
cross_sell_ids: [],
|
|
50
48
|
category_ids: [],
|
|
51
|
-
|
|
49
|
+
ribbon_ids: [],
|
|
52
50
|
images: [],
|
|
53
51
|
attributes: [],
|
|
54
52
|
default_attributes: [],
|
|
@@ -72,16 +70,23 @@ export default function ProductEditor() {
|
|
|
72
70
|
() => base44.entities["commerce.ProductCategory"].list(undefined, 1000),
|
|
73
71
|
[]
|
|
74
72
|
);
|
|
75
|
-
const { data:
|
|
76
|
-
() => base44.entities["commerce.
|
|
73
|
+
const { data: ribbons, refetch: refreshRibbons } = useAsync(
|
|
74
|
+
() => base44.entities["commerce.ProductRibbon"].list(undefined, 1000),
|
|
77
75
|
[]
|
|
78
76
|
);
|
|
79
77
|
const { data: attributes, refetch: refreshAttributes } = useAsync(
|
|
80
78
|
() => base44.entities["commerce.ProductAttribute"].list("order", 500),
|
|
81
79
|
[]
|
|
82
80
|
);
|
|
83
|
-
|
|
84
|
-
const { data:
|
|
81
|
+
// Tax groups are named per Shipping & Tax Location; the product picks by name.
|
|
82
|
+
const { data: locations } = useAsync(() => base44.entities["commerce.ShippingTaxLocation"].list("order", 500), []);
|
|
83
|
+
const taxGroups = useMemo(() => {
|
|
84
|
+
const names = new Set(["Products"]);
|
|
85
|
+
for (const loc of locations || []) {
|
|
86
|
+
for (const g of loc.tax_groups || []) if (g?.name) names.add(g.name);
|
|
87
|
+
}
|
|
88
|
+
return [...names];
|
|
89
|
+
}, [locations]);
|
|
85
90
|
|
|
86
91
|
const load = useCallback(async () => {
|
|
87
92
|
if (isNew) {
|
|
@@ -158,8 +163,8 @@ export default function ProductEditor() {
|
|
|
158
163
|
}
|
|
159
164
|
|
|
160
165
|
const refs = {
|
|
161
|
-
categories,
|
|
162
|
-
refreshCategories,
|
|
166
|
+
categories, ribbons, attributes, taxGroups,
|
|
167
|
+
refreshCategories, refreshRibbons, refreshAttributes,
|
|
163
168
|
};
|
|
164
169
|
|
|
165
170
|
return (
|
|
@@ -205,14 +210,6 @@ export default function ProductEditor() {
|
|
|
205
210
|
</CardContent>
|
|
206
211
|
</Card>
|
|
207
212
|
|
|
208
|
-
<ProductDataPanel
|
|
209
|
-
product={product}
|
|
210
|
-
up={up}
|
|
211
|
-
refs={refs}
|
|
212
|
-
variations={variations}
|
|
213
|
-
setVariations={setVariations}
|
|
214
|
-
/>
|
|
215
|
-
|
|
216
213
|
<Card>
|
|
217
214
|
<CardHeader className="pb-2">
|
|
218
215
|
<CardTitle className="text-base">Short description</CardTitle>
|
|
@@ -226,6 +223,14 @@ export default function ProductEditor() {
|
|
|
226
223
|
/>
|
|
227
224
|
</CardContent>
|
|
228
225
|
</Card>
|
|
226
|
+
|
|
227
|
+
<ProductDataPanel
|
|
228
|
+
product={product}
|
|
229
|
+
up={up}
|
|
230
|
+
refs={refs}
|
|
231
|
+
variations={variations}
|
|
232
|
+
setVariations={setVariations}
|
|
233
|
+
/>
|
|
229
234
|
</div>
|
|
230
235
|
|
|
231
236
|
{/* Sidebar */}
|
|
@@ -17,10 +17,12 @@ import {
|
|
|
17
17
|
SelectTrigger,
|
|
18
18
|
SelectValue,
|
|
19
19
|
} from "@/components/ui/select";
|
|
20
|
+
import { Switch } from "@/components/ui/switch";
|
|
20
21
|
import { Check, MessageSquare, Pencil, ShieldAlert, Star, Trash, Trash2 } from "lucide-react";
|
|
21
22
|
import { toast } from "sonner";
|
|
22
23
|
|
|
23
24
|
import { base44, call } from "../../lib/api";
|
|
25
|
+
import { useSettings } from "../../context/SettingsContext";
|
|
24
26
|
import usePagedList from "../../hooks/usePagedList";
|
|
25
27
|
import PageHeader from "../../components/PageHeader";
|
|
26
28
|
import DataTable from "../../components/DataTable";
|
|
@@ -170,7 +172,11 @@ export default function Reviews() {
|
|
|
170
172
|
|
|
171
173
|
return (
|
|
172
174
|
<div className="space-y-4">
|
|
173
|
-
<PageHeader
|
|
175
|
+
<PageHeader
|
|
176
|
+
title="Reviews"
|
|
177
|
+
description="Moderate customer product reviews"
|
|
178
|
+
actions={<AutoApproveToggle />}
|
|
179
|
+
/>
|
|
174
180
|
|
|
175
181
|
<Tabs value={tab} onValueChange={(v) => { setTab(v); setSelected([]); }}>
|
|
176
182
|
<TabsList>
|
|
@@ -253,3 +259,33 @@ export default function Reviews() {
|
|
|
253
259
|
</div>
|
|
254
260
|
);
|
|
255
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The one server-enforced review setting: everything else (login-gating,
|
|
265
|
+
* verified-buyers-only, required ratings) is storefront policy — see
|
|
266
|
+
* .agents/skills/commerce/references/reviews.md.
|
|
267
|
+
*/
|
|
268
|
+
function AutoApproveToggle() {
|
|
269
|
+
const settings = useSettings();
|
|
270
|
+
const [saving, setSaving] = useState(false);
|
|
271
|
+
const enabled = !!settings?.get("products", "auto_approve_reviews", false);
|
|
272
|
+
|
|
273
|
+
const toggle = async (v) => {
|
|
274
|
+
setSaving(true);
|
|
275
|
+
try {
|
|
276
|
+
await settings.saveGroup("products", { ...(settings.settings?.products || {}), auto_approve_reviews: v });
|
|
277
|
+
toast.success(v ? "New reviews publish immediately" : "New reviews are held for moderation");
|
|
278
|
+
} catch (e) {
|
|
279
|
+
toast.error(e.message || "Failed to save");
|
|
280
|
+
} finally {
|
|
281
|
+
setSaving(false);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<label className="flex items-center gap-2 text-sm">
|
|
287
|
+
<span className="text-muted-foreground">Auto-approve new reviews</span>
|
|
288
|
+
<Switch checked={enabled} disabled={saving} onCheckedChange={toggle} />
|
|
289
|
+
</label>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
3
3
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
4
4
|
import { Separator } from "@/components/ui/separator";
|
|
@@ -10,21 +10,33 @@ import LinkedTab from "./tabs/LinkedTab";
|
|
|
10
10
|
import DownloadsTab from "./tabs/DownloadsTab";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* The "Product data" panel:
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* The "Product data" panel: every section stacked one below the other — no
|
|
14
|
+
* side menu. There is no product type, and nothing about a purchasable thing
|
|
15
|
+
* is split off elsewhere — Price & Inventory carries tax, the attributes, and
|
|
16
|
+
* a row per variant (or the product itself as its default variant), each row
|
|
17
|
+
* owning its price, stock and shipping.
|
|
17
18
|
*/
|
|
18
19
|
export default function ProductDataPanel({ product, up, refs, variations, setVariations }) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
const sections = [
|
|
21
|
+
{
|
|
22
|
+
id: "price",
|
|
23
|
+
label: "Price & Inventory",
|
|
24
|
+
body: (
|
|
25
|
+
<PriceInventoryTab
|
|
26
|
+
product={product}
|
|
27
|
+
up={up}
|
|
28
|
+
refs={refs}
|
|
29
|
+
variations={variations}
|
|
30
|
+
setVariations={setVariations}
|
|
31
|
+
/>
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
{ id: "modifiers", label: "Modifiers", body: <ModifiersTab product={product} up={up} /> },
|
|
35
|
+
...(product.downloadable
|
|
36
|
+
? [{ id: "downloads", label: "Downloads", body: <DownloadsTab product={product} up={up} /> }]
|
|
37
|
+
: []),
|
|
38
|
+
{ id: "linked", label: "Linked products", body: <LinkedTab product={product} up={up} /> },
|
|
39
|
+
];
|
|
28
40
|
|
|
29
41
|
return (
|
|
30
42
|
<Card>
|
|
@@ -48,39 +60,13 @@ export default function ProductDataPanel({ product, up, refs, variations, setVar
|
|
|
48
60
|
</div>
|
|
49
61
|
</CardHeader>
|
|
50
62
|
<CardContent className="p-0">
|
|
51
|
-
<div className="
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
onClick={() => setActive(t.id)}
|
|
59
|
-
className={`rounded-md px-3 py-2 text-left text-sm transition-colors ${
|
|
60
|
-
activeTab === t.id
|
|
61
|
-
? "bg-background font-medium shadow-sm"
|
|
62
|
-
: "text-muted-foreground hover:bg-background/60"
|
|
63
|
-
}`}
|
|
64
|
-
>
|
|
65
|
-
{t.label}
|
|
66
|
-
</button>
|
|
67
|
-
))}
|
|
68
|
-
</nav>
|
|
69
|
-
|
|
70
|
-
<div className="min-w-0 flex-1 p-4">
|
|
71
|
-
{activeTab === "price" && (
|
|
72
|
-
<PriceInventoryTab
|
|
73
|
-
product={product}
|
|
74
|
-
up={up}
|
|
75
|
-
refs={refs}
|
|
76
|
-
variations={variations}
|
|
77
|
-
setVariations={setVariations}
|
|
78
|
-
/>
|
|
79
|
-
)}
|
|
80
|
-
{activeTab === "modifiers" && <ModifiersTab product={product} up={up} />}
|
|
81
|
-
{activeTab === "downloads" && <DownloadsTab product={product} up={up} />}
|
|
82
|
-
{activeTab === "linked" && <LinkedTab product={product} up={up} />}
|
|
83
|
-
</div>
|
|
63
|
+
<div className="divide-y">
|
|
64
|
+
{sections.map((s) => (
|
|
65
|
+
<section key={s.id} className="p-4">
|
|
66
|
+
<h3 className="mb-3 text-sm font-semibold">{s.label}</h3>
|
|
67
|
+
{s.body}
|
|
68
|
+
</section>
|
|
69
|
+
))}
|
|
84
70
|
</div>
|
|
85
71
|
<Separator />
|
|
86
72
|
<div className="px-4 py-2 text-xs text-muted-foreground">
|