@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
|
@@ -24,6 +24,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|
|
24
24
|
import { Copy, ExternalLink, Loader2, Mail, Pencil, Plus, RotateCw, Ticket, Trash2, Truck, UserPlus } from "lucide-react";
|
|
25
25
|
import { toast } from "sonner";
|
|
26
26
|
|
|
27
|
+
import { t } from "../../i18n";
|
|
27
28
|
import { call, base44 } from "../../lib/api";
|
|
28
29
|
import useAsync from "../../hooks/useAsync";
|
|
29
30
|
import useDebounce from "../../hooks/useDebounce";
|
|
@@ -183,7 +184,7 @@ export default function OrderEditor() {
|
|
|
183
184
|
.catch((err) => {
|
|
184
185
|
if (cancelled) return;
|
|
185
186
|
setPriceView(null);
|
|
186
|
-
setPreviewError(err.message || "
|
|
187
|
+
setPreviewError(err.message || t("orders.preview_failed_generic"));
|
|
187
188
|
})
|
|
188
189
|
.finally(() => !cancelled && setPreviewing(false));
|
|
189
190
|
return () => {
|
|
@@ -213,7 +214,7 @@ export default function OrderEditor() {
|
|
|
213
214
|
if (!dirty) return;
|
|
214
215
|
setSaving(true);
|
|
215
216
|
try {
|
|
216
|
-
if (await persist()) toast.success("
|
|
217
|
+
if (await persist()) toast.success(t("orders.saved"));
|
|
217
218
|
refetchAll();
|
|
218
219
|
} catch {
|
|
219
220
|
/* toast handled by call() */
|
|
@@ -248,7 +249,7 @@ export default function OrderEditor() {
|
|
|
248
249
|
"recalculate",
|
|
249
250
|
async () => {
|
|
250
251
|
await call("admin-orders", "recalculate", { order_id: id });
|
|
251
|
-
toast.success(dirty ? "
|
|
252
|
+
toast.success(dirty ? t("orders.saved_recalculated") : t("orders.recalculated"));
|
|
252
253
|
},
|
|
253
254
|
{ flush: true }
|
|
254
255
|
);
|
|
@@ -256,13 +257,13 @@ export default function OrderEditor() {
|
|
|
256
257
|
const sendInvoice = () =>
|
|
257
258
|
serverOp("invoice", async () => {
|
|
258
259
|
await call("admin-orders", "send-email", { order_id: id, type: "customer_invoice" });
|
|
259
|
-
toast.success("
|
|
260
|
+
toast.success(t("orders.invoice_sent"));
|
|
260
261
|
});
|
|
261
262
|
|
|
262
263
|
const deleteOrder = () =>
|
|
263
264
|
serverOp("delete", async () => {
|
|
264
265
|
await call("admin-orders", "delete", { order_id: id });
|
|
265
|
-
toast.success("
|
|
266
|
+
toast.success(t("orders.deleted"));
|
|
266
267
|
navigate(href("orders"));
|
|
267
268
|
});
|
|
268
269
|
|
|
@@ -272,7 +273,7 @@ export default function OrderEditor() {
|
|
|
272
273
|
return rows.map((c) => ({
|
|
273
274
|
value: c.id,
|
|
274
275
|
label: [c.first_name, c.last_name].filter(Boolean).join(" ") || c.email,
|
|
275
|
-
meta: [c.email, c.orders_count ?
|
|
276
|
+
meta: [c.email, c.orders_count ? t("orders.n_orders", { count: c.orders_count }) : null].filter(Boolean).join(" · "),
|
|
276
277
|
record: c,
|
|
277
278
|
}));
|
|
278
279
|
}, []);
|
|
@@ -316,6 +317,10 @@ export default function OrderEditor() {
|
|
|
316
317
|
return { filled, skipped };
|
|
317
318
|
};
|
|
318
319
|
|
|
320
|
+
/** "billing", "shipping" → localized, joined for toast copy. */
|
|
321
|
+
const addressPartNames = (keys) =>
|
|
322
|
+
keys.map((k) => (k === "billing" ? t("orders.addr_billing") : t("orders.addr_shipping"))).join(t("orders.and_separator"));
|
|
323
|
+
|
|
319
324
|
const pickCustomer = async (opt) => {
|
|
320
325
|
setCustomerOpt(opt);
|
|
321
326
|
patchDraft({ customer_id: opt?.value || "" });
|
|
@@ -324,11 +329,11 @@ export default function OrderEditor() {
|
|
|
324
329
|
const record = opt.record ?? (await base44.entities["commerce.Customer"].get(opt.value).catch(() => null));
|
|
325
330
|
const { filled, skipped } = await applyCustomerAddresses(record);
|
|
326
331
|
if (filled.length) {
|
|
327
|
-
toast.success(
|
|
332
|
+
toast.success(t("orders.filled_from", { parts: addressPartNames(filled), name: opt.label }));
|
|
328
333
|
} else if (skipped.length) {
|
|
329
|
-
toast.info("
|
|
334
|
+
toast.info(t("orders.kept_existing_addresses"));
|
|
330
335
|
} else {
|
|
331
|
-
toast.info(
|
|
336
|
+
toast.info(t("orders.no_saved_address_enter_below", { name: opt.label }));
|
|
332
337
|
setEditBilling(true);
|
|
333
338
|
}
|
|
334
339
|
};
|
|
@@ -341,12 +346,12 @@ export default function OrderEditor() {
|
|
|
341
346
|
const record =
|
|
342
347
|
customerOpt?.record ?? (await base44.entities["commerce.Customer"].get(draft.customer_id).catch(() => null));
|
|
343
348
|
if (!record) {
|
|
344
|
-
toast.error("
|
|
349
|
+
toast.error(t("orders.customer_load_failed"));
|
|
345
350
|
return;
|
|
346
351
|
}
|
|
347
352
|
const { filled } = await applyCustomerAddresses(record, { overwrite: true });
|
|
348
|
-
if (filled.length) toast.success(
|
|
349
|
-
else toast.info("
|
|
353
|
+
if (filled.length) toast.success(t("orders.replaced_from_customer", { parts: addressPartNames(filled) }));
|
|
354
|
+
else toast.info(t("orders.customer_no_saved_address"));
|
|
350
355
|
} finally {
|
|
351
356
|
setFillingAddresses(false);
|
|
352
357
|
}
|
|
@@ -390,8 +395,8 @@ export default function OrderEditor() {
|
|
|
390
395
|
patchDraft({ customer_id: record.id });
|
|
391
396
|
toast.success(
|
|
392
397
|
existing
|
|
393
|
-
?
|
|
394
|
-
:
|
|
398
|
+
? t("orders.already_customer_linked", { name: label })
|
|
399
|
+
: t("orders.customer_created", { name: label })
|
|
395
400
|
);
|
|
396
401
|
} catch {
|
|
397
402
|
/* toast handled by call() */
|
|
@@ -431,7 +436,7 @@ export default function OrderEditor() {
|
|
|
431
436
|
);
|
|
432
437
|
}
|
|
433
438
|
if (!order.data) {
|
|
434
|
-
return <p className="py-12 text-center text-sm text-muted-foreground">
|
|
439
|
+
return <p className="py-12 text-center text-sm text-muted-foreground">{t("orders.not_found")}</p>;
|
|
435
440
|
}
|
|
436
441
|
|
|
437
442
|
const o = order.data;
|
|
@@ -461,11 +466,11 @@ export default function OrderEditor() {
|
|
|
461
466
|
<PageHeader
|
|
462
467
|
title={
|
|
463
468
|
<span className="flex items-center gap-3">
|
|
464
|
-
|
|
469
|
+
{t("orders.order_number", { number: o.order_number || o.id })}
|
|
465
470
|
<StatusBadge status={o.status} />
|
|
466
471
|
</span>
|
|
467
472
|
}
|
|
468
|
-
description={
|
|
473
|
+
description={`${t("orders.placed_via", { date: formatDateTime(o.created_date), via: o.created_via || "checkout" })}${
|
|
469
474
|
o.payment_method_title ? ` · ${o.payment_method_title}` : ""
|
|
470
475
|
}`}
|
|
471
476
|
backHref={href("orders")}
|
|
@@ -477,14 +482,14 @@ export default function OrderEditor() {
|
|
|
477
482
|
{/* General */}
|
|
478
483
|
<Card>
|
|
479
484
|
<CardHeader className="pb-3">
|
|
480
|
-
<CardTitle className="text-base">
|
|
485
|
+
<CardTitle className="text-base">{t("orders.general")}</CardTitle>
|
|
481
486
|
</CardHeader>
|
|
482
487
|
{/* items-start: without it the shorter Status cell stretches to the
|
|
483
488
|
row height set by the Customer cell's helper text, and its
|
|
484
489
|
dropdown drifts below the Customer one. */}
|
|
485
490
|
<CardContent className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
|
|
486
491
|
<div className="grid gap-1.5">
|
|
487
|
-
<Label className="text-xs text-muted-foreground">
|
|
492
|
+
<Label className="text-xs text-muted-foreground">{t("common.status")}</Label>
|
|
488
493
|
<Select value={draft.status} onValueChange={(v) => patchDraft({ status: v })}>
|
|
489
494
|
<SelectTrigger>
|
|
490
495
|
<SelectValue />
|
|
@@ -499,12 +504,12 @@ export default function OrderEditor() {
|
|
|
499
504
|
</Select>
|
|
500
505
|
</div>
|
|
501
506
|
<div className="grid gap-1.5">
|
|
502
|
-
<Label className="text-xs text-muted-foreground">
|
|
507
|
+
<Label className="text-xs text-muted-foreground">{t("routes.customer")}</Label>
|
|
503
508
|
<SearchSelect
|
|
504
509
|
search={searchCustomers}
|
|
505
510
|
value={customerOpt}
|
|
506
511
|
onChange={pickCustomer}
|
|
507
|
-
placeholder="
|
|
512
|
+
placeholder={t("orders.guest_search_customers")}
|
|
508
513
|
/>
|
|
509
514
|
|
|
510
515
|
{draft.customer_id ? (
|
|
@@ -515,18 +520,18 @@ export default function OrderEditor() {
|
|
|
515
520
|
className="h-7 px-2 text-xs"
|
|
516
521
|
disabled={fillingAddresses}
|
|
517
522
|
onClick={refillFromCustomer}
|
|
518
|
-
title="
|
|
523
|
+
title={t("orders.use_customer_addresses_tip")}
|
|
519
524
|
>
|
|
520
525
|
{fillingAddresses ? (
|
|
521
526
|
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
|
522
527
|
) : (
|
|
523
528
|
<RotateCw className="mr-1 h-3.5 w-3.5" />
|
|
524
529
|
)}
|
|
525
|
-
|
|
530
|
+
{t("orders.use_customer_addresses")}
|
|
526
531
|
</Button>
|
|
527
532
|
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs" asChild>
|
|
528
533
|
<Link to={href(`customers/${draft.customer_id}`)}>
|
|
529
|
-
<ExternalLink className="mr-1 h-3.5 w-3.5" />
|
|
534
|
+
<ExternalLink className="mr-1 h-3.5 w-3.5" /> {t("orders.open_customer")}
|
|
530
535
|
</Link>
|
|
531
536
|
</Button>
|
|
532
537
|
</div>
|
|
@@ -541,8 +546,8 @@ export default function OrderEditor() {
|
|
|
541
546
|
onClick={saveAsCustomer}
|
|
542
547
|
title={
|
|
543
548
|
billingEmail
|
|
544
|
-
? "
|
|
545
|
-
: "
|
|
549
|
+
? t("orders.save_as_customer_tip")
|
|
550
|
+
: t("orders.save_as_customer_needs_email_tip")
|
|
546
551
|
}
|
|
547
552
|
>
|
|
548
553
|
{savingCustomer ? (
|
|
@@ -550,16 +555,15 @@ export default function OrderEditor() {
|
|
|
550
555
|
) : (
|
|
551
556
|
<UserPlus className="mr-1 h-3.5 w-3.5" />
|
|
552
557
|
)}
|
|
553
|
-
|
|
558
|
+
{t("orders.save_as_customer")}
|
|
554
559
|
</Button>
|
|
555
560
|
<span className="text-xs text-muted-foreground">
|
|
556
|
-
{billingEmail ? "
|
|
561
|
+
{billingEmail ? t("orders.guest_order") : t("orders.guest_order_needs_email")}
|
|
557
562
|
</span>
|
|
558
563
|
</div>
|
|
559
564
|
) : (
|
|
560
565
|
<p className="text-xs text-muted-foreground">
|
|
561
|
-
|
|
562
|
-
below — the billing email is where this order's emails go.
|
|
566
|
+
{t("orders.guest_order_hint")}
|
|
563
567
|
</p>
|
|
564
568
|
)}
|
|
565
569
|
</div>
|
|
@@ -569,8 +573,8 @@ export default function OrderEditor() {
|
|
|
569
573
|
{/* Addresses */}
|
|
570
574
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
571
575
|
{[
|
|
572
|
-
{ key: "billing", title: "
|
|
573
|
-
{ key: "shipping", title: "
|
|
576
|
+
{ key: "billing", title: t("orders.billing"), edit: editBilling, setEdit: setEditBilling, showEmail: true },
|
|
577
|
+
{ key: "shipping", title: t("orders.shipping"), edit: editShipping, setEdit: setEditShipping, showEmail: false },
|
|
574
578
|
].map(({ key, title, edit, setEdit, showEmail }) => (
|
|
575
579
|
<Card key={key}>
|
|
576
580
|
<CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
|
|
@@ -581,9 +585,9 @@ export default function OrderEditor() {
|
|
|
581
585
|
variant="ghost"
|
|
582
586
|
size="sm"
|
|
583
587
|
onClick={() => patchDraft({ shipping: { ...draft.billing, email: undefined } })}
|
|
584
|
-
title="
|
|
588
|
+
title={t("orders.copy_from_billing")}
|
|
585
589
|
>
|
|
586
|
-
<Copy className="mr-1 h-3.5 w-3.5" />
|
|
590
|
+
<Copy className="mr-1 h-3.5 w-3.5" /> {t("orders.from_billing")}
|
|
587
591
|
</Button>
|
|
588
592
|
)}
|
|
589
593
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setEdit(!edit)}>
|
|
@@ -609,7 +613,7 @@ export default function OrderEditor() {
|
|
|
609
613
|
{/* Items */}
|
|
610
614
|
<Card>
|
|
611
615
|
<CardHeader className="pb-3">
|
|
612
|
-
<CardTitle className="text-base">
|
|
616
|
+
<CardTitle className="text-base">{t("orders.items")}</CardTitle>
|
|
613
617
|
</CardHeader>
|
|
614
618
|
<CardContent className="p-0">
|
|
615
619
|
<LineItemsTable
|
|
@@ -622,7 +626,7 @@ export default function OrderEditor() {
|
|
|
622
626
|
"coupon",
|
|
623
627
|
async () => {
|
|
624
628
|
await call("admin-orders", "remove-coupon", { order_id: id, code });
|
|
625
|
-
toast.success(
|
|
629
|
+
toast.success(t("orders.coupon_removed", { code }));
|
|
626
630
|
},
|
|
627
631
|
{ flush: true }
|
|
628
632
|
)
|
|
@@ -631,22 +635,22 @@ export default function OrderEditor() {
|
|
|
631
635
|
{editable && (
|
|
632
636
|
<div className="flex flex-wrap gap-2 border-t p-3">
|
|
633
637
|
<Button variant="outline" size="sm" onClick={() => setAddProductOpen(true)}>
|
|
634
|
-
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
638
|
+
<Plus className="mr-1 h-3.5 w-3.5" /> {t("orders.add_products")}
|
|
635
639
|
</Button>
|
|
636
640
|
<Button variant="outline" size="sm" onClick={() => setFeeDialog(true)}>
|
|
637
|
-
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
641
|
+
<Plus className="mr-1 h-3.5 w-3.5" /> {t("orders.add_fee")}
|
|
638
642
|
</Button>
|
|
639
643
|
<Button variant="outline" size="sm" onClick={() => setShippingDialog(true)}>
|
|
640
|
-
<Truck className="mr-1 h-3.5 w-3.5" />
|
|
644
|
+
<Truck className="mr-1 h-3.5 w-3.5" /> {t("orders.add_shipping")}
|
|
641
645
|
</Button>
|
|
642
646
|
<Button
|
|
643
647
|
variant="outline"
|
|
644
648
|
size="sm"
|
|
645
649
|
disabled={Boolean(working)}
|
|
646
|
-
title={dirty ? "
|
|
650
|
+
title={dirty ? t("orders.apply_coupon_dirty_tip") : undefined}
|
|
647
651
|
onClick={() => setCouponDialog(true)}
|
|
648
652
|
>
|
|
649
|
-
<Ticket className="mr-1 h-3.5 w-3.5" />
|
|
653
|
+
<Ticket className="mr-1 h-3.5 w-3.5" /> {t("orders.apply_coupon")}
|
|
650
654
|
</Button>
|
|
651
655
|
<Button
|
|
652
656
|
variant="outline"
|
|
@@ -655,8 +659,8 @@ export default function OrderEditor() {
|
|
|
655
659
|
disabled={Boolean(working) || saving}
|
|
656
660
|
title={
|
|
657
661
|
dirty
|
|
658
|
-
? "
|
|
659
|
-
: "
|
|
662
|
+
? t("orders.recalculate_dirty_tip")
|
|
663
|
+
: t("orders.recalculate_tip")
|
|
660
664
|
}
|
|
661
665
|
onClick={recalculate}
|
|
662
666
|
>
|
|
@@ -665,7 +669,7 @@ export default function OrderEditor() {
|
|
|
665
669
|
) : (
|
|
666
670
|
<RotateCw className="mr-1 h-3.5 w-3.5" />
|
|
667
671
|
)}
|
|
668
|
-
{dirty ? "
|
|
672
|
+
{dirty ? t("orders.save_and_recalculate") : t("orders.recalculate")}
|
|
669
673
|
</Button>
|
|
670
674
|
</div>
|
|
671
675
|
)}
|
|
@@ -674,15 +678,15 @@ export default function OrderEditor() {
|
|
|
674
678
|
<div className="mb-2 flex items-center justify-end gap-1.5 text-xs text-muted-foreground">
|
|
675
679
|
{previewStale ? (
|
|
676
680
|
<>
|
|
677
|
-
<Loader2 className="h-3 w-3 animate-spin" />
|
|
681
|
+
<Loader2 className="h-3 w-3 animate-spin" /> {t("orders.pricing_changes")}
|
|
678
682
|
</>
|
|
679
683
|
) : previewError ? (
|
|
680
684
|
<span className="text-destructive">
|
|
681
|
-
|
|
685
|
+
{t("orders.preview_failed", { error: previewError })}
|
|
682
686
|
</span>
|
|
683
687
|
) : (
|
|
684
688
|
<Badge variant="secondary" className="font-normal">
|
|
685
|
-
|
|
689
|
+
{t("orders.unsaved_totals")}
|
|
686
690
|
</Badge>
|
|
687
691
|
)}
|
|
688
692
|
</div>
|
|
@@ -690,7 +694,7 @@ export default function OrderEditor() {
|
|
|
690
694
|
<TotalsBox order={totalsView} />
|
|
691
695
|
<div className="mt-3 flex justify-end">
|
|
692
696
|
<Button variant="outline" size="sm" onClick={() => setShowRefund((v) => !v)}>
|
|
693
|
-
{showRefund ? "
|
|
697
|
+
{showRefund ? t("orders.close_refund") : t("orders.refund")}
|
|
694
698
|
</Button>
|
|
695
699
|
</div>
|
|
696
700
|
</div>
|
|
@@ -718,7 +722,7 @@ export default function OrderEditor() {
|
|
|
718
722
|
{/* Custom fields */}
|
|
719
723
|
<Card>
|
|
720
724
|
<CardHeader className="pb-3">
|
|
721
|
-
<CardTitle className="text-base">
|
|
725
|
+
<CardTitle className="text-base">{t("orders.custom_fields")}</CardTitle>
|
|
722
726
|
</CardHeader>
|
|
723
727
|
<CardContent>
|
|
724
728
|
<MetaDataEditor value={draft.meta_data || []} onChange={(v) => patchDraft({ meta_data: v })} />
|
|
@@ -730,12 +734,12 @@ export default function OrderEditor() {
|
|
|
730
734
|
<div className="space-y-6">
|
|
731
735
|
<Card>
|
|
732
736
|
<CardHeader className="pb-3">
|
|
733
|
-
<CardTitle className="text-base">
|
|
737
|
+
<CardTitle className="text-base">{t("orders.order_actions")}</CardTitle>
|
|
734
738
|
</CardHeader>
|
|
735
739
|
<CardContent className="space-y-2">
|
|
736
740
|
<Button className="w-full" onClick={save} disabled={!dirty || saving}>
|
|
737
741
|
{saving && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
|
|
738
|
-
|
|
742
|
+
{t("orders.save_order")}
|
|
739
743
|
</Button>
|
|
740
744
|
<Button
|
|
741
745
|
variant="outline"
|
|
@@ -748,7 +752,7 @@ export default function OrderEditor() {
|
|
|
748
752
|
) : (
|
|
749
753
|
<Mail className="mr-1.5 h-4 w-4" />
|
|
750
754
|
)}
|
|
751
|
-
|
|
755
|
+
{t("orders.send_details")}
|
|
752
756
|
</Button>
|
|
753
757
|
<Separator />
|
|
754
758
|
<Button
|
|
@@ -756,7 +760,7 @@ export default function OrderEditor() {
|
|
|
756
760
|
className="w-full text-destructive hover:text-destructive"
|
|
757
761
|
onClick={() => setDeleteOpen(true)}
|
|
758
762
|
>
|
|
759
|
-
<Trash2 className="mr-1.5 h-4 w-4" />
|
|
763
|
+
<Trash2 className="mr-1.5 h-4 w-4" /> {t("orders.delete_order")}
|
|
760
764
|
</Button>
|
|
761
765
|
</CardContent>
|
|
762
766
|
</Card>
|
|
@@ -766,7 +770,7 @@ export default function OrderEditor() {
|
|
|
766
770
|
{o.customer_note && (
|
|
767
771
|
<Card>
|
|
768
772
|
<CardHeader className="pb-3">
|
|
769
|
-
<CardTitle className="text-base">
|
|
773
|
+
<CardTitle className="text-base">{t("orders.customer_provided_note")}</CardTitle>
|
|
770
774
|
</CardHeader>
|
|
771
775
|
<CardContent>
|
|
772
776
|
<p className="whitespace-pre-wrap rounded-md border border-blue-200 bg-blue-50 p-2.5 text-sm">
|
|
@@ -791,8 +795,8 @@ export default function OrderEditor() {
|
|
|
791
795
|
<NameAmountDialog
|
|
792
796
|
open={feeDialog}
|
|
793
797
|
onOpenChange={setFeeDialog}
|
|
794
|
-
title="
|
|
795
|
-
nameLabel="
|
|
798
|
+
title={t("orders.add_fee")}
|
|
799
|
+
nameLabel={t("orders.fee_name")}
|
|
796
800
|
onSubmit={({ name, amount }) =>
|
|
797
801
|
patchDraft({
|
|
798
802
|
fee_lines: [
|
|
@@ -806,8 +810,8 @@ export default function OrderEditor() {
|
|
|
806
810
|
<NameAmountDialog
|
|
807
811
|
open={shippingDialog}
|
|
808
812
|
onOpenChange={setShippingDialog}
|
|
809
|
-
title="
|
|
810
|
-
nameLabel="
|
|
813
|
+
title={t("orders.add_shipping")}
|
|
814
|
+
nameLabel={t("orders.method_title")}
|
|
811
815
|
onSubmit={({ name, amount }) =>
|
|
812
816
|
patchDraft({
|
|
813
817
|
shipping_lines: [
|
|
@@ -826,7 +830,7 @@ export default function OrderEditor() {
|
|
|
826
830
|
"coupon",
|
|
827
831
|
async () => {
|
|
828
832
|
await call("admin-orders", "apply-coupon", { order_id: id, code });
|
|
829
|
-
toast.success(
|
|
833
|
+
toast.success(t("orders.coupon_applied", { code }));
|
|
830
834
|
},
|
|
831
835
|
{ flush: true }
|
|
832
836
|
)
|
|
@@ -836,9 +840,9 @@ export default function OrderEditor() {
|
|
|
836
840
|
<ConfirmDialog
|
|
837
841
|
open={deleteOpen}
|
|
838
842
|
onOpenChange={setDeleteOpen}
|
|
839
|
-
title={
|
|
840
|
-
description="
|
|
841
|
-
confirmLabel="
|
|
843
|
+
title={t("orders.delete_order_title", { number: o.order_number || o.id })}
|
|
844
|
+
description={t("orders.delete_order_description")}
|
|
845
|
+
confirmLabel={t("orders.delete_order")}
|
|
842
846
|
loading={working === "delete"}
|
|
843
847
|
onConfirm={deleteOrder}
|
|
844
848
|
/>
|
|
@@ -859,7 +863,7 @@ function AddressDisplay({ address = {} }) {
|
|
|
859
863
|
address.email,
|
|
860
864
|
address.phone,
|
|
861
865
|
].filter(Boolean);
|
|
862
|
-
if (rows.length === 0) return <p className="text-sm text-muted-foreground">
|
|
866
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">{t("orders.no_address_set")}</p>;
|
|
863
867
|
return (
|
|
864
868
|
<address className="text-sm not-italic leading-6 text-muted-foreground">
|
|
865
869
|
{rows.map((r, i) => (
|
|
@@ -892,13 +896,13 @@ function NameAmountDialog({ open, onOpenChange, title, nameLabel, onSubmit }) {
|
|
|
892
896
|
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
|
893
897
|
</div>
|
|
894
898
|
<div className="grid gap-1.5">
|
|
895
|
-
<Label>
|
|
899
|
+
<Label>{t("common.amount")}</Label>
|
|
896
900
|
<Input type="number" step="any" value={amount} onChange={(e) => setAmount(Number(e.target.value) || 0)} />
|
|
897
901
|
</div>
|
|
898
902
|
</div>
|
|
899
903
|
<DialogFooter>
|
|
900
904
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
901
|
-
|
|
905
|
+
{t("common.cancel")}
|
|
902
906
|
</Button>
|
|
903
907
|
<Button
|
|
904
908
|
disabled={!name.trim()}
|
|
@@ -907,7 +911,7 @@ function NameAmountDialog({ open, onOpenChange, title, nameLabel, onSubmit }) {
|
|
|
907
911
|
onOpenChange(false);
|
|
908
912
|
}}
|
|
909
913
|
>
|
|
910
|
-
|
|
914
|
+
{t("common.add")}
|
|
911
915
|
</Button>
|
|
912
916
|
</DialogFooter>
|
|
913
917
|
</DialogContent>
|
|
@@ -926,15 +930,15 @@ function CouponDialog({ open, onOpenChange, onSubmit }) {
|
|
|
926
930
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
927
931
|
<DialogContent className="sm:max-w-sm">
|
|
928
932
|
<DialogHeader>
|
|
929
|
-
<DialogTitle>
|
|
933
|
+
<DialogTitle>{t("orders.apply_coupon")}</DialogTitle>
|
|
930
934
|
</DialogHeader>
|
|
931
935
|
<div className="grid gap-1.5">
|
|
932
|
-
<Label>
|
|
933
|
-
<Input value={code} onChange={(e) => setCode(e.target.value)} placeholder="
|
|
936
|
+
<Label>{t("orders.coupon_code")}</Label>
|
|
937
|
+
<Input value={code} onChange={(e) => setCode(e.target.value)} placeholder={t("orders.coupon_code_placeholder")} />
|
|
934
938
|
</div>
|
|
935
939
|
<DialogFooter>
|
|
936
940
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
937
|
-
|
|
941
|
+
{t("common.cancel")}
|
|
938
942
|
</Button>
|
|
939
943
|
<Button
|
|
940
944
|
disabled={!code.trim()}
|
|
@@ -943,7 +947,7 @@ function CouponDialog({ open, onOpenChange, onSubmit }) {
|
|
|
943
947
|
onOpenChange(false);
|
|
944
948
|
}}
|
|
945
949
|
>
|
|
946
|
-
|
|
950
|
+
{t("orders.apply")}
|
|
947
951
|
</Button>
|
|
948
952
|
</DialogFooter>
|
|
949
953
|
</DialogContent>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { Loader2, Plus, Search, ShoppingCart } from "lucide-react";
|
|
13
13
|
import { toast } from "sonner";
|
|
14
14
|
|
|
15
|
+
import { t } from "../../i18n";
|
|
15
16
|
import { call, base44 } from "../../lib/api";
|
|
16
17
|
import useAsync from "../../hooks/useAsync";
|
|
17
18
|
import usePagedList from "../../hooks/usePagedList";
|
|
@@ -27,7 +28,7 @@ import DataTable from "../../components/DataTable";
|
|
|
27
28
|
import DateRangePicker from "../../components/DateRangePicker";
|
|
28
29
|
import StatusBadge from "../../components/StatusBadge";
|
|
29
30
|
|
|
30
|
-
const TABS = [{ value: "", label: "
|
|
31
|
+
const TABS = [{ value: "", label: t("orders.all") }, ...ORDER_STATUSES];
|
|
31
32
|
|
|
32
33
|
export default function OrdersList() {
|
|
33
34
|
const href = useAdminHref();
|
|
@@ -90,7 +91,7 @@ export default function OrdersList() {
|
|
|
90
91
|
setBulkBusy(true);
|
|
91
92
|
try {
|
|
92
93
|
await call("admin-orders", "bulk-status", { ids: selected, status: bulkStatus });
|
|
93
|
-
toast.success(
|
|
94
|
+
toast.success(t("orders.n_updated", { count: selected.length }));
|
|
94
95
|
setSelected([]);
|
|
95
96
|
setBulkStatus("");
|
|
96
97
|
list.refetch();
|
|
@@ -119,16 +120,16 @@ export default function OrdersList() {
|
|
|
119
120
|
() => [
|
|
120
121
|
{
|
|
121
122
|
key: "order_number",
|
|
122
|
-
label: "
|
|
123
|
+
label: t("routes.order"),
|
|
123
124
|
render: (o) => (
|
|
124
125
|
<span className="font-medium">
|
|
125
126
|
#{o.order_number || o.id} <span className="font-normal text-muted-foreground">{customerName(o)}</span>
|
|
126
127
|
</span>
|
|
127
128
|
),
|
|
128
129
|
},
|
|
129
|
-
{ key: "created_date", label: "
|
|
130
|
-
{ key: "status", label: "
|
|
131
|
-
{ key: "total", label: "
|
|
130
|
+
{ key: "created_date", label: t("orders.date"), sortable: true, render: (o) => formatDate(o.created_date) },
|
|
131
|
+
{ key: "status", label: t("common.status"), render: (o) => <StatusBadge status={o.status} /> },
|
|
132
|
+
{ key: "total", label: t("orders.total"), sortable: true, className: "text-right", render: (o) => format(o.total || 0) },
|
|
132
133
|
],
|
|
133
134
|
[format]
|
|
134
135
|
);
|
|
@@ -138,11 +139,11 @@ export default function OrdersList() {
|
|
|
138
139
|
return (
|
|
139
140
|
<div>
|
|
140
141
|
<PageHeader
|
|
141
|
-
title="
|
|
142
|
+
title={t("routes.orders")}
|
|
142
143
|
actions={
|
|
143
144
|
<Button onClick={addOrder} disabled={creating}>
|
|
144
145
|
{creating ? <Loader2 className="mr-1.5 h-4 w-4 animate-spin" /> : <Plus className="mr-1.5 h-4 w-4" />}
|
|
145
|
-
|
|
146
|
+
{t("routes.orders_new")}
|
|
146
147
|
</Button>
|
|
147
148
|
}
|
|
148
149
|
/>
|
|
@@ -176,17 +177,17 @@ export default function OrdersList() {
|
|
|
176
177
|
<Input
|
|
177
178
|
value={query}
|
|
178
179
|
onChange={(e) => setQuery(e.target.value)}
|
|
179
|
-
placeholder="
|
|
180
|
+
placeholder={t("orders.search_placeholder")}
|
|
180
181
|
className="w-64 pl-8"
|
|
181
182
|
/>
|
|
182
183
|
</div>
|
|
183
184
|
<DateRangePicker value={dateRange} onChange={setDateRange} />
|
|
184
185
|
{selected.length > 0 && (
|
|
185
186
|
<div className="ml-auto flex items-center gap-2 rounded-md border bg-muted/50 px-2 py-1">
|
|
186
|
-
<span className="text-sm text-muted-foreground">{selected.length}
|
|
187
|
+
<span className="text-sm text-muted-foreground">{t("common.selected_count", { count: selected.length })}</span>
|
|
187
188
|
<Select value={bulkStatus} onValueChange={setBulkStatus}>
|
|
188
189
|
<SelectTrigger className="h-8 w-44">
|
|
189
|
-
<SelectValue placeholder="
|
|
190
|
+
<SelectValue placeholder={t("orders.change_status_to")} />
|
|
190
191
|
</SelectTrigger>
|
|
191
192
|
<SelectContent>
|
|
192
193
|
{ORDER_STATUSES.map((s) => (
|
|
@@ -198,7 +199,7 @@ export default function OrdersList() {
|
|
|
198
199
|
</Select>
|
|
199
200
|
<Button size="sm" onClick={applyBulk} disabled={!bulkStatus || bulkBusy}>
|
|
200
201
|
{bulkBusy && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />}
|
|
201
|
-
|
|
202
|
+
{t("orders.apply")}
|
|
202
203
|
</Button>
|
|
203
204
|
</div>
|
|
204
205
|
)}
|
|
@@ -214,12 +215,12 @@ export default function OrdersList() {
|
|
|
214
215
|
selected={selected}
|
|
215
216
|
onSelectChange={setSelected}
|
|
216
217
|
onRowClick={(o) => navigate(href(`orders/${o.id}`))}
|
|
217
|
-
rowActions={(o) => [{ label: "
|
|
218
|
+
rowActions={(o) => [{ label: t("orders.view_edit"), onClick: () => navigate(href(`orders/${o.id}`)) }]}
|
|
218
219
|
pagination={{ page: list.page, hasNext: list.hasNext, onNext: list.next, onPrev: list.prev }}
|
|
219
220
|
empty={{
|
|
220
221
|
icon: ShoppingCart,
|
|
221
|
-
title: "
|
|
222
|
-
description: searchMode ? "
|
|
222
|
+
title: t("orders.empty_title"),
|
|
223
|
+
description: searchMode ? t("orders.empty_search_hint") : t("orders.empty_description"),
|
|
223
224
|
}}
|
|
224
225
|
/>
|
|
225
226
|
</div>
|