@base44/app-plugin-commerce 0.7.1 → 0.8.0
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 +4 -10
- package/base44/entities/commerce.ProductReview.jsonc +2 -2
- package/base44/functions/commerce/storefront-catalog/entry.ts +7 -3
- package/package.json +1 -1
- package/scripts/install.js +2 -4
- package/skills/commerce/SKILL.md +9 -3
- package/skills/commerce/docs/api-storefront.md +3 -3
- package/skills/commerce/install/01-install.md +8 -24
- package/skills/commerce/install/02-storefront.md +21 -9
- package/skills/commerce/references/admin-localization.md +95 -0
- package/skills/commerce/references/reviews.md +7 -6
- package/src/commerce/admin/README.md +36 -14
- package/src/commerce/admin/bot/StoreAdminBot.jsx +12 -11
- package/src/commerce/admin/components/AddressForm.jsx +13 -12
- package/src/commerce/admin/components/ConfirmDialog.jsx +5 -4
- package/src/commerce/admin/components/CountrySelect.jsx +12 -8
- package/src/commerce/admin/components/DataTable.jsx +3 -2
- package/src/commerce/admin/components/DateRangePicker.jsx +11 -10
- package/src/commerce/admin/components/EmptyState.jsx +2 -1
- package/src/commerce/admin/components/MediaUploader.jsx +3 -2
- package/src/commerce/admin/components/MetaDataEditor.jsx +4 -3
- package/src/commerce/admin/components/SearchSelect.jsx +4 -3
- package/src/commerce/admin/context/SettingsContext.jsx +12 -25
- package/src/commerce/admin/hooks/useMoney.js +3 -2
- package/src/commerce/admin/i18n/index.js +54 -0
- package/src/commerce/admin/i18n/locales/de.js +896 -0
- package/src/commerce/admin/i18n/locales/en.js +900 -0
- package/src/commerce/admin/i18n/locales/es.js +896 -0
- package/src/commerce/admin/i18n/locales/fr.js +896 -0
- package/src/commerce/admin/i18n/locales/ja.js +896 -0
- package/src/commerce/admin/i18n/locales/pt.js +896 -0
- package/src/commerce/admin/index.jsx +6 -30
- package/src/commerce/admin/layout/AccessDenied.jsx +8 -9
- package/src/commerce/admin/layout/AuthGuard.jsx +2 -1
- package/src/commerce/admin/layout/Sidebar.jsx +29 -18
- package/src/commerce/admin/layout/Topbar.jsx +4 -3
- package/src/commerce/admin/lib/api.js +3 -2
- package/src/commerce/admin/lib/constants.js +44 -43
- package/src/commerce/admin/lib/format.js +4 -3
- package/src/commerce/admin/lib/paths.js +2 -3
- package/src/commerce/admin/pages/Dashboard.jsx +25 -24
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +55 -57
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +22 -21
- package/src/commerce/admin/pages/customers/CustomerEditor.jsx +31 -30
- package/src/commerce/admin/pages/customers/CustomersList.jsx +19 -18
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +75 -71
- package/src/commerce/admin/pages/orders/OrdersList.jsx +16 -15
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +12 -11
- package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +14 -13
- package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +12 -11
- package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +11 -10
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +22 -22
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +18 -17
- package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +18 -17
- package/src/commerce/admin/pages/products/Categories.jsx +14 -13
- package/src/commerce/admin/pages/products/ProductEditor.jsx +16 -15
- package/src/commerce/admin/pages/products/ProductsList.jsx +41 -36
- package/src/commerce/admin/pages/products/Reviews.jsx +54 -46
- package/src/commerce/admin/pages/products/components/AttributesSection.jsx +45 -39
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +8 -7
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +10 -9
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +18 -17
- package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +11 -10
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +9 -10
- package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +3 -9
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +76 -78
- package/src/commerce/admin/pages/reports/Reports.jsx +33 -32
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +36 -39
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +13 -14
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +12 -13
- package/src/commerce/admin/pages/settings/LocationEditor.jsx +48 -47
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +32 -33
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +13 -12
- package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +31 -31
- package/src/commerce/admin/pages/settings/useGroupForm.jsx +4 -3
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +34 -31
- package/src/commerce/admin/pages/status/Webhooks.jsx +11 -10
- package/src/commerce/admin/routes.jsx +31 -43
- package/src/commerce/storefront/pickers.jsx +19 -5
- package/src/commerce/storefront/useCheckout.jsx +28 -3
- package/src/commerce/utils/storefront.js +49 -2
|
@@ -6,6 +6,7 @@ import { FolderTree, Loader2, Plus, Trash2 } from "lucide-react";
|
|
|
6
6
|
import { toast } from "sonner";
|
|
7
7
|
|
|
8
8
|
import { base44, call } from "../../lib/api";
|
|
9
|
+
import { t } from "../../i18n";
|
|
9
10
|
import useAsync from "../../hooks/useAsync";
|
|
10
11
|
import PageHeader from "../../components/PageHeader";
|
|
11
12
|
import ConfirmDialog from "../../components/ConfirmDialog";
|
|
@@ -80,7 +81,7 @@ export default function Categories() {
|
|
|
80
81
|
parent_id: row.parent_id,
|
|
81
82
|
},
|
|
82
83
|
});
|
|
83
|
-
toast.success(row.id ? "
|
|
84
|
+
toast.success(row.id ? t("categories.saved") : t("categories.added"));
|
|
84
85
|
refetch();
|
|
85
86
|
} catch {
|
|
86
87
|
/* toast handled by call() */
|
|
@@ -93,7 +94,7 @@ export default function Categories() {
|
|
|
93
94
|
setDeleting(true);
|
|
94
95
|
try {
|
|
95
96
|
await call("admin-products", "delete-term", { taxonomy: "category", id: confirmDelete.id });
|
|
96
|
-
toast.success("
|
|
97
|
+
toast.success(t("categories.deleted"));
|
|
97
98
|
setConfirmDelete(null);
|
|
98
99
|
refetch();
|
|
99
100
|
} catch {
|
|
@@ -115,7 +116,7 @@ export default function Categories() {
|
|
|
115
116
|
|
|
116
117
|
return (
|
|
117
118
|
<div className="space-y-4">
|
|
118
|
-
<PageHeader title="
|
|
119
|
+
<PageHeader title={t("routes.categories")} description={t("categories.description")} />
|
|
119
120
|
|
|
120
121
|
<Card className="max-w-3xl">
|
|
121
122
|
<CardContent className="space-y-2 py-4">
|
|
@@ -124,15 +125,15 @@ export default function Categories() {
|
|
|
124
125
|
{!loading && !rows.length && (
|
|
125
126
|
<div className="flex items-center gap-3 py-6 text-muted-foreground">
|
|
126
127
|
<FolderTree className="h-5 w-5" />
|
|
127
|
-
<span className="text-sm">
|
|
128
|
+
<span className="text-sm">{t("categories.empty")}</span>
|
|
128
129
|
</div>
|
|
129
130
|
)}
|
|
130
131
|
|
|
131
132
|
{rows.length > 0 && (
|
|
132
133
|
<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>
|
|
134
|
-
<span>
|
|
135
|
-
<span>
|
|
134
|
+
<span>{t("common.name")}</span>
|
|
135
|
+
<span>{t("categories.slug")}</span>
|
|
136
|
+
<span>{t("categories.order")}</span>
|
|
136
137
|
<span />
|
|
137
138
|
</div>
|
|
138
139
|
)}
|
|
@@ -141,14 +142,14 @@ export default function Categories() {
|
|
|
141
142
|
<div key={row.id || `new-${i}`} className="grid grid-cols-[1fr_1fr_5rem_2.25rem] items-center gap-2">
|
|
142
143
|
<Input
|
|
143
144
|
autoFocus={!row.id && !row.name}
|
|
144
|
-
placeholder="
|
|
145
|
+
placeholder={t("categories.name_placeholder")}
|
|
145
146
|
value={row.name}
|
|
146
147
|
onChange={(e) => setField(i, "name", e.target.value)}
|
|
147
148
|
onBlur={() => commit(i)}
|
|
148
149
|
onKeyDown={blurOnEnter}
|
|
149
150
|
/>
|
|
150
151
|
<Input
|
|
151
|
-
placeholder="
|
|
152
|
+
placeholder={t("categories.slug_placeholder")}
|
|
152
153
|
className="font-mono text-xs lowercase"
|
|
153
154
|
value={row.slug}
|
|
154
155
|
onChange={(e) => setField(i, "slug", e.target.value)}
|
|
@@ -187,7 +188,7 @@ export default function Categories() {
|
|
|
187
188
|
])
|
|
188
189
|
}
|
|
189
190
|
>
|
|
190
|
-
<Plus className="mr-1 h-3 w-3" />
|
|
191
|
+
<Plus className="mr-1 h-3 w-3" /> {t("categories.add")}
|
|
191
192
|
</Button>
|
|
192
193
|
</CardContent>
|
|
193
194
|
</Card>
|
|
@@ -195,9 +196,9 @@ export default function Categories() {
|
|
|
195
196
|
<ConfirmDialog
|
|
196
197
|
open={Boolean(confirmDelete)}
|
|
197
198
|
onOpenChange={(o) => !o && setConfirmDelete(null)}
|
|
198
|
-
title={
|
|
199
|
-
description="
|
|
200
|
-
confirmLabel="
|
|
199
|
+
title={t("categories.confirm_delete_title", { name: confirmDelete?.name })}
|
|
200
|
+
description={t("categories.confirm_delete_description")}
|
|
201
|
+
confirmLabel={t("common.delete")}
|
|
201
202
|
onConfirm={doDelete}
|
|
202
203
|
loading={deleting}
|
|
203
204
|
/>
|
|
@@ -6,6 +6,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|
|
6
6
|
import { toast } from "sonner";
|
|
7
7
|
|
|
8
8
|
import { base44, call } from "../../lib/api";
|
|
9
|
+
import { t } from "../../i18n";
|
|
9
10
|
import useAsync from "../../hooks/useAsync";
|
|
10
11
|
import { useAdminHref } from "../../context/BasePathContext";
|
|
11
12
|
import PageHeader from "../../components/PageHeader";
|
|
@@ -107,7 +108,7 @@ export default function ProductEditor() {
|
|
|
107
108
|
}, [id, isNew]);
|
|
108
109
|
|
|
109
110
|
useEffect(() => {
|
|
110
|
-
load().catch((e) => toast.error(e.message || "
|
|
111
|
+
load().catch((e) => toast.error(e.message || t("products.load_failed")));
|
|
111
112
|
}, [load]);
|
|
112
113
|
|
|
113
114
|
/** Patch the product object. */
|
|
@@ -123,7 +124,7 @@ export default function ProductEditor() {
|
|
|
123
124
|
|
|
124
125
|
const save = async () => {
|
|
125
126
|
if (!product.name?.trim()) {
|
|
126
|
-
toast.error("
|
|
127
|
+
toast.error(t("products.name_required"));
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
130
|
setSaving(true);
|
|
@@ -131,7 +132,7 @@ export default function ProductEditor() {
|
|
|
131
132
|
const payload = { product: { ...product, name: product.name.trim() }, variations };
|
|
132
133
|
const data = await call("admin-products", "save", payload);
|
|
133
134
|
const savedId = data?.product?.id || data?.id || id;
|
|
134
|
-
toast.success(isNew ? "
|
|
135
|
+
toast.success(isNew ? t("products.created") : t("products.saved"));
|
|
135
136
|
if (isNew && savedId) navigate(href(`products/${savedId}`), { replace: true });
|
|
136
137
|
else await load();
|
|
137
138
|
} catch {
|
|
@@ -143,7 +144,7 @@ export default function ProductEditor() {
|
|
|
143
144
|
|
|
144
145
|
const doDelete = async () => {
|
|
145
146
|
await call("admin-products", "delete", { id });
|
|
146
|
-
toast.success("
|
|
147
|
+
toast.success(t("products.deleted"));
|
|
147
148
|
navigate(href("products"));
|
|
148
149
|
};
|
|
149
150
|
|
|
@@ -170,7 +171,7 @@ export default function ProductEditor() {
|
|
|
170
171
|
return (
|
|
171
172
|
<div className="space-y-4">
|
|
172
173
|
<PageHeader
|
|
173
|
-
title={isNew ? "
|
|
174
|
+
title={isNew ? t("products.add_new_product") : t("routes.products_edit")}
|
|
174
175
|
backHref={href("products")}
|
|
175
176
|
/>
|
|
176
177
|
|
|
@@ -179,13 +180,13 @@ export default function ProductEditor() {
|
|
|
179
180
|
<div className="min-w-0 space-y-4">
|
|
180
181
|
<Input
|
|
181
182
|
className="h-11 text-lg font-medium"
|
|
182
|
-
placeholder="
|
|
183
|
+
placeholder={t("products.name_placeholder")}
|
|
183
184
|
value={product.name}
|
|
184
185
|
onChange={(e) => setName(e.target.value)}
|
|
185
186
|
/>
|
|
186
187
|
|
|
187
188
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
188
|
-
<span>
|
|
189
|
+
<span>{t("products.slug_label")}</span>
|
|
189
190
|
<Input
|
|
190
191
|
className="h-7 w-72 text-xs"
|
|
191
192
|
value={product.slug || ""}
|
|
@@ -198,12 +199,12 @@ export default function ProductEditor() {
|
|
|
198
199
|
|
|
199
200
|
<Card>
|
|
200
201
|
<CardHeader className="pb-2">
|
|
201
|
-
<CardTitle className="text-base">
|
|
202
|
+
<CardTitle className="text-base">{t("common.description")}</CardTitle>
|
|
202
203
|
</CardHeader>
|
|
203
204
|
<CardContent>
|
|
204
205
|
<RichTextarea
|
|
205
206
|
rows={8}
|
|
206
|
-
placeholder="
|
|
207
|
+
placeholder={t("products.description_placeholder")}
|
|
207
208
|
value={product.description || ""}
|
|
208
209
|
onChange={(v) => up({ description: v })}
|
|
209
210
|
/>
|
|
@@ -212,12 +213,12 @@ export default function ProductEditor() {
|
|
|
212
213
|
|
|
213
214
|
<Card>
|
|
214
215
|
<CardHeader className="pb-2">
|
|
215
|
-
<CardTitle className="text-base">
|
|
216
|
+
<CardTitle className="text-base">{t("products.short_description")}</CardTitle>
|
|
216
217
|
</CardHeader>
|
|
217
218
|
<CardContent>
|
|
218
219
|
<RichTextarea
|
|
219
220
|
rows={4}
|
|
220
|
-
placeholder="
|
|
221
|
+
placeholder={t("products.short_description_placeholder")}
|
|
221
222
|
value={product.short_description || ""}
|
|
222
223
|
onChange={(v) => up({ short_description: v })}
|
|
223
224
|
/>
|
|
@@ -246,19 +247,19 @@ export default function ProductEditor() {
|
|
|
246
247
|
|
|
247
248
|
<Card>
|
|
248
249
|
<CardHeader className="pb-2">
|
|
249
|
-
<CardTitle className="text-base">
|
|
250
|
+
<CardTitle className="text-base">{t("products.product_image")}</CardTitle>
|
|
250
251
|
</CardHeader>
|
|
251
252
|
<CardContent>
|
|
252
|
-
<MediaUploader value={primaryImage} onChange={setPrimaryImage} label="
|
|
253
|
+
<MediaUploader value={primaryImage} onChange={setPrimaryImage} label={t("products.set_product_image")} />
|
|
253
254
|
</CardContent>
|
|
254
255
|
</Card>
|
|
255
256
|
|
|
256
257
|
<Card>
|
|
257
258
|
<CardHeader className="pb-2">
|
|
258
|
-
<CardTitle className="text-base">
|
|
259
|
+
<CardTitle className="text-base">{t("products.product_gallery")}</CardTitle>
|
|
259
260
|
</CardHeader>
|
|
260
261
|
<CardContent>
|
|
261
|
-
<MediaUploader multiple value={gallery} onChange={setGallery} label="
|
|
262
|
+
<MediaUploader multiple value={gallery} onChange={setGallery} label={t("products.add_gallery_images")} />
|
|
262
263
|
</CardContent>
|
|
263
264
|
</Card>
|
|
264
265
|
|
|
@@ -21,6 +21,7 @@ import { Copy, ImageIcon, Package, Pencil, Plus, Star, Trash2, Zap } from "lucid
|
|
|
21
21
|
import { toast } from "sonner";
|
|
22
22
|
|
|
23
23
|
import { base44, call } from "../../lib/api";
|
|
24
|
+
import { t } from "../../i18n";
|
|
24
25
|
import usePagedList from "../../hooks/usePagedList";
|
|
25
26
|
import useAsync from "../../hooks/useAsync";
|
|
26
27
|
import useDebounce from "../../hooks/useDebounce";
|
|
@@ -59,7 +60,7 @@ function QuickEdit({ product, open, onOpenChange, onSaved }) {
|
|
|
59
60
|
setSaving(true);
|
|
60
61
|
try {
|
|
61
62
|
await call("admin-products", "save", { product: { id: product.id, ...form } });
|
|
62
|
-
toast.success("
|
|
63
|
+
toast.success(t("products.updated"));
|
|
63
64
|
onOpenChange(false);
|
|
64
65
|
onSaved();
|
|
65
66
|
} catch {
|
|
@@ -74,20 +75,20 @@ function QuickEdit({ product, open, onOpenChange, onSaved }) {
|
|
|
74
75
|
<DialogContent className="sm:max-w-sm">
|
|
75
76
|
<DialogHeader>
|
|
76
77
|
<DialogTitle className="flex items-center gap-2">
|
|
77
|
-
<Zap className="h-4 w-4" />
|
|
78
|
+
<Zap className="h-4 w-4" /> {t("products.quick_edit")}
|
|
78
79
|
</DialogTitle>
|
|
79
80
|
</DialogHeader>
|
|
80
81
|
<div className="space-y-3">
|
|
81
82
|
<div className="space-y-1.5">
|
|
82
|
-
<Label>
|
|
83
|
+
<Label>{t("products.regular_price")}</Label>
|
|
83
84
|
<MoneyInput value={form.regular_price} onChange={(v) => setForm((f) => ({ ...f, regular_price: v }))} />
|
|
84
85
|
</div>
|
|
85
86
|
<div className="space-y-1.5">
|
|
86
|
-
<Label>
|
|
87
|
+
<Label>{t("products.sale_price")}</Label>
|
|
87
88
|
<MoneyInput value={form.sale_price} onChange={(v) => setForm((f) => ({ ...f, sale_price: v }))} />
|
|
88
89
|
</div>
|
|
89
90
|
<div className="space-y-1.5">
|
|
90
|
-
<Label>
|
|
91
|
+
<Label>{t("products.stock_quantity")}</Label>
|
|
91
92
|
<Input
|
|
92
93
|
type="number"
|
|
93
94
|
value={form.stock_quantity ?? ""}
|
|
@@ -97,7 +98,7 @@ function QuickEdit({ product, open, onOpenChange, onSaved }) {
|
|
|
97
98
|
/>
|
|
98
99
|
</div>
|
|
99
100
|
<div className="space-y-1.5">
|
|
100
|
-
<Label>
|
|
101
|
+
<Label>{t("common.status")}</Label>
|
|
101
102
|
<Select value={form.status} onValueChange={(v) => setForm((f) => ({ ...f, status: v }))}>
|
|
102
103
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
103
104
|
<SelectContent>
|
|
@@ -109,7 +110,7 @@ function QuickEdit({ product, open, onOpenChange, onSaved }) {
|
|
|
109
110
|
</div>
|
|
110
111
|
</div>
|
|
111
112
|
<DialogFooter>
|
|
112
|
-
<Button onClick={save} disabled={saving}>
|
|
113
|
+
<Button onClick={save} disabled={saving}>{t("common.save")}</Button>
|
|
113
114
|
</DialogFooter>
|
|
114
115
|
</DialogContent>
|
|
115
116
|
</Dialog>
|
|
@@ -168,7 +169,7 @@ export default function ProductsList() {
|
|
|
168
169
|
const duplicate = async (row) => {
|
|
169
170
|
const data = await call("admin-products", "duplicate", { id: row.id });
|
|
170
171
|
const newId = data?.product?.id || data?.id;
|
|
171
|
-
toast.success("
|
|
172
|
+
toast.success(t("products.duplicated"));
|
|
172
173
|
if (newId) navigate(href(`products/${newId}`));
|
|
173
174
|
else list.refetch();
|
|
174
175
|
};
|
|
@@ -178,11 +179,11 @@ export default function ProductsList() {
|
|
|
178
179
|
try {
|
|
179
180
|
if (confirmDelete === "bulk") {
|
|
180
181
|
for (const id of selected) await call("admin-products", "delete", { id });
|
|
181
|
-
toast.success(
|
|
182
|
+
toast.success(t("products.n_deleted", { count: selected.length }));
|
|
182
183
|
setSelected([]);
|
|
183
184
|
} else {
|
|
184
185
|
await call("admin-products", "delete", { id: confirmDelete.id });
|
|
185
|
-
toast.success("
|
|
186
|
+
toast.success(t("products.deleted"));
|
|
186
187
|
}
|
|
187
188
|
setConfirmDelete(null);
|
|
188
189
|
list.refetch();
|
|
@@ -209,7 +210,7 @@ export default function ProductsList() {
|
|
|
209
210
|
},
|
|
210
211
|
{
|
|
211
212
|
key: "name",
|
|
212
|
-
label: "
|
|
213
|
+
label: t("common.name"),
|
|
213
214
|
sortable: true,
|
|
214
215
|
render: (row) => (
|
|
215
216
|
<div>
|
|
@@ -218,16 +219,16 @@ export default function ProductsList() {
|
|
|
218
219
|
className="font-medium hover:underline"
|
|
219
220
|
onClick={() => navigate(href(`products/${row.id}`))}
|
|
220
221
|
>
|
|
221
|
-
{row.name || "
|
|
222
|
+
{row.name || t("products.no_title")}
|
|
222
223
|
</button>
|
|
223
|
-
{row.sku && <div className="text-xs text-muted-foreground">
|
|
224
|
+
{row.sku && <div className="text-xs text-muted-foreground">{t("products.sku_line", { sku: row.sku })}</div>}
|
|
224
225
|
</div>
|
|
225
226
|
),
|
|
226
227
|
},
|
|
227
|
-
{ key: "sku", label: "
|
|
228
|
+
{ key: "sku", label: t("products.sku"), className: "hidden md:table-cell", render: (row) => row.sku || "—" },
|
|
228
229
|
{
|
|
229
230
|
key: "stock_status",
|
|
230
|
-
label: "
|
|
231
|
+
label: t("products.stock"),
|
|
231
232
|
render: (row) => (
|
|
232
233
|
<div className="flex items-center gap-1.5">
|
|
233
234
|
<StatusBadge status={row.stock_status} map={STOCK_STATUSES} />
|
|
@@ -239,7 +240,7 @@ export default function ProductsList() {
|
|
|
239
240
|
},
|
|
240
241
|
{
|
|
241
242
|
key: "price",
|
|
242
|
-
label: "
|
|
243
|
+
label: t("products.price"),
|
|
243
244
|
sortable: true,
|
|
244
245
|
render: (row) =>
|
|
245
246
|
row.on_sale && row.sale_price != null ? (
|
|
@@ -255,7 +256,7 @@ export default function ProductsList() {
|
|
|
255
256
|
},
|
|
256
257
|
{
|
|
257
258
|
key: "categories",
|
|
258
|
-
label: "
|
|
259
|
+
label: t("routes.categories"),
|
|
259
260
|
className: "hidden lg:table-cell",
|
|
260
261
|
render: (row) => {
|
|
261
262
|
const names = (row.category_ids || []).map(categoryName).filter(Boolean);
|
|
@@ -267,7 +268,7 @@ export default function ProductsList() {
|
|
|
267
268
|
label: "★",
|
|
268
269
|
className: "w-10",
|
|
269
270
|
render: (row) => (
|
|
270
|
-
<button type="button" onClick={() => toggleFeatured(row)} title="
|
|
271
|
+
<button type="button" onClick={() => toggleFeatured(row)} title={t("products.toggle_featured")}>
|
|
271
272
|
<Star
|
|
272
273
|
className={`h-4 w-4 ${row.featured ? "fill-amber-400 text-amber-400" : "text-muted-foreground/40"}`}
|
|
273
274
|
/>
|
|
@@ -276,14 +277,14 @@ export default function ProductsList() {
|
|
|
276
277
|
},
|
|
277
278
|
{
|
|
278
279
|
key: "created_date",
|
|
279
|
-
label: "
|
|
280
|
+
label: t("products.date"),
|
|
280
281
|
sortable: true,
|
|
281
282
|
className: "hidden md:table-cell",
|
|
282
283
|
render: (row) => formatDate(row.created_date),
|
|
283
284
|
},
|
|
284
285
|
{
|
|
285
286
|
key: "status",
|
|
286
|
-
label: "
|
|
287
|
+
label: t("common.status"),
|
|
287
288
|
render: (row) => <StatusBadge status={row.status} map={PRODUCT_STATUSES} />,
|
|
288
289
|
},
|
|
289
290
|
];
|
|
@@ -291,25 +292,25 @@ export default function ProductsList() {
|
|
|
291
292
|
return (
|
|
292
293
|
<div className="space-y-4">
|
|
293
294
|
<PageHeader
|
|
294
|
-
title="
|
|
295
|
-
description="
|
|
295
|
+
title={t("routes.products")}
|
|
296
|
+
description={t("products.manage_catalog")}
|
|
296
297
|
actions={
|
|
297
298
|
<Button onClick={() => navigate(href("products/new"))}>
|
|
298
|
-
<Plus className="mr-2 h-4 w-4" />
|
|
299
|
+
<Plus className="mr-2 h-4 w-4" /> {t("products.add_new")}
|
|
299
300
|
</Button>
|
|
300
301
|
}
|
|
301
302
|
/>
|
|
302
303
|
|
|
303
304
|
<div className="flex flex-wrap items-end gap-2">
|
|
304
305
|
<Input
|
|
305
|
-
placeholder="
|
|
306
|
+
placeholder={t("products.search_placeholder")}
|
|
306
307
|
value={q}
|
|
307
308
|
onChange={(e) => setQ(e.target.value)}
|
|
308
309
|
className="w-64"
|
|
309
310
|
/>
|
|
310
311
|
<SearchSelect
|
|
311
312
|
className="w-56"
|
|
312
|
-
placeholder="
|
|
313
|
+
placeholder={t("products.all_categories")}
|
|
313
314
|
value={category}
|
|
314
315
|
onChange={setCategory}
|
|
315
316
|
search={async (query) =>
|
|
@@ -322,7 +323,7 @@ export default function ProductsList() {
|
|
|
322
323
|
<Select value={stock} onValueChange={setStock}>
|
|
323
324
|
<SelectTrigger className="w-40"><SelectValue /></SelectTrigger>
|
|
324
325
|
<SelectContent>
|
|
325
|
-
<SelectItem value={ALL}>
|
|
326
|
+
<SelectItem value={ALL}>{t("products.all_stock")}</SelectItem>
|
|
326
327
|
{STOCK_STATUSES.map((s) => (
|
|
327
328
|
<SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
|
|
328
329
|
))}
|
|
@@ -330,7 +331,7 @@ export default function ProductsList() {
|
|
|
330
331
|
</Select>
|
|
331
332
|
{selected.length > 0 && (
|
|
332
333
|
<Button variant="destructive" size="sm" onClick={() => setConfirmDelete("bulk")}>
|
|
333
|
-
<Trash2 className="mr-2 h-4 w-4" />
|
|
334
|
+
<Trash2 className="mr-2 h-4 w-4" /> {t("products.delete_count", { count: selected.length })}
|
|
334
335
|
</Button>
|
|
335
336
|
)}
|
|
336
337
|
</div>
|
|
@@ -346,15 +347,15 @@ export default function ProductsList() {
|
|
|
346
347
|
onSelectChange={setSelected}
|
|
347
348
|
pagination={{ page: list.page, hasNext: list.hasNext, onNext: list.next, onPrev: list.prev }}
|
|
348
349
|
rowActions={(row) => [
|
|
349
|
-
{ label: "
|
|
350
|
-
{ label: "
|
|
351
|
-
{ label: "
|
|
352
|
-
{ label: "
|
|
350
|
+
{ label: t("common.edit"), icon: Pencil, onClick: () => navigate(href(`products/${row.id}`)) },
|
|
351
|
+
{ label: t("products.quick_edit"), icon: Zap, onClick: () => setQuickEdit(row) },
|
|
352
|
+
{ label: t("products.duplicate"), icon: Copy, onClick: () => duplicate(row) },
|
|
353
|
+
{ label: t("common.delete"), icon: Trash2, destructive: true, onClick: () => setConfirmDelete(row) },
|
|
353
354
|
]}
|
|
354
355
|
empty={{
|
|
355
356
|
icon: Package,
|
|
356
|
-
title: "
|
|
357
|
-
description: usingSearch ? "
|
|
357
|
+
title: t("products.empty_title"),
|
|
358
|
+
description: usingSearch ? t("products.empty_search") : t("products.empty_create"),
|
|
358
359
|
}}
|
|
359
360
|
/>
|
|
360
361
|
|
|
@@ -368,9 +369,13 @@ export default function ProductsList() {
|
|
|
368
369
|
<ConfirmDialog
|
|
369
370
|
open={Boolean(confirmDelete)}
|
|
370
371
|
onOpenChange={(o) => !o && setConfirmDelete(null)}
|
|
371
|
-
title={
|
|
372
|
-
|
|
373
|
-
|
|
372
|
+
title={
|
|
373
|
+
confirmDelete === "bulk"
|
|
374
|
+
? t("products.confirm_delete_bulk", { count: selected.length })
|
|
375
|
+
: t("products.confirm_delete_one", { name: confirmDelete?.name })
|
|
376
|
+
}
|
|
377
|
+
description={t("products.confirm_delete_description")}
|
|
378
|
+
confirmLabel={t("common.delete")}
|
|
374
379
|
onConfirm={doDelete}
|
|
375
380
|
loading={deleting}
|
|
376
381
|
/>
|