Files
kakeibo/frontend/src/features/debt/import-section.tsx
T

126 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useRef, useState } from 'react'
import { CircleCheck, FileUp, Loader2 } from 'lucide-react'
import { toast } from 'sonner'
import { FormError } from '@/components/form-field'
import { Button } from '@/components/ui/button'
import { useDebtImport } from '@/lib/debt'
import { formatDate, formatMoney } from '@/lib/format'
import { formatMonth } from '@/lib/reports'
import { t } from '@/lib/strings'
import { cn } from '@/lib/utils'
import type { DebtImportReport } from '@/types/DebtImportReport'
const s = t.debt
/** Borç verisi dosyası → ön izleme → onay (aynı dosya iki kez eklenmez) */
export function ImportSection() {
const input = useRef<HTMLInputElement>(null)
const run = useDebtImport()
const [json, setJson] = useState<unknown>(null)
const [report, setReport] = useState<DebtImportReport | null>(null)
const [error, setError] = useState<string>()
async function choose(f: File) {
setError(undefined)
setReport(null)
try {
const parsed: unknown = JSON.parse(await f.text())
setJson(parsed)
setReport(await run.mutateAsync({ json: parsed, commit: false }))
} catch (e) {
setError(e instanceof SyntaxError ? 'Dosya okunamadı: geçerli bir JSON değil.' : String(e))
}
}
async function commit() {
try {
const r = await run.mutateAsync({ json, commit: true })
setReport(r)
toast.success(s.imported)
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
}
}
const fresh = report?.rows.filter((r) => r.status === 'new' || r.status === 'update').length ?? 0
const label = (kind: string, l: string) =>
kind === 'snapshot' ? formatDate(l) : kind === 'target' ? formatMonth(l) : l
return (
<div className="space-y-3">
<p className="text-sm text-muted-foreground">{s.importHint}</p>
<input
ref={input}
id="debt-import"
type="file"
accept=".json,application/json"
className="sr-only"
onChange={(e) => {
const f = e.target.files?.[0]
if (f) void choose(f)
e.target.value = ''
}}
/>
<Button asChild variant="outline" disabled={run.isPending}>
<label htmlFor="debt-import" className="cursor-pointer">
<FileUp aria-hidden />
{s.chooseFile}
</label>
</Button>
<FormError message={error} />
{report && (
<div className="space-y-3" aria-live="polite">
{report.errors.length > 0 && (
<ul className="rounded-xl bg-expense/5 p-3 text-sm text-expense">
{report.errors.map((e) => (
<li key={e}>{e}</li>
))}
</ul>
)}
<div className="table-wrap overflow-x-auto">
<table className="w-full text-sm">
<tbody>
{report.rows.map((r, i) => (
<tr key={i} className="border-b last:border-0">
<td className="py-1.5 pr-3 text-muted-foreground">{s.kinds[r.kind]}</td>
<td className="py-1.5 pr-3">{label(r.kind, r.label)}</td>
<td className="money py-1.5 pr-3 text-right whitespace-nowrap">
{formatMoney(r.amount)}
</td>
<td
className={cn(
'py-1.5 whitespace-nowrap',
r.status === 'error'
? 'text-expense'
: r.status === 'exists'
? 'text-muted-foreground'
: 'font-medium',
)}
>
{s.statuses[r.status]}
{r.message && <div className="text-xs">{r.message}</div>}
</td>
</tr>
))}
</tbody>
</table>
</div>
{report.committed ? (
<p className="flex items-center gap-2 font-medium text-income">
<CircleCheck className="size-5" aria-hidden />
{s.imported}
</p>
) : fresh > 0 ? (
<Button onClick={() => void commit()} disabled={run.isPending}>
{run.isPending && <Loader2 className="animate-spin" aria-hidden />}
{s.importCommit(fresh)}
</Button>
) : (
<p className="text-sm text-muted-foreground">{s.nothingNew}</p>
)}
</div>
)}
</div>
)
}