@voyant-travel/admin 0.111.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.
Files changed (115) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +285 -0
  3. package/dist/app/extension-routes.d.ts +99 -0
  4. package/dist/app/extension-routes.d.ts.map +1 -0
  5. package/dist/app/extension-routes.js +134 -0
  6. package/dist/app/index.d.ts +9 -0
  7. package/dist/app/index.d.ts.map +1 -0
  8. package/dist/app/index.js +4 -0
  9. package/dist/app/root.d.ts +47 -0
  10. package/dist/app/root.d.ts.map +1 -0
  11. package/dist/app/root.js +55 -0
  12. package/dist/app/router.d.ts +30 -0
  13. package/dist/app/router.d.ts.map +1 -0
  14. package/dist/app/router.js +51 -0
  15. package/dist/app/workspace.d.ts +84 -0
  16. package/dist/app/workspace.d.ts.map +1 -0
  17. package/dist/app/workspace.js +87 -0
  18. package/dist/components/admin-breadcrumbs.d.ts +18 -0
  19. package/dist/components/admin-breadcrumbs.d.ts.map +1 -0
  20. package/dist/components/admin-breadcrumbs.js +84 -0
  21. package/dist/components/admin-nav-group.d.ts +11 -0
  22. package/dist/components/admin-nav-group.d.ts.map +1 -0
  23. package/dist/components/admin-nav-group.js +49 -0
  24. package/dist/components/admin-nav-link.d.ts +10 -0
  25. package/dist/components/admin-nav-link.d.ts.map +1 -0
  26. package/dist/components/admin-nav-link.js +5 -0
  27. package/dist/components/admin-page-head.d.ts +17 -0
  28. package/dist/components/admin-page-head.d.ts.map +1 -0
  29. package/dist/components/admin-page-head.js +107 -0
  30. package/dist/components/admin-widget-slot.d.ts +8 -0
  31. package/dist/components/admin-widget-slot.d.ts.map +1 -0
  32. package/dist/components/admin-widget-slot.js +19 -0
  33. package/dist/components/brand/voyant-mark.d.ts +3 -0
  34. package/dist/components/brand/voyant-mark.d.ts.map +1 -0
  35. package/dist/components/brand/voyant-mark.js +4 -0
  36. package/dist/components/brand/voyant-wordmark.d.ts +3 -0
  37. package/dist/components/brand/voyant-wordmark.d.ts.map +1 -0
  38. package/dist/components/brand/voyant-wordmark.js +4 -0
  39. package/dist/components/operator-admin-bootstrap-gate.d.ts +26 -0
  40. package/dist/components/operator-admin-bootstrap-gate.d.ts.map +1 -0
  41. package/dist/components/operator-admin-bootstrap-gate.js +22 -0
  42. package/dist/components/operator-admin-page-shell.d.ts +13 -0
  43. package/dist/components/operator-admin-page-shell.d.ts.map +1 -0
  44. package/dist/components/operator-admin-page-shell.js +6 -0
  45. package/dist/components/operator-admin-sidebar.d.ts +57 -0
  46. package/dist/components/operator-admin-sidebar.d.ts.map +1 -0
  47. package/dist/components/operator-admin-sidebar.js +104 -0
  48. package/dist/components/operator-admin-user-menu.d.ts +10 -0
  49. package/dist/components/operator-admin-user-menu.d.ts.map +1 -0
  50. package/dist/components/operator-admin-user-menu.js +19 -0
  51. package/dist/components/team-settings-page.d.ts +10 -0
  52. package/dist/components/team-settings-page.d.ts.map +1 -0
  53. package/dist/components/team-settings-page.js +149 -0
  54. package/dist/dashboard/dashboard-empty-states.d.ts +67 -0
  55. package/dist/dashboard/dashboard-empty-states.d.ts.map +1 -0
  56. package/dist/dashboard/dashboard-empty-states.js +65 -0
  57. package/dist/dashboard/dashboard-kpi-card.d.ts +13 -0
  58. package/dist/dashboard/dashboard-kpi-card.d.ts.map +1 -0
  59. package/dist/dashboard/dashboard-kpi-card.js +12 -0
  60. package/dist/dashboard/dashboard-page.d.ts +7 -0
  61. package/dist/dashboard/dashboard-page.d.ts.map +1 -0
  62. package/dist/dashboard/dashboard-page.js +150 -0
  63. package/dist/dashboard/dashboard-query-options.d.ts +224 -0
  64. package/dist/dashboard/dashboard-query-options.d.ts.map +1 -0
  65. package/dist/dashboard/dashboard-query-options.js +153 -0
  66. package/dist/dashboard/dashboard-skeleton.d.ts +13 -0
  67. package/dist/dashboard/dashboard-skeleton.d.ts.map +1 -0
  68. package/dist/dashboard/dashboard-skeleton.js +28 -0
  69. package/dist/extensions.d.ts +254 -0
  70. package/dist/extensions.d.ts.map +1 -0
  71. package/dist/extensions.js +139 -0
  72. package/dist/index.d.ts +51 -0
  73. package/dist/index.d.ts.map +1 -0
  74. package/dist/index.js +53 -0
  75. package/dist/lib/i18n.d.ts +2 -0
  76. package/dist/lib/i18n.d.ts.map +1 -0
  77. package/dist/lib/i18n.js +1 -0
  78. package/dist/lib/initials.d.ts +24 -0
  79. package/dist/lib/initials.d.ts.map +1 -0
  80. package/dist/lib/initials.js +45 -0
  81. package/dist/navigation/destinations.d.ts +83 -0
  82. package/dist/navigation/destinations.d.ts.map +1 -0
  83. package/dist/navigation/destinations.js +65 -0
  84. package/dist/navigation/operator-navigation.d.ts +10 -0
  85. package/dist/navigation/operator-navigation.d.ts.map +1 -0
  86. package/dist/navigation/operator-navigation.js +191 -0
  87. package/dist/providers/admin-extensions.d.ts +9 -0
  88. package/dist/providers/admin-extensions.d.ts.map +1 -0
  89. package/dist/providers/admin-extensions.js +10 -0
  90. package/dist/providers/admin-provider.d.ts +53 -0
  91. package/dist/providers/admin-provider.d.ts.map +1 -0
  92. package/dist/providers/admin-provider.js +26 -0
  93. package/dist/providers/locale-preferences.d.ts +12 -0
  94. package/dist/providers/locale-preferences.d.ts.map +1 -0
  95. package/dist/providers/locale-preferences.js +32 -0
  96. package/dist/providers/locale.d.ts +23 -0
  97. package/dist/providers/locale.d.ts.map +1 -0
  98. package/dist/providers/locale.js +98 -0
  99. package/dist/providers/operator-admin-messages.d.ts +14 -0
  100. package/dist/providers/operator-admin-messages.d.ts.map +1 -0
  101. package/dist/providers/operator-admin-messages.js +16 -0
  102. package/dist/providers/operator-admin-shell.d.ts +35 -0
  103. package/dist/providers/operator-admin-shell.d.ts.map +1 -0
  104. package/dist/providers/operator-admin-shell.js +20 -0
  105. package/dist/providers/query-client.d.ts +19 -0
  106. package/dist/providers/query-client.d.ts.map +1 -0
  107. package/dist/providers/query-client.js +34 -0
  108. package/dist/providers/theme.d.ts +29 -0
  109. package/dist/providers/theme.d.ts.map +1 -0
  110. package/dist/providers/theme.js +63 -0
  111. package/dist/types.d.ts +60 -0
  112. package/dist/types.d.ts.map +1 -0
  113. package/dist/types.js +2 -0
  114. package/package.json +222 -0
  115. package/src/styles.css +11 -0
@@ -0,0 +1,149 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useVoyantReactContext } from "@voyant-travel/react";
5
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, } from "@voyant-travel/ui/components";
6
+ import { Skeleton } from "@voyant-travel/ui/components/skeleton";
7
+ import { Copy, Loader2, Mail, Trash2, UserPlus } from "lucide-react";
8
+ import { createContext, useContext, useMemo, useState } from "react";
9
+ import { formatMessage } from "../lib/i18n.js";
10
+ import { useLocale } from "../providers/locale.js";
11
+ import { useOperatorAdminMessages } from "../providers/operator-admin-messages.js";
12
+ const QK = ["admin-invitations"];
13
+ const TeamSettingsPageApiContext = createContext(null);
14
+ function joinUrl(baseUrl, path) {
15
+ const trimmedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
16
+ const trimmedPath = path.startsWith("/") ? path : `/${path}`;
17
+ return `${trimmedBase}${trimmedPath}`;
18
+ }
19
+ async function readJson(response) {
20
+ if (!response.ok) {
21
+ let body;
22
+ try {
23
+ body = await response.json();
24
+ }
25
+ catch {
26
+ body = await response.text().catch(() => undefined);
27
+ }
28
+ const message = typeof body === "object" && body !== null && "error" in body
29
+ ? String(body.error)
30
+ : `API error: ${response.status} ${response.statusText}`;
31
+ throw new Error(message);
32
+ }
33
+ if (response.status === 204)
34
+ return undefined;
35
+ return response.json();
36
+ }
37
+ function createTeamSettingsPageApi(baseUrl, fetcher) {
38
+ const request = async (path, init = {}) => {
39
+ const headers = new Headers(init.headers);
40
+ if (init.body !== undefined && !headers.has("Content-Type")) {
41
+ headers.set("Content-Type", "application/json");
42
+ }
43
+ return readJson(await fetcher(joinUrl(baseUrl, path), { ...init, headers }));
44
+ };
45
+ const api = {
46
+ get: (path) => request(path, { method: "GET" }),
47
+ post: (path, body) => request(path, {
48
+ method: "POST",
49
+ body: body !== undefined ? JSON.stringify(body) : undefined,
50
+ }),
51
+ delete: (path) => request(path, { method: "DELETE" }),
52
+ };
53
+ return api;
54
+ }
55
+ function useTeamSettingsPageApi() {
56
+ const api = useContext(TeamSettingsPageApiContext);
57
+ if (!api)
58
+ throw new Error("TeamSettingsPage requires a TeamSettingsPageApiContext provider");
59
+ return api;
60
+ }
61
+ export function TeamSettingsPage({ api: apiProp } = {}) {
62
+ if (apiProp)
63
+ return _jsx(TeamSettingsPageContent, { api: apiProp });
64
+ return _jsx(TeamSettingsPageWithDefaultApi, {});
65
+ }
66
+ function TeamSettingsPageWithDefaultApi() {
67
+ const { baseUrl, fetcher } = useVoyantReactContext();
68
+ const api = useMemo(() => createTeamSettingsPageApi(baseUrl, fetcher), [baseUrl, fetcher]);
69
+ return _jsx(TeamSettingsPageContent, { api: api });
70
+ }
71
+ function TeamSettingsPageContent({ api }) {
72
+ const messages = useOperatorAdminMessages();
73
+ const { resolvedLocale } = useLocale();
74
+ const queryClient = useQueryClient();
75
+ const invitesQuery = useQuery({
76
+ queryKey: QK,
77
+ queryFn: () => api.get("/v1/admin/invitations"),
78
+ });
79
+ const revoke = useMutation({
80
+ mutationFn: (id) => api.delete(`/v1/admin/invitations/${id}`),
81
+ onSuccess: () => void queryClient.invalidateQueries({ queryKey: QK }),
82
+ });
83
+ const invites = invitesQuery.data?.data ?? [];
84
+ const pending = invites.filter((i) => !i.redeemedAt && new Date(i.expiresAt) > new Date());
85
+ const spent = invites.filter((i) => i.redeemedAt || new Date(i.expiresAt) <= new Date());
86
+ return (_jsx(TeamSettingsPageApiContext.Provider, { value: api, children: _jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: messages.team.title }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: messages.team.description })] }), _jsx(InviteMemberDialog, {})] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: messages.team.pendingInvitations }), _jsx(CardDescription, { children: pending.length === 0
87
+ ? messages.team.noOutstandingInvitations
88
+ : formatMessage(pending.length === 1
89
+ ? messages.team.invitationWaitingSingular
90
+ : messages.team.invitationWaitingPlural, { count: pending.length }) })] }), _jsx(CardContent, { children: invitesQuery.isPending ? (_jsx("ul", { className: "flex flex-col divide-y", children: Array.from({ length: 3 }).map((_, i) => (_jsxs("li", { className: "flex items-center gap-4 py-3", children: [_jsx(Skeleton, { className: "h-4 w-4 rounded" }), _jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-48" }), _jsx(Skeleton, { className: "h-3 w-40" })] }), _jsx(Skeleton, { className: "h-7 w-20" })] }, i))) })) : pending.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.team.nothingHereYet })) : (_jsx("ul", { className: "flex flex-col divide-y", children: pending.map((invite) => (_jsxs("li", { className: "flex items-center gap-4 py-3", children: [_jsx(Mail, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: invite.email }), _jsx("p", { className: "text-xs text-muted-foreground", children: formatMessage(messages.team.expires, {
91
+ date: new Date(invite.expiresAt).toLocaleString(resolvedLocale),
92
+ }) })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "text-destructive", onClick: () => {
93
+ if (confirm(formatMessage(messages.team.revokeConfirm, {
94
+ email: invite.email,
95
+ }))) {
96
+ revoke.mutate(invite.id);
97
+ }
98
+ }, disabled: revoke.isPending, children: [_jsx(Trash2, { className: "mr-1 h-3.5 w-3.5" }), messages.team.revoke] })] }, invite.id))) })) })] }), spent.length > 0 ? (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: messages.team.history }), _jsx(CardDescription, { children: messages.team.historyDescription })] }), _jsx(CardContent, { children: _jsx("ul", { className: "flex flex-col divide-y text-sm", children: spent.map((invite) => (_jsxs("li", { className: "flex items-center gap-4 py-2.5 text-muted-foreground", children: [_jsx(Mail, { className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: invite.email }), _jsx("span", { className: "text-xs", children: invite.redeemedAt
99
+ ? formatMessage(messages.team.redeemed, {
100
+ date: new Date(invite.redeemedAt).toLocaleDateString(resolvedLocale),
101
+ })
102
+ : formatMessage(messages.team.expired, {
103
+ date: new Date(invite.expiresAt).toLocaleDateString(resolvedLocale),
104
+ }) })] }, invite.id))) }) })] })) : null] }) }));
105
+ }
106
+ function InviteMemberDialog() {
107
+ const messages = useOperatorAdminMessages();
108
+ const api = useTeamSettingsPageApi();
109
+ const queryClient = useQueryClient();
110
+ const [open, setOpen] = useState(false);
111
+ const [email, setEmail] = useState("");
112
+ const [result, setResult] = useState(null);
113
+ const [error, setError] = useState(null);
114
+ const [copied, setCopied] = useState(false);
115
+ const create = useMutation({
116
+ mutationFn: () => api.post("/v1/admin/invitations", { email: email.trim() }),
117
+ onSuccess: (response) => {
118
+ setResult(response.data);
119
+ setError(null);
120
+ void queryClient.invalidateQueries({ queryKey: QK });
121
+ },
122
+ onError: (e) => setError(e instanceof Error ? e.message : messages.team.errorCouldNotSendInvitation),
123
+ });
124
+ const close = () => {
125
+ setOpen(false);
126
+ // Let the dialog close animation finish before resetting state
127
+ window.setTimeout(() => {
128
+ setEmail("");
129
+ setResult(null);
130
+ setError(null);
131
+ setCopied(false);
132
+ }, 200);
133
+ };
134
+ const copyLink = async () => {
135
+ if (!result)
136
+ return;
137
+ await navigator.clipboard.writeText(result.acceptUrl);
138
+ setCopied(true);
139
+ window.setTimeout(() => setCopied(false), 1500);
140
+ };
141
+ return (_jsxs(_Fragment, { children: [_jsxs(Button, { size: "sm", onClick: () => setOpen(true), children: [_jsx(UserPlus, { className: "mr-1 h-4 w-4" }), messages.team.inviteMember] }), _jsx(Dialog, { open: open, onOpenChange: (o) => (o ? setOpen(true) : close()), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: messages.team.inviteDialogTitle }), _jsx(DialogDescription, { children: messages.team.inviteDialogDescription })] }), result ? (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "rounded-md border bg-muted/30 p-3 text-sm", children: _jsxs("p", { children: [formatMessage(messages.team.inviteCreated, {
142
+ email: result.email,
143
+ }), " ", result.emailSent
144
+ ? messages.team.inviteEmailSentSuffix
145
+ : messages.team.inviteManualShareSuffix] }) }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: messages.team.acceptLink }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { value: result.acceptUrl, readOnly: true, className: "font-mono text-xs" }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => void copyLink(), children: [_jsx(Copy, { className: "mr-1 h-3.5 w-3.5" }), copied ? messages.team.copied : messages.team.copy] })] })] }), _jsx(DialogFooter, { children: _jsx(Button, { onClick: close, children: messages.team.done }) })] })) : (_jsxs("form", { className: "space-y-4", onSubmit: (e) => {
146
+ e.preventDefault();
147
+ create.mutate();
148
+ }, children: [error && (_jsx("div", { className: "rounded-md bg-destructive/10 p-3 text-sm text-destructive", children: error })), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "invite-email", children: messages.team.emailLabel }), _jsx(Input, { id: "invite-email", type: "email", value: email, onChange: (e) => setEmail(e.target.value), placeholder: messages.team.emailPlaceholder, required: true, autoComplete: "off", autoFocus: true })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: close, children: messages.team.cancel }), _jsxs(Button, { type: "submit", disabled: create.isPending, children: [create.isPending && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), messages.team.sendInvitation] })] })] }))] }) })] }));
149
+ }
@@ -0,0 +1,67 @@
1
+ import type { ReactNode } from "react";
2
+ import type { OperatorAdminMessages } from "../providers/operator-admin-messages.js";
3
+ export type DashboardEmptyStateKey = "revenueTrend" | "bookingStatus" | "monthlyBookings" | "upcomingDepartures" | "outstandingInvoices" | "onboarding";
4
+ export interface DashboardEmptyAction {
5
+ href: string;
6
+ label: string;
7
+ }
8
+ export interface DashboardEmptyStateConfig {
9
+ action?: DashboardEmptyAction | null;
10
+ description?: string | null;
11
+ icon?: ReactNode;
12
+ title?: string | null;
13
+ }
14
+ export declare function buildDashboardEmptyStates(messages: OperatorAdminMessages, emptyStates: Partial<Record<DashboardEmptyStateKey, DashboardEmptyStateConfig>>): {
15
+ revenueTrend: {
16
+ action: DashboardEmptyAction | null;
17
+ description: string | null;
18
+ icon: string | number | bigint | boolean | import("react/jsx-runtime").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null;
19
+ title: string | null;
20
+ };
21
+ bookingStatus: {
22
+ action: DashboardEmptyAction | null;
23
+ description: string | null;
24
+ icon: string | number | bigint | boolean | import("react/jsx-runtime").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null;
25
+ title: string | null;
26
+ };
27
+ monthlyBookings: {
28
+ action: DashboardEmptyAction | null;
29
+ description: string | null;
30
+ icon: string | number | bigint | boolean | import("react/jsx-runtime").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null;
31
+ title: string | null;
32
+ };
33
+ upcomingDepartures: {
34
+ action: DashboardEmptyAction | null;
35
+ description: string | null;
36
+ icon: string | number | bigint | boolean | import("react/jsx-runtime").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null;
37
+ title: string | null;
38
+ };
39
+ outstandingInvoices: {
40
+ action: DashboardEmptyAction | null;
41
+ description: string | null;
42
+ icon: string | number | bigint | boolean | import("react/jsx-runtime").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null;
43
+ title: string | null;
44
+ };
45
+ onboarding: {
46
+ action?: DashboardEmptyAction | null;
47
+ description: string | null;
48
+ icon: string | number | bigint | boolean | import("react/jsx-runtime").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null;
49
+ title: string | null;
50
+ };
51
+ };
52
+ export declare function DashboardEmptyState({ compact, emptyState, }: {
53
+ compact?: boolean;
54
+ emptyState: DashboardEmptyStateConfig;
55
+ }): import("react/jsx-runtime").JSX.Element;
56
+ export declare function DashboardOnboardingEmptyState({ children, emptyState, }: {
57
+ children: ReactNode;
58
+ emptyState: DashboardEmptyStateConfig;
59
+ }): import("react/jsx-runtime").JSX.Element;
60
+ export declare function OnboardingAction({ actionLabel, description, href, icon, title, }: {
61
+ actionLabel: string;
62
+ description: string;
63
+ href: string;
64
+ icon: ReactNode;
65
+ title: string;
66
+ }): import("react/jsx-runtime").JSX.Element;
67
+ //# sourceMappingURL=dashboard-empty-states.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-empty-states.d.ts","sourceRoot":"","sources":["../../src/dashboard/dashboard-empty-states.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAA;AAEpF,MAAM,MAAM,sBAAsB,GAC9B,cAAc,GACd,eAAe,GACf,iBAAiB,GACjB,oBAAoB,GACpB,qBAAqB,GACrB,YAAY,CAAA;AAEhB,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAA;IACpC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,qBAAqB,EAC/B,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,sBAAsB,EAAE,yBAAyB,CAAC,CAAC;;gBARtE,oBAAoB,GAAG,IAAI;qBACtB,MAAM,GAAG,IAAI;;eAEnB,MAAM,GAAG,IAAI;;;gBAHZ,oBAAoB,GAAG,IAAI;qBACtB,MAAM,GAAG,IAAI;;eAEnB,MAAM,GAAG,IAAI;;;gBAHZ,oBAAoB,GAAG,IAAI;qBACtB,MAAM,GAAG,IAAI;;eAEnB,MAAM,GAAG,IAAI;;;gBAHZ,oBAAoB,GAAG,IAAI;qBACtB,MAAM,GAAG,IAAI;;eAEnB,MAAM,GAAG,IAAI;;;gBAHZ,oBAAoB,GAAG,IAAI;qBACtB,MAAM,GAAG,IAAI;;eAEnB,MAAM,GAAG,IAAI;;;iBAHZ,oBAAoB,GAAG,IAAI;qBACtB,MAAM,GAAG,IAAI;;eAEnB,MAAM,GAAG,IAAI;;EAqDtB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,OAAe,EACf,UAAU,GACX,EAAE;IACD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,yBAAyB,CAAA;CACtC,2CAqBA;AAED,wBAAgB,6BAA6B,CAAC,EAC5C,QAAQ,EACR,UAAU,GACX,EAAE;IACD,QAAQ,EAAE,SAAS,CAAA;IACnB,UAAU,EAAE,yBAAyB,CAAA;CACtC,2CAiBA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,WAAW,EACX,WAAW,EACX,IAAI,EACJ,IAAI,EACJ,KAAK,GACN,EAAE;IACD,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,SAAS,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd,2CAkBA"}
@@ -0,0 +1,65 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Card, CardContent } from "@voyant-travel/ui/components";
4
+ import { buttonVariants } from "@voyant-travel/ui/components/button";
5
+ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "@voyant-travel/ui/components/empty";
6
+ import { cn } from "@voyant-travel/ui/lib/utils";
7
+ import { BarChart3, CalendarCheck, ClipboardList, DollarSign, FileText, Sparkles, } from "lucide-react";
8
+ export function buildDashboardEmptyStates(messages, emptyStates) {
9
+ return {
10
+ revenueTrend: {
11
+ title: messages.dashboard.revenueTrendEmptyTitle,
12
+ description: messages.dashboard.revenueTrendEmptyDescription,
13
+ action: { href: "/bookings", label: messages.dashboard.revenueTrendEmptyAction },
14
+ icon: _jsx(DollarSign, { className: "size-5" }),
15
+ ...emptyStates.revenueTrend,
16
+ },
17
+ bookingStatus: {
18
+ title: messages.dashboard.bookingStatusEmptyTitle,
19
+ description: messages.dashboard.bookingStatusEmptyDescription,
20
+ action: { href: "/bookings", label: messages.dashboard.bookingStatusEmptyAction },
21
+ icon: _jsx(ClipboardList, { className: "size-5" }),
22
+ ...emptyStates.bookingStatus,
23
+ },
24
+ monthlyBookings: {
25
+ title: messages.dashboard.monthlyBookingsEmptyTitle,
26
+ description: messages.dashboard.monthlyBookingsEmptyDescription,
27
+ action: { href: "/bookings", label: messages.dashboard.monthlyBookingsEmptyAction },
28
+ icon: _jsx(BarChart3, { className: "size-5" }),
29
+ ...emptyStates.monthlyBookings,
30
+ },
31
+ upcomingDepartures: {
32
+ title: messages.dashboard.noUpcomingDepartures,
33
+ description: messages.dashboard.noUpcomingDeparturesDescription,
34
+ action: null,
35
+ icon: _jsx(CalendarCheck, { className: "size-5" }),
36
+ ...emptyStates.upcomingDepartures,
37
+ },
38
+ outstandingInvoices: {
39
+ title: messages.dashboard.outstandingInvoicesEmptyTitle,
40
+ description: messages.dashboard.outstandingInvoicesEmptyDescription,
41
+ action: {
42
+ href: "/finance",
43
+ label: messages.dashboard.outstandingInvoicesEmptyAction,
44
+ },
45
+ icon: _jsx(FileText, { className: "size-5" }),
46
+ ...emptyStates.outstandingInvoices,
47
+ },
48
+ onboarding: {
49
+ title: messages.dashboard.onboardingTitle,
50
+ description: messages.dashboard.onboardingDescription,
51
+ icon: _jsx(Sparkles, { className: "size-5" }),
52
+ ...emptyStates.onboarding,
53
+ },
54
+ };
55
+ }
56
+ export function DashboardEmptyState({ compact = false, emptyState, }) {
57
+ const action = emptyState.action ?? null;
58
+ return (_jsxs(Empty, { className: cn("min-h-[250px] border", compact && "min-h-[180px] p-8"), children: [_jsxs(EmptyHeader, { children: [emptyState.icon ? _jsx(EmptyMedia, { variant: "icon", children: emptyState.icon }) : null, emptyState.title ? _jsx(EmptyTitle, { children: emptyState.title }) : null, emptyState.description ? (_jsx(EmptyDescription, { children: emptyState.description })) : null] }), action ? (_jsx(EmptyContent, { children: _jsx("a", { href: action.href, className: cn(buttonVariants({ size: "sm" })), children: action.label }) })) : null] }));
59
+ }
60
+ export function DashboardOnboardingEmptyState({ children, emptyState, }) {
61
+ return (_jsx(Card, { children: _jsx(CardContent, { className: "p-6", children: _jsxs(Empty, { className: "border p-6", children: [_jsxs(EmptyHeader, { children: [emptyState.icon ? _jsx(EmptyMedia, { variant: "icon", children: emptyState.icon }) : null, emptyState.title ? _jsx(EmptyTitle, { children: emptyState.title }) : null, emptyState.description ? (_jsx(EmptyDescription, { children: emptyState.description })) : null] }), _jsx("div", { className: "grid w-full gap-3 md:grid-cols-2", children: children })] }) }) }));
62
+ }
63
+ export function OnboardingAction({ actionLabel, description, href, icon, title, }) {
64
+ return (_jsxs("a", { href: href, className: "flex min-w-0 items-start justify-between gap-4 rounded-lg border bg-background p-4 text-left transition-colors hover:bg-muted/50", children: [_jsxs("span", { className: "flex min-w-0 gap-3", children: [_jsx("span", { className: "mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: icon }), _jsxs("span", { className: "min-w-0 space-y-1", children: [_jsx("span", { className: "block text-sm font-medium", children: title }), _jsx("span", { className: "block text-sm text-muted-foreground", children: description })] })] }), _jsx("span", { className: "shrink-0 text-sm font-medium text-primary", children: actionLabel })] }));
65
+ }
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from "react";
2
+ export declare function KpiCard({ title, value, description, icon, trend, trendLabel, empty, emptyLabel, isLoading, }: {
3
+ title: string;
4
+ value: string;
5
+ description: string;
6
+ icon: ReactNode;
7
+ trend?: number;
8
+ trendLabel?: string;
9
+ empty?: boolean;
10
+ emptyLabel?: string;
11
+ isLoading?: boolean;
12
+ }): import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=dashboard-kpi-card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-kpi-card.d.ts","sourceRoot":"","sources":["../../src/dashboard/dashboard-kpi-card.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,KAAK,EACL,WAAW,EACX,IAAI,EACJ,KAAK,EACL,UAAU,EACV,KAAK,EACL,UAAU,EACV,SAAS,GACV,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,SAAS,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,2CA0DA"}
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Card, CardContent, CardHeader, CardTitle } from "@voyant-travel/ui/components";
4
+ import { Skeleton } from "@voyant-travel/ui/components/skeleton";
5
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@voyant-travel/ui/components/tooltip";
6
+ import { ArrowDownRight, ArrowUpRight } from "lucide-react";
7
+ export function KpiCard({ title, value, description, icon, trend, trendLabel, empty, emptyLabel, isLoading, }) {
8
+ const isPositive = (trend ?? 0) >= 0;
9
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0 pb-2", children: [_jsx(CardTitle, { className: "text-sm font-medium", children: title }), icon] }), _jsx(CardContent, { children: isLoading ? (_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-7 w-28" }), _jsx(Skeleton, { className: "h-3 w-40" }), trendLabel ? _jsx(Skeleton, { className: "mt-3 h-5 w-28 rounded-full" }) : null] })) : (_jsxs(_Fragment, { children: [empty ? (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { className: "text-2xl font-semibold tracking-tight text-muted-foreground", "aria-label": emptyLabel ?? description, children: "-" }), _jsx(TooltipContent, { children: emptyLabel ?? description })] }) })) : (_jsx("div", { className: "text-2xl font-semibold tracking-tight", children: value })), _jsx("p", { className: "text-xs text-muted-foreground", children: description }), !empty && trend != null && trendLabel ? (_jsxs("div", { className: "mt-3 flex items-center gap-1 text-xs text-muted-foreground", children: [_jsxs("span", { className: `inline-flex items-center gap-1 rounded-full px-2 py-0.5 ${isPositive
10
+ ? "bg-emerald-500/10 text-emerald-600"
11
+ : "bg-rose-500/10 text-rose-600"}`, children: [isPositive ? (_jsx(ArrowUpRight, { className: "h-3 w-3" })) : (_jsx(ArrowDownRight, { className: "h-3 w-3" })), Math.abs(trend).toFixed(1), "%"] }), _jsx("span", { children: trendLabel })] })) : null] })) })] }));
12
+ }
@@ -0,0 +1,7 @@
1
+ import { type DashboardEmptyStateConfig, type DashboardEmptyStateKey } from "./dashboard-empty-states.js";
2
+ export type { DashboardEmptyAction, DashboardEmptyStateConfig, DashboardEmptyStateKey, } from "./dashboard-empty-states.js";
3
+ export interface DashboardPageProps {
4
+ emptyStates?: Partial<Record<DashboardEmptyStateKey, DashboardEmptyStateConfig>>;
5
+ }
6
+ export declare function DashboardPage({ emptyStates }?: DashboardPageProps): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=dashboard-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-page.d.ts","sourceRoot":"","sources":["../../src/dashboard/dashboard-page.tsx"],"names":[],"mappings":"AAsCA,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAG5B,MAAM,6BAA6B,CAAA;AAoBpC,YAAY,EACV,oBAAoB,EACpB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,6BAA6B,CAAA;AAEpC,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,sBAAsB,EAAE,yBAAyB,CAAC,CAAC,CAAA;CACjF;AAED,wBAAgB,aAAa,CAAC,EAAE,WAAgB,EAAE,GAAE,kBAAuB,2CAoe1E"}
@@ -0,0 +1,150 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { Link } from "@tanstack/react-router";
5
+ import { useVoyantReactContext } from "@voyant-travel/react";
6
+ import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@voyant-travel/ui/components";
7
+ import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, } from "@voyant-travel/ui/components/chart";
8
+ import { CalendarCheck, CalendarPlus, DollarSign, Package, PackagePlus, Users } from "lucide-react";
9
+ import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, XAxis, YAxis, } from "recharts";
10
+ import { AdminWidgetSlotRenderer } from "../components/admin-widget-slot.js";
11
+ import { formatMessage } from "../lib/i18n.js";
12
+ import { useLocale } from "../providers/locale.js";
13
+ import { useOperatorAdminMessages } from "../providers/operator-admin-messages.js";
14
+ import { buildDashboardEmptyStates, DashboardEmptyState, DashboardOnboardingEmptyState, OnboardingAction, } from "./dashboard-empty-states.js";
15
+ import { KpiCard } from "./dashboard-kpi-card.js";
16
+ import { buildMonthSeries, formatCurrency, getDashboardBookingsAggregatesQueryOptions, getDashboardFinanceAggregatesQueryOptions, getDashboardProductsAggregatesQueryOptions, getDashboardSuppliersAggregatesQueryOptions, getStatusColor, pickPrimaryCurrency, } from "./dashboard-query-options.js";
17
+ import { DashboardAreaChartSkeleton, DashboardBarChartSkeleton, DashboardOutstandingInvoicesSkeleton, DashboardPieChartSkeleton, DashboardUpcomingListSkeleton, } from "./dashboard-skeleton.js";
18
+ export function DashboardPage({ emptyStates = {} } = {}) {
19
+ const client = useVoyantReactContext();
20
+ const messages = useOperatorAdminMessages();
21
+ const { resolvedLocale } = useLocale();
22
+ const { data: bookingsAggregates, isPending: bookingsPending } = useQuery(getDashboardBookingsAggregatesQueryOptions(client));
23
+ const { data: productsAggregates, isPending: productsPending } = useQuery(getDashboardProductsAggregatesQueryOptions(client));
24
+ const { data: suppliersAggregates, isPending: suppliersPending } = useQuery(getDashboardSuppliersAggregatesQueryOptions(client));
25
+ const { data: financeAggregates, isPending: financePending } = useQuery(getDashboardFinanceAggregatesQueryOptions(client));
26
+ const bookings = bookingsAggregates?.data;
27
+ const products = productsAggregates?.data;
28
+ const suppliers = suppliersAggregates?.data;
29
+ const finance = financeAggregates?.data;
30
+ const monthSeries = buildMonthSeries();
31
+ const defaultCurrency = pickPrimaryCurrency(bookings?.monthlyRevenue ?? []);
32
+ const monthlyRevenue = monthSeries.map((entry) => {
33
+ const revenue = bookings?.monthlyRevenue
34
+ .filter((row) => row.yearMonth === entry.yearMonth)
35
+ .reduce((sum, row) => sum + row.sellAmountCents, 0) ?? 0;
36
+ const bookingsInMonth = bookings?.monthlyCounts.find((row) => row.yearMonth === entry.yearMonth)?.count ?? 0;
37
+ return { month: entry.month, revenue: revenue / 100, bookings: bookingsInMonth };
38
+ });
39
+ const monthlyBookings = monthSeries.map((entry) => ({
40
+ month: entry.month,
41
+ count: bookings?.monthlyCounts.find((row) => row.yearMonth === entry.yearMonth)?.count ?? 0,
42
+ }));
43
+ const totalRevenueCents = bookings?.monthlyRevenue
44
+ .filter((row) => row.currency === defaultCurrency)
45
+ .reduce((sum, row) => sum + row.sellAmountCents, 0) ?? 0;
46
+ const confirmedBookings = (bookings?.countsByStatus.find((row) => row.status === "confirmed")?.count ?? 0) +
47
+ (bookings?.countsByStatus.find((row) => row.status === "in_progress")?.count ?? 0);
48
+ const totalPax = bookings?.totalPax ?? 0;
49
+ const activeProducts = products?.active ?? 0;
50
+ const totalProducts = products?.total ?? 0;
51
+ const totalSuppliers = suppliers?.total ?? 0;
52
+ const outstandingInvoiceCount = finance?.outstanding.reduce((sum, row) => sum + row.count, 0) ?? 0;
53
+ const outstandingPrimaryCurrency = finance?.outstanding[0]?.currency ?? defaultCurrency;
54
+ const outstandingAmount = finance?.outstanding.find((row) => row.currency === outstandingPrimaryCurrency)
55
+ ?.balanceDueCents ?? 0;
56
+ const outstandingTopN = finance?.outstandingTopN ?? [];
57
+ const currentMonthRevenue = monthlyRevenue[monthlyRevenue.length - 1]?.revenue ?? 0;
58
+ const prevMonthRevenue = monthlyRevenue[monthlyRevenue.length - 2]?.revenue ?? 0;
59
+ const revenueTrend = prevMonthRevenue > 0 ? ((currentMonthRevenue - prevMonthRevenue) / prevMonthRevenue) * 100 : 0;
60
+ const currentMonthBookings = monthlyBookings[monthlyBookings.length - 1]?.count ?? 0;
61
+ const prevMonthBookings = monthlyBookings[monthlyBookings.length - 2]?.count ?? 0;
62
+ const bookingTrend = prevMonthBookings > 0
63
+ ? ((currentMonthBookings - prevMonthBookings) / prevMonthBookings) * 100
64
+ : 0;
65
+ const revenueChartConfig = {
66
+ revenue: { label: messages.dashboard.chartRevenueLabel, color: "hsl(221 83% 53%)" },
67
+ bookings: { label: messages.dashboard.chartBookingsLabel, color: "hsl(142 71% 45%)" },
68
+ };
69
+ const bookingStatusConfig = {
70
+ confirmed: { label: messages.dashboard.statusConfirmedLabel, color: "hsl(142 71% 45%)" },
71
+ completed: { label: messages.dashboard.statusCompletedLabel, color: "hsl(221 83% 53%)" },
72
+ in_progress: { label: messages.dashboard.statusInProgressLabel, color: "hsl(47 96% 53%)" },
73
+ draft: { label: messages.dashboard.statusDraftLabel, color: "hsl(215 14% 55%)" },
74
+ cancelled: { label: messages.dashboard.statusCancelledLabel, color: "hsl(0 84% 60%)" },
75
+ };
76
+ const monthlyBookingsConfig = {
77
+ count: { label: messages.dashboard.chartBookingsLabel, color: "hsl(221 83% 53%)" },
78
+ };
79
+ const localizedStatusBreakdown = (bookings?.countsByStatus ?? [])
80
+ .filter((entry) => entry.count > 0)
81
+ .map((entry) => ({
82
+ // Keep the raw status key so the chart config (keyed by
83
+ // `confirmed`/`completed`/...) can resolve the localized label
84
+ // for both the legend and the tooltip.
85
+ status: entry.status,
86
+ count: entry.count,
87
+ fill: getStatusColor(entry.status),
88
+ }));
89
+ const upcoming = bookings?.upcomingDepartures.items ?? [];
90
+ const hasRevenueData = monthlyRevenue.some((entry) => entry.revenue > 0);
91
+ const hasMonthlyBookingsData = monthlyBookings.some((entry) => entry.count > 0);
92
+ const hasBookingStatusData = localizedStatusBreakdown.length > 0;
93
+ const hasOutstandingInvoices = outstandingInvoiceCount > 0 || outstandingTopN.length > 0;
94
+ const allAggregatesLoaded = !bookingsPending && !productsPending && !suppliersPending && !financePending;
95
+ const isBrandNewTenant = allAggregatesLoaded &&
96
+ (bookings?.total ?? 0) === 0 &&
97
+ (products?.total ?? 0) === 0 &&
98
+ (suppliers?.total ?? 0) === 0 &&
99
+ (finance?.total ?? 0) === 0;
100
+ const resolvedEmptyStates = buildDashboardEmptyStates(messages, emptyStates);
101
+ const dashboardMetrics = {
102
+ totalRevenueCents,
103
+ confirmedBookings,
104
+ totalPax,
105
+ activeProducts,
106
+ outstandingAmount,
107
+ outstandingInvoiceCount,
108
+ defaultCurrency,
109
+ };
110
+ const widgetProps = {
111
+ bookingsAggregates: bookings ?? null,
112
+ productsAggregates: products ?? null,
113
+ suppliersAggregates: suppliers ?? null,
114
+ financeAggregates: finance ?? null,
115
+ metrics: dashboardMetrics,
116
+ emptyStates: resolvedEmptyStates,
117
+ };
118
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsx(AdminWidgetSlotRenderer, { slot: "dashboard.header", props: widgetProps }), isBrandNewTenant ? (_jsxs(_Fragment, { children: [_jsxs(DashboardOnboardingEmptyState, { emptyState: resolvedEmptyStates.onboarding, children: [_jsx(OnboardingAction, { href: "/products", icon: _jsx(PackagePlus, { className: "size-4" }), title: messages.dashboard.onboardingProductsTitle, description: messages.dashboard.onboardingProductsDescription, actionLabel: messages.dashboard.onboardingProductsAction }), _jsx(OnboardingAction, { href: "/suppliers", icon: _jsx(Users, { className: "size-4" }), title: messages.dashboard.onboardingSuppliersTitle, description: messages.dashboard.onboardingSuppliersDescription, actionLabel: messages.dashboard.onboardingSuppliersAction }), _jsx(OnboardingAction, { href: "/contacts", icon: _jsx(Users, { className: "size-4" }), title: messages.dashboard.onboardingCustomersTitle, description: messages.dashboard.onboardingCustomersDescription, actionLabel: messages.dashboard.onboardingCustomersAction }), _jsx(OnboardingAction, { href: "/bookings", icon: _jsx(CalendarPlus, { className: "size-4" }), title: messages.dashboard.onboardingBookingsTitle, description: messages.dashboard.onboardingBookingsDescription, actionLabel: messages.dashboard.onboardingBookingsAction })] }), _jsx(AdminWidgetSlotRenderer, { slot: "dashboard.footer", props: widgetProps })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4", children: [_jsx(KpiCard, { title: messages.dashboard.totalRevenueTitle, value: formatCurrency(totalRevenueCents, defaultCurrency), description: messages.dashboard.totalRevenueDescription, icon: _jsx(DollarSign, { className: "h-4 w-4 text-muted-foreground" }), trend: revenueTrend, trendLabel: messages.dashboard.trendVsLastMonth, empty: !hasRevenueData, emptyLabel: messages.dashboard.metricUnavailable, isLoading: bookingsPending }), _jsx(KpiCard, { title: messages.dashboard.activeBookingsTitle, value: confirmedBookings.toString(), description: formatMessage(messages.dashboard.activeBookingsDescription, {
119
+ count: bookings?.total ?? 0,
120
+ }), icon: _jsx(CalendarCheck, { className: "h-4 w-4 text-muted-foreground" }), trend: bookingTrend, trendLabel: messages.dashboard.trendVsLastMonth, empty: (bookings?.total ?? 0) === 0, emptyLabel: messages.dashboard.metricUnavailable, isLoading: bookingsPending }), _jsx(KpiCard, { title: messages.dashboard.totalTravelersTitle, value: totalPax.toLocaleString(), description: messages.dashboard.totalTravelersDescription, icon: _jsx(Users, { className: "h-4 w-4 text-muted-foreground" }), empty: totalPax === 0 && (bookings?.total ?? 0) === 0, emptyLabel: messages.dashboard.metricUnavailable, isLoading: bookingsPending }), _jsx(KpiCard, { title: messages.dashboard.activeProductsTitle, value: activeProducts.toString(), description: formatMessage(messages.dashboard.activeProductsDescription, {
121
+ products: totalProducts,
122
+ suppliers: totalSuppliers,
123
+ }), icon: _jsx(Package, { className: "h-4 w-4 text-muted-foreground" }), empty: activeProducts === 0 && totalProducts === 0, emptyLabel: messages.dashboard.metricUnavailable, isLoading: productsPending || suppliersPending })] }), _jsx(AdminWidgetSlotRenderer, { slot: "dashboard.after-kpis", props: widgetProps }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: messages.dashboard.revenueTrendTitle }), _jsx(CardDescription, { children: messages.dashboard.revenueTrendDescription })] }), _jsx(CardContent, { children: bookingsPending ? (_jsx(DashboardAreaChartSkeleton, {})) : !hasRevenueData ? (_jsx(DashboardEmptyState, { emptyState: resolvedEmptyStates.revenueTrend })) : (_jsx(ChartContainer, { config: revenueChartConfig, className: "h-[300px] w-full", children: _jsxs(AreaChart, { data: monthlyRevenue, margin: { top: 10, right: 10, left: 0, bottom: 0 }, children: [_jsx("defs", { children: _jsxs("linearGradient", { id: "fillRevenue", x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "5%", stopColor: "hsl(221 83% 53%)", stopOpacity: 0.3 }), _jsx("stop", { offset: "95%", stopColor: "hsl(221 83% 53%)", stopOpacity: 0 })] }) }), _jsx(CartesianGrid, { vertical: false }), _jsx(XAxis, { dataKey: "month", tickLine: false, axisLine: false, tickMargin: 8 }), _jsx(YAxis, { tickLine: false, axisLine: false, tickMargin: 8, tickFormatter: (value) => `$${(value / 1000).toFixed(0)}k` }), _jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { formatter: (value) => typeof value === "number"
124
+ ? formatCurrency(value * 100, defaultCurrency)
125
+ : String(value) }) }), _jsx(Area, { type: "monotone", dataKey: "revenue", stroke: "hsl(221 83% 53%)", fill: "url(#fillRevenue)", strokeWidth: 2 })] }) })) })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: messages.dashboard.monthlyBookingsTitle }), _jsx(CardDescription, { children: messages.dashboard.monthlyBookingsDescription })] }), _jsx(CardContent, { children: bookingsPending ? (_jsx(DashboardBarChartSkeleton, {})) : !hasMonthlyBookingsData ? (_jsx(DashboardEmptyState, { emptyState: resolvedEmptyStates.monthlyBookings, compact: true })) : (_jsx(ChartContainer, { config: monthlyBookingsConfig, className: "h-[250px] w-full", children: _jsxs(BarChart, { data: monthlyBookings, margin: { top: 10, right: 10, left: 0, bottom: 0 }, children: [_jsx(CartesianGrid, { vertical: false }), _jsx(XAxis, { dataKey: "month", tickLine: false, axisLine: false, tickMargin: 8 }), _jsx(YAxis, { tickLine: false, axisLine: false, tickMargin: 8, allowDecimals: false }), _jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, {}) }), _jsx(Bar, { dataKey: "count", fill: "hsl(221 83% 53%)", radius: [4, 4, 0, 0] })] }) })) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: messages.dashboard.bookingStatusTitle }), _jsx(CardDescription, { children: messages.dashboard.bookingStatusDescription })] }), _jsx(CardContent, { children: bookingsPending ? (_jsx(DashboardPieChartSkeleton, {})) : !hasBookingStatusData ? (_jsx(DashboardEmptyState, { emptyState: resolvedEmptyStates.bookingStatus })) : (_jsx(ChartContainer, { config: bookingStatusConfig, className: "mx-auto h-[300px] w-full", children: _jsxs(PieChart, { children: [_jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { nameKey: "status", hideLabel: true }) }), _jsx(Pie, { data: localizedStatusBreakdown, dataKey: "count", nameKey: "status", cx: "50%", cy: "50%", innerRadius: 60, outerRadius: 100, paddingAngle: 2, children: localizedStatusBreakdown.map((entry) => (_jsx(Cell, { fill: entry.fill }, entry.status))) }), _jsx(ChartLegend, { content: _jsx(ChartLegendContent, { nameKey: "status" }) })] }) })) })] })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: messages.dashboard.upcomingDeparturesTitle }), _jsx(CardDescription, { children: messages.dashboard.upcomingDeparturesDescription })] }), _jsx(Link, { to: "/bookings", className: "text-sm text-primary hover:underline", children: messages.dashboard.viewAll })] }), _jsx(CardContent, { children: bookingsPending ? (_jsx(DashboardUpcomingListSkeleton, {})) : upcoming.length === 0 ? (_jsx(DashboardEmptyState, { emptyState: resolvedEmptyStates.upcomingDepartures, compact: true })) : (_jsx("div", { className: "space-y-3", children: upcoming.map((booking) => (_jsxs(Link, { to: "/bookings/$id", params: { id: booking.id }, className: "flex items-center justify-between rounded-lg border p-3 transition-colors hover:bg-muted/50", children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-sm font-medium", children: booking.bookingNumber ?? booking.id.slice(0, 8) }), _jsxs("span", { className: "text-xs text-muted-foreground", children: [booking.startDate
126
+ ? new Date(booking.startDate).toLocaleDateString(resolvedLocale, {
127
+ month: "short",
128
+ day: "numeric",
129
+ year: "numeric",
130
+ })
131
+ : messages.dashboard.noDate, booking.pax
132
+ ? ` · ${formatMessage(messages.dashboard.paxCount, {
133
+ count: booking.pax,
134
+ })}`
135
+ : ""] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [booking.sellAmountCents != null && (_jsx("span", { className: "text-sm font-medium tabular-nums", children: formatCurrency(booking.sellAmountCents, booking.sellCurrency ?? defaultCurrency) })), _jsx(Badge, { variant: "outline", className: "capitalize", children: booking.status.replace(/_/g, " ") })] })] }, booking.id))) })) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: messages.dashboard.outstandingInvoicesTitle }), _jsx(CardDescription, { children: messages.dashboard.outstandingInvoicesDescription })] }), _jsx(CardContent, { children: financePending ? (_jsx(DashboardOutstandingInvoicesSkeleton, {})) : !hasOutstandingInvoices ? (_jsx(DashboardEmptyState, { emptyState: resolvedEmptyStates.outstandingInvoices })) : (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between rounded-lg border border-dashed p-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: messages.dashboard.outstandingTotalTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: formatMessage(messages.dashboard.outstandingInvoicesDue, {
136
+ count: outstandingInvoiceCount,
137
+ }) })] }), _jsx("p", { className: "text-lg font-semibold", children: formatCurrency(outstandingAmount, outstandingPrimaryCurrency) })] }), outstandingTopN.map((invoice) => (_jsxs("div", { className: "flex items-center justify-between rounded-lg border p-3", children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-sm font-medium", children: invoice.invoiceNumber ?? invoice.id.slice(0, 8) }), _jsx("span", { className: "text-xs text-muted-foreground", children: invoice.dueDate
138
+ ? new Date(invoice.dueDate).toLocaleDateString(resolvedLocale, {
139
+ month: "short",
140
+ day: "numeric",
141
+ year: "numeric",
142
+ })
143
+ : invoice.issueDate
144
+ ? new Date(invoice.issueDate).toLocaleDateString(resolvedLocale, {
145
+ month: "short",
146
+ day: "numeric",
147
+ year: "numeric",
148
+ })
149
+ : messages.dashboard.noIssueDate })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: formatCurrency(invoice.balanceDueCents, invoice.currency) }), _jsx(Badge, { variant: "secondary", className: "capitalize", children: invoice.status })] })] }, invoice.id)))] })) })] })] }), _jsx(AdminWidgetSlotRenderer, { slot: "dashboard.footer", props: widgetProps })] }))] }));
150
+ }