marmar-context / web / context / src / clinician / charts / DoseBars.tsx
DoseBars.tsx
Raw
import { useState, type ReactNode } from "react";
import type { DoseCounts, SharedDoses } from "../../api/types";
import { doseHeadline, doseKinds, doseLine, doseStack, type DoseKind, type DosePeriod } from "../../logic/charts";
import { scheduleLabel } from "../../logic/format";
import { cn, Segmented } from "../kit";

const fill: Record<DoseKind, string> = {
  onTime: "bg-green-icon",
  late: "bg-amber-icon",
  skipped: "bg-subtle",
  missed: "bg-red-icon",
  rest: "bg-hairline shadow-[inset_0_0_0_1px_var(--color-line)]",
};
const periods: { value: DosePeriod; label: string }[] = [
  { value: "week", label: "Week" },
  { value: "last30", label: "30 days" },
];

/** A stacked bar out of what was due; the line under it says the same in words. */
function DoseRow({ name, schedule, counts, period }: { name: string; schedule: string | null; counts: DoseCounts; period: DosePeriod }) {
  const stack = doseStack(counts);
  return (
    <li className="flex flex-col gap-1.5 py-3 shadow-[inset_0_-1px_0_var(--color-hairline)] last:shadow-none">
      <div className="flex items-baseline justify-between gap-3">
        <span className="min-w-0 text-[13.5px] leading-5">
          <strong className="font-medium text-ink">{name}</strong>
          {schedule && <span className="text-muted"> · {scheduleLabel(schedule)}</span>}
        </span>
        <span className="shrink-0 font-mono text-[12px] text-muted tabular-nums">
          {stack.taken}/{stack.expected}
        </span>
      </div>
      <div aria-hidden="true" className="flex h-2.5 gap-[2px]">
        {stack.segments.length ? (
          stack.segments.map((s) => <span key={s.key} className={cn("min-w-1 rounded-full", fill[s.key])} style={{ flexGrow: s.count, flexBasis: 0 }} />)
        ) : (
          <span className={cn("flex-1 rounded-full", fill.rest)} />
        )}
      </div>
      <p className="m-0 text-[12.5px] leading-[18px] text-muted">
        {period === "week" ? "This week" : "Last 30 days"}: {doseLine(counts)}
      </p>
      {stack.over && (
        <p className="m-0 text-[12.5px] leading-[18px] text-amber-text">
          More doses were logged than were due: {stack.logged} logged, {stack.expected} due. The bar shows what was logged.
        </p>
      )}
    </li>
  );
}

/** Doses taken: the figure for the period, then one bar per medicine, then the colours in words. */
export function DoseBars({ doses, notes }: { doses: SharedDoses; notes?: ReactNode }) {
  const [period, setPeriod] = useState<DosePeriod>("week");
  const head = doseHeadline(doses, period);
  return (
    <div className="flex flex-col gap-4">
      <div className="flex flex-wrap items-start justify-between gap-3">
        <div className="flex min-w-0 flex-col gap-1">
          <p className="m-0 flex flex-wrap items-baseline gap-x-2">
            {head.percent === null ? (
              <span className="text-[28px] leading-9 font-semibold tracking-[-0.02em] text-ink">None due</span>
            ) : (
              <>
                <span className="text-[38px] leading-[42px] font-semibold tracking-[-0.03em] text-ink tabular-nums">
                  {head.percent}
                  <span className="text-subtle">%</span>
                </span>
                <span className="text-[15px] font-medium text-subtle">taken {head.when}</span>
              </>
            )}
          </p>
          <p className="m-0 text-[13.5px] leading-5 text-body">{head.sentence}</p>
        </div>
        <Segmented label="Show doses for" options={periods} value={period} onChange={setPeriod} />
      </div>
      {notes}
      {doses.perMedicine.length > 0 && (
        <ul aria-label="Doses for each medicine" className="m-0 flex list-none flex-col p-0">
          {doses.perMedicine.map((m) => (
            <DoseRow key={m.medicineId} name={m.name} schedule={m.schedule} counts={period === "week" ? m.week : m.last30} period={period} />
          ))}
        </ul>
      )}
      <ul aria-label="What the colours mean" className="m-0 flex list-none flex-wrap gap-x-4 gap-y-1.5 p-0 text-[12px] text-muted">
        {doseKinds.map((k) => (
          <li key={k.key} className="flex items-center gap-1.5">
            <span aria-hidden="true" className={cn("size-2 rounded-[2px]", fill[k.key])} />
            {k.label}
          </li>
        ))}
      </ul>
    </div>
  );
}