mirror of
https://github.com/temetro/temetro.git
synced 2026-08-10 18:49:29 +00:00
321a6298a4
- 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>
184 lines
4.9 KiB
TypeScript
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,
|
|
};
|
|
}
|