Files
temetro/frontend/components/charts/use-chart-phase-orchestrator.ts
T
Khalid Abdi 321a6298a4 frontend: pharmacy inventory, lab add-result & analysis charts
- Pharmacy: the sidebar entry now expands into Pharmacy + a new Inventory page
  (searchable medication stock with derived in-stock/low/out availability,
  backed by /api/inventory). Fix the dispensing-queue "Expiring" badge so it
  only flags courses ending within the next 7 days, not ones already elapsed.
- Lab "Add analysis result": the patient picker no longer lists patients until
  you type and supports arrow-key + Enter selection; the test field offers a
  catalog of common analyses with an Advanced option (custom analysis + ref
  range); submitted results now show immediately in a Recent results feed.
- Analysis: add a Live panel (real-time line chart) and replace the flat trend
  sparklines with bklit-ui area + bar charts (installed from the @bklit shadcn
  registry; chart CSS variables wired into the theme for light and dark).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 19:22:07 +03:00

184 lines
4.9 KiB
TypeScript

"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
type ChartPhase,
type ChartStatus,
resolveRestingChartPhase,
} from "./chart-phase";
export interface UseChartPhaseOrchestratorOptions {
chartStatus: ChartStatus;
targetData: Record<string, unknown>[];
skeletonData: Record<string, unknown>[];
animationDuration: number;
yDomainTweenDuration: number;
/** Signature of motion URL state — replays clip reveal in Studio. */
revealSignature?: string;
/** Skip mount/signature enter reveal (static docs previews). */
skipEnterReveal?: boolean;
}
export function useChartPhaseOrchestrator({
chartStatus,
targetData,
skeletonData,
animationDuration,
yDomainTweenDuration,
revealSignature = "",
skipEnterReveal = false,
}: UseChartPhaseOrchestratorOptions) {
const [chartPhase, setChartPhase] = useState<ChartPhase>(() =>
resolveRestingChartPhase(chartStatus)
);
const [plotData, setPlotData] = useState<Record<string, unknown>[]>(() =>
chartStatus === "loading" ? skeletonData : targetData
);
const [revealEpoch, setRevealEpoch] = useState(0);
const [concealEpoch, setConcealEpoch] = useState(0);
const [isLoaded, setIsLoaded] = useState(() => chartStatus === "ready");
const prevStatusRef = useRef(chartStatus);
const phaseRef = useRef(chartPhase);
phaseRef.current = chartPhase;
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: status transition branches for animation durations
useEffect(() => {
const prevStatus = prevStatusRef.current;
if (prevStatus === chartStatus) {
return;
}
prevStatusRef.current = chartStatus;
if (chartStatus === "ready" && prevStatus === "loading") {
setIsLoaded(false);
if (animationDuration <= 0) {
if (yDomainTweenDuration <= 0) {
setPlotData(targetData);
setChartPhase("revealing");
} else {
setChartPhase("gridTweenReady");
}
} else {
setChartPhase("exiting");
}
return;
}
if (chartStatus === "loading" && prevStatus === "ready") {
setIsLoaded(false);
if (animationDuration <= 0) {
if (yDomainTweenDuration <= 0) {
setPlotData(skeletonData);
setChartPhase("loading");
} else {
setChartPhase("gridTweenLoading");
}
} else {
setConcealEpoch((epoch) => epoch + 1);
setChartPhase("exitingReady");
}
}
}, [
animationDuration,
chartStatus,
skeletonData,
targetData,
yDomainTweenDuration,
]);
// biome-ignore lint/correctness/useExhaustiveDependencies: revealSignature replays enter
useEffect(() => {
if (skipEnterReveal) {
return;
}
if (chartStatus !== "ready") {
return;
}
if (phaseRef.current !== "ready") {
return;
}
setChartPhase("revealing");
setIsLoaded(false);
}, [animationDuration, chartStatus, revealSignature, skipEnterReveal]);
useEffect(() => {
switch (chartPhase) {
case "loading":
if (chartStatus === "loading") {
setPlotData(skeletonData);
}
break;
case "exiting":
setPlotData(skeletonData);
break;
case "exitingReady":
case "gridTweenLoading":
case "gridTweenReady":
case "revealing":
case "ready":
setPlotData(targetData);
break;
default:
break;
}
}, [chartPhase, chartStatus, skeletonData, targetData]);
/** Loading pulse exit finished — tween grid to ready spacing next. */
const notifyLoadingPulseComplete = useCallback(() => {
if (phaseRef.current !== "exiting") {
return;
}
setChartPhase("gridTweenReady");
}, []);
/** Ready series conceal finished — tween grid to loading spacing next. */
const notifyRevealConcealComplete = useCallback(() => {
if (phaseRef.current !== "exitingReady") {
return;
}
setChartPhase("gridTweenLoading");
}, []);
/** Grid tween finished — enter the next resting phase. */
const notifyYDomainTweenComplete = useCallback(() => {
if (phaseRef.current === "gridTweenLoading") {
setChartPhase("loading");
return;
}
if (phaseRef.current === "gridTweenReady") {
setChartPhase("revealing");
}
}, []);
useEffect(() => {
if (chartPhase !== "revealing") {
return;
}
setRevealEpoch((epoch) => epoch + 1);
if (animationDuration <= 0) {
setChartPhase("ready");
setIsLoaded(true);
return;
}
const timer = window.setTimeout(() => {
setChartPhase("ready");
setIsLoaded(true);
}, animationDuration);
return () => window.clearTimeout(timer);
}, [animationDuration, chartPhase]);
return {
chartPhase,
plotData,
revealEpoch,
concealEpoch,
isLoaded,
notifyLoadingPulseComplete,
notifyRevealConcealComplete,
notifyYDomainTweenComplete,
};
}