c063186e80
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
4.4 KiB
TypeScript
126 lines
4.4 KiB
TypeScript
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>
|
||
)
|
||
}
|