feat: add delivery address migration, update companies server actions and rewrite companies page to support creation and edit of full address
All checks were successful
Staging Build / build (push) Successful in 2m37s

This commit is contained in:
DanielS
2026-06-25 01:52:05 +02:00
parent 85df922498
commit a1ad75c2db
4 changed files with 168 additions and 37 deletions

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Building2, Plus, Trash2, Loader2 } from 'lucide-react' import { Building2, Plus, Trash2, Loader2, Pencil, Mail, MapPin } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
@@ -13,13 +13,20 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { getCompanies, createCompany, deleteCompany } from '@/lib/actions/companies' import { getCompanies, createCompany, updateCompany, deleteCompany } from '@/lib/actions/companies'
export default function CompaniesPage() { export default function CompaniesPage() {
const [companies, setCompanies] = useState<any[]>([]) const [companies, setCompanies] = useState<any[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [isCreateOpen, setIsCreateOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [editingCompany, setEditingCompany] = useState<any>(null)
const [name, setName] = useState('') 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 [creating, setCreating] = useState(false)
const [createError, setCreateError] = useState<string | null>(null) const [createError, setCreateError] = useState<string | null>(null)
const [deletingId, setDeletingId] = useState<string | null>(null) const [deletingId, setDeletingId] = useState<string | null>(null)
@@ -40,17 +47,38 @@ export default function CompaniesPage() {
loadCompanies() loadCompanies()
}, []) }, [])
const handleCreate = async (e: React.FormEvent) => { 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() e.preventDefault()
setCreating(true) setCreating(true)
setCreateError(null) setCreateError(null)
try { try {
await createCompany(name) const payload = {
setIsCreateOpen(false) name: name.trim(),
setName('') 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() loadCompanies()
} catch (err: any) { } catch (err: any) {
setCreateError(err.message || 'Fehler beim Anlegen.') setCreateError(err.message || 'Fehler beim Speichern.')
} finally { } finally {
setCreating(false) setCreating(false)
} }
@@ -70,7 +98,7 @@ export default function CompaniesPage() {
} }
return ( return (
<div className="p-6 max-w-4xl mx-auto text-slate-900 dark:text-white space-y-8"> <div className="p-6 max-w-5xl mx-auto text-slate-900 dark:text-white space-y-8">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -79,11 +107,11 @@ export default function CompaniesPage() {
Firmenverwaltung Firmenverwaltung
</h1> </h1>
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1"> <p className="text-slate-500 dark:text-slate-400 text-sm mt-1">
Verwalten Sie Firmen und weisen Sie Partner zu. Verwalten Sie Firmen, deren Lieferadresse und weisen Sie Partner zu.
</p> </p>
</div> </div>
<Button <Button
onClick={() => setIsCreateOpen(true)} onClick={() => handleOpenDialog()}
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold" className="bg-blue-600 hover:bg-blue-700 text-white font-semibold"
> >
<Plus className="w-4 h-4 mr-2" /> Firma anlegen <Plus className="w-4 h-4 mr-2" /> Firma anlegen
@@ -107,6 +135,8 @@ export default function CompaniesPage() {
<thead> <thead>
<tr className="border-b border-slate-200 dark:border-white/10 text-slate-400 text-xs font-semibold uppercase"> <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">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">Erstellt am</th>
<th className="pb-3 text-right">Aktionen</th> <th className="pb-3 text-right">Aktionen</th>
</tr> </tr>
@@ -120,22 +150,49 @@ export default function CompaniesPage() {
{company.name} {company.name}
</div> </div>
</td> </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"> <td className="py-4 text-sm text-slate-500 dark:text-slate-400">
{company.created_at ? new Date(company.created_at).toLocaleDateString('de-DE') : ''} {company.created_at ? new Date(company.created_at).toLocaleDateString('de-DE') : ''}
</td> </td>
<td className="py-4 text-right"> <td className="py-4 text-right">
<Button <div className="flex justify-end gap-2">
variant="ghost" <Button
size="icon" variant="ghost"
disabled={deletingId === company.id} size="icon"
onClick={() => handleDelete(company.id, company.name)} onClick={() => handleOpenDialog(company)}
className="h-8 w-8 text-red-500 hover:text-red-400 hover:bg-red-500/10" className="h-8 w-8 text-blue-500 hover:text-blue-400 hover:bg-blue-500/10"
title="Firma löschen" title="Firma bearbeiten"
> >
{deletingId === company.id <Pencil className="h-4 w-4" />
? <Loader2 className="h-4 w-4 animate-spin" /> </Button>
: <Trash2 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> </td>
</tr> </tr>
))} ))}
@@ -144,16 +201,18 @@ export default function CompaniesPage() {
)} )}
</div> </div>
{/* Dialog: Neue Firma */} {/* Dialog: Firma anlegen / bearbeiten */}
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}> <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"> <DialogContent className="bg-white dark:bg-slate-950 border-slate-200 dark:border-white/10 text-slate-900 dark:text-white shadow-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Neue Firma anlegen</DialogTitle> <DialogTitle>
{editingCompany ? 'Firma bearbeiten' : 'Neue Firma anlegen'}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400"> <DialogDescription className="text-slate-500 dark:text-slate-400">
Geben Sie den Namen der Firma ein. Partner können danach zugewiesen werden. Geben Sie die Firmendetails und die vollständige Lieferadresse ein. Alle Felder sind Pflichtfelder.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleCreate} className="space-y-4 mt-2"> <form onSubmit={handleSubmit} className="space-y-4 mt-2">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="company-name">Firmenname</Label> <Label htmlFor="company-name">Firmenname</Label>
<Input <Input
@@ -166,16 +225,70 @@ export default function CompaniesPage() {
className="bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10" 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-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 && ( {createError && (
<p className="text-sm text-red-600 dark:text-red-400">{createError}</p> <p className="text-sm text-red-600 dark:text-red-400">{createError}</p>
)} )}
<DialogFooter className="pt-2"> <DialogFooter className="pt-2">
<Button type="button" variant="outline" onClick={() => setIsCreateOpen(false)} disabled={creating}> <Button type="button" variant="outline" onClick={() => setIsOpen(false)} disabled={creating}>
Abbrechen Abbrechen
</Button> </Button>
<Button type="submit" disabled={creating} className="bg-blue-600 hover:bg-blue-700 text-white font-semibold"> <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} {creating ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
Anlegen {editingCompany ? 'Speichern' : 'Anlegen'}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@@ -280,7 +280,7 @@ export function UserDialog({ companies }: UserDialogProps) {
if (trimmed) { if (trimmed) {
setIsCreatingCompany(true) setIsCreatingCompany(true)
try { try {
const created = await createCompany(trimmed) const created = await createCompany({ name: trimmed })
if (created) { if (created) {
setLocalCompanies((prev) => [...prev, created]) setLocalCompanies((prev) => [...prev, created])
field.onChange(created.id) field.onChange(created.id)

View File

@@ -9,20 +9,32 @@ export async function getCompanies() {
return data return data
} }
export async function createCompany(name: string) { export async function createCompany(data: { name: string; street?: string; zip?: string; city?: string; email?: string }) {
const admin = createAdminClient() const admin = createAdminClient()
const { data, error } = await admin.from('companies').insert({ name }).select().single() const { data: dbData, error } = await admin.from('companies').insert({
name: data.name,
street: data.street || null,
zip: data.zip || null,
city: data.city || null,
email: data.email || null,
}).select().single()
if (error) throw error if (error) throw error
revalidatePath('/admin/companies') revalidatePath('/admin/companies')
return data return dbData
} }
export async function updateCompany(id: string, name: string) { export async function updateCompany(id: string, data: { name: string; street?: string; zip?: string; city?: string; email?: string }) {
const admin = createAdminClient() const admin = createAdminClient()
const { data, error } = await admin.from('companies').update({ name }).eq('id', id).select().single() const { data: dbData, error } = await admin.from('companies').update({
name: data.name,
street: data.street || null,
zip: data.zip || null,
city: data.city || null,
email: data.email || null,
}).eq('id', id).select().single()
if (error) throw error if (error) throw error
revalidatePath('/admin/companies') revalidatePath('/admin/companies')
return data return dbData
} }
export async function deleteCompany(id: string) { export async function deleteCompany(id: string) {

View File

@@ -0,0 +1,6 @@
-- Migration: Add delivery address columns to companies table
ALTER TABLE public.companies
ADD COLUMN IF NOT EXISTS street TEXT,
ADD COLUMN IF NOT EXISTS zip TEXT,
ADD COLUMN IF NOT EXISTS city TEXT,
ADD COLUMN IF NOT EXISTS email TEXT;