62 lines
4.1 KiB
TypeScript
62 lines
4.1 KiB
TypeScript
import { FormMessage } from "./design-system/WorkspacePanel";
|
||
import { useEffect, useRef, useState } from "react";
|
||
import { habitRequest, formatTrackingDate, type TodayHabit } from "../lib/dashboard";
|
||
import { Button, Checkbox } from "./design-system/primitives";
|
||
import { Field } from "./design-system/Field";
|
||
|
||
export function CheckInEditor({ habitId, date, disabled, onSaved, onExpired, onBusyChange }: {
|
||
habitId: string; date: string; disabled?: boolean; onSaved: () => void; onExpired?: () => void; onBusyChange?: (busy: boolean) => void;
|
||
}) {
|
||
const [day, setDay] = useState<TodayHabit | null>(null);
|
||
const [count, setCount] = useState("");
|
||
const [error, setError] = useState("");
|
||
const [notice, setNotice] = useState("");
|
||
const [busy, setBusy] = useState(false);
|
||
const [attempt, setAttempt] = useState(0);
|
||
const saving = useRef(false);
|
||
const expired = useRef(onExpired); expired.current = onExpired;
|
||
useEffect(() => {
|
||
const controller = new AbortController();
|
||
setDay(null); setError("");
|
||
habitRequest<TodayHabit>(`/habits/${habitId}/days/${date}`, { signal: controller.signal })
|
||
.then(value => { if (!controller.signal.aborted) { setDay(value); setCount(String(value.value)); } })
|
||
.catch(error => { if (!controller.signal.aborted) {
|
||
if (error.message.includes("session has expired")) expired.current?.();
|
||
setError(error.message);
|
||
} });
|
||
return () => controller.abort();
|
||
}, [habitId, date, attempt]);
|
||
async function save(body: { count: number } | { done: boolean }, taskId?: string) {
|
||
if (saving.current || disabled) return;
|
||
saving.current = true; setBusy(true); onBusyChange?.(true); setError(""); setNotice("");
|
||
try {
|
||
const result = await habitRequest<TodayHabit>(`/habits/${habitId}/days/${date}/${taskId ? `tasks/${taskId}` : "progress"}`, {
|
||
method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
|
||
});
|
||
setDay(result); setCount(String(result.value)); setNotice(`Saved check-in for ${date}.`); onSaved();
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : "Could not save. Try again.";
|
||
setError(message); if (message.includes("session has expired")) expired.current?.();
|
||
} finally { saving.current = false; setBusy(false); onBusyChange?.(false); }
|
||
}
|
||
return <div className="ds-form-stack ds-check-in-controls" aria-label={`Check-in for ${date}`}>
|
||
<header className="ds-check-in-heading"><p className="type-ui-heading">{formatTrackingDate(date)}</p></header>
|
||
<FormMessage error>{error}</FormMessage>
|
||
{!day ? error ? <Button onClick={() => setAttempt(value => value + 1)}>Retry check-in</Button> : <p role="status">Loading check-in…</p>
|
||
: day.future ? <p>Future check-ins cannot be edited.</p>
|
||
: !day.due ? <p>Nothing was scheduled for this date.</p>
|
||
: <>
|
||
<p className="ds-supporting-copy">{day.timezone} · Using this day’s saved target and schedule.</p>
|
||
{day.method === "count" ? <form className="ds-form-stack" onSubmit={event => { event.preventDefault(); void save({ count: Number(count) }); }}>
|
||
<Field label={`Count (${day.unit}, target ${day.target})`}>
|
||
{id => <input id={id} type="number" min={0} max={1000000000} step={1} required value={count} disabled={busy || disabled} onChange={event => setCount(event.target.value)} />}
|
||
</Field>
|
||
{day.carriedFrom && <p className="type-small">Includes carryover from {day.carriedFrom}. Saving sets this day's total.</p>}
|
||
<div className="ds-action-row"><Button type="submit" disabled={busy || disabled}>{busy ? "Saving…" : "Save check-in"}</Button></div>
|
||
</form> : day.method === "manual" ? <Checkbox label="Completed on this date" checked={day.complete} disabled={busy || disabled} onChange={event => void save({ done: event.target.checked })} />
|
||
: day.tasks.map(task => <Checkbox key={task.taskId} label={task.name} checked={task.done} disabled={busy || disabled} onChange={event => void save({ done: event.target.checked }, task.taskId)} />)}
|
||
</>}
|
||
<FormMessage>{notice}</FormMessage>
|
||
</div>;
|
||
}
|