marmar-context / web / context / src / clinician / settings / HoursEditor.tsx
HoursEditor.tsx
Raw
import { useState } from "react";
import type { WeeklyHours } from "../../api/types";
import { changeDay, DAY_NAMES, dayRows, hoursErrors, hoursFrom, type DayRow } from "../../logic/hours";
import { checkClass, cn, FieldError, fieldClass, Hint } from "../kit";

const timeClass = cn(fieldClass, "w-[122px] px-2 tabular-nums");

/** One week of hours, a row a day: open with its times, or closed. Tells the parent the hours and whether they can be saved. */
export function HoursEditor({
  id,
  legend,
  help,
  hours,
  disabled,
  onChange,
}: {
  id: string;
  legend: string;
  help: string;
  hours: WeeklyHours;
  disabled: boolean;
  onChange: (hours: WeeklyHours, valid: boolean) => void;
}) {
  const [rows, setRows] = useState<DayRow[]>(() => dayRows(hours));
  const errors = hoursErrors(rows);
  const update = (day: number, change: Partial<Omit<DayRow, "day">>) => {
    const next = changeDay(rows, day, change);
    setRows(next);
    onChange(hoursFrom(next), Object.keys(hoursErrors(next)).length === 0);
  };
  return (
    <fieldset className="m-0 flex min-w-0 flex-col border-0 p-0" disabled={disabled} aria-describedby={`${id}-help`}>
      <legend className="sr-only">{legend}</legend>
      <Hint id={`${id}-help`} className="-mt-2 mb-2">
        {help}
      </Hint>
      {rows.map((r) => {
        const name = DAY_NAMES[r.day];
        const errorId = `${id}-${r.day}-error`;
        return (
          <div key={r.day} className="flex flex-wrap items-center gap-x-3 gap-y-1.5 py-2 shadow-[inset_0_-1px_0_var(--color-hairline)] last:shadow-none">
            <label className="flex w-[100px] shrink-0 cursor-pointer items-center gap-2.5 text-[13.5px] font-medium text-ink has-disabled:cursor-not-allowed">
              <input type="checkbox" className={checkClass.replace("mt-0.5 ", "")} checked={r.open} onChange={(e) => update(r.day, { open: e.target.checked })} />
              {name}
            </label>
            {r.open ? (
              <span className="flex flex-wrap items-center gap-2 text-[13px] text-muted">
                <input
                  type="time"
                  step={300}
                  className={timeClass}
                  aria-label={`${name}, from`}
                  value={r.from}
                  aria-invalid={errors[r.day] ? true : undefined}
                  aria-describedby={errors[r.day] ? errorId : undefined}
                  onChange={(e) => update(r.day, { from: e.target.value })}
                />
                <span aria-hidden="true">to</span>
                <input
                  type="time"
                  step={300}
                  className={timeClass}
                  aria-label={`${name}, until`}
                  value={r.to}
                  aria-invalid={errors[r.day] ? true : undefined}
                  aria-describedby={errors[r.day] ? errorId : undefined}
                  onChange={(e) => update(r.day, { to: e.target.value })}
                />
                {r.more.length > 0 && <small className="text-[12.5px] text-muted">also {r.more.map((m) => `${m.open}–${m.close}`).join(", ")}</small>}
              </span>
            ) : (
              <span className="flex h-[38px] items-center text-[13px] text-subtle">Closed</span>
            )}
            {errors[r.day] && (
              <FieldError id={errorId} className="basis-full pl-[112px] max-sm:pl-0">
                {errors[r.day]}
              </FieldError>
            )}
          </div>
        );
      })}
    </fieldset>
  );
}