@base44/app-plugin-commerce 0.7.0 → 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 +18 -30
- package/skills/commerce/install/02-storefront.md +23 -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 -15
- 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
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { Loader2, Wand2, X } from "lucide-react";
|
|
21
21
|
|
|
22
22
|
import { call, base44 } from "../../lib/api";
|
|
23
|
+
import { t } from "../../i18n";
|
|
23
24
|
import { COUPON_TYPES } from "../../lib/constants";
|
|
24
25
|
import useMoney from "../../hooks/useMoney";
|
|
25
26
|
import { useAdminHref } from "../../context/BasePathContext";
|
|
@@ -82,7 +83,7 @@ function EmailChips({ value = [], onChange }) {
|
|
|
82
83
|
}
|
|
83
84
|
}}
|
|
84
85
|
onBlur={add}
|
|
85
|
-
placeholder="
|
|
86
|
+
placeholder={t("coupons.email_chips_placeholder")}
|
|
86
87
|
/>
|
|
87
88
|
{value.length > 0 && (
|
|
88
89
|
<div className="mt-2 flex flex-wrap gap-1">
|
|
@@ -104,13 +105,13 @@ function EmailChips({ value = [], onChange }) {
|
|
|
104
105
|
);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
function NumberField({ value, onChange, placeholder
|
|
108
|
+
function NumberField({ value, onChange, placeholder }) {
|
|
108
109
|
return (
|
|
109
110
|
<Input
|
|
110
111
|
type="number"
|
|
111
112
|
min="0"
|
|
112
113
|
value={value ?? ""}
|
|
113
|
-
placeholder={placeholder}
|
|
114
|
+
placeholder={placeholder ?? t("coupons.unlimited")}
|
|
114
115
|
onChange={(e) => onChange(e.target.value === "" ? null : Number(e.target.value))}
|
|
115
116
|
/>
|
|
116
117
|
);
|
|
@@ -189,11 +190,11 @@ export default function CouponEditor() {
|
|
|
189
190
|
|
|
190
191
|
const save = async () => {
|
|
191
192
|
if (!coupon.code?.trim()) {
|
|
192
|
-
toast.error("
|
|
193
|
+
toast.error(t("coupons.code_required"));
|
|
193
194
|
return;
|
|
194
195
|
}
|
|
195
196
|
if (coupon.amount === null || coupon.amount === undefined) {
|
|
196
|
-
toast.error("
|
|
197
|
+
toast.error(t("coupons.amount_required"));
|
|
197
198
|
return;
|
|
198
199
|
}
|
|
199
200
|
setSaving(true);
|
|
@@ -207,7 +208,7 @@ export default function CouponEditor() {
|
|
|
207
208
|
: null,
|
|
208
209
|
};
|
|
209
210
|
const saved = await call("admin-coupons", "save", { coupon: payload });
|
|
210
|
-
toast.success("
|
|
211
|
+
toast.success(t("coupons.saved"));
|
|
211
212
|
if (isNew && saved?.id) navigate(href(`coupons/${saved.id}`), { replace: true });
|
|
212
213
|
else if (saved) setCoupon((c) => ({ ...c, ...saved }));
|
|
213
214
|
} catch {
|
|
@@ -221,7 +222,7 @@ export default function CouponEditor() {
|
|
|
221
222
|
setDeleting(true);
|
|
222
223
|
try {
|
|
223
224
|
await call("admin-coupons", "delete", { id });
|
|
224
|
-
toast.success("
|
|
225
|
+
toast.success(t("coupons.deleted"));
|
|
225
226
|
navigate(href("coupons"));
|
|
226
227
|
} catch {
|
|
227
228
|
setDeleting(false);
|
|
@@ -241,7 +242,7 @@ export default function CouponEditor() {
|
|
|
241
242
|
return (
|
|
242
243
|
<div>
|
|
243
244
|
<PageHeader
|
|
244
|
-
title={isNew ? "
|
|
245
|
+
title={isNew ? t("routes.coupons_new") : t("coupons.title_with_code", { code: coupon.code })}
|
|
245
246
|
backHref={href("coupons")}
|
|
246
247
|
/>
|
|
247
248
|
|
|
@@ -251,12 +252,12 @@ export default function CouponEditor() {
|
|
|
251
252
|
<Card>
|
|
252
253
|
<CardContent className="space-y-4 pt-6">
|
|
253
254
|
<div className="grid gap-1.5">
|
|
254
|
-
<Label>
|
|
255
|
+
<Label>{t("coupons.coupon_code")}</Label>
|
|
255
256
|
<div className="flex gap-2">
|
|
256
257
|
<Input
|
|
257
258
|
value={coupon.code}
|
|
258
259
|
onChange={(e) => set("code")(e.target.value)}
|
|
259
|
-
placeholder="
|
|
260
|
+
placeholder={t("coupons.code_placeholder")}
|
|
260
261
|
className="font-mono lowercase"
|
|
261
262
|
/>
|
|
262
263
|
<Button
|
|
@@ -264,12 +265,12 @@ export default function CouponEditor() {
|
|
|
264
265
|
variant="outline"
|
|
265
266
|
onClick={() => set("code")(randomCode())}
|
|
266
267
|
>
|
|
267
|
-
<Wand2 className="mr-1 h-4 w-4" />
|
|
268
|
+
<Wand2 className="mr-1 h-4 w-4" /> {t("coupons.generate")}
|
|
268
269
|
</Button>
|
|
269
270
|
</div>
|
|
270
271
|
</div>
|
|
271
272
|
<div className="grid gap-1.5">
|
|
272
|
-
<Label>
|
|
273
|
+
<Label>{t("coupons.description_optional")}</Label>
|
|
273
274
|
<Textarea
|
|
274
275
|
rows={2}
|
|
275
276
|
value={coupon.description || ""}
|
|
@@ -282,11 +283,11 @@ export default function CouponEditor() {
|
|
|
282
283
|
{/* Coupon data: stacked cards, like the product editor's sections */}
|
|
283
284
|
<Card>
|
|
284
285
|
<CardHeader className="pb-2">
|
|
285
|
-
<CardTitle className="text-base">
|
|
286
|
+
<CardTitle className="text-base">{t("coupons.general")}</CardTitle>
|
|
286
287
|
</CardHeader>
|
|
287
288
|
<CardContent className="space-y-4">
|
|
288
289
|
<div className="grid gap-1.5 sm:max-w-sm">
|
|
289
|
-
<Label>
|
|
290
|
+
<Label>{t("coupons.discount_type")}</Label>
|
|
290
291
|
<Select value={coupon.discount_type} onValueChange={set("discount_type")}>
|
|
291
292
|
<SelectTrigger>
|
|
292
293
|
<SelectValue />
|
|
@@ -301,7 +302,7 @@ export default function CouponEditor() {
|
|
|
301
302
|
</Select>
|
|
302
303
|
</div>
|
|
303
304
|
<div className="grid gap-1.5 sm:max-w-sm">
|
|
304
|
-
<Label>
|
|
305
|
+
<Label>{isPercent ? t("coupons.amount_label_percent") : t("coupons.amount_label")}</Label>
|
|
305
306
|
{isPercent ? (
|
|
306
307
|
<div className="relative">
|
|
307
308
|
<Input
|
|
@@ -330,14 +331,14 @@ export default function CouponEditor() {
|
|
|
330
331
|
className="mt-0.5"
|
|
331
332
|
/>
|
|
332
333
|
<span>
|
|
333
|
-
|
|
334
|
+
{t("coupons.free_shipping")}
|
|
334
335
|
<span className="block text-xs text-muted-foreground">
|
|
335
|
-
|
|
336
|
+
{t("coupons.free_shipping_hint")}
|
|
336
337
|
</span>
|
|
337
338
|
</span>
|
|
338
339
|
</label>
|
|
339
340
|
<div className="grid gap-1.5 sm:max-w-sm">
|
|
340
|
-
<Label>
|
|
341
|
+
<Label>{t("coupons.coupon_expiry_date")}</Label>
|
|
341
342
|
<Input
|
|
342
343
|
type="date"
|
|
343
344
|
value={coupon.date_expires ? coupon.date_expires.slice(0, 10) : ""}
|
|
@@ -349,24 +350,24 @@ export default function CouponEditor() {
|
|
|
349
350
|
|
|
350
351
|
<Card>
|
|
351
352
|
<CardHeader className="pb-2">
|
|
352
|
-
<CardTitle className="text-base">
|
|
353
|
+
<CardTitle className="text-base">{t("coupons.usage_restriction")}</CardTitle>
|
|
353
354
|
</CardHeader>
|
|
354
355
|
<CardContent className="space-y-4">
|
|
355
356
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
356
357
|
<div className="grid gap-1.5">
|
|
357
|
-
<Label>
|
|
358
|
+
<Label>{t("coupons.minimum_spend", { code: money.code })}</Label>
|
|
358
359
|
<MoneyInput
|
|
359
360
|
value={coupon.minimum_amount}
|
|
360
361
|
onChange={set("minimum_amount")}
|
|
361
|
-
placeholder="
|
|
362
|
+
placeholder={t("coupons.no_minimum")}
|
|
362
363
|
/>
|
|
363
364
|
</div>
|
|
364
365
|
<div className="grid gap-1.5">
|
|
365
|
-
<Label>
|
|
366
|
+
<Label>{t("coupons.maximum_spend", { code: money.code })}</Label>
|
|
366
367
|
<MoneyInput
|
|
367
368
|
value={coupon.maximum_amount}
|
|
368
369
|
onChange={set("maximum_amount")}
|
|
369
|
-
placeholder="
|
|
370
|
+
placeholder={t("coupons.no_maximum")}
|
|
370
371
|
/>
|
|
371
372
|
</div>
|
|
372
373
|
</div>
|
|
@@ -377,9 +378,9 @@ export default function CouponEditor() {
|
|
|
377
378
|
className="mt-0.5"
|
|
378
379
|
/>
|
|
379
380
|
<span>
|
|
380
|
-
|
|
381
|
+
{t("coupons.individual_use")}
|
|
381
382
|
<span className="block text-xs text-muted-foreground">
|
|
382
|
-
|
|
383
|
+
{t("coupons.individual_use_hint")}
|
|
383
384
|
</span>
|
|
384
385
|
</span>
|
|
385
386
|
</label>
|
|
@@ -390,21 +391,21 @@ export default function CouponEditor() {
|
|
|
390
391
|
className="mt-0.5"
|
|
391
392
|
/>
|
|
392
393
|
<span>
|
|
393
|
-
|
|
394
|
+
{t("coupons.exclude_sale_items")}
|
|
394
395
|
<span className="block text-xs text-muted-foreground">
|
|
395
|
-
|
|
396
|
+
{t("coupons.exclude_sale_items_hint")}
|
|
396
397
|
</span>
|
|
397
398
|
</span>
|
|
398
399
|
</label>
|
|
399
400
|
<Separator />
|
|
400
401
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
401
402
|
<div className="grid gap-1.5">
|
|
402
|
-
<Label>
|
|
403
|
+
<Label>{t("routes.products")}</Label>
|
|
403
404
|
<SearchSelect
|
|
404
405
|
multiple
|
|
405
406
|
search={searchProducts}
|
|
406
407
|
value={productOpts.include}
|
|
407
|
-
placeholder="
|
|
408
|
+
placeholder={t("coupons.any_product")}
|
|
408
409
|
onChange={(opts) => {
|
|
409
410
|
setProductOpts((o) => ({ ...o, include: opts }));
|
|
410
411
|
set("product_ids")(opts.map((o) => o.value));
|
|
@@ -412,12 +413,12 @@ export default function CouponEditor() {
|
|
|
412
413
|
/>
|
|
413
414
|
</div>
|
|
414
415
|
<div className="grid gap-1.5">
|
|
415
|
-
<Label>
|
|
416
|
+
<Label>{t("coupons.exclude_products")}</Label>
|
|
416
417
|
<SearchSelect
|
|
417
418
|
multiple
|
|
418
419
|
search={searchProducts}
|
|
419
420
|
value={productOpts.exclude}
|
|
420
|
-
placeholder="
|
|
421
|
+
placeholder={t("coupons.no_exclusions")}
|
|
421
422
|
onChange={(opts) => {
|
|
422
423
|
setProductOpts((o) => ({ ...o, exclude: opts }));
|
|
423
424
|
set("excluded_product_ids")(opts.map((o) => o.value));
|
|
@@ -425,22 +426,22 @@ export default function CouponEditor() {
|
|
|
425
426
|
/>
|
|
426
427
|
</div>
|
|
427
428
|
<div className="grid gap-1.5">
|
|
428
|
-
<Label>
|
|
429
|
+
<Label>{t("coupons.product_categories")}</Label>
|
|
429
430
|
<SearchSelect
|
|
430
431
|
multiple
|
|
431
432
|
search={searchCategories}
|
|
432
433
|
value={categoryOpts(coupon.product_category_ids)}
|
|
433
|
-
placeholder="
|
|
434
|
+
placeholder={t("coupons.any_category")}
|
|
434
435
|
onChange={(opts) => set("product_category_ids")(opts.map((o) => o.value))}
|
|
435
436
|
/>
|
|
436
437
|
</div>
|
|
437
438
|
<div className="grid gap-1.5">
|
|
438
|
-
<Label>
|
|
439
|
+
<Label>{t("coupons.exclude_categories")}</Label>
|
|
439
440
|
<SearchSelect
|
|
440
441
|
multiple
|
|
441
442
|
search={searchCategories}
|
|
442
443
|
value={categoryOpts(coupon.excluded_product_category_ids)}
|
|
443
|
-
placeholder="
|
|
444
|
+
placeholder={t("coupons.no_exclusions")}
|
|
444
445
|
onChange={(opts) =>
|
|
445
446
|
set("excluded_product_category_ids")(opts.map((o) => o.value))
|
|
446
447
|
}
|
|
@@ -448,14 +449,13 @@ export default function CouponEditor() {
|
|
|
448
449
|
</div>
|
|
449
450
|
</div>
|
|
450
451
|
<div className="grid gap-1.5">
|
|
451
|
-
<Label>
|
|
452
|
+
<Label>{t("coupons.allowed_emails")}</Label>
|
|
452
453
|
<EmailChips
|
|
453
454
|
value={coupon.email_restrictions || []}
|
|
454
455
|
onChange={set("email_restrictions")}
|
|
455
456
|
/>
|
|
456
457
|
<p className="text-xs text-muted-foreground">
|
|
457
|
-
|
|
458
|
-
<code>*@example.com</code> are allowed.
|
|
458
|
+
{t("coupons.allowed_emails_hint")}
|
|
459
459
|
</p>
|
|
460
460
|
</div>
|
|
461
461
|
</CardContent>
|
|
@@ -463,36 +463,35 @@ export default function CouponEditor() {
|
|
|
463
463
|
|
|
464
464
|
<Card>
|
|
465
465
|
<CardHeader className="pb-2">
|
|
466
|
-
<CardTitle className="text-base">
|
|
466
|
+
<CardTitle className="text-base">{t("coupons.usage_limits")}</CardTitle>
|
|
467
467
|
</CardHeader>
|
|
468
468
|
<CardContent className="max-w-sm space-y-4">
|
|
469
469
|
<div className="grid gap-1.5">
|
|
470
|
-
<Label>
|
|
470
|
+
<Label>{t("coupons.usage_limit_per_coupon")}</Label>
|
|
471
471
|
<NumberField value={coupon.usage_limit} onChange={set("usage_limit")} />
|
|
472
472
|
<p className="text-xs text-muted-foreground">
|
|
473
|
-
|
|
473
|
+
{t("coupons.usage_limit_per_coupon_hint")}
|
|
474
474
|
</p>
|
|
475
475
|
</div>
|
|
476
476
|
<div className="grid gap-1.5">
|
|
477
|
-
<Label>
|
|
477
|
+
<Label>{t("coupons.limit_usage_items")}</Label>
|
|
478
478
|
<NumberField
|
|
479
479
|
value={coupon.limit_usage_to_x_items}
|
|
480
480
|
onChange={set("limit_usage_to_x_items")}
|
|
481
|
-
placeholder="
|
|
481
|
+
placeholder={t("coupons.apply_all_items")}
|
|
482
482
|
/>
|
|
483
483
|
<p className="text-xs text-muted-foreground">
|
|
484
|
-
|
|
485
|
-
discounts).
|
|
484
|
+
{t("coupons.limit_usage_items_hint")}
|
|
486
485
|
</p>
|
|
487
486
|
</div>
|
|
488
487
|
<div className="grid gap-1.5">
|
|
489
|
-
<Label>
|
|
488
|
+
<Label>{t("coupons.usage_limit_per_user")}</Label>
|
|
490
489
|
<NumberField
|
|
491
490
|
value={coupon.usage_limit_per_user}
|
|
492
491
|
onChange={set("usage_limit_per_user")}
|
|
493
492
|
/>
|
|
494
493
|
<p className="text-xs text-muted-foreground">
|
|
495
|
-
|
|
494
|
+
{t("coupons.usage_limit_per_user_hint")}
|
|
496
495
|
</p>
|
|
497
496
|
</div>
|
|
498
497
|
</CardContent>
|
|
@@ -503,11 +502,11 @@ export default function CouponEditor() {
|
|
|
503
502
|
<div className="space-y-4">
|
|
504
503
|
<Card>
|
|
505
504
|
<CardHeader className="pb-3">
|
|
506
|
-
<CardTitle className="text-base">
|
|
505
|
+
<CardTitle className="text-base">{t("coupons.publish")}</CardTitle>
|
|
507
506
|
</CardHeader>
|
|
508
507
|
<CardContent className="space-y-3">
|
|
509
508
|
<label className="flex items-center justify-between text-sm font-medium">
|
|
510
|
-
|
|
509
|
+
{t("coupons.enabled")}
|
|
511
510
|
<Switch
|
|
512
511
|
checked={coupon.enabled !== false}
|
|
513
512
|
onCheckedChange={(v) => set("enabled")(!!v)}
|
|
@@ -515,18 +514,17 @@ export default function CouponEditor() {
|
|
|
515
514
|
</label>
|
|
516
515
|
{coupon.enabled === false && (
|
|
517
516
|
<p className="text-xs text-muted-foreground">
|
|
518
|
-
|
|
517
|
+
{t("coupons.disabled_hint")}
|
|
519
518
|
</p>
|
|
520
519
|
)}
|
|
521
520
|
{!isNew && (
|
|
522
521
|
<p className="text-sm text-muted-foreground">
|
|
523
|
-
|
|
524
|
-
time{(coupon.usage_count ?? 0) === 1 ? "" : "s"}
|
|
522
|
+
{t("coupons.used_times", { count: coupon.usage_count ?? 0 })}
|
|
525
523
|
</p>
|
|
526
524
|
)}
|
|
527
525
|
<Button className="w-full" onClick={save} disabled={saving}>
|
|
528
526
|
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
529
|
-
{isNew ? "
|
|
527
|
+
{isNew ? t("coupons.create") : t("coupons.save")}
|
|
530
528
|
</Button>
|
|
531
529
|
{!isNew && (
|
|
532
530
|
<Button
|
|
@@ -534,7 +532,7 @@ export default function CouponEditor() {
|
|
|
534
532
|
className="w-full text-destructive hover:text-destructive"
|
|
535
533
|
onClick={() => setConfirmDelete(true)}
|
|
536
534
|
>
|
|
537
|
-
|
|
535
|
+
{t("common.delete")}
|
|
538
536
|
</Button>
|
|
539
537
|
)}
|
|
540
538
|
</CardContent>
|
|
@@ -545,9 +543,9 @@ export default function CouponEditor() {
|
|
|
545
543
|
<ConfirmDialog
|
|
546
544
|
open={confirmDelete}
|
|
547
545
|
onOpenChange={setConfirmDelete}
|
|
548
|
-
title="
|
|
549
|
-
description="
|
|
550
|
-
confirmLabel="
|
|
546
|
+
title={t("coupons.confirm_delete_one")}
|
|
547
|
+
description={t("coupons.confirm_delete_description")}
|
|
548
|
+
confirmLabel={t("common.delete")}
|
|
551
549
|
loading={deleting}
|
|
552
550
|
onConfirm={doDelete}
|
|
553
551
|
/>
|
|
@@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
|
|
|
6
6
|
import { Plus, Search, TicketPercent, Trash2, Pencil } from "lucide-react";
|
|
7
7
|
|
|
8
8
|
import { call, base44 } from "../../lib/api";
|
|
9
|
+
import { t } from "../../i18n";
|
|
9
10
|
import { COUPON_TYPES } from "../../lib/constants";
|
|
10
11
|
import { formatDate, truncate } from "../../lib/format";
|
|
11
12
|
import usePagedList from "../../hooks/usePagedList";
|
|
@@ -52,11 +53,11 @@ export default function CouponsList() {
|
|
|
52
53
|
try {
|
|
53
54
|
if (pendingDelete === "bulk") {
|
|
54
55
|
await call("admin-coupons", "batch", { delete: selected });
|
|
55
|
-
toast.success(
|
|
56
|
+
toast.success(t("coupons.n_deleted", { count: selected.length }));
|
|
56
57
|
setSelected([]);
|
|
57
58
|
} else {
|
|
58
59
|
await call("admin-coupons", "delete", { id: pendingDelete.id });
|
|
59
|
-
toast.success("
|
|
60
|
+
toast.success(t("coupons.deleted"));
|
|
60
61
|
}
|
|
61
62
|
setPendingDelete(null);
|
|
62
63
|
list.refetch();
|
|
@@ -70,7 +71,7 @@ export default function CouponsList() {
|
|
|
70
71
|
const columns = [
|
|
71
72
|
{
|
|
72
73
|
key: "code",
|
|
73
|
-
label: "
|
|
74
|
+
label: t("coupons.code"),
|
|
74
75
|
sortable: !debouncedQuery,
|
|
75
76
|
render: (c) => (
|
|
76
77
|
<span className="inline-flex items-center gap-2">
|
|
@@ -83,7 +84,7 @@ export default function CouponsList() {
|
|
|
83
84
|
</Link>
|
|
84
85
|
{c.enabled === false && (
|
|
85
86
|
<span className="rounded border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
86
|
-
|
|
87
|
+
{t("coupons.disabled")}
|
|
87
88
|
</span>
|
|
88
89
|
)}
|
|
89
90
|
</span>
|
|
@@ -91,23 +92,23 @@ export default function CouponsList() {
|
|
|
91
92
|
},
|
|
92
93
|
{
|
|
93
94
|
key: "discount_type",
|
|
94
|
-
label: "
|
|
95
|
+
label: t("common.type"),
|
|
95
96
|
render: (c) =>
|
|
96
97
|
COUPON_TYPES.find((t) => t.value === c.discount_type)?.label || c.discount_type,
|
|
97
98
|
},
|
|
98
|
-
{ key: "amount", label: "
|
|
99
|
+
{ key: "amount", label: t("common.amount"), render: amountLabel },
|
|
99
100
|
{
|
|
100
101
|
key: "description",
|
|
101
|
-
label: "
|
|
102
|
+
label: t("common.description"),
|
|
102
103
|
className: "max-w-64",
|
|
103
104
|
render: (c) => (
|
|
104
105
|
<span className="text-muted-foreground">{truncate(c.description, 60) || "—"}</span>
|
|
105
106
|
),
|
|
106
107
|
},
|
|
107
|
-
{ key: "usage", label: "
|
|
108
|
+
{ key: "usage", label: t("coupons.usage_limit_col"), render: usageLabel },
|
|
108
109
|
{
|
|
109
110
|
key: "date_expires",
|
|
110
|
-
label: "
|
|
111
|
+
label: t("coupons.expiry_date"),
|
|
111
112
|
render: (c) => (c.date_expires ? formatDate(c.date_expires) : "—"),
|
|
112
113
|
},
|
|
113
114
|
];
|
|
@@ -115,12 +116,12 @@ export default function CouponsList() {
|
|
|
115
116
|
return (
|
|
116
117
|
<div>
|
|
117
118
|
<PageHeader
|
|
118
|
-
title="
|
|
119
|
-
description="
|
|
119
|
+
title={t("routes.coupons")}
|
|
120
|
+
description={t("coupons.subtitle")}
|
|
120
121
|
actions={
|
|
121
122
|
<Button asChild>
|
|
122
123
|
<Link to={href("coupons/new")}>
|
|
123
|
-
<Plus className="mr-1 h-4 w-4" />
|
|
124
|
+
<Plus className="mr-1 h-4 w-4" /> {t("routes.coupons_new")}
|
|
124
125
|
</Link>
|
|
125
126
|
</Button>
|
|
126
127
|
}
|
|
@@ -131,14 +132,14 @@ export default function CouponsList() {
|
|
|
131
132
|
<Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
132
133
|
<Input
|
|
133
134
|
className="pl-8"
|
|
134
|
-
placeholder="
|
|
135
|
+
placeholder={t("coupons.search_placeholder")}
|
|
135
136
|
value={query}
|
|
136
137
|
onChange={(e) => setQuery(e.target.value)}
|
|
137
138
|
/>
|
|
138
139
|
</div>
|
|
139
140
|
{selected.length > 0 && (
|
|
140
141
|
<Button variant="outline" size="sm" onClick={() => setPendingDelete("bulk")}>
|
|
141
|
-
<Trash2 className="mr-1 h-4 w-4" />
|
|
142
|
+
<Trash2 className="mr-1 h-4 w-4" /> {t("coupons.delete_selected", { count: selected.length })}
|
|
142
143
|
</Button>
|
|
143
144
|
)}
|
|
144
145
|
</div>
|
|
@@ -154,23 +155,23 @@ export default function CouponsList() {
|
|
|
154
155
|
onSelectChange={setSelected}
|
|
155
156
|
onRowClick={(c) => navigate(href(`coupons/${c.id}`))}
|
|
156
157
|
rowActions={(c) => [
|
|
157
|
-
{ label: "
|
|
158
|
-
{ label: "
|
|
158
|
+
{ label: t("common.edit"), icon: Pencil, onClick: () => navigate(href(`coupons/${c.id}`)) },
|
|
159
|
+
{ label: t("common.delete"), icon: Trash2, destructive: true, onClick: () => setPendingDelete(c) },
|
|
159
160
|
]}
|
|
160
161
|
pagination={{ page: list.page, hasNext: list.hasNext, onNext: list.next, onPrev: list.prev }}
|
|
161
162
|
empty={{
|
|
162
163
|
icon: TicketPercent,
|
|
163
|
-
title: "
|
|
164
|
-
description: "
|
|
164
|
+
title: t("coupons.empty_title"),
|
|
165
|
+
description: t("coupons.empty_description"),
|
|
165
166
|
}}
|
|
166
167
|
/>
|
|
167
168
|
|
|
168
169
|
<ConfirmDialog
|
|
169
170
|
open={!!pendingDelete}
|
|
170
171
|
onOpenChange={(o) => !o && setPendingDelete(null)}
|
|
171
|
-
title={pendingDelete === "bulk" ?
|
|
172
|
-
description="
|
|
173
|
-
confirmLabel="
|
|
172
|
+
title={pendingDelete === "bulk" ? t("coupons.confirm_delete_many", { count: selected.length }) : t("coupons.confirm_delete_one")}
|
|
173
|
+
description={t("coupons.confirm_delete_description")}
|
|
174
|
+
confirmLabel={t("common.delete")}
|
|
174
175
|
loading={deleting}
|
|
175
176
|
onConfirm={doDelete}
|
|
176
177
|
/>
|