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'
|
'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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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