mirror of
https://github.com/temetro/temetro.git
synced 2026-07-28 04:38:57 +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>
89 lines
2.2 KiB
TypeScript
89 lines
2.2 KiB
TypeScript
import { type RefObject, useEffect, useState } from "react";
|
|
|
|
export function findPathLengthAtX(
|
|
path: SVGPathElement | null,
|
|
pathLength: number,
|
|
targetX: number
|
|
): number {
|
|
if (!path || pathLength === 0) {
|
|
return 0;
|
|
}
|
|
let low = 0;
|
|
let high = pathLength;
|
|
const tolerance = 0.5;
|
|
|
|
while (high - low > tolerance) {
|
|
const mid = (low + high) / 2;
|
|
const point = path.getPointAtLength(mid);
|
|
if (point.x < targetX) {
|
|
low = mid;
|
|
} else {
|
|
high = mid;
|
|
}
|
|
}
|
|
return (low + high) / 2;
|
|
}
|
|
|
|
interface PathStrokeMetrics {
|
|
pathD: string | null;
|
|
pathLength: number;
|
|
}
|
|
|
|
const EMPTY_METRICS: PathStrokeMetrics = { pathD: null, pathLength: 0 };
|
|
|
|
/**
|
|
* Caller passes the references that drive the rendered path (renderData,
|
|
* innerWidth, etc.) as `deps`. A stringified summary like
|
|
* `${renderData.length}:${innerWidth}` is *not* safe here — same-length
|
|
* in-place mutations of `renderData` keep the summary identical, so the
|
|
* effect would never re-fire and `pathD`/`pathLength` would stay frozen on
|
|
* the previous geometry (the area fill repaints from `renderData` directly
|
|
* and would diverge from the stroke).
|
|
*/
|
|
export function usePathStrokeMetrics(
|
|
pathRef: RefObject<SVGPathElement | null>,
|
|
deps: readonly unknown[]
|
|
): PathStrokeMetrics {
|
|
const [metrics, setMetrics] = useState<PathStrokeMetrics>(EMPTY_METRICS);
|
|
|
|
useEffect(() => {
|
|
const path = pathRef.current;
|
|
if (!path) {
|
|
return;
|
|
}
|
|
const d = path.getAttribute("d");
|
|
const len = d ? path.getTotalLength() : 0;
|
|
setMetrics((prev) =>
|
|
prev.pathD === d && prev.pathLength === len
|
|
? prev
|
|
: { pathD: d, pathLength: len }
|
|
);
|
|
}, deps);
|
|
|
|
return metrics;
|
|
}
|
|
|
|
export function resolveDashTailBounds(
|
|
dashFromIndex: number | undefined,
|
|
dataLength: number
|
|
): boolean {
|
|
return (
|
|
dashFromIndex != null &&
|
|
dashFromIndex >= 0 &&
|
|
dashFromIndex < dataLength - 1
|
|
);
|
|
}
|
|
|
|
export function resolveDashStartX(
|
|
data: Record<string, unknown>[],
|
|
dashFromIndex: number,
|
|
xScale: (value: Date | number) => number | undefined,
|
|
xAccessor: (datum: Record<string, unknown>) => Date | number
|
|
): number {
|
|
const dashFromPoint = data[dashFromIndex];
|
|
if (!dashFromPoint) {
|
|
return 0;
|
|
}
|
|
return xScale(xAccessor(dashFromPoint)) ?? 0;
|
|
}
|