import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Alert, Box, Button, Checkbox, Chip, IconButton, LinearProgress, Paper, Skeleton, Stack, TextField, Tooltip, Typography, } from "@mui/material"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import { getApiErrorMessage } from "../api"; import { CHECKLIST_CATEGORIES, Checklist, ChecklistItem, applicationChecklistApi, } from "../applicationWorkspace"; // Phase 5 Milestone 2 — the application checklist. // // One workflow surface, not a new tracker: the system items arrive already ticked wherever the // existing readiness signals say the work is done (CV attached, cover letter written, follow-up // scheduled...). Everything here is the user's to tick, add to, reorder or dismiss. // docs/architecture/application-workspace.md. export default function ApplicationChecklist({ jobId, onChanged }: { jobId: number; onChanged?: () => void }) { const [checklist, setChecklist] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [draft, setDraft] = useState(""); const load = useCallback(async () => { try { setChecklist(await applicationChecklistApi.get(jobId)); setError(null); } catch (err) { setError(getApiErrorMessage(err, "Could not load the checklist.")); } }, [jobId]); useEffect(() => { load(); }, [load]); // Every mutation re-reads: the backend re-syncs the auto-completed items on each read, so the // response is the only thing that knows the real state. const mutate = useCallback(async (run: () => Promise) => { setBusy(true); try { await run(); await load(); onChanged?.(); } catch (err) { setError(getApiErrorMessage(err, "Could not update the checklist.")); } finally { setBusy(false); } }, [load, onChanged]); const toggle = (item: ChecklistItem) => mutate(() => applicationChecklistApi.update(jobId, item.id, { status: item.status === "done" ? "pending" : "done", })); const remove = (item: ChecklistItem) => mutate(() => applicationChecklistApi.remove(jobId, item.id)); const move = (item: ChecklistItem, delta: -1 | 1) => { if (!checklist) return; const ids = checklist.items.map((i) => i.id); const from = ids.indexOf(item.id); const to = from + delta; if (to < 0 || to >= ids.length) return; [ids[from], ids[to]] = [ids[to], ids[from]]; return mutate(() => applicationChecklistApi.reorder(jobId, ids)); }; const add = (e: React.FormEvent) => { e.preventDefault(); const title = draft.trim(); if (!title) return; setDraft(""); return mutate(() => applicationChecklistApi.add(jobId, title)); }; const grouped = useMemo(() => { const live = checklist?.items.filter((i) => i.status !== "dismissed") ?? []; return CHECKLIST_CATEGORIES .map((c) => ({ ...c, items: live.filter((i) => i.category === c.key) })) .filter((c) => c.items.length > 0); }, [checklist]); if (!checklist && !error) { return {[0, 1, 2].map((i) => )}; } const progress = checklist?.progress; return ( {error && setError(null)}>{error}} {progress && ( Application checklist {progress.completed} of {progress.total} done )} {grouped.map((group) => ( {group.label} {group.items.map((item) => ( toggle(item)} inputProps={{ "aria-label": item.title }} sx={{ mt: -0.25 }} /> {item.title} {!item.isSystemGenerated && } {item.isAutoCompleted && } {item.description && ( {item.description} )} move(item, -1)}> move(item, 1)}> remove(item)}> ))} ))} setDraft(e.target.value)} /> ); }