This commit is contained in:
Timo Knuth
2026-02-27 15:19:24 +01:00
parent b7f8221095
commit 253c3c1c6d
134 changed files with 11188 additions and 1871 deletions

View File

@@ -0,0 +1,89 @@
'use client'
import { useActionState, useState } from 'react'
import { createAdmin } from '../../actions'
export function CreateAdminForm({ orgId }: { orgId: string }) {
const [state, action, isPending] = useActionState(createAdmin, { success: false, error: '' })
const [showForm, setShowForm] = useState(false)
if (!showForm) {
return (
<button
onClick={() => setShowForm(true)}
className="w-full py-2 border-2 border-dashed border-gray-200 rounded-lg text-sm text-gray-500 hover:border-brand-500 hover:text-brand-500 transition-all font-medium"
>
+ Administrator hinzufügen
</button>
)
}
return (
<div className="bg-gray-50 border rounded-xl p-4 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-900">Neuen Admin anlegen</h3>
<button
onClick={() => setShowForm(false)}
className="text-xs text-gray-400 hover:text-gray-600"
>
Abbrechen
</button>
</div>
<form action={action} className="space-y-3">
<input type="hidden" name="orgId" value={orgId} />
<div>
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Name</label>
<input
name="name"
required
placeholder="z.B. Max Mustermann"
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">E-Mail</label>
<input
name="email"
type="email"
required
placeholder="admin@beispiel.de"
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Passwort</label>
<div className="relative">
<input
name="password"
type="text"
required
defaultValue={Math.random().toString(36).slice(-10)}
className="w-full px-3 py-2 border rounded-lg text-sm font-mono focus:ring-2 focus:ring-brand-500 outline-none"
/>
<p className="text-[10px] text-gray-400 mt-1">Das Passwort muss dem Admin manuell mitgeteilt werden.</p>
</div>
</div>
{state.error && (
<p className="text-xs text-red-600 bg-red-50 p-2 rounded">{state.error}</p>
)}
{state.success && (
<p className="text-xs text-green-600 bg-green-50 p-2 rounded">Administrator erfolgreich angelegt.</p>
)}
<button
type="submit"
disabled={isPending}
className="w-full bg-gray-900 text-white py-2 rounded-lg text-sm font-medium hover:bg-gray-800 disabled:opacity-50 transition-colors"
>
{isPending ? 'Wird angelegt...' : 'Admin anlegen'}
</button>
</form>
</div>
)
}

View File

@@ -0,0 +1,94 @@
'use client'
import { useActionState } from 'react'
import { createMember } from '../../actions'
const initialState = {
success: false,
error: '',
}
export function CreateMemberForm({ orgId }: { orgId: string }) {
const [state, action, isPending] = useActionState(createMember, initialState)
return (
<div className="space-y-4">
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wider">Mitglied manuell hinzufügen</h3>
<form action={action} className="space-y-3">
<input type="hidden" name="orgId" value={orgId} />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Name (Ansprechpartner)</label>
<input
name="name"
type="text"
required
placeholder="Anrede Vorname Nachname"
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">E-Mail</label>
<input
name="email"
type="email"
required
placeholder="email@beispiel.de"
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
/>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Betrieb</label>
<input
name="betrieb"
type="text"
required
placeholder="Name des Betriebs"
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Sparte</label>
<input
name="sparte"
type="text"
required
placeholder="z.B. Sanitär"
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
/>
</div>
<div>
<label className="block text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-1">Ort</label>
<input
name="ort"
type="text"
required
placeholder="Stadt"
className="w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-brand-500 outline-none"
/>
</div>
</div>
{state.error && (
<p className="text-xs text-red-600 bg-red-50 p-2 rounded">{state.error}</p>
)}
{state.success && (
<p className="text-xs text-green-600 bg-green-50 p-2 rounded">Mitglied erfolgreich angelegt.</p>
)}
<button
type="submit"
disabled={isPending}
className="w-full bg-gray-900 text-white py-2 rounded-lg text-sm font-medium hover:bg-gray-800 disabled:opacity-50 transition-colors"
>
{isPending ? 'Wird angelegt...' : 'Mitglied anlegen'}
</button>
</form>
</div>
)
}

View File

@@ -0,0 +1,19 @@
'use client'
import { deleteOrganization } from '../../actions'
export function DeleteOrgButton({ id, name }: { id: string; name: string }) {
async function handleDelete() {
if (!confirm(`Innung "${name}" wirklich unwiderruflich löschen? Alle Daten (Mitglieder, News, Termine, Stellen) werden gelöscht.`)) return
await deleteOrganization(id)
}
return (
<button
onClick={handleDelete}
className="w-full mt-2 px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-lg hover:bg-red-700 transition-colors"
>
Innung löschen
</button>
)
}

View File

@@ -0,0 +1,344 @@
'use client'
import { useActionState, useState } from 'react'
import { updateOrganization } from '../../actions'
interface Props {
org: {
id: string
name: string
plan: string
contactEmail: string | null
logoUrl: string | null
primaryColor: string | null
secondaryColor: string | null
landingPageTitle: string | null
landingPageText: string | null
landingPageSectionTitle: string | null
landingPageButtonText: string | null
landingPageHeroImage: string | null
landingPageHeroOverlayOpacity: number | null
landingPageFeatures: string | null
landingPageFooter: string | null
appStoreUrl: string | null
playStoreUrl: string | null
}
}
const initialState = { success: false, error: '' }
export function EditOrgForm({ org }: Props) {
const boundAction = updateOrganization.bind(null, org.id)
const [state, formAction, isPending] = useActionState(boundAction, initialState)
const [logoUrl, setLogoUrl] = useState(org.logoUrl || '')
const [heroImageUrl, setHeroImageUrl] = useState(org.landingPageHeroImage || '')
const [isUploading, setIsUploading] = useState<{ logo?: boolean; hero?: boolean }>({})
const [themeColor, setThemeColor] = useState(org.primaryColor || '#E63946')
const [secondaryColor, setSecondaryColor] = useState(org.secondaryColor || '#FFFFFF')
let initialFeatures = ''
try {
if (org.landingPageFeatures) {
const parsed = JSON.parse(org.landingPageFeatures)
if (Array.isArray(parsed)) {
initialFeatures = parsed.join('\n')
} else {
initialFeatures = org.landingPageFeatures
}
}
} catch {
initialFeatures = org.landingPageFeatures || ''
}
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>, type: 'logo' | 'hero') => {
const file = e.target.files?.[0]
if (!file) return
setIsUploading(prev => ({ ...prev, [type]: true }))
const uploadFormData = new FormData()
uploadFormData.append('file', file)
try {
const res = await fetch('/api/upload', {
method: 'POST',
body: uploadFormData
})
const data = await res.json()
if (data.url) {
if (type === 'logo') setLogoUrl(data.url)
if (type === 'hero') setHeroImageUrl(data.url)
}
} catch (err) {
console.error('Upload failed', err)
} finally {
setIsUploading(prev => ({ ...prev, [type]: false }))
}
}
return (
<div className="bg-white rounded-xl border p-6">
<h2 className="text-base font-semibold text-gray-900 mb-4">Innung bearbeiten</h2>
{state.success && (
<div className="mb-4 p-3 bg-green-50 text-green-700 rounded-lg text-sm">Änderungen gespeichert.</div>
)}
{state.error && (
<div className="mb-4 p-3 bg-red-50 text-red-700 rounded-lg text-sm">{state.error}</div>
)}
<form action={formAction} className="space-y-6">
{/* BASISDATEN */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-gray-900 border-b pb-2">Basisdaten</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name der Innung</label>
<input
type="text"
name="name"
required
defaultValue={org.name}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Plan</label>
<select
name="plan"
defaultValue={org.plan}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500 bg-white"
>
<option value="pilot">Pilot</option>
<option value="standard">Standard</option>
<option value="pro">Pro</option>
<option value="verband">Verband</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Kontakt E-Mail</label>
<input
type="email"
name="contactEmail"
defaultValue={org.contactEmail ?? ''}
placeholder="info@innung.de"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
/>
</div>
</div>
{/* BRANDING */}
<div className="space-y-4 pt-4">
<h3 className="text-sm font-semibold text-gray-900 border-b pb-2">Branding</h3>
<input type="hidden" name="logoUrl" value={logoUrl} />
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Logo</label>
<div className="flex items-center gap-3">
{logoUrl ? (
<div className="w-10 h-10 rounded border bg-gray-50 flex items-center justify-center p-1">
<img src={logoUrl} alt="Logo" className="max-w-full max-h-full object-contain" />
</div>
) : (
<div className="w-10 h-10 rounded border-2 border-dashed flex items-center justify-center text-gray-300">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
</div>
)}
<label className="flex-1 cursor-pointer">
<div className={`px-3 py-2 border rounded-lg text-sm text-center font-medium hover:bg-gray-50 transition-colors ${isUploading.logo ? 'opacity-50' : ''}`}>
{isUploading.logo ? 'Wird hochgeladen...' : 'Logo ändern'}
</div>
<input type="file" onChange={(e) => handleUpload(e, 'logo')} accept="image/*" className="hidden" disabled={isUploading.logo} />
</label>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Primärfarbe</label>
<div className="flex gap-2">
<input
type="color"
name="primaryColor"
value={themeColor}
onChange={(e) => setThemeColor(e.target.value)}
className="h-9 w-12 p-1 border rounded cursor-pointer"
/>
<input
type="text"
value={themeColor}
onChange={(e) => setThemeColor(e.target.value)}
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 font-mono text-sm"
pattern="^#([A-Fa-f0-9]{6})$"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Sekundärfarbe</label>
<div className="flex gap-2">
<input
type="color"
name="secondaryColor"
value={secondaryColor}
onChange={(e) => setSecondaryColor(e.target.value)}
className="h-9 w-12 p-1 border rounded cursor-pointer"
/>
<input
type="text"
value={secondaryColor}
onChange={(e) => setSecondaryColor(e.target.value)}
className="flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500 font-mono text-sm"
pattern="^#([A-Fa-f0-9]{6})$"
/>
</div>
</div>
</div>
</div>
{/* LANDING PAGE */}
<div className="space-y-4 pt-4">
<h3 className="text-sm font-semibold text-gray-900 border-b pb-2">Landing Page</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Hero Titel</label>
<input
type="text"
name="landingPageTitle"
defaultValue={org.landingPageTitle ?? ''}
placeholder="Zukunft des Handwerks gestalten"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Hero Untertitel / Text</label>
<textarea
name="landingPageText"
defaultValue={org.landingPageText ?? ''}
rows={3}
placeholder="Gemeinsam stark für unsere Region."
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Aufmacher Überschrift (Buttons)</label>
<input
type="text"
name="landingPageSectionTitle"
defaultValue={org.landingPageSectionTitle ?? ''}
placeholder={`${org.name || 'Ihre Innung'} Gemeinsam stark fürs Handwerk`}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Button Text (CTA)</label>
<input
type="text"
name="landingPageButtonText"
defaultValue={org.landingPageButtonText ?? ''}
placeholder="Jetzt Mitglied werden"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
/>
</div>
</div>
<input type="hidden" name="landingPageHeroImage" value={heroImageUrl} />
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Hero Hintergrundbild</label>
<div className="flex items-center gap-3">
<label className="flex-1 cursor-pointer">
<div className={`px-3 py-2 border rounded-lg text-sm text-center font-medium hover:bg-gray-50 transition-colors ${isUploading.hero ? 'opacity-50' : ''}`}>
{isUploading.hero ? 'Wird hochgeladen...' : 'Bild auswählen'}
</div>
<input type="file" onChange={(e) => handleUpload(e, 'hero')} accept="image/*" className="hidden" disabled={isUploading.hero} />
</label>
{heroImageUrl && (
<button type="button" onClick={() => setHeroImageUrl('')} className="text-red-500 hover:text-red-600 text-sm">
Entfernen
</button>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex justify-between">
<span>Overlay Deckkraft</span>
<span className="text-gray-500">{org.landingPageHeroOverlayOpacity ?? 50}%</span>
</label>
<input
type="range"
name="landingPageHeroOverlayOpacity"
min="0"
max="100"
defaultValue={org.landingPageHeroOverlayOpacity ?? 50}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Legt fest, wie dunkel der Schleier über dem Hintergrundbild ist, damit der Text gut lesbar bleibt.</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Vorteile / Features</label>
<textarea
name="landingPageFeatures"
defaultValue={initialFeatures}
rows={5}
placeholder="Ein Benefit pro Zeile..."
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
/>
<p className="text-xs text-gray-500 mt-1">Bitte geben Sie pro Zeile einen Vorteil ein. Diese werden als Checkliste auf der Landingpage angezeigt.</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">App Store URL</label>
<input
type="url"
name="appStoreUrl"
defaultValue={org.appStoreUrl ?? ''}
placeholder="https://apps.apple.com/..."
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Google Play URL</label>
<input
type="url"
name="playStoreUrl"
defaultValue={org.playStoreUrl ?? ''}
placeholder="https://play.google.com/..."
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Footer Text</label>
<textarea
name="landingPageFooter"
defaultValue={org.landingPageFooter ?? ''}
rows={2}
placeholder="© 2024 Innung. Alle Rechte vorbehalten."
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-brand-500"
/>
</div>
</div>
<div className="pt-2">
<button
type="submit"
disabled={isPending}
className="w-full bg-brand-500 text-white font-medium py-2 px-4 rounded-lg hover:bg-brand-600 transition-colors disabled:opacity-50"
>
{isPending ? 'Wird gespeichert…' : 'Speichern'}
</button>
</div>
</form>
</div>
)
}

View File

@@ -0,0 +1,25 @@
'use client'
import { removeMember } from '../../actions'
import { useState } from 'react'
export function MemberActions({ member, orgId }: { member: { id: string, name: string }, orgId: string }) {
const [isPending, setIsPending] = useState(false)
const handleRemove = async () => {
if (!confirm(`Möchten Sie das Mitglied ${member.name} wirklich entfernen?`)) return
setIsPending(true)
await removeMember(member.id, orgId)
setIsPending(false)
}
return (
<button
onClick={handleRemove}
disabled={isPending}
className="text-xs text-red-600 hover:text-red-700 font-medium transition-colors"
>
Entfernen
</button>
)
}

View File

@@ -0,0 +1,42 @@
'use client'
import { removeUserRole, updateUserRole } from '../../actions'
import { useState } from 'react'
export function UserRoleActions({ ur, orgId }: { ur: { id: string, role: string, user: { email: string } }, orgId: string }) {
const [isPending, setIsPending] = useState(false)
const handleRemove = async () => {
if (!confirm(`Möchten Sie den Zugriff für ${ur.user.email} wirklich entfernen?`)) return
setIsPending(true)
await removeUserRole(ur.id, orgId)
setIsPending(false)
}
const handleToggleRole = async () => {
const newRole = ur.role === 'admin' ? 'member' : 'admin'
setIsPending(true)
await updateUserRole(ur.id, orgId, newRole)
setIsPending(false)
}
return (
<div className="flex items-center gap-2">
<button
onClick={handleToggleRole}
disabled={isPending}
className="text-xs text-gray-600 hover:text-brand-600 font-medium transition-colors"
title={ur.role === 'admin' ? 'Zum Mitglied machen' : 'Zum Admin machen'}
>
{ur.role === 'admin' ? 'Rolle: Admin' : 'Rolle: Mitglied'}
</button>
<button
onClick={handleRemove}
disabled={isPending}
className="text-xs text-red-600 hover:text-red-700 font-medium transition-colors"
>
Entfernen
</button>
</div>
)
}

View File

@@ -0,0 +1,234 @@
import { prisma } from '@innungsapp/shared'
import { notFound } from 'next/navigation'
import { format } from 'date-fns'
import { de } from 'date-fns/locale'
import Link from 'next/link'
import { EditOrgForm } from './EditOrgForm'
import { DeleteOrgButton } from './DeleteOrgButton'
import { CreateAdminForm } from './CreateAdminForm'
import { CreateMemberForm } from './CreateMemberForm'
import { UserRoleActions } from './UserRoleActions'
import { MemberActions } from './MemberActions'
import { toggleAiFeature } from '../../actions'
const PLAN_COLORS: Record<string, string> = {
pilot: 'bg-gray-100 text-gray-700',
standard: 'bg-blue-100 text-blue-800',
pro: 'bg-purple-100 text-purple-800',
verband: 'bg-amber-100 text-amber-800',
}
export default async function OrgDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const org = await prisma.organization.findUnique({
where: { id },
include: {
_count: {
select: {
members: true,
userRoles: true,
news: true,
termine: true,
stellen: true,
},
},
userRoles: {
include: { user: true },
},
members: {
take: 5,
orderBy: { createdAt: 'desc' },
select: { id: true, name: true, betrieb: true, status: true, createdAt: true },
},
},
})
if (!org) notFound()
const planColor = PLAN_COLORS[org.plan] ?? 'bg-gray-100 text-gray-700'
return (
<div className="space-y-6">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-gray-500">
<Link href="/superadmin" className="hover:text-gray-900 transition-colors">
Alle Innungen
</Link>
</div>
{/* Header */}
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-gray-900">{org.name}</h1>
<span className={`text-xs font-semibold px-2.5 py-0.5 rounded ${planColor}`}>
{org.plan}
</span>
</div>
<div className="flex items-center gap-3 mt-1 text-sm text-gray-500">
<span className="font-mono bg-gray-100 px-2 py-0.5 rounded text-[11px]">{org.slug}</span>
<span></span>
<span>Erstellt {format(org.createdAt, 'dd. MMMM yyyy', { locale: de })}</span>
{org.avvAccepted && (
<>
<span></span>
<span className="text-green-600">AVV akzeptiert</span>
</>
)}
</div>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 sm:grid-cols-5 gap-3">
{[
{ label: 'Mitglieder', value: org._count.members },
{ label: 'Admins', value: org._count.userRoles },
{ label: 'News', value: org._count.news },
{ label: 'Termine', value: org._count.termine },
{ label: 'Stellen', value: org._count.stellen },
].map(({ label, value }) => (
<div key={label} className="bg-white rounded-xl border p-4 text-center">
<div className="text-2xl font-bold text-gray-900">{value}</div>
<div className="text-xs text-gray-500 mt-0.5">{label}</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Edit form */}
<div className="lg:col-span-1 space-y-4">
<EditOrgForm org={org} />
{/* KI-Assistent */}
<div className="bg-white rounded-xl border p-4 space-y-3">
<div>
<h3 className="text-sm font-semibold text-gray-900">KI-Assistent</h3>
<p className="text-xs text-gray-500 mt-0.5">
Aktiviert den KI-Chat-Assistenten für Mitglieder dieser Innung.
</p>
</div>
<div className="flex items-center justify-between">
<span className={`text-sm font-medium ${org.aiEnabled ? 'text-green-700' : 'text-gray-400'}`}>
{org.aiEnabled ? 'Aktiviert' : 'Deaktiviert'}
</span>
<form action={async () => {
'use server'
await toggleAiFeature(org.id, !org.aiEnabled)
}}>
<button
type="submit"
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${org.aiEnabled ? 'bg-green-500' : 'bg-gray-200'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${org.aiEnabled ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</form>
</div>
</div>
{/* Danger zone */}
<div className="bg-white rounded-xl border border-red-200 p-4">
<h3 className="text-sm font-semibold text-red-700 mb-1">Gefahrenzone</h3>
<p className="text-xs text-gray-500 mb-3">
Das Löschen einer Innung entfernt alle zugehörigen Daten unwiderruflich.
</p>
<DeleteOrgButton id={org.id} name={org.name} />
</div>
</div>
{/* Right column: admins + recent members */}
<div className="lg:col-span-2 space-y-6">
{/* Admins */}
<div className="bg-white rounded-xl border overflow-hidden">
<div className="p-4 border-b flex items-center justify-between">
<h2 className="text-base font-semibold text-gray-900">
Nutzer & Rollen ({org.userRoles.length})
</h2>
</div>
<div className="p-4 bg-gray-50/50 border-b">
<CreateAdminForm orgId={org.id} />
</div>
<div className="divide-y">
{org.userRoles.length === 0 ? (
<p className="p-4 text-sm text-gray-400">Noch keine Nutzer zugewiesen.</p>
) : (
org.userRoles.map((ur) => (
<div key={ur.id} className="p-4 flex items-center justify-between">
<div>
<div className="text-sm font-medium text-gray-900">{ur.user.name}</div>
<div className="text-xs text-gray-500">
{ur.user.email}
<span className="ml-2 font-mono text-[10px] bg-gray-100 px-1 py-0.5 rounded">
{ur.role}
</span>
</div>
</div>
<div className="flex items-center gap-4">
{ur.user.emailVerified ? (
<span className="text-[10px] text-green-600 bg-green-50 px-2 py-0.5 rounded-full uppercase font-bold tracking-wider">
Verifiziert
</span>
) : (
<span className="text-[10px] text-amber-600 bg-amber-50 px-2 py-0.5 rounded-full uppercase font-bold tracking-wider">
Eingeladen
</span>
)}
<UserRoleActions ur={ur} orgId={org.id} />
</div>
</div>
))
)}
</div>
</div>
{/* Recent members */}
<div className="bg-white rounded-xl border overflow-hidden">
<div className="p-4 border-b flex items-center justify-between">
<h2 className="text-base font-semibold text-gray-900">
Mitglieder
</h2>
<span className="text-xs text-gray-400">{org._count.members} gesamt</span>
</div>
<div className="p-4 bg-gray-50/50 border-b">
<CreateMemberForm orgId={org.id} />
</div>
<div className="divide-y">
{org.members.length === 0 ? (
<p className="p-4 text-sm text-gray-400">Noch keine Mitglieder.</p>
) : (
org.members.map((m) => (
<div key={m.id} className="p-4 flex items-center justify-between">
<div>
<div className="text-sm font-medium text-gray-900">{m.name}</div>
<div className="text-xs text-gray-500">{m.betrieb}</div>
</div>
<div className="flex items-center gap-4">
<span
className={`text-xs px-2 py-0.5 rounded-full ${m.status === 'aktiv'
? 'bg-green-50 text-green-700'
: 'bg-gray-100 text-gray-500'
}`}
>
{m.status}
</span>
<span className="text-xs text-gray-400">
{format(m.createdAt, 'dd.MM.yy', { locale: de })}
</span>
<MemberActions member={m} orgId={org.id} />
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
</div>
)
}