marmar-context / web / context / src / clinician / patient / AddRecords.tsx
AddRecords.tsx
Raw
import type { DragEvent, RefObject } from "react";
import { useRef, useState } from "react";
import type { RecordType } from "../../api/types";
import { recordTypes, RECORD_LIMITS } from "../../logic/records";
import { Button, Callout, cn, fieldClass, Icon, labelClass, Tag } from "../kit";

export type Pending = { key: string; file: File | null; text: string | null; title: string; type: RecordType; place: string; recordDate: string; origin: "upload" | "camera" | "pasted"; error: string | null };

/** REC-1 to REC-3: drop, choose, photograph or paste records, name each one, then add them all. */
export function AddRecords({
  pending,
  notice,
  busy,
  chooseRef,
  onChoose,
  onPaste,
  onUpdate,
  onRemove,
  onClear,
  onAddAll,
}: {
  pending: Pending[];
  notice: string;
  busy: boolean;
  chooseRef: RefObject<HTMLButtonElement | null>;
  onChoose: (files: FileList | File[], origin: "upload" | "camera") => void;
  onPaste: () => void;
  onUpdate: (key: string, change: Partial<Pending>) => void;
  onRemove: (key: string) => void;
  onClear: () => void;
  onAddAll: () => void;
}) {
  const [drag, setDrag] = useState(false);
  const fileInput = useRef<HTMLInputElement>(null);
  const cameraInput = useRef<HTMLInputElement>(null);
  const ready = pending.filter((p) => !p.error).length;
  const onDrop = (e: DragEvent) => {
    e.preventDefault();
    setDrag(false);
    if (e.dataTransfer.files.length) onChoose(e.dataTransfer.files, "upload");
  };
  return (
    <section aria-label="Add records" data-add-records="" className="flex scroll-mt-20 flex-col gap-4 rounded-2xl bg-page p-5 shadow-card max-sm:p-4">
      <div className="flex flex-wrap items-center justify-between gap-2">
        <h2 className="m-0 flex items-center gap-2 text-[15px] leading-5 font-semibold tracking-[-0.01em] text-ink">
          <Icon name="upload-cloud-2-line" size={17} className="text-muted" />
          Add records
        </h2>
        <span className="font-mono text-[11.5px] text-muted">TXT, PDF, JPG, PNG, HEIC · 15 MB each</span>
      </div>
      <div
        className={cn(
          "flex flex-col items-center gap-3 rounded-xl border border-dashed px-4 py-7 text-center transition-colors duration-150",
          drag ? "border-brand bg-green-fill" : "border-line bg-tile",
        )}
        onDragOver={(e) => {
          e.preventDefault();
          setDrag(true);
        }}
        onDragLeave={() => setDrag(false)}
        onDrop={onDrop}
      >
        <Icon name="upload-2-line" size={22} className="text-muted" />
        <p className="m-0 text-[13.5px] text-cell">Drag files here, or</p>
        <div className="flex flex-wrap justify-center gap-2">
          <Button ref={chooseRef} variant="quiet" className="bg-page shadow-chip hover:bg-page hover:shadow-card-hover" onClick={() => fileInput.current?.click()}>
            Choose files
          </Button>
          <Button variant="quiet" className="bg-page shadow-chip hover:bg-page hover:shadow-card-hover" onClick={() => cameraInput.current?.click()}>
            Take a photo
          </Button>
          <Button variant="quiet" className="bg-page shadow-chip hover:bg-page hover:shadow-card-hover" onClick={onPaste}>
            Paste or type a note
          </Button>
        </div>
        <input ref={fileInput} hidden type="file" multiple accept=".txt,.pdf,.jpg,.jpeg,.png,.heic,text/plain,application/pdf,image/jpeg,image/png,image/heic" onChange={(e) => { if (e.target.files) onChoose(e.target.files, "upload"); e.target.value = ""; }} />
        <input ref={cameraInput} hidden type="file" accept="image/*" capture="environment" onChange={(e) => { if (e.target.files) onChoose(e.target.files, "camera"); e.target.value = ""; }} />
        <small className="max-w-md text-[12px] leading-[18px] text-muted">
          Up to {RECORD_LIMITS.perCheck} records per check and {RECORD_LIMITS.pagesPerDocument} pages per document. Scans and photos are read on MARMAR's servers.
        </small>
      </div>
      {notice && (
        <Callout tone="amber" role="status">
          {notice}
        </Callout>
      )}
      {pending.length > 0 && (
        <div className="flex flex-col gap-3">
          {pending.map((p) => (
            <fieldset key={p.key} className={cn("m-0 flex min-w-0 flex-col gap-3 rounded-xl border-0 p-4", p.error ? "bg-red-fill/50 shadow-[inset_0_0_0_1px_var(--color-red-line)]" : "bg-tile")}>
              <legend className="sr-only">{p.file ? p.file.name : "Pasted note"}</legend>
              <div className="flex min-w-0 items-center justify-between gap-3">
                <span aria-hidden="true" className="flex min-w-0">
                  <Tag lead={p.file ? "file-text-line" : "edit-line"} className="min-w-0 truncate">
                    {p.file ? p.file.name : "Pasted note"}
                  </Tag>
                </span>
                <Button variant="ghost" size="sm" onClick={() => onRemove(p.key)}>
                  Remove
                </Button>
              </div>
              <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
                <label className={labelClass}>
                  Title
                  <input className={fieldClass} value={p.title} maxLength={120} onChange={(e) => onUpdate(p.key, { title: e.target.value })} />
                </label>
                <label className={labelClass}>
                  Type
                  <select className={cn(fieldClass, "pr-8")} value={p.type} onChange={(e) => onUpdate(p.key, { type: e.target.value as RecordType })}>
                    {Object.entries(recordTypes).map(([v, l]) => (
                      <option key={v} value={v}>
                        {l}
                      </option>
                    ))}
                  </select>
                </label>
                <label className={labelClass}>
                  Where it came from
                  <input className={fieldClass} value={p.place} placeholder="Clinic, lab or hospital" maxLength={120} onChange={(e) => onUpdate(p.key, { place: e.target.value })} />
                </label>
                <label className={labelClass}>
                  Date on the record
                  <input className={fieldClass} type="date" value={p.recordDate} onChange={(e) => onUpdate(p.key, { recordDate: e.target.value })} />
                </label>
              </div>
              {p.error && (
                <p className="m-0 text-[12.5px] font-medium text-red-text" role="alert">
                  {p.error}
                </p>
              )}
            </fieldset>
          ))}
          <div className="flex flex-wrap justify-end gap-2">
            <Button variant="ghost" onClick={onClear}>
              Clear
            </Button>
            <Button variant="primary" disabled={busy || pending.every((p) => p.error)} onClick={onAddAll}>
              {busy ? "Adding…" : `Add ${ready} ${ready === 1 ? "record" : "records"}`}
            </Button>
          </div>
        </div>
      )}
    </section>
  );
}