@base44/app-plugin-commerce 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/base44/agents/commerce/StoreAdmin.jsonc +2 -2
- package/base44/entities/commerce.Cart.jsonc +1 -1
- package/base44/entities/commerce.Coupon.jsonc +5 -0
- package/base44/entities/commerce.Order.jsonc +6 -7
- package/base44/entities/commerce.OrderRefund.jsonc +1 -1
- package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
- package/base44/entities/commerce.Product.jsonc +6 -16
- package/base44/entities/{commerce.ProductTag.jsonc → commerce.ProductRibbon.jsonc} +2 -2
- package/base44/entities/commerce.ProductVariation.jsonc +1 -9
- package/base44/entities/commerce.ShippingTaxLocation.jsonc +85 -0
- package/base44/entities/commerce.Webhook.jsonc +1 -1
- package/base44/functions/commerce/admin-orders/helpers.ts +7 -13
- package/base44/functions/commerce/admin-products/entry.ts +11 -17
- package/base44/functions/commerce/admin-refunds/entry.ts +10 -9
- package/base44/functions/commerce/admin-reports/entry.ts +3 -3
- package/base44/functions/commerce/admin-tools/entry.ts +9 -36
- package/base44/functions/commerce/payment-webhook/entry.ts +50 -89
- package/base44/functions/commerce/payments/entry.ts +46 -42
- package/base44/functions/commerce/seed-store/defaults.ts +35 -42
- package/base44/functions/commerce/seed-store/entry.ts +68 -46
- package/base44/functions/commerce/seed-store/sample-data.ts +2 -15
- package/base44/functions/commerce/seed-store/seed-catalog.ts +105 -55
- package/base44/functions/commerce/storefront-cart/cart-pricing.ts +6 -11
- package/base44/functions/commerce/storefront-cart/entry.ts +36 -2
- package/base44/functions/commerce/storefront-catalog/entry.ts +55 -72
- package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +6 -11
- package/base44/functions/commerce/storefront-checkout/entry.ts +43 -56
- package/base44/shared/commerce/card-payment.ts +80 -0
- package/base44/shared/commerce/coupons.ts +16 -10
- package/base44/shared/commerce/emails.ts +30 -18
- package/base44/shared/commerce/money.ts +11 -24
- package/base44/shared/commerce/payments.ts +55 -286
- package/base44/shared/commerce/scan.ts +1 -1
- package/base44/shared/commerce/sequence.ts +1 -1
- package/base44/shared/commerce/settings.ts +4 -9
- package/base44/shared/commerce/shipping.ts +65 -133
- package/base44/shared/commerce/tax.ts +48 -96
- package/base44/shared/commerce/totals.ts +77 -91
- package/package.json +1 -1
- package/scripts/install.js +28 -7
- package/skills/commerce/SKILL.md +14 -14
- package/skills/commerce/docs/api-admin.md +23 -26
- package/skills/commerce/docs/api-storefront.md +67 -61
- package/skills/commerce/installation-guidelines.md +8 -8
- package/skills/commerce/post-installation.md +60 -39
- package/skills/commerce/references/admin-product-form.md +15 -12
- package/skills/commerce/references/emails.md +2 -2
- package/skills/commerce/references/guest-access-security.md +2 -2
- package/skills/commerce/references/online-payments.md +24 -166
- package/skills/commerce/references/product-render.md +18 -18
- package/skills/commerce/references/reviews.md +14 -8
- package/skills/commerce/references/storefront-product-page.md +1 -1
- package/src/commerce/admin/README.md +4 -5
- package/src/commerce/admin/bot/Markdown.jsx +1 -1
- package/src/commerce/admin/hooks/useMoney.js +13 -22
- package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
- package/src/commerce/admin/lib/constants.js +2 -29
- package/src/commerce/admin/lib/order-utils.js +1 -1
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +131 -140
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +14 -7
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +3 -3
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +17 -28
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +5 -11
- package/src/commerce/admin/pages/products/Categories.jsx +147 -177
- package/src/commerce/admin/pages/products/ProductEditor.jsx +23 -18
- package/src/commerce/admin/pages/products/Reviews.jsx +37 -1
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +33 -47
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +13 -34
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +29 -29
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +14 -44
- package/src/commerce/admin/pages/reports/Reports.jsx +2 -2
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +101 -68
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +40 -38
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -41
- package/src/commerce/admin/pages/settings/LocationEditor.jsx +377 -0
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +137 -119
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +2 -4
- package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +191 -0
- package/src/commerce/admin/routes.jsx +6 -12
- package/src/commerce/utils/index.js +2 -2
- package/src/commerce/utils/shipping-promos.js +45 -49
- package/src/commerce/utils/variants.js +1 -1
- package/base44/entities/commerce.ShippingClass.jsonc +0 -30
- package/base44/entities/commerce.ShippingZone.jsonc +0 -41
- package/base44/entities/commerce.ShippingZoneMethod.jsonc +0 -84
- package/base44/entities/commerce.TaxClass.jsonc +0 -23
- package/base44/entities/commerce.TaxRate.jsonc +0 -68
- package/base44/shared/commerce/stripe.ts +0 -463
- package/src/commerce/admin/hooks/usePaymentProvider.js +0 -27
- package/src/commerce/admin/pages/settings/ProductsSettings.jsx +0 -118
- package/src/commerce/admin/pages/settings/ShippingSettings.jsx +0 -304
- package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +0 -514
- package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +0 -231
- package/src/commerce/admin/pages/settings/TaxSettings.jsx +0 -280
|
@@ -3,20 +3,14 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { Label } from "@/components/ui/label";
|
|
5
5
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
6
|
-
import {
|
|
7
|
-
Select,
|
|
8
|
-
SelectContent,
|
|
9
|
-
SelectItem,
|
|
10
|
-
SelectTrigger,
|
|
11
|
-
SelectValue,
|
|
12
|
-
} from "@/components/ui/select";
|
|
6
|
+
import { Switch } from "@/components/ui/switch";
|
|
13
7
|
import { Loader2, Save, Trash2 } from "lucide-react";
|
|
14
8
|
|
|
15
9
|
import ConfirmDialog from "../../../components/ConfirmDialog";
|
|
16
|
-
import { PRODUCT_STATUSES, CATALOG_VISIBILITIES } from "../../../lib/constants";
|
|
17
10
|
|
|
18
11
|
/**
|
|
19
|
-
* Product editor "Publish" sidebar box.
|
|
12
|
+
* Product editor "Publish" sidebar box. One Visible toggle drives `status`
|
|
13
|
+
* (publish/draft) — there is no separate catalog visibility.
|
|
20
14
|
* Props: { product, up, onSave, onDelete (null hides delete), saving, dirty }
|
|
21
15
|
*/
|
|
22
16
|
export default function PublishBox({ product, up, onSave, onDelete, saving, dirty }) {
|
|
@@ -33,37 +27,22 @@ export default function PublishBox({ product, up, onSave, onDelete, saving, dirt
|
|
|
33
27
|
}
|
|
34
28
|
};
|
|
35
29
|
|
|
30
|
+
const visible = product.status === "publish";
|
|
31
|
+
|
|
36
32
|
return (
|
|
37
33
|
<Card>
|
|
38
34
|
<CardHeader className="pb-2">
|
|
39
35
|
<CardTitle className="text-base">Publish</CardTitle>
|
|
40
36
|
</CardHeader>
|
|
41
37
|
<CardContent className="space-y-3">
|
|
42
|
-
<div className="
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
</SelectContent>
|
|
51
|
-
</Select>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<div className="space-y-1.5">
|
|
55
|
-
<Label>Catalog visibility</Label>
|
|
56
|
-
<Select
|
|
57
|
-
value={product.catalog_visibility || "visible"}
|
|
58
|
-
onValueChange={(v) => up({ catalog_visibility: v })}
|
|
59
|
-
>
|
|
60
|
-
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
61
|
-
<SelectContent>
|
|
62
|
-
{CATALOG_VISIBILITIES.map((v) => (
|
|
63
|
-
<SelectItem key={v.value} value={v.value}>{v.label}</SelectItem>
|
|
64
|
-
))}
|
|
65
|
-
</SelectContent>
|
|
66
|
-
</Select>
|
|
38
|
+
<div className="flex items-start justify-between gap-4">
|
|
39
|
+
<div>
|
|
40
|
+
<Label>Visible</Label>
|
|
41
|
+
<p className="text-xs text-muted-foreground">
|
|
42
|
+
{visible ? "Shown in the store" : "Hidden — saved as a draft"}
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
<Switch checked={visible} onCheckedChange={(v) => up({ status: v ? "publish" : "draft" })} />
|
|
67
46
|
</div>
|
|
68
47
|
|
|
69
48
|
<label className="flex cursor-pointer items-center gap-2 text-sm">
|
|
@@ -23,13 +23,13 @@ const NO_PARENT = "__none__";
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Product editor sidebar panel: categories (hierarchical checkboxes + quick-add)
|
|
26
|
-
* and
|
|
26
|
+
* and ribbons (multi-select + create-on-enter).
|
|
27
27
|
*
|
|
28
|
-
* Props: { product, up, refs } where refs = { categories,
|
|
28
|
+
* Props: { product, up, refs } where refs = { categories, ribbons, refreshCategories, refreshRibbons }
|
|
29
29
|
*/
|
|
30
30
|
export default function TaxonomyPanel({ product, up, refs }) {
|
|
31
31
|
const categories = refs?.categories || [];
|
|
32
|
-
const
|
|
32
|
+
const ribbons = refs?.ribbons || [];
|
|
33
33
|
|
|
34
34
|
return (
|
|
35
35
|
<>
|
|
@@ -39,11 +39,11 @@ export default function TaxonomyPanel({ product, up, refs }) {
|
|
|
39
39
|
onChange={(ids) => up({ category_ids: ids })}
|
|
40
40
|
refresh={refs?.refreshCategories}
|
|
41
41
|
/>
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
selected={product.
|
|
45
|
-
onChange={(ids) => up({
|
|
46
|
-
refresh={refs?.
|
|
42
|
+
<RibbonsCard
|
|
43
|
+
ribbons={ribbons}
|
|
44
|
+
selected={product.ribbon_ids || []}
|
|
45
|
+
onChange={(ids) => up({ ribbon_ids: ids })}
|
|
46
|
+
refresh={refs?.refreshRibbons}
|
|
47
47
|
/>
|
|
48
48
|
</>
|
|
49
49
|
);
|
|
@@ -162,46 +162,46 @@ function CategoriesCard({ categories, selected, onChange, refresh }) {
|
|
|
162
162
|
);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
function
|
|
166
|
-
const [
|
|
165
|
+
function RibbonsCard({ ribbons, selected, onChange, refresh }) {
|
|
166
|
+
const [newRibbon, setNewRibbon] = useState("");
|
|
167
167
|
const [saving, setSaving] = useState(false);
|
|
168
168
|
|
|
169
169
|
const value = useMemo(
|
|
170
170
|
() =>
|
|
171
171
|
selected
|
|
172
172
|
.map((id) => {
|
|
173
|
-
const t =
|
|
173
|
+
const t = ribbons.find((x) => x.id === id);
|
|
174
174
|
return t ? { value: t.id, label: t.name } : null;
|
|
175
175
|
})
|
|
176
176
|
.filter(Boolean),
|
|
177
|
-
[selected,
|
|
177
|
+
[selected, ribbons]
|
|
178
178
|
);
|
|
179
179
|
|
|
180
180
|
const search = async (q) =>
|
|
181
|
-
|
|
181
|
+
ribbons
|
|
182
182
|
.filter((t) => !q || (t.name || "").toLowerCase().includes(q.toLowerCase()))
|
|
183
183
|
.slice(0, 30)
|
|
184
184
|
.map((t) => ({ value: t.id, label: t.name }));
|
|
185
185
|
|
|
186
|
-
const
|
|
187
|
-
const label =
|
|
186
|
+
const createRibbon = async () => {
|
|
187
|
+
const label = newRibbon.trim();
|
|
188
188
|
if (!label) return;
|
|
189
|
-
// Reuse an existing
|
|
190
|
-
const existing =
|
|
189
|
+
// Reuse an existing ribbon with the same name if present.
|
|
190
|
+
const existing = ribbons.find((t) => (t.name || "").toLowerCase() === label.toLowerCase());
|
|
191
191
|
if (existing) {
|
|
192
192
|
if (!selected.includes(existing.id)) onChange([...selected, existing.id]);
|
|
193
|
-
|
|
193
|
+
setNewRibbon("");
|
|
194
194
|
return;
|
|
195
195
|
}
|
|
196
196
|
setSaving(true);
|
|
197
197
|
try {
|
|
198
|
-
const created = await base44.entities["commerce.
|
|
199
|
-
toast.success("
|
|
200
|
-
|
|
198
|
+
const created = await base44.entities["commerce.ProductRibbon"].create({ name: label, count: 0 });
|
|
199
|
+
toast.success("Ribbon created");
|
|
200
|
+
setNewRibbon("");
|
|
201
201
|
await refresh?.();
|
|
202
202
|
if (created?.id) onChange([...selected, created.id]);
|
|
203
203
|
} catch (e) {
|
|
204
|
-
toast.error(e.message || "Failed to create
|
|
204
|
+
toast.error(e.message || "Failed to create ribbon");
|
|
205
205
|
} finally {
|
|
206
206
|
setSaving(false);
|
|
207
207
|
}
|
|
@@ -210,30 +210,30 @@ function TagsCard({ tags, selected, onChange, refresh }) {
|
|
|
210
210
|
return (
|
|
211
211
|
<Card>
|
|
212
212
|
<CardHeader className="pb-2">
|
|
213
|
-
<CardTitle className="text-base">
|
|
213
|
+
<CardTitle className="text-base">Ribbons</CardTitle>
|
|
214
214
|
</CardHeader>
|
|
215
215
|
<CardContent className="space-y-2">
|
|
216
216
|
<SearchSelect
|
|
217
217
|
multiple
|
|
218
|
-
placeholder="Select
|
|
218
|
+
placeholder="Select ribbons"
|
|
219
219
|
value={value}
|
|
220
220
|
onChange={(next) => onChange(next.map((o) => o.value))}
|
|
221
221
|
search={search}
|
|
222
222
|
/>
|
|
223
223
|
<div className="flex gap-2">
|
|
224
224
|
<Input
|
|
225
|
-
value={
|
|
226
|
-
onChange={(e) =>
|
|
225
|
+
value={newRibbon}
|
|
226
|
+
onChange={(e) => setNewRibbon(e.target.value)}
|
|
227
227
|
onKeyDown={(e) => {
|
|
228
228
|
if (e.key === "Enter") {
|
|
229
229
|
e.preventDefault();
|
|
230
|
-
|
|
230
|
+
createRibbon();
|
|
231
231
|
}
|
|
232
232
|
}}
|
|
233
|
-
placeholder="Add new
|
|
233
|
+
placeholder="Add new ribbon"
|
|
234
234
|
className="h-8 text-sm"
|
|
235
235
|
/>
|
|
236
|
-
<Button size="sm" variant="outline" onClick={
|
|
236
|
+
<Button size="sm" variant="outline" onClick={createRibbon} disabled={saving || !newRibbon.trim()}>
|
|
237
237
|
{saving ? <Loader2 className="h-3 w-3 animate-spin" /> : "Add"}
|
|
238
238
|
</Button>
|
|
239
239
|
</div>
|
|
@@ -24,7 +24,6 @@ import AttributesSection from "../AttributesSection";
|
|
|
24
24
|
import { comboKey, generateVariationCombos, variationLabel } from "../../../../lib/product-utils";
|
|
25
25
|
import { BACKORDER_OPTIONS, STOCK_STATUSES, TAX_STATUSES } from "../../../../lib/constants";
|
|
26
26
|
|
|
27
|
-
const NONE = "__none__";
|
|
28
27
|
const ANY = "__any__";
|
|
29
28
|
const num = (v) => (v === "" ? null : Number(v));
|
|
30
29
|
const toDateInput = (iso) => (iso ? String(iso).slice(0, 10) : "");
|
|
@@ -43,7 +42,7 @@ const COPY_GROUPS = [
|
|
|
43
42
|
{
|
|
44
43
|
key: "shipping",
|
|
45
44
|
label: "Shipping",
|
|
46
|
-
fields: ["weight", "dimensions", "
|
|
45
|
+
fields: ["weight", "dimensions", "virtual", "downloadable"],
|
|
47
46
|
},
|
|
48
47
|
];
|
|
49
48
|
|
|
@@ -60,14 +59,12 @@ const NEW_VARIATION = {
|
|
|
60
59
|
download_limit: -1,
|
|
61
60
|
download_expiry: -1,
|
|
62
61
|
tax_status: "parent",
|
|
63
|
-
tax_class: "",
|
|
64
62
|
manage_stock: "parent",
|
|
65
63
|
stock_quantity: null,
|
|
66
64
|
stock_status: "instock",
|
|
67
65
|
backorders: "no",
|
|
68
66
|
weight: null,
|
|
69
67
|
dimensions: { length: null, width: null, height: null },
|
|
70
|
-
shipping_class_id: "",
|
|
71
68
|
image: null,
|
|
72
69
|
menu_order: 0,
|
|
73
70
|
description: "",
|
|
@@ -161,10 +158,10 @@ const Section = ({ label, children, className = "" }) => (
|
|
|
161
158
|
);
|
|
162
159
|
|
|
163
160
|
/**
|
|
164
|
-
* Weight
|
|
165
|
-
*
|
|
161
|
+
* Weight and dimensions live per purchasable thing, not per product — a large
|
|
162
|
+
* mug weighs more than a small one.
|
|
166
163
|
*/
|
|
167
|
-
function ShippingFields({ values, set, units
|
|
164
|
+
function ShippingFields({ values, set, units }) {
|
|
168
165
|
const dims = values.dimensions || {};
|
|
169
166
|
return (
|
|
170
167
|
<>
|
|
@@ -192,21 +189,6 @@ function ShippingFields({ values, set, units, classes, inheritLabel }) {
|
|
|
192
189
|
))}
|
|
193
190
|
</div>
|
|
194
191
|
</div>
|
|
195
|
-
<div className="space-y-1.5">
|
|
196
|
-
<Label className="text-xs">Shipping class</Label>
|
|
197
|
-
<Select
|
|
198
|
-
value={values.shipping_class_id || NONE}
|
|
199
|
-
onValueChange={(v) => set({ shipping_class_id: v === NONE ? "" : v })}
|
|
200
|
-
>
|
|
201
|
-
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
202
|
-
<SelectContent>
|
|
203
|
-
<SelectItem value={NONE}>{inheritLabel}</SelectItem>
|
|
204
|
-
{classes.map((c) => (
|
|
205
|
-
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
|
|
206
|
-
))}
|
|
207
|
-
</SelectContent>
|
|
208
|
-
</Select>
|
|
209
|
-
</div>
|
|
210
192
|
</>
|
|
211
193
|
);
|
|
212
194
|
}
|
|
@@ -346,13 +328,7 @@ function DefaultRow({ product, up, money, units, refs }) {
|
|
|
346
328
|
|
|
347
329
|
{!product.virtual && (
|
|
348
330
|
<Section label="Shipping">
|
|
349
|
-
<ShippingFields
|
|
350
|
-
values={product}
|
|
351
|
-
set={up}
|
|
352
|
-
units={units}
|
|
353
|
-
classes={refs.shippingClasses || []}
|
|
354
|
-
inheritLabel="No shipping class"
|
|
355
|
-
/>
|
|
331
|
+
<ShippingFields values={product} set={up} units={units} />
|
|
356
332
|
</Section>
|
|
357
333
|
)}
|
|
358
334
|
</div>
|
|
@@ -438,13 +414,7 @@ function VariantRow({ variation, onChange, refs, money, units, virtual }) {
|
|
|
438
414
|
|
|
439
415
|
{!virtual && (
|
|
440
416
|
<Section label="Shipping">
|
|
441
|
-
<ShippingFields
|
|
442
|
-
values={variation}
|
|
443
|
-
set={set}
|
|
444
|
-
units={units}
|
|
445
|
-
classes={refs.shippingClasses || []}
|
|
446
|
-
inheritLabel="Same as product"
|
|
447
|
-
/>
|
|
417
|
+
<ShippingFields values={variation} set={set} units={units} />
|
|
448
418
|
</Section>
|
|
449
419
|
)}
|
|
450
420
|
|
|
@@ -491,11 +461,12 @@ function VariantRow({ variation, onChange, refs, money, units, virtual }) {
|
|
|
491
461
|
export default function PriceInventoryTab({ product, up, refs, variations, setVariations }) {
|
|
492
462
|
const money = useMoney();
|
|
493
463
|
const settings = useSettings();
|
|
494
|
-
const weightUnit = settings?.get("
|
|
495
|
-
const dimensionUnit = settings?.get("
|
|
464
|
+
const weightUnit = settings?.get("general", "weight_unit", "kg");
|
|
465
|
+
const dimensionUnit = settings?.get("general", "dimension_unit", "cm");
|
|
496
466
|
const [copyFrom, setCopyFrom] = useState("0");
|
|
497
467
|
const [copyGroups, setCopyGroups] = useState(["price"]);
|
|
498
|
-
|
|
468
|
+
// Tax groups are named per Shipping & Tax Location; a product picks by name.
|
|
469
|
+
const taxGroups = refs.taxGroups?.length ? refs.taxGroups : ["Products"];
|
|
499
470
|
const units = { weightUnit, dimensionUnit };
|
|
500
471
|
|
|
501
472
|
const hasAttributes = (product.attributes || []).length > 0;
|
|
@@ -547,7 +518,6 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
547
518
|
backorders: product.backorders,
|
|
548
519
|
weight: product.weight,
|
|
549
520
|
dimensions: { ...(product.dimensions || {}) },
|
|
550
|
-
shipping_class_id: product.shipping_class_id,
|
|
551
521
|
};
|
|
552
522
|
|
|
553
523
|
const added = [...wanted.entries()]
|
|
@@ -603,12 +573,12 @@ export default function PriceInventoryTab({ product, up, refs, variations, setVa
|
|
|
603
573
|
</Select>
|
|
604
574
|
</div>
|
|
605
575
|
<div className="space-y-1.5">
|
|
606
|
-
<Label>Tax
|
|
607
|
-
<Select value={product.
|
|
576
|
+
<Label>Tax group</Label>
|
|
577
|
+
<Select value={product.tax_group || "Products"} onValueChange={(v) => up({ tax_group: v })}>
|
|
608
578
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
609
579
|
<SelectContent>
|
|
610
|
-
{
|
|
611
|
-
<SelectItem key={
|
|
580
|
+
{taxGroups.map((g) => (
|
|
581
|
+
<SelectItem key={g} value={g}>{g}</SelectItem>
|
|
612
582
|
))}
|
|
613
583
|
</SelectContent>
|
|
614
584
|
</Select>
|
|
@@ -327,7 +327,7 @@ const TOTALS_ACTIONS = [
|
|
|
327
327
|
"coupons-totals",
|
|
328
328
|
"reviews-totals",
|
|
329
329
|
"categories-totals",
|
|
330
|
-
"
|
|
330
|
+
"ribbons-totals",
|
|
331
331
|
"attributes-totals",
|
|
332
332
|
];
|
|
333
333
|
|
|
@@ -381,7 +381,7 @@ function TotalsTab() {
|
|
|
381
381
|
rows={normalizeTotals(d["reviews-totals"], REVIEW_STATUSES)}
|
|
382
382
|
/>
|
|
383
383
|
<TotalsCard title="Categories" loading={loading} rows={normalizeTotals(d["categories-totals"])} />
|
|
384
|
-
<TotalsCard title="
|
|
384
|
+
<TotalsCard title="Ribbons" loading={loading} rows={normalizeTotals(d["ribbons-totals"])} />
|
|
385
385
|
<TotalsCard title="Attributes" loading={loading} rows={normalizeTotals(d["attributes-totals"])} />
|
|
386
386
|
</div>
|
|
387
387
|
);
|
|
@@ -63,6 +63,21 @@ export default function EmailsSettings() {
|
|
|
63
63
|
text.split(",").map((s) => s.trim()).filter(Boolean)
|
|
64
64
|
);
|
|
65
65
|
|
|
66
|
+
// What the Recipient column shows for an admin-targeted row: the per-type
|
|
67
|
+
// override, then the configured admin recipients, then the resolved admin users.
|
|
68
|
+
const adminRecipient = (e) =>
|
|
69
|
+
typeCfg(e.id).recipient ||
|
|
70
|
+
recipientsText ||
|
|
71
|
+
effective?.recipients.join(", ") ||
|
|
72
|
+
"Admin";
|
|
73
|
+
|
|
74
|
+
// One list for every email; a delimiter row splits the admin-targeted
|
|
75
|
+
// emails from the ones the customer receives.
|
|
76
|
+
const GROUPS = [
|
|
77
|
+
{ label: "Sent to the store admin", types: EMAIL_TYPES.filter((e) => e.recipient === "admin") },
|
|
78
|
+
{ label: "Sent to the customer", types: EMAIL_TYPES.filter((e) => e.recipient === "customer") },
|
|
79
|
+
];
|
|
80
|
+
|
|
66
81
|
const active = activeId ? EMAIL_TYPES.find((e) => e.id === activeId) : null;
|
|
67
82
|
|
|
68
83
|
return (
|
|
@@ -117,7 +132,10 @@ export default function EmailsSettings() {
|
|
|
117
132
|
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
118
133
|
<div className="space-y-1.5">
|
|
119
134
|
<CardTitle>Email notifications</CardTitle>
|
|
120
|
-
<CardDescription>
|
|
135
|
+
<CardDescription>
|
|
136
|
+
Enable, disable and customize each transactional email, including the
|
|
137
|
+
low-stock and out-of-stock alerts.
|
|
138
|
+
</CardDescription>
|
|
121
139
|
</div>
|
|
122
140
|
<SaveButton />
|
|
123
141
|
</CardHeader>
|
|
@@ -132,47 +150,58 @@ export default function EmailsSettings() {
|
|
|
132
150
|
</TableRow>
|
|
133
151
|
</TableHeader>
|
|
134
152
|
<TableBody>
|
|
135
|
-
{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{managed && (
|
|
144
|
-
<Badge variant="secondary" className="font-normal text-muted-foreground">
|
|
145
|
-
Handled by Base44 auth
|
|
146
|
-
</Badge>
|
|
147
|
-
)}
|
|
148
|
-
</div>
|
|
149
|
-
</TableCell>
|
|
150
|
-
<TableCell className="text-sm text-muted-foreground">
|
|
151
|
-
{managed
|
|
152
|
-
? "—"
|
|
153
|
-
: e.recipient === "admin"
|
|
154
|
-
? typeCfg(e.id).recipient || recipientsText ||
|
|
155
|
-
effective?.recipients.join(", ") || "Admin"
|
|
156
|
-
: "Customer"}
|
|
157
|
-
</TableCell>
|
|
158
|
-
<TableCell>
|
|
159
|
-
{managed ? (
|
|
160
|
-
<span className="text-xs text-muted-foreground">—</span>
|
|
161
|
-
) : (
|
|
162
|
-
<Switch checked={isEnabled(e.id)} onCheckedChange={(v) => setTypeField(e.id, "enabled", !!v)} />
|
|
163
|
-
)}
|
|
164
|
-
</TableCell>
|
|
165
|
-
<TableCell>
|
|
166
|
-
{!managed && (
|
|
167
|
-
<Button variant="ghost" size="sm" onClick={() => setActiveId(e.id)}>
|
|
168
|
-
<Settings2 className="mr-2 h-4 w-4" />
|
|
169
|
-
Manage
|
|
170
|
-
</Button>
|
|
171
|
-
)}
|
|
153
|
+
{GROUPS.map((group) => (
|
|
154
|
+
<React.Fragment key={group.label}>
|
|
155
|
+
<TableRow className="hover:bg-transparent">
|
|
156
|
+
<TableCell
|
|
157
|
+
colSpan={4}
|
|
158
|
+
className="bg-muted/50 py-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground"
|
|
159
|
+
>
|
|
160
|
+
{group.label}
|
|
172
161
|
</TableCell>
|
|
173
162
|
</TableRow>
|
|
174
|
-
|
|
175
|
-
|
|
163
|
+
{group.types.map((e) => {
|
|
164
|
+
const managed = !!e.managed_by_auth;
|
|
165
|
+
return (
|
|
166
|
+
<TableRow key={e.id}>
|
|
167
|
+
<TableCell>
|
|
168
|
+
<div className="flex items-center gap-2">
|
|
169
|
+
<span className="font-medium">{e.label}</span>
|
|
170
|
+
{e.manual && <Badge variant="outline" className="font-normal">Manual</Badge>}
|
|
171
|
+
{managed && (
|
|
172
|
+
<Badge variant="secondary" className="font-normal text-muted-foreground">
|
|
173
|
+
Handled by Base44 auth
|
|
174
|
+
</Badge>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
</TableCell>
|
|
178
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
179
|
+
{managed
|
|
180
|
+
? "—"
|
|
181
|
+
: e.recipient === "admin"
|
|
182
|
+
? adminRecipient(e)
|
|
183
|
+
: "Customer"}
|
|
184
|
+
</TableCell>
|
|
185
|
+
<TableCell>
|
|
186
|
+
{managed ? (
|
|
187
|
+
<span className="text-xs text-muted-foreground">—</span>
|
|
188
|
+
) : (
|
|
189
|
+
<Switch checked={isEnabled(e.id)} onCheckedChange={(v) => setTypeField(e.id, "enabled", !!v)} />
|
|
190
|
+
)}
|
|
191
|
+
</TableCell>
|
|
192
|
+
<TableCell>
|
|
193
|
+
{!managed && (
|
|
194
|
+
<Button variant="ghost" size="sm" onClick={() => setActiveId(e.id)}>
|
|
195
|
+
<Settings2 className="mr-2 h-4 w-4" />
|
|
196
|
+
Manage
|
|
197
|
+
</Button>
|
|
198
|
+
)}
|
|
199
|
+
</TableCell>
|
|
200
|
+
</TableRow>
|
|
201
|
+
);
|
|
202
|
+
})}
|
|
203
|
+
</React.Fragment>
|
|
204
|
+
))}
|
|
176
205
|
</TableBody>
|
|
177
206
|
</Table>
|
|
178
207
|
</CardContent>
|
|
@@ -198,34 +227,38 @@ export default function EmailsSettings() {
|
|
|
198
227
|
</p>
|
|
199
228
|
</div>
|
|
200
229
|
)}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
{!active.stock && (
|
|
231
|
+
<>
|
|
232
|
+
<div className="space-y-1.5">
|
|
233
|
+
<Label>Subject</Label>
|
|
234
|
+
<Input
|
|
235
|
+
placeholder="Leave blank for the default"
|
|
236
|
+
value={typeCfg(active.id).subject || ""}
|
|
237
|
+
onChange={(e) => setTypeField(active.id, "subject", e.target.value)}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
<div className="space-y-1.5">
|
|
241
|
+
<Label>Email heading</Label>
|
|
242
|
+
<Input
|
|
243
|
+
placeholder="Leave blank for the default"
|
|
244
|
+
value={typeCfg(active.id).heading || ""}
|
|
245
|
+
onChange={(e) => setTypeField(active.id, "heading", e.target.value)}
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
<div className="space-y-1.5">
|
|
249
|
+
<Label>Additional content</Label>
|
|
250
|
+
<Textarea
|
|
251
|
+
rows={3}
|
|
252
|
+
placeholder="Text appended below the email body."
|
|
253
|
+
value={typeCfg(active.id).additional_content || ""}
|
|
254
|
+
onChange={(e) => setTypeField(active.id, "additional_content", e.target.value)}
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
<p className="text-xs text-muted-foreground">
|
|
258
|
+
Placeholders: {"{order_number}"}, {"{customer_name}"}, {"{store_name}"}, {"{order_total}"}.
|
|
259
|
+
</p>
|
|
260
|
+
</>
|
|
261
|
+
)}
|
|
229
262
|
</div>
|
|
230
263
|
<DialogFooter>
|
|
231
264
|
<Button onClick={() => setActiveId(null)}>Done</Button>
|