272 lines
12 KiB
TypeScript
272 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { use } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { trpc } from '@/lib/trpc-client'
|
|
import { getTrpcErrorMessage } from '@/lib/trpc-error'
|
|
import Link from 'next/link'
|
|
import { useState, useEffect } from 'react'
|
|
import { SPARTEN, MEMBER_STATUS_LABELS } from '@innungsapp/shared'
|
|
import { Trash2 } from 'lucide-react'
|
|
|
|
export default function MitgliedEditPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>
|
|
}) {
|
|
const { id } = use(params)
|
|
const router = useRouter()
|
|
const { data: member, isLoading } = trpc.members.byId.useQuery({ id })
|
|
const updateMutation = trpc.members.update.useMutation({
|
|
onSuccess: () => router.push('/dashboard/mitglieder'),
|
|
})
|
|
const deleteMutation = trpc.members.delete.useMutation({
|
|
onSuccess: () => router.push('/dashboard/mitglieder'),
|
|
})
|
|
const resendMutation = trpc.members.resendInvite.useMutation()
|
|
|
|
const [form, setForm] = useState({
|
|
name: '',
|
|
betrieb: '',
|
|
sparte: '',
|
|
ort: '',
|
|
telefon: '',
|
|
email: '',
|
|
status: 'aktiv' as 'aktiv' | 'ruhend' | 'ausgetreten',
|
|
istAusbildungsbetrieb: false,
|
|
seit: undefined as number | undefined,
|
|
role: 'member' as 'member' | 'admin',
|
|
password: '',
|
|
})
|
|
const [isChangingPassword, setIsChangingPassword] = useState(false)
|
|
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (member) {
|
|
setForm({
|
|
name: member.name || '',
|
|
betrieb: member.betrieb || '',
|
|
sparte: member.sparte || '',
|
|
ort: member.ort || '',
|
|
telefon: member.telefon ?? '',
|
|
email: member.email || '',
|
|
status: (member.status as 'aktiv' | 'ruhend' | 'ausgetreten') || 'aktiv',
|
|
istAusbildungsbetrieb: member.istAusbildungsbetrieb || false,
|
|
seit: member.seit ?? undefined,
|
|
// @ts-ignore
|
|
role: member.role || 'member',
|
|
password: '',
|
|
})
|
|
}
|
|
}, [member])
|
|
|
|
if (isLoading) return <div className="text-gray-500">Wird geladen...</div>
|
|
if (!member) return null
|
|
|
|
function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault()
|
|
updateMutation.mutate({ id, data: form })
|
|
}
|
|
|
|
const inputClass =
|
|
'w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent'
|
|
|
|
return (
|
|
<div className="max-w-2xl space-y-6">
|
|
<div className="flex items-center gap-3">
|
|
<Link href="/dashboard/mitglieder" className="text-xs text-gray-400 hover:text-gray-600 uppercase tracking-wide">
|
|
← Zurück
|
|
</Link>
|
|
<span className="text-gray-200">/</span>
|
|
<h1 className="text-2xl font-bold text-gray-900">Mitglied bearbeiten</h1>
|
|
</div>
|
|
|
|
{/* Invite Status */}
|
|
<div className="bg-white rounded-lg border p-4 flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-700">App-Zugang</p>
|
|
<p className="text-xs text-gray-500 mt-0.5">
|
|
{member.userId
|
|
? 'Mitglied hat sich eingeloggt'
|
|
: 'Noch nicht eingeladen / eingeloggt'}
|
|
</p>
|
|
</div>
|
|
{!member.userId && (
|
|
<button
|
|
onClick={() => resendMutation.mutate({ memberId: id })}
|
|
disabled={resendMutation.isPending}
|
|
className="text-sm text-brand-600 hover:underline disabled:opacity-50"
|
|
>
|
|
{resendMutation.isPending ? 'Sende...' : resendMutation.isSuccess ? 'Gesendet' : 'Einladung senden'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-6 pb-20">
|
|
<form onSubmit={handleSubmit} className="bg-white rounded-lg border p-6 space-y-6">
|
|
{/* Section: Stammdaten */}
|
|
<div>
|
|
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Stammdaten</p>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={inputClass} />
|
|
</div>
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Betrieb</label>
|
|
<input value={form.betrieb} onChange={(e) => setForm({ ...form, betrieb: e.target.value })} className={inputClass} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Sparte</label>
|
|
<select value={form.sparte} onChange={(e) => setForm({ ...form, sparte: e.target.value })} className={inputClass}>
|
|
{SPARTEN.map((s) => <option key={s}>{s}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Ort</label>
|
|
<input value={form.ort} onChange={(e) => setForm({ ...form, ort: e.target.value })} className={inputClass} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section: Kontakt */}
|
|
<div className="border-t pt-5">
|
|
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Kontakt</p>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">E-Mail</label>
|
|
<input type="email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} className={inputClass} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Telefon</label>
|
|
<input type="tel" value={form.telefon} onChange={(e) => setForm({ ...form, telefon: e.target.value })} className={inputClass} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section: Status */}
|
|
<div className="border-t pt-5">
|
|
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Status</p>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
|
<select value={form.status} onChange={(e) => setForm({ ...form, status: e.target.value as typeof form.status })} className={inputClass}>
|
|
{(['aktiv', 'ruhend', 'ausgetreten'] as const).map((s) => (
|
|
<option key={s} value={s}>{MEMBER_STATUS_LABELS[s]}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Rolle</label>
|
|
<select value={form.role} onChange={(e) => setForm({ ...form, role: e.target.value as 'member' | 'admin' })} className={inputClass}>
|
|
<option value="member">Mitglied</option>
|
|
<option value="admin">Administrator</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Passwort</label>
|
|
{isChangingPassword ? (
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="password"
|
|
placeholder="Neues Passwort festlegen"
|
|
value={form.password}
|
|
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
|
className={inputClass}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => { setIsChangingPassword(false); setForm({ ...form, password: '' }) }}
|
|
className="text-xs text-gray-400 hover:text-gray-600 px-2"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
readOnly
|
|
value="••••••••"
|
|
className={`${inputClass} bg-gray-50 text-gray-400 cursor-default`}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsChangingPassword(true)}
|
|
className="text-xs text-brand-600 hover:underline px-2 whitespace-nowrap"
|
|
>
|
|
{member.userId ? 'Ändern' : 'Setzen'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Mitglied seit</label>
|
|
<input type="number" value={form.seit ?? ''} onChange={(e) => setForm({ ...form, seit: e.target.value ? Number(e.target.value) : undefined })} className={inputClass} />
|
|
</div>
|
|
<div className="col-span-2">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" checked={form.istAusbildungsbetrieb} onChange={(e) => setForm({ ...form, istAusbildungsbetrieb: e.target.checked })} className="rounded border-gray-300 text-brand-500 focus:ring-brand-500" />
|
|
<span className="text-sm text-gray-700">Ausbildungsbetrieb</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{(updateMutation.error || deleteMutation.error) && (
|
|
<p className="text-sm text-red-600 bg-red-50 px-4 py-2 rounded-lg">
|
|
{getTrpcErrorMessage(updateMutation.error || deleteMutation.error)}
|
|
</p>
|
|
)}
|
|
|
|
<div className="flex gap-3 pt-2 border-t">
|
|
<button type="submit" disabled={updateMutation.isPending} className="bg-brand-500 text-white px-6 py-2 rounded-lg text-sm font-medium hover:bg-brand-600 disabled:opacity-60 transition-colors">
|
|
{updateMutation.isPending ? 'Wird gespeichert...' : 'Speichern'}
|
|
</button>
|
|
<Link href="/dashboard/mitglieder" className="px-6 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-100 transition-colors">
|
|
Abbrechen
|
|
</Link>
|
|
</div>
|
|
</form>
|
|
|
|
{/* Danger Zone */}
|
|
<div className="bg-red-50 rounded-lg border border-red-100 p-6 flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-bold text-red-900">Mitglied löschen</p>
|
|
<p className="text-xs text-red-700 mt-1 max-w-sm">
|
|
Dies entfernt das Mitglied permanent. Der App-Zugang wird ebenfalls entzogen.
|
|
Diese Aktion kann nicht rückgängig gemacht werden.
|
|
</p>
|
|
</div>
|
|
|
|
{showConfirmDelete ? (
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => deleteMutation.mutate({ id })}
|
|
disabled={deleteMutation.isPending}
|
|
className="bg-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-red-700 transition-colors shadow-sm disabled:opacity-50"
|
|
>
|
|
{deleteMutation.isPending ? 'Lösche...' : 'Endgültig löschen'}
|
|
</button>
|
|
<button
|
|
onClick={() => setShowConfirmDelete(false)}
|
|
className="bg-white text-gray-700 px-4 py-2 rounded-lg text-sm font-medium border border-gray-200 hover:bg-gray-50 transition-colors"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<button
|
|
onClick={() => setShowConfirmDelete(true)}
|
|
className="text-red-600 hover:text-red-700 font-medium text-sm flex items-center gap-1 bg-white px-4 py-2 rounded-lg border border-red-200 hover:bg-red-50 transition-all shadow-sm"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
Löschen
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|