From 29347488fadb36c47692b57be8735e91ab4752ca Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Wed, 17 Jun 2026 19:04:55 +0300 Subject: [PATCH] frontend: replace Analyze "Live" card with a Patient-visits area chart The realtime "Live" hospital card is dropped in favour of a six-month patient visits area chart, aggregated client-side from real appointments. Renames the analysis.live.* i18n block to analysis.area.* and removes the dead live-hospital-chart component. Co-Authored-By: Claude Opus 4.8 --- .../components/analysis/analysis-view.tsx | 54 +++++++- .../analysis/live-hospital-chart.tsx | 128 ------------------ frontend/lib/i18n/locales/en/translation.json | 11 +- 3 files changed, 51 insertions(+), 142 deletions(-) delete mode 100644 frontend/components/analysis/live-hospital-chart.tsx diff --git a/frontend/components/analysis/analysis-view.tsx b/frontend/components/analysis/analysis-view.tsx index 79fb982..aa42698 100644 --- a/frontend/components/analysis/analysis-view.tsx +++ b/frontend/components/analysis/analysis-view.tsx @@ -4,7 +4,6 @@ import { type ReactNode, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { EarningsChart } from "@/components/analysis/earnings-chart"; -import { LiveHospitalChart } from "@/components/analysis/live-hospital-chart"; import { Area, AreaChart } from "@/components/charts/area-chart"; import { Bar } from "@/components/charts/bar"; import { BarChart } from "@/components/charts/bar-chart"; @@ -14,6 +13,7 @@ import { ChartTooltip } from "@/components/charts/tooltip"; import { XAxis } from "@/components/charts/x-axis"; import { Card } from "@/components/ui/card"; import { type Analytics, getAnalytics } from "@/lib/analytics"; +import { type Appointment, listAppointments } from "@/lib/appointments"; import { formatMoney } from "@/lib/invoices"; // Clinic analytics computed on the server from real data (patients, @@ -90,6 +90,7 @@ function ChartCard({ export function AnalysisView() { const { t } = useTranslation(); const [data, setData] = useState(null); + const [appointments, setAppointments] = useState([]); useEffect(() => { let active = true; @@ -100,6 +101,13 @@ export function AnalysisView() { .catch(() => { /* api-client redirects on 401; otherwise leave it loading */ }); + listAppointments() + .then((a) => { + if (active) setAppointments(a); + }) + .catch(() => { + /* leave the visits chart empty on failure */ + }); return () => { active = false; }; @@ -107,6 +115,28 @@ export function AnalysisView() { const n = (v: number | undefined) => String(v ?? 0); + // Patient visits per month over the last six months, aggregated from real + // appointments (no server-side monthly series exists yet). + const visitData = useMemo(() => { + const now = new Date(); + const months = Array.from({ length: 6 }, (_, i) => ({ + date: new Date(now.getFullYear(), now.getMonth() - (5 - i), 1), + visits: 0, + })); + for (const a of appointments) { + const d = new Date(`${a.date}T00:00:00`); + if (Number.isNaN(d.getTime())) continue; + const m = months.find( + (mo) => + mo.date.getFullYear() === d.getFullYear() && + mo.date.getMonth() === d.getMonth(), + ); + if (m) m.visits += 1; + } + return months; + }, [appointments]); + const visitTotal = visitData.reduce((sum, p) => sum + p.visits, 0); + // The area chart needs real Date x-values: synthesise one month per point, // ending with the current month. const monthData = useMemo(() => { @@ -147,17 +177,27 @@ export function AnalysisView() {

- {t("analysis.live.title")} + {t("analysis.area.title")}

- {t("analysis.live.subtitle")} + {t("analysis.area.subtitle")}

- - {t("analysis.live.label")} - - +
+ + {t("analysis.area.label")} + + + {visitTotal} + +
+ + + + + +
diff --git a/frontend/components/analysis/live-hospital-chart.tsx b/frontend/components/analysis/live-hospital-chart.tsx deleted file mode 100644 index 0a63b29..0000000 --- a/frontend/components/analysis/live-hospital-chart.tsx +++ /dev/null @@ -1,128 +0,0 @@ -"use client"; - -import { Pause, Play } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { - LiveLineChart, - type LiveLinePoint, -} from "@/components/charts/live-line-chart"; -import { LiveLine } from "@/components/charts/live-line"; -import { LiveYAxis } from "@/components/charts/live-y-axis"; -import { ChartTooltip } from "@/components/charts/tooltip"; -import { Button } from "@/components/ui/button"; -import { getLiveMetric } from "@/lib/analytics"; - -// The "Live" panel plots a REAL clinic signal — patients checked in today — by -// polling GET /api/analytics/live. It only runs while the clinician toggles it -// on, so the Analysis page stays idle by default. The metric changes slowly -// (only as people check in), so the line scrolls smoothly using the last value -// and refreshes from the server every few seconds. -const WINDOW_SECONDS = 30; -const REFETCH_EVERY_TICKS = 5; // poll the server every 5s (1s render ticks) - -export function LiveHospitalChart() { - const { t } = useTranslation(); - const [live, setLive] = useState(false); - const [data, setData] = useState([]); - const [value, setValue] = useState(0); - const valueRef = useRef(0); - - useEffect(() => { - if (!live) return; - let active = true; - - const refetch = () => - getLiveMetric() - .then((r) => { - if (!active) return; - valueRef.current = r.value; - setValue(r.value); - }) - .catch(() => { - /* keep the last value on a transient error */ - }); - - // Seed a short flat history from the first reading so the line draws at once. - refetch().finally(() => { - if (!active) return; - const now = Date.now() / 1000; - setData( - Array.from({ length: WINDOW_SECONDS }, (_, i) => ({ - time: now - (WINDOW_SECONDS - i), - value: valueRef.current, - })), - ); - }); - - let tick = 0; - const id = setInterval(() => { - tick += 1; - if (tick % REFETCH_EVERY_TICKS === 0) void refetch(); - setData((prev) => [ - ...prev.slice(-500), - { time: Date.now() / 1000, value: valueRef.current }, - ]); - }, 1000); - return () => { - active = false; - clearInterval(id); - }; - }, [live]); - - return ( -
-
- -
- - {live ? ( -
- - String(Math.round(v))} - momentumColors={{ - up: "var(--color-emerald-500)", - down: "var(--color-red-500)", - flat: "var(--chart-line-primary)", - }} - /> - String(Math.round(v))} - position="left" - /> - - -
- ) : ( -
- {t("analysis.live.idle")} -
- )} -
- ); -} diff --git a/frontend/lib/i18n/locales/en/translation.json b/frontend/lib/i18n/locales/en/translation.json index b83d075..eb3d20c 100644 --- a/frontend/lib/i18n/locales/en/translation.json +++ b/frontend/lib/i18n/locales/en/translation.json @@ -702,13 +702,10 @@ "analysis": { "title": "Analysis", "subtitle": "Clinic performance at a glance, computed from your clinic's data.", - "live": { - "title": "Live", - "subtitle": "Patients currently in the building, updating in real time.", - "label": "In the building now", - "start": "Go live", - "pause": "Pause", - "idle": "Live stream paused — press Go live to start." + "area": { + "title": "Patient visits", + "subtitle": "Visit volume over the last six months.", + "label": "Total visits" }, "patientVolume": { "title": "Patient volume",