573 lines
25 KiB
TypeScript
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>
|
|
);
|
|
}
|