stadtwerke/innungsapp/apps/admin/app/superadmin/page.tsx

237 lines
15 KiB
TypeScript

import { prisma } from '@innungsapp/shared'
import { format } from 'date-fns'
import { de } from 'date-fns/locale'
import Link from 'next/link'
import { toggleAiFeature } from './actions'
const PLAN_LABELS: Record<string, string> = {
pilot: 'Pilot',
standard: 'Standard',
pro: 'Pro',
verband: 'Verband',
}
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',
}
const PAGE_SIZE = 20
export default async function SuperAdminPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string }>
}) {
const { q = '', page = '1' } = await searchParams
const currentPage = Math.max(1, parseInt(page, 10))
const skip = (currentPage - 1) * PAGE_SIZE
const where = q
? {
OR: [
{ name: { contains: q, mode: 'insensitive' } },
{ slug: { contains: q, mode: 'insensitive' } },
{ contactEmail: { contains: q, mode: 'insensitive' } },
],
}
: {}
const [organizations, total] = await Promise.all([
prisma.organization.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: PAGE_SIZE,
include: { _count: { select: { members: true, userRoles: true } } },
}),
prisma.organization.count({ where }),
])
const totalPages = Math.ceil(total / PAGE_SIZE)
return (
<div className="max-w-[1400px] mx-auto space-y-12 py-4">
<div className="flex justify-between items-center">
<div className="text-left space-y-2">
<h1 className="text-3xl font-black text-gray-900 tracking-tight font-outfit">
Innungs-Verwaltung <span className="text-[#E63946]">PRO</span>
</h1>
<p className="text-gray-400 font-medium">Hierüber werden alle Mandanten der Lösung verwaltet.</p>
</div>
<Link
href="/superadmin/create"
className="bg-[#E63946] text-white font-bold py-3 px-6 rounded-xl hover:bg-[#D62839] transition-all shadow-md shadow-red-100 flex items-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2.5} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Neue Innung anlegen
</Link>
</div>
<div className="grid grid-cols-1 gap-12 items-start">
{/* List */}
<div className="space-y-6">
{/* Search & Filter */}
<div className="bg-white p-2 rounded-2xl border shadow-sm flex items-center">
<form method="GET" className="flex-1 flex gap-2">
<div className="relative flex-1 group">
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none text-gray-400 group-focus-within:text-[#E63946] transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-4 h-4">
<path strokeLinecap="round" strokeLinejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>
</div>
<input
type="search"
name="q"
defaultValue={q}
placeholder="Innung suchen..."
className="w-full pl-9 pr-4 py-3 bg-transparent text-sm outline-none placeholder:text-gray-300"
/>
</div>
<button
type="submit"
className="px-6 py-2.5 bg-gray-900 text-white rounded-xl text-sm font-bold hover:bg-black transition-all active:scale-[0.98]"
>
Suchen
</button>
{q && (
<Link
href="/superadmin"
className="p-2.5 bg-gray-50 text-gray-400 rounded-xl hover:bg-gray-100 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</Link>
)}
</form>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between px-2">
<h2 className="text-sm font-bold text-gray-400 uppercase tracking-widest">
Registrierte Innungen ({total})
</h2>
</div>
<div className="flex flex-col gap-4">
{organizations.length === 0 ? (
<div className="bg-white p-12 text-center rounded-2xl border border-dashed border-gray-200">
<div className="text-gray-300 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1} stroke="currentColor" className="w-12 h-12 mx-auto">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 21v-7.5a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75V21m-1.5 0H21m-8.47-17.69-6 6a.75.75 0 0 0-.215.53V21m1.5 0H1.875a.375.375 0 0 1-.375-.375V11.25c0-4.46 3.07-8.189 7.5-9.088a9 9 0 0 1 1.585-.152Z" />
</svg>
</div>
<p className="text-gray-500 font-medium">
{q ? 'Keine Treffer für Ihre Suche.' : 'Bisher keine Innungen angelegt.'}
</p>
</div>
) : (
organizations.map((org) => (
<div key={org.id} className="group bg-white p-6 rounded-2xl border hover:border-[#E63946] hover:shadow-xl hover:shadow-red-500/5 transition-all duration-300 relative overflow-hidden">
<div className="flex justify-between items-start gap-6 relative z-10">
<Link href={`/superadmin/organizations/${org.id}`} className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h3 className="font-bold text-lg text-gray-900 group-hover:text-[#E63946] transition-colors">{org.name}</h3>
<span className={`text-[10px] font-black uppercase tracking-tighter px-2 py-0.5 rounded-full border ${PLAN_COLORS[org.plan] ?? 'bg-gray-100 text-gray-700'}`}>
{org.plan}
</span>
</div>
<div className="flex items-center gap-4 text-xs text-gray-400 font-medium">
<div className="flex items-center gap-1.5 font-mono">
<span className="text-[#E63946]">@</span>
<span>{org.slug}</span>
</div>
<span className="w-1 h-1 rounded-full bg-gray-200" />
<span>{org.contactEmail || 'Keine Kontaktmail'}</span>
</div>
</Link>
<div className="flex items-center gap-2 lg:opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-2 group-hover:translate-x-0">
<form action={async () => {
'use server'
await toggleAiFeature(org.id, !org.aiEnabled)
}}>
<button
type="submit"
className={`p-2 rounded-xl border transition-all ${org.aiEnabled
? 'bg-green-50 text-green-600 border-green-100 hover:bg-red-50 hover:text-red-600'
: 'bg-gray-50 text-gray-400 border-gray-100 hover:bg-green-50 hover:text-green-600'}`}
title={org.aiEnabled ? 'KI Deaktivieren' : 'KI Aktivieren'}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.59 14.37a6 6 0 0 1-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 0 0 6.16-12.12A14.98 14.98 0 0 0 9.59 8.31m5.84 6.06a6.01 6.01 0 0 1-5.84-1.29m0 0a6.01 6.01 0 0 1 0-8.5l.08.08a6.01 6.01 0 0 1 0 8.42Z" />
</svg>
</button>
</form>
<Link
href={`/superadmin/organizations/${org.id}`}
className="p-2 bg-gray-900 text-white rounded-xl hover:bg-black transition-all"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</Link>
</div>
</div>
<div className="mt-6 flex items-center gap-6">
<div className="flex flex-col">
<span className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Mitglieder</span>
<span className="font-bold text-gray-900">{org._count.members}</span>
</div>
<div className="w-px h-6 bg-gray-100" />
<div className="flex flex-col">
<span className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Admins</span>
<span className="font-bold text-gray-900">{org._count.userRoles}</span>
</div>
<div className="w-px h-6 bg-gray-100 ml-auto" />
<div className="flex flex-col items-end">
<span className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Erstellt am</span>
<span className="text-xs font-semibold text-gray-600">{format(org.createdAt, 'dd.MM.yyyy', { locale: de })}</span>
</div>
</div>
</div>
))
)}
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="pt-8 flex items-center justify-between border-t border-gray-100">
<span className="text-xs font-bold text-gray-400 uppercase tracking-widest">
Seite {currentPage} / {totalPages}
</span>
<div className="flex gap-2">
{currentPage > 1 && (
<Link
href={`/superadmin?${new URLSearchParams({ q, page: String(currentPage - 1) })}`}
className="px-4 py-2 bg-white border border-gray-200 rounded-xl text-xs font-bold text-gray-600 hover:bg-gray-50 transition-all active:scale-[0.98]"
>
Zurück
</Link>
)}
{currentPage < totalPages && (
<Link
href={`/superadmin?${new URLSearchParams({ q, page: String(currentPage + 1) })}`}
className="px-4 py-2 bg-white border border-gray-200 rounded-xl text-xs font-bold text-gray-600 hover:bg-gray-50 transition-all active:scale-[0.98]"
>
Weiter
</Link>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div>
)
}