feat(T06): Verkaufsmodul + Rechtsdokumente + DATEV-Export + Sales UI
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { ContractPreview } from '@/components/sales/ContractPreview';
|
||||
import { getSale, deleteSale, verifyUstId, type SaleResponse } from '@/lib/sales';
|
||||
|
||||
export default function SaleDetailPage() {
|
||||
const params = useParams();
|
||||
const saleId = params.id as string;
|
||||
|
||||
const [sale, setSale] = useState<SaleResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [ustResult, setUstResult] = useState<string | null>(null);
|
||||
|
||||
const fetchSale = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await getSale(saleId);
|
||||
setSale(data);
|
||||
} catch (err: unknown) {
|
||||
const apiErr = err as { error?: { message?: string } };
|
||||
setError(apiErr?.error?.message || 'Failed to load sale');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [saleId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSale();
|
||||
}, [fetchSale]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm('Verkauf wirklich stornieren?')) return;
|
||||
try {
|
||||
await deleteSale(saleId);
|
||||
fetchSale();
|
||||
} catch (err: unknown) {
|
||||
const apiErr = err as { error?: { message?: string } };
|
||||
setError(apiErr?.error?.message || 'Failed to cancel sale');
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyUstId = async () => {
|
||||
try {
|
||||
const result = await verifyUstId(saleId);
|
||||
setUstResult(`${result.verified ? 'Verifiziert' : 'Nicht verifiziert'}: ${result.message}`);
|
||||
} catch (err: unknown) {
|
||||
const apiErr = err as { error?: { message?: string } };
|
||||
setError(apiErr?.error?.message || 'Failed to verify USt-IdNr.');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div data-testid="sale-detail-loading">Wird geladen...</div>;
|
||||
if (error) return <div className="bg-red-50 text-red-600 p-3 rounded" data-testid="sale-detail-error">{error}</div>;
|
||||
if (!sale) return <div data-testid="sale-detail-not-found">Verkauf nicht gefunden</div>;
|
||||
|
||||
return (
|
||||
<div className="space-y-6" data-testid="sale-detail">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Verkauf #{sale.id.slice(0, 8)}</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleVerifyUstId} data-testid="sale-verify-ust-btn">
|
||||
USt-IdNr. prüfen
|
||||
</Button>
|
||||
<Button onClick={handleDelete} variant="secondary" data-testid="sale-delete-btn">
|
||||
Stornieren
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ustResult && (
|
||||
<div className="bg-blue-50 text-blue-600 p-3 rounded" data-testid="sale-ust-result">
|
||||
{ustResult}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-gray-50 p-4 rounded">
|
||||
<h2 className="font-semibold mb-2">Fahrzeug</h2>
|
||||
{sale.vehicle ? (
|
||||
<div data-testid="sale-detail-vehicle">
|
||||
<p>{sale.vehicle.make} {sale.vehicle.model}</p>
|
||||
<p className="text-sm text-gray-600">FIN: {sale.vehicle.fin}</p>
|
||||
<p className="text-sm text-gray-600">Typ: {sale.vehicle.vehicle_type}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500">Nicht verfügbar</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded">
|
||||
<h2 className="font-semibold mb-2">Käufer</h2>
|
||||
{sale.buyer ? (
|
||||
<div data-testid="sale-detail-buyer">
|
||||
<p>{sale.buyer.company_name}</p>
|
||||
<p className="text-sm text-gray-600">{sale.buyer.address_city}, {sale.buyer.address_country}</p>
|
||||
{sale.buyer.vat_id && <p className="text-sm text-gray-600">USt-IdNr.: {sale.buyer.vat_id}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500">Nicht verfügbar</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded">
|
||||
<h2 className="font-semibold mb-2">Verkaufsdetails</h2>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<span className="text-sm text-gray-500">Preis</span>
|
||||
<p className="font-medium" data-testid="sale-detail-price">
|
||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(sale.sale_price)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500">Datum</span>
|
||||
<p className="font-medium">{new Date(sale.sale_date).toLocaleDateString('de-DE')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500">Status</span>
|
||||
<p className="font-medium">{sale.status}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500">GwG</span>
|
||||
<p className="font-medium">{sale.is_gwg ? 'Ja' : 'Nein'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ContractPreview saleId={sale.id} contractPdfPath={sale.contract_pdf_path} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user