Initial commit - ERP System
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// /opt/erp-system/app/billing/[customerId]/page.tsx
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, Printer } from 'lucide-react';
|
||||
import { jsPDF } from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
|
||||
export default function InvoiceDraftPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const customerId = params.customerId;
|
||||
|
||||
const [entries, setEntries] = useState<any[]>([]);
|
||||
const [customer, setCustomer] = useState<any>(null);
|
||||
const [settings, setSettings] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (customerId) fetchData();
|
||||
}, [customerId]);
|
||||
|
||||
const fetchData = async () => {
|
||||
const [billingRes, settingsRes] = await Promise.all([
|
||||
fetch('/api/billing'),
|
||||
fetch('/api/settings')
|
||||
]);
|
||||
|
||||
if (billingRes.ok && settingsRes.ok) {
|
||||
const allEntries = await billingRes.json();
|
||||
const filtered = allEntries.filter((e: any) => e.ticket.customerId === parseInt(customerId as string));
|
||||
setEntries(filtered);
|
||||
if (filtered.length > 0) setCustomer(filtered[0].ticket.customer);
|
||||
setSettings(await settingsRes.json());
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleUpdateEntry = async (id: number, field: string, value: any) => {
|
||||
const updatedEntries = entries.map(e => e.id === id ? { ...e, [field]: value } : e);
|
||||
setEntries(updatedEntries);
|
||||
const entry = updatedEntries.find(e => e.id === id);
|
||||
await fetch(`/api/time-entries/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ description: entry.description, durationMins: entry.durationMins })
|
||||
});
|
||||
};
|
||||
|
||||
const generatePDF = () => {
|
||||
const doc = new jsPDF();
|
||||
const dateStr = new Date().toLocaleDateString('de-DE');
|
||||
const invNo = `RE-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`;
|
||||
|
||||
doc.setFontSize(20);
|
||||
doc.text(settings.companyName || "ERP SYSTEM", 14, 20);
|
||||
doc.setFontSize(9);
|
||||
doc.text(settings.companyInfo || "", 14, 30);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.text("Rechnung an:", 14, 55);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.text(customer.companyName || `${customer.firstName} ${customer.lastName}`, 14, 62);
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.text(`${customer.address || ''}\n${customer.zipCode || ''} ${customer.city || ''}`, 14, 68);
|
||||
|
||||
doc.text(`Datum: ${dateStr}`, 140, 62);
|
||||
doc.text(`Rechnungs-Nr: ${invNo}`, 140, 68);
|
||||
|
||||
const tableData = entries.map(e => [
|
||||
new Date(e.createdAt).toLocaleDateString('de-DE'),
|
||||
`${e.ticket.title}\n${e.description}`,
|
||||
(e.durationMins / 60).toFixed(2) + " h",
|
||||
settings.hourlyRate.toFixed(2) + " €",
|
||||
((e.durationMins / 60) * settings.hourlyRate).toFixed(2) + " €"
|
||||
]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: 90,
|
||||
head: [['Datum', 'Leistung', 'Menge', 'Einzelpreis', 'Gesamt']],
|
||||
body: tableData,
|
||||
theme: 'striped',
|
||||
headStyles: { fillColor: [79, 70, 229] }
|
||||
});
|
||||
|
||||
const netTotal = entries.reduce((sum, e) => sum + (e.durationMins / 60 * settings.hourlyRate), 0);
|
||||
const tax = netTotal * (settings.taxRate / 100);
|
||||
const grossTotal = netTotal + tax;
|
||||
|
||||
const finalY = (doc as any).lastAutoTable.finalY + 10;
|
||||
doc.text(`Netto Gesamt:`, 140, finalY);
|
||||
doc.text(`${netTotal.toFixed(2)} €`, 180, finalY, { align: 'right' });
|
||||
doc.text(`USt. ${settings.taxRate}%:`, 140, finalY + 6);
|
||||
doc.text(`${tax.toFixed(2)} €`, 180, finalY + 6, { align: 'right' });
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.text(`Rechnungsbetrag:`, 140, finalY + 14);
|
||||
doc.text(`${grossTotal.toFixed(2)} €`, 180, finalY + 14, { align: 'right' });
|
||||
|
||||
doc.save(`Rechnung_${invNo}.pdf`);
|
||||
};
|
||||
|
||||
if (loading) return <div className="p-8">Lade Entwurf...</div>;
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<button onClick={() => router.back()} className="text-slate-500 hover:text-slate-800 flex items-center gap-2 transition">
|
||||
<ArrowLeft className="w-4 h-4" /> Abbrechen
|
||||
</button>
|
||||
<button onClick={generatePDF} className="bg-indigo-600 text-white px-6 py-2.5 rounded-lg font-bold shadow-lg flex items-center gap-2 hover:bg-indigo-700 transition">
|
||||
<Printer className="w-5 h-5" /> PDF erstellen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200">
|
||||
<h1 className="text-2xl font-bold text-slate-900 mb-6">Rechnung korrigieren</h1>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50 text-slate-600 border-y border-slate-100">
|
||||
<tr>
|
||||
<th className="py-3 px-4 text-left">Beschreibung</th>
|
||||
<th className="py-3 px-4 text-right">Minuten</th>
|
||||
<th className="py-3 px-4 text-right">Betrag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{entries.map((entry) => (
|
||||
<tr key={entry.id}>
|
||||
<td className="py-4 px-4">
|
||||
<textarea className="w-full border-none p-0 focus:ring-0 text-slate-700 bg-transparent resize-none" rows={2} value={entry.description} onChange={(e) => handleUpdateEntry(entry.id, 'description', e.target.value)} />
|
||||
</td>
|
||||
<td className="py-4 px-4 text-right">
|
||||
<input type="number" step="15" className="w-20 text-right border border-slate-200 rounded p-1" value={entry.durationMins} onChange={(e) => handleUpdateEntry(entry.id, 'durationMins', parseInt(e.target.value))} />
|
||||
</td>
|
||||
<td className="py-4 px-4 text-right font-mono">{(entry.durationMins / 60 * settings.hourlyRate).toFixed(2)} €</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
// /opt/erp-system/app/billing/page.tsx
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FileText, CheckCircle, Clock, User as UserIcon, Printer, Search } from 'lucide-react';
|
||||
import { jsPDF } from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import { useToast } from '../components/ToastProvider';
|
||||
|
||||
export default function T2medBillingPage() {
|
||||
const [groups, setGroups] = useState<any[]>([]);
|
||||
const [settings, setSettings] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const { toast } = useToast();
|
||||
|
||||
// T2med Split-Screen States
|
||||
const [activeCustomer, setActiveCustomer] = useState<any>(null);
|
||||
const [draftEntries, setDraftEntries] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
const [billingRes, settingsRes] = await Promise.all([
|
||||
fetch('/api/billing'),
|
||||
fetch('/api/settings')
|
||||
]);
|
||||
|
||||
if (billingRes.ok && settingsRes.ok) {
|
||||
const entries = await billingRes.json();
|
||||
const loadedSettings = await settingsRes.json();
|
||||
setSettings(loadedSettings);
|
||||
|
||||
// Gruppieren nach Kunde
|
||||
const groupedData = entries.reduce((acc: any, entry: any) => {
|
||||
const cid = entry.ticket.customerId;
|
||||
if (!acc[cid]) {
|
||||
acc[cid] = { customer: entry.ticket.customer, entries: [], totalMins: 0 };
|
||||
}
|
||||
acc[cid].entries.push(entry);
|
||||
acc[cid].totalMins += entry.durationMins;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
setGroups(Object.values(groupedData));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// Aktiviert den Kunden für die rechte Bildschirmhälfte
|
||||
const selectCustomer = (group: any) => {
|
||||
setActiveCustomer(group.customer);
|
||||
// Erstelle eine tiefe Kopie der Einträge für das Inline-Editing
|
||||
setDraftEntries(JSON.parse(JSON.stringify(group.entries)));
|
||||
};
|
||||
|
||||
// Inline-Editing im Rechnungsblatt
|
||||
const handleUpdateDraft = async (id: number, field: string, value: any) => {
|
||||
const updated = draftEntries.map(e => e.id === id ? { ...e, [field]: value } : e);
|
||||
setDraftEntries(updated);
|
||||
|
||||
// Speichere die Änderung sofort im Hintergrund in der DB ab (T2med-Feeling)
|
||||
const entry = updated.find(e => e.id === id);
|
||||
await fetch(`/api/time-entries/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ description: entry.description, durationMins: entry.durationMins })
|
||||
});
|
||||
|
||||
// Aktualisiere die linke Liste leise mit den neuen Summen
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const markAsBilled = async () => {
|
||||
const entryIds = draftEntries.map(e => e.id);
|
||||
const res = await fetch('/api/billing', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ entryIds })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setActiveCustomer(null);
|
||||
setDraftEntries([]);
|
||||
fetchData();
|
||||
} else {
|
||||
toast('Fehler beim Abschließen.', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const generatePDF = () => {
|
||||
if (!settings) return;
|
||||
const doc = new jsPDF();
|
||||
const dateStr = new Date().toLocaleDateString('de-DE');
|
||||
const yyyy = new Date().getFullYear();
|
||||
const mm = String(new Date().getMonth() + 1).padStart(2, '0');
|
||||
const seq = String(Date.now()).slice(-5);
|
||||
const invNo = `RE-${yyyy}${mm}-${seq}`;
|
||||
|
||||
doc.setFontSize(20);
|
||||
doc.text(settings.companyName || "ERP SYSTEM", 14, 20);
|
||||
doc.setFontSize(9);
|
||||
doc.text(settings.companyInfo || "", 14, 30);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.text("Rechnung an:", 14, 55);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.text(activeCustomer.companyName || `${activeCustomer.firstName} ${activeCustomer.lastName}`, 14, 62);
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.text(`${activeCustomer.address || ''}\n${activeCustomer.zipCode || ''} ${activeCustomer.city || ''}`, 14, 68);
|
||||
|
||||
doc.text(`Datum: ${dateStr}`, 140, 62);
|
||||
doc.text(`Rechnungs-Nr: ${invNo}`, 140, 68);
|
||||
|
||||
const tableData = draftEntries.map(e => [
|
||||
new Date(e.createdAt).toLocaleDateString('de-DE'),
|
||||
`${e.ticket.title}\n${e.description}`,
|
||||
(e.durationMins / 60).toFixed(2) + " h",
|
||||
settings.hourlyRate.toFixed(2) + " €",
|
||||
((e.durationMins / 60) * settings.hourlyRate).toFixed(2) + " €"
|
||||
]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: 90,
|
||||
head: [['Datum', 'Leistung', 'Menge', 'Einzelpreis', 'Gesamt']],
|
||||
body: tableData,
|
||||
theme: 'striped',
|
||||
headStyles: { fillColor: [79, 70, 229] },
|
||||
styles: { cellPadding: 4, fontSize: 9 }
|
||||
});
|
||||
|
||||
const netTotal = draftEntries.reduce((sum, e) => sum + (e.durationMins / 60 * settings.hourlyRate), 0);
|
||||
const tax = netTotal * (settings.taxRate / 100);
|
||||
const grossTotal = netTotal + tax;
|
||||
|
||||
const finalY = (doc as any).lastAutoTable.finalY + 10;
|
||||
doc.text(`Netto Gesamt:`, 140, finalY);
|
||||
doc.text(`${netTotal.toFixed(2)} €`, 180, finalY, { align: 'right' });
|
||||
doc.text(`USt. ${settings.taxRate}%:`, 140, finalY + 6);
|
||||
doc.text(`${tax.toFixed(2)} €`, 180, finalY + 6, { align: 'right' });
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.text(`Rechnungsbetrag:`, 140, finalY + 14);
|
||||
doc.text(`${grossTotal.toFixed(2)} €`, 180, finalY + 14, { align: 'right' });
|
||||
|
||||
doc.save(`Rechnung_${invNo}.pdf`);
|
||||
};
|
||||
|
||||
const filteredGroups = groups.filter(g => {
|
||||
const name = g.customer.companyName || `${g.customer.firstName} ${g.customer.lastName}`;
|
||||
return name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
});
|
||||
|
||||
if (loading && groups.length === 0) return <div className="p-8 font-medium text-slate-500 animate-pulse">Lade Abrechnungs-Modul...</div>;
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-8rem)] -m-8 bg-white border-t border-slate-200 animate-fade-in-up">
|
||||
|
||||
{/* LINKE SPALTE: Kunden-Liste (T2med-Style) */}
|
||||
<div className="w-1/3 min-w-[320px] max-w-sm border-r border-slate-200 bg-slate-50/50 flex flex-col h-full shadow-[4px_0_24px_rgba(0,0,0,0.02)] z-10">
|
||||
<div className="p-4 border-b border-slate-200 bg-white shadow-sm z-10">
|
||||
<h2 className="text-lg font-bold text-slate-800 flex items-center gap-2 mb-3">
|
||||
<FileText className="w-5 h-5 text-indigo-600" /> Abrechnungslauf
|
||||
</h2>
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Kunde suchen..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 bg-slate-100/80 border border-slate-200 rounded-xl text-sm focus:bg-white focus:border-indigo-400 focus:ring-4 focus:ring-indigo-500/10 outline-none transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto divide-y divide-slate-100">
|
||||
{filteredGroups.length === 0 ? (
|
||||
<div className="p-6 text-center text-slate-500 text-sm">Keine offenen Positionen.</div>
|
||||
) : (
|
||||
filteredGroups.map((group, idx) => {
|
||||
const name = group.customer.companyName || `${group.customer.firstName} ${group.customer.lastName}`;
|
||||
const isActive = activeCustomer?.id === group.customer.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => selectCustomer(group)}
|
||||
className={`w-full text-left p-4 transition-all border-l-4 ${isActive ? 'bg-indigo-50 border-indigo-600' : 'bg-transparent border-transparent hover:bg-slate-100'}`}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<span className={`font-semibold ${isActive ? 'text-indigo-900' : 'text-slate-800'}`}>{name}</span>
|
||||
<span className="text-xs font-bold text-slate-500 bg-white px-2 py-1 rounded-md shadow-sm border border-slate-200">
|
||||
{group.entries.length} Pos.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-2 text-sm text-slate-500">
|
||||
<span className="flex items-center gap-1"><Clock className="w-3.5 h-3.5" /> {(group.totalMins / 60).toFixed(1)} h</span>
|
||||
{settings && (
|
||||
<span className="font-mono text-indigo-600 font-medium">{((group.totalMins / 60) * settings.hourlyRate).toFixed(2)} €</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RECHTE SPALTE: Das aktive Rechnungsblatt */}
|
||||
<div className="flex-1 bg-slate-100/50 flex flex-col h-full overflow-hidden relative">
|
||||
{!activeCustomer ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-slate-400 animate-fade-in-up">
|
||||
<UserIcon className="w-16 h-16 mb-4 opacity-20" />
|
||||
<p className="font-medium text-lg text-slate-500">Bitte wähle links einen Kunden aus</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto p-8 relative">
|
||||
|
||||
{/* Action Bar (Oben rechts, sticky) */}
|
||||
<div className="absolute top-8 right-8 flex gap-3 z-10">
|
||||
<button onClick={generatePDF} className="bg-white border border-slate-200 text-indigo-600 px-4 py-2 rounded-xl font-bold shadow-sm hover:shadow-md hover:border-indigo-200 transition-all flex items-center gap-2 text-sm hover-lift">
|
||||
<Printer className="w-4 h-4" /> PDF erstellen
|
||||
</button>
|
||||
<button onClick={markAsBilled} className="bg-emerald-600 text-white px-5 py-2 rounded-xl font-bold shadow-lg shadow-emerald-600/20 hover:bg-emerald-700 transition-all flex items-center gap-2 text-sm hover-lift">
|
||||
<CheckCircle className="w-4 h-4" /> Als erledigt markieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Das "Papier"-Dokument */}
|
||||
<div className="bg-white max-w-4xl mx-auto min-h-[800px] shadow-2xl shadow-slate-300/40 border border-slate-100 rounded-sm p-12 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="border-b border-slate-200 pb-8 mb-8 flex justify-between items-start">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 mb-2">Rechnungsentwurf</h1>
|
||||
<p className="text-lg text-slate-600 font-medium">{activeCustomer.companyName || `${activeCustomer.firstName} ${activeCustomer.lastName}`}</p>
|
||||
<p className="text-sm text-slate-500">{activeCustomer.address || 'Keine Adresse hinterlegt'}</p>
|
||||
</div>
|
||||
<div className="text-right text-slate-500 text-sm">
|
||||
<p>Stundensatz: <strong className="text-slate-800">{settings?.hourlyRate} €</strong></p>
|
||||
<p>MwSt: <strong className="text-slate-800">{settings?.taxRate} %</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* T2med-Style Inline-Editing Tabelle */}
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-12 gap-4 pb-3 border-b-2 border-slate-800 font-bold text-slate-700 text-sm">
|
||||
<div className="col-span-2">Datum</div>
|
||||
<div className="col-span-6">Leistung</div>
|
||||
<div className="col-span-2 text-right">Minuten</div>
|
||||
<div className="col-span-2 text-right">Betrag</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-100">
|
||||
{draftEntries.map((entry) => (
|
||||
<div key={entry.id} className="grid grid-cols-12 gap-4 py-3 group hover:bg-indigo-50/50 transition-colors -mx-4 px-4 rounded-lg">
|
||||
<div className="col-span-2 text-sm text-slate-500 pt-1.5">
|
||||
{new Date(entry.createdAt).toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
<p className="text-xs text-indigo-500 font-bold mb-1">Ticket #{entry.ticket.id}</p>
|
||||
<textarea
|
||||
className="w-full bg-transparent border border-transparent group-hover:border-slate-200 focus:bg-white focus:border-indigo-300 focus:ring-2 focus:ring-indigo-100 rounded p-1.5 text-sm text-slate-800 outline-none resize-none transition"
|
||||
rows={2}
|
||||
value={entry.description}
|
||||
onChange={(e) => handleUpdateDraft(entry.id, 'description', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 text-right pt-1.5">
|
||||
<input
|
||||
type="number"
|
||||
step="15"
|
||||
className="w-16 text-right bg-transparent border border-transparent group-hover:border-slate-200 focus:bg-white focus:border-indigo-300 focus:ring-2 focus:ring-indigo-100 rounded p-1 text-sm text-slate-800 font-mono outline-none transition inline-block"
|
||||
value={entry.durationMins}
|
||||
onChange={(e) => handleUpdateDraft(entry.id, 'durationMins', parseInt(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 text-right pt-2 font-mono text-slate-700 font-medium">
|
||||
{settings ? ((entry.durationMins / 60) * settings.hourlyRate).toFixed(2) : '0.00'} €
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-4 border-t-2 border-slate-800 grid grid-cols-12 gap-4">
|
||||
<div className="col-span-10 text-right font-bold text-slate-700">Netto Gesamt:</div>
|
||||
<div className="col-span-2 text-right font-bold text-slate-900 font-mono text-lg">
|
||||
{settings ? draftEntries.reduce((sum, e) => sum + (e.durationMins / 60 * settings.hourlyRate), 0).toFixed(2) : '0.00'} €
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user