Files
erp-system/app/customers/[id]/page.tsx
T
2026-05-20 18:58:23 +00:00

782 lines
44 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import { useParams, useRouter } from 'next/navigation';
import {
ArrowLeft, Save, Building2, Mail, Key, Users, Plus, Trash2,
Activity, FileText, FileBadge, Download, CheckCircle, Ticket,
Phone, MapPin, Eye, EyeOff, RefreshCw, Copy
} from 'lucide-react';
import { useToast } from '../../components/ToastProvider';
import { getStatusBadge } from '../../components/AppShell';
import { useSession } from 'next-auth/react';
type Tab = 'OVERVIEW' | 'TICKETS' | 'CONTACTS' | 'CONTRACTS' | 'MASTER_DATA' | 'CREDENTIALS' | 'DOCUMENTS' | 'SALES_DOCS';
export default function CustomerDashboard() {
const params = useParams();
const router = useRouter();
const customerId = params.id;
const { toast, confirm } = useToast();
const { data: session } = useSession();
const perms = (session?.user as any)?.permissions || [];
const canEdit = perms.includes('CUSTOMERS_MANAGE') || perms.includes('CUSTOMERS_EDIT');
const [customer, setCustomer] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<Tab>('OVERVIEW');
// Credentials State
const [saving, setSaving] = useState(false);
const [formData, setFormData] = useState<any>({});
const [additionalEmails, setAdditionalEmails] = useState<string[]>([]);
const [visiblePasswords, setVisiblePasswords] = useState<Record<number, boolean>>({});
const [newEmailInput, setNewEmailInput] = useState('');
// Modals
const [showContactModal, setShowContactModal] = useState(false);
const [contactForm, setContactForm] = useState({ firstName: '', lastName: '', email: '', phone: '' });
const [showCredentialModal, setShowCredentialModal] = useState(false);
const [credentialForm, setCredentialForm] = useState({ title: '', username: '', password: '', description: '' });
const [showTicketModal, setShowTicketModal] = useState(false);
const [ticketForm, setTicketForm] = useState({ title: '', description: '', priority: 'MEDIUM' });
const [salesDocs, setSalesDocs] = useState<any[]>([]);
const generatePassword = (len = 16) => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%&*';
return Array.from({ length: len }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
};
const fileInputRef = useRef<HTMLInputElement>(null);
const [uploading, setUploading] = useState(false);
useEffect(() => {
fetchCustomer();
}, [customerId]);
const fetchCustomer = async () => {
const res = await fetch(`/api/customers/${customerId}`);
if (res.ok) {
const data = await res.json();
setCustomer(data);
setFormData({
companyName: data.companyName || '',
firstName: data.firstName || '',
lastName: data.lastName || '',
email: data.email || '',
phone: data.phone || '',
address: data.address || '',
zipCode: data.zipCode || '',
city: data.city || ''
});
setAdditionalEmails(data.additionalEmails || []);
}
setLoading(false);
};
useEffect(() => {
if (customerId) fetch(`/api/sales?customerId=${customerId}`).then(r => r.json()).then(setSalesDocs).catch(() => {});
}, [customerId]);
const handleCreateTicket = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch('/api/tickets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...ticketForm, customerId })
});
if (res.ok) {
setShowTicketModal(false);
setTicketForm({ title: '', description: '', priority: 'MEDIUM' });
fetchCustomer();
toast('Ticket erstellt', 'success');
}
};
// ── STAMMDATEN ──
const handleSaveCustomer = async (e?: React.FormEvent) => {
if (e) e.preventDefault();
if (!canEdit) { toast('Keine Berechtigung', 'error'); return; }
setSaving(true);
const res = await fetch(`/api/customers/${customerId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...formData, additionalEmails })
});
if (res.ok) {
toast('Kundendaten erfolgreich gespeichert.', 'success');
fetchCustomer();
} else {
const data = await res.json();
toast(data.error || 'Fehler beim Speichern', 'error');
}
setSaving(false);
};
// ── CONTACTS ──
const handleAddContact = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch(`/api/customers/${customerId}/contacts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(contactForm)
});
if (res.ok) {
setShowContactModal(false);
setContactForm({ firstName: '', lastName: '', email: '', phone: '' });
fetchCustomer();
}
};
const handleDeleteContact = async (contactId: number) => {
const isConfirmed = await confirm({ title: 'Mitarbeiter löschen', message: 'Mitarbeiter wirklich löschen?', danger: true });
if (!isConfirmed) return;
await fetch(`/api/customers/${customerId}/contacts?contactId=${contactId}`, { method: 'DELETE' });
fetchCustomer();
};
// ── CREDENTIALS ──
const handleAddCredential = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch(`/api/customers/${customerId}/credentials`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentialForm)
});
if (res.ok) {
setShowCredentialModal(false);
setCredentialForm({ title: '', username: '', password: '', description: '' });
fetchCustomer();
toast('Zugangsdaten gespeichert', 'success');
}
};
const handleDeleteCredential = async (credId: number) => {
const isConfirmed = await confirm({ title: 'Löschen', message: 'Zugangsdaten wirklich löschen?', danger: true });
if (!isConfirmed) return;
await fetch(`/api/customers/${customerId}/credentials?credId=${credId}`, { method: 'DELETE' });
fetchCustomer();
};
// ── DOCUMENTS ──
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
const uploadData = new FormData();
uploadData.append('file', file);
const res = await fetch(`/api/customers/${customerId}/documents`, {
method: 'POST',
body: uploadData
});
setUploading(false);
if (fileInputRef.current) fileInputRef.current.value = '';
if (res.ok) {
toast('Dokument hochgeladen', 'success');
fetchCustomer();
} else {
toast('Fehler beim Hochladen', 'error');
}
};
const handleDeleteDocument = async (docId: number) => {
const isConfirmed = await confirm({ title: 'Dokument löschen', message: 'Dokument wirklich löschen?', danger: true });
if (!isConfirmed) return;
await fetch(`/api/customers/${customerId}/documents?docId=${docId}`, { method: 'DELETE' });
fetchCustomer();
};
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
if (loading) return <div className="p-8 text-slate-500 font-medium">Lade Kundendaten...</div>;
if (!customer) return <div className="p-8 text-red-600 font-medium">Kunde nicht gefunden.</div>;
const openTickets = customer.tickets?.filter((t:any) => t.status === 'OPEN' || t.status === 'IN_PROGRESS').length || 0;
const totalTickets = customer.tickets?.length || 0;
const activeContracts = customer.contracts?.filter((c:any) => c.status === 'ACTIVE').length || 0;
const tabs: { id: Tab, label: string, icon: any }[] = [
{ id: 'OVERVIEW', label: 'Alles', icon: Activity },
{ id: 'TICKETS', label: `Tickets (${totalTickets})`, icon: Ticket },
{ id: 'CONTACTS', label: 'Mitarbeiter', icon: Users },
{ id: 'CONTRACTS', label: 'Abos & Verträge', icon: FileBadge },
{ id: 'SALES_DOCS', label: `Belege (${salesDocs.length})`, icon: FileText },
{ id: 'MASTER_DATA', label: 'Stammdaten', icon: Building2 },
{ id: 'CREDENTIALS', label: 'Zugangsdaten', icon: Key },
{ id: 'DOCUMENTS', label: 'Dokumente', icon: FileText },
];
return (
<div className="max-w-7xl mx-auto space-y-4 pb-12 animate-fade-in-up">
{/* Header */}
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 p-6">
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4">
<div className="flex items-start gap-4">
<button onClick={() => router.push('/customers')} className="p-2 hover:bg-slate-200 rounded-lg text-slate-600 transition mt-1">
<ArrowLeft className="w-5 h-5" />
</button>
<div>
<h1 className="text-2xl font-bold text-slate-900">
{customer.companyName || `${customer.firstName} ${customer.lastName}`}
</h1>
{customer.companyName && <p className="text-sm text-slate-500 mt-0.5">{customer.firstName} {customer.lastName}</p>}
<div className="flex flex-wrap items-center gap-x-5 gap-y-1 mt-2 text-sm text-slate-500">
<span className="flex items-center gap-1.5"><Mail className="w-3.5 h-3.5" /> {customer.email}</span>
{customer.phone && <span className="flex items-center gap-1.5"><Phone className="w-3.5 h-3.5" /> {customer.phone}</span>}
{(customer.address || customer.city) && (
<span className="flex items-center gap-1.5"><MapPin className="w-3.5 h-3.5" /> {[customer.address, customer.zipCode, customer.city].filter(Boolean).join(', ')}</span>
)}
</div>
</div>
</div>
{/* Quick Actions */}
<div className="flex items-center gap-2 flex-shrink-0">
{activeTab === 'MASTER_DATA' && canEdit && (
<button onClick={handleSaveCustomer} disabled={saving} className="bg-indigo-600 text-white px-5 py-2 rounded-lg font-bold shadow-md hover:bg-indigo-700 transition disabled:opacity-50 flex items-center gap-2 text-sm">
<Save className="w-4 h-4" /> {saving ? 'Speichert...' : 'Speichern'}
</button>
)}
{activeTab === 'TICKETS' && (
<button onClick={() => setShowTicketModal(true)} className="bg-indigo-600 text-white px-5 py-2 rounded-lg font-bold shadow-md hover:bg-indigo-700 transition flex items-center gap-2 text-sm">
<Plus className="w-4 h-4" /> Neues Ticket
</button>
)}
{activeTab === 'CONTACTS' && canEdit && (
<button onClick={() => setShowContactModal(true)} className="bg-indigo-600 text-white px-5 py-2 rounded-lg font-bold shadow-md hover:bg-indigo-700 transition flex items-center gap-2 text-sm">
<Plus className="w-4 h-4" /> Mitarbeiter
</button>
)}
{activeTab === 'CREDENTIALS' && canEdit && (
<button onClick={() => setShowCredentialModal(true)} className="bg-indigo-600 text-white px-5 py-2 rounded-lg font-bold shadow-md hover:bg-indigo-700 transition flex items-center gap-2 text-sm">
<Plus className="w-4 h-4" /> Zugangsdaten
</button>
)}
{activeTab === 'DOCUMENTS' && (
<div>
<input type="file" className="hidden" ref={fileInputRef} onChange={handleFileUpload} />
<button onClick={() => fileInputRef.current?.click()} disabled={uploading} className="bg-indigo-600 text-white px-5 py-2 rounded-lg font-bold shadow-md hover:bg-indigo-700 transition flex items-center gap-2 text-sm">
<Plus className="w-4 h-4" /> {uploading ? 'Lädt...' : 'Dokument'}
</button>
</div>
)}
</div>
</div>
{/* Status Bar */}
<div className="flex flex-wrap items-center gap-3 mt-4 pt-4 border-t border-slate-100">
<span className={`text-xs font-bold px-2.5 py-1 rounded-full ${openTickets > 0 ? 'bg-amber-100 text-amber-700' : 'bg-emerald-100 text-emerald-700'}`}>
{openTickets > 0 ? `${openTickets} offene Tickets` : 'Keine offenen Tickets'}
</span>
<span className="text-xs font-bold px-2.5 py-1 rounded-full bg-slate-100 text-slate-600">
{totalTickets} Tickets gesamt
</span>
<span className="text-xs font-bold px-2.5 py-1 rounded-full bg-indigo-50 text-indigo-600">
{activeContracts} aktive Verträge
</span>
<span className="text-xs font-bold px-2.5 py-1 rounded-full bg-slate-100 text-slate-600">
{customer.contacts?.length || 0} Mitarbeiter
</span>
<span className="text-xs font-bold px-2.5 py-1 rounded-full bg-slate-100 text-slate-600">
{customer.credentials?.length || 0} Zugangsdaten
</span>
</div>
</div>
{/* Tabs */}
<div className="flex overflow-x-auto border-b border-slate-200 gap-6 no-scrollbar">
{tabs.map(tab => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors relative whitespace-nowrap
${isActive ? 'text-indigo-600' : 'text-slate-500 hover:text-slate-700'}`}
>
<Icon className="w-4 h-4" />
{tab.label}
{isActive && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-indigo-600 animate-in fade-in duration-300"></div>}
</button>
);
})}
</div>
{/* Tab Content */}
<div className="mt-6">
{/* ── OVERVIEW ── */}
{activeTab === 'OVERVIEW' && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 animate-fade-in-up">
<div className="md:col-span-2 space-y-6">
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100">
<h2 className="font-semibold text-slate-800 mb-4 flex items-center gap-2">
<Activity className="w-5 h-5 text-indigo-600" /> Letzte Aktivitäten
</h2>
<div className="space-y-4">
{customer.tickets?.length === 0 ? (
<p className="text-sm text-slate-500 text-center py-4">Bisher keine Tickets vorhanden.</p>
) : (
customer.tickets?.slice(0, 5).map((t: any) => (
<div key={t.id} onClick={() => router.push(`/tickets/${t.id}`)} className="flex items-start justify-between p-4 bg-slate-50 hover:bg-indigo-50/50 rounded-xl border border-slate-100 cursor-pointer transition-colors group">
<div className="flex gap-4">
<div className="p-2 bg-white rounded-lg shadow-sm border border-slate-200 group-hover:border-indigo-200">
<Ticket className="w-5 h-5 text-indigo-500" />
</div>
<div>
<p className="font-semibold text-slate-800 text-sm">{t.title}</p>
<p className="text-xs text-slate-500 mt-1 line-clamp-1">{t.description}</p>
<p className="text-[10px] text-slate-400 mt-1 font-mono">{new Date(t.createdAt).toLocaleDateString('de-DE')}</p>
</div>
</div>
<div>
{getStatusBadge(t.status)}
</div>
</div>
))
)}
</div>
</div>
</div>
{/* Quick Stats Sidebar */}
<div className="space-y-6">
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 flex items-center gap-4 hover-lift">
<div className="p-3 bg-amber-50 text-amber-600 rounded-xl">
<Ticket className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-slate-500">Tickets gesamt</p>
<p className="text-2xl font-bold text-slate-900">{customer.tickets?.length || 0}</p>
</div>
</div>
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 flex items-center gap-4 hover-lift">
<div className="p-3 bg-indigo-50 text-indigo-600 rounded-xl">
<FileBadge className="w-6 h-6" />
</div>
<div>
<p className="text-sm font-medium text-slate-500">Aktive Verträge</p>
<p className="text-2xl font-bold text-slate-900">{customer.contracts?.filter((c:any) => c.status === 'ACTIVE').length || 0}</p>
</div>
</div>
</div>
</div>
)}
{/* ── TICKETS ── */}
{activeTab === 'TICKETS' && (
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 overflow-hidden animate-fade-in-up">
{customer.tickets?.length === 0 ? (
<div className="p-8 text-slate-500 text-sm text-center">Keine Tickets vorhanden.</div>
) : (
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 text-slate-600 font-medium border-b border-slate-100">
<tr>
<th className="px-6 py-4">ID</th>
<th className="px-6 py-4">Titel</th>
<th className="px-6 py-4">Status</th>
<th className="px-6 py-4">Erstellt</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{customer.tickets.map((t: any) => (
<tr key={t.id} onClick={() => router.push(`/tickets/${t.id}`)} className="hover:bg-indigo-50/40 transition-colors cursor-pointer">
<td className="px-6 py-4 font-mono text-slate-400">#{t.id.toString().padStart(4,'0')}</td>
<td className="px-6 py-4 font-medium text-slate-900">{t.title}</td>
<td className="px-6 py-4">{getStatusBadge(t.status)}</td>
<td className="px-6 py-4 text-slate-500">{new Date(t.createdAt).toLocaleDateString('de-DE')}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{/* ── CONTACTS ── */}
{activeTab === 'CONTACTS' && (
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 overflow-hidden animate-fade-in-up">
{customer.contacts?.length === 0 ? (
<div className="p-8 text-slate-500 text-sm text-center">Keine Mitarbeiter hinterlegt.</div>
) : (
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 text-slate-600 font-medium border-b border-slate-100">
<tr>
<th className="px-6 py-4">Name</th>
<th className="px-6 py-4">E-Mail</th>
<th className="px-6 py-4">Telefon</th>
<th className="px-6 py-4 text-right">Aktion</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{customer.contacts.map((contact: any) => (
<tr key={contact.id} className="hover:bg-slate-50/80 transition-colors">
<td className="px-6 py-4 font-medium text-slate-900">{contact.firstName} {contact.lastName}</td>
<td className="px-6 py-4 text-slate-600">{contact.email}</td>
<td className="px-6 py-4 text-slate-600">{contact.phone || '-'}</td>
<td className="px-6 py-4 text-right">
<button onClick={() => handleDeleteContact(contact.id)} className="text-slate-400 hover:text-red-600 transition p-1">
<Trash2 className="w-4 h-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{/* ── CONTRACTS ── */}
{activeTab === 'CONTRACTS' && (
<div className="space-y-4 animate-fade-in-up">
{customer.contracts?.length === 0 ? (
<div className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200 text-center text-slate-500">
Keine Verträge vorhanden.
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{customer.contracts?.map((contract: any) => (
<div key={contract.id} className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 hover-lift group relative">
<div className="flex items-center gap-3 mb-4">
<div className={`p-2.5 rounded-xl ${contract.status === 'ACTIVE' ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-100 text-slate-500'}`}>
<FileBadge className="w-5 h-5" />
</div>
<div>
<h3 className="font-bold text-slate-900">{contract.title}</h3>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full ${contract.status === 'ACTIVE' ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-200 text-slate-600'}`}>
{contract.status === 'ACTIVE' ? 'Aktiv' : 'Inaktiv'}
</span>
</div>
</div>
{contract.description && <p className="text-sm text-slate-600 mb-4 line-clamp-2">{contract.description}</p>}
<div className="flex items-end justify-between pt-4 border-t border-slate-100">
<div>
<p className="text-xs text-slate-400">Gültig ab</p>
<p className="text-sm font-semibold text-slate-700">{new Date(contract.startDate).toLocaleDateString('de-DE')}</p>
</div>
<div className="text-right">
<p className="text-xs text-slate-400">Preis / Monat</p>
<p className="text-lg font-bold text-indigo-600">{contract.monthlyPrice.toFixed(2)} </p>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* ── MASTER DATA ── */}
{activeTab === 'MASTER_DATA' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 animate-fade-in-up">
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 space-y-4">
<h2 className="font-semibold text-slate-800 mb-4 flex items-center gap-2">
<Building2 className="w-5 h-5 text-indigo-600" /> Stammdaten
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">Firmenname</label>
<input type="text" disabled={!canEdit} className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all disabled:opacity-60 disabled:bg-slate-50" value={formData.companyName} onChange={e => setFormData({...formData, companyName: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Vorname *</label>
<input type="text" required disabled={!canEdit} className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all disabled:opacity-60 disabled:bg-slate-50" value={formData.firstName} onChange={e => setFormData({...formData, firstName: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Nachname *</label>
<input type="text" required disabled={!canEdit} className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all disabled:opacity-60 disabled:bg-slate-50" value={formData.lastName} onChange={e => setFormData({...formData, lastName: e.target.value})} />
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">Straße & Hausnummer</label>
<input type="text" disabled={!canEdit} className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all disabled:opacity-60 disabled:bg-slate-50" value={formData.address} onChange={e => setFormData({...formData, address: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">PLZ</label>
<input type="text" disabled={!canEdit} className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all disabled:opacity-60 disabled:bg-slate-50" value={formData.zipCode} onChange={e => setFormData({...formData, zipCode: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Stadt</label>
<input type="text" disabled={!canEdit} className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all disabled:opacity-60 disabled:bg-slate-50" value={formData.city} onChange={e => setFormData({...formData, city: e.target.value})} />
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">Telefon</label>
<input type="text" disabled={!canEdit} className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all disabled:opacity-60 disabled:bg-slate-50" value={formData.phone} onChange={e => setFormData({...formData, phone: e.target.value})} />
</div>
</div>
</div>
<div className="space-y-6">
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100">
<h2 className="font-semibold text-slate-800 mb-4 flex items-center gap-2">
<Key className="w-5 h-5 text-indigo-600" /> Portal-Zugang
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Haupt-Login (E-Mail) *</label>
<input type="email" required disabled={!canEdit} className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all bg-slate-50 text-slate-600 disabled:opacity-60" value={formData.email} onChange={e => setFormData({...formData, email: e.target.value})} />
</div>
</div>
</div>
<div className="bg-white p-6 rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100">
<h2 className="font-semibold text-slate-800 mb-4 flex items-center gap-2">
<Mail className="w-5 h-5 text-indigo-600" /> Ticket E-Mails
</h2>
<div className="flex gap-2 mb-4">
<input type="email" placeholder="E-Mail hinzufügen..." className="flex-1 border border-slate-300 p-2 rounded-xl text-sm outline-none focus-ring" value={newEmailInput} onChange={e => setNewEmailInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && setAdditionalEmails([...additionalEmails, newEmailInput])} />
<button onClick={() => { setAdditionalEmails([...additionalEmails, newEmailInput]); setNewEmailInput(''); }} type="button" className="bg-slate-100 text-slate-700 px-3 py-2 rounded-xl text-sm font-medium hover:bg-slate-200 transition">Add</button>
</div>
<div className="space-y-2">
{additionalEmails.map((email, idx) => (
<div key={idx} className="flex items-center justify-between bg-slate-50 border border-slate-100 px-3 py-2 rounded-xl group">
<span className="text-sm text-slate-700">{email}</span>
<button type="button" onClick={() => setAdditionalEmails(additionalEmails.filter(e => e !== email))} className="text-slate-400 hover:text-red-600 transition opacity-0 group-hover:opacity-100">
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
</div>
</div>
)}
{/* ── CREDENTIALS ── */}
{activeTab === 'CREDENTIALS' && (
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 overflow-hidden animate-fade-in-up">
{customer.credentials?.length === 0 ? (
<div className="p-8 text-slate-500 text-sm text-center">Keine Zugangsdaten hinterlegt.</div>
) : (
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 text-slate-600 font-medium border-b border-slate-100">
<tr>
<th className="px-6 py-4">Bezeichnung</th>
<th className="px-6 py-4">Benutzername</th>
<th className="px-6 py-4">Passwort</th>
<th className="px-6 py-4 text-right">Aktion</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{customer.credentials.map((cred: any) => (
<tr key={cred.id} className="hover:bg-slate-50/80 transition-colors">
<td className="px-6 py-4 font-medium text-slate-900">{cred.title}</td>
<td className="px-6 py-4 text-slate-600">
<button onClick={() => { navigator.clipboard.writeText(cred.username); toast('Kopiert', 'success'); }} className="hover:text-indigo-600 transition flex items-center gap-1.5" title="Kopieren">
{cred.username} <Copy className="w-3 h-3 opacity-40" />
</button>
</td>
<td className="px-6 py-4 text-slate-600 font-mono">
<div className="flex items-center gap-2">
<span>{visiblePasswords[cred.id] ? cred.password : '••••••••'}</span>
<button onClick={() => setVisiblePasswords(p => ({...p, [cred.id]: !p[cred.id]}))} className="text-slate-400 hover:text-indigo-600 transition" title="Anzeigen/Verbergen">
{visiblePasswords[cred.id] ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
<button onClick={() => { navigator.clipboard.writeText(cred.password); toast('Passwort kopiert', 'success'); }} className="text-slate-400 hover:text-indigo-600 transition" title="Kopieren">
<Copy className="w-3.5 h-3.5" />
</button>
</div>
</td>
<td className="px-6 py-4 text-right">
<button onClick={() => handleDeleteCredential(cred.id)} className="text-slate-400 hover:text-red-600 transition p-1">
<Trash2 className="w-4 h-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{/* ── DOCUMENTS ── */}
{activeTab === 'DOCUMENTS' && (
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 overflow-hidden animate-fade-in-up">
{customer.documents?.length === 0 ? (
<div className="p-8 text-slate-500 text-sm text-center">Keine Dokumente hinterlegt.</div>
) : (
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 text-slate-600 font-medium border-b border-slate-100">
<tr>
<th className="px-6 py-4">Dateiname</th>
<th className="px-6 py-4">Größe</th>
<th className="px-6 py-4">Hochgeladen</th>
<th className="px-6 py-4 text-right">Aktionen</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{customer.documents.map((doc: any) => (
<tr key={doc.id} className="hover:bg-slate-50/80 transition-colors group">
<td className="px-6 py-4 font-medium text-slate-900 flex items-center gap-2">
<FileText className="w-4 h-4 text-slate-400" />
{doc.fileName}
</td>
<td className="px-6 py-4 text-slate-600">{formatBytes(doc.fileSize)}</td>
<td className="px-6 py-4 text-slate-600">{new Date(doc.createdAt).toLocaleDateString('de-DE')}</td>
<td className="px-6 py-4 text-right flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<a href={`/api/customers/${customerId}/documents?download=${doc.id}`} download className="p-1.5 text-indigo-600 hover:bg-indigo-50 rounded-lg transition">
<Download className="w-4 h-4" />
</a>
<button onClick={() => handleDeleteDocument(doc.id)} className="p-1.5 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition">
<Trash2 className="w-4 h-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{/* ── SALES DOCS ── */}
{activeTab === 'SALES_DOCS' && (
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 overflow-hidden animate-fade-in-up">
{salesDocs.length === 0 ? (
<div className="p-8 text-slate-500 text-sm text-center">Keine Belege vorhanden.</div>
) : (
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 text-slate-600 font-medium border-b border-slate-100">
<tr>
<th className="px-6 py-4">Nummer</th>
<th className="px-6 py-4">Typ</th>
<th className="px-6 py-4">Status</th>
<th className="px-6 py-4 text-right">Betrag</th>
<th className="px-6 py-4">Datum</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{salesDocs.map((d: any) => (
<tr key={d.id} onClick={() => router.push(`/sales/${d.id}`)} className="hover:bg-indigo-50/40 transition-colors cursor-pointer">
<td className="px-6 py-4 font-mono font-semibold text-slate-900">{d.number}</td>
<td className="px-6 py-4"><span className="px-2 py-0.5 rounded-md text-xs font-semibold bg-slate-100 text-slate-600">{{'QUOTE':'Angebot','ORDER_CONFIRMATION':'AB','DELIVERY_NOTE':'Lieferschein','INVOICE':'Rechnung','CREDIT_NOTE':'RK'}[d.type as string]}</span></td>
<td className="px-6 py-4"><span className="px-2 py-0.5 rounded-full text-xs font-bold bg-slate-100 text-slate-600">{{'DRAFT':'Entwurf','SENT':'Gesendet','ACCEPTED':'Angenommen','REJECTED':'Abgelehnt','DELIVERED':'Geliefert','PAID':'Bezahlt','CANCELLED':'Storniert','ARCHIVED':'Archiviert'}[d.status as string]}</span></td>
<td className="px-6 py-4 text-right font-semibold">{d.total.toFixed(2)} </td>
<td className="px-6 py-4 text-slate-500">{new Date(d.createdAt).toLocaleDateString('de-DE')}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</div>
{/* MODAL: Mitarbeiter */}
{showContactModal && (
<div className="fixed inset-0 bg-slate-900/60 flex items-center justify-center z-50 backdrop-blur-sm p-4 animate-fade-in">
<div className="bg-white p-8 rounded-3xl shadow-2xl max-w-md w-full">
<h2 className="text-xl font-bold text-slate-900 mb-6">Mitarbeiter hinzufügen</h2>
<form onSubmit={handleAddContact} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Vorname *</label>
<input type="text" required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={contactForm.firstName} onChange={e => setContactForm({...contactForm, firstName: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Nachname *</label>
<input type="text" required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={contactForm.lastName} onChange={e => setContactForm({...contactForm, lastName: e.target.value})} />
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">E-Mail *</label>
<input type="email" required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={contactForm.email} onChange={e => setContactForm({...contactForm, email: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Telefon</label>
<input type="text" className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={contactForm.phone} onChange={e => setContactForm({...contactForm, phone: e.target.value})} />
</div>
<div className="flex justify-end gap-3 mt-8">
<button type="button" onClick={() => setShowContactModal(false)} className="px-4 py-2.5 text-slate-600 font-medium hover:bg-slate-100 rounded-xl transition">Abbrechen</button>
<button type="submit" className="bg-indigo-600 text-white px-6 py-2.5 rounded-xl font-medium hover:bg-indigo-700 transition">Speichern</button>
</div>
</form>
</div>
</div>
)}
{/* MODAL: Zugangsdaten */}
{showCredentialModal && (
<div className="fixed inset-0 bg-slate-900/60 flex items-center justify-center z-50 backdrop-blur-sm p-4 animate-fade-in">
<div className="bg-white p-8 rounded-3xl shadow-2xl max-w-md w-full">
<h2 className="text-xl font-bold text-slate-900 mb-6">Zugangsdaten anlegen</h2>
<form onSubmit={handleAddCredential} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Bezeichnung *</label>
<input type="text" required placeholder="z.B. Microsoft 365 Admin" className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={credentialForm.title} onChange={e => setCredentialForm({...credentialForm, title: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Benutzername *</label>
<input type="text" required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={credentialForm.username} onChange={e => setCredentialForm({...credentialForm, username: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
<input type="text" placeholder="Optional" className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={credentialForm.description} onChange={e => setCredentialForm({...credentialForm, description: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Passwort *</label>
<div className="flex gap-2">
<input type="text" required className="flex-1 border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all font-mono" value={credentialForm.password} onChange={e => setCredentialForm({...credentialForm, password: e.target.value})} />
<button type="button" onClick={() => setCredentialForm({...credentialForm, password: generatePassword()})} className="bg-slate-100 hover:bg-slate-200 text-slate-700 px-3 py-2 rounded-xl transition flex items-center gap-1.5 text-sm font-medium flex-shrink-0" title="Passwort generieren">
<RefreshCw className="w-4 h-4" /> Generieren
</button>
</div>
</div>
<div className="flex justify-end gap-3 mt-8">
<button type="button" onClick={() => setShowCredentialModal(false)} className="px-4 py-2.5 text-slate-600 font-medium hover:bg-slate-100 rounded-xl transition">Abbrechen</button>
<button type="submit" className="bg-indigo-600 text-white px-6 py-2.5 rounded-xl font-medium hover:bg-indigo-700 transition">Speichern</button>
</div>
</form>
</div>
</div>
)}
{/* MODAL: Neues Ticket */}
{showTicketModal && (
<div className="fixed inset-0 bg-slate-900/60 flex items-center justify-center z-50 backdrop-blur-sm p-4 animate-fade-in">
<div className="bg-white p-8 rounded-3xl shadow-2xl max-w-md w-full">
<h2 className="text-xl font-bold text-slate-900 mb-6">Neues Ticket erstellen</h2>
<form onSubmit={handleCreateTicket} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Titel *</label>
<input type="text" required className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all" value={ticketForm.title} onChange={e => setTicketForm({...ticketForm, title: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung *</label>
<textarea required rows={3} className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none transition-all resize-none" value={ticketForm.description} onChange={e => setTicketForm({...ticketForm, description: e.target.value})} />
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Priorität</label>
<select className="w-full border border-slate-300 p-2.5 rounded-xl focus-ring outline-none bg-white" value={ticketForm.priority} onChange={e => setTicketForm({...ticketForm, priority: e.target.value})}>
<option value="LOW">Niedrig</option>
<option value="MEDIUM">Mittel</option>
<option value="HIGH">Hoch</option>
<option value="CRITICAL">Kritisch</option>
</select>
</div>
<div className="flex justify-end gap-3 mt-8">
<button type="button" onClick={() => setShowTicketModal(false)} className="px-4 py-2.5 text-slate-600 font-medium hover:bg-slate-100 rounded-xl transition">Abbrechen</button>
<button type="submit" className="bg-indigo-600 text-white px-6 py-2.5 rounded-xl font-medium hover:bg-indigo-700 transition">Ticket erstellen</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}