@base44/app-plugin-commerce 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -10
- package/base44/entities/commerce.ProductReview.jsonc +2 -2
- package/base44/functions/commerce/storefront-catalog/entry.ts +7 -3
- package/package.json +1 -1
- package/scripts/install.js +2 -4
- package/skills/commerce/SKILL.md +9 -3
- package/skills/commerce/docs/api-storefront.md +3 -3
- package/skills/commerce/install/01-install.md +8 -24
- package/skills/commerce/install/02-storefront.md +22 -9
- package/skills/commerce/references/admin-localization.md +95 -0
- package/skills/commerce/references/catalog-rendering.md +1 -1
- package/skills/commerce/references/reviews.md +7 -6
- package/src/commerce/admin/README.md +36 -14
- package/src/commerce/admin/bot/StoreAdminBot.jsx +12 -11
- package/src/commerce/admin/components/AddressForm.jsx +13 -12
- package/src/commerce/admin/components/ConfirmDialog.jsx +5 -4
- package/src/commerce/admin/components/CountrySelect.jsx +12 -8
- package/src/commerce/admin/components/DataTable.jsx +3 -2
- package/src/commerce/admin/components/DateRangePicker.jsx +11 -10
- package/src/commerce/admin/components/EmptyState.jsx +2 -1
- package/src/commerce/admin/components/MediaUploader.jsx +3 -2
- package/src/commerce/admin/components/MetaDataEditor.jsx +4 -3
- package/src/commerce/admin/components/SearchSelect.jsx +4 -3
- package/src/commerce/admin/context/SettingsContext.jsx +12 -25
- package/src/commerce/admin/hooks/useMoney.js +3 -2
- package/src/commerce/admin/i18n/index.js +54 -0
- package/src/commerce/admin/i18n/locales/de.js +896 -0
- package/src/commerce/admin/i18n/locales/en.js +900 -0
- package/src/commerce/admin/i18n/locales/es.js +896 -0
- package/src/commerce/admin/i18n/locales/fr.js +896 -0
- package/src/commerce/admin/i18n/locales/ja.js +896 -0
- package/src/commerce/admin/i18n/locales/pt.js +896 -0
- package/src/commerce/admin/index.jsx +6 -30
- package/src/commerce/admin/layout/AccessDenied.jsx +8 -9
- package/src/commerce/admin/layout/AuthGuard.jsx +2 -1
- package/src/commerce/admin/layout/Sidebar.jsx +29 -18
- package/src/commerce/admin/layout/Topbar.jsx +4 -3
- package/src/commerce/admin/lib/api.js +3 -2
- package/src/commerce/admin/lib/constants.js +44 -43
- package/src/commerce/admin/lib/format.js +4 -3
- package/src/commerce/admin/lib/paths.js +2 -3
- package/src/commerce/admin/pages/Dashboard.jsx +25 -24
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +55 -57
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +22 -21
- package/src/commerce/admin/pages/customers/CustomerEditor.jsx +31 -30
- package/src/commerce/admin/pages/customers/CustomersList.jsx +19 -18
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +75 -71
- package/src/commerce/admin/pages/orders/OrdersList.jsx +16 -15
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +12 -11
- package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +14 -13
- package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +12 -11
- package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +11 -10
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +22 -22
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +18 -17
- package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +18 -17
- package/src/commerce/admin/pages/products/Categories.jsx +14 -13
- package/src/commerce/admin/pages/products/ProductEditor.jsx +16 -15
- package/src/commerce/admin/pages/products/ProductsList.jsx +41 -36
- package/src/commerce/admin/pages/products/Reviews.jsx +54 -46
- package/src/commerce/admin/pages/products/components/AttributesSection.jsx +45 -39
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +8 -7
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +10 -9
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +18 -17
- package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +11 -10
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +9 -10
- package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +3 -9
- package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +76 -78
- package/src/commerce/admin/pages/reports/Reports.jsx +33 -32
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +36 -39
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +13 -14
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +12 -13
- package/src/commerce/admin/pages/settings/LocationEditor.jsx +48 -47
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +32 -33
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +13 -12
- package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +31 -31
- package/src/commerce/admin/pages/settings/useGroupForm.jsx +4 -3
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +34 -31
- package/src/commerce/admin/pages/status/Webhooks.jsx +11 -10
- package/src/commerce/admin/routes.jsx +31 -43
- package/src/commerce/storefront/pickers.jsx +19 -5
- package/src/commerce/storefront/useCheckout.jsx +28 -3
- package/src/commerce/utils/storefront.js +49 -2
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { useOutlet } from "react-router-dom";
|
|
3
2
|
import { Toaster } from "sonner";
|
|
4
3
|
import AuthGuard from "./layout/AuthGuard";
|
|
5
4
|
import AdminLayout from "./layout/AdminLayout";
|
|
@@ -7,50 +6,27 @@ import AdminRoutes from "./routes";
|
|
|
7
6
|
import { SettingsProvider } from "./context/SettingsContext";
|
|
8
7
|
import { BasePathProvider } from "./context/BasePathContext";
|
|
9
8
|
|
|
10
|
-
export { default as AdminRoutes } from "./routes";
|
|
11
|
-
|
|
12
9
|
/**
|
|
13
10
|
* The store admin application.
|
|
14
11
|
*
|
|
15
|
-
* Mount
|
|
16
|
-
*
|
|
17
|
-
* JSX, and hand everything deeper to `<AdminRoutes />` on a splat. Base44
|
|
18
|
-
* discovers an app's pages by reading that file rather than running it, so a
|
|
19
|
-
* route declared anywhere else is reachable by URL but never listed — and a
|
|
20
|
-
* splat is skipped by discovery, which is what keeps the editors and the
|
|
21
|
-
* settings tabs off the list while leaving them navigable.
|
|
22
|
-
*
|
|
23
|
-
* <Route path="/store-admin" element={<AdminApp />}>
|
|
24
|
-
* <Route index element={<Dashboard />} />
|
|
25
|
-
* <Route path="orders" element={<OrdersList />} />
|
|
26
|
-
* …
|
|
27
|
-
* <Route path="*" element={<AdminRoutes />} />
|
|
28
|
-
* </Route>
|
|
12
|
+
* Mount inside your app's router:
|
|
13
|
+
* <Route path="/store-admin/*" element={<AdminApp />} />
|
|
29
14
|
*
|
|
30
15
|
* If mounted somewhere other than /store-admin, pass the prefix:
|
|
31
|
-
* <Route path="/backoffice" element={<AdminApp basePath="/backoffice" />}
|
|
32
|
-
*
|
|
33
|
-
* The providers belong here, above the outlet, and must not be repeated per
|
|
34
|
-
* route: SettingsProvider fetches commerce.StoreSettings on mount, so wrapping
|
|
35
|
-
* each route would cost a round trip and a remounted sidebar on every admin
|
|
36
|
-
* navigation.
|
|
16
|
+
* <Route path="/backoffice/*" element={<AdminApp basePath="/backoffice" />} />
|
|
37
17
|
*
|
|
38
18
|
* Requires an authenticated user with role "admin" (enforced by AuthGuard,
|
|
39
19
|
* and independently by entity RLS + requireAdmin() in backend functions).
|
|
40
20
|
*/
|
|
41
21
|
export default function AdminApp({ basePath = "/store-admin" }) {
|
|
42
|
-
// Installs from before the routes moved into App.jsx mount the whole admin
|
|
43
|
-
// behind one splat route (`<Route path="/store-admin/*" element={<AdminApp />} />`)
|
|
44
|
-
// and declare no children, so nothing ever fills the outlet. Fall back to the
|
|
45
|
-
// kit's own router there rather than drawing an empty page area under the
|
|
46
|
-
// sidebar; those apps keep working until an agent rewrites their App.jsx.
|
|
47
|
-
const outlet = useOutlet();
|
|
48
22
|
return (
|
|
49
23
|
<BasePathProvider value={basePath}>
|
|
50
24
|
<Toaster richColors position="top-right" />
|
|
51
25
|
<AuthGuard>
|
|
52
26
|
<SettingsProvider>
|
|
53
|
-
<AdminLayout>
|
|
27
|
+
<AdminLayout>
|
|
28
|
+
<AdminRoutes />
|
|
29
|
+
</AdminLayout>
|
|
54
30
|
</SettingsProvider>
|
|
55
31
|
</AuthGuard>
|
|
56
32
|
</BasePathProvider>
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { Lock, ShieldAlert } from "lucide-react";
|
|
5
|
+
import { t } from "../i18n";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Access screens for the admin.
|
|
@@ -21,29 +22,27 @@ export default function AccessDenied({ variant = "forbidden", email, onLogin, on
|
|
|
21
22
|
<ShieldAlert className="h-6 w-6 text-destructive" />
|
|
22
23
|
)}
|
|
23
24
|
</div>
|
|
24
|
-
<CardTitle>{unauth ? "
|
|
25
|
+
<CardTitle>{unauth ? t("auth.sign_in_required") : t("auth.admin_required")}</CardTitle>
|
|
25
26
|
<CardDescription>
|
|
26
27
|
{unauth
|
|
27
28
|
? timedOut
|
|
28
|
-
? "
|
|
29
|
-
: "
|
|
30
|
-
: "
|
|
29
|
+
? t("auth.sign_in_timeout")
|
|
30
|
+
: t("auth.must_sign_in")
|
|
31
|
+
: t("auth.no_admin_role")}
|
|
31
32
|
</CardDescription>
|
|
32
33
|
</CardHeader>
|
|
33
34
|
<CardContent className="space-y-3">
|
|
34
35
|
{unauth ? (
|
|
35
36
|
<Button onClick={onLogin} className="w-full">
|
|
36
|
-
|
|
37
|
+
{t("auth.sign_in")}
|
|
37
38
|
</Button>
|
|
38
39
|
) : (
|
|
39
40
|
<>
|
|
40
41
|
{email && (
|
|
41
|
-
<p className="text-sm text-muted-foreground">
|
|
42
|
-
Signed in as <span className="font-medium">{email}</span>
|
|
43
|
-
</p>
|
|
42
|
+
<p className="text-sm text-muted-foreground">{t("auth.signed_in_as", { email })}</p>
|
|
44
43
|
)}
|
|
45
44
|
<Button variant="outline" onClick={onLogout} className="w-full">
|
|
46
|
-
|
|
45
|
+
{t("auth.sign_out")}
|
|
47
46
|
</Button>
|
|
48
47
|
</>
|
|
49
48
|
)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
|
2
2
|
import { Loader2 } from "lucide-react";
|
|
3
3
|
import { base44 } from "../lib/api";
|
|
4
|
+
import { t } from "../i18n";
|
|
4
5
|
import AccessDenied from "./AccessDenied";
|
|
5
6
|
|
|
6
7
|
const AuthContext = createContext(null);
|
|
@@ -55,7 +56,7 @@ export default function AuthGuard({ children }) {
|
|
|
55
56
|
return (
|
|
56
57
|
<div className="flex h-screen flex-col items-center justify-center gap-3">
|
|
57
58
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
58
|
-
<p className="text-xs text-muted-foreground">
|
|
59
|
+
<p className="text-xs text-muted-foreground">{t("auth.checking_access")}</p>
|
|
59
60
|
</div>
|
|
60
61
|
);
|
|
61
62
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { NavLink, useLocation } from "react-router-dom";
|
|
2
|
+
import { Link, NavLink, useLocation } from "react-router-dom";
|
|
3
3
|
import { Badge } from "@/components/ui/badge";
|
|
4
4
|
import {
|
|
5
5
|
BadgePercent,
|
|
@@ -9,36 +9,38 @@ import {
|
|
|
9
9
|
Settings,
|
|
10
10
|
ShoppingCart,
|
|
11
11
|
Sparkles,
|
|
12
|
+
Store,
|
|
12
13
|
Users,
|
|
13
14
|
} from "lucide-react";
|
|
14
15
|
import { call } from "../lib/api";
|
|
15
16
|
import { useAdminHref } from "../context/BasePathContext";
|
|
17
|
+
import { t } from "../i18n";
|
|
16
18
|
|
|
17
19
|
const NAV = [
|
|
18
|
-
{ items: [{ label: "
|
|
20
|
+
{ items: [{ label: t("routes.dashboard"), path: "", icon: LayoutDashboard, end: true }] },
|
|
19
21
|
{
|
|
20
22
|
items: [
|
|
21
|
-
{ label: "
|
|
22
|
-
{ label: "
|
|
23
|
-
{ label: "
|
|
23
|
+
{ label: t("routes.orders"), path: "orders", icon: ShoppingCart, badge: "processing" },
|
|
24
|
+
{ label: t("routes.customers"), path: "customers", icon: Users },
|
|
25
|
+
{ label: t("routes.reports"), path: "reports", icon: BarChart3 },
|
|
24
26
|
],
|
|
25
27
|
},
|
|
26
28
|
{
|
|
27
|
-
title: "
|
|
29
|
+
title: t("nav.products_group"),
|
|
28
30
|
icon: Package,
|
|
29
31
|
items: [
|
|
30
|
-
{ label: "
|
|
31
|
-
{ label: "
|
|
32
|
-
{ label: "
|
|
32
|
+
{ label: t("nav.all_products"), path: "products", end: true },
|
|
33
|
+
{ label: t("routes.categories"), path: "products/categories" },
|
|
34
|
+
{ label: t("routes.reviews"), path: "products/reviews" },
|
|
33
35
|
],
|
|
34
36
|
},
|
|
35
37
|
{
|
|
36
|
-
title: "
|
|
38
|
+
title: t("nav.marketing_group"),
|
|
37
39
|
icon: BadgePercent,
|
|
38
|
-
items: [{ label: "
|
|
40
|
+
items: [{ label: t("routes.coupons"), path: "coupons" }],
|
|
39
41
|
},
|
|
40
42
|
{
|
|
41
|
-
items: [{ label: "
|
|
43
|
+
items: [{ label: t("routes.settings"), path: "settings", icon: Settings }],
|
|
42
44
|
},
|
|
43
45
|
];
|
|
44
46
|
|
|
@@ -70,7 +72,7 @@ export default function Sidebar({ onNavigate, onOpenBot }) {
|
|
|
70
72
|
<div className="flex h-full flex-col">
|
|
71
73
|
<div className="flex h-14 items-center gap-2 border-b px-4">
|
|
72
74
|
<Package className="h-5 w-5 text-primary" />
|
|
73
|
-
<span className="font-semibold">
|
|
75
|
+
<span className="font-semibold">{t("nav.store_management")}</span>
|
|
74
76
|
</div>
|
|
75
77
|
<nav className="flex-1 space-y-4 overflow-y-auto p-3">
|
|
76
78
|
{NAV.map((group, gi) => (
|
|
@@ -105,18 +107,27 @@ export default function Sidebar({ onNavigate, onOpenBot }) {
|
|
|
105
107
|
</div>
|
|
106
108
|
))}
|
|
107
109
|
</nav>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
<div className="space-y-0.5 border-t p-3">
|
|
111
|
+
{/* The way back out — the storefront the merchant is managing. */}
|
|
112
|
+
<Link
|
|
113
|
+
to="/"
|
|
114
|
+
onClick={onNavigate}
|
|
115
|
+
className="flex items-center gap-2.5 rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
116
|
+
>
|
|
117
|
+
<Store className="h-4 w-4 shrink-0" />
|
|
118
|
+
<span className="flex-1">{t("nav.view_storefront")}</span>
|
|
119
|
+
</Link>
|
|
120
|
+
{onOpenBot && (
|
|
110
121
|
<button
|
|
111
122
|
type="button"
|
|
112
123
|
onClick={onOpenBot}
|
|
113
124
|
className="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
114
125
|
>
|
|
115
126
|
<Sparkles className="h-4 w-4 shrink-0 text-primary" />
|
|
116
|
-
<span className="flex-1 text-left">
|
|
127
|
+
<span className="flex-1 text-left">{t("nav.storeadmin_bot")}</span>
|
|
117
128
|
</button>
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
120
131
|
</div>
|
|
121
132
|
);
|
|
122
133
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@/components/ui/dropdown-menu";
|
|
12
12
|
import { ChevronRight, LogOut, Menu, UserCircle } from "lucide-react";
|
|
13
13
|
import { base44 } from "../lib/api";
|
|
14
|
+
import { t } from "../i18n";
|
|
14
15
|
import { useAuth } from "./AuthGuard";
|
|
15
16
|
import { useBasePath, useAdminHref } from "../context/BasePathContext";
|
|
16
17
|
import { ADMIN_ROUTES } from "../routes";
|
|
@@ -30,7 +31,7 @@ function useBreadcrumbs() {
|
|
|
30
31
|
const base = useBasePath();
|
|
31
32
|
const rel = pathname.startsWith(base) ? pathname.slice(base.length) : pathname;
|
|
32
33
|
const segs = rel.split("/").filter(Boolean);
|
|
33
|
-
const crumbs = [{ label: "
|
|
34
|
+
const crumbs = [{ label: t("routes.dashboard"), path: "" }];
|
|
34
35
|
segs.forEach((seg, i) => {
|
|
35
36
|
const prefix = segs.slice(0, i + 1).join("/");
|
|
36
37
|
const route = matchRoute(prefix);
|
|
@@ -80,12 +81,12 @@ export default function Topbar({ onMenuClick }) {
|
|
|
80
81
|
</DropdownMenuTrigger>
|
|
81
82
|
<DropdownMenuContent align="end">
|
|
82
83
|
<DropdownMenuLabel className="font-normal">
|
|
83
|
-
<div className="text-sm font-medium">{user?.full_name || "
|
|
84
|
+
<div className="text-sm font-medium">{user?.full_name || t("auth.administrator")}</div>
|
|
84
85
|
<div className="text-xs text-muted-foreground">{user?.email}</div>
|
|
85
86
|
</DropdownMenuLabel>
|
|
86
87
|
<DropdownMenuSeparator />
|
|
87
88
|
<DropdownMenuItem onClick={() => base44.auth.logout()}>
|
|
88
|
-
<LogOut className="mr-2 h-4 w-4" />
|
|
89
|
+
<LogOut className="mr-2 h-4 w-4" /> {t("auth.sign_out")}
|
|
89
90
|
</DropdownMenuItem>
|
|
90
91
|
</DropdownMenuContent>
|
|
91
92
|
</DropdownMenu>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { base44 } from "@/api/base44Client";
|
|
11
11
|
import { toast } from "sonner";
|
|
12
|
+
import { t } from "../i18n";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Invoke a backend function using the template's `{action, ...payload}` convention.
|
|
@@ -31,7 +32,7 @@ export async function call(fn, action, payload = {}, opts = {}) {
|
|
|
31
32
|
const data = res?.data;
|
|
32
33
|
if (data && typeof data === "object" && "success" in data) {
|
|
33
34
|
if (data.success === false) {
|
|
34
|
-
throw Object.assign(new Error(data.error || "
|
|
35
|
+
throw Object.assign(new Error(data.error || t("common.request_failed")), {
|
|
35
36
|
code: data.code,
|
|
36
37
|
details: data,
|
|
37
38
|
});
|
|
@@ -41,7 +42,7 @@ export async function call(fn, action, payload = {}, opts = {}) {
|
|
|
41
42
|
return data;
|
|
42
43
|
} catch (err) {
|
|
43
44
|
const body = err.response?.data;
|
|
44
|
-
const message = body?.error || err.message || "
|
|
45
|
+
const message = body?.error || err.message || t("common.request_failed");
|
|
45
46
|
const wrapped = Object.assign(new Error(message), {
|
|
46
47
|
code: body?.code || err.code,
|
|
47
48
|
details: body || err.details || null,
|
|
@@ -1,56 +1,57 @@
|
|
|
1
|
-
/** Shared enums, labels and badge colors for the admin UI. */
|
|
1
|
+
/** Shared enums, labels and badge colors for the admin UI. Labels come from i18n. */
|
|
2
|
+
import { t } from "../i18n";
|
|
2
3
|
|
|
3
4
|
export const ORDER_STATUSES = [
|
|
4
|
-
{ value: "pending", label: "
|
|
5
|
-
{ value: "processing", label: "
|
|
6
|
-
{ value: "on-hold", label: "
|
|
7
|
-
{ value: "completed", label: "
|
|
8
|
-
{ value: "cancelled", label: "
|
|
9
|
-
{ value: "refunded", label: "
|
|
10
|
-
{ value: "failed", label: "
|
|
5
|
+
{ value: "pending", label: t("status.order.pending"), color: "bg-amber-100 text-amber-800 border-amber-200" },
|
|
6
|
+
{ value: "processing", label: t("status.order.processing"), color: "bg-green-100 text-green-800 border-green-200" },
|
|
7
|
+
{ value: "on-hold", label: t("status.order.on-hold"), color: "bg-orange-100 text-orange-800 border-orange-200" },
|
|
8
|
+
{ value: "completed", label: t("status.order.completed"), color: "bg-blue-100 text-blue-800 border-blue-200" },
|
|
9
|
+
{ value: "cancelled", label: t("status.order.cancelled"), color: "bg-gray-100 text-gray-700 border-gray-200" },
|
|
10
|
+
{ value: "refunded", label: t("status.order.refunded"), color: "bg-slate-100 text-slate-700 border-slate-200" },
|
|
11
|
+
{ value: "failed", label: t("status.order.failed"), color: "bg-red-100 text-red-800 border-red-200" },
|
|
11
12
|
];
|
|
12
13
|
|
|
13
14
|
export const PRODUCT_STATUSES = [
|
|
14
|
-
{ value: "draft", label: "
|
|
15
|
-
{ value: "pending", label: "
|
|
16
|
-
{ value: "private", label: "
|
|
17
|
-
{ value: "publish", label: "
|
|
15
|
+
{ value: "draft", label: t("status.product.draft"), color: "bg-gray-100 text-gray-700 border-gray-200" },
|
|
16
|
+
{ value: "pending", label: t("status.product.pending"), color: "bg-amber-100 text-amber-800 border-amber-200" },
|
|
17
|
+
{ value: "private", label: t("status.product.private"), color: "bg-slate-100 text-slate-700 border-slate-200" },
|
|
18
|
+
{ value: "publish", label: t("status.product.publish"), color: "bg-green-100 text-green-800 border-green-200" },
|
|
18
19
|
];
|
|
19
20
|
|
|
20
21
|
export const STOCK_STATUSES = [
|
|
21
|
-
{ value: "instock", label: "
|
|
22
|
-
{ value: "outofstock", label: "
|
|
23
|
-
{ value: "onbackorder", label: "
|
|
22
|
+
{ value: "instock", label: t("status.stock.instock"), color: "bg-green-100 text-green-800 border-green-200" },
|
|
23
|
+
{ value: "outofstock", label: t("status.stock.outofstock"), color: "bg-red-100 text-red-800 border-red-200" },
|
|
24
|
+
{ value: "onbackorder", label: t("status.stock.onbackorder"), color: "bg-amber-100 text-amber-800 border-amber-200" },
|
|
24
25
|
];
|
|
25
26
|
|
|
26
27
|
export const BACKORDER_OPTIONS = [
|
|
27
|
-
{ value: "no", label: "
|
|
28
|
-
{ value: "notify", label: "
|
|
29
|
-
{ value: "yes", label: "
|
|
28
|
+
{ value: "no", label: t("backorder.no") },
|
|
29
|
+
{ value: "notify", label: t("backorder.notify") },
|
|
30
|
+
{ value: "yes", label: t("backorder.yes") },
|
|
30
31
|
];
|
|
31
32
|
|
|
32
33
|
export const TAX_STATUSES = [
|
|
33
|
-
{ value: "taxable", label: "
|
|
34
|
-
{ value: "none", label: "
|
|
34
|
+
{ value: "taxable", label: t("tax_status.taxable") },
|
|
35
|
+
{ value: "none", label: t("tax_status.none") },
|
|
35
36
|
];
|
|
36
37
|
|
|
37
38
|
export const COUPON_TYPES = [
|
|
38
|
-
{ value: "percent", label: "
|
|
39
|
-
{ value: "fixed_cart", label: "
|
|
40
|
-
{ value: "fixed_product", label: "
|
|
39
|
+
{ value: "percent", label: t("coupon_type.percent") },
|
|
40
|
+
{ value: "fixed_cart", label: t("coupon_type.fixed_cart") },
|
|
41
|
+
{ value: "fixed_product", label: t("coupon_type.fixed_product") },
|
|
41
42
|
];
|
|
42
43
|
|
|
43
44
|
export const REVIEW_STATUSES = [
|
|
44
|
-
{ value: "approved", label: "
|
|
45
|
-
{ value: "hold", label: "
|
|
46
|
-
{ value: "spam", label: "
|
|
47
|
-
{ value: "trash", label: "
|
|
45
|
+
{ value: "approved", label: t("status.review.approved"), color: "bg-green-100 text-green-800 border-green-200" },
|
|
46
|
+
{ value: "hold", label: t("status.review.hold"), color: "bg-amber-100 text-amber-800 border-amber-200" },
|
|
47
|
+
{ value: "spam", label: t("status.review.spam"), color: "bg-red-100 text-red-800 border-red-200" },
|
|
48
|
+
{ value: "trash", label: t("status.review.trash"), color: "bg-gray-100 text-gray-700 border-gray-200" },
|
|
48
49
|
];
|
|
49
50
|
|
|
50
51
|
export const WEBHOOK_STATUSES = [
|
|
51
|
-
{ value: "active", label: "
|
|
52
|
-
{ value: "paused", label: "
|
|
53
|
-
{ value: "disabled", label: "
|
|
52
|
+
{ value: "active", label: t("status.webhook.active"), color: "bg-green-100 text-green-800 border-green-200" },
|
|
53
|
+
{ value: "paused", label: t("status.webhook.paused"), color: "bg-amber-100 text-amber-800 border-amber-200" },
|
|
54
|
+
{ value: "disabled", label: t("status.webhook.disabled"), color: "bg-gray-100 text-gray-700 border-gray-200" },
|
|
54
55
|
];
|
|
55
56
|
|
|
56
57
|
export const WEBHOOK_TOPICS = [
|
|
@@ -66,19 +67,19 @@ export const WEBHOOK_TOPICS = [
|
|
|
66
67
|
* `managed_by_auth` emails are handled by Base44 auth, not this template.
|
|
67
68
|
*/
|
|
68
69
|
export const EMAIL_TYPES = [
|
|
69
|
-
{ id: "new_order", label: "
|
|
70
|
-
{ id: "cancelled_order", label: "
|
|
71
|
-
{ id: "failed_order", label: "
|
|
72
|
-
{ id: "low_stock", label: "
|
|
73
|
-
{ id: "out_of_stock", label: "
|
|
74
|
-
{ id: "on_hold_order", label: "
|
|
75
|
-
{ id: "processing_order", label: "
|
|
76
|
-
{ id: "completed_order", label: "
|
|
77
|
-
{ id: "refunded_order", label: "
|
|
78
|
-
{ id: "customer_invoice", label: "
|
|
79
|
-
{ id: "customer_note", label: "
|
|
80
|
-
{ id: "reset_password", label: "
|
|
81
|
-
{ id: "new_account", label: "
|
|
70
|
+
{ id: "new_order", label: t("email_type.new_order"), recipient: "admin" },
|
|
71
|
+
{ id: "cancelled_order", label: t("email_type.cancelled_order"), recipient: "admin" },
|
|
72
|
+
{ id: "failed_order", label: t("email_type.failed_order"), recipient: "admin" },
|
|
73
|
+
{ id: "low_stock", label: t("email_type.low_stock"), recipient: "admin", stock: true },
|
|
74
|
+
{ id: "out_of_stock", label: t("email_type.out_of_stock"), recipient: "admin", stock: true },
|
|
75
|
+
{ id: "on_hold_order", label: t("email_type.on_hold_order"), recipient: "customer" },
|
|
76
|
+
{ id: "processing_order", label: t("email_type.processing_order"), recipient: "customer" },
|
|
77
|
+
{ id: "completed_order", label: t("email_type.completed_order"), recipient: "customer" },
|
|
78
|
+
{ id: "refunded_order", label: t("email_type.refunded_order"), recipient: "customer" },
|
|
79
|
+
{ id: "customer_invoice", label: t("email_type.customer_invoice"), recipient: "customer", manual: true },
|
|
80
|
+
{ id: "customer_note", label: t("email_type.customer_note"), recipient: "customer" },
|
|
81
|
+
{ id: "reset_password", label: t("email_type.reset_password"), recipient: "customer", managed_by_auth: true },
|
|
82
|
+
{ id: "new_account", label: t("email_type.new_account"), recipient: "customer", managed_by_auth: true },
|
|
82
83
|
];
|
|
83
84
|
|
|
84
85
|
export const CURRENCIES = [
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
/** Date/text formatting helpers. */
|
|
1
|
+
/** Date/text formatting helpers. Dates render in the admin's i18n locale. */
|
|
2
|
+
import { locale } from "../i18n";
|
|
2
3
|
|
|
3
4
|
export function formatDate(iso) {
|
|
4
5
|
if (!iso) return "—";
|
|
5
6
|
const d = new Date(iso);
|
|
6
7
|
if (isNaN(d)) return "—";
|
|
7
|
-
return d.toLocaleDateString(
|
|
8
|
+
return d.toLocaleDateString(locale, { year: "numeric", month: "short", day: "numeric" });
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function formatDateTime(iso) {
|
|
11
12
|
if (!iso) return "—";
|
|
12
13
|
const d = new Date(iso);
|
|
13
14
|
if (isNaN(d)) return "—";
|
|
14
|
-
return d.toLocaleString(
|
|
15
|
+
return d.toLocaleString(locale, {
|
|
15
16
|
year: "numeric",
|
|
16
17
|
month: "short",
|
|
17
18
|
day: "numeric",
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mount-path handling for the admin app.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* into places they shouldn't: the `basePath` prop
|
|
4
|
+
* The admin is mounted with a splat route — `<Route path="/store-admin/*">` — and both
|
|
5
|
+
* halves of that pattern leak into places they shouldn't: the `basePath` prop
|
|
7
6
|
* gets the pattern pasted in verbatim, and the literal URL `/store-admin/*` gets
|
|
8
7
|
* opened (pattern copied into the address bar, or a nav link built from the
|
|
9
8
|
* route table). Neither is a real page, so both are normalized here rather than
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { BarChart3, PackagePlus, Plus, ShoppingCart, TicketPercent, TrendingUp, AlertTriangle } from "lucide-react";
|
|
16
16
|
|
|
17
17
|
import { call, base44 } from "../lib/api";
|
|
18
|
+
import { t } from "../i18n";
|
|
18
19
|
import useAsync from "../hooks/useAsync";
|
|
19
20
|
import useRealtime from "../hooks/useRealtime";
|
|
20
21
|
import useMoney from "../hooks/useMoney";
|
|
@@ -85,23 +86,23 @@ export default function Dashboard() {
|
|
|
85
86
|
<div className="space-y-6">
|
|
86
87
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
87
88
|
<div>
|
|
88
|
-
<h1 className="text-xl font-semibold tracking-tight">
|
|
89
|
-
<p className="text-sm text-muted-foreground">
|
|
89
|
+
<h1 className="text-xl font-semibold tracking-tight">{t("routes.dashboard")}</h1>
|
|
90
|
+
<p className="text-sm text-muted-foreground">{t("dashboard.subtitle")}</p>
|
|
90
91
|
</div>
|
|
91
92
|
<div className="flex gap-2">
|
|
92
93
|
<Button variant="outline" size="sm" asChild>
|
|
93
94
|
<Link to={href("products/new")}>
|
|
94
|
-
<PackagePlus className="mr-1.5 h-4 w-4" />
|
|
95
|
+
<PackagePlus className="mr-1.5 h-4 w-4" /> {t("routes.products_new")}
|
|
95
96
|
</Link>
|
|
96
97
|
</Button>
|
|
97
98
|
<Button variant="outline" size="sm" asChild>
|
|
98
99
|
<Link to={href("coupons/new")}>
|
|
99
|
-
<TicketPercent className="mr-1.5 h-4 w-4" />
|
|
100
|
+
<TicketPercent className="mr-1.5 h-4 w-4" /> {t("dashboard.create_coupon")}
|
|
100
101
|
</Link>
|
|
101
102
|
</Button>
|
|
102
103
|
<Button variant="outline" size="sm" asChild>
|
|
103
104
|
<Link to={href("reports")}>
|
|
104
|
-
<BarChart3 className="mr-1.5 h-4 w-4" />
|
|
105
|
+
<BarChart3 className="mr-1.5 h-4 w-4" /> {t("dashboard.view_reports")}
|
|
105
106
|
</Link>
|
|
106
107
|
</Button>
|
|
107
108
|
</div>
|
|
@@ -110,28 +111,28 @@ export default function Dashboard() {
|
|
|
110
111
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
111
112
|
<StatCard
|
|
112
113
|
icon={TrendingUp}
|
|
113
|
-
label="
|
|
114
|
+
label={t("dashboard.net_sales_today")}
|
|
114
115
|
value={format(asMoneyNumber(s.sales_today))}
|
|
115
116
|
loading={summary.loading}
|
|
116
117
|
/>
|
|
117
118
|
<StatCard
|
|
118
119
|
icon={BarChart3}
|
|
119
|
-
label="
|
|
120
|
+
label={t("dashboard.net_sales_month")}
|
|
120
121
|
value={format(asMoneyNumber(s.sales_month))}
|
|
121
122
|
loading={summary.loading}
|
|
122
123
|
/>
|
|
123
124
|
<StatCard
|
|
124
125
|
icon={ShoppingCart}
|
|
125
|
-
label="
|
|
126
|
+
label={t("dashboard.awaiting_processing")}
|
|
126
127
|
value={processingCount}
|
|
127
|
-
hint="
|
|
128
|
+
hint={t("dashboard.orders_to_fulfill")}
|
|
128
129
|
loading={summary.loading}
|
|
129
130
|
/>
|
|
130
131
|
<StatCard
|
|
131
132
|
icon={AlertTriangle}
|
|
132
|
-
label="
|
|
133
|
+
label={t("dashboard.low_stock")}
|
|
133
134
|
value={s.low_stock_count ?? lowStock.length}
|
|
134
|
-
hint="
|
|
135
|
+
hint={t("dashboard.low_stock_hint")}
|
|
135
136
|
loading={summary.loading && stock.loading}
|
|
136
137
|
/>
|
|
137
138
|
</div>
|
|
@@ -141,7 +142,7 @@ export default function Dashboard() {
|
|
|
141
142
|
{ORDER_STATUSES.map((st) => (
|
|
142
143
|
<Link key={st.value} to={href(`orders?status=${st.value}`)}>
|
|
143
144
|
<Badge variant="outline" className={`${st.color} cursor-pointer`}>
|
|
144
|
-
{st.label
|
|
145
|
+
{t("dashboard.status_count", { status: st.label, count: ordersByStatus[st.value] || 0 })}
|
|
145
146
|
</Badge>
|
|
146
147
|
</Link>
|
|
147
148
|
))}
|
|
@@ -151,7 +152,7 @@ export default function Dashboard() {
|
|
|
151
152
|
{/* Latest orders */}
|
|
152
153
|
<Card className="xl:col-span-2">
|
|
153
154
|
<CardHeader className="pb-2">
|
|
154
|
-
<CardTitle className="text-base">
|
|
155
|
+
<CardTitle className="text-base">{t("dashboard.latest_orders")}</CardTitle>
|
|
155
156
|
</CardHeader>
|
|
156
157
|
<CardContent className="p-0">
|
|
157
158
|
{latest.loading ? (
|
|
@@ -164,19 +165,19 @@ export default function Dashboard() {
|
|
|
164
165
|
<div className="py-10">
|
|
165
166
|
<EmptyState
|
|
166
167
|
icon={ShoppingCart}
|
|
167
|
-
title="
|
|
168
|
-
description="
|
|
168
|
+
title={t("dashboard.no_orders_title")}
|
|
169
|
+
description={t("dashboard.no_orders_description")}
|
|
169
170
|
/>
|
|
170
171
|
</div>
|
|
171
172
|
) : (
|
|
172
173
|
<Table>
|
|
173
174
|
<TableHeader>
|
|
174
175
|
<TableRow>
|
|
175
|
-
<TableHead>
|
|
176
|
-
<TableHead>
|
|
177
|
-
<TableHead>
|
|
178
|
-
<TableHead>
|
|
179
|
-
<TableHead className="text-right">
|
|
176
|
+
<TableHead>{t("routes.order")}</TableHead>
|
|
177
|
+
<TableHead>{t("dashboard.date")}</TableHead>
|
|
178
|
+
<TableHead>{t("common.status")}</TableHead>
|
|
179
|
+
<TableHead>{t("routes.customer")}</TableHead>
|
|
180
|
+
<TableHead className="text-right">{t("dashboard.total")}</TableHead>
|
|
180
181
|
</TableRow>
|
|
181
182
|
</TableHeader>
|
|
182
183
|
<TableBody>
|
|
@@ -204,7 +205,7 @@ export default function Dashboard() {
|
|
|
204
205
|
{/* Low stock */}
|
|
205
206
|
<Card>
|
|
206
207
|
<CardHeader className="pb-2">
|
|
207
|
-
<CardTitle className="text-base">
|
|
208
|
+
<CardTitle className="text-base">{t("dashboard.low_on_stock")}</CardTitle>
|
|
208
209
|
</CardHeader>
|
|
209
210
|
<CardContent className="p-0">
|
|
210
211
|
{stock.loading ? (
|
|
@@ -215,7 +216,7 @@ export default function Dashboard() {
|
|
|
215
216
|
</div>
|
|
216
217
|
) : lowStock.length === 0 ? (
|
|
217
218
|
<div className="py-10">
|
|
218
|
-
<EmptyState icon={Plus} title="
|
|
219
|
+
<EmptyState icon={Plus} title={t("dashboard.all_stocked_title")} description={t("dashboard.all_stocked_description")} />
|
|
219
220
|
</div>
|
|
220
221
|
) : (
|
|
221
222
|
<ul className="divide-y">
|
|
@@ -227,10 +228,10 @@ export default function Dashboard() {
|
|
|
227
228
|
>
|
|
228
229
|
<span className="min-w-0">
|
|
229
230
|
<span className="block truncate text-sm font-medium">{p.name}</span>
|
|
230
|
-
{p.sku && <span className="text-xs text-muted-foreground">
|
|
231
|
+
{p.sku && <span className="text-xs text-muted-foreground">{t("dashboard.sku", { sku: p.sku })}</span>}
|
|
231
232
|
</span>
|
|
232
233
|
<Badge variant="outline" className="ml-2 shrink-0 bg-amber-100 text-amber-800 border-amber-200">
|
|
233
|
-
{p.stock_quantity ?? 0}
|
|
234
|
+
{t("dashboard.n_left", { count: p.stock_quantity ?? 0 })}
|
|
234
235
|
</Badge>
|
|
235
236
|
</Link>
|
|
236
237
|
</li>
|