medusa-analytics 0.0.2 → 0.0.4

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.
@@ -3,7 +3,40 @@ import { useState, useEffect } from "react";
3
3
  import { defineRouteConfig } from "@medusajs/admin-sdk";
4
4
  import { ChartBar } from "@medusajs/icons";
5
5
  import { Text, Container, Heading, Label, Button } from "@medusajs/ui";
6
- import { ResponsiveContainer, LineChart, CartesianGrid, XAxis, YAxis, Tooltip, Line, PieChart, Pie, Cell, Legend } from "recharts";
6
+ import { ResponsiveContainer, PieChart, Pie, Cell, Tooltip, Legend, LineChart, CartesianGrid, XAxis, YAxis, Line } from "recharts";
7
+ function OrdersOverTimeTooltip({
8
+ active,
9
+ payload,
10
+ label
11
+ }) {
12
+ var _a, _b;
13
+ if (!active || !(payload == null ? void 0 : payload.length)) return null;
14
+ const row = (_a = payload[0]) == null ? void 0 : _a.payload;
15
+ const displayLabel = (row == null ? void 0 : row.label) ?? (label ? new Date(label).toLocaleDateString() : label);
16
+ const value = ((_b = payload[0]) == null ? void 0 : _b.value) ?? 0;
17
+ return /* @__PURE__ */ jsxs(
18
+ "div",
19
+ {
20
+ style: {
21
+ backgroundColor: "rgb(254, 233, 233)",
22
+ color: "rgba(5, 0, 0, 0)",
23
+ border: "1px solidrgb(0, 0, 0)",
24
+ borderRadius: "8px",
25
+ padding: "12px 16px",
26
+ boxShadow: "0 4px 14px rgba(102, 102, 102, 0.99)",
27
+ fontSize: "14px",
28
+ lineHeight: 1.5
29
+ },
30
+ children: [
31
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, marginBottom: "6px", color: "#111827" }, children: displayLabel }),
32
+ /* @__PURE__ */ jsxs("div", { style: { color: "#374151" }, children: [
33
+ "Orders: ",
34
+ /* @__PURE__ */ jsx("strong", { style: { color: "#1f2937" }, children: value })
35
+ ] })
36
+ ]
37
+ }
38
+ );
39
+ }
7
40
  const STATUS_COLORS = {
8
41
  pending: "#F59E0B",
9
42
  confirmed: "#3B82F6",
@@ -28,25 +61,32 @@ function formatCurrency(value) {
28
61
  maximumFractionDigits: 0
29
62
  }).format(value);
30
63
  }
64
+ const SUMMARY_PERIODS = [
65
+ { value: "0", label: "Today's orders" },
66
+ { value: "7", label: "Last 7 days" },
67
+ { value: "30", label: "Last 30 days" },
68
+ { value: "90", label: "Last 90 days" }
69
+ ];
70
+ const OVER_TIME_PERIODS = [
71
+ { value: "daily", label: "Daily" },
72
+ { value: "weekly", label: "Weekly" },
73
+ { value: "monthly", label: "Monthly" }
74
+ ];
31
75
  function OrdersDashboard() {
32
76
  const [data, setData] = useState(null);
33
77
  const [loading, setLoading] = useState(true);
34
78
  const [error, setError] = useState(null);
35
79
  const [filter, setFilter] = useState("7");
80
+ const [dailyOrders, setDailyOrders] = useState([]);
81
+ const [overTimePeriod, setOverTimePeriod] = useState("weekly");
82
+ const [overTimeLoading, setOverTimeLoading] = useState(true);
83
+ const [overTimeError, setOverTimeError] = useState(null);
36
84
  useEffect(() => {
37
85
  let cancelled = false;
38
86
  setLoading(true);
39
87
  setError(null);
40
88
  const url = `/admin/analytics/orders-summary?days=${filter}`;
41
- if (typeof fetch !== "undefined") {
42
- fetch("http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288", { method: "POST", headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "21bff8" }, body: JSON.stringify({ sessionId: "21bff8", location: "OrdersDashboard.tsx:useEffect", message: "OrdersDashboard fetch", data: { filter, url }, timestamp: Date.now(), hypothesisId: "C" }) }).catch(() => {
43
- });
44
- }
45
89
  fetch(url).then((res) => {
46
- if (typeof fetch !== "undefined") {
47
- fetch("http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288", { method: "POST", headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "21bff8" }, body: JSON.stringify({ sessionId: "21bff8", location: "OrdersDashboard.tsx:fetch.then", message: "API response", data: { status: res.status, statusText: res.statusText, url: res.url }, timestamp: Date.now(), hypothesisId: "C" }) }).catch(() => {
48
- });
49
- }
50
90
  if (!res.ok) throw new Error(res.statusText);
51
91
  return res.json();
52
92
  }).then((body) => {
@@ -60,6 +100,25 @@ function OrdersDashboard() {
60
100
  cancelled = true;
61
101
  };
62
102
  }, [filter]);
103
+ useEffect(() => {
104
+ let cancelled = false;
105
+ setOverTimeLoading(true);
106
+ setOverTimeError(null);
107
+ const url = `/admin/analytics/orders-over-time?period=${overTimePeriod}`;
108
+ fetch(url).then((res) => {
109
+ if (!res.ok) throw new Error(res.statusText);
110
+ return res.json();
111
+ }).then((body) => {
112
+ if (!cancelled) setDailyOrders(body.dailyOrders ?? []);
113
+ }).catch((e) => {
114
+ if (!cancelled) setOverTimeError(e instanceof Error ? e.message : String(e));
115
+ }).finally(() => {
116
+ if (!cancelled) setOverTimeLoading(false);
117
+ });
118
+ return () => {
119
+ cancelled = true;
120
+ };
121
+ }, [overTimePeriod]);
63
122
  if (loading) {
64
123
  return /* @__PURE__ */ jsx(
65
124
  "div",
@@ -84,24 +143,19 @@ function OrdersDashboard() {
84
143
  return /* @__PURE__ */ jsxs("div", { className: "space-y-8", children: [
85
144
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4", children: [
86
145
  /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Orders Analytics" }),
87
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
146
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
88
147
  /* @__PURE__ */ jsx(Label, { htmlFor: "analytics-period", className: "text-ui-fg-muted text-sm", children: "Period" }),
89
- /* @__PURE__ */ jsxs(
148
+ /* @__PURE__ */ jsx(
90
149
  "select",
91
150
  {
92
151
  id: "analytics-period",
93
152
  value: filter,
94
153
  onChange: (e) => setFilter(e.target.value),
95
154
  className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
96
- children: [
97
- /* @__PURE__ */ jsx("option", { value: "0", children: "Today's orders" }),
98
- /* @__PURE__ */ jsx("option", { value: "7", children: "Last 7 days" }),
99
- /* @__PURE__ */ jsx("option", { value: "30", children: "Last 30 days" }),
100
- /* @__PURE__ */ jsx("option", { value: "90", children: "Last 90 days" })
101
- ]
155
+ children: SUMMARY_PERIODS.map((p) => /* @__PURE__ */ jsx("option", { value: p.value, children: p.label }, p.value))
102
156
  }
103
157
  )
104
- ] })
158
+ ] }) })
105
159
  ] }),
106
160
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4", children: [
107
161
  /* @__PURE__ */ jsxs(Container, { className: "p-4", children: [
@@ -132,11 +186,56 @@ function OrdersDashboard() {
132
186
  ] })
133
187
  ] }),
134
188
  /* @__PURE__ */ jsxs(Container, { className: "p-6", children: [
135
- /* @__PURE__ */ jsx(Heading, { level: "h3", className: "mb-4", children: "Orders over time" }),
136
- /* @__PURE__ */ jsx("div", { className: "h-72", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(
189
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "mb-4", children: "Orders by status" }),
190
+ /* @__PURE__ */ jsx("div", { className: "h-72", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(PieChart, { children: [
191
+ /* @__PURE__ */ jsx(
192
+ Pie,
193
+ {
194
+ data: pieData,
195
+ cx: "50%",
196
+ cy: "50%",
197
+ innerRadius: 60,
198
+ outerRadius: 100,
199
+ paddingAngle: 2,
200
+ dataKey: "value",
201
+ nameKey: "name",
202
+ label: ({ name, value }) => `${name}: ${value}`,
203
+ children: pieData.map((entry, index) => /* @__PURE__ */ jsx(Cell, { fill: entry.color }, `cell-${index}`))
204
+ }
205
+ ),
206
+ /* @__PURE__ */ jsx(Tooltip, { formatter: (value) => [value ?? 0, "Orders"] }),
207
+ /* @__PURE__ */ jsx(Legend, {})
208
+ ] }) }) })
209
+ ] }),
210
+ /* @__PURE__ */ jsxs(Container, { className: "p-6", children: [
211
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4 mb-4", children: [
212
+ /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Orders over time" }),
213
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
214
+ /* @__PURE__ */ jsx(Label, { htmlFor: "over-time-period", className: "text-ui-fg-muted text-sm", children: "Range" }),
215
+ /* @__PURE__ */ jsx(
216
+ "select",
217
+ {
218
+ id: "over-time-period",
219
+ value: overTimePeriod,
220
+ onChange: (e) => setOverTimePeriod(e.target.value),
221
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
222
+ children: OVER_TIME_PERIODS.map((p) => /* @__PURE__ */ jsx("option", { value: p.value, children: p.label }, p.value))
223
+ }
224
+ )
225
+ ] })
226
+ ] }),
227
+ /* @__PURE__ */ jsx("div", { className: "h-72", children: overTimeLoading ? /* @__PURE__ */ jsx(
228
+ "div",
229
+ {
230
+ className: "flex items-center justify-center h-full",
231
+ role: "status",
232
+ "aria-label": "Loading orders over time",
233
+ children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted", children: "Loading chart…" })
234
+ }
235
+ ) : overTimeError ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-danger", children: overTimeError }) }) : /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(
137
236
  LineChart,
138
237
  {
139
- data: data.dailyOrders,
238
+ data: dailyOrders,
140
239
  margin: { top: 8, right: 8, left: 8, bottom: 8 },
141
240
  children: [
142
241
  /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", className: "stroke-ui-border-base" }),
@@ -145,20 +244,16 @@ function OrdersDashboard() {
145
244
  {
146
245
  dataKey: "date",
147
246
  tick: { fontSize: 12 },
148
- tickFormatter: (v) => {
247
+ tickFormatter: (v, index) => {
248
+ const row = dailyOrders[index];
249
+ if (row == null ? void 0 : row.label) return row.label;
149
250
  const d = new Date(v);
150
251
  return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`;
151
252
  }
152
253
  }
153
254
  ),
154
255
  /* @__PURE__ */ jsx(YAxis, { tick: { fontSize: 12 } }),
155
- /* @__PURE__ */ jsx(
156
- Tooltip,
157
- {
158
- labelFormatter: (v) => new Date(v).toLocaleDateString(),
159
- formatter: (value) => [value ?? 0, "Orders"]
160
- }
161
- ),
256
+ /* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(OrdersOverTimeTooltip, {}) }),
162
257
  /* @__PURE__ */ jsx(
163
258
  Line,
164
259
  {
@@ -173,42 +268,311 @@ function OrdersDashboard() {
173
268
  ]
174
269
  }
175
270
  ) }) })
176
- ] }),
177
- /* @__PURE__ */ jsxs(Container, { className: "p-6", children: [
178
- /* @__PURE__ */ jsx(Heading, { level: "h3", className: "mb-4", children: "Orders by status" }),
179
- /* @__PURE__ */ jsx("div", { className: "h-72", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(PieChart, { children: [
271
+ ] })
272
+ ] });
273
+ }
274
+ function CustomersOverTimeTooltip({
275
+ active,
276
+ payload,
277
+ label
278
+ }) {
279
+ var _a, _b, _c;
280
+ if (!active || !(payload == null ? void 0 : payload.length)) return null;
281
+ const row = (_a = payload[0]) == null ? void 0 : _a.payload;
282
+ const displayLabel = (row == null ? void 0 : row.label) ?? (label ? new Date(label).toLocaleDateString() : label);
283
+ const registered = Math.floor(((_b = payload.find((p) => p.name === "Registered")) == null ? void 0 : _b.value) ?? 0);
284
+ const guest = Math.floor(((_c = payload.find((p) => p.name === "Guest")) == null ? void 0 : _c.value) ?? 0);
285
+ return /* @__PURE__ */ jsxs(
286
+ "div",
287
+ {
288
+ style: {
289
+ backgroundColor: "rgb(254, 254, 254)",
290
+ color: "#111827",
291
+ border: "1px solid rgb(229, 231, 235)",
292
+ borderRadius: "8px",
293
+ padding: "12px 16px",
294
+ boxShadow: "0 4px 14px rgba(0, 0, 0, 0.08)",
295
+ fontSize: "14px",
296
+ lineHeight: 1.5
297
+ },
298
+ children: [
299
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, marginBottom: "6px", color: "#111827" }, children: displayLabel }),
300
+ /* @__PURE__ */ jsxs("div", { style: { color: "#374151" }, children: [
301
+ "Registered: ",
302
+ /* @__PURE__ */ jsx("strong", { style: { color: "#1f2937" }, children: registered })
303
+ ] }),
304
+ /* @__PURE__ */ jsxs("div", { style: { color: "#374151" }, children: [
305
+ "Guest: ",
306
+ /* @__PURE__ */ jsx("strong", { style: { color: "#1f2937" }, children: guest })
307
+ ] })
308
+ ]
309
+ }
310
+ );
311
+ }
312
+ const COUNT_DAY_PRESETS = [
313
+ { value: "3", label: "Last 3 days" },
314
+ { value: "5", label: "Last 5 days" },
315
+ { value: "7", label: "Last 7 days" },
316
+ { value: "9", label: "Last 9 days" },
317
+ { value: "14", label: "Last 14 days" },
318
+ { value: "30", label: "Last 30 days" },
319
+ { value: "90", label: "Last 90 days" }
320
+ ];
321
+ const GRAPH_PERIODS = [
322
+ { value: "daily", label: "Daily" },
323
+ { value: "weekly", label: "Weekly" },
324
+ { value: "monthly", label: "Monthly" },
325
+ { value: "yearly", label: "Yearly" }
326
+ ];
327
+ const REGISTERED_COLOR = "var(--medusa-color-ui-fg-interactive)";
328
+ const GUEST_COLOR = "#6B7280";
329
+ function CustomersDashboard() {
330
+ const [data, setData] = useState(null);
331
+ const [loading, setLoading] = useState(true);
332
+ const [error, setError] = useState(null);
333
+ const [countDays, setCountDays] = useState("7");
334
+ const [series, setSeries] = useState([]);
335
+ const [graphPeriod, setGraphPeriod] = useState("weekly");
336
+ const [graphLoading, setGraphLoading] = useState(true);
337
+ const [graphError, setGraphError] = useState(null);
338
+ useEffect(() => {
339
+ let cancelled = false;
340
+ setLoading(true);
341
+ setError(null);
342
+ const url = `/admin/analytics/customers-summary?days=${countDays}`;
343
+ fetch(url).then((res) => {
344
+ if (!res.ok) throw new Error(res.statusText);
345
+ return res.json();
346
+ }).then((body) => {
347
+ if (!cancelled) setData(body);
348
+ }).catch((e) => {
349
+ if (!cancelled) setError(e instanceof Error ? e.message : String(e));
350
+ }).finally(() => {
351
+ if (!cancelled) setLoading(false);
352
+ });
353
+ return () => {
354
+ cancelled = true;
355
+ };
356
+ }, [countDays]);
357
+ useEffect(() => {
358
+ let cancelled = false;
359
+ setGraphLoading(true);
360
+ setGraphError(null);
361
+ const url = `/admin/analytics/customers-over-time?period=${graphPeriod}`;
362
+ fetch("http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288", {
363
+ method: "POST",
364
+ headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "4a7f37" },
365
+ body: JSON.stringify({
366
+ sessionId: "4a7f37",
367
+ location: "CustomersDashboard.tsx:fetch",
368
+ message: "customers-over-time fetch start",
369
+ data: { graphPeriod, url },
370
+ timestamp: Date.now(),
371
+ hypothesisId: "H1-H2"
372
+ })
373
+ }).catch(() => {
374
+ });
375
+ fetch(url).then((res) => {
376
+ if (!res.ok) throw new Error(res.statusText);
377
+ return res.json();
378
+ }).then((body) => {
379
+ var _a, _b;
380
+ const received = body.series ?? [];
381
+ fetch("http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288", {
382
+ method: "POST",
383
+ headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "4a7f37" },
384
+ body: JSON.stringify({
385
+ sessionId: "4a7f37",
386
+ location: "CustomersDashboard.tsx:response",
387
+ message: "customers-over-time response",
388
+ data: { graphPeriod, seriesLength: received.length, firstDate: (_a = received[0]) == null ? void 0 : _a.date, lastDate: (_b = received[received.length - 1]) == null ? void 0 : _b.date, cancelled },
389
+ timestamp: Date.now(),
390
+ hypothesisId: "H2-H5"
391
+ })
392
+ }).catch(() => {
393
+ });
394
+ if (!cancelled) setSeries(received);
395
+ }).catch((e) => {
396
+ if (!cancelled) setGraphError(e instanceof Error ? e.message : String(e));
397
+ }).finally(() => {
398
+ if (!cancelled) setGraphLoading(false);
399
+ });
400
+ return () => {
401
+ cancelled = true;
402
+ };
403
+ }, [graphPeriod]);
404
+ if (loading) {
405
+ return /* @__PURE__ */ jsx(
406
+ "div",
407
+ {
408
+ className: "flex items-center justify-center min-h-[320px]",
409
+ role: "status",
410
+ "aria-label": "Loading analytics",
411
+ children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted", children: "Loading…" })
412
+ }
413
+ );
414
+ }
415
+ if (error || !data) {
416
+ return /* @__PURE__ */ jsx(Container, { className: "p-6", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-danger", children: error ?? "Failed to load analytics" }) });
417
+ }
418
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-8", children: [
419
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4", children: [
420
+ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Customers Analytics" }),
421
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
180
422
  /* @__PURE__ */ jsx(
181
- Pie,
423
+ Label,
182
424
  {
183
- data: pieData,
184
- cx: "50%",
185
- cy: "50%",
186
- innerRadius: 60,
187
- outerRadius: 100,
188
- paddingAngle: 2,
189
- dataKey: "value",
190
- nameKey: "name",
191
- label: ({ name, value }) => `${name}: ${value}`,
192
- children: pieData.map((entry, index) => /* @__PURE__ */ jsx(Cell, { fill: entry.color }, `cell-${index}`))
425
+ htmlFor: "customers-count-period",
426
+ className: "text-ui-fg-muted text-sm",
427
+ children: "Period (counts)"
193
428
  }
194
429
  ),
195
- /* @__PURE__ */ jsx(Tooltip, { formatter: (value) => [value ?? 0, "Orders"] }),
196
- /* @__PURE__ */ jsx(Legend, {})
197
- ] }) }) })
430
+ /* @__PURE__ */ jsx(
431
+ "select",
432
+ {
433
+ id: "customers-count-period",
434
+ value: countDays,
435
+ onChange: (e) => setCountDays(e.target.value),
436
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
437
+ children: COUNT_DAY_PRESETS.map((p) => /* @__PURE__ */ jsx("option", { value: p.value, children: p.label }, p.value))
438
+ }
439
+ )
440
+ ] }) })
441
+ ] }),
442
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-3 gap-4", children: [
443
+ /* @__PURE__ */ jsxs(Container, { className: "p-4", children: [
444
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Guest customers" }),
445
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: Math.floor(data.guestCount).toLocaleString() })
446
+ ] }),
447
+ /* @__PURE__ */ jsxs(Container, { className: "p-4", children: [
448
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Registered customers" }),
449
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: Math.floor(data.registeredCount).toLocaleString() })
450
+ ] }),
451
+ /* @__PURE__ */ jsxs(Container, { className: "p-4", children: [
452
+ /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted text-sm", children: "Overall customers" }),
453
+ /* @__PURE__ */ jsx(Heading, { level: "h2", children: Math.floor(data.totalCount).toLocaleString() })
454
+ ] })
455
+ ] }),
456
+ /* @__PURE__ */ jsxs(Container, { className: "p-6", children: [
457
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4 mb-4", children: [
458
+ /* @__PURE__ */ jsx(Heading, { level: "h3", children: "Customers over time" }),
459
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
460
+ /* @__PURE__ */ jsx(
461
+ Label,
462
+ {
463
+ htmlFor: "customers-graph-period",
464
+ className: "text-ui-fg-muted text-sm",
465
+ children: "Range"
466
+ }
467
+ ),
468
+ /* @__PURE__ */ jsx(
469
+ "select",
470
+ {
471
+ id: "customers-graph-period",
472
+ value: graphPeriod,
473
+ onChange: (e) => setGraphPeriod(e.target.value),
474
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
475
+ children: GRAPH_PERIODS.map((p) => /* @__PURE__ */ jsx("option", { value: p.value, children: p.label }, p.value))
476
+ }
477
+ )
478
+ ] })
479
+ ] }),
480
+ /* @__PURE__ */ jsx("div", { className: "h-72", children: graphLoading ? /* @__PURE__ */ jsx(
481
+ "div",
482
+ {
483
+ className: "flex items-center justify-center h-full",
484
+ role: "status",
485
+ "aria-label": "Loading customers over time",
486
+ children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-muted", children: "Loading chart…" })
487
+ }
488
+ ) : graphError ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full", children: /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-danger", children: graphError }) }) : /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(
489
+ LineChart,
490
+ {
491
+ data: series,
492
+ margin: { top: 8, right: 8, left: 8, bottom: 8 },
493
+ children: [
494
+ /* @__PURE__ */ jsx(
495
+ CartesianGrid,
496
+ {
497
+ strokeDasharray: "3 3",
498
+ className: "stroke-ui-border-base"
499
+ }
500
+ ),
501
+ /* @__PURE__ */ jsx(
502
+ XAxis,
503
+ {
504
+ dataKey: "date",
505
+ tick: { fontSize: 12 },
506
+ tickFormatter: (v, index) => {
507
+ const row = series[index];
508
+ if (row == null ? void 0 : row.label) return row.label;
509
+ const d = new Date(v);
510
+ return `${d.getUTCMonth() + 1}/${d.getUTCDate()}`;
511
+ }
512
+ }
513
+ ),
514
+ /* @__PURE__ */ jsx(
515
+ YAxis,
516
+ {
517
+ tick: { fontSize: 12 },
518
+ tickFormatter: (v) => String(Math.floor(Number(v))),
519
+ allowDecimals: false
520
+ }
521
+ ),
522
+ /* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(CustomersOverTimeTooltip, {}) }),
523
+ /* @__PURE__ */ jsx(Legend, {}),
524
+ /* @__PURE__ */ jsx(
525
+ Line,
526
+ {
527
+ type: "monotone",
528
+ dataKey: "registered_count",
529
+ name: "Registered",
530
+ stroke: REGISTERED_COLOR,
531
+ strokeWidth: 2,
532
+ dot: { r: 3 }
533
+ }
534
+ ),
535
+ /* @__PURE__ */ jsx(
536
+ Line,
537
+ {
538
+ type: "monotone",
539
+ dataKey: "guest_count",
540
+ name: "Guest",
541
+ stroke: GUEST_COLOR,
542
+ strokeWidth: 2,
543
+ dot: { r: 3 }
544
+ }
545
+ )
546
+ ]
547
+ }
548
+ ) }) })
198
549
  ] })
199
550
  ] });
200
551
  }
201
- const ANALYTICS_MODULES = [{ id: "orders", label: "Orders" }];
552
+ const ANALYTICS_MODULES = [
553
+ { id: "orders", label: "Orders" },
554
+ { id: "customers", label: "Customers" }
555
+ ];
202
556
  const PLUGIN_VERSION = "0.2";
203
557
  const AnalyticsPage = () => {
204
558
  const [activeModule, setActiveModule] = useState("orders");
205
- if (typeof fetch !== "undefined") {
206
- fetch("http://127.0.0.1:7242/ingest/47f434f8-0643-48e5-bd1a-edfe05973288", { method: "POST", headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "21bff8" }, body: JSON.stringify({ sessionId: "21bff8", location: "page.tsx:AnalyticsPage", message: "Analytics page mounted", data: { pluginVersion: PLUGIN_VERSION, activeModule }, timestamp: Date.now(), hypothesisId: "A" }) }).catch(() => {
207
- });
208
- }
559
+ const [sidebarOpen, setSidebarOpen] = useState(false);
209
560
  return /* @__PURE__ */ jsxs("div", { className: "flex h-full bg-ui-bg-base", children: [
210
- /* @__PURE__ */ jsxs("aside", { className: "w-56 shrink-0 border-r border-ui-border-base bg-ui-bg-subtle p-4 flex flex-col", children: [
211
- /* @__PURE__ */ jsx(Heading, { level: "h3", className: "mb-3 text-ui-fg-subtle", children: "Analysis" }),
561
+ sidebarOpen && /* @__PURE__ */ jsxs("aside", { className: "w-56 shrink-0 border-r border-ui-border-base bg-ui-bg-subtle p-4 flex flex-col relative", children: [
562
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
563
+ /* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-ui-fg-subtle", children: "Analysis" }),
564
+ /* @__PURE__ */ jsx(
565
+ Button,
566
+ {
567
+ variant: "transparent",
568
+ size: "small",
569
+ className: "shrink-0",
570
+ onClick: () => setSidebarOpen(false),
571
+ "aria-label": "Close sidebar",
572
+ children: "×"
573
+ }
574
+ )
575
+ ] }),
212
576
  /* @__PURE__ */ jsx("nav", { className: "flex flex-col gap-1 flex-1", children: ANALYTICS_MODULES.map((m) => /* @__PURE__ */ jsx(
213
577
  Button,
214
578
  {
@@ -224,7 +588,20 @@ const AnalyticsPage = () => {
224
588
  PLUGIN_VERSION
225
589
  ] })
226
590
  ] }),
227
- /* @__PURE__ */ jsx("main", { className: "flex-1 overflow-auto", children: /* @__PURE__ */ jsx(Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsx("div", { className: "py-6 px-6", children: activeModule === "orders" && /* @__PURE__ */ jsx(OrdersDashboard, {}) }) }) })
591
+ !sidebarOpen && /* @__PURE__ */ jsx("div", { className: "shrink-0 border-r border-ui-border-base p-2", children: /* @__PURE__ */ jsx(
592
+ Button,
593
+ {
594
+ variant: "secondary",
595
+ size: "small",
596
+ onClick: () => setSidebarOpen(true),
597
+ "aria-label": "Show sidebar",
598
+ children: "Analysis"
599
+ }
600
+ ) }),
601
+ /* @__PURE__ */ jsx("main", { className: "flex-1 overflow-auto", children: /* @__PURE__ */ jsx(Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxs("div", { className: "py-6 px-6", children: [
602
+ activeModule === "orders" && /* @__PURE__ */ jsx(OrdersDashboard, {}),
603
+ activeModule === "customers" && /* @__PURE__ */ jsx(CustomersDashboard, {})
604
+ ] }) }) })
228
605
  ] });
229
606
  };
230
607
  const config = defineRouteConfig({