2026-02-24 21:47:18 +00:00

375 lines
14 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { AppShell } from "../../components/app-shell";
type ApiResponse<T> = {
data: T;
meta: { timestamp: string; version: "v1" };
error: null | { message: string; code?: string };
};
type TaxReturn = {
id: string;
taxYear: number;
filingType: "individual" | "business";
jurisdictions: string[];
status: "draft" | "ready" | "exported";
updatedAt: string;
};
export default function TaxPage() {
const [returns, setReturns] = useState<TaxReturn[]>([]);
const [status, setStatus] = useState("");
const [useLocal, setUseLocal] = useState(true);
const [useSample, setUseSample] = useState(true);
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: [
"W-2 (Northwind Labs)",
"1099-INT (City Bank)",
"1099-DIV (Index Fund)",
"1099-NEC (Doe Consulting)",
"Health Insurance 1095-A",
"State withholding statement"
]
};
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 localKey = "ledgerone_tax_returns";
const ensureUserId = () => {
if (typeof window === "undefined") {
return "";
}
let userId = localStorage.getItem("ledgerone_user_id");
if (!userId) {
userId = `demo_${crypto.randomUUID()}`;
localStorage.setItem("ledgerone_user_id", userId);
}
return userId;
};
const loadReturns = async () => {
if (useLocal && typeof window !== "undefined") {
const raw = localStorage.getItem(localKey);
setReturns(raw ? (JSON.parse(raw) as TaxReturn[]) : []);
setStatus("Running in local-only mode.");
return;
}
const userId = ensureUserId();
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
try {
const res = await fetch(`/api/tax/returns${query}`);
const payload = (await res.json()) as ApiResponse<TaxReturn[]>;
if (!res.ok || payload.error) {
throw new Error(payload.error?.message ?? "Unable to load returns.");
}
setReturns(payload.data);
setUseLocal(false);
return;
} catch {
if (typeof window !== "undefined") {
const raw = localStorage.getItem(localKey);
setReturns(raw ? (JSON.parse(raw) as TaxReturn[]) : []);
setUseLocal(true);
setStatus("Running in local-only mode (no backend).");
}
}
};
useEffect(() => {
loadReturns().catch(() => {
setStatus("Unable to load returns.");
});
}, []);
const createReturn = async () => {
const userId = ensureUserId();
setStatus("Creating return...");
const payload = {
userId,
taxYear: year,
filingType,
jurisdictions
};
if (useLocal) {
const nextReturn: TaxReturn = {
id: `local_${crypto.randomUUID()}`,
taxYear: payload.taxYear,
filingType: payload.filingType,
jurisdictions: payload.jurisdictions,
status: "draft",
updatedAt: new Date().toISOString()
};
const next = [...returns, nextReturn];
localStorage.setItem(localKey, JSON.stringify(next));
setReturns(next);
setStatus("Return created locally.");
return;
}
const res = await fetch("/api/tax/returns", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const response = (await res.json()) as ApiResponse<TaxReturn>;
if (!res.ok || response.error) {
setStatus(response.error?.message ?? "Unable to create return.");
return;
}
setStatus("Return created.");
await loadReturns();
};
const exportReturn = async (id: string) => {
setStatus("Exporting return...");
if (useLocal) {
const ret = returns.find((item) => item.id === id);
const payload = {
return: ret,
documents: useSample ? sampleProfile.documents : [],
sampleData: useSample ? sampleProfile : null
};
const blob = new Blob([JSON.stringify(payload, null, 2)], {
type: "application/json"
});
const url = URL.createObjectURL(blob);
window.open(url, "_blank", "noopener,noreferrer");
setStatus("Export ready (local).");
return;
}
const res = await fetch(`/api/tax/returns/${id}/export`, { method: "POST" });
const response = (await res.json()) as ApiResponse<{
return: TaxReturn;
documents: unknown[];
}>;
if (!res.ok || 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();
};
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} className="flex items-center justify-between">
<span>{doc}</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">
{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>
{useSample ? (
<p className="text-xs text-muted-foreground">Sample: John Doe</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={() => exportReturn(ret.id)}
className="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>
))
) : (
<p className="text-sm text-muted-foreground">No returns yet.</p>
)}
</div>
</div>
</div>
</AppShell>
);
}