161 lines
7.4 KiB
TypeScript
161 lines
7.4 KiB
TypeScript
"use client";
|
|
|
|
import { FormEvent, useEffect, useMemo, useState } from "react";
|
|
import { AppShell } from "@/components/app-shell";
|
|
import { apiFetch } from "@/lib/api";
|
|
|
|
type Household = { id: string; name: string };
|
|
type Person = { id: string; email: string; fullName?: string | null };
|
|
type AccountantTask = {
|
|
id: string;
|
|
title: string;
|
|
description?: string | null;
|
|
taskType: string;
|
|
status: string;
|
|
priority: string;
|
|
dueDate?: string | null;
|
|
completedAt?: string | null;
|
|
assignedTo?: Person | null;
|
|
createdBy?: Person | null;
|
|
};
|
|
|
|
export default function AccountantPage() {
|
|
const [households, setHouseholds] = useState<Household[]>([]);
|
|
const [householdId, setHouseholdId] = useState("");
|
|
const [tasks, setTasks] = useState<AccountantTask[]>([]);
|
|
const [draft, setDraft] = useState({ title: "", description: "", taskType: "review", priority: "medium", dueDate: "" });
|
|
const [status, setStatus] = useState("");
|
|
const selectedHousehold = useMemo(() => households.find((item) => item.id === householdId), [households, householdId]);
|
|
|
|
useEffect(() => {
|
|
void loadHouseholds();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (householdId) void loadTasks(householdId);
|
|
}, [householdId]);
|
|
|
|
const loadHouseholds = async () => {
|
|
const res = await apiFetch<Household[]>("/api/households");
|
|
if (res.error) {
|
|
setStatus(res.error.message);
|
|
return;
|
|
}
|
|
const list = res.data ?? [];
|
|
setHouseholds(list);
|
|
setHouseholdId((current) => current || list[0]?.id || "");
|
|
};
|
|
|
|
const loadTasks = async (id = householdId) => {
|
|
const res = await apiFetch<AccountantTask[]>(`/api/households/${id}/accountant-tasks`);
|
|
if (res.error) {
|
|
setStatus(res.error.message);
|
|
setTasks([]);
|
|
return;
|
|
}
|
|
setStatus("");
|
|
setTasks(res.data ?? []);
|
|
};
|
|
|
|
const createTask = async (event: FormEvent) => {
|
|
event.preventDefault();
|
|
if (!householdId || !draft.title.trim()) return;
|
|
const res = await apiFetch<AccountantTask>(`/api/households/${householdId}/accountant-tasks`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
title: draft.title,
|
|
description: draft.description || undefined,
|
|
taskType: draft.taskType,
|
|
priority: draft.priority,
|
|
dueDate: draft.dueDate || undefined,
|
|
}),
|
|
});
|
|
if (res.error) {
|
|
setStatus(res.error.message);
|
|
return;
|
|
}
|
|
setDraft({ title: "", description: "", taskType: "review", priority: "medium", dueDate: "" });
|
|
await loadTasks();
|
|
};
|
|
|
|
const updateTask = async (task: AccountantTask, nextStatus: string) => {
|
|
if (!householdId) return;
|
|
const res = await apiFetch<AccountantTask>(`/api/households/${householdId}/accountant-tasks/${task.id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ status: nextStatus }),
|
|
});
|
|
if (res.error) {
|
|
setStatus(res.error.message);
|
|
return;
|
|
}
|
|
await loadTasks();
|
|
};
|
|
|
|
return (
|
|
<AppShell title="Accountant Workflow" subtitle="Track household advisor requests, reviews, and follow-ups.">
|
|
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6">
|
|
{status ? <div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">{status}</div> : null}
|
|
|
|
<section className="rounded-lg border border-slate-200 bg-white p-4">
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<h2 className="text-base font-semibold text-slate-950">Household</h2>
|
|
<p className="text-sm text-slate-600">{selectedHousehold?.name ?? "Choose a household to manage advisor work."}</p>
|
|
</div>
|
|
<select value={householdId} onChange={(event) => setHouseholdId(event.target.value)} className="rounded-md border border-slate-300 px-3 py-2 text-sm">
|
|
{households.map((household) => (
|
|
<option key={household.id} value={household.id}>{household.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</section>
|
|
|
|
<form onSubmit={createTask} className="rounded-lg border border-slate-200 bg-white p-4">
|
|
<h2 className="text-base font-semibold text-slate-950">New Task</h2>
|
|
<div className="mt-4 grid gap-3 md:grid-cols-2">
|
|
<input value={draft.title} onChange={(event) => setDraft({ ...draft, title: event.target.value })} placeholder="Task title" className="rounded-md border border-slate-300 px-3 py-2 text-sm" />
|
|
<input type="date" value={draft.dueDate} onChange={(event) => setDraft({ ...draft, dueDate: event.target.value })} className="rounded-md border border-slate-300 px-3 py-2 text-sm" />
|
|
<select value={draft.taskType} onChange={(event) => setDraft({ ...draft, taskType: event.target.value })} className="rounded-md border border-slate-300 px-3 py-2 text-sm">
|
|
<option value="review">Review</option>
|
|
<option value="document_request">Document request</option>
|
|
<option value="tax_prep">Tax prep</option>
|
|
<option value="advice">Advice</option>
|
|
</select>
|
|
<select value={draft.priority} onChange={(event) => setDraft({ ...draft, priority: event.target.value })} className="rounded-md border border-slate-300 px-3 py-2 text-sm">
|
|
<option value="low">Low</option>
|
|
<option value="medium">Medium</option>
|
|
<option value="high">High</option>
|
|
</select>
|
|
</div>
|
|
<textarea value={draft.description} onChange={(event) => setDraft({ ...draft, description: event.target.value })} placeholder="Notes or request details" className="mt-3 min-h-24 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
|
|
<button className="mt-3 rounded-md bg-slate-950 px-4 py-2 text-sm font-medium text-white">Create task</button>
|
|
</form>
|
|
|
|
<section className="grid gap-3">
|
|
{tasks.length ? tasks.map((task) => (
|
|
<article key={task.id} className="rounded-lg border border-slate-200 bg-white p-4">
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
<div>
|
|
<h3 className="font-semibold text-slate-950">{task.title}</h3>
|
|
<p className="mt-1 text-sm text-slate-600">{task.description || "No details added."}</p>
|
|
<p className="mt-2 text-xs uppercase text-slate-500">
|
|
{task.taskType.replace(/_/g, " ")} · {task.priority} · {task.dueDate ? new Date(task.dueDate).toLocaleDateString() : "No due date"}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button onClick={() => updateTask(task, "open")} className="rounded-md border border-slate-300 px-3 py-1.5 text-xs">Open</button>
|
|
<button onClick={() => updateTask(task, "in_progress")} className="rounded-md border border-blue-200 px-3 py-1.5 text-xs text-blue-700">In progress</button>
|
|
<button onClick={() => updateTask(task, "completed")} className="rounded-md border border-emerald-200 px-3 py-1.5 text-xs text-emerald-700">Complete</button>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 text-xs text-slate-500">Status: {task.status.replace(/_/g, " ")}</div>
|
|
</article>
|
|
)) : (
|
|
<div className="rounded-lg border border-dashed border-slate-300 bg-white p-6 text-sm text-slate-500">No accountant tasks yet.</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
</AppShell>
|
|
);
|
|
}
|