159 lines
7.1 KiB
TypeScript
159 lines
7.1 KiB
TypeScript
'use client'
|
|
import React, { useState } from 'react'
|
|
import PageLayout from '../../../components/PageLayout'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useVatRates } from '../hooks/getTaxes'
|
|
import { importVatCsv } from './hooks/TaxImporter'
|
|
import { exportVatCsv, exportVatPdf } from './hooks/TaxExporter'
|
|
|
|
export default function VatEditPage() {
|
|
const router = useRouter()
|
|
const { rates, loading, error, reload } = useVatRates()
|
|
const [filter, setFilter] = useState('')
|
|
const [importResult, setImportResult] = useState<string | null>(null)
|
|
const [importing, setImporting] = useState(false)
|
|
const [page, setPage] = useState(1)
|
|
const [pageSize, setPageSize] = useState(10)
|
|
|
|
const onImport = async (file?: File | null) => {
|
|
if (!file) return
|
|
setImportResult(null)
|
|
setImporting(true)
|
|
const res = await importVatCsv(file)
|
|
if (res.ok) {
|
|
setImportResult(res.summary ? JSON.stringify(res.summary) : res.message || 'Import successful')
|
|
await reload()
|
|
} else {
|
|
setImportResult(res.message || 'Import failed')
|
|
}
|
|
setImporting(false)
|
|
}
|
|
|
|
const filtered = rates.filter(v =>
|
|
v.country_name.toLowerCase().includes(filter.toLowerCase()) ||
|
|
v.country_code.toLowerCase().includes(filter.toLowerCase())
|
|
)
|
|
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))
|
|
const pageData = filtered.slice((page - 1) * pageSize, page * pageSize)
|
|
|
|
return (
|
|
<PageLayout>
|
|
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
|
|
<div className="rounded-2xl bg-white border border-blue-100 shadow-lg px-8 py-8 flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-extrabold text-blue-900">Edit VAT rates</h1>
|
|
<p className="text-sm text-blue-700">Import, export, and review (dummy data).</p>
|
|
</div>
|
|
<button
|
|
onClick={() => router.push('/admin/finance-management')}
|
|
className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-semibold text-blue-900 hover:bg-gray-50"
|
|
>
|
|
Back
|
|
</button>
|
|
</div>
|
|
|
|
<div className="rounded-2xl bg-white border border-gray-100 shadow-lg p-6 space-y-3">
|
|
<div className="flex flex-wrap gap-2 text-sm items-center">
|
|
<label className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50 cursor-pointer">
|
|
<input
|
|
type="file"
|
|
accept=".csv"
|
|
className="hidden"
|
|
onChange={e => onImport(e.target.files?.[0] || null)}
|
|
disabled={importing}
|
|
/>
|
|
{importing ? 'Importing...' : 'Import CSV'}
|
|
</label>
|
|
<button
|
|
onClick={() => exportVatCsv(rates)}
|
|
className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50"
|
|
>
|
|
Export CSV
|
|
</button>
|
|
<button
|
|
onClick={() => exportVatPdf(rates)}
|
|
className="rounded-lg border border-gray-200 px-3 py-2 hover:bg-gray-50"
|
|
>
|
|
Export PDF
|
|
</button>
|
|
{importResult && <span className="text-xs text-blue-900 break-all">{importResult}</span>}
|
|
</div>
|
|
|
|
<div className="text-sm">
|
|
{error && <div className="mb-3 text-red-600">{error}</div>}
|
|
<input
|
|
value={filter}
|
|
onChange={e => { setFilter(e.target.value); setPage(1); }}
|
|
placeholder="Filter by country or code"
|
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 mb-3 focus:ring-2 focus:ring-blue-900 focus:border-transparent"
|
|
/>
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full text-sm">
|
|
<thead>
|
|
<tr className="bg-blue-50 text-left text-blue-900">
|
|
<th className="px-3 py-2 font-semibold">Country</th>
|
|
<th className="px-3 py-2 font-semibold">Code</th>
|
|
<th className="px-3 py-2 font-semibold">Standard</th>
|
|
<th className="px-3 py-2 font-semibold">Reduced</th>
|
|
<th className="px-3 py-2 font-semibold">Super reduced</th>
|
|
<th className="px-3 py-2 font-semibold">Parking</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{loading && (
|
|
<tr><td colSpan={6} className="px-3 py-4 text-center text-gray-500">Loading VAT rates…</td></tr>
|
|
)}
|
|
{!loading && pageData.map(v => (
|
|
<tr key={v.country_code} className="border-b last:border-0">
|
|
<td className="px-3 py-2">{v.country_name}</td>
|
|
<td className="px-3 py-2">{v.country_code}</td>
|
|
<td className="px-3 py-2">{v.standard_rate ?? '—'}</td>
|
|
<td className="px-3 py-2">{v.reduced_rate_1 ?? '—'}</td>
|
|
<td className="px-3 py-2">{v.super_reduced_rate ?? '—'}</td>
|
|
<td className="px-3 py-2">{v.parking_rate ?? '—'}</td>
|
|
</tr>
|
|
))}
|
|
{!loading && !error && pageData.length === 0 && (
|
|
<tr><td colSpan={6} className="px-3 py-4 text-center text-gray-500">No entries found.</td></tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mt-4 text-sm text-gray-700">
|
|
<div className="flex items-center gap-2">
|
|
<span>Rows per page:</span>
|
|
<select
|
|
value={pageSize}
|
|
onChange={e => { setPageSize(Number(e.target.value)); setPage(1); }}
|
|
className="rounded border border-gray-300 px-2 py-1 text-sm"
|
|
>
|
|
{[10, 20, 50, 100].map(n => <option key={n} value={n}>{n}</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
disabled={page === 1}
|
|
className="px-3 py-1 rounded border border-gray-300 bg-white disabled:opacity-50"
|
|
>
|
|
Prev
|
|
</button>
|
|
<span>Page {page} / {totalPages}</span>
|
|
<button
|
|
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
|
disabled={page === totalPages}
|
|
className="px-3 py-1 rounded border border-gray-300 bg-white disabled:opacity-50"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</PageLayout>
|
|
)
|
|
}
|