Files
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

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;
}