/** Block detail + thick feedback sheet for schedule blocks. */ import { useEffect, useState } from "preact/hooks"; import { addPlanBlock, deletePlanBlock, getScheduleTemplates, postBlockFeedback, type ScheduledBlock, type ScheduleTemplate, type TaskKind, } from "../api"; import { Button } from "./Button"; import { Pressable } from "./Pressable"; import { Sheet } from "./Sheet"; import { useToast } from "./Toast"; const KIND_OPTIONS: { value: TaskKind; label: string }[] = [ { value: "earn_ship", label: "Work / earn" }, { value: "admin_spain", label: "Trip / admin" }, { value: "body_care", label: "Body or room" }, { value: "move_out", label: "Move-out step" }, { value: "food_out", label: "Food" }, { value: "stabilize", label: "Stabilize / reset" }, { value: "explore", label: "Try something new" }, { value: "restore_fun", label: "Fun recharge" }, { value: "boundary", label: "Boundary practice" }, { value: "sleep_window", label: "Sleep wind-down" }, { value: "other", label: "Other" }, ]; function addMinutes(hhmm: string, mins: number): string { const [h, m] = hhmm.split(":").map(Number); const total = h * 60 + m + mins; const nh = Math.floor(((total % (24 * 60)) + 24 * 60) % (24 * 60) / 60); const nm = ((total % (24 * 60)) + 24 * 60) % (24 * 60) % 60; return `${String(nh).padStart(2, "0")}:${String(nm).padStart(2, "0")}`; } type Props = { open: boolean; day: string; block: ScheduledBlock | null; mode: "view" | "create"; onClose: () => void; onChanged: () => void; }; export function BlockSheet({ open, day, block, mode, onClose, onChanged }: Props) { const toast = useToast(); const [did, setDid] = useState<"done" | "partial" | "skipped">("done"); const [actualMin, setActualMin] = useState(30); const [quality, setQuality] = useState(3); const [fun, setFun] = useState(3); const [energy, setEnergy] = useState(0); const [wouldRepeat, setWouldRepeat] = useState<"yes" | "no" | "maybe" | "">(""); const [skipReason, setSkipReason] = useState(""); const [note, setNote] = useState(""); const [money, setMoney] = useState(""); const [emotions, setEmotions] = useState([]); const [fseEvent, setFseEvent] = useState(""); const [intensity, setIntensity] = useState(5); const [showThick, setShowThick] = useState(false); const [busy, setBusy] = useState(false); const [templates, setTemplates] = useState([]); const [title, setTitle] = useState("New block"); const [kind, setKind] = useState("other"); const [start, setStart] = useState("09:00"); const [end, setEnd] = useState("09:30"); const [priority, setPriority] = useState<"P0" | "P1" | "P2">("P2"); const [intent, setIntent] = useState("duty"); useEffect(() => { if (!open) return; if (mode === "create") { void getScheduleTemplates() .then((r) => setTemplates(r.items || [])) .catch(() => setTemplates([])); } if (mode === "view" && block) { setActualMin(block.feedback?.actual_min ?? block.planned_min ?? 30); setDid((block.feedback?.did as typeof did) || (block.status === "skipped" ? "skipped" : "done")); setQuality(block.feedback?.quality ?? 3); setFun(block.feedback?.fun ?? 3); setEnergy(block.feedback?.energy_after ?? 0); setWouldRepeat((block.feedback?.would_repeat as typeof wouldRepeat) || ""); setSkipReason(block.feedback?.skip_reason || ""); setNote(block.feedback?.note || ""); setMoney(block.feedback?.money_amount != null ? String(block.feedback.money_amount) : ""); setEmotions(block.feedback?.emotions || []); setFseEvent(block.feedback?.fse_event || ""); setIntensity(block.feedback?.intensity ?? 5); setShowThick(false); } if (mode === "create") { setTitle("New block"); setKind("other"); setStart("09:00"); setEnd("09:30"); setPriority("P2"); setIntent("duty"); } }, [open, mode, block?.id]); const applyTemplate = (tmpl: ScheduleTemplate) => { setTitle(tmpl.title); setKind(tmpl.kind); setPriority((tmpl.priority as "P0" | "P1" | "P2") || "P2"); setIntent( (tmpl.intent as ScheduledBlock["intent"]) || (tmpl.kind === "explore" ? "explore" : tmpl.kind === "restore_fun" ? "restore_fun" : "duty"), ); setEnd(addMinutes(start, tmpl.default_min || 30)); }; const saveFeedback = async () => { if (!block) return; setBusy(true); try { await postBlockFeedback(day, block.id, { did, actual_min: did === "skipped" ? null : actualMin, quality: did === "skipped" || !showThick ? null : quality, fun: did === "skipped" || !showThick ? null : fun, energy_after: showThick ? energy : null, money_amount: money ? Number(money) : null, would_repeat: wouldRepeat || null, skip_reason: did === "skipped" ? skipReason || "other" : null, note, emotions, fse_event: fseEvent, intensity: did === "skipped" ? null : intensity, }); toast.show("Saved"); onChanged(); onClose(); } catch (e) { toast.show(e instanceof Error ? e.message : "Save failed", "error"); } finally { setBusy(false); } }; const createBlock = async () => { setBusy(true); try { await addPlanBlock(day, { title, kind, start, end, priority, intent: kind === "explore" ? "explore" : kind === "restore_fun" ? "restore_fun" : kind === "stabilize" ? "measure" : intent, locked: priority === "P0", }); toast.show("Block added"); onChanged(); onClose(); } catch (e) { toast.show(e instanceof Error ? e.message : "Could not add", "error"); } finally { setBusy(false); } }; const removeBlock = async () => { if (!block || !confirm("Remove this block?")) return; setBusy(true); try { await deletePlanBlock(day, block.id); toast.show("Removed"); onChanged(); onClose(); } catch (e) { toast.show(e instanceof Error ? e.message : "Delete failed", "error"); } finally { setBusy(false); } }; return ( {mode === "create" ? (
{templates.length > 0 && ( <> From template
{templates.slice(0, 8).map((tmpl) => ( applyTemplate(tmpl)} > {tmpl.title} ))}
)} Type
{KIND_OPTIONS.map((opt) => ( setKind(opt.value)} > {opt.label} ))}
Priority
{(["P0", "P1", "P2"] as const).map((p) => ( setPriority(p)}> {p === "P0" ? "Locked" : p === "P1" ? "Important" : "Flex"} ))}
) : block ? (

{block.start}–{block.end} · {block.label || block.kind} ·{" "} {block.priority === "P0" ? "Locked" : block.priority} {block.locked ? " · locked" : ""}

How did it go?
{([ ["done", "Done"], ["partial", "Partial"], ["skipped", "Skipped"], ] as const).map(([value, label]) => ( setDid(value)} > {label} ))}
{did !== "skipped" && ( )}