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

243 lines
9.1 KiB
TypeScript

"use client";
import { 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 ExportData = { status: string; csv?: string; rowCount?: number };
export default function ExportsPage() {
const [status, setStatus] = useState("");
const [datePreset, setDatePreset] = useState("custom");
const [filters, setFilters] = useState({
startDate: "",
endDate: "",
minAmount: "",
maxAmount: "",
category: "",
source: "",
includeHidden: false
});
const applyPreset = (preset: string) => {
setDatePreset(preset);
if (preset === "custom") {
return;
}
const now = new Date();
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate());
let start = new Date(end);
if (preset === "this_month") {
start = new Date(end.getFullYear(), end.getMonth(), 1);
} else if (preset === "last_month") {
start = new Date(end.getFullYear(), end.getMonth() - 1, 1);
end.setDate(0);
} else if (preset === "last_6_months") {
start = new Date(end.getFullYear(), end.getMonth() - 5, 1);
} else if (preset === "last_year") {
start = new Date(end.getFullYear() - 1, 0, 1);
end.setMonth(11, 31);
}
const format = (value: Date) => value.toISOString().slice(0, 10);
setFilters((prev) => ({
...prev,
startDate: format(start),
endDate: format(end)
}));
};
const onExport = async () => {
setStatus("Generating export...");
const userId = localStorage.getItem("ledgerone_user_id");
const params = new URLSearchParams();
if (userId) {
params.set("user_id", userId);
}
if (filters.startDate) {
params.set("start_date", filters.startDate);
}
if (filters.endDate) {
params.set("end_date", filters.endDate);
}
if (filters.minAmount) {
params.set("min_amount", filters.minAmount);
}
if (filters.maxAmount) {
params.set("max_amount", filters.maxAmount);
}
if (filters.category) {
params.set("category", filters.category);
}
if (filters.source) {
params.set("source", filters.source);
}
if (filters.includeHidden) {
params.set("include_hidden", "true");
}
const query = params.toString() ? `?${params.toString()}` : "";
try {
const res = await fetch(`/api/exports/csv${query}`);
const payload = (await res.json()) as ApiResponse<ExportData>;
if (!res.ok || payload.error) {
setStatus(payload.error?.message ?? "Export failed.");
return;
}
if (payload.data.csv) {
const blob = new Blob([payload.data.csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
window.open(url, "_blank", "noopener,noreferrer");
setStatus(`Export ready (${payload.data.rowCount ?? 0} rows).`);
} else {
setStatus("Export ready.");
}
} catch {
setStatus("Export failed.");
}
};
return (
<AppShell
title="Exports"
subtitle="Generate CSV datasets with raw and derived fields."
>
<div className="glass-panel p-8 rounded-2xl shadow-sm">
<div className="grid gap-4 md:grid-cols-3">
<div>
<label className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">Start date</label>
<input
type="date"
value={filters.startDate}
onChange={(event) =>
setFilters((prev) => ({ ...prev, startDate: event.target.value }))
}
className="mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-2 text-sm text-foreground focus:border-primary focus:ring-primary"
disabled={datePreset !== "custom"}
/>
</div>
<div>
<label className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">End date</label>
<input
type="date"
value={filters.endDate}
onChange={(event) =>
setFilters((prev) => ({ ...prev, endDate: event.target.value }))
}
className="mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-2 text-sm text-foreground focus:border-primary focus:ring-primary"
disabled={datePreset !== "custom"}
/>
</div>
<div>
<label className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">Date range</label>
<select
value={datePreset}
onChange={(event) => applyPreset(event.target.value)}
className="mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-2 text-sm text-foreground focus:border-primary focus:ring-primary"
>
<option value="custom">Custom</option>
<option value="this_month">This month</option>
<option value="last_month">Last month</option>
<option value="last_6_months">Last 6 months</option>
<option value="last_year">Last year</option>
</select>
</div>
<div>
<label className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">Category contains</label>
<input
type="text"
value={filters.category}
onChange={(event) =>
setFilters((prev) => ({ ...prev, category: event.target.value }))
}
placeholder="Dining, Payroll, Utilities"
className="mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-2 text-sm text-foreground focus:border-primary focus:ring-primary"
/>
</div>
<div>
<label className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">Min amount</label>
<input
type="number"
value={filters.minAmount}
onChange={(event) =>
setFilters((prev) => ({ ...prev, minAmount: event.target.value }))
}
placeholder="0.00"
className="mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-2 text-sm text-foreground focus:border-primary focus:ring-primary"
/>
</div>
<div>
<label className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">Max amount</label>
<input
type="number"
value={filters.maxAmount}
onChange={(event) =>
setFilters((prev) => ({ ...prev, maxAmount: event.target.value }))
}
placeholder="10000.00"
className="mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-2 text-sm text-foreground focus:border-primary focus:ring-primary"
/>
</div>
<div>
<label className="text-xs text-muted-foreground font-semibold uppercase tracking-wider">Source contains</label>
<input
type="text"
value={filters.source}
onChange={(event) =>
setFilters((prev) => ({ ...prev, source: event.target.value }))
}
placeholder="plaid"
className="mt-2 w-full rounded-xl border border-border bg-background/50 px-4 py-2 text-sm text-foreground focus:border-primary focus:ring-primary"
/>
</div>
</div>
<div className="mt-4 flex flex-wrap items-center gap-4">
<label className="flex items-center gap-2 text-xs text-muted-foreground font-medium">
<input
type="checkbox"
checked={filters.includeHidden}
onChange={(event) =>
setFilters((prev) => ({ ...prev, includeHidden: event.target.checked }))
}
className="rounded border-border text-primary focus:ring-primary"
/>
Include hidden transactions
</label>
</div>
<div className="mt-6 flex flex-wrap items-center gap-3">
<button
type="button"
onClick={onExport}
className="rounded-full bg-primary px-6 py-3 text-sm font-bold text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
>
Generate CSV export
</button>
<button
type="button"
onClick={() =>
setFilters({
startDate: "",
endDate: "",
minAmount: "",
maxAmount: "",
category: "",
source: "",
includeHidden: false
})
}
className="rounded-full border border-border bg-background px-6 py-3 text-sm font-semibold text-foreground hover:bg-secondary transition-colors"
>
Reset filters
</button>
</div>
{status ? <p className="mt-4 text-xs font-medium text-primary">{status}</p> : null}
</div>
</AppShell>
);
}