kundencenter/apps/web/src/app/(app)/rechnungen/faellig/page.tsx

46 lines
3.7 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.

'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { api, errMsg } from '@/lib/api';
import { Alert, Empty, eur, fmt } from '@/components/ui';
interface OverdueInv { id: string; number: string | null; orgName: string; customerNumber: string; dueDate: string | null; totalGrossCents: number }
interface DueContract { id: string; number: string; orgId: string; orgName: string; customerNumber: string; productName: string | null; lastInvoicedAt: string | null; dueSince: string; lastGrossCents: number | null }
interface Data { overdueInvoices: OverdueInv[]; dueContracts: DueContract[] }
export default function Faellig() {
const r = useRouter();
const [d, setD] = useState<Data | null>(null); const [err, setErr] = useState(''); const [busy, setBusy] = useState('');
useEffect(() => { api<Data>('GET', '/invoices/reminders').then(setD).catch((e) => setErr(errMsg(e))); }, []);
async function createFor(c: DueContract) {
setErr(''); setBusy(c.id);
try { const inv = await api<{ id: string }>('POST', '/invoices', { orgId: c.orgId }); r.push(`/rechnungen/${inv.id}?fromContract=${c.id}`); }
catch (x) { setErr(errMsg(x)); setBusy(''); }
}
return (<>
<div className="row between"><h1>Fällig</h1><Link className="btn" href="/rechnungen">← Rechnungen</Link></div>
<p className="muted">Überfällige, bereits ausgestellte Rechnungen und aktive Verträge, bei denen die letzte Rechnung länger zurückliegt als ihre Verlängerungslaufzeit.</p>
{err && <Alert kind="err">{err}</Alert>}
{d === null ? <p className="muted" role="status">Wird geladen …</p> : (<>
<div className="card">
<h2>Überfällige Rechnungen ({d.overdueInvoices.length})</h2>
{d.overdueInvoices.length === 0 ? <Empty title="Keine überfälligen Rechnungen" /> :
<div className="tablewrap"><table><thead><tr><th>Nr.</th><th>Kunde</th><th>Fällig seit</th><th>Betrag</th></tr></thead><tbody>
{d.overdueInvoices.map((i) => (<tr key={i.id}><td className="mono"><Link href={`/rechnungen/${i.id}`}>{i.number}</Link></td><td>{i.orgName} <span className="muted small">({i.customerNumber})</span></td>
<td><span className="badge err"><span aria-hidden="true">▲</span>{i.dueDate ? fmt(i.dueDate) : '–'}</span></td><td>{eur(i.totalGrossCents)}</td></tr>))}
</tbody></table></div>}
</div>
<div className="card">
<h2>Verträge ohne aktuelle Rechnung ({d.dueContracts.length})</h2>
{d.dueContracts.length === 0 ? <Empty title="Kein Vertrag fällig" /> :
<div className="tablewrap"><table><thead><tr><th>Vertrag</th><th>Kunde</th><th>Produkt</th><th>Zuletzt berechnet</th><th>Fällig seit</th><th>Letzter Betrag</th><th></th></tr></thead><tbody>
{d.dueContracts.map((c) => (<tr key={c.id}><td className="mono">{c.number}</td><td>{c.orgName} <span className="muted small">({c.customerNumber})</span></td><td>{c.productName ?? '–'}</td>
<td className="small">{c.lastInvoicedAt ? fmt(c.lastInvoicedAt) : <span className="muted">nie</span>}</td><td><span className="badge warn"><span aria-hidden="true">▲</span>{fmt(c.dueSince)}</span></td>
<td>{c.lastGrossCents === null ? '–' : eur(c.lastGrossCents)}</td><td><button className="btn small" disabled={busy === c.id} onClick={() => createFor(c)}>{busy === c.id ? '…' : 'Rechnung erstellen'}</button></td></tr>))}
</tbody></table></div>}
<p className="muted small">Grundlage ist die Verlängerungslaufzeit des Vertrags, nicht das Abrechnungsintervall des Produkts. Bei manuell abweichender Abrechnung bitte selbst prüfen.</p>
</div>
</>)}
</>);
}