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
All checks were successful
Staging Build / build (push) Successful in 2m37s
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
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 { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -13,13 +13,20 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} 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() {
|
||||
const [companies, setCompanies] = useState<any[]>([])
|
||||
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 [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)
|
||||
@@ -40,17 +47,38 @@ export default function CompaniesPage() {
|
||||
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()
|
||||
setCreating(true)
|
||||
setCreateError(null)
|
||||
try {
|
||||
await createCompany(name)
|
||||
setIsCreateOpen(false)
|
||||
setName('')
|
||||
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 Anlegen.')
|
||||
setCreateError(err.message || 'Fehler beim Speichern.')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
@@ -70,7 +98,7 @@ export default function CompaniesPage() {
|
||||
}
|
||||
|
||||
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 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -79,11 +107,11 @@ export default function CompaniesPage() {
|
||||
Firmenverwaltung
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setIsCreateOpen(true)}
|
||||
onClick={() => handleOpenDialog()}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" /> Firma anlegen
|
||||
@@ -107,6 +135,8 @@ export default function CompaniesPage() {
|
||||
<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>
|
||||
@@ -120,10 +150,36 @@ export default function CompaniesPage() {
|
||||
{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"
|
||||
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"
|
||||
@@ -136,6 +192,7 @@ export default function CompaniesPage() {
|
||||
? <Loader2 className="h-4 w-4 animate-spin" />
|
||||
: <Trash2 className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -144,16 +201,18 @@ export default function CompaniesPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dialog: Neue Firma */}
|
||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
||||
{/* 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>Neue Firma anlegen</DialogTitle>
|
||||
<DialogTitle>
|
||||
{editingCompany ? 'Firma bearbeiten' : 'Neue Firma anlegen'}
|
||||
</DialogTitle>
|
||||
<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>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleCreate} className="space-y-4 mt-2">
|
||||
<form onSubmit={handleSubmit} className="space-y-4 mt-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="company-name">Firmenname</Label>
|
||||
<Input
|
||||
@@ -166,16 +225,70 @@ export default function CompaniesPage() {
|
||||
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={() => setIsCreateOpen(false)} disabled={creating}>
|
||||
<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}
|
||||
Anlegen
|
||||
{editingCompany ? 'Speichern' : 'Anlegen'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@@ -280,7 +280,7 @@ export function UserDialog({ companies }: UserDialogProps) {
|
||||
if (trimmed) {
|
||||
setIsCreatingCompany(true)
|
||||
try {
|
||||
const created = await createCompany(trimmed)
|
||||
const created = await createCompany({ name: trimmed })
|
||||
if (created) {
|
||||
setLocalCompanies((prev) => [...prev, created])
|
||||
field.onChange(created.id)
|
||||
|
||||
@@ -9,20 +9,32 @@ export async function getCompanies() {
|
||||
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 { 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
|
||||
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 { 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
|
||||
revalidatePath('/admin/companies')
|
||||
return data
|
||||
return dbData
|
||||
}
|
||||
|
||||
export async function deleteCompany(id: string) {
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user