@base44/app-plugin-commerce 0.7.1 → 0.8.1
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 +22 -9
- package/skills/commerce/references/admin-localization.md +95 -0
- package/skills/commerce/references/catalog-rendering.md +1 -1
- 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
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "recharts";
|
|
19
19
|
|
|
20
20
|
import { call } from "../../lib/api";
|
|
21
|
+
import { t } from "../../i18n";
|
|
21
22
|
import useAsync from "../../hooks/useAsync";
|
|
22
23
|
import useRealtime from "../../hooks/useRealtime";
|
|
23
24
|
import useMoney from "../../hooks/useMoney";
|
|
@@ -105,13 +106,13 @@ function SalesTab({ range, setRange }) {
|
|
|
105
106
|
);
|
|
106
107
|
|
|
107
108
|
const stats = [
|
|
108
|
-
{ label: "
|
|
109
|
-
{ label: "
|
|
110
|
-
{ label: "
|
|
111
|
-
{ label: "
|
|
112
|
-
{ label: "
|
|
113
|
-
{ label: "
|
|
114
|
-
{ label: "
|
|
109
|
+
{ label: t("reports.gross_sales"), value: money.format(num(totals.gross_sales, totals.gross)) },
|
|
110
|
+
{ label: t("reports.net_sales"), value: money.format(num(totals.net_sales, totals.net)) },
|
|
111
|
+
{ label: t("routes.orders"), value: num(totals.orders, totals.orders_count) },
|
|
112
|
+
{ label: t("reports.items_sold"), value: num(totals.items, totals.items_sold) },
|
|
113
|
+
{ label: t("status.order.refunded"), value: money.format(num(totals.refunds, totals.refunded)) },
|
|
114
|
+
{ label: t("reports.discounted"), value: money.format(num(totals.discount, totals.discounted)) },
|
|
115
|
+
{ label: t("reports.shipping"), value: money.format(num(totals.shipping, totals.shipping_total)) },
|
|
115
116
|
];
|
|
116
117
|
|
|
117
118
|
return (
|
|
@@ -127,7 +128,7 @@ function SalesTab({ range, setRange }) {
|
|
|
127
128
|
<Card>
|
|
128
129
|
<CardHeader className="pb-2">
|
|
129
130
|
<CardTitle className="text-base">
|
|
130
|
-
|
|
131
|
+
{t(`reports.sales_by_${interval}`)}
|
|
131
132
|
</CardTitle>
|
|
132
133
|
</CardHeader>
|
|
133
134
|
<CardContent>
|
|
@@ -135,7 +136,7 @@ function SalesTab({ range, setRange }) {
|
|
|
135
136
|
<Skeleton className="h-72 w-full" />
|
|
136
137
|
) : series.length === 0 ? (
|
|
137
138
|
<p className="py-16 text-center text-sm text-muted-foreground">
|
|
138
|
-
|
|
139
|
+
{t("reports.no_sales_in_period")}
|
|
139
140
|
</p>
|
|
140
141
|
) : (
|
|
141
142
|
<ResponsiveContainer width="100%" height={300}>
|
|
@@ -151,7 +152,7 @@ function SalesTab({ range, setRange }) {
|
|
|
151
152
|
<YAxis yAxisId="count" orientation="right" tick={{ fontSize: 12 }} allowDecimals={false} />
|
|
152
153
|
<Tooltip
|
|
153
154
|
formatter={(value, name) =>
|
|
154
|
-
name === "
|
|
155
|
+
name === t("routes.orders") ? [value, name] : [money.format(value), name]
|
|
155
156
|
}
|
|
156
157
|
/>
|
|
157
158
|
<Legend />
|
|
@@ -159,7 +160,7 @@ function SalesTab({ range, setRange }) {
|
|
|
159
160
|
yAxisId="money"
|
|
160
161
|
type="monotone"
|
|
161
162
|
dataKey="net"
|
|
162
|
-
name="
|
|
163
|
+
name={t("reports.net_sales")}
|
|
163
164
|
stroke="#6366f1"
|
|
164
165
|
strokeWidth={2}
|
|
165
166
|
dot={false}
|
|
@@ -168,7 +169,7 @@ function SalesTab({ range, setRange }) {
|
|
|
168
169
|
yAxisId="count"
|
|
169
170
|
type="monotone"
|
|
170
171
|
dataKey="orders"
|
|
171
|
-
name="
|
|
172
|
+
name={t("routes.orders")}
|
|
172
173
|
stroke="#22c55e"
|
|
173
174
|
strokeWidth={2}
|
|
174
175
|
dot={false}
|
|
@@ -219,14 +220,14 @@ function TopSellersTab({ range, setRange }) {
|
|
|
219
220
|
|
|
220
221
|
<Card>
|
|
221
222
|
<CardHeader className="pb-2">
|
|
222
|
-
<CardTitle className="text-base">
|
|
223
|
+
<CardTitle className="text-base">{t("reports.top_sellers_by_quantity")}</CardTitle>
|
|
223
224
|
</CardHeader>
|
|
224
225
|
<CardContent>
|
|
225
226
|
{loading ? (
|
|
226
227
|
<Skeleton className="h-64 w-full" />
|
|
227
228
|
) : rows.length === 0 ? (
|
|
228
229
|
<p className="py-16 text-center text-sm text-muted-foreground">
|
|
229
|
-
|
|
230
|
+
{t("reports.no_sales_in_period")}
|
|
230
231
|
</p>
|
|
231
232
|
) : (
|
|
232
233
|
<ResponsiveContainer width="100%" height={Math.max(220, rows.length * 36)}>
|
|
@@ -239,8 +240,8 @@ function TopSellersTab({ range, setRange }) {
|
|
|
239
240
|
width={180}
|
|
240
241
|
tick={{ fontSize: 12 }}
|
|
241
242
|
/>
|
|
242
|
-
<Tooltip formatter={(v, name) => (name === "
|
|
243
|
-
<Bar dataKey="quantity" name="
|
|
243
|
+
<Tooltip formatter={(v, name) => (name === t("reports.quantity") ? [v, name] : [money.format(v), name])} />
|
|
244
|
+
<Bar dataKey="quantity" name={t("reports.quantity")} fill="#6366f1" radius={[0, 4, 4, 0]} />
|
|
244
245
|
</BarChart>
|
|
245
246
|
</ResponsiveContainer>
|
|
246
247
|
)}
|
|
@@ -251,7 +252,7 @@ function TopSellersTab({ range, setRange }) {
|
|
|
251
252
|
columns={[
|
|
252
253
|
{
|
|
253
254
|
key: "name",
|
|
254
|
-
label: "
|
|
255
|
+
label: t("reports.product"),
|
|
255
256
|
render: (r) =>
|
|
256
257
|
r.product_id ? (
|
|
257
258
|
<Link to={href(`products/${r.product_id}`)} className="font-medium hover:underline">
|
|
@@ -261,12 +262,12 @@ function TopSellersTab({ range, setRange }) {
|
|
|
261
262
|
r.name
|
|
262
263
|
),
|
|
263
264
|
},
|
|
264
|
-
{ key: "quantity", label: "
|
|
265
|
-
{ key: "total", label: "
|
|
265
|
+
{ key: "quantity", label: t("reports.quantity_sold") },
|
|
266
|
+
{ key: "total", label: t("reports.net_revenue"), render: (r) => money.format(r.total) },
|
|
266
267
|
]}
|
|
267
268
|
rows={rows}
|
|
268
269
|
loading={loading}
|
|
269
|
-
empty={{ icon: BarChart3, title: "
|
|
270
|
+
empty={{ icon: BarChart3, title: t("reports.empty_no_sales") }}
|
|
270
271
|
/>
|
|
271
272
|
</div>
|
|
272
273
|
);
|
|
@@ -359,30 +360,30 @@ function TotalsTab() {
|
|
|
359
360
|
return (
|
|
360
361
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
361
362
|
<TotalsCard
|
|
362
|
-
title="
|
|
363
|
+
title={t("reports.orders_by_status")}
|
|
363
364
|
loading={loading}
|
|
364
365
|
rows={normalizeTotals(d["orders-totals"])}
|
|
365
366
|
renderLabel={(r) => <StatusBadge status={r.key} className="text-[10px]" />}
|
|
366
367
|
/>
|
|
367
368
|
<TotalsCard
|
|
368
|
-
title="
|
|
369
|
+
title={t("reports.products_by_status")}
|
|
369
370
|
loading={loading}
|
|
370
371
|
rows={normalizeTotals(d["products-totals"], PRODUCT_STATUSES)}
|
|
371
372
|
/>
|
|
372
|
-
<TotalsCard title="
|
|
373
|
+
<TotalsCard title={t("routes.customers")} loading={loading} rows={normalizeTotals(d["customers-totals"])} />
|
|
373
374
|
<TotalsCard
|
|
374
|
-
title="
|
|
375
|
+
title={t("reports.coupons_by_type")}
|
|
375
376
|
loading={loading}
|
|
376
377
|
rows={normalizeTotals(d["coupons-totals"], COUPON_TYPES)}
|
|
377
378
|
/>
|
|
378
379
|
<TotalsCard
|
|
379
|
-
title="
|
|
380
|
+
title={t("reports.reviews_by_status")}
|
|
380
381
|
loading={loading}
|
|
381
382
|
rows={normalizeTotals(d["reviews-totals"], REVIEW_STATUSES)}
|
|
382
383
|
/>
|
|
383
|
-
<TotalsCard title="
|
|
384
|
-
<TotalsCard title="
|
|
385
|
-
<TotalsCard title="
|
|
384
|
+
<TotalsCard title={t("routes.categories")} loading={loading} rows={normalizeTotals(d["categories-totals"])} />
|
|
385
|
+
<TotalsCard title={t("reports.ribbons")} loading={loading} rows={normalizeTotals(d["ribbons-totals"])} />
|
|
386
|
+
<TotalsCard title={t("reports.attributes")} loading={loading} rows={normalizeTotals(d["attributes-totals"])} />
|
|
386
387
|
</div>
|
|
387
388
|
);
|
|
388
389
|
}
|
|
@@ -394,12 +395,12 @@ export default function Reports() {
|
|
|
394
395
|
|
|
395
396
|
return (
|
|
396
397
|
<div>
|
|
397
|
-
<PageHeader title="
|
|
398
|
+
<PageHeader title={t("routes.reports")} description={t("reports.subtitle")} />
|
|
398
399
|
<Tabs defaultValue="sales">
|
|
399
400
|
<TabsList className="mb-4">
|
|
400
|
-
<TabsTrigger value="sales">
|
|
401
|
-
<TabsTrigger value="top-sellers">
|
|
402
|
-
<TabsTrigger value="totals">
|
|
401
|
+
<TabsTrigger value="sales">{t("reports.tab_sales")}</TabsTrigger>
|
|
402
|
+
<TabsTrigger value="top-sellers">{t("reports.tab_top_sellers")}</TabsTrigger>
|
|
403
|
+
<TabsTrigger value="totals">{t("reports.tab_totals")}</TabsTrigger>
|
|
403
404
|
</TabsList>
|
|
404
405
|
<TabsContent value="sales">
|
|
405
406
|
<SalesTab range={range} setRange={setRange} />
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { Settings2 } from "lucide-react";
|
|
25
25
|
import { EMAIL_TYPES } from "../../lib/constants";
|
|
26
26
|
import { call } from "../../lib/api";
|
|
27
|
+
import { t } from "../../i18n";
|
|
27
28
|
import useGroupForm from "./useGroupForm";
|
|
28
29
|
|
|
29
30
|
const DEFAULTS = {
|
|
@@ -69,13 +70,13 @@ export default function EmailsSettings() {
|
|
|
69
70
|
typeCfg(e.id).recipient ||
|
|
70
71
|
recipientsText ||
|
|
71
72
|
effective?.recipients.join(", ") ||
|
|
72
|
-
"
|
|
73
|
+
t("settings.emails.admin");
|
|
73
74
|
|
|
74
75
|
// One list for every email; a delimiter row splits the admin-targeted
|
|
75
76
|
// emails from the ones the customer receives.
|
|
76
77
|
const GROUPS = [
|
|
77
|
-
{ label: "
|
|
78
|
-
{ label: "
|
|
78
|
+
{ label: t("settings.emails.sent_to_admin"), types: EMAIL_TYPES.filter((e) => e.recipient === "admin") },
|
|
79
|
+
{ label: t("settings.emails.sent_to_customer"), types: EMAIL_TYPES.filter((e) => e.recipient === "customer") },
|
|
79
80
|
];
|
|
80
81
|
|
|
81
82
|
const active = activeId ? EMAIL_TYPES.find((e) => e.id === activeId) : null;
|
|
@@ -85,42 +86,39 @@ export default function EmailsSettings() {
|
|
|
85
86
|
<Card>
|
|
86
87
|
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
87
88
|
<div className="space-y-1.5">
|
|
88
|
-
<CardTitle>
|
|
89
|
+
<CardTitle>{t("settings.emails.sender_options")}</CardTitle>
|
|
89
90
|
<CardDescription>
|
|
90
|
-
|
|
91
|
-
address is controlled by your Base44 email configuration, not here.
|
|
91
|
+
{t("settings.emails.sender_options_description")}
|
|
92
92
|
</CardDescription>
|
|
93
93
|
</div>
|
|
94
94
|
<SaveButton />
|
|
95
95
|
</CardHeader>
|
|
96
96
|
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
97
97
|
<div className="space-y-1.5 sm:col-span-2">
|
|
98
|
-
<Label>
|
|
98
|
+
<Label>{t("setup.store_name")}</Label>
|
|
99
99
|
<Input
|
|
100
100
|
value={form.store_name || ""}
|
|
101
|
-
placeholder="
|
|
101
|
+
placeholder={t("setup.store_name_placeholder")}
|
|
102
102
|
onChange={(e) => setField("store_name", e.target.value)}
|
|
103
103
|
/>
|
|
104
104
|
<p className="text-xs text-muted-foreground">
|
|
105
|
-
|
|
106
|
-
“[{storeName || "Your store"}]: New order #1002”, sent from {storeName || "Your store"}.
|
|
107
|
-
Leave it blank to send as your app's name.
|
|
105
|
+
{t("settings.emails.store_name_hint", { name: storeName || t("settings.emails.your_store") })}
|
|
108
106
|
</p>
|
|
109
107
|
</div>
|
|
110
108
|
<div className="space-y-1.5 sm:col-span-2">
|
|
111
|
-
<Label>
|
|
109
|
+
<Label>{t("settings.emails.admin_recipients")}</Label>
|
|
112
110
|
<Input
|
|
113
111
|
value={recipientsText}
|
|
114
|
-
placeholder={adminUsers || "
|
|
112
|
+
placeholder={adminUsers || t("settings.emails.recipients_placeholder")}
|
|
115
113
|
onChange={(e) => setRecipients(e.target.value)}
|
|
116
114
|
/>
|
|
117
115
|
<p className="text-xs text-muted-foreground">
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
{t("settings.emails.recipients_hint", {
|
|
117
|
+
users: adminUsers || t("settings.emails.admin_users_fallback"),
|
|
118
|
+
})}
|
|
121
119
|
{effective && !adminUsers && (
|
|
122
120
|
<span className="block">
|
|
123
|
-
|
|
121
|
+
{t("settings.emails.no_admin_warning")}
|
|
124
122
|
</span>
|
|
125
123
|
)}
|
|
126
124
|
</p>
|
|
@@ -131,10 +129,9 @@ export default function EmailsSettings() {
|
|
|
131
129
|
<Card>
|
|
132
130
|
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
133
131
|
<div className="space-y-1.5">
|
|
134
|
-
<CardTitle>
|
|
132
|
+
<CardTitle>{t("settings.emails.notifications")}</CardTitle>
|
|
135
133
|
<CardDescription>
|
|
136
|
-
|
|
137
|
-
low-stock and out-of-stock alerts.
|
|
134
|
+
{t("settings.emails.notifications_description")}
|
|
138
135
|
</CardDescription>
|
|
139
136
|
</div>
|
|
140
137
|
<SaveButton />
|
|
@@ -143,9 +140,9 @@ export default function EmailsSettings() {
|
|
|
143
140
|
<Table>
|
|
144
141
|
<TableHeader>
|
|
145
142
|
<TableRow>
|
|
146
|
-
<TableHead>
|
|
147
|
-
<TableHead>
|
|
148
|
-
<TableHead className="w-24">
|
|
143
|
+
<TableHead>{t("common.email")}</TableHead>
|
|
144
|
+
<TableHead>{t("settings.emails.recipient")}</TableHead>
|
|
145
|
+
<TableHead className="w-24">{t("settings.emails.enabled")}</TableHead>
|
|
149
146
|
<TableHead className="w-24" />
|
|
150
147
|
</TableRow>
|
|
151
148
|
</TableHeader>
|
|
@@ -167,10 +164,10 @@ export default function EmailsSettings() {
|
|
|
167
164
|
<TableCell>
|
|
168
165
|
<div className="flex items-center gap-2">
|
|
169
166
|
<span className="font-medium">{e.label}</span>
|
|
170
|
-
{e.manual && <Badge variant="outline" className="font-normal">
|
|
167
|
+
{e.manual && <Badge variant="outline" className="font-normal">{t("settings.emails.manual")}</Badge>}
|
|
171
168
|
{managed && (
|
|
172
169
|
<Badge variant="secondary" className="font-normal text-muted-foreground">
|
|
173
|
-
|
|
170
|
+
{t("settings.emails.managed_by_auth")}
|
|
174
171
|
</Badge>
|
|
175
172
|
)}
|
|
176
173
|
</div>
|
|
@@ -180,7 +177,7 @@ export default function EmailsSettings() {
|
|
|
180
177
|
? "—"
|
|
181
178
|
: e.recipient === "admin"
|
|
182
179
|
? adminRecipient(e)
|
|
183
|
-
: "
|
|
180
|
+
: t("routes.customer")}
|
|
184
181
|
</TableCell>
|
|
185
182
|
<TableCell>
|
|
186
183
|
{managed ? (
|
|
@@ -193,7 +190,7 @@ export default function EmailsSettings() {
|
|
|
193
190
|
{!managed && (
|
|
194
191
|
<Button variant="ghost" size="sm" onClick={() => setActiveId(e.id)}>
|
|
195
192
|
<Settings2 className="mr-2 h-4 w-4" />
|
|
196
|
-
|
|
193
|
+
{t("settings.emails.manage")}
|
|
197
194
|
</Button>
|
|
198
195
|
)}
|
|
199
196
|
</TableCell>
|
|
@@ -211,57 +208,57 @@ export default function EmailsSettings() {
|
|
|
211
208
|
<Dialog open onOpenChange={(o) => !o && setActiveId(null)}>
|
|
212
209
|
<DialogContent>
|
|
213
210
|
<DialogHeader>
|
|
214
|
-
<DialogTitle>{active.label}
|
|
211
|
+
<DialogTitle>{t("settings.emails.type_email_title", { type: active.label })}</DialogTitle>
|
|
215
212
|
</DialogHeader>
|
|
216
213
|
<div className="space-y-4">
|
|
217
214
|
{active.recipient === "admin" && (
|
|
218
215
|
<div className="space-y-1.5">
|
|
219
|
-
<Label>
|
|
216
|
+
<Label>{t("settings.emails.recipients_label")}</Label>
|
|
220
217
|
<Input
|
|
221
|
-
placeholder="
|
|
218
|
+
placeholder={t("settings.emails.recipient_placeholder")}
|
|
222
219
|
value={typeCfg(active.id).recipient || ""}
|
|
223
220
|
onChange={(e) => setTypeField(active.id, "recipient", e.target.value)}
|
|
224
221
|
/>
|
|
225
222
|
<p className="text-xs text-muted-foreground">
|
|
226
|
-
|
|
223
|
+
{t("settings.emails.recipient_override_hint")}
|
|
227
224
|
</p>
|
|
228
225
|
</div>
|
|
229
226
|
)}
|
|
230
227
|
{!active.stock && (
|
|
231
228
|
<>
|
|
232
229
|
<div className="space-y-1.5">
|
|
233
|
-
<Label>
|
|
230
|
+
<Label>{t("settings.emails.subject")}</Label>
|
|
234
231
|
<Input
|
|
235
|
-
placeholder="
|
|
232
|
+
placeholder={t("settings.emails.default_placeholder")}
|
|
236
233
|
value={typeCfg(active.id).subject || ""}
|
|
237
234
|
onChange={(e) => setTypeField(active.id, "subject", e.target.value)}
|
|
238
235
|
/>
|
|
239
236
|
</div>
|
|
240
237
|
<div className="space-y-1.5">
|
|
241
|
-
<Label>
|
|
238
|
+
<Label>{t("settings.emails.heading")}</Label>
|
|
242
239
|
<Input
|
|
243
|
-
placeholder="
|
|
240
|
+
placeholder={t("settings.emails.default_placeholder")}
|
|
244
241
|
value={typeCfg(active.id).heading || ""}
|
|
245
242
|
onChange={(e) => setTypeField(active.id, "heading", e.target.value)}
|
|
246
243
|
/>
|
|
247
244
|
</div>
|
|
248
245
|
<div className="space-y-1.5">
|
|
249
|
-
<Label>
|
|
246
|
+
<Label>{t("settings.emails.additional_content")}</Label>
|
|
250
247
|
<Textarea
|
|
251
248
|
rows={3}
|
|
252
|
-
placeholder="
|
|
249
|
+
placeholder={t("settings.emails.additional_content_placeholder")}
|
|
253
250
|
value={typeCfg(active.id).additional_content || ""}
|
|
254
251
|
onChange={(e) => setTypeField(active.id, "additional_content", e.target.value)}
|
|
255
252
|
/>
|
|
256
253
|
</div>
|
|
257
254
|
<p className="text-xs text-muted-foreground">
|
|
258
|
-
|
|
255
|
+
{t("settings.emails.placeholders_hint")}
|
|
259
256
|
</p>
|
|
260
257
|
</>
|
|
261
258
|
)}
|
|
262
259
|
</div>
|
|
263
260
|
<DialogFooter>
|
|
264
|
-
<Button onClick={() => setActiveId(null)}>
|
|
261
|
+
<Button onClick={() => setActiveId(null)}>{t("settings.emails.done")}</Button>
|
|
265
262
|
</DialogFooter>
|
|
266
263
|
</DialogContent>
|
|
267
264
|
</Dialog>
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
SelectValue,
|
|
11
11
|
} from "@/components/ui/select";
|
|
12
12
|
import { CURRENCIES, WEIGHT_UNITS, DIMENSION_UNITS } from "../../lib/constants";
|
|
13
|
+
import { t, currencyName } from "../../i18n";
|
|
13
14
|
import useGroupForm from "./useGroupForm";
|
|
14
15
|
|
|
15
16
|
const DEFAULTS = {
|
|
@@ -27,22 +28,21 @@ export default function GeneralSettings() {
|
|
|
27
28
|
<Card>
|
|
28
29
|
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
29
30
|
<div className="space-y-1.5">
|
|
30
|
-
<CardTitle>
|
|
31
|
-
<CardDescription>
|
|
31
|
+
<CardTitle>{t("settings.general.checkout")}</CardTitle>
|
|
32
|
+
<CardDescription>{t("settings.general.checkout_description")}</CardDescription>
|
|
32
33
|
</div>
|
|
33
34
|
<SaveButton />
|
|
34
35
|
</CardHeader>
|
|
35
36
|
<CardContent>
|
|
36
37
|
<div className="space-y-1.5">
|
|
37
|
-
<Label>
|
|
38
|
+
<Label>{t("settings.general.payment_return_path")}</Label>
|
|
38
39
|
<Input
|
|
39
40
|
placeholder="/order-received"
|
|
40
41
|
value={form.order_received_path || ""}
|
|
41
42
|
onChange={(e) => setField("order_received_path", e.target.value)}
|
|
42
43
|
/>
|
|
43
44
|
<p className="text-xs text-muted-foreground">
|
|
44
|
-
|
|
45
|
-
address. It must match the page you built — a wrong path sends paying customers to a 404.
|
|
45
|
+
{t("settings.general.payment_return_path_hint")}
|
|
46
46
|
</p>
|
|
47
47
|
</div>
|
|
48
48
|
</CardContent>
|
|
@@ -51,17 +51,16 @@ export default function GeneralSettings() {
|
|
|
51
51
|
<Card>
|
|
52
52
|
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
53
53
|
<div className="space-y-1.5">
|
|
54
|
-
<CardTitle>
|
|
54
|
+
<CardTitle>{t("settings.general.currency")}</CardTitle>
|
|
55
55
|
<CardDescription>
|
|
56
|
-
|
|
57
|
-
(Intl) — there is nothing else to configure.
|
|
56
|
+
{t("settings.general.currency_description")}
|
|
58
57
|
</CardDescription>
|
|
59
58
|
</div>
|
|
60
59
|
<SaveButton />
|
|
61
60
|
</CardHeader>
|
|
62
61
|
<CardContent>
|
|
63
62
|
<div className="max-w-xs space-y-1.5">
|
|
64
|
-
<Label>
|
|
63
|
+
<Label>{t("settings.general.currency")}</Label>
|
|
65
64
|
<Select value={form.currency} onValueChange={(v) => setField("currency", v)}>
|
|
66
65
|
<SelectTrigger>
|
|
67
66
|
<SelectValue />
|
|
@@ -69,7 +68,7 @@ export default function GeneralSettings() {
|
|
|
69
68
|
<SelectContent>
|
|
70
69
|
{CURRENCIES.map((c) => (
|
|
71
70
|
<SelectItem key={c.code} value={c.code}>
|
|
72
|
-
{c.name} ({c.symbol})
|
|
71
|
+
{currencyName(c.code, c.name)} ({c.symbol})
|
|
73
72
|
</SelectItem>
|
|
74
73
|
))}
|
|
75
74
|
</SelectContent>
|
|
@@ -81,14 +80,14 @@ export default function GeneralSettings() {
|
|
|
81
80
|
<Card>
|
|
82
81
|
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
83
82
|
<div className="space-y-1.5">
|
|
84
|
-
<CardTitle>
|
|
85
|
-
<CardDescription>
|
|
83
|
+
<CardTitle>{t("settings.general.measurements")}</CardTitle>
|
|
84
|
+
<CardDescription>{t("settings.general.measurements_description")}</CardDescription>
|
|
86
85
|
</div>
|
|
87
86
|
<SaveButton />
|
|
88
87
|
</CardHeader>
|
|
89
88
|
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
90
89
|
<div className="space-y-1.5">
|
|
91
|
-
<Label>
|
|
90
|
+
<Label>{t("settings.general.weight_unit")}</Label>
|
|
92
91
|
<Select value={form.weight_unit} onValueChange={(v) => setField("weight_unit", v)}>
|
|
93
92
|
<SelectTrigger>
|
|
94
93
|
<SelectValue />
|
|
@@ -103,7 +102,7 @@ export default function GeneralSettings() {
|
|
|
103
102
|
</Select>
|
|
104
103
|
</div>
|
|
105
104
|
<div className="space-y-1.5">
|
|
106
|
-
<Label>
|
|
105
|
+
<Label>{t("settings.general.dimension_unit")}</Label>
|
|
107
106
|
<Select value={form.dimension_unit} onValueChange={(v) => setField("dimension_unit", v)}>
|
|
108
107
|
<SelectTrigger>
|
|
109
108
|
<SelectValue />
|
|
@@ -3,6 +3,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|
|
3
3
|
import { Input } from "@/components/ui/input";
|
|
4
4
|
import { Label } from "@/components/ui/label";
|
|
5
5
|
import { Switch } from "@/components/ui/switch";
|
|
6
|
+
import { t } from "../../i18n";
|
|
6
7
|
import useGroupForm from "./useGroupForm";
|
|
7
8
|
|
|
8
9
|
const DEFAULTS = {
|
|
@@ -25,22 +26,22 @@ export default function InventorySettings() {
|
|
|
25
26
|
<Card>
|
|
26
27
|
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
27
28
|
<div className="space-y-1.5">
|
|
28
|
-
<CardTitle>
|
|
29
|
-
<CardDescription>
|
|
29
|
+
<CardTitle>{t("routes.settings_inventory")}</CardTitle>
|
|
30
|
+
<CardDescription>{t("settings.inventory.description")}</CardDescription>
|
|
30
31
|
</div>
|
|
31
32
|
<SaveButton />
|
|
32
33
|
</CardHeader>
|
|
33
34
|
<CardContent className="space-y-4">
|
|
34
35
|
<div className="flex items-start justify-between gap-4">
|
|
35
36
|
<div>
|
|
36
|
-
<Label>
|
|
37
|
-
<p className="text-xs text-muted-foreground">
|
|
37
|
+
<Label>{t("settings.inventory.manage_stock")}</Label>
|
|
38
|
+
<p className="text-xs text-muted-foreground">{t("settings.inventory.manage_stock_hint")}</p>
|
|
38
39
|
</div>
|
|
39
40
|
<Switch checked={!!form.manage_stock} onCheckedChange={(v) => setField("manage_stock", v)} />
|
|
40
41
|
</div>
|
|
41
42
|
|
|
42
43
|
<div className="space-y-1.5">
|
|
43
|
-
<Label className={off ? "opacity-50" : ""}>
|
|
44
|
+
<Label className={off ? "opacity-50" : ""}>{t("settings.inventory.hold_stock")}</Label>
|
|
44
45
|
<Input
|
|
45
46
|
type="number"
|
|
46
47
|
min={0}
|
|
@@ -49,15 +50,13 @@ export default function InventorySettings() {
|
|
|
49
50
|
onChange={num("hold_stock_minutes", 60)}
|
|
50
51
|
/>
|
|
51
52
|
<p className="text-xs text-muted-foreground">
|
|
52
|
-
|
|
53
|
-
pending order is cancelled and its stock released. Release runs opportunistically —
|
|
54
|
-
see the commerce skill's references/operations.md.
|
|
53
|
+
{t("settings.inventory.hold_stock_hint")}
|
|
55
54
|
</p>
|
|
56
55
|
</div>
|
|
57
56
|
|
|
58
57
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
59
58
|
<div className="space-y-1.5">
|
|
60
|
-
<Label className={off ? "opacity-50" : ""}>
|
|
59
|
+
<Label className={off ? "opacity-50" : ""}>{t("settings.inventory.low_stock_threshold")}</Label>
|
|
61
60
|
<Input
|
|
62
61
|
type="number"
|
|
63
62
|
min={0}
|
|
@@ -67,7 +66,7 @@ export default function InventorySettings() {
|
|
|
67
66
|
/>
|
|
68
67
|
</div>
|
|
69
68
|
<div className="space-y-1.5">
|
|
70
|
-
<Label className={off ? "opacity-50" : ""}>
|
|
69
|
+
<Label className={off ? "opacity-50" : ""}>{t("settings.inventory.out_of_stock_threshold")}</Label>
|
|
71
70
|
<Input
|
|
72
71
|
type="number"
|
|
73
72
|
disabled={off}
|
|
@@ -82,16 +81,16 @@ export default function InventorySettings() {
|
|
|
82
81
|
<Card>
|
|
83
82
|
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
84
83
|
<div className="space-y-1.5">
|
|
85
|
-
<CardTitle>
|
|
84
|
+
<CardTitle>{t("settings.inventory.visibility")}</CardTitle>
|
|
86
85
|
</div>
|
|
87
86
|
<SaveButton />
|
|
88
87
|
</CardHeader>
|
|
89
88
|
<CardContent>
|
|
90
89
|
<div className="flex items-start justify-between gap-4">
|
|
91
90
|
<div>
|
|
92
|
-
<Label>
|
|
91
|
+
<Label>{t("settings.inventory.hide_out_of_stock")}</Label>
|
|
93
92
|
<p className="text-xs text-muted-foreground">
|
|
94
|
-
|
|
93
|
+
{t("settings.inventory.hide_out_of_stock_hint")}
|
|
95
94
|
</p>
|
|
96
95
|
</div>
|
|
97
96
|
<Switch
|