mirror of
https://github.com/temetro/temetro.git
synced 2026-08-28 03:17:05 +00:00
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>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user