Files
webshop/shop/app/admin/companies/page.tsx
DanielS 0ff4df2a12
Some checks failed
Staging Build / build (push) Has been cancelled
feat: Add end customer view for companies with CRUD and partner assignment
2026-06-25 02:18:00 +02:00

336 lines
13 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 Link from 'next/link'
import { Building2, Plus, Trash2, Loader2, Pencil, Mail, MapPin, Search, Users } 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 { getCompanies, createCompany, updateCompany, deleteCompany } from '@/lib/actions/companies'
export default function CompaniesPage() {
const [companies, setCompanies] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [isOpen, setIsOpen] = useState(false)
const [editingCompany, setEditingCompany] = useState<any>(null)
const [name, setName] = useState('')
const [street, setStreet] = useState('')
const [zip, setZip] = useState('')
const [city, setCity] = useState('')
const [email, setEmail] = useState('')
const [creating, setCreating] = useState(false)
const [createError, setCreateError] = useState<string | null>(null)
const [deletingId, setDeletingId] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const loadCompanies = async () => {
setLoading(true)
try {
const data = await getCompanies()
setCompanies(data)
} catch (err) {
console.error('Fehler beim Laden der Partner:', err)
} finally {
setLoading(false)
}
}
useEffect(() => {
loadCompanies()
}, [])
const handleOpenDialog = (company: any = null) => {
setEditingCompany(company)
setName(company ? company.name || '' : '')
setStreet(company ? company.street || '' : '')
setZip(company ? company.zip || '' : '')
setCity(company ? company.city || '' : '')
setEmail(company ? company.email || '' : '')
setCreateError(null)
setIsOpen(true)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setCreating(true)
setCreateError(null)
try {
const payload = {
name: name.trim(),
street: street.trim(),
zip: zip.trim(),
city: city.trim(),
email: email.trim(),
}
if (editingCompany) {
await updateCompany(editingCompany.id, payload)
} else {
await createCompany(payload)
}
setIsOpen(false)
loadCompanies()
} catch (err: any) {
setCreateError(err.message || 'Fehler beim Speichern.')
} finally {
setCreating(false)
}
}
const handleDelete = async (id: string, firmName: string) => {
if (!confirm(`Firma "${firmName}" wirklich löschen? Alle zugewiesenen Partner werden getrennt.`)) return
setDeletingId(id)
try {
await deleteCompany(id)
loadCompanies()
} catch (err) {
console.error('Fehler beim Löschen:', err)
} finally {
setDeletingId(null)
}
}
const filteredCompanies = companies.filter((c) => {
const nameMatch = c.name?.toLowerCase().includes(searchQuery.toLowerCase())
const addressMatch = `${c.street || ''} ${c.zip || ''} ${c.city || ''}`.toLowerCase().includes(searchQuery.toLowerCase())
const emailMatch = c.email?.toLowerCase().includes(searchQuery.toLowerCase())
return nameMatch || addressMatch || emailMatch
})
return (
<div className="p-6 max-w-5xl mx-auto text-slate-900 dark:text-white space-y-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-extrabold tracking-tight flex items-center gap-3">
<Building2 className="w-8 h-8 text-blue-500" />
Firmenverwaltung
</h1>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">
Verwalten Sie Firmen, deren Lieferadresse und weisen Sie Partner zu.
</p>
</div>
<Button
onClick={() => handleOpenDialog()}
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold"
>
<Plus className="w-4 h-4 mr-2" /> Firma anlegen
</Button>
</div>
{/* Firmen-Tabelle */}
<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="Firmen suchen (Name, Adresse, E-Mail)..."
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-blue-500 transition-all"
/>
</div>
{loading ? (
<div className="flex justify-center items-center py-16">
<Loader2 className="w-8 h-8 text-blue-500 animate-spin" />
<span className="ml-3 text-slate-400 text-sm">Lade Firmen...</span>
</div>
) : filteredCompanies.length === 0 ? (
<div className="text-center py-16 border-2 border-dashed border-slate-200 dark:border-white/5 rounded-xl text-slate-500">
<Building2 className="w-10 h-10 mx-auto mb-3 opacity-30" />
Keine Firmen 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">Firmenname</th>
<th className="pb-3">Lieferadresse</th>
<th className="pb-3">E-Mail</th>
<th className="pb-3">Erstellt am</th>
<th className="pb-3 text-right">Aktionen</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-white/5">
{filteredCompanies.map((company) => (
<tr key={company.id} className="hover:bg-slate-50 dark:hover:bg-white/3 transition-colors">
<td className="py-4 font-semibold text-slate-900 dark:text-white">
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-blue-500 shrink-0" />
{company.name}
</div>
</td>
<td className="py-4 text-sm text-slate-700 dark:text-slate-300">
<div className="flex items-center gap-2">
<MapPin className="w-3.5 h-3.5 text-slate-400 shrink-0" />
<span>
{company.street ? `${company.street}, ${company.zip} ${company.city}` : ''}
</span>
</div>
</td>
<td className="py-4 text-sm text-slate-700 dark:text-slate-300">
{company.email ? (
<div className="flex items-center gap-2">
<Mail className="w-3.5 h-3.5 text-slate-400 shrink-0" />
<span>{company.email}</span>
</div>
) : ''}
</td>
<td className="py-4 text-sm text-slate-500 dark:text-slate-400">
{company.created_at ? new Date(company.created_at).toLocaleDateString('de-DE') : ''}
</td>
<td className="py-4 text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="icon"
asChild
className="h-8 w-8 text-indigo-500 hover:text-indigo-400 hover:bg-indigo-500/10"
title="Endkunden dieser Firma verwalten"
>
<Link href={`/admin/companies/${company.id}/customers`}>
<Users className="h-4 w-4" />
</Link>
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleOpenDialog(company)}
className="h-8 w-8 text-blue-500 hover:text-blue-400 hover:bg-blue-500/10"
title="Firma bearbeiten"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
disabled={deletingId === company.id}
onClick={() => handleDelete(company.id, company.name)}
className="h-8 w-8 text-red-500 hover:text-red-400 hover:bg-red-500/10"
title="Firma löschen"
>
{deletingId === company.id
? <Loader2 className="h-4 w-4 animate-spin" />
: <Trash2 className="h-4 w-4" />}
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Dialog: Firma 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>
{editingCompany ? 'Firma bearbeiten' : 'Neue Firma anlegen'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400">
Geben Sie die Firmendetails und die vollständige Lieferadresse ein. Alle Felder sind Pflichtfelder.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-2">
<div className="space-y-2">
<Label htmlFor="company-name">Firmenname</Label>
<Input
id="company-name"
type="text"
required
placeholder="Muster GmbH"
value={name}
onChange={(e) => setName(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="company-street">Straße & Hausnummer</Label>
<Input
id="company-street"
type="text"
required
placeholder="Musterstraße 123"
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="company-zip">PLZ</Label>
<Input
id="company-zip"
type="text"
required
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="company-city">Ort</Label>
<Input
id="company-city"
type="text"
required
placeholder="Musterstadt"
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="space-y-2">
<Label htmlFor="company-email">E-Mail für Lieferadresse</Label>
<Input
id="company-email"
type="email"
required
placeholder="lieferung@musterfirma.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>
{createError && (
<p className="text-sm text-red-600 dark:text-red-400">{createError}</p>
)}
<DialogFooter className="pt-2">
<Button type="button" variant="outline" onClick={() => setIsOpen(false)} disabled={creating}>
Abbrechen
</Button>
<Button type="submit" disabled={creating} className="bg-blue-600 hover:bg-blue-700 text-white font-semibold">
{creating ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
{editingCompany ? 'Speichern' : 'Anlegen'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
)
}