243 lines
9.1 KiB
TypeScript
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>
|
|
);
|
|
}
|