@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
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "@/components/ui/select";
|
|
19
19
|
import { Loader2 } from "lucide-react";
|
|
20
20
|
|
|
21
|
+
import { t } from "../../../i18n";
|
|
21
22
|
import { call, base44 } from "../../../lib/api";
|
|
22
23
|
import SearchSelect from "../../../components/SearchSelect";
|
|
23
24
|
import { isVariable, variationLabel } from "../../../lib/product-utils";
|
|
@@ -50,7 +51,7 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
|
|
|
50
51
|
return rows.map((p) => ({
|
|
51
52
|
value: p.id,
|
|
52
53
|
label: p.name,
|
|
53
|
-
meta: p.sku ?
|
|
54
|
+
meta: p.sku ? t("orders.sku", { sku: p.sku }) : undefined,
|
|
54
55
|
product: p,
|
|
55
56
|
}));
|
|
56
57
|
};
|
|
@@ -83,33 +84,33 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
|
|
|
83
84
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
84
85
|
<DialogContent className="sm:max-w-md">
|
|
85
86
|
<DialogHeader>
|
|
86
|
-
<DialogTitle>
|
|
87
|
+
<DialogTitle>{t("routes.products_new")}</DialogTitle>
|
|
87
88
|
</DialogHeader>
|
|
88
89
|
|
|
89
90
|
<div className="space-y-4">
|
|
90
91
|
<div className="grid gap-1.5">
|
|
91
|
-
<Label>
|
|
92
|
+
<Label>{t("orders.product")}</Label>
|
|
92
93
|
<SearchSelect
|
|
93
94
|
search={searchProducts}
|
|
94
95
|
value={picked}
|
|
95
96
|
onChange={handlePick}
|
|
96
|
-
placeholder="
|
|
97
|
+
placeholder={t("orders.search_products")}
|
|
97
98
|
/>
|
|
98
99
|
</div>
|
|
99
100
|
|
|
100
101
|
{hasVariants && (
|
|
101
102
|
<div className="grid gap-1.5">
|
|
102
|
-
<Label>
|
|
103
|
+
<Label>{t("orders.variation")}</Label>
|
|
103
104
|
{loadingVariations ? (
|
|
104
105
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
105
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
106
|
+
<Loader2 className="h-4 w-4 animate-spin" /> {t("orders.loading_variations")}
|
|
106
107
|
</div>
|
|
107
108
|
) : (variations || []).length === 0 ? (
|
|
108
|
-
<p className="text-sm text-muted-foreground">
|
|
109
|
+
<p className="text-sm text-muted-foreground">{t("orders.no_variations")}</p>
|
|
109
110
|
) : (
|
|
110
111
|
<Select value={variationId} onValueChange={setVariationId}>
|
|
111
112
|
<SelectTrigger>
|
|
112
|
-
<SelectValue placeholder="
|
|
113
|
+
<SelectValue placeholder={t("orders.choose_variation")} />
|
|
113
114
|
</SelectTrigger>
|
|
114
115
|
<SelectContent>
|
|
115
116
|
{(variations || []).map((v) => (
|
|
@@ -124,7 +125,7 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
|
|
|
124
125
|
)}
|
|
125
126
|
|
|
126
127
|
<div className="grid gap-1.5">
|
|
127
|
-
<Label>
|
|
128
|
+
<Label>{t("orders.quantity")}</Label>
|
|
128
129
|
<Input
|
|
129
130
|
type="number"
|
|
130
131
|
min="1"
|
|
@@ -137,10 +138,10 @@ export default function AddProductDialog({ open, onOpenChange, onAdd }) {
|
|
|
137
138
|
|
|
138
139
|
<DialogFooter>
|
|
139
140
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
140
|
-
|
|
141
|
+
{t("common.cancel")}
|
|
141
142
|
</Button>
|
|
142
143
|
<Button onClick={submit} disabled={!canAdd}>
|
|
143
|
-
|
|
144
|
+
{t("orders.add_to_order")}
|
|
144
145
|
</Button>
|
|
145
146
|
</DialogFooter>
|
|
146
147
|
</DialogContent>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { Loader2, X } from "lucide-react";
|
|
13
13
|
import { toast } from "sonner";
|
|
14
14
|
|
|
15
|
+
import { t } from "../../../i18n";
|
|
15
16
|
import { call } from "../../../lib/api";
|
|
16
17
|
import SearchSelect from "../../../components/SearchSelect";
|
|
17
18
|
import { formatDate } from "../../../lib/format";
|
|
@@ -29,7 +30,7 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
|
|
|
29
30
|
const rows = data?.rows || data || [];
|
|
30
31
|
return rows
|
|
31
32
|
.filter((p) => p.downloadable)
|
|
32
|
-
.map((p) => ({ value: p.id, label: p.name, meta: p.sku ?
|
|
33
|
+
.map((p) => ({ value: p.id, label: p.name, meta: p.sku ? t("orders.sku", { sku: p.sku }) : undefined }));
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
const grant = async () => {
|
|
@@ -37,7 +38,7 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
|
|
|
37
38
|
setBusy(true);
|
|
38
39
|
try {
|
|
39
40
|
await call("admin-orders", "grant-download", { order_id: orderId, product_id: picked.value });
|
|
40
|
-
toast.success("
|
|
41
|
+
toast.success(t("orders.download_granted"));
|
|
41
42
|
setPicked(null);
|
|
42
43
|
onChanged();
|
|
43
44
|
} catch {
|
|
@@ -50,7 +51,7 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
|
|
|
50
51
|
const revoke = async (permissionId) => {
|
|
51
52
|
try {
|
|
52
53
|
await call("admin-orders", "revoke-download", { order_id: orderId, permission_id: permissionId });
|
|
53
|
-
toast.success("
|
|
54
|
+
toast.success(t("orders.download_revoked"));
|
|
54
55
|
onChanged();
|
|
55
56
|
} catch {
|
|
56
57
|
/* toast handled by call() */
|
|
@@ -60,7 +61,7 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
|
|
|
60
61
|
return (
|
|
61
62
|
<Card>
|
|
62
63
|
<CardHeader className="pb-3">
|
|
63
|
-
<CardTitle className="text-base">
|
|
64
|
+
<CardTitle className="text-base">{t("orders.download_permissions")}</CardTitle>
|
|
64
65
|
</CardHeader>
|
|
65
66
|
<CardContent className="space-y-3">
|
|
66
67
|
{loading ? (
|
|
@@ -68,14 +69,14 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
|
|
|
68
69
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
69
70
|
</div>
|
|
70
71
|
) : (permissions || []).length === 0 ? (
|
|
71
|
-
<p className="text-sm text-muted-foreground">
|
|
72
|
+
<p className="text-sm text-muted-foreground">{t("orders.no_download_permissions")}</p>
|
|
72
73
|
) : (
|
|
73
74
|
<Table>
|
|
74
75
|
<TableHeader>
|
|
75
76
|
<TableRow>
|
|
76
|
-
<TableHead>
|
|
77
|
-
<TableHead className="w-32 text-right">
|
|
78
|
-
<TableHead className="w-32">
|
|
77
|
+
<TableHead>{t("orders.file")}</TableHead>
|
|
78
|
+
<TableHead className="w-32 text-right">{t("orders.remaining")}</TableHead>
|
|
79
|
+
<TableHead className="w-32">{t("orders.expires")}</TableHead>
|
|
79
80
|
<TableHead className="w-10" />
|
|
80
81
|
</TableRow>
|
|
81
82
|
</TableHeader>
|
|
@@ -85,11 +86,11 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
|
|
|
85
86
|
<TableCell className="text-sm">{p.download_name || p.file_url || p.product_id}</TableCell>
|
|
86
87
|
<TableCell className="text-right text-sm">
|
|
87
88
|
{p.downloads_remaining === -1 ? "∞" : p.downloads_remaining}
|
|
88
|
-
<span className="text-xs text-muted-foreground"> (
|
|
89
|
+
<span className="text-xs text-muted-foreground"> {t("orders.used_count", { count: p.download_count || 0 })}</span>
|
|
89
90
|
</TableCell>
|
|
90
|
-
<TableCell className="text-sm">{p.access_expires ? formatDate(p.access_expires) : "
|
|
91
|
+
<TableCell className="text-sm">{p.access_expires ? formatDate(p.access_expires) : t("orders.never")}</TableCell>
|
|
91
92
|
<TableCell>
|
|
92
|
-
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => revoke(p.id)} title="
|
|
93
|
+
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => revoke(p.id)} title={t("orders.revoke")}>
|
|
93
94
|
<X className="h-4 w-4" />
|
|
94
95
|
</Button>
|
|
95
96
|
</TableCell>
|
|
@@ -105,12 +106,12 @@ export default function DownloadPermissionsPanel({ orderId, permissions, loading
|
|
|
105
106
|
search={searchDownloadable}
|
|
106
107
|
value={picked}
|
|
107
108
|
onChange={setPicked}
|
|
108
|
-
placeholder="
|
|
109
|
+
placeholder={t("orders.search_downloadable")}
|
|
109
110
|
/>
|
|
110
111
|
</div>
|
|
111
112
|
<Button size="sm" onClick={grant} disabled={!picked || busy}>
|
|
112
113
|
{busy && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />}
|
|
113
|
-
|
|
114
|
+
{t("orders.grant_access")}
|
|
114
115
|
</Button>
|
|
115
116
|
</div>
|
|
116
117
|
</CardContent>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "@/components/ui/table";
|
|
13
13
|
import { Package, X } from "lucide-react";
|
|
14
14
|
|
|
15
|
+
import { t } from "../../../i18n";
|
|
15
16
|
import useMoney from "../../../hooks/useMoney";
|
|
16
17
|
import { round2 } from "../../../lib/order-utils";
|
|
17
18
|
|
|
@@ -56,11 +57,11 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
|
|
|
56
57
|
<Table>
|
|
57
58
|
<TableHeader>
|
|
58
59
|
<TableRow>
|
|
59
|
-
<TableHead>
|
|
60
|
-
<TableHead className="w-28 text-right">
|
|
61
|
-
<TableHead className="w-20 text-right">
|
|
62
|
-
<TableHead className="w-28 text-right">
|
|
63
|
-
<TableHead className="w-24 text-right">
|
|
60
|
+
<TableHead>{t("orders.item")}</TableHead>
|
|
61
|
+
<TableHead className="w-28 text-right">{t("orders.cost")}</TableHead>
|
|
62
|
+
<TableHead className="w-20 text-right">{t("orders.qty")}</TableHead>
|
|
63
|
+
<TableHead className="w-28 text-right">{t("orders.total")}</TableHead>
|
|
64
|
+
<TableHead className="w-24 text-right">{t("orders.tax")}</TableHead>
|
|
64
65
|
{editable && <TableHead className="w-10" />}
|
|
65
66
|
</TableRow>
|
|
66
67
|
</TableHeader>
|
|
@@ -68,7 +69,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
|
|
|
68
69
|
{lines.length === 0 && fees.length === 0 && shipping.length === 0 && (
|
|
69
70
|
<TableRow>
|
|
70
71
|
<TableCell colSpan={editable ? 6 : 5} className="py-8 text-center text-sm text-muted-foreground">
|
|
71
|
-
|
|
72
|
+
{t("orders.no_items")} {editable ? t("orders.no_items_hint") : ""}
|
|
72
73
|
</TableCell>
|
|
73
74
|
</TableRow>
|
|
74
75
|
)}
|
|
@@ -87,7 +88,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
|
|
|
87
88
|
<div className="min-w-0">
|
|
88
89
|
<p className="truncate text-sm font-medium">{line.name}</p>
|
|
89
90
|
<p className="text-xs text-muted-foreground">
|
|
90
|
-
{line.sku && <span>
|
|
91
|
+
{line.sku && <span>{t("orders.sku", { sku: line.sku })}</span>}
|
|
91
92
|
{(line.attributes || []).length > 0 && (
|
|
92
93
|
<span>
|
|
93
94
|
{line.sku ? " · " : ""}
|
|
@@ -142,7 +143,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
|
|
|
142
143
|
<TableRow key={fee.line_id} className="bg-muted/30">
|
|
143
144
|
<TableCell>
|
|
144
145
|
<span className="text-sm">
|
|
145
|
-
<Badge variant="outline" className="mr-2">
|
|
146
|
+
<Badge variant="outline" className="mr-2">{t("orders.fee")}</Badge>
|
|
146
147
|
{fee.name}
|
|
147
148
|
</span>
|
|
148
149
|
</TableCell>
|
|
@@ -164,7 +165,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
|
|
|
164
165
|
<TableRow key={line.line_id} className="bg-muted/30">
|
|
165
166
|
<TableCell>
|
|
166
167
|
<span className="text-sm">
|
|
167
|
-
<Badge variant="outline" className="mr-2">
|
|
168
|
+
<Badge variant="outline" className="mr-2">{t("orders.shipping")}</Badge>
|
|
168
169
|
{line.method_title || line.method_id}
|
|
169
170
|
</span>
|
|
170
171
|
</TableCell>
|
|
@@ -186,7 +187,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
|
|
|
186
187
|
|
|
187
188
|
{coupons.length > 0 && (
|
|
188
189
|
<div className="flex flex-wrap items-center gap-2 border-t px-3 py-2">
|
|
189
|
-
<span className="text-xs text-muted-foreground">
|
|
190
|
+
<span className="text-xs text-muted-foreground">{t("orders.coupons_label")}</span>
|
|
190
191
|
{coupons.map((c) => (
|
|
191
192
|
<Badge key={c.code} variant="secondary" className="gap-1 pr-1">
|
|
192
193
|
{c.code} (−{format(c.discount || 0)})
|
|
@@ -195,7 +196,7 @@ export default function LineItemsTable({ order, editable, onChange, onRemoveCoup
|
|
|
195
196
|
className="rounded-full p-0.5 hover:bg-muted-foreground/20 disabled:opacity-50"
|
|
196
197
|
disabled={removingCoupon === c.code}
|
|
197
198
|
onClick={() => onRemoveCoupon(c.code)}
|
|
198
|
-
title="
|
|
199
|
+
title={t("orders.remove_coupon")}
|
|
199
200
|
>
|
|
200
201
|
<X className="h-3 w-3" />
|
|
201
202
|
</button>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { Loader2, X } from "lucide-react";
|
|
13
13
|
import { toast } from "sonner";
|
|
14
14
|
|
|
15
|
+
import { t } from "../../../i18n";
|
|
15
16
|
import { call } from "../../../lib/api";
|
|
16
17
|
import { formatDateTime } from "../../../lib/format";
|
|
17
18
|
|
|
@@ -40,7 +41,7 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
|
|
|
40
41
|
note: text.trim(),
|
|
41
42
|
is_customer_note: kind === "customer",
|
|
42
43
|
});
|
|
43
|
-
toast.success(kind === "customer" ? "
|
|
44
|
+
toast.success(kind === "customer" ? t("orders.note_sent") : t("orders.note_added"));
|
|
44
45
|
setText("");
|
|
45
46
|
onChanged();
|
|
46
47
|
} catch {
|
|
@@ -62,7 +63,7 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
|
|
|
62
63
|
return (
|
|
63
64
|
<Card>
|
|
64
65
|
<CardHeader className="pb-3">
|
|
65
|
-
<CardTitle className="text-base">
|
|
66
|
+
<CardTitle className="text-base">{t("orders.order_notes")}</CardTitle>
|
|
66
67
|
</CardHeader>
|
|
67
68
|
<CardContent className="space-y-3">
|
|
68
69
|
{loading ? (
|
|
@@ -70,7 +71,7 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
|
|
|
70
71
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
71
72
|
</div>
|
|
72
73
|
) : (notes || []).length === 0 ? (
|
|
73
|
-
<p className="text-sm text-muted-foreground">
|
|
74
|
+
<p className="text-sm text-muted-foreground">{t("orders.no_notes")}</p>
|
|
74
75
|
) : (
|
|
75
76
|
<ul className="max-h-96 space-y-2 overflow-y-auto pr-1">
|
|
76
77
|
{notes.map((n) => (
|
|
@@ -78,14 +79,14 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
|
|
|
78
79
|
<p className="whitespace-pre-wrap">{n.note}</p>
|
|
79
80
|
<p className="mt-1.5 text-xs text-muted-foreground">
|
|
80
81
|
{formatDateTime(n.created_date)}
|
|
81
|
-
{n.added_by && n.added_by !== "system" ? ` · ${n.added_by}` : n.added_by === "system" ?
|
|
82
|
-
{n.is_customer_note ?
|
|
82
|
+
{n.added_by && n.added_by !== "system" ? ` · ${n.added_by}` : n.added_by === "system" ? ` · ${t("orders.system")}` : ""}
|
|
83
|
+
{n.is_customer_note ? ` · ${t("orders.sent_to_customer")}` : ""}
|
|
83
84
|
</p>
|
|
84
85
|
<button
|
|
85
86
|
type="button"
|
|
86
87
|
onClick={() => deleteNote(n.id)}
|
|
87
88
|
className="absolute right-1.5 top-1.5 hidden rounded p-0.5 hover:bg-black/5 group-hover:block"
|
|
88
|
-
title="
|
|
89
|
+
title={t("orders.delete_note")}
|
|
89
90
|
>
|
|
90
91
|
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
|
91
92
|
</button>
|
|
@@ -97,7 +98,7 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
|
|
|
97
98
|
<div className="space-y-2 border-t pt-3">
|
|
98
99
|
<Textarea
|
|
99
100
|
rows={3}
|
|
100
|
-
placeholder="
|
|
101
|
+
placeholder={t("orders.add_note_placeholder")}
|
|
101
102
|
value={text}
|
|
102
103
|
onChange={(e) => setText(e.target.value)}
|
|
103
104
|
/>
|
|
@@ -107,13 +108,13 @@ export default function OrderNotesPanel({ orderId, notes, loading, onChanged })
|
|
|
107
108
|
<SelectValue />
|
|
108
109
|
</SelectTrigger>
|
|
109
110
|
<SelectContent>
|
|
110
|
-
<SelectItem value="private">
|
|
111
|
-
<SelectItem value="customer">
|
|
111
|
+
<SelectItem value="private">{t("orders.private_note")}</SelectItem>
|
|
112
|
+
<SelectItem value="customer">{t("orders.note_to_customer")}</SelectItem>
|
|
112
113
|
</SelectContent>
|
|
113
114
|
</Select>
|
|
114
115
|
<Button size="sm" onClick={addNote} disabled={busy || !text.trim()}>
|
|
115
116
|
{busy && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />}
|
|
116
|
-
|
|
117
|
+
{t("common.add")}
|
|
117
118
|
</Button>
|
|
118
119
|
</div>
|
|
119
120
|
</div>
|
|
@@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
|
|
|
6
6
|
import { Check, Copy, CreditCard, ExternalLink, Loader2, RotateCw } from "lucide-react";
|
|
7
7
|
import { toast } from "sonner";
|
|
8
8
|
|
|
9
|
+
import { t } from "../../../i18n";
|
|
9
10
|
import { call } from "../../../lib/api";
|
|
10
11
|
import useMoney from "../../../hooks/useMoney";
|
|
11
12
|
import { formatDateTime } from "../../../lib/format";
|
|
@@ -47,7 +48,7 @@ export default function PaymentPanel({ order, onChanged }) {
|
|
|
47
48
|
setLink(data?.url || "");
|
|
48
49
|
setCopied(false);
|
|
49
50
|
if (data?.url) {
|
|
50
|
-
toast.success("
|
|
51
|
+
toast.success(t("orders.payment_link_created"));
|
|
51
52
|
if (willSwitchMethod) onChanged?.(); // the order's method changed
|
|
52
53
|
}
|
|
53
54
|
} catch {
|
|
@@ -62,10 +63,10 @@ export default function PaymentPanel({ order, onChanged }) {
|
|
|
62
63
|
try {
|
|
63
64
|
const data = await call("payments", "verify", { order_id: order.id });
|
|
64
65
|
if (data?.paid) {
|
|
65
|
-
toast.success("
|
|
66
|
+
toast.success(t("orders.payment_confirmed"));
|
|
66
67
|
onChanged?.();
|
|
67
68
|
} else {
|
|
68
|
-
toast.info("
|
|
69
|
+
toast.info(t("orders.no_payment_yet"));
|
|
69
70
|
}
|
|
70
71
|
} catch {
|
|
71
72
|
/* toast handled by call() */
|
|
@@ -78,37 +79,37 @@ export default function PaymentPanel({ order, onChanged }) {
|
|
|
78
79
|
try {
|
|
79
80
|
await navigator.clipboard.writeText(link);
|
|
80
81
|
setCopied(true);
|
|
81
|
-
toast.success("
|
|
82
|
+
toast.success(t("orders.link_copied"));
|
|
82
83
|
} catch {
|
|
83
|
-
toast.error("
|
|
84
|
+
toast.error(t("orders.copy_failed"));
|
|
84
85
|
}
|
|
85
86
|
};
|
|
86
87
|
|
|
87
88
|
return (
|
|
88
89
|
<Card>
|
|
89
90
|
<CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
|
|
90
|
-
<CardTitle className="text-base">
|
|
91
|
+
<CardTitle className="text-base">{t("orders.payment")}</CardTitle>
|
|
91
92
|
{paid ? (
|
|
92
|
-
<Badge variant="secondary" className="font-normal">
|
|
93
|
+
<Badge variant="secondary" className="font-normal">{t("orders.paid")}</Badge>
|
|
93
94
|
) : (
|
|
94
|
-
<Badge variant="outline" className="font-normal">
|
|
95
|
+
<Badge variant="outline" className="font-normal">{t("orders.awaiting_payment")}</Badge>
|
|
95
96
|
)}
|
|
96
97
|
</CardHeader>
|
|
97
98
|
<CardContent className="space-y-3">
|
|
98
99
|
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-sm">
|
|
99
|
-
<dt className="text-muted-foreground">
|
|
100
|
+
<dt className="text-muted-foreground">{t("orders.method")}</dt>
|
|
100
101
|
<dd>{order.payment_method_title || order.payment_method || "—"}</dd>
|
|
101
|
-
<dt className="text-muted-foreground">
|
|
102
|
+
<dt className="text-muted-foreground">{t("orders.total")}</dt>
|
|
102
103
|
<dd>{format(order.total || 0)}</dd>
|
|
103
104
|
{order.date_paid && (
|
|
104
105
|
<>
|
|
105
|
-
<dt className="text-muted-foreground">
|
|
106
|
+
<dt className="text-muted-foreground">{t("orders.paid")}</dt>
|
|
106
107
|
<dd>{formatDateTime(order.date_paid)}</dd>
|
|
107
108
|
</>
|
|
108
109
|
)}
|
|
109
110
|
{order.transaction_id && (
|
|
110
111
|
<>
|
|
111
|
-
<dt className="text-muted-foreground">
|
|
112
|
+
<dt className="text-muted-foreground">{t("orders.reference")}</dt>
|
|
112
113
|
<dd className="truncate font-mono text-xs">{order.transaction_id}</dd>
|
|
113
114
|
</>
|
|
114
115
|
)}
|
|
@@ -117,8 +118,8 @@ export default function PaymentPanel({ order, onChanged }) {
|
|
|
117
118
|
{!paid && !isCard && (
|
|
118
119
|
<p className="text-xs text-muted-foreground">
|
|
119
120
|
{order.payment_method
|
|
120
|
-
?
|
|
121
|
-
: "
|
|
121
|
+
? t("orders.pay_outside_hint", { method: order.payment_method_title || order.payment_method })
|
|
122
|
+
: t("orders.no_payment_method_hint")}
|
|
122
123
|
</p>
|
|
123
124
|
)}
|
|
124
125
|
|
|
@@ -136,7 +137,7 @@ export default function PaymentPanel({ order, onChanged }) {
|
|
|
136
137
|
) : (
|
|
137
138
|
<CreditCard className="mr-1.5 h-3.5 w-3.5" />
|
|
138
139
|
)}
|
|
139
|
-
{link ? "
|
|
140
|
+
{link ? t("orders.new_payment_link") : t("orders.create_payment_link")}
|
|
140
141
|
</Button>
|
|
141
142
|
<Button
|
|
142
143
|
variant="outline"
|
|
@@ -149,30 +150,29 @@ export default function PaymentPanel({ order, onChanged }) {
|
|
|
149
150
|
) : (
|
|
150
151
|
<RotateCw className="mr-1.5 h-3.5 w-3.5" />
|
|
151
152
|
)}
|
|
152
|
-
|
|
153
|
+
{t("orders.check_payment")}
|
|
153
154
|
</Button>
|
|
154
155
|
</div>
|
|
155
156
|
|
|
156
157
|
{willSwitchMethod && !link && (
|
|
157
158
|
<p className="text-xs text-muted-foreground">
|
|
158
159
|
{order.payment_method
|
|
159
|
-
?
|
|
160
|
-
: "
|
|
160
|
+
? t("orders.link_switches_method", { method: order.payment_method_title || order.payment_method })
|
|
161
|
+
: t("orders.link_sets_method")}
|
|
161
162
|
</p>
|
|
162
163
|
)}
|
|
163
164
|
|
|
164
165
|
{link && (
|
|
165
166
|
<div className="space-y-1.5">
|
|
166
167
|
<p className="text-xs text-muted-foreground">
|
|
167
|
-
|
|
168
|
-
updates itself once they pay.
|
|
168
|
+
{t("orders.payment_link_hint")}
|
|
169
169
|
</p>
|
|
170
170
|
<div className="flex gap-1.5">
|
|
171
171
|
<Input readOnly value={link} className="h-8 font-mono text-xs" onFocus={(e) => e.target.select()} />
|
|
172
|
-
<Button variant="outline" size="icon" className="h-8 w-8 shrink-0" onClick={copy} title="
|
|
172
|
+
<Button variant="outline" size="icon" className="h-8 w-8 shrink-0" onClick={copy} title={t("orders.copy_link")}>
|
|
173
173
|
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
|
|
174
174
|
</Button>
|
|
175
|
-
<Button variant="outline" size="icon" className="h-8 w-8 shrink-0" asChild title="
|
|
175
|
+
<Button variant="outline" size="icon" className="h-8 w-8 shrink-0" asChild title={t("orders.open")}>
|
|
176
176
|
<a href={link} target="_blank" rel="noreferrer">
|
|
177
177
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
178
178
|
</a>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { Loader2 } from "lucide-react";
|
|
15
15
|
import { toast } from "sonner";
|
|
16
16
|
|
|
17
|
+
import { t } from "../../../i18n";
|
|
17
18
|
import { call } from "../../../lib/api";
|
|
18
19
|
import useMoney from "../../../hooks/useMoney";
|
|
19
20
|
import { formatDate } from "../../../lib/format";
|
|
@@ -90,8 +91,8 @@ export default function RefundPanel({ order, refunds, onDone }) {
|
|
|
90
91
|
});
|
|
91
92
|
toast.success(
|
|
92
93
|
data?.gateway_refund
|
|
93
|
-
?
|
|
94
|
-
:
|
|
94
|
+
? t("orders.refunded_via_provider", { amount: format(total) })
|
|
95
|
+
: t("orders.refunded_amount", { amount: format(total) }),
|
|
95
96
|
);
|
|
96
97
|
setRows({});
|
|
97
98
|
setShippingAmount(0);
|
|
@@ -106,15 +107,15 @@ export default function RefundPanel({ order, refunds, onDone }) {
|
|
|
106
107
|
|
|
107
108
|
return (
|
|
108
109
|
<div className="space-y-4 rounded-lg border bg-muted/20 p-4">
|
|
109
|
-
<h3 className="text-sm font-semibold">
|
|
110
|
+
<h3 className="text-sm font-semibold">{t("orders.refund")}</h3>
|
|
110
111
|
|
|
111
112
|
<Table>
|
|
112
113
|
<TableHeader>
|
|
113
114
|
<TableRow>
|
|
114
|
-
<TableHead>
|
|
115
|
-
<TableHead className="w-24 text-right">
|
|
116
|
-
<TableHead className="w-24 text-right">
|
|
117
|
-
<TableHead className="w-32 text-right">
|
|
115
|
+
<TableHead>{t("orders.item")}</TableHead>
|
|
116
|
+
<TableHead className="w-24 text-right">{t("orders.refunded")}</TableHead>
|
|
117
|
+
<TableHead className="w-24 text-right">{t("orders.qty")}</TableHead>
|
|
118
|
+
<TableHead className="w-32 text-right">{t("common.amount")}</TableHead>
|
|
118
119
|
</TableRow>
|
|
119
120
|
</TableHeader>
|
|
120
121
|
<TableBody>
|
|
@@ -158,7 +159,7 @@ export default function RefundPanel({ order, refunds, onDone }) {
|
|
|
158
159
|
|
|
159
160
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
|
160
161
|
<div className="grid gap-1.5">
|
|
161
|
-
<Label className="text-xs">
|
|
162
|
+
<Label className="text-xs">{t("orders.refund_shipping")}</Label>
|
|
162
163
|
<Input
|
|
163
164
|
type="number"
|
|
164
165
|
step="any"
|
|
@@ -168,7 +169,7 @@ export default function RefundPanel({ order, refunds, onDone }) {
|
|
|
168
169
|
/>
|
|
169
170
|
</div>
|
|
170
171
|
<div className="grid gap-1.5 sm:col-span-2">
|
|
171
|
-
<Label className="text-xs">
|
|
172
|
+
<Label className="text-xs">{t("orders.refund_reason")}</Label>
|
|
172
173
|
<Input value={reason} onChange={(e) => setReason(e.target.value)} />
|
|
173
174
|
</div>
|
|
174
175
|
</div>
|
|
@@ -176,7 +177,7 @@ export default function RefundPanel({ order, refunds, onDone }) {
|
|
|
176
177
|
<div className="flex flex-wrap items-center gap-4">
|
|
177
178
|
<label className="flex items-center gap-2 text-sm">
|
|
178
179
|
<Checkbox checked={restock} onCheckedChange={(v) => setRestock(Boolean(v))} />
|
|
179
|
-
|
|
180
|
+
{t("orders.restock_items")}
|
|
180
181
|
</label>
|
|
181
182
|
{order.payment_method === "card" && (
|
|
182
183
|
<label
|
|
@@ -184,7 +185,7 @@ export default function RefundPanel({ order, refunds, onDone }) {
|
|
|
184
185
|
title={
|
|
185
186
|
canRefundOnline
|
|
186
187
|
? undefined
|
|
187
|
-
: "
|
|
188
|
+
: t("orders.no_card_payment_tip")
|
|
188
189
|
}
|
|
189
190
|
>
|
|
190
191
|
<Checkbox
|
|
@@ -192,7 +193,7 @@ export default function RefundPanel({ order, refunds, onDone }) {
|
|
|
192
193
|
disabled={!canRefundOnline}
|
|
193
194
|
onCheckedChange={(v) => setViaGateway(Boolean(v))}
|
|
194
195
|
/>
|
|
195
|
-
|
|
196
|
+
{t("orders.refund_via_provider")}
|
|
196
197
|
</label>
|
|
197
198
|
)}
|
|
198
199
|
</div>
|
|
@@ -200,27 +201,27 @@ export default function RefundPanel({ order, refunds, onDone }) {
|
|
|
200
201
|
|
|
201
202
|
<div className="flex items-center justify-between">
|
|
202
203
|
<p className="text-sm text-muted-foreground">
|
|
203
|
-
|
|
204
|
+
{t("orders.available_to_refund")} <span className="font-medium text-foreground">{format(maxRefund)}</span>
|
|
204
205
|
</p>
|
|
205
206
|
<Button onClick={submit} disabled={busy || total <= 0 || overMax} variant="destructive">
|
|
206
207
|
{busy && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
|
|
207
|
-
|
|
208
|
+
{t("orders.refund_amount", { amount: format(total) })}
|
|
208
209
|
</Button>
|
|
209
210
|
</div>
|
|
210
211
|
{overMax && (
|
|
211
|
-
<p className="text-right text-xs text-destructive">
|
|
212
|
+
<p className="text-right text-xs text-destructive">{t("orders.refund_over_max")}</p>
|
|
212
213
|
)}
|
|
213
214
|
|
|
214
215
|
{(refunds || []).length > 0 && (
|
|
215
216
|
<div className="border-t pt-3">
|
|
216
|
-
<h4 className="mb-2 text-xs font-medium text-muted-foreground">
|
|
217
|
+
<h4 className="mb-2 text-xs font-medium text-muted-foreground">{t("orders.previous_refunds")}</h4>
|
|
217
218
|
<ul className="space-y-1 text-sm">
|
|
218
219
|
{refunds.map((r) => (
|
|
219
220
|
<li key={r.id} className="flex justify-between">
|
|
220
221
|
<span>
|
|
221
222
|
{formatDate(r.created_date)}
|
|
222
223
|
{r.reason ? ` — ${r.reason}` : ""}
|
|
223
|
-
{r.refunded_by ? <span className="text-muted-foreground">
|
|
224
|
+
{r.refunded_by ? <span className="text-muted-foreground"> {t("orders.refunded_by", { name: r.refunded_by })}</span> : null}
|
|
224
225
|
</span>
|
|
225
226
|
<span className="text-destructive">− {format(r.amount || 0)}</span>
|
|
226
227
|
</li>
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Separator } from "@/components/ui/separator";
|
|
3
|
+
import { t } from "../../../i18n";
|
|
3
4
|
import useMoney from "../../../hooks/useMoney";
|
|
4
5
|
import { orderTotals } from "../../../lib/order-utils";
|
|
5
6
|
|
|
6
7
|
/** Order totals summary. Props: { order } */
|
|
7
8
|
export default function TotalsBox({ order }) {
|
|
8
9
|
const { format } = useMoney();
|
|
9
|
-
const
|
|
10
|
-
if (!
|
|
10
|
+
const totals = orderTotals(order);
|
|
11
|
+
if (!totals) return null;
|
|
11
12
|
|
|
12
13
|
const row = (label, value, className = "") => (
|
|
13
14
|
<div className={`flex items-center justify-between py-1 text-sm ${className}`}>
|
|
@@ -20,30 +21,30 @@ export default function TotalsBox({ order }) {
|
|
|
20
21
|
|
|
21
22
|
return (
|
|
22
23
|
<div className="ml-auto w-full max-w-sm">
|
|
23
|
-
{row("
|
|
24
|
-
{
|
|
25
|
-
row(couponCodes ?
|
|
26
|
-
{
|
|
27
|
-
{row("
|
|
28
|
-
{(
|
|
29
|
-
?
|
|
24
|
+
{row(t("orders.items_subtotal"), format(totals.itemsSubtotal))}
|
|
25
|
+
{totals.discount > 0 &&
|
|
26
|
+
row(couponCodes ? t("orders.coupons_codes", { codes: couponCodes }) : t("orders.discount"), `− ${format(totals.discount)}`)}
|
|
27
|
+
{totals.fees !== 0 && row(t("orders.fees"), format(totals.fees))}
|
|
28
|
+
{row(t("orders.shipping"), format(totals.shipping))}
|
|
29
|
+
{(totals.taxLines || []).length > 0
|
|
30
|
+
? totals.taxLines.map((tl, i) => (
|
|
30
31
|
<div key={tl.rate_id || i} className="flex items-center justify-between py-1 text-sm">
|
|
31
|
-
<span className="text-muted-foreground">{tl.label || "
|
|
32
|
+
<span className="text-muted-foreground">{tl.label || t("orders.tax")}</span>
|
|
32
33
|
<span>{format((tl.tax_total || 0) + (tl.shipping_tax_total || 0))}</span>
|
|
33
34
|
</div>
|
|
34
35
|
))
|
|
35
|
-
: row("
|
|
36
|
+
: row(t("orders.tax"), format(totals.taxTotal))}
|
|
36
37
|
<Separator className="my-1.5" />
|
|
37
38
|
<div className="flex items-center justify-between py-1 font-semibold">
|
|
38
|
-
<span>
|
|
39
|
-
<span>{format(
|
|
39
|
+
<span>{t("orders.order_total")}</span>
|
|
40
|
+
<span>{format(totals.total)}</span>
|
|
40
41
|
</div>
|
|
41
|
-
{
|
|
42
|
+
{totals.refunded > 0 && (
|
|
42
43
|
<>
|
|
43
|
-
{row("
|
|
44
|
+
{row(t("orders.refunded"), `− ${format(totals.refunded)}`, "text-destructive")}
|
|
44
45
|
<div className="flex items-center justify-between py-1 text-sm font-medium">
|
|
45
|
-
<span>
|
|
46
|
-
<span>{format(
|
|
46
|
+
<span>{t("orders.net_payment")}</span>
|
|
47
|
+
<span>{format(totals.net)}</span>
|
|
47
48
|
</div>
|
|
48
49
|
</>
|
|
49
50
|
)}
|