@burdenoff/microfe-ggnomad 2026.705.4 → 2026.705.5
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/dist/ggnomad/pages/AnalyticsPage.js +39 -1
- package/dist/ggnomad/pages/AnalyticsPage.js.map +1 -1
- package/dist/ggnomad/pages/BookingsPage.js +135 -80
- package/dist/ggnomad/pages/BookingsPage.js.map +1 -1
- package/dist/ggnomad/pages/DashboardPage.js +136 -78
- package/dist/ggnomad/pages/DashboardPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -24,7 +24,16 @@ function v() {
|
|
|
24
24
|
label: _(e),
|
|
25
25
|
value: Number(t)
|
|
26
26
|
})).filter((e) => Number.isFinite(e.value) && e.value > 0).sort((e, t) => t.value - e.value);
|
|
27
|
-
}, [b?.revenueByCategory]), S = x.length > 0 ? x[0].value : 0
|
|
27
|
+
}, [b?.revenueByCategory]), S = x.length > 0 ? x[0].value : 0, C = i(() => {
|
|
28
|
+
let e = b?.monthlyTrends;
|
|
29
|
+
return e ? Array.isArray(e) ? e.map((e) => ({
|
|
30
|
+
label: e.month || "",
|
|
31
|
+
value: Number(e.revenue || 0)
|
|
32
|
+
})).filter((e) => Number.isFinite(e.value) && e.label) : Object.entries(e).map(([e, t]) => ({
|
|
33
|
+
label: e,
|
|
34
|
+
value: Number(t)
|
|
35
|
+
})).filter((e) => Number.isFinite(e.value)) : [];
|
|
36
|
+
}, [b?.monthlyTrends]), w = C.length > 0 ? Math.max(...C.map((e) => e.value)) : 0;
|
|
28
37
|
return /* @__PURE__ */ h(t, {
|
|
29
38
|
title: v("ggnomad.analytics.title", "Analytics"),
|
|
30
39
|
titleAccent: v("ggnomad.analytics.title.accent", "Analytics"),
|
|
@@ -110,6 +119,35 @@ function v() {
|
|
|
110
119
|
})] }, e.label);
|
|
111
120
|
})
|
|
112
121
|
})
|
|
122
|
+
}),
|
|
123
|
+
C.length > 0 && /* @__PURE__ */ m(r, {
|
|
124
|
+
title: v("ggnomad.analytics.revenueTrend", "Revenue trend"),
|
|
125
|
+
children: /* @__PURE__ */ h("div", {
|
|
126
|
+
className: "flex flex-col gap-4",
|
|
127
|
+
children: [/* @__PURE__ */ m("div", {
|
|
128
|
+
className: "flex items-end gap-2 overflow-x-auto pb-2",
|
|
129
|
+
children: C.map((e, t) => /* @__PURE__ */ h("div", {
|
|
130
|
+
className: "flex flex-col items-center gap-1",
|
|
131
|
+
children: [/* @__PURE__ */ m("div", {
|
|
132
|
+
className: "flex h-24 items-end",
|
|
133
|
+
children: /* @__PURE__ */ m("div", {
|
|
134
|
+
className: "w-8 rounded-t transition-all duration-500 ease-out",
|
|
135
|
+
style: {
|
|
136
|
+
height: `${w > 0 ? Math.max(e.value / w * 100, 5) : 5}%`,
|
|
137
|
+
backgroundColor: g[t % g.length]
|
|
138
|
+
},
|
|
139
|
+
title: `${e.label}: $${e.value.toLocaleString()}`
|
|
140
|
+
})
|
|
141
|
+
}), /* @__PURE__ */ m("p", {
|
|
142
|
+
className: "truncate text-xs text-text-secondary",
|
|
143
|
+
children: e.label
|
|
144
|
+
})]
|
|
145
|
+
}, e.label))
|
|
146
|
+
}), /* @__PURE__ */ m("p", {
|
|
147
|
+
className: "text-xs text-text-secondary",
|
|
148
|
+
children: v("ggnomad.analytics.trendNote", "Revenue trends over time showing performance across all domains.")
|
|
149
|
+
})]
|
|
150
|
+
})
|
|
113
151
|
})
|
|
114
152
|
]
|
|
115
153
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/ggnomad/pages/AnalyticsPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { BarChart3, CalendarCheck, DollarSign, Percent, Users } from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_OVERVIEW_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst humanizeCategory = (key: string): string =>\n key\n .replace(/[_-]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (char) => char.toUpperCase()) || key;\n\nexport function AnalyticsPage() {\n const { t } = useI18n();\n const { data } = useQuery(\n GGNOMAD_OVERVIEW_QUERY as TypedDocumentNode<\n {\n ggnomadAnalytics: {\n totalRevenue: number;\n totalBookings: number;\n totalUsers: number;\n conversionRate: number;\n revenueByCategory: Record<string, unknown>;\n };\n },\n Record<string, never>\n >\n );\n const analytics = data?.ggnomadAnalytics;\n\n const categories = useMemo(() => {\n const raw = analytics?.revenueByCategory ?? {};\n return Object.entries(raw)\n .map(([key, value]) => ({ label: humanizeCategory(key), value: Number(value) }))\n .filter((entry) => Number.isFinite(entry.value) && entry.value > 0)\n .sort((a, b) => b.value - a.value);\n }, [analytics?.revenueByCategory]);\n\n const maxCategory = categories.length > 0 ? categories[0].value : 0;\n\n return (\n <PageShell\n title={t('ggnomad.analytics.title', 'Analytics')}\n titleAccent={t('ggnomad.analytics.title.accent', 'Analytics')}\n subtitle={t('ggnomad.analytics.subtitle', 'GGNomad performance and growth analytics')}\n >\n <PagePurpose icon={<BarChart3 className=\"size-4\" />}>\n {t(\n 'ggnomad.analytics.purpose',\n 'Understand how the marketplace is performing — total revenue, bookings, active users and conversion rate at a glance, with a category breakdown to see which domains (stays, tours, events) are driving growth.'\n )}\n </PagePurpose>\n\n <EmphasisPanel>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.analytics.totalRevenue', 'Total revenue')}\n </p>\n <p className=\"mt-1 text-3xl font-semibold tabular-nums text-text-primary\">\n ${Number(analytics?.totalRevenue ?? 0).toLocaleString()}\n </p>\n </EmphasisPanel>\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.analytics.conversion', 'Conversion')}\n value={`${Number(analytics?.conversionRate ?? 0).toFixed(2)}%`}\n icon={<Percent />}\n />\n <StatCard\n label={t('ggnomad.analytics.revenue', 'Revenue')}\n value={`$${Number(analytics?.totalRevenue ?? 0).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.analytics.bookings', 'Bookings')}\n value={Number(analytics?.totalBookings ?? 0).toLocaleString()}\n icon={<CalendarCheck />}\n />\n <StatCard\n label={t('ggnomad.analytics.users', 'Users')}\n value={Number(analytics?.totalUsers ?? 0).toLocaleString()}\n icon={<Users />}\n />\n </section>\n\n <SectionCard title={t('ggnomad.analytics.revenueByCategory', 'Revenue by category')}>\n {categories.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <BarChart3 className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.analytics.empty.title', 'No analytics yet')}\n </p>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {t(\n 'ggnomad.analytics.empty.description',\n 'Revenue by category appears here as bookings come in across stays, tours and events.'\n )}\n </p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {categories.map((category, index) => {\n const pct = maxCategory > 0 ? Math.max((category.value / maxCategory) * 100, 2) : 0;\n return (\n <li key={category.label}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {category.label}\n </span>\n <span className=\"flex-shrink-0 text-sm tabular-nums text-text-secondary\">\n ${category.value.toLocaleString()}\n </span>\n </div>\n <div className=\"mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full transition-[width] duration-500 ease-out\"\n style={{\n width: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </SectionCard>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/ggnomad/pages/AnalyticsPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { BarChart3, CalendarCheck, DollarSign, Percent, Users } from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_OVERVIEW_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst humanizeCategory = (key: string): string =>\n key\n .replace(/[_-]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (char) => char.toUpperCase()) || key;\n\nexport function AnalyticsPage() {\n const { t } = useI18n();\n const { data } = useQuery(\n GGNOMAD_OVERVIEW_QUERY as TypedDocumentNode<\n {\n ggnomadAnalytics: {\n totalRevenue: number;\n totalBookings: number;\n totalUsers: number;\n conversionRate: number;\n revenueByCategory: Record<string, unknown>;\n monthlyTrends?: Array<{ month: string; revenue: number }> | Record<string, number>;\n };\n },\n Record<string, never>\n >\n );\n const analytics = data?.ggnomadAnalytics;\n\n const categories = useMemo(() => {\n const raw = analytics?.revenueByCategory ?? {};\n return Object.entries(raw)\n .map(([key, value]) => ({ label: humanizeCategory(key), value: Number(value) }))\n .filter((entry) => Number.isFinite(entry.value) && entry.value > 0)\n .sort((a, b) => b.value - a.value);\n }, [analytics?.revenueByCategory]);\n\n const maxCategory = categories.length > 0 ? categories[0].value : 0;\n\n const trends = useMemo(() => {\n const raw = analytics?.monthlyTrends;\n if (!raw) return [];\n if (Array.isArray(raw)) {\n return raw\n .map((entry) => ({\n label: entry.month || '',\n value: Number(entry.revenue || 0),\n }))\n .filter((entry) => Number.isFinite(entry.value) && entry.label);\n }\n return Object.entries(raw)\n .map(([key, value]) => ({\n label: key,\n value: Number(value),\n }))\n .filter((entry) => Number.isFinite(entry.value));\n }, [analytics?.monthlyTrends]);\n\n const maxTrend = trends.length > 0 ? Math.max(...trends.map((t) => t.value)) : 0;\n\n return (\n <PageShell\n title={t('ggnomad.analytics.title', 'Analytics')}\n titleAccent={t('ggnomad.analytics.title.accent', 'Analytics')}\n subtitle={t('ggnomad.analytics.subtitle', 'GGNomad performance and growth analytics')}\n >\n <PagePurpose icon={<BarChart3 className=\"size-4\" />}>\n {t(\n 'ggnomad.analytics.purpose',\n 'Understand how the marketplace is performing — total revenue, bookings, active users and conversion rate at a glance, with a category breakdown to see which domains (stays, tours, events) are driving growth.'\n )}\n </PagePurpose>\n\n <EmphasisPanel>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.analytics.totalRevenue', 'Total revenue')}\n </p>\n <p className=\"mt-1 text-3xl font-semibold tabular-nums text-text-primary\">\n ${Number(analytics?.totalRevenue ?? 0).toLocaleString()}\n </p>\n </EmphasisPanel>\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.analytics.conversion', 'Conversion')}\n value={`${Number(analytics?.conversionRate ?? 0).toFixed(2)}%`}\n icon={<Percent />}\n />\n <StatCard\n label={t('ggnomad.analytics.revenue', 'Revenue')}\n value={`$${Number(analytics?.totalRevenue ?? 0).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.analytics.bookings', 'Bookings')}\n value={Number(analytics?.totalBookings ?? 0).toLocaleString()}\n icon={<CalendarCheck />}\n />\n <StatCard\n label={t('ggnomad.analytics.users', 'Users')}\n value={Number(analytics?.totalUsers ?? 0).toLocaleString()}\n icon={<Users />}\n />\n </section>\n\n <SectionCard title={t('ggnomad.analytics.revenueByCategory', 'Revenue by category')}>\n {categories.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <BarChart3 className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.analytics.empty.title', 'No analytics yet')}\n </p>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {t(\n 'ggnomad.analytics.empty.description',\n 'Revenue by category appears here as bookings come in across stays, tours and events.'\n )}\n </p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {categories.map((category, index) => {\n const pct = maxCategory > 0 ? Math.max((category.value / maxCategory) * 100, 2) : 0;\n return (\n <li key={category.label}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {category.label}\n </span>\n <span className=\"flex-shrink-0 text-sm tabular-nums text-text-secondary\">\n ${category.value.toLocaleString()}\n </span>\n </div>\n <div className=\"mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full transition-[width] duration-500 ease-out\"\n style={{\n width: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </SectionCard>\n\n {trends.length > 0 && (\n <SectionCard title={t('ggnomad.analytics.revenueTrend', 'Revenue trend')}>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-end gap-2 overflow-x-auto pb-2\">\n {trends.map((trend, index) => {\n const pct = maxTrend > 0 ? Math.max((trend.value / maxTrend) * 100, 5) : 5;\n return (\n <div key={trend.label} className=\"flex flex-col items-center gap-1\">\n <div className=\"flex h-24 items-end\">\n <div\n className=\"w-8 rounded-t transition-all duration-500 ease-out\"\n style={{\n height: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n title={`${trend.label}: $${trend.value.toLocaleString()}`}\n />\n </div>\n <p className=\"truncate text-xs text-text-secondary\">{trend.label}</p>\n </div>\n );\n })}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {t(\n 'ggnomad.analytics.trendNote',\n 'Revenue trends over time showing performance across all domains.'\n )}\n </p>\n </div>\n </SectionCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -4,20 +4,31 @@ import { StatusBadge as a } from "../components/StatusBadge.js";
|
|
|
4
4
|
import { EntityCard as o } from "../components/EntityCard.js";
|
|
5
5
|
import { notify as s } from "../utils/notify.js";
|
|
6
6
|
import "../components/index.js";
|
|
7
|
-
import {
|
|
8
|
-
import { CalendarCheck as
|
|
9
|
-
import { useI18n as
|
|
10
|
-
import { useNavigate as
|
|
11
|
-
import { useMutation as
|
|
12
|
-
import { AlertDialog as
|
|
13
|
-
import { jsx as
|
|
7
|
+
import { useMemo as c, useState as l } from "react";
|
|
8
|
+
import { CalendarCheck as u, Compass as d, Hotel as f, Plus as p, Ticket as m, XCircle as h } from "lucide-react";
|
|
9
|
+
import { useI18n as g } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
10
|
+
import { useNavigate as _ } from "react-router-dom";
|
|
11
|
+
import { useMutation as v, useQuery as y } from "@apollo/client/react";
|
|
12
|
+
import { AlertDialog as b, AlertDialogAction as x, AlertDialogCancel as S, AlertDialogContent as C, AlertDialogDescription as w, AlertDialogFooter as T, AlertDialogHeader as E, AlertDialogTitle as D, CTAOverflowMenu as O, GlassCard as k, IllustratedEmptyState as A, NextSteps as j, PagePurpose as M } from "@burdenoff/fe-libs/ui";
|
|
13
|
+
import { jsx as N, jsxs as P } from "react/jsx-runtime";
|
|
14
14
|
//#region src/ggnomad/pages/BookingsPage.tsx
|
|
15
|
-
function
|
|
16
|
-
let { t:
|
|
15
|
+
function F() {
|
|
16
|
+
let { t: F } = g(), I = _(), [L, R] = l(null), { data: z, refetch: B } = y(r, { variables: {
|
|
17
17
|
first: 20,
|
|
18
18
|
skip: 0
|
|
19
|
-
} }), { data:
|
|
20
|
-
let e =
|
|
19
|
+
} }), { data: V } = y(n), [H, { loading: U }] = v(t), [W, { loading: G }] = v(e), K = c(() => z?.ggnomadBookings ?? [], [z?.ggnomadBookings]), q = c(() => {
|
|
20
|
+
let e = {
|
|
21
|
+
CONFIRMED: 0,
|
|
22
|
+
CHECKED_IN: 0,
|
|
23
|
+
COMPLETED: 0,
|
|
24
|
+
CANCELLED: 0
|
|
25
|
+
};
|
|
26
|
+
return K.forEach((t) => {
|
|
27
|
+
let n = t.status?.toUpperCase();
|
|
28
|
+
n && n in e && (e[n] += 1);
|
|
29
|
+
}), e;
|
|
30
|
+
}, [K]), J = () => {
|
|
31
|
+
let e = V?.ggnomadProperties?.[0];
|
|
21
32
|
if (e) return {
|
|
22
33
|
type: "PROPERTY",
|
|
23
34
|
listing: {
|
|
@@ -27,34 +38,34 @@ function P() {
|
|
|
27
38
|
currency: e.currency
|
|
28
39
|
}
|
|
29
40
|
};
|
|
30
|
-
let t =
|
|
41
|
+
let t = V?.ggnomadTours?.[0];
|
|
31
42
|
if (t) return {
|
|
32
43
|
type: "TOUR",
|
|
33
44
|
listing: t
|
|
34
45
|
};
|
|
35
|
-
let n =
|
|
46
|
+
let n = V?.ggnomadEvents?.[0];
|
|
36
47
|
if (n) return {
|
|
37
48
|
type: "EVENT",
|
|
38
49
|
listing: n
|
|
39
50
|
};
|
|
40
|
-
let r =
|
|
51
|
+
let r = V?.ggnomadActivities?.[0];
|
|
41
52
|
return r ? {
|
|
42
53
|
type: "ACTIVITY",
|
|
43
54
|
listing: r
|
|
44
55
|
} : null;
|
|
45
|
-
},
|
|
46
|
-
let e =
|
|
56
|
+
}, Y = async () => {
|
|
57
|
+
let e = J();
|
|
47
58
|
if (!e) {
|
|
48
|
-
s.warning(
|
|
59
|
+
s.warning(F("ggnomad.bookings.toast.noListingsTitle", "No listings available to book yet"), F("ggnomad.bookings.toast.noListingsBody", "Add a property, tour, event or activity first, then create a booking against it."));
|
|
49
60
|
return;
|
|
50
61
|
}
|
|
51
62
|
let { type: t, listing: n } = e;
|
|
52
63
|
try {
|
|
53
|
-
let e = await
|
|
64
|
+
let e = await H({ variables: { input: {
|
|
54
65
|
bookingType: t,
|
|
55
66
|
itemId: n.id,
|
|
56
67
|
itemName: n.name,
|
|
57
|
-
guestName:
|
|
68
|
+
guestName: F("ggnomad.bookings.demo.guestName", "Demo Guest"),
|
|
58
69
|
guestEmail: "demo@burdenoff.com",
|
|
59
70
|
guests: 2,
|
|
60
71
|
totalPrice: Number(n.price ?? 0),
|
|
@@ -62,132 +73,176 @@ function P() {
|
|
|
62
73
|
paymentMethod: "card"
|
|
63
74
|
} } });
|
|
64
75
|
if (e.error) {
|
|
65
|
-
s.error(
|
|
76
|
+
s.error(F("ggnomad.bookings.toast.createErrorTitle", "Could not create booking"), e.error.message);
|
|
66
77
|
return;
|
|
67
78
|
}
|
|
68
79
|
let r = e.data?.createGgnomadBooking;
|
|
69
|
-
s.success(
|
|
80
|
+
s.success(F("ggnomad.bookings.toast.createSuccessTitle", "Booking confirmed"), r ? F("ggnomad.bookings.toast.createSuccessBody", "{{ref}} · {{item}}").replace("{{ref}}", r.referenceCode).replace("{{item}}", r.itemName) : n.name), await B();
|
|
70
81
|
} catch (e) {
|
|
71
|
-
s.error(
|
|
82
|
+
s.error(F("ggnomad.bookings.toast.createErrorTitle", "Could not create booking"), e instanceof Error ? e.message : String(e));
|
|
72
83
|
}
|
|
73
|
-
},
|
|
74
|
-
if (
|
|
75
|
-
let e = await
|
|
76
|
-
id:
|
|
77
|
-
reason:
|
|
84
|
+
}, X = async () => {
|
|
85
|
+
if (L) try {
|
|
86
|
+
let e = await W({ variables: {
|
|
87
|
+
id: L.id,
|
|
88
|
+
reason: F("ggnomad.bookings.actions.cancelReason", "Cancelled via UI")
|
|
78
89
|
} });
|
|
79
90
|
if (e.error) {
|
|
80
|
-
s.error(
|
|
91
|
+
s.error(F("ggnomad.bookings.toast.cancelErrorTitle", "Could not cancel booking"), e.error.message);
|
|
81
92
|
return;
|
|
82
93
|
}
|
|
83
|
-
s.success(
|
|
94
|
+
s.success(F("ggnomad.bookings.toast.cancelSuccessTitle", "Booking cancelled"), L.referenceCode), await B();
|
|
84
95
|
} catch (e) {
|
|
85
|
-
s.error(
|
|
96
|
+
s.error(F("ggnomad.bookings.toast.cancelErrorTitle", "Could not cancel booking"), e instanceof Error ? e.message : String(e));
|
|
86
97
|
} finally {
|
|
87
|
-
|
|
98
|
+
R(null);
|
|
88
99
|
}
|
|
89
100
|
};
|
|
90
|
-
return /* @__PURE__ */
|
|
91
|
-
title:
|
|
92
|
-
titleAccent:
|
|
93
|
-
subtitle:
|
|
94
|
-
actions: /* @__PURE__ */
|
|
101
|
+
return /* @__PURE__ */ P(i, {
|
|
102
|
+
title: F("ggnomad.bookings.title", "Bookings"),
|
|
103
|
+
titleAccent: F("ggnomad.bookings.title.accent", "Bookings"),
|
|
104
|
+
subtitle: F("ggnomad.bookings.subtitle", "End-to-end booking operations with real API mutations"),
|
|
105
|
+
actions: /* @__PURE__ */ N(O, {
|
|
95
106
|
primary: {
|
|
96
|
-
label:
|
|
97
|
-
onSelect: () => void
|
|
98
|
-
disabled:
|
|
99
|
-
icon: /* @__PURE__ */
|
|
107
|
+
label: F("ggnomad.bookings.actions.createDemoBooking", "Create demo booking"),
|
|
108
|
+
onSelect: () => void Y(),
|
|
109
|
+
disabled: U,
|
|
110
|
+
icon: /* @__PURE__ */ N(p, {})
|
|
100
111
|
},
|
|
101
112
|
actions: []
|
|
102
113
|
}),
|
|
103
114
|
children: [
|
|
104
|
-
/* @__PURE__ */ M
|
|
105
|
-
icon: /* @__PURE__ */
|
|
106
|
-
children:
|
|
115
|
+
/* @__PURE__ */ N(M, {
|
|
116
|
+
icon: /* @__PURE__ */ N(u, { className: "size-4" }),
|
|
117
|
+
children: F("ggnomad.bookings.purpose", "Your single place to manage every reservation across travel, stays, tours and events. Track each booking’s status and reference code in real time, and cancel a reservation when plans change — all backed by live API mutations.")
|
|
107
118
|
}),
|
|
108
|
-
/* @__PURE__ */
|
|
119
|
+
/* @__PURE__ */ N(j, {
|
|
109
120
|
storageKey: "ggnomad-bookings",
|
|
110
121
|
steps: [
|
|
111
122
|
{
|
|
112
123
|
id: "book-stay",
|
|
113
|
-
label:
|
|
114
|
-
description:
|
|
115
|
-
icon: /* @__PURE__ */
|
|
116
|
-
onClick: () =>
|
|
124
|
+
label: F("ggnomad.bookings.next.stay", "Book a place to stay"),
|
|
125
|
+
description: F("ggnomad.bookings.next.stayDesc", "Browse hospitality listings and reserve a property."),
|
|
126
|
+
icon: /* @__PURE__ */ N(f, { className: "size-4" }),
|
|
127
|
+
onClick: () => I("../hospitality")
|
|
117
128
|
},
|
|
118
129
|
{
|
|
119
130
|
id: "book-tour",
|
|
120
|
-
label:
|
|
121
|
-
description:
|
|
122
|
-
icon: /* @__PURE__ */
|
|
123
|
-
onClick: () =>
|
|
131
|
+
label: F("ggnomad.bookings.next.tour", "Add a tour or activity"),
|
|
132
|
+
description: F("ggnomad.bookings.next.tourDesc", "Pick a curated experience from Tourism."),
|
|
133
|
+
icon: /* @__PURE__ */ N(d, { className: "size-4" }),
|
|
134
|
+
onClick: () => I("../tourism")
|
|
124
135
|
},
|
|
125
136
|
{
|
|
126
137
|
id: "book-event",
|
|
127
|
-
label:
|
|
128
|
-
description:
|
|
129
|
-
icon: /* @__PURE__ */
|
|
130
|
-
onClick: () =>
|
|
138
|
+
label: F("ggnomad.bookings.next.event", "Grab event tickets"),
|
|
139
|
+
description: F("ggnomad.bookings.next.eventDesc", "Find live events to round out the itinerary."),
|
|
140
|
+
icon: /* @__PURE__ */ N(m, { className: "size-4" }),
|
|
141
|
+
onClick: () => I("../entertainment")
|
|
131
142
|
}
|
|
132
143
|
]
|
|
133
144
|
}),
|
|
134
|
-
|
|
145
|
+
K.length > 0 && /* @__PURE__ */ N("div", {
|
|
146
|
+
className: "grid gap-3 sm:grid-cols-4",
|
|
147
|
+
children: [
|
|
148
|
+
{
|
|
149
|
+
status: "CONFIRMED",
|
|
150
|
+
label: F("ggnomad.bookings.pipeline.confirmed", "Confirmed"),
|
|
151
|
+
icon: u
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
status: "CHECKED_IN",
|
|
155
|
+
label: F("ggnomad.bookings.pipeline.checkedIn", "Checked-in"),
|
|
156
|
+
icon: f
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
status: "COMPLETED",
|
|
160
|
+
label: F("ggnomad.bookings.pipeline.completed", "Completed"),
|
|
161
|
+
icon: m
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
status: "CANCELLED",
|
|
165
|
+
label: F("ggnomad.bookings.pipeline.cancelled", "Cancelled"),
|
|
166
|
+
icon: h
|
|
167
|
+
}
|
|
168
|
+
].map((e) => {
|
|
169
|
+
let t = e.icon, n = q[e.status];
|
|
170
|
+
return /* @__PURE__ */ P("div", {
|
|
171
|
+
className: "flex flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface/40 p-3 text-center",
|
|
172
|
+
children: [
|
|
173
|
+
/* @__PURE__ */ N("div", {
|
|
174
|
+
className: "flex size-8 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary",
|
|
175
|
+
children: /* @__PURE__ */ N(t, { className: "size-4" })
|
|
176
|
+
}),
|
|
177
|
+
/* @__PURE__ */ N("p", {
|
|
178
|
+
className: "text-xs font-medium text-text-secondary",
|
|
179
|
+
children: e.label
|
|
180
|
+
}),
|
|
181
|
+
/* @__PURE__ */ N("p", {
|
|
182
|
+
className: "text-lg font-semibold tabular-nums text-text-primary",
|
|
183
|
+
children: n
|
|
184
|
+
})
|
|
185
|
+
]
|
|
186
|
+
}, e.status);
|
|
187
|
+
})
|
|
188
|
+
}),
|
|
189
|
+
K.length === 0 ? /* @__PURE__ */ N(A, {
|
|
135
190
|
illustration: "empty-data",
|
|
136
|
-
title:
|
|
137
|
-
description:
|
|
138
|
-
}) : /* @__PURE__ */
|
|
191
|
+
title: F("ggnomad.bookings.emptyTitle", "No bookings yet"),
|
|
192
|
+
description: F("ggnomad.bookings.emptyDescription", "Create your first booking to see it tracked here in real time.")
|
|
193
|
+
}) : /* @__PURE__ */ P(k, {
|
|
139
194
|
treatment: "glass",
|
|
140
195
|
glow: !0,
|
|
141
196
|
className: "border-border-seam p-5",
|
|
142
|
-
children: [/* @__PURE__ */
|
|
197
|
+
children: [/* @__PURE__ */ N("h2", {
|
|
143
198
|
className: "mb-4 text-lg font-medium tracking-tight text-text-primary",
|
|
144
|
-
children:
|
|
145
|
-
}), /* @__PURE__ */
|
|
199
|
+
children: F("ggnomad.bookings.activeBookings", "Your bookings")
|
|
200
|
+
}), /* @__PURE__ */ N("div", {
|
|
146
201
|
className: "grid gap-3 sm:grid-cols-2",
|
|
147
|
-
children:
|
|
202
|
+
children: K.map((e) => /* @__PURE__ */ N(o, {
|
|
148
203
|
title: e.referenceCode,
|
|
149
204
|
meta: e.itemName,
|
|
150
205
|
price: `${e.currency} ${Number(e.totalPrice).toFixed(2)}`,
|
|
151
|
-
badge: /* @__PURE__ */
|
|
206
|
+
badge: /* @__PURE__ */ N(a, {
|
|
152
207
|
status: e.status,
|
|
153
208
|
live: e.status !== "CANCELLED"
|
|
154
209
|
}),
|
|
155
|
-
footer: e.status === "CANCELLED" ? null : /* @__PURE__ */
|
|
210
|
+
footer: e.status === "CANCELLED" ? null : /* @__PURE__ */ N(O, {
|
|
156
211
|
align: "end",
|
|
157
212
|
actions: [{
|
|
158
|
-
label:
|
|
159
|
-
onSelect: () =>
|
|
213
|
+
label: F("ggnomad.bookings.actions.cancel", "Cancel booking"),
|
|
214
|
+
onSelect: () => R({
|
|
160
215
|
id: e.id,
|
|
161
216
|
referenceCode: e.referenceCode
|
|
162
217
|
}),
|
|
163
218
|
intent: "destructive",
|
|
164
|
-
icon: /* @__PURE__ */
|
|
219
|
+
icon: /* @__PURE__ */ N(h, {})
|
|
165
220
|
}]
|
|
166
221
|
})
|
|
167
222
|
}, e.id))
|
|
168
223
|
})]
|
|
169
224
|
}),
|
|
170
|
-
/* @__PURE__ */
|
|
171
|
-
open:
|
|
225
|
+
/* @__PURE__ */ N(b, {
|
|
226
|
+
open: L !== null,
|
|
172
227
|
onOpenChange: (e) => {
|
|
173
|
-
e ||
|
|
228
|
+
e || R(null);
|
|
174
229
|
},
|
|
175
|
-
children: /* @__PURE__ */
|
|
176
|
-
disabled:
|
|
177
|
-
children:
|
|
178
|
-
}), /* @__PURE__ */
|
|
230
|
+
children: /* @__PURE__ */ P(C, { children: [/* @__PURE__ */ P(E, { children: [/* @__PURE__ */ N(D, { children: F("ggnomad.bookings.cancelDialog.title", "Cancel this booking?") }), /* @__PURE__ */ N(w, { children: F("ggnomad.bookings.cancelDialog.body", "This releases reservation {{ref}} and cannot be undone.").replace("{{ref}}", L?.referenceCode ?? "") })] }), /* @__PURE__ */ P(T, { children: [/* @__PURE__ */ N(S, {
|
|
231
|
+
disabled: G,
|
|
232
|
+
children: F("ggnomad.bookings.cancelDialog.keep", "Keep booking")
|
|
233
|
+
}), /* @__PURE__ */ N(x, {
|
|
179
234
|
className: "bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover",
|
|
180
|
-
disabled:
|
|
235
|
+
disabled: G,
|
|
181
236
|
onClick: (e) => {
|
|
182
|
-
e.preventDefault(),
|
|
237
|
+
e.preventDefault(), X();
|
|
183
238
|
},
|
|
184
|
-
children:
|
|
239
|
+
children: G ? F("ggnomad.bookings.cancelDialog.cancelling", "Cancelling…") : F("ggnomad.bookings.cancelDialog.confirm", "Cancel booking")
|
|
185
240
|
})] })] })
|
|
186
241
|
})
|
|
187
242
|
]
|
|
188
243
|
});
|
|
189
244
|
}
|
|
190
245
|
//#endregion
|
|
191
|
-
export {
|
|
246
|
+
export { F as BookingsPage };
|
|
192
247
|
|
|
193
248
|
//# sourceMappingURL=BookingsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BookingsPage.js","names":[],"sources":["../../../src/ggnomad/pages/BookingsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { CalendarCheck, Compass, Hotel, Plus, Ticket, XCircle } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n CTAOverflowMenu,\n GlassCard,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n CANCEL_GGNOMAD_BOOKING_MUTATION,\n CREATE_GGNOMAD_BOOKING_MUTATION,\n GGNOMAD_BOOKABLE_SAMPLE_QUERY,\n GGNOMAD_BOOKINGS_QUERY,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { EntityCard, StatusBadge } from '../components';\nimport { notify } from '../utils/notify';\n\ntype BookingType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype PricedListing = { id: string; name: string; price: number; currency: string };\n\ntype CancelTarget = { id: string; referenceCode: string };\n\nexport function BookingsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const [cancelTarget, setCancelTarget] = useState<CancelTarget | null>(null);\n\n const { data, refetch } = useQuery(\n GGNOMAD_BOOKINGS_QUERY as TypedDocumentNode<\n {\n ggnomadBookings: Array<{\n id: string;\n referenceCode: string;\n itemName: string;\n status: string;\n totalPrice: number;\n currency: string;\n }>;\n },\n { first: number; skip: number }\n >,\n { variables: { first: 20, skip: 0 } }\n );\n\n // One real listing of each type so the demo booking targets an id that\n // actually exists in the workspace (the backend rejects synthetic ids).\n const { data: sampleData } = useQuery(\n GGNOMAD_BOOKABLE_SAMPLE_QUERY as TypedDocumentNode<\n {\n ggnomadProperties: Array<{\n id: string;\n name: string;\n pricePerNight: number;\n currency: string;\n }>;\n ggnomadTours: Array<{ id: string; name: string; price: number; currency: string }>;\n ggnomadEvents: Array<{ id: string; name: string; price: number; currency: string }>;\n ggnomadActivities: Array<{ id: string; name: string; price: number; currency: string }>;\n },\n Record<string, never>\n >\n );\n\n const [createBooking, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { createGgnomadBooking: { id: string; referenceCode: string; itemName: string } },\n {\n input: {\n bookingType: BookingType;\n itemId: string;\n itemName: string;\n guestName: string;\n guestEmail: string;\n guests: number;\n totalPrice: number;\n currency: string;\n paymentMethod: string;\n };\n }\n >\n );\n\n const [cancelBooking, { loading: cancelling }] = useMutation(\n CANCEL_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { cancelGgnomadBooking: { id: string; status: string } },\n { id: string; reason: string }\n >\n );\n\n const bookings = data?.ggnomadBookings ?? [];\n\n /** First available real listing, in priority order, mapped to a booking type. */\n const resolveDemoTarget = (): { type: BookingType; listing: PricedListing } | null => {\n const property = sampleData?.ggnomadProperties?.[0];\n if (property) {\n return {\n type: 'PROPERTY',\n listing: {\n id: property.id,\n name: property.name,\n price: property.pricePerNight,\n currency: property.currency,\n },\n };\n }\n const tour = sampleData?.ggnomadTours?.[0];\n if (tour) return { type: 'TOUR', listing: tour };\n const event = sampleData?.ggnomadEvents?.[0];\n if (event) return { type: 'EVENT', listing: event };\n const activity = sampleData?.ggnomadActivities?.[0];\n if (activity) return { type: 'ACTIVITY', listing: activity };\n return null;\n };\n\n const handleCreateDemoBooking = async () => {\n const target = resolveDemoTarget();\n if (!target) {\n notify.warning(\n t('ggnomad.bookings.toast.noListingsTitle', 'No listings available to book yet'),\n t(\n 'ggnomad.bookings.toast.noListingsBody',\n 'Add a property, tour, event or activity first, then create a booking against it.'\n )\n );\n return;\n }\n const { type, listing } = target;\n try {\n const result = await createBooking({\n variables: {\n input: {\n bookingType: type,\n itemId: listing.id,\n itemName: listing.name,\n guestName: t('ggnomad.bookings.demo.guestName', 'Demo Guest'),\n guestEmail: 'demo@burdenoff.com',\n guests: 2,\n totalPrice: Number(listing.price ?? 0),\n currency: listing.currency ?? 'USD',\n paymentMethod: 'card',\n },\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.bookings.toast.createErrorTitle', 'Could not create booking'),\n result.error.message\n );\n return;\n }\n const created = result.data?.createGgnomadBooking;\n notify.success(\n t('ggnomad.bookings.toast.createSuccessTitle', 'Booking confirmed'),\n created\n ? t('ggnomad.bookings.toast.createSuccessBody', '{{ref}} · {{item}}')\n .replace('{{ref}}', created.referenceCode)\n .replace('{{item}}', created.itemName)\n : listing.name\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.bookings.toast.createErrorTitle', 'Could not create booking'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const confirmCancel = async () => {\n if (!cancelTarget) return;\n try {\n const result = await cancelBooking({\n variables: {\n id: cancelTarget.id,\n reason: t('ggnomad.bookings.actions.cancelReason', 'Cancelled via UI'),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.bookings.toast.cancelErrorTitle', 'Could not cancel booking'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.bookings.toast.cancelSuccessTitle', 'Booking cancelled'),\n cancelTarget.referenceCode\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.bookings.toast.cancelErrorTitle', 'Could not cancel booking'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setCancelTarget(null);\n }\n };\n\n return (\n <PageShell\n title={t('ggnomad.bookings.title', 'Bookings')}\n titleAccent={t('ggnomad.bookings.title.accent', 'Bookings')}\n subtitle={t(\n 'ggnomad.bookings.subtitle',\n 'End-to-end booking operations with real API mutations'\n )}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.bookings.actions.createDemoBooking', 'Create demo booking'),\n onSelect: () => void handleCreateDemoBooking(),\n disabled: creating,\n icon: <Plus />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<CalendarCheck className=\"size-4\" />}>\n {t(\n 'ggnomad.bookings.purpose',\n 'Your single place to manage every reservation across travel, stays, tours and events. Track each booking’s status and reference code in real time, and cancel a reservation when plans change — all backed by live API mutations.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-bookings\"\n steps={[\n {\n id: 'book-stay',\n label: t('ggnomad.bookings.next.stay', 'Book a place to stay'),\n description: t(\n 'ggnomad.bookings.next.stayDesc',\n 'Browse hospitality listings and reserve a property.'\n ),\n icon: <Hotel className=\"size-4\" />,\n onClick: () => navigate('../hospitality'),\n },\n {\n id: 'book-tour',\n label: t('ggnomad.bookings.next.tour', 'Add a tour or activity'),\n description: t(\n 'ggnomad.bookings.next.tourDesc',\n 'Pick a curated experience from Tourism.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate('../tourism'),\n },\n {\n id: 'book-event',\n label: t('ggnomad.bookings.next.event', 'Grab event tickets'),\n description: t(\n 'ggnomad.bookings.next.eventDesc',\n 'Find live events to round out the itinerary.'\n ),\n icon: <Ticket className=\"size-4\" />,\n onClick: () => navigate('../entertainment'),\n },\n ]}\n />\n\n {bookings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.bookings.emptyTitle', 'No bookings yet')}\n description={t(\n 'ggnomad.bookings.emptyDescription',\n 'Create your first booking to see it tracked here in real time.'\n )}\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.bookings.activeBookings', 'Your bookings')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {bookings.map((booking) => (\n <EntityCard\n key={booking.id}\n title={booking.referenceCode}\n meta={booking.itemName}\n price={`${booking.currency} ${Number(booking.totalPrice).toFixed(2)}`}\n badge={\n <StatusBadge status={booking.status} live={booking.status !== 'CANCELLED'} />\n }\n footer={\n booking.status !== 'CANCELLED' ? (\n <CTAOverflowMenu\n align=\"end\"\n actions={[\n {\n label: t('ggnomad.bookings.actions.cancel', 'Cancel booking'),\n onSelect: () =>\n setCancelTarget({\n id: booking.id,\n referenceCode: booking.referenceCode,\n }),\n intent: 'destructive',\n icon: <XCircle />,\n },\n ]}\n />\n ) : null\n }\n />\n ))}\n </div>\n </GlassCard>\n )}\n\n <AlertDialog\n open={cancelTarget !== null}\n onOpenChange={(open) => {\n if (!open) setCancelTarget(null);\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.bookings.cancelDialog.title', 'Cancel this booking?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.bookings.cancelDialog.body',\n 'This releases reservation {{ref}} and cannot be undone.'\n ).replace('{{ref}}', cancelTarget?.referenceCode ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={cancelling}>\n {t('ggnomad.bookings.cancelDialog.keep', 'Keep booking')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={cancelling}\n onClick={(event) => {\n event.preventDefault();\n void confirmCancel();\n }}\n >\n {cancelling\n ? t('ggnomad.bookings.cancelDialog.cancelling', 'Cancelling…')\n : t('ggnomad.bookings.cancelDialog.confirm', 'Cancel booking')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BookingsPage.js","names":[],"sources":["../../../src/ggnomad/pages/BookingsPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { CalendarCheck, Compass, Hotel, Plus, Ticket, XCircle } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n CTAOverflowMenu,\n GlassCard,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n CANCEL_GGNOMAD_BOOKING_MUTATION,\n CREATE_GGNOMAD_BOOKING_MUTATION,\n GGNOMAD_BOOKABLE_SAMPLE_QUERY,\n GGNOMAD_BOOKINGS_QUERY,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { EntityCard, StatusBadge } from '../components';\nimport { notify } from '../utils/notify';\n\ntype BookingType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype PricedListing = { id: string; name: string; price: number; currency: string };\n\ntype CancelTarget = { id: string; referenceCode: string };\n\nexport function BookingsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const [cancelTarget, setCancelTarget] = useState<CancelTarget | null>(null);\n\n const { data, refetch } = useQuery(\n GGNOMAD_BOOKINGS_QUERY as TypedDocumentNode<\n {\n ggnomadBookings: Array<{\n id: string;\n referenceCode: string;\n itemName: string;\n status: string;\n totalPrice: number;\n currency: string;\n }>;\n },\n { first: number; skip: number }\n >,\n { variables: { first: 20, skip: 0 } }\n );\n\n // One real listing of each type so the demo booking targets an id that\n // actually exists in the workspace (the backend rejects synthetic ids).\n const { data: sampleData } = useQuery(\n GGNOMAD_BOOKABLE_SAMPLE_QUERY as TypedDocumentNode<\n {\n ggnomadProperties: Array<{\n id: string;\n name: string;\n pricePerNight: number;\n currency: string;\n }>;\n ggnomadTours: Array<{ id: string; name: string; price: number; currency: string }>;\n ggnomadEvents: Array<{ id: string; name: string; price: number; currency: string }>;\n ggnomadActivities: Array<{ id: string; name: string; price: number; currency: string }>;\n },\n Record<string, never>\n >\n );\n\n const [createBooking, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { createGgnomadBooking: { id: string; referenceCode: string; itemName: string } },\n {\n input: {\n bookingType: BookingType;\n itemId: string;\n itemName: string;\n guestName: string;\n guestEmail: string;\n guests: number;\n totalPrice: number;\n currency: string;\n paymentMethod: string;\n };\n }\n >\n );\n\n const [cancelBooking, { loading: cancelling }] = useMutation(\n CANCEL_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { cancelGgnomadBooking: { id: string; status: string } },\n { id: string; reason: string }\n >\n );\n\n const bookings = useMemo(() => data?.ggnomadBookings ?? [], [data?.ggnomadBookings]);\n\n const statusCounts = useMemo(() => {\n const counts: Record<string, number> = {\n CONFIRMED: 0,\n CHECKED_IN: 0,\n COMPLETED: 0,\n CANCELLED: 0,\n };\n bookings.forEach((booking) => {\n const status = booking.status?.toUpperCase();\n if (status && status in counts) {\n counts[status] += 1;\n }\n });\n return counts;\n }, [bookings]);\n\n /** First available real listing, in priority order, mapped to a booking type. */\n const resolveDemoTarget = (): { type: BookingType; listing: PricedListing } | null => {\n const property = sampleData?.ggnomadProperties?.[0];\n if (property) {\n return {\n type: 'PROPERTY',\n listing: {\n id: property.id,\n name: property.name,\n price: property.pricePerNight,\n currency: property.currency,\n },\n };\n }\n const tour = sampleData?.ggnomadTours?.[0];\n if (tour) return { type: 'TOUR', listing: tour };\n const event = sampleData?.ggnomadEvents?.[0];\n if (event) return { type: 'EVENT', listing: event };\n const activity = sampleData?.ggnomadActivities?.[0];\n if (activity) return { type: 'ACTIVITY', listing: activity };\n return null;\n };\n\n const handleCreateDemoBooking = async () => {\n const target = resolveDemoTarget();\n if (!target) {\n notify.warning(\n t('ggnomad.bookings.toast.noListingsTitle', 'No listings available to book yet'),\n t(\n 'ggnomad.bookings.toast.noListingsBody',\n 'Add a property, tour, event or activity first, then create a booking against it.'\n )\n );\n return;\n }\n const { type, listing } = target;\n try {\n const result = await createBooking({\n variables: {\n input: {\n bookingType: type,\n itemId: listing.id,\n itemName: listing.name,\n guestName: t('ggnomad.bookings.demo.guestName', 'Demo Guest'),\n guestEmail: 'demo@burdenoff.com',\n guests: 2,\n totalPrice: Number(listing.price ?? 0),\n currency: listing.currency ?? 'USD',\n paymentMethod: 'card',\n },\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.bookings.toast.createErrorTitle', 'Could not create booking'),\n result.error.message\n );\n return;\n }\n const created = result.data?.createGgnomadBooking;\n notify.success(\n t('ggnomad.bookings.toast.createSuccessTitle', 'Booking confirmed'),\n created\n ? t('ggnomad.bookings.toast.createSuccessBody', '{{ref}} · {{item}}')\n .replace('{{ref}}', created.referenceCode)\n .replace('{{item}}', created.itemName)\n : listing.name\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.bookings.toast.createErrorTitle', 'Could not create booking'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const confirmCancel = async () => {\n if (!cancelTarget) return;\n try {\n const result = await cancelBooking({\n variables: {\n id: cancelTarget.id,\n reason: t('ggnomad.bookings.actions.cancelReason', 'Cancelled via UI'),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.bookings.toast.cancelErrorTitle', 'Could not cancel booking'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.bookings.toast.cancelSuccessTitle', 'Booking cancelled'),\n cancelTarget.referenceCode\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.bookings.toast.cancelErrorTitle', 'Could not cancel booking'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setCancelTarget(null);\n }\n };\n\n return (\n <PageShell\n title={t('ggnomad.bookings.title', 'Bookings')}\n titleAccent={t('ggnomad.bookings.title.accent', 'Bookings')}\n subtitle={t(\n 'ggnomad.bookings.subtitle',\n 'End-to-end booking operations with real API mutations'\n )}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.bookings.actions.createDemoBooking', 'Create demo booking'),\n onSelect: () => void handleCreateDemoBooking(),\n disabled: creating,\n icon: <Plus />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<CalendarCheck className=\"size-4\" />}>\n {t(\n 'ggnomad.bookings.purpose',\n 'Your single place to manage every reservation across travel, stays, tours and events. Track each booking’s status and reference code in real time, and cancel a reservation when plans change — all backed by live API mutations.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-bookings\"\n steps={[\n {\n id: 'book-stay',\n label: t('ggnomad.bookings.next.stay', 'Book a place to stay'),\n description: t(\n 'ggnomad.bookings.next.stayDesc',\n 'Browse hospitality listings and reserve a property.'\n ),\n icon: <Hotel className=\"size-4\" />,\n onClick: () => navigate('../hospitality'),\n },\n {\n id: 'book-tour',\n label: t('ggnomad.bookings.next.tour', 'Add a tour or activity'),\n description: t(\n 'ggnomad.bookings.next.tourDesc',\n 'Pick a curated experience from Tourism.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate('../tourism'),\n },\n {\n id: 'book-event',\n label: t('ggnomad.bookings.next.event', 'Grab event tickets'),\n description: t(\n 'ggnomad.bookings.next.eventDesc',\n 'Find live events to round out the itinerary.'\n ),\n icon: <Ticket className=\"size-4\" />,\n onClick: () => navigate('../entertainment'),\n },\n ]}\n />\n\n {bookings.length > 0 && (\n <div className=\"grid gap-3 sm:grid-cols-4\">\n {[\n {\n status: 'CONFIRMED',\n label: t('ggnomad.bookings.pipeline.confirmed', 'Confirmed'),\n icon: CalendarCheck,\n },\n {\n status: 'CHECKED_IN',\n label: t('ggnomad.bookings.pipeline.checkedIn', 'Checked-in'),\n icon: Hotel,\n },\n {\n status: 'COMPLETED',\n label: t('ggnomad.bookings.pipeline.completed', 'Completed'),\n icon: Ticket,\n },\n {\n status: 'CANCELLED',\n label: t('ggnomad.bookings.pipeline.cancelled', 'Cancelled'),\n icon: XCircle,\n },\n ].map((stage) => {\n const Icon = stage.icon;\n const count = statusCounts[stage.status];\n return (\n <div\n key={stage.status}\n className=\"flex flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface/40 p-3 text-center\"\n >\n <div className=\"flex size-8 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Icon className=\"size-4\" />\n </div>\n <p className=\"text-xs font-medium text-text-secondary\">{stage.label}</p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">{count}</p>\n </div>\n );\n })}\n </div>\n )}\n\n {bookings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.bookings.emptyTitle', 'No bookings yet')}\n description={t(\n 'ggnomad.bookings.emptyDescription',\n 'Create your first booking to see it tracked here in real time.'\n )}\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.bookings.activeBookings', 'Your bookings')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {bookings.map((booking) => (\n <EntityCard\n key={booking.id}\n title={booking.referenceCode}\n meta={booking.itemName}\n price={`${booking.currency} ${Number(booking.totalPrice).toFixed(2)}`}\n badge={\n <StatusBadge status={booking.status} live={booking.status !== 'CANCELLED'} />\n }\n footer={\n booking.status !== 'CANCELLED' ? (\n <CTAOverflowMenu\n align=\"end\"\n actions={[\n {\n label: t('ggnomad.bookings.actions.cancel', 'Cancel booking'),\n onSelect: () =>\n setCancelTarget({\n id: booking.id,\n referenceCode: booking.referenceCode,\n }),\n intent: 'destructive',\n icon: <XCircle />,\n },\n ]}\n />\n ) : null\n }\n />\n ))}\n </div>\n </GlassCard>\n )}\n\n <AlertDialog\n open={cancelTarget !== null}\n onOpenChange={(open) => {\n if (!open) setCancelTarget(null);\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.bookings.cancelDialog.title', 'Cancel this booking?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.bookings.cancelDialog.body',\n 'This releases reservation {{ref}} and cannot be undone.'\n ).replace('{{ref}}', cancelTarget?.referenceCode ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={cancelling}>\n {t('ggnomad.bookings.cancelDialog.keep', 'Keep booking')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={cancelling}\n onClick={(event) => {\n event.preventDefault();\n void confirmCancel();\n }}\n >\n {cancelling\n ? t('ggnomad.bookings.cancelDialog.cancelling', 'Cancelling…')\n : t('ggnomad.bookings.cancelDialog.confirm', 'Cancel booking')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -4,150 +4,208 @@ import { StatCard as o } from "../components/StatCard.js";
|
|
|
4
4
|
import { StatusBadge as s } from "../components/StatusBadge.js";
|
|
5
5
|
import { SectionCard as c } from "../components/SectionCard.js";
|
|
6
6
|
import "../components/index.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
7
|
+
import { useMemo as l } from "react";
|
|
8
|
+
import { Building as u, CalendarCheck as d, Compass as f, DollarSign as p, Hotel as m, Search as h, Users as g } from "lucide-react";
|
|
9
|
+
import { useI18n as _ } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
10
|
+
import { useNavigate as v } from "react-router-dom";
|
|
11
|
+
import { useQuery as y } from "@apollo/client/react";
|
|
12
|
+
import { EmphasisPanel as b, IllustratedEmptyState as x, NextSteps as S, PagePurpose as C } from "@burdenoff/fe-libs/ui";
|
|
13
|
+
import { jsx as w, jsxs as T } from "react/jsx-runtime";
|
|
13
14
|
//#region src/ggnomad/pages/DashboardPage.tsx
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
var E = [
|
|
16
|
+
"var(--color-chart-1)",
|
|
17
|
+
"var(--color-chart-2)",
|
|
18
|
+
"var(--color-chart-3)",
|
|
19
|
+
"var(--color-chart-4)",
|
|
20
|
+
"var(--color-chart-5)"
|
|
21
|
+
], D = (e) => e.replace(/[_-]+/g, " ").trim().replace(/\b\w/g, (e) => e.toUpperCase()) || e;
|
|
22
|
+
function O() {
|
|
23
|
+
let { t: O } = _(), k = v(), A = y(n), j = y(e, { variables: {
|
|
16
24
|
first: 5,
|
|
17
25
|
skip: 0
|
|
18
|
-
} }),
|
|
26
|
+
} }), M = y(r, { variables: {
|
|
19
27
|
first: 4,
|
|
20
28
|
skip: 0
|
|
21
|
-
} }),
|
|
29
|
+
} }), N = y(i, { variables: {
|
|
22
30
|
first: 4,
|
|
23
31
|
skip: 0
|
|
24
|
-
} }),
|
|
32
|
+
} }), P = y(t, { variables: {
|
|
25
33
|
first: 4,
|
|
26
34
|
skip: 0
|
|
27
|
-
} }),
|
|
28
|
-
|
|
35
|
+
} }), F = A.data?.ggnomadOverviewStats, I = A.data?.ggnomadAnalytics, L = j.data?.ggnomadBookings ?? [], R = l(() => {
|
|
36
|
+
let e = I?.revenueByCategory ?? {};
|
|
37
|
+
return Object.entries(e).map(([e, t]) => ({
|
|
38
|
+
label: D(e),
|
|
39
|
+
value: Number(t)
|
|
40
|
+
})).filter((e) => Number.isFinite(e.value) && e.value > 0).sort((e, t) => t.value - e.value);
|
|
41
|
+
}, [I?.revenueByCategory]), z = R.length > 0 ? R[0].value : 0;
|
|
42
|
+
return /* @__PURE__ */ T(a, {
|
|
29
43
|
hero: !0,
|
|
30
|
-
title:
|
|
31
|
-
titleAccent:
|
|
32
|
-
subtitle:
|
|
44
|
+
title: O("ggnomad.dashboard.title", "GGNomad Dashboard"),
|
|
45
|
+
titleAccent: O("ggnomad.dashboard.title.accent", "GGNomad"),
|
|
46
|
+
subtitle: O("ggnomad.dashboard.subtitle", "Unified travel, hospitality, tourism and entertainment operations"),
|
|
33
47
|
children: [
|
|
34
|
-
/* @__PURE__ */ C
|
|
35
|
-
icon: /* @__PURE__ */
|
|
36
|
-
children:
|
|
48
|
+
/* @__PURE__ */ w(C, {
|
|
49
|
+
icon: /* @__PURE__ */ w(f, { className: "size-4" }),
|
|
50
|
+
children: O("ggnomad.dashboard.purpose", "Your command centre for the whole GGNomad marketplace — see live revenue, users, listings and bookings across travel, hospitality, tourism and entertainment in one place, and jump straight into the area you want to work on.")
|
|
37
51
|
}),
|
|
38
|
-
/* @__PURE__ */
|
|
52
|
+
/* @__PURE__ */ w(S, {
|
|
39
53
|
storageKey: "ggnomad-dashboard",
|
|
40
54
|
steps: [
|
|
41
55
|
{
|
|
42
56
|
id: "search",
|
|
43
|
-
label:
|
|
44
|
-
description:
|
|
45
|
-
icon: /* @__PURE__ */
|
|
46
|
-
onClick: () =>
|
|
57
|
+
label: O("ggnomad.dashboard.next.search", "Search the catalogue"),
|
|
58
|
+
description: O("ggnomad.dashboard.next.searchDesc", "Find inventory across every domain at once."),
|
|
59
|
+
icon: /* @__PURE__ */ w(h, { className: "size-4" }),
|
|
60
|
+
onClick: () => k("search")
|
|
47
61
|
},
|
|
48
62
|
{
|
|
49
63
|
id: "stays",
|
|
50
|
-
label:
|
|
51
|
-
description:
|
|
52
|
-
icon: /* @__PURE__ */
|
|
53
|
-
onClick: () =>
|
|
64
|
+
label: O("ggnomad.dashboard.next.stays", "Browse places to stay"),
|
|
65
|
+
description: O("ggnomad.dashboard.next.staysDesc", "Explore hospitality listings and open a property."),
|
|
66
|
+
icon: /* @__PURE__ */ w(m, { className: "size-4" }),
|
|
67
|
+
onClick: () => k("hospitality")
|
|
54
68
|
},
|
|
55
69
|
{
|
|
56
70
|
id: "tourism",
|
|
57
|
-
label:
|
|
58
|
-
description:
|
|
59
|
-
icon: /* @__PURE__ */
|
|
60
|
-
onClick: () =>
|
|
71
|
+
label: O("ggnomad.dashboard.next.tourism", "Discover tours & activities"),
|
|
72
|
+
description: O("ggnomad.dashboard.next.tourismDesc", "Plan what to do at each destination."),
|
|
73
|
+
icon: /* @__PURE__ */ w(f, { className: "size-4" }),
|
|
74
|
+
onClick: () => k("tourism")
|
|
61
75
|
},
|
|
62
76
|
{
|
|
63
77
|
id: "bookings",
|
|
64
|
-
label:
|
|
65
|
-
description:
|
|
66
|
-
icon: /* @__PURE__ */
|
|
67
|
-
onClick: () =>
|
|
78
|
+
label: O("ggnomad.dashboard.next.bookings", "Manage bookings"),
|
|
79
|
+
description: O("ggnomad.dashboard.next.bookingsDesc", "Track and cancel reservations in real time."),
|
|
80
|
+
icon: /* @__PURE__ */ w(d, { className: "size-4" }),
|
|
81
|
+
onClick: () => k("bookings")
|
|
68
82
|
}
|
|
69
83
|
]
|
|
70
84
|
}),
|
|
71
|
-
/* @__PURE__ */
|
|
85
|
+
/* @__PURE__ */ T("section", {
|
|
72
86
|
className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4",
|
|
73
87
|
children: [
|
|
74
|
-
/* @__PURE__ */
|
|
88
|
+
/* @__PURE__ */ w(o, {
|
|
75
89
|
hero: !0,
|
|
76
|
-
label:
|
|
77
|
-
value: `$${Number(
|
|
78
|
-
icon: /* @__PURE__ */
|
|
90
|
+
label: O("ggnomad.dashboard.stats.monthlyRevenue", "Monthly Revenue"),
|
|
91
|
+
value: `$${Number(F?.monthlyRevenue ?? 0).toLocaleString()}`,
|
|
92
|
+
icon: /* @__PURE__ */ w(p, {})
|
|
79
93
|
}),
|
|
80
|
-
/* @__PURE__ */
|
|
81
|
-
label:
|
|
82
|
-
value: Number(
|
|
83
|
-
icon: /* @__PURE__ */
|
|
94
|
+
/* @__PURE__ */ w(o, {
|
|
95
|
+
label: O("ggnomad.dashboard.stats.users", "Users"),
|
|
96
|
+
value: Number(F?.users ?? 0).toLocaleString(),
|
|
97
|
+
icon: /* @__PURE__ */ w(g, {})
|
|
84
98
|
}),
|
|
85
|
-
/* @__PURE__ */
|
|
86
|
-
label:
|
|
87
|
-
value: Number(
|
|
88
|
-
icon: /* @__PURE__ */
|
|
99
|
+
/* @__PURE__ */ w(o, {
|
|
100
|
+
label: O("ggnomad.dashboard.stats.listings", "Listings"),
|
|
101
|
+
value: Number(F?.listings ?? 0).toLocaleString(),
|
|
102
|
+
icon: /* @__PURE__ */ w(u, {})
|
|
89
103
|
}),
|
|
90
|
-
/* @__PURE__ */
|
|
91
|
-
label:
|
|
92
|
-
value: Number(
|
|
93
|
-
icon: /* @__PURE__ */
|
|
104
|
+
/* @__PURE__ */ w(o, {
|
|
105
|
+
label: O("ggnomad.dashboard.stats.bookings", "Bookings"),
|
|
106
|
+
value: Number(F?.bookings ?? 0).toLocaleString(),
|
|
107
|
+
icon: /* @__PURE__ */ w(d, {})
|
|
94
108
|
})
|
|
95
109
|
]
|
|
96
110
|
}),
|
|
97
|
-
/* @__PURE__ */
|
|
111
|
+
/* @__PURE__ */ T(b, { children: [/* @__PURE__ */ T("div", {
|
|
98
112
|
className: "grid gap-4 sm:grid-cols-3",
|
|
99
113
|
children: [
|
|
100
|
-
/* @__PURE__ */
|
|
114
|
+
/* @__PURE__ */ T("div", { children: [/* @__PURE__ */ w("p", {
|
|
101
115
|
className: "text-sm text-text-secondary",
|
|
102
|
-
children:
|
|
103
|
-
}), /* @__PURE__ */
|
|
116
|
+
children: O("ggnomad.dashboard.inventory.properties", "Properties")
|
|
117
|
+
}), /* @__PURE__ */ w("p", {
|
|
104
118
|
className: "mt-1 text-2xl font-semibold tabular-nums text-text-primary",
|
|
105
|
-
children:
|
|
119
|
+
children: M.data?.ggnomadProperties?.length ?? 0
|
|
106
120
|
})] }),
|
|
107
|
-
/* @__PURE__ */
|
|
121
|
+
/* @__PURE__ */ T("div", { children: [/* @__PURE__ */ w("p", {
|
|
108
122
|
className: "text-sm text-text-secondary",
|
|
109
|
-
children:
|
|
110
|
-
}), /* @__PURE__ */
|
|
123
|
+
children: O("ggnomad.dashboard.inventory.tours", "Tours")
|
|
124
|
+
}), /* @__PURE__ */ w("p", {
|
|
111
125
|
className: "mt-1 text-2xl font-semibold tabular-nums text-text-primary",
|
|
112
|
-
children:
|
|
126
|
+
children: N.data?.ggnomadTours?.length ?? 0
|
|
113
127
|
})] }),
|
|
114
|
-
/* @__PURE__ */
|
|
128
|
+
/* @__PURE__ */ T("div", { children: [/* @__PURE__ */ w("p", {
|
|
115
129
|
className: "text-sm text-text-secondary",
|
|
116
|
-
children:
|
|
117
|
-
}), /* @__PURE__ */
|
|
130
|
+
children: O("ggnomad.dashboard.inventory.events", "Events")
|
|
131
|
+
}), /* @__PURE__ */ w("p", {
|
|
118
132
|
className: "mt-1 text-2xl font-semibold tabular-nums text-text-primary",
|
|
119
|
-
children:
|
|
133
|
+
children: P.data?.ggnomadEvents?.length ?? 0
|
|
120
134
|
})] })
|
|
121
135
|
]
|
|
122
|
-
}), /* @__PURE__ */
|
|
136
|
+
}), /* @__PURE__ */ w("p", {
|
|
123
137
|
className: "mt-4 text-sm text-text-secondary",
|
|
124
|
-
children:
|
|
138
|
+
children: O("ggnomad.dashboard.inventory.liveDataNotice", "Data is live from `wspace-ggnomad-svc` GraphQL APIs.")
|
|
125
139
|
})] }),
|
|
126
|
-
/* @__PURE__ */
|
|
127
|
-
title:
|
|
128
|
-
children:
|
|
140
|
+
/* @__PURE__ */ w(c, {
|
|
141
|
+
title: O("ggnomad.dashboard.sections.recentBookings", "Recent bookings"),
|
|
142
|
+
children: L.length === 0 ? /* @__PURE__ */ w(x, {
|
|
129
143
|
illustration: "empty-data",
|
|
130
|
-
title:
|
|
131
|
-
description:
|
|
132
|
-
}) : /* @__PURE__ */
|
|
144
|
+
title: O("ggnomad.dashboard.recentBookings.emptyTitle", "No recent bookings"),
|
|
145
|
+
description: O("ggnomad.dashboard.recentBookings.emptyDescription", "New bookings across travel, hospitality and tourism will appear here.")
|
|
146
|
+
}) : /* @__PURE__ */ w("ul", {
|
|
133
147
|
className: "space-y-2 text-sm",
|
|
134
|
-
children:
|
|
148
|
+
children: L.map((e) => /* @__PURE__ */ T("li", {
|
|
135
149
|
className: "flex items-center justify-between gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-3",
|
|
136
|
-
children: [/* @__PURE__ */
|
|
150
|
+
children: [/* @__PURE__ */ T("span", {
|
|
137
151
|
className: "min-w-0 truncate text-text-primary",
|
|
138
152
|
children: [
|
|
139
153
|
e.referenceCode,
|
|
140
154
|
" — ",
|
|
141
155
|
e.itemName
|
|
142
156
|
]
|
|
143
|
-
}), /* @__PURE__ */
|
|
157
|
+
}), /* @__PURE__ */ w(s, { status: e.status })]
|
|
144
158
|
}, e.id))
|
|
145
159
|
})
|
|
160
|
+
}),
|
|
161
|
+
/* @__PURE__ */ w(c, {
|
|
162
|
+
title: O("ggnomad.dashboard.sections.revenueByCategory", "Revenue by category"),
|
|
163
|
+
children: R.length === 0 ? /* @__PURE__ */ T("div", {
|
|
164
|
+
className: "flex flex-col items-center gap-2 py-8 text-center",
|
|
165
|
+
children: [
|
|
166
|
+
/* @__PURE__ */ w("span", {
|
|
167
|
+
className: "flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary",
|
|
168
|
+
children: /* @__PURE__ */ w(p, { className: "size-5" })
|
|
169
|
+
}),
|
|
170
|
+
/* @__PURE__ */ w("p", {
|
|
171
|
+
className: "text-sm font-medium text-text-primary",
|
|
172
|
+
children: O("ggnomad.dashboard.empty.title", "Revenue data coming soon")
|
|
173
|
+
}),
|
|
174
|
+
/* @__PURE__ */ w("p", {
|
|
175
|
+
className: "max-w-sm text-sm text-text-secondary",
|
|
176
|
+
children: O("ggnomad.dashboard.empty.description", "Revenue by category appears here as bookings come in across stays, tours and events.")
|
|
177
|
+
})
|
|
178
|
+
]
|
|
179
|
+
}) : /* @__PURE__ */ w("ul", {
|
|
180
|
+
className: "flex flex-col gap-4",
|
|
181
|
+
children: R.map((e, t) => {
|
|
182
|
+
let n = z > 0 ? Math.max(e.value / z * 100, 2) : 0;
|
|
183
|
+
return /* @__PURE__ */ T("li", { children: [/* @__PURE__ */ T("div", {
|
|
184
|
+
className: "flex items-baseline justify-between gap-3",
|
|
185
|
+
children: [/* @__PURE__ */ w("span", {
|
|
186
|
+
className: "truncate text-sm font-medium text-text-primary",
|
|
187
|
+
children: e.label
|
|
188
|
+
}), /* @__PURE__ */ T("span", {
|
|
189
|
+
className: "flex-shrink-0 text-sm tabular-nums text-text-secondary",
|
|
190
|
+
children: ["$", e.value.toLocaleString()]
|
|
191
|
+
})]
|
|
192
|
+
}), /* @__PURE__ */ w("div", {
|
|
193
|
+
className: "mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken",
|
|
194
|
+
children: /* @__PURE__ */ w("div", {
|
|
195
|
+
className: "h-full rounded-full transition-[width] duration-500 ease-out",
|
|
196
|
+
style: {
|
|
197
|
+
width: `${n}%`,
|
|
198
|
+
backgroundColor: E[t % E.length]
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
})] }, e.label);
|
|
202
|
+
})
|
|
203
|
+
})
|
|
146
204
|
})
|
|
147
205
|
]
|
|
148
206
|
});
|
|
149
207
|
}
|
|
150
208
|
//#endregion
|
|
151
|
-
export {
|
|
209
|
+
export { O as DashboardPage };
|
|
152
210
|
|
|
153
211
|
//# sourceMappingURL=DashboardPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/ggnomad/pages/DashboardPage.tsx"],"sourcesContent":["import { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Building, CalendarCheck, Compass, DollarSign, Hotel, Search, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_OVERVIEW_QUERY,\n GGNOMAD_BOOKINGS_QUERY,\n GGNOMAD_PROPERTIES_QUERY,\n GGNOMAD_TOURS_QUERY,\n GGNOMAD_EVENTS_QUERY,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard, StatusBadge } from '../components';\n\nexport function DashboardPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const overview = useQuery(\n GGNOMAD_OVERVIEW_QUERY as TypedDocumentNode<\n {\n ggnomadOverviewStats: {\n users: number;\n listings: number;\n bookings: number;\n monthlyRevenue: number;\n };\n },\n Record<string, never>\n >\n );\n const bookings = useQuery(\n GGNOMAD_BOOKINGS_QUERY as TypedDocumentNode<\n {\n ggnomadBookings: Array<{\n id: string;\n referenceCode: string;\n itemName: string;\n status: string;\n }>;\n },\n { first: number; skip: number }\n >,\n {\n variables: { first: 5, skip: 0 },\n }\n );\n const properties = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n { ggnomadProperties: Array<{ id: string }> },\n { first: number; skip: number }\n >,\n { variables: { first: 4, skip: 0 } }\n );\n const tours = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<\n { ggnomadTours: Array<{ id: string }> },\n { first: number; skip: number }\n >,\n {\n variables: { first: 4, skip: 0 },\n }\n );\n const events = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n { ggnomadEvents: Array<{ id: string }> },\n { first: number; skip: number }\n >,\n {\n variables: { first: 4, skip: 0 },\n }\n );\n\n const stats = overview.data?.ggnomadOverviewStats;\n const recentBookings = bookings.data?.ggnomadBookings ?? [];\n\n return (\n <PageShell\n hero\n title={t('ggnomad.dashboard.title', 'GGNomad Dashboard')}\n titleAccent={t('ggnomad.dashboard.title.accent', 'GGNomad')}\n subtitle={t(\n 'ggnomad.dashboard.subtitle',\n 'Unified travel, hospitality, tourism and entertainment operations'\n )}\n >\n <PagePurpose icon={<Compass className=\"size-4\" />}>\n {t(\n 'ggnomad.dashboard.purpose',\n 'Your command centre for the whole GGNomad marketplace — see live revenue, users, listings and bookings across travel, hospitality, tourism and entertainment in one place, and jump straight into the area you want to work on.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-dashboard\"\n steps={[\n {\n id: 'search',\n label: t('ggnomad.dashboard.next.search', 'Search the catalogue'),\n description: t(\n 'ggnomad.dashboard.next.searchDesc',\n 'Find inventory across every domain at once.'\n ),\n icon: <Search className=\"size-4\" />,\n onClick: () => navigate('search'),\n },\n {\n id: 'stays',\n label: t('ggnomad.dashboard.next.stays', 'Browse places to stay'),\n description: t(\n 'ggnomad.dashboard.next.staysDesc',\n 'Explore hospitality listings and open a property.'\n ),\n icon: <Hotel className=\"size-4\" />,\n onClick: () => navigate('hospitality'),\n },\n {\n id: 'tourism',\n label: t('ggnomad.dashboard.next.tourism', 'Discover tours & activities'),\n description: t(\n 'ggnomad.dashboard.next.tourismDesc',\n 'Plan what to do at each destination.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate('tourism'),\n },\n {\n id: 'bookings',\n label: t('ggnomad.dashboard.next.bookings', 'Manage bookings'),\n description: t(\n 'ggnomad.dashboard.next.bookingsDesc',\n 'Track and cancel reservations in real time.'\n ),\n icon: <CalendarCheck className=\"size-4\" />,\n onClick: () => navigate('bookings'),\n },\n ]}\n />\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.dashboard.stats.monthlyRevenue', 'Monthly Revenue')}\n value={`$${Number(stats?.monthlyRevenue ?? 0).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.users', 'Users')}\n value={Number(stats?.users ?? 0).toLocaleString()}\n icon={<Users />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.listings', 'Listings')}\n value={Number(stats?.listings ?? 0).toLocaleString()}\n icon={<Building />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.bookings', 'Bookings')}\n value={Number(stats?.bookings ?? 0).toLocaleString()}\n icon={<CalendarCheck />}\n />\n </section>\n\n <EmphasisPanel>\n <div className=\"grid gap-4 sm:grid-cols-3\">\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.properties', 'Properties')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {properties.data?.ggnomadProperties?.length ?? 0}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.tours', 'Tours')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {tours.data?.ggnomadTours?.length ?? 0}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.events', 'Events')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {events.data?.ggnomadEvents?.length ?? 0}\n </p>\n </div>\n </div>\n <p className=\"mt-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.dashboard.inventory.liveDataNotice',\n 'Data is live from `wspace-ggnomad-svc` GraphQL APIs.'\n )}\n </p>\n </EmphasisPanel>\n\n <SectionCard title={t('ggnomad.dashboard.sections.recentBookings', 'Recent bookings')}>\n {recentBookings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.dashboard.recentBookings.emptyTitle', 'No recent bookings')}\n description={t(\n 'ggnomad.dashboard.recentBookings.emptyDescription',\n 'New bookings across travel, hospitality and tourism will appear here.'\n )}\n />\n ) : (\n <ul className=\"space-y-2 text-sm\">\n {recentBookings.map((booking) => (\n <li\n key={booking.id}\n className=\"flex items-center justify-between gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-3\"\n >\n <span className=\"min-w-0 truncate text-text-primary\">\n {booking.referenceCode} — {booking.itemName}\n </span>\n <StatusBadge status={booking.status} />\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAsBA,SAAgB,IAAgB;CAC9B,IAAM,EAAE,SAAM,GAAS,EACjB,IAAW,GAAa,EACxB,IAAW,EACf,EAWD,EACK,IAAW,EACf,GAWA,EACE,WAAW;EAAE,OAAO;EAAG,MAAM;EAAG,EACjC,CACF,EACK,IAAa,EACjB,GAIA,EAAE,WAAW;EAAE,OAAO;EAAG,MAAM;EAAG,EAAE,CACrC,EACK,IAAQ,EACZ,GAIA,EACE,WAAW;EAAE,OAAO;EAAG,MAAM;EAAG,EACjC,CACF,EACK,IAAS,EACb,GAIA,EACE,WAAW;EAAE,OAAO;EAAG,MAAM;EAAG,EACjC,CACF,EAEK,IAAQ,EAAS,MAAM,sBACvB,IAAiB,EAAS,MAAM,mBAAmB,EAAE;AAE3D,QACE,kBAAC,GAAD;EACE,MAAA;EACA,OAAO,EAAE,2BAA2B,oBAAoB;EACxD,aAAa,EAAE,kCAAkC,UAAU;EAC3D,UAAU,EACR,8BACA,oEACD;YAPH;GASE,kBAAC,GAAD;IAAa,MAAM,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;cAC9C,EACC,6BACA,kOACD;IACW,CAAA;GAEd,kBAAC,GAAD;IACE,YAAW;IACX,OAAO;KACL;MACE,IAAI;MACJ,OAAO,EAAE,iCAAiC,uBAAuB;MACjE,aAAa,EACX,qCACA,8CACD;MACD,MAAM,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;MACnC,eAAe,EAAS,SAAS;MAClC;KACD;MACE,IAAI;MACJ,OAAO,EAAE,gCAAgC,wBAAwB;MACjE,aAAa,EACX,oCACA,oDACD;MACD,MAAM,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;MAClC,eAAe,EAAS,cAAc;MACvC;KACD;MACE,IAAI;MACJ,OAAO,EAAE,kCAAkC,8BAA8B;MACzE,aAAa,EACX,sCACA,uCACD;MACD,MAAM,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;MACpC,eAAe,EAAS,UAAU;MACnC;KACD;MACE,IAAI;MACJ,OAAO,EAAE,mCAAmC,kBAAkB;MAC9D,aAAa,EACX,uCACA,8CACD;MACD,MAAM,kBAAC,GAAD,EAAe,WAAU,UAAW,CAAA;MAC1C,eAAe,EAAS,WAAW;MACpC;KACF;IACD,CAAA;GAEF,kBAAC,WAAD;IAAS,WAAU;cAAnB;KACE,kBAAC,GAAD;MACE,MAAA;MACA,OAAO,EAAE,0CAA0C,kBAAkB;MACrE,OAAO,IAAI,OAAO,GAAO,kBAAkB,EAAE,CAAC,gBAAgB;MAC9D,MAAM,kBAAC,GAAD,EAAc,CAAA;MACpB,CAAA;KACF,kBAAC,GAAD;MACE,OAAO,EAAE,iCAAiC,QAAQ;MAClD,OAAO,OAAO,GAAO,SAAS,EAAE,CAAC,gBAAgB;MACjD,MAAM,kBAAC,GAAD,EAAS,CAAA;MACf,CAAA;KACF,kBAAC,GAAD;MACE,OAAO,EAAE,oCAAoC,WAAW;MACxD,OAAO,OAAO,GAAO,YAAY,EAAE,CAAC,gBAAgB;MACpD,MAAM,kBAAC,GAAD,EAAY,CAAA;MAClB,CAAA;KACF,kBAAC,GAAD;MACE,OAAO,EAAE,oCAAoC,WAAW;MACxD,OAAO,OAAO,GAAO,YAAY,EAAE,CAAC,gBAAgB;MACpD,MAAM,kBAAC,GAAD,EAAiB,CAAA;MACvB,CAAA;KACM;;GAEV,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,0CAA0C,aAAa;MACxD,CAAA,EACJ,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAW,MAAM,mBAAmB,UAAU;MAC7C,CAAA,CACA,EAAA,CAAA;KACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,qCAAqC,QAAQ;MAC9C,CAAA,EACJ,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAM,MAAM,cAAc,UAAU;MACnC,CAAA,CACA,EAAA,CAAA;KACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,sCAAsC,SAAS;MAChD,CAAA,EACJ,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAO,MAAM,eAAe,UAAU;MACrC,CAAA,CACA,EAAA,CAAA;KACF;OACN,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,8CACA,uDACD;IACC,CAAA,CACU,EAAA,CAAA;GAEhB,kBAAC,GAAD;IAAa,OAAO,EAAE,6CAA6C,kBAAkB;cAClF,EAAe,WAAW,IACzB,kBAAC,GAAD;KACE,cAAa;KACb,OAAO,EAAE,+CAA+C,qBAAqB;KAC7E,aAAa,EACX,qDACA,wEACD;KACD,CAAA,GAEF,kBAAC,MAAD;KAAI,WAAU;eACX,EAAe,KAAK,MACnB,kBAAC,MAAD;MAEE,WAAU;gBAFZ,CAIE,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACG,EAAQ;QAAc;QAAI,EAAQ;QAC9B;UACP,kBAAC,GAAD,EAAa,QAAQ,EAAQ,QAAU,CAAA,CACpC;QAPE,EAAQ,GAOV,CACL;KACC,CAAA;IAEK,CAAA;GACJ"}
|
|
1
|
+
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/ggnomad/pages/DashboardPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Building, CalendarCheck, Compass, DollarSign, Hotel, Search, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_OVERVIEW_QUERY,\n GGNOMAD_BOOKINGS_QUERY,\n GGNOMAD_PROPERTIES_QUERY,\n GGNOMAD_TOURS_QUERY,\n GGNOMAD_EVENTS_QUERY,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard, StatusBadge } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst humanizeCategory = (key: string): string =>\n key\n .replace(/[_-]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (char) => char.toUpperCase()) || key;\n\nexport function DashboardPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const overview = useQuery(\n GGNOMAD_OVERVIEW_QUERY as TypedDocumentNode<\n {\n ggnomadOverviewStats: {\n users: number;\n listings: number;\n bookings: number;\n monthlyRevenue: number;\n };\n ggnomadAnalytics: {\n totalRevenue: number;\n totalBookings: number;\n totalUsers: number;\n conversionRate: number;\n revenueByCategory: Record<string, unknown>;\n };\n },\n Record<string, never>\n >\n );\n const bookings = useQuery(\n GGNOMAD_BOOKINGS_QUERY as TypedDocumentNode<\n {\n ggnomadBookings: Array<{\n id: string;\n referenceCode: string;\n itemName: string;\n status: string;\n }>;\n },\n { first: number; skip: number }\n >,\n {\n variables: { first: 5, skip: 0 },\n }\n );\n const properties = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n { ggnomadProperties: Array<{ id: string }> },\n { first: number; skip: number }\n >,\n { variables: { first: 4, skip: 0 } }\n );\n const tours = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<\n { ggnomadTours: Array<{ id: string }> },\n { first: number; skip: number }\n >,\n {\n variables: { first: 4, skip: 0 },\n }\n );\n const events = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n { ggnomadEvents: Array<{ id: string }> },\n { first: number; skip: number }\n >,\n {\n variables: { first: 4, skip: 0 },\n }\n );\n\n const stats = overview.data?.ggnomadOverviewStats;\n const analytics = overview.data?.ggnomadAnalytics;\n const recentBookings = bookings.data?.ggnomadBookings ?? [];\n\n const categories = useMemo(() => {\n const raw = analytics?.revenueByCategory ?? {};\n return Object.entries(raw)\n .map(([key, value]) => ({ label: humanizeCategory(key), value: Number(value) }))\n .filter((entry) => Number.isFinite(entry.value) && entry.value > 0)\n .sort((a, b) => b.value - a.value);\n }, [analytics?.revenueByCategory]);\n\n const maxCategory = categories.length > 0 ? categories[0].value : 0;\n\n return (\n <PageShell\n hero\n title={t('ggnomad.dashboard.title', 'GGNomad Dashboard')}\n titleAccent={t('ggnomad.dashboard.title.accent', 'GGNomad')}\n subtitle={t(\n 'ggnomad.dashboard.subtitle',\n 'Unified travel, hospitality, tourism and entertainment operations'\n )}\n >\n <PagePurpose icon={<Compass className=\"size-4\" />}>\n {t(\n 'ggnomad.dashboard.purpose',\n 'Your command centre for the whole GGNomad marketplace — see live revenue, users, listings and bookings across travel, hospitality, tourism and entertainment in one place, and jump straight into the area you want to work on.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-dashboard\"\n steps={[\n {\n id: 'search',\n label: t('ggnomad.dashboard.next.search', 'Search the catalogue'),\n description: t(\n 'ggnomad.dashboard.next.searchDesc',\n 'Find inventory across every domain at once.'\n ),\n icon: <Search className=\"size-4\" />,\n onClick: () => navigate('search'),\n },\n {\n id: 'stays',\n label: t('ggnomad.dashboard.next.stays', 'Browse places to stay'),\n description: t(\n 'ggnomad.dashboard.next.staysDesc',\n 'Explore hospitality listings and open a property.'\n ),\n icon: <Hotel className=\"size-4\" />,\n onClick: () => navigate('hospitality'),\n },\n {\n id: 'tourism',\n label: t('ggnomad.dashboard.next.tourism', 'Discover tours & activities'),\n description: t(\n 'ggnomad.dashboard.next.tourismDesc',\n 'Plan what to do at each destination.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate('tourism'),\n },\n {\n id: 'bookings',\n label: t('ggnomad.dashboard.next.bookings', 'Manage bookings'),\n description: t(\n 'ggnomad.dashboard.next.bookingsDesc',\n 'Track and cancel reservations in real time.'\n ),\n icon: <CalendarCheck className=\"size-4\" />,\n onClick: () => navigate('bookings'),\n },\n ]}\n />\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.dashboard.stats.monthlyRevenue', 'Monthly Revenue')}\n value={`$${Number(stats?.monthlyRevenue ?? 0).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.users', 'Users')}\n value={Number(stats?.users ?? 0).toLocaleString()}\n icon={<Users />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.listings', 'Listings')}\n value={Number(stats?.listings ?? 0).toLocaleString()}\n icon={<Building />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.bookings', 'Bookings')}\n value={Number(stats?.bookings ?? 0).toLocaleString()}\n icon={<CalendarCheck />}\n />\n </section>\n\n <EmphasisPanel>\n <div className=\"grid gap-4 sm:grid-cols-3\">\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.properties', 'Properties')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {properties.data?.ggnomadProperties?.length ?? 0}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.tours', 'Tours')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {tours.data?.ggnomadTours?.length ?? 0}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.events', 'Events')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {events.data?.ggnomadEvents?.length ?? 0}\n </p>\n </div>\n </div>\n <p className=\"mt-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.dashboard.inventory.liveDataNotice',\n 'Data is live from `wspace-ggnomad-svc` GraphQL APIs.'\n )}\n </p>\n </EmphasisPanel>\n\n <SectionCard title={t('ggnomad.dashboard.sections.recentBookings', 'Recent bookings')}>\n {recentBookings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.dashboard.recentBookings.emptyTitle', 'No recent bookings')}\n description={t(\n 'ggnomad.dashboard.recentBookings.emptyDescription',\n 'New bookings across travel, hospitality and tourism will appear here.'\n )}\n />\n ) : (\n <ul className=\"space-y-2 text-sm\">\n {recentBookings.map((booking) => (\n <li\n key={booking.id}\n className=\"flex items-center justify-between gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-3\"\n >\n <span className=\"min-w-0 truncate text-text-primary\">\n {booking.referenceCode} — {booking.itemName}\n </span>\n <StatusBadge status={booking.status} />\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.dashboard.sections.revenueByCategory', 'Revenue by category')}>\n {categories.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <DollarSign className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.dashboard.empty.title', 'Revenue data coming soon')}\n </p>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {t(\n 'ggnomad.dashboard.empty.description',\n 'Revenue by category appears here as bookings come in across stays, tours and events.'\n )}\n </p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {categories.map((category, index) => {\n const pct = maxCategory > 0 ? Math.max((category.value / maxCategory) * 100, 2) : 0;\n return (\n <li key={category.label}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {category.label}\n </span>\n <span className=\"flex-shrink-0 text-sm tabular-nums text-text-secondary\">\n ${category.value.toLocaleString()}\n </span>\n </div>\n <div className=\"mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full transition-[width] duration-500 ease-out\"\n style={{\n width: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </SectionCard>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|