Rebuild as InnungsApp project: replace stadtwerke analysis with full documentation
- PRD: vollständige Produktspezifikation (5 Module, Scope, Akzeptanzkriterien) - ARCHITECTURE: Tech Stack, Ordnerstruktur, Multi-Tenancy, Push, Kosten - DATABASE_SCHEMA: Vollständiges SQL-Schema mit RLS Policies und Views - USER_STORIES: 40+ Stories nach Rolle (Admin, Mitglied, Azubi, Obermeister) - PERSONAS: 5 detaillierte Nutzerprofile mit Alltag, Zitaten und Erwartungen - BUSINESS_MODEL: Preistabellen, Unit Economics, Revenue-Projektionen, Distribution - ROADMAP: 6 Phasen, Sprint-Planung, Meilensteine und KPIs - COMPETITIVE_ANALYSIS: Wettbewerbsmatrix, USPs, Preispositionierung - API_DESIGN: Supabase Query Patterns, Edge Functions, Realtime Subscriptions - ONBOARDING_FLOWS: 7 User Flows von Setup bis Fehlerfall - GTM_STRATEGY: 3-Phasen-Vertrieb, Outreach-Sequenz, Einwandbehandlung - AZUBI_MODULE: Video-Feed, 1-Click-Apply, Chat, Berichtsheft, Quiz - DSGVO_KONZEPT: Rechtsgrundlagen, TOMs, AVV, Minderjährige, Incident Response - FEATURES_BACKLOG: 72 Features nach MoSCoW + Technische Schulden Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
124
innungsapp/apps/admin/app/dashboard/termine/page.tsx
Normal file
124
innungsapp/apps/admin/app/dashboard/termine/page.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { prisma } from '@innungsapp/shared'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { headers } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { TERMIN_TYP_LABELS } from '@innungsapp/shared'
|
||||
import { format } from 'date-fns'
|
||||
import { de } from 'date-fns/locale'
|
||||
|
||||
const TYP_COLORS: Record<string, string> = {
|
||||
Pruefung: 'bg-blue-100 text-blue-700',
|
||||
Versammlung: 'bg-purple-100 text-purple-700',
|
||||
Kurs: 'bg-green-100 text-green-700',
|
||||
Event: 'bg-yellow-100 text-yellow-700',
|
||||
Sonstiges: 'bg-gray-100 text-gray-700',
|
||||
}
|
||||
|
||||
export default async function TerminePage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
if (!session?.user) redirect('/login')
|
||||
const userRole = await prisma.userRole.findFirst({
|
||||
where: { userId: session.user.id, role: 'admin' },
|
||||
})
|
||||
if (!userRole) redirect('/dashboard')
|
||||
|
||||
const now = new Date()
|
||||
const [upcoming, past] = await Promise.all([
|
||||
prisma.termin.findMany({
|
||||
where: { orgId: userRole.orgId, datum: { gte: now } },
|
||||
include: { anmeldungen: { select: { id: true } } },
|
||||
orderBy: { datum: 'asc' },
|
||||
}),
|
||||
prisma.termin.findMany({
|
||||
where: { orgId: userRole.orgId, datum: { lt: now } },
|
||||
include: { anmeldungen: { select: { id: true } } },
|
||||
orderBy: { datum: 'desc' },
|
||||
take: 10,
|
||||
}),
|
||||
])
|
||||
|
||||
const TerminRow = ({ t }: { t: typeof upcoming[0] }) => (
|
||||
<tr>
|
||||
<td>
|
||||
<div className="text-center w-10">
|
||||
<p className="font-bold text-brand-500">{format(t.datum, 'dd', { locale: de })}</p>
|
||||
<p className="text-xs text-gray-400 uppercase">{format(t.datum, 'MMM', { locale: de })}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<p className="font-medium text-gray-900">{t.titel}</p>
|
||||
{t.ort && <p className="text-xs text-gray-500">{t.ort}</p>}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${TYP_COLORS[t.typ]}`}>
|
||||
{TERMIN_TYP_LABELS[t.typ]}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{t.maxTeilnehmer
|
||||
? `${t.anmeldungen.length} / ${t.maxTeilnehmer}`
|
||||
: t.anmeldungen.length}
|
||||
</td>
|
||||
<td>
|
||||
<Link href={`/dashboard/termine/${t.id}`} className="text-sm text-brand-600 hover:underline">
|
||||
Bearbeiten
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Termine</h1>
|
||||
<Link
|
||||
href="/dashboard/termine/neu"
|
||||
className="bg-brand-500 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-brand-600 transition-colors"
|
||||
>
|
||||
+ Termin anlegen
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
||||
Bevorstehend ({upcoming.length})
|
||||
</h2>
|
||||
<div className="bg-white rounded-xl border shadow-sm overflow-hidden">
|
||||
<table className="w-full data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Titel</th>
|
||||
<th>Typ</th>
|
||||
<th>Anmeldungen</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{upcoming.map((t) => <TerminRow key={t.id} t={t} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
{upcoming.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">Keine bevorstehenden Termine</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{past.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
||||
Vergangen
|
||||
</h2>
|
||||
<div className="bg-white rounded-xl border shadow-sm overflow-hidden opacity-70">
|
||||
<table className="w-full data-table">
|
||||
<tbody>
|
||||
{past.map((t) => <TerminRow key={t.id} t={t} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user