573 lines
25 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { AppShell } from "../../components/app-shell";
import { apiFetch } from "@/lib/api";
type TaxReturn = {
id: string;
taxYear: number;
filingType: "individual" | "business";
jurisdictions: string[];
status: "draft" | "ready" | "exported" | "efile_submitted" | "efile_accepted";
summary?: { intake?: TaxIntake; intakeReadiness?: TaxReadiness; eFile?: EFileStatus };
updatedAt: string;
documents?: TaxDocument[];
};
type TaxDocument = {
id: string;
docType: string;
metadata?: Record<string, unknown>;
};
type TaxIntake = {
taxpayer: {
name?: string;
filingStatus?: string;
address?: string;
};
income: {
wages?: number;
business?: number;
interest?: number;
dividends?: number;
total?: number;
};
deductions: {
standard?: boolean;
charitable?: number;
studentLoanInterest?: number;
};
credits: {
education?: number;
childTax?: number;
};
notes?: string;
};
type TaxReadiness = {
complete: boolean;
missingFields: string[];
};
type EFileStatus = {
provider?: string;
providerMode?: string;
submissionId?: string;
status?: string;
acknowledgementId?: string;
lastCheckedAt?: string;
};
const emptyIntake: TaxIntake = {
taxpayer: { name: "", filingStatus: "Single", address: "" },
income: { wages: 0, business: 0, interest: 0, dividends: 0, total: 0 },
deductions: { standard: true, charitable: 0, studentLoanInterest: 0 },
credits: { education: 0, childTax: 0 },
notes: "",
};
export default function TaxPage() {
const [returns, setReturns] = useState<TaxReturn[]>([]);
const [status, setStatus] = useState("");
const [loading, setLoading] = useState(true);
const [useSample, setUseSample] = useState(true);
const [selectedReturnId, setSelectedReturnId] = useState("");
const [intake, setIntake] = useState<TaxIntake>(emptyIntake);
const [readiness, setReadiness] = useState<TaxReadiness | null>(null);
const [efileConsent, setEfileConsent] = useState(false);
const [year, setYear] = useState(new Date().getFullYear());
const [filingType, setFilingType] = useState<"individual" | "business">("individual");
const [jurisdictions, setJurisdictions] = useState<string[]>(["CA", "NY"]);
const sampleProfile = {
taxpayer: {
name: "John Doe",
ssn: "111-22-3333",
dob: "1989-04-12",
filingStatus: "Single",
address: "123 Market St, San Francisco, CA"
},
income: {
w2: [{ employer: "Northwind Labs", wages: 92000, federalWithheld: 12000 }],
interest: [{ payer: "City Bank", amount: 320 }],
dividends: [{ payer: "Index Fund", amount: 480 }],
selfEmployment: [{ business: "Doe Consulting", income: 28000, expenses: 6400 }]
},
deductions: {
standard: true,
charitable: 600,
studentLoanInterest: 900
},
credits: {
education: 0,
childTax: 0
},
documents: [
{ docType: "w2_or_1099", label: "W-2 / 1099 income forms" },
{ docType: "interest_and_dividend_forms", label: "1099-INT / 1099-DIV forms" },
{ docType: "deduction_support", label: "Deduction support" },
{ docType: "identity_information", label: "Identity information" },
{ docType: "state_withholding_statement", label: "State withholding statement" },
{ docType: "health_insurance_statement", label: "Health insurance 1095-A" }
]
};
const states = [
{ code: "AL", name: "Alabama" },
{ code: "AK", name: "Alaska" },
{ code: "AZ", name: "Arizona" },
{ code: "AR", name: "Arkansas" },
{ code: "CA", name: "California" },
{ code: "CO", name: "Colorado" },
{ code: "CT", name: "Connecticut" },
{ code: "DE", name: "Delaware" },
{ code: "FL", name: "Florida" },
{ code: "GA", name: "Georgia" },
{ code: "HI", name: "Hawaii" },
{ code: "ID", name: "Idaho" },
{ code: "IL", name: "Illinois" },
{ code: "IN", name: "Indiana" },
{ code: "IA", name: "Iowa" },
{ code: "KS", name: "Kansas" },
{ code: "KY", name: "Kentucky" },
{ code: "LA", name: "Louisiana" },
{ code: "ME", name: "Maine" },
{ code: "MD", name: "Maryland" },
{ code: "MA", name: "Massachusetts" },
{ code: "MI", name: "Michigan" },
{ code: "MN", name: "Minnesota" },
{ code: "MS", name: "Mississippi" },
{ code: "MO", name: "Missouri" },
{ code: "MT", name: "Montana" },
{ code: "NE", name: "Nebraska" },
{ code: "NV", name: "Nevada" },
{ code: "NH", name: "New Hampshire" },
{ code: "NJ", name: "New Jersey" },
{ code: "NM", name: "New Mexico" },
{ code: "NY", name: "New York" },
{ code: "NC", name: "North Carolina" },
{ code: "ND", name: "North Dakota" },
{ code: "OH", name: "Ohio" },
{ code: "OK", name: "Oklahoma" },
{ code: "OR", name: "Oregon" },
{ code: "PA", name: "Pennsylvania" },
{ code: "RI", name: "Rhode Island" },
{ code: "SC", name: "South Carolina" },
{ code: "SD", name: "South Dakota" },
{ code: "TN", name: "Tennessee" },
{ code: "TX", name: "Texas" },
{ code: "UT", name: "Utah" },
{ code: "VT", name: "Vermont" },
{ code: "VA", name: "Virginia" },
{ code: "WA", name: "Washington" },
{ code: "WV", name: "West Virginia" },
{ code: "WI", name: "Wisconsin" },
{ code: "WY", name: "Wyoming" }
];
const loadReturns = async () => {
setLoading(true);
const payload = await apiFetch<TaxReturn[]>("/api/tax/returns");
setLoading(false);
if (payload.error) {
setStatus(payload.error.message ?? "Unable to load returns.");
return;
}
setReturns(payload.data ?? []);
if (!selectedReturnId && payload.data?.[0]) {
setSelectedReturnId(payload.data[0].id);
setIntake({ ...emptyIntake, ...(payload.data[0].summary?.intake ?? {}) });
setReadiness(payload.data[0].summary?.intakeReadiness ?? null);
}
setStatus("");
};
useEffect(() => {
loadReturns().catch(() => {
setStatus("Unable to load returns.");
});
}, []);
const createReturn = async () => {
setStatus("Creating return...");
const payload = {
taxYear: year,
filingType,
jurisdictions
};
const response = await apiFetch<TaxReturn>("/api/tax/returns", {
method: "POST",
body: JSON.stringify(payload)
});
if (response.error) {
setStatus(response.error?.message ?? "Unable to create return.");
return;
}
if (useSample) {
await Promise.all(
sampleProfile.documents.map((document) =>
apiFetch(`/api/tax/returns/${response.data.id}/documents`, {
method: "POST",
body: JSON.stringify({
docType: document.docType,
metadata: { source: "sample", label: document.label, taxpayer: sampleProfile.taxpayer.name },
}),
}),
),
);
}
setStatus("Return created.");
await loadReturns();
setSelectedReturnId(response.data.id);
if (useSample) {
const sampleIntake = intakeFromSample();
setIntake(sampleIntake);
await saveIntake(response.data.id, sampleIntake, true);
}
};
const intakeFromSample = (): TaxIntake => {
const wages = sampleProfile.income.w2.reduce((sum, item) => sum + item.wages, 0);
const business = sampleProfile.income.selfEmployment.reduce((sum, item) => sum + item.income - item.expenses, 0);
const interest = sampleProfile.income.interest.reduce((sum, item) => sum + item.amount, 0);
const dividends = sampleProfile.income.dividends.reduce((sum, item) => sum + item.amount, 0);
return {
taxpayer: {
name: sampleProfile.taxpayer.name,
filingStatus: sampleProfile.taxpayer.filingStatus,
address: sampleProfile.taxpayer.address,
},
income: { wages, business, interest, dividends, total: wages + business + interest + dividends },
deductions: sampleProfile.deductions,
credits: sampleProfile.credits,
notes: "Sample intake generated from John Doe dataset.",
};
};
const selectReturn = async (ret: TaxReturn) => {
setSelectedReturnId(ret.id);
setStatus("Loading intake...");
const response = await apiFetch<{ intake: TaxIntake; readiness: TaxReadiness }>(`/api/tax/returns/${ret.id}/intake`);
if (response.error) {
setStatus(response.error.message ?? "Unable to load intake.");
return;
}
setIntake({ ...emptyIntake, ...(response.data.intake ?? {}) });
setReadiness(response.data.readiness);
setStatus("");
};
const saveIntake = async (returnId = selectedReturnId, nextIntake = intake, submit = false) => {
if (!returnId) {
setStatus("Select a return before saving intake.");
return;
}
const total =
Number(nextIntake.income.wages ?? 0) +
Number(nextIntake.income.business ?? 0) +
Number(nextIntake.income.interest ?? 0) +
Number(nextIntake.income.dividends ?? 0);
const normalized = { ...nextIntake, income: { ...nextIntake.income, total } };
setStatus(submit ? "Submitting intake..." : "Saving intake...");
const response = await apiFetch<{ intake: TaxIntake; readiness: TaxReadiness }>(`/api/tax/returns/${returnId}/intake`, {
method: "PUT",
body: JSON.stringify({ intake: normalized, submit }),
});
if (response.error) {
setStatus(response.error.message ?? "Unable to save intake.");
return;
}
setIntake(response.data.intake);
setReadiness(response.data.readiness);
setStatus(submit ? "Intake submitted." : "Intake saved.");
await loadReturns();
};
const updateIntake = (section: keyof TaxIntake, key: string, value: string | number | boolean) => {
setIntake((current) => ({
...current,
[section]: {
...(current[section] as Record<string, unknown>),
[key]: value,
},
}));
};
const exportReturn = async (id: string) => {
setStatus("Exporting return...");
const response = await apiFetch<{
return: TaxReturn;
documents: unknown[];
}>(`/api/tax/returns/${id}/export`, { method: "POST" });
if (response.error) {
setStatus(response.error?.message ?? "Export failed.");
return;
}
const blob = new Blob([JSON.stringify(response.data, null, 2)], {
type: "application/json"
});
const url = URL.createObjectURL(blob);
window.open(url, "_blank", "noopener,noreferrer");
setStatus("Export ready.");
await loadReturns();
};
const submitEFile = async (id: string) => {
setStatus("Submitting e-file package...");
const response = await apiFetch<{ eFile: EFileStatus }>(`/api/tax/returns/${id}/efile`, {
method: "POST",
body: JSON.stringify({ consentAccepted: efileConsent }),
});
if (response.error) {
setStatus(response.error?.message ?? "E-file submission failed.");
return;
}
setStatus(`E-file submitted to ${response.data.eFile.provider ?? "provider"}.`);
await loadReturns();
};
const refreshEFileStatus = async (id: string) => {
setStatus("Checking e-file status...");
const response = await apiFetch<{ eFile?: EFileStatus; status?: string }>(`/api/tax/returns/${id}/efile`);
if (response.error) {
setStatus(response.error?.message ?? "Unable to check e-file status.");
return;
}
setStatus(response.data.eFile?.status ? `E-file status: ${response.data.eFile.status}.` : "Return has not been e-filed.");
await loadReturns();
};
return (
<AppShell title="Tax" subtitle="Prepare returns and export audit-ready packages.">
<div className="grid gap-6 lg:grid-cols-[1.05fr_0.95fr]">
<div className="glass-panel p-6 rounded-2xl shadow-sm">
<h2 className="text-lg font-bold text-foreground">Create a return</h2>
<div className="mt-4 grid gap-4">
<div className="rounded-xl border border-border bg-background/50 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-foreground">Sample dataset</p>
<p className="text-xs text-muted-foreground">Use John Doe sample intake data.</p>
</div>
<button
type="button"
onClick={() => setUseSample((value) => !value)}
className={`rounded-full px-3 py-1 text-xs font-semibold transition-colors ${useSample ? "bg-primary text-primary-foreground" : "bg-secondary text-muted-foreground"
}`}
>
{useSample ? "On" : "Off"}
</button>
</div>
{useSample ? (
<div className="mt-4 text-xs text-muted-foreground">
<p>W-2 wages: $92,000 - 1099-NEC: $28,000</p>
<p>Standard deduction - CA + NY filings</p>
</div>
) : null}
</div>
<div>
<label className="text-xs uppercase tracking-[0.2em] text-muted-foreground font-semibold">Tax year</label>
<input
className="mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-3 text-sm text-foreground focus:border-primary focus:ring-primary"
type="number"
value={year}
onChange={(event) => setYear(Number(event.target.value))}
/>
</div>
<div>
<label className="text-xs uppercase tracking-[0.2em] text-muted-foreground font-semibold">
Filing type
</label>
<select
className="mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-3 text-sm text-foreground focus:border-primary focus:ring-primary"
value={filingType}
onChange={(event) =>
setFilingType(event.target.value as "individual" | "business")
}
>
<option value="individual">Individual (1040)</option>
<option value="business">Business (1120/1065)</option>
</select>
</div>
<div>
<label className="text-xs uppercase tracking-[0.2em] text-muted-foreground font-semibold">
Jurisdictions (states)
</label>
<select
className="mt-2 h-44 w-full rounded-xl border border-border bg-background/50 px-4 py-3 text-sm text-foreground focus:border-primary focus:ring-primary"
multiple
value={jurisdictions}
onChange={(event) =>
setJurisdictions(
Array.from(event.target.selectedOptions).map((option) => option.value)
)
}
>
{states.map((state) => (
<option key={state.code} value={state.code}>
{state.name} ({state.code})
</option>
))}
</select>
<p className="mt-2 text-xs text-muted-foreground">
Hold Ctrl/Command to select multiple states.
</p>
</div>
<div className="rounded-xl border border-border bg-background/50 p-4">
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground font-semibold">Required documents</p>
<div className="mt-3 grid gap-2 text-xs text-muted-foreground">
{sampleProfile.documents.map((doc) => (
<div key={doc.docType} className="flex items-center justify-between">
<span>{doc.label}</span>
<span className="rounded-full bg-secondary px-2 py-1 text-foreground font-medium">Pending</span>
</div>
))}
</div>
</div>
<button
type="button"
onClick={createReturn}
className="rounded-xl bg-primary px-4 py-3 text-sm font-bold text-primary-foreground hover:bg-primary/90 transition-colors"
>
Create return
</button>
{status ? <p className="text-xs font-medium text-primary">{status}</p> : null}
</div>
</div>
<div className="glass-panel p-6 rounded-2xl shadow-sm">
<h2 className="text-lg font-bold text-foreground">Your returns</h2>
<div className="mt-4 space-y-4">
{loading ? (
<p className="text-sm text-muted-foreground">Loading returns...</p>
) : returns.length ? (
returns.map((ret) => (
<div key={ret.id} className="rounded-xl border border-border bg-background/50 p-4">
<div className="flex items-center justify-between">
<div>
<p className="font-bold text-foreground">
{ret.taxYear} - {ret.filingType}
</p>
<p className="text-xs text-muted-foreground">
States: {ret.jurisdictions.join(", ")}
</p>
{ret.documents?.length ? (
<p className="text-xs text-muted-foreground">
Documents: {ret.documents.length}
</p>
) : null}
</div>
<span className="rounded-full bg-secondary px-2 py-1 text-xs font-medium text-foreground">
{ret.status}
</span>
</div>
<button
type="button"
onClick={() => selectReturn(ret)}
className="mt-3 rounded-lg border border-border bg-secondary/30 px-3 py-2 text-xs font-bold text-foreground hover:bg-secondary transition-colors"
>
Open intake
</button>
<button
type="button"
onClick={() => exportReturn(ret.id)}
className="ml-2 mt-3 rounded-lg bg-primary px-3 py-2 text-xs font-bold text-primary-foreground hover:bg-primary/90 transition-colors"
>
Export package
</button>
<div className="mt-3 flex flex-wrap items-center gap-2">
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<input type="checkbox" checked={efileConsent} onChange={(event) => setEfileConsent(event.target.checked)} />
E-file consent
</label>
<button
type="button"
onClick={() => submitEFile(ret.id)}
className="rounded-lg bg-primary px-3 py-2 text-xs font-bold text-primary-foreground hover:bg-primary/90 transition-colors"
>
Submit e-file
</button>
<button
type="button"
onClick={() => refreshEFileStatus(ret.id)}
className="rounded-lg border border-border bg-secondary/30 px-3 py-2 text-xs font-bold text-foreground hover:bg-secondary transition-colors"
>
Check status
</button>
</div>
{ret.summary?.eFile?.submissionId ? (
<p className="mt-2 text-xs text-muted-foreground">
E-file: {ret.summary.eFile.status} - {ret.summary.eFile.submissionId}
</p>
) : null}
</div>
))
) : (
<p className="text-sm text-muted-foreground">No returns yet.</p>
)}
</div>
</div>
</div>
<div className="mt-6 glass-panel p-6 rounded-2xl shadow-sm">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h2 className="text-lg font-bold text-foreground">Tax intake</h2>
<p className="mt-1 text-sm text-muted-foreground">
Capture taxpayer details, income, deductions, and credits before export.
</p>
</div>
{readiness ? (
<span className={`rounded-full px-3 py-1 text-xs font-semibold ${readiness.complete ? "bg-primary/10 text-primary" : "bg-yellow-500/10 text-yellow-500"}`}>
{readiness.complete ? "Ready" : `${readiness.missingFields.length} missing`}
</span>
) : null}
</div>
{selectedReturnId ? (
<div className="mt-5 grid gap-5 lg:grid-cols-4">
<div className="grid gap-3">
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground font-semibold">Taxpayer</p>
<input className="rounded-xl border border-border bg-background/50 px-4 py-3 text-sm" placeholder="Name" value={intake.taxpayer.name ?? ""} onChange={(e) => updateIntake("taxpayer", "name", e.target.value)} />
<input className="rounded-xl border border-border bg-background/50 px-4 py-3 text-sm" placeholder="Filing status" value={intake.taxpayer.filingStatus ?? ""} onChange={(e) => updateIntake("taxpayer", "filingStatus", e.target.value)} />
<input className="rounded-xl border border-border bg-background/50 px-4 py-3 text-sm" placeholder="Address" value={intake.taxpayer.address ?? ""} onChange={(e) => updateIntake("taxpayer", "address", e.target.value)} />
</div>
<div className="grid gap-3">
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground font-semibold">Income</p>
<input className="rounded-xl border border-border bg-background/50 px-4 py-3 text-sm" type="number" placeholder="Wages" value={intake.income.wages ?? 0} onChange={(e) => updateIntake("income", "wages", Number(e.target.value))} />
<input className="rounded-xl border border-border bg-background/50 px-4 py-3 text-sm" type="number" placeholder="Business net" value={intake.income.business ?? 0} onChange={(e) => updateIntake("income", "business", Number(e.target.value))} />
<input className="rounded-xl border border-border bg-background/50 px-4 py-3 text-sm" type="number" placeholder="Interest" value={intake.income.interest ?? 0} onChange={(e) => updateIntake("income", "interest", Number(e.target.value))} />
<input className="rounded-xl border border-border bg-background/50 px-4 py-3 text-sm" type="number" placeholder="Dividends" value={intake.income.dividends ?? 0} onChange={(e) => updateIntake("income", "dividends", Number(e.target.value))} />
</div>
<div className="grid gap-3">
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground font-semibold">Deductions / Credits</p>
<label className="flex items-center gap-2 text-sm text-muted-foreground">
<input type="checkbox" checked={Boolean(intake.deductions.standard)} onChange={(e) => updateIntake("deductions", "standard", e.target.checked)} />
Standard deduction
</label>
<input className="rounded-xl border border-border bg-background/50 px-4 py-3 text-sm" type="number" placeholder="Charitable" value={intake.deductions.charitable ?? 0} onChange={(e) => updateIntake("deductions", "charitable", Number(e.target.value))} />
<input className="rounded-xl border border-border bg-background/50 px-4 py-3 text-sm" type="number" placeholder="Student loan interest" value={intake.deductions.studentLoanInterest ?? 0} onChange={(e) => updateIntake("deductions", "studentLoanInterest", Number(e.target.value))} />
<input className="rounded-xl border border-border bg-background/50 px-4 py-3 text-sm" type="number" placeholder="Education credit" value={intake.credits.education ?? 0} onChange={(e) => updateIntake("credits", "education", Number(e.target.value))} />
</div>
<div className="grid gap-3">
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground font-semibold">Review</p>
<textarea className="min-h-32 rounded-xl border border-border bg-background/50 px-4 py-3 text-sm" placeholder="Reviewer notes" value={intake.notes ?? ""} onChange={(e) => setIntake((current) => ({ ...current, notes: e.target.value }))} />
{readiness?.missingFields?.length ? (
<p className="text-xs text-yellow-500">Missing: {readiness.missingFields.join(", ")}</p>
) : null}
<div className="flex gap-2">
<button type="button" onClick={() => saveIntake()} className="rounded-lg border border-border bg-secondary/30 px-3 py-2 text-xs font-bold text-foreground hover:bg-secondary">Save draft</button>
<button type="button" onClick={() => saveIntake(selectedReturnId, intake, true)} className="rounded-lg bg-primary px-3 py-2 text-xs font-bold text-primary-foreground hover:bg-primary/90">Submit intake</button>
</div>
</div>
</div>
) : (
<p className="mt-4 text-sm text-muted-foreground">Create or select a return to start intake.</p>
)}
</div>
</AppShell>
);
}