Files
webshop/shop/app/admin/companies/[id]/customers/page.tsx
DanielS 5f65f890ee
All checks were successful
Staging Build / build (push) Successful in 3m37s
feat: link end customers directly to companies
2026-07-04 15:17:04 +02:00

456 lines
18 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 { useState, useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import {
Building2,
Plus,
Trash2,
Loader2,
Pencil,
ArrowLeft,
Search,
MapPin,
User,
Users,
Briefcase,
AlertCircle
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
getCompanyDetails,
getEndCustomersByCompanyWithPartners,
getCompanyPartners,
adminCreateEndCustomer,
adminUpdateEndCustomer,
adminDeleteEndCustomer
} from '@/lib/actions/companies'
export default function CompanyCustomersPage() {
const params = useParams()
const router = useRouter()
const companyId = params.id as string
const [company, setCompany] = useState<any>(null)
const [customers, setCustomers] = useState<any[]>([])
const [partners, setPartners] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [loadingAction, setLoadingAction] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
// Dialog State
const [isOpen, setIsOpen] = useState(false)
const [editingCustomer, setEditingCustomer] = useState<any>(null)
// Form State
const [companyName, setCompanyName] = useState('')
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [street, setStreet] = useState('')
const [zip, setZip] = useState('')
const [city, setCity] = useState('')
const [vatId, setVatId] = useState('')
const [partnerId, setPartnerId] = useState('')
const [email, setEmail] = useState('')
const [error, setError] = useState<string | null>(null)
const loadData = async () => {
setLoading(true)
try {
const [compDetails, custData, partData] = await Promise.all([
getCompanyDetails(companyId),
getEndCustomersByCompanyWithPartners(companyId),
getCompanyPartners(companyId)
])
setCompany(compDetails)
setCustomers(custData)
setPartners(partData)
} catch (err) {
console.error('Fehler beim Laden der Daten:', err)
} finally {
setLoading(false)
}
}
useEffect(() => {
if (companyId) {
loadData()
}
}, [companyId])
const handleOpenDialog = (customer: any = null) => {
setEditingCustomer(customer)
setCompanyName(customer ? customer.company_name || '' : '')
setFirstName(customer ? customer.first_name || '' : '')
setLastName(customer ? customer.last_name || '' : '')
setStreet(customer ? customer.street || '' : '')
setZip(customer ? customer.zip || '' : '')
setCity(customer ? customer.city || '' : '')
setVatId(customer ? customer.vat_id || '' : '')
setPartnerId(companyId)
setEmail(customer ? customer.email || '' : '')
setError(null)
setIsOpen(true)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoadingAction(true)
setError(null)
try {
const payload = {
partner_id: companyId,
company_name: companyName.trim(),
first_name: firstName.trim() || undefined,
last_name: lastName.trim() || undefined,
street: street.trim() || undefined,
zip: zip.trim() || undefined,
city: city.trim() || undefined,
vat_id: vatId.trim() || undefined,
email: email.trim() || undefined,
}
if (editingCustomer) {
await adminUpdateEndCustomer(editingCustomer.id, payload)
} else {
await adminCreateEndCustomer(payload)
}
setIsOpen(false)
loadData()
} catch (err: any) {
setError(err.message || 'Fehler beim Speichern des Endkunden.')
} finally {
setLoadingAction(false)
}
}
const handleDelete = async (id: string, name: string) => {
if (!confirm(`Endkunden "${name}" wirklich löschen?`)) return
setLoadingAction(true)
try {
await adminDeleteEndCustomer(id)
loadData()
} catch (err) {
console.error('Fehler beim Löschen des Endkunden:', err)
} finally {
setLoadingAction(false)
}
}
const filteredCustomers = customers.filter((c) => {
const q = searchQuery.toLowerCase()
const nameMatch = c.company_name?.toLowerCase().includes(q)
const personMatch = `${c.first_name || ''} ${c.last_name || ''}`.toLowerCase().includes(q)
const addressMatch = `${c.street || ''} ${c.zip || ''} ${c.city || ''}`.toLowerCase().includes(q)
const partnerMatch = c.partner_name?.toLowerCase().includes(q)
const emailMatch = c.email?.toLowerCase().includes(q)
return nameMatch || personMatch || addressMatch || partnerMatch || emailMatch
})
return (
<div className="p-6 max-w-5xl mx-auto text-slate-900 dark:text-white space-y-8">
{/* Back Link */}
<div>
<Link
href="/admin/companies"
className="inline-flex items-center gap-2 text-sm font-semibold text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Zurück zur Firmenverwaltung
</Link>
</div>
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-extrabold tracking-tight flex items-center gap-3">
<Briefcase className="w-8 h-8 text-indigo-500" />
Endkunden von {company ? company.name : '...'}
</h1>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">
Hier verwalten Sie alle Endkunden, die Partnern dieser Firma zugeordnet sind.
</p>
</div>
<Button
onClick={() => handleOpenDialog()}
disabled={loading || partners.length === 0}
className="bg-indigo-600 hover:bg-indigo-700 text-white font-semibold"
title={partners.length === 0 ? 'Es müssen zuerst Benutzer für diese Firma existieren' : ''}
>
<Plus className="w-4 h-4 mr-2" /> Endkunde anlegen
</Button>
</div>
{/* Warning if no partners exist */}
{!loading && partners.length === 0 && (
<div className="p-4 bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900/30 rounded-xl flex gap-3 text-amber-800 dark:text-amber-300 text-sm">
<AlertCircle className="w-5 h-5 shrink-0 text-amber-600 dark:text-amber-400" />
<div>
<p className="font-semibold">Keine Benutzer zugewiesen</p>
<p className="mt-1">
Diese Firma hat aktuell keine zugewiesenen Partner (Benutzer mit der Rolle &quot;partner&quot;).
Sie müssen zuerst in der <Link href="/admin/users" className="underline font-semibold hover:text-amber-900 dark:hover:text-amber-200">Benutzerverwaltung</Link> Partner erstellen und dieser Firma zuweisen, um Endkunden anzulegen.
</p>
</div>
</div>
)}
{/* Table Section */}
<div className="p-5 bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-white/10 shadow-sm">
<div className="relative mb-6 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<input
type="text"
placeholder="Endkunden suchen (Name, Ansprechpartner, Ort)..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 text-sm bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-md text-slate-900 dark:text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all"
/>
</div>
{loading ? (
<div className="flex justify-center items-center py-16">
<Loader2 className="w-8 h-8 text-indigo-500 animate-spin" />
<span className="ml-3 text-slate-400 text-sm">Lade Endkunden...</span>
</div>
) : filteredCustomers.length === 0 ? (
<div className="text-center py-16 border-2 border-dashed border-slate-200 dark:border-white/5 rounded-xl text-slate-500">
<Users className="w-10 h-10 mx-auto mb-3 opacity-30" />
Keine Endkunden gefunden.
</div>
) : (
<table className="w-full text-left">
<thead>
<tr className="border-b border-slate-200 dark:border-white/10 text-slate-400 text-xs font-semibold uppercase">
<th className="pb-3">Endkunde (Firma / Kontakt)</th>
<th className="pb-3">Lieferadresse</th>
<th className="pb-3">E-Mail</th>
<th className="pb-3">USt-ID</th>
<th className="pb-3">Zugeordneter Partner</th>
<th className="pb-3 text-right">Aktionen</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-white/5">
{filteredCustomers.map((customer) => (
<tr key={customer.id} className="hover:bg-slate-50 dark:hover:bg-white/3 transition-colors">
<td className="py-4">
<div className="font-semibold text-slate-900 dark:text-white">
{customer.company_name}
</div>
{(customer.first_name || customer.last_name) && (
<div className="text-xs text-slate-500 dark:text-slate-400 flex items-center gap-1 mt-0.5">
<User className="w-3 h-3 text-slate-400 shrink-0" />
<span>{customer.first_name || ''} {customer.last_name || ''}</span>
</div>
)}
</td>
<td className="py-4 text-sm text-slate-700 dark:text-slate-300">
{customer.street ? (
<div className="flex items-center gap-2">
<MapPin className="w-3.5 h-3.5 text-slate-400 shrink-0" />
<span>
{customer.street}, {customer.zip} {customer.city}
</span>
</div>
) : (
<span className="text-slate-400 dark:text-slate-600"></span>
)}
</td>
<td className="py-4 text-sm text-slate-700 dark:text-slate-300">
{customer.email || <span className="text-slate-400 dark:text-slate-600"></span>}
</td>
<td className="py-4 text-sm text-slate-700 dark:text-slate-300">
{customer.vat_id || <span className="text-slate-400 dark:text-slate-600"></span>}
</td>
<td className="py-4 text-sm text-slate-700 dark:text-slate-300">
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-semibold bg-indigo-50 dark:bg-indigo-950/30 text-indigo-700 dark:text-indigo-300">
{customer.partner_name || 'Unbekannt'}
</span>
</td>
<td className="py-4 text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => handleOpenDialog(customer)}
className="h-8 w-8 text-indigo-500 hover:text-indigo-400 hover:bg-indigo-500/10"
title="Endkunde bearbeiten"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
disabled={loadingAction}
onClick={() => handleDelete(customer.id, customer.company_name)}
className="h-8 w-8 text-red-500 hover:text-red-400 hover:bg-red-500/10"
title="Endkunde löschen"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Dialog: Endkunde anlegen / bearbeiten */}
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="bg-white dark:bg-slate-950 border-slate-200 dark:border-white/10 text-slate-900 dark:text-white shadow-2xl">
<DialogHeader>
<DialogTitle>
{editingCustomer ? 'Endkunden bearbeiten' : 'Neuen Endkunden anlegen'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400">
Geben Sie die Firmendaten und optional Kontaktdaten und Adresse des Endkunden an.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-2">
<div className="space-y-2">
<Label htmlFor="cust-company">Firmenname *</Label>
<Input
id="cust-company"
type="text"
required
placeholder="Endkunde GmbH"
value={companyName}
onChange={(e) => setCompanyName(e.target.value)}
className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="cust-firstname">Vorname</Label>
<Input
id="cust-firstname"
type="text"
placeholder="Max"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cust-lastname">Nachname</Label>
<Input
id="cust-lastname"
type="text"
placeholder="Mustermann"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="cust-street">Straße & Hausnummer</Label>
<Input
id="cust-street"
type="text"
placeholder="Beispielstraße 12"
value={street}
onChange={(e) => setStreet(e.target.value)}
className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10"
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2 col-span-1">
<Label htmlFor="cust-zip">PLZ</Label>
<Input
id="cust-zip"
type="text"
placeholder="12345"
value={zip}
onChange={(e) => setZip(e.target.value)}
className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10"
/>
</div>
<div className="space-y-2 col-span-2">
<Label htmlFor="cust-city">Ort</Label>
<Input
id="cust-city"
type="text"
placeholder="Musterort"
value={city}
onChange={(e) => setCity(e.target.value)}
className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="cust-vat">USt-ID (VAT-ID)</Label>
<Input
id="cust-vat"
type="text"
placeholder="DE123456789"
value={vatId}
onChange={(e) => setVatId(e.target.value)}
className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cust-partner">Zugeordnetes Unternehmen</Label>
<Input
id="cust-partner"
type="text"
disabled
value={company ? company.name : ''}
className="bg-slate-100 dark:bg-white/10 border-slate-200 dark:border-white/10 cursor-not-allowed"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="cust-email">E-Mail</Label>
<Input
id="cust-email"
type="email"
placeholder="kunde@beispiel.de"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10"
/>
</div>
{error && (
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
)}
<DialogFooter className="pt-2">
<Button type="button" variant="outline" onClick={() => setIsOpen(false)} disabled={loadingAction}>
Abbrechen
</Button>
<Button type="submit" disabled={loadingAction} className="bg-indigo-600 hover:bg-indigo-700 text-white font-semibold">
{loadingAction ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
{editingCustomer ? 'Speichern' : 'Anlegen'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
)
}