Files
cannamanage/cannamanage-frontend/src/app/(dashboard-layout)/assemblies/[id]/page.tsx
T

327 lines
10 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { useParams, useRouter } from "next/navigation"
import {
closeVote,
completeAssembly,
downloadProtocol,
getAssemblyDetail,
sendInvitations,
startAssembly,
} from "@/services/assemblies"
import {
ArrowLeft,
CheckCircle,
FileDown,
Play,
Send,
Square,
Users,
Vote,
} from "lucide-react"
import type { AssemblyDetail, AssemblyStatus } from "@/services/assemblies"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Progress } from "@/components/ui/progress"
const statusLabels: Record<AssemblyStatus, string> = {
PLANNED: "Geplant",
INVITED: "Eingeladen",
IN_PROGRESS: "Läuft",
COMPLETED: "Abgeschlossen",
CANCELLED: "Abgesagt",
}
const statusColors: Record<AssemblyStatus, string> = {
PLANNED: "bg-gray-500/20 text-gray-400",
INVITED: "bg-blue-500/20 text-blue-400",
IN_PROGRESS: "bg-green-500/20 text-green-400",
COMPLETED: "bg-emerald-500/20 text-emerald-400",
CANCELLED: "bg-red-500/20 text-red-400",
}
export default function AssemblyDetailPage() {
const { id } = useParams<{ id: string }>()
const router = useRouter()
const [detail, setDetail] = useState<AssemblyDetail | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (id) loadDetail()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id])
async function loadDetail() {
try {
const data = await getAssemblyDetail(id)
setDetail(data)
} catch (e) {
console.error("Failed to load assembly", e)
} finally {
setLoading(false)
}
}
async function handleSendInvitations() {
await sendInvitations(id)
loadDetail()
}
async function handleStart() {
await startAssembly(id)
loadDetail()
}
async function handleComplete() {
await completeAssembly(id)
loadDetail()
}
async function handleDownloadProtocol() {
const blob = await downloadProtocol(id)
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `protokoll-${id}.pdf`
a.click()
window.URL.revokeObjectURL(url)
}
async function handleCloseVote(voteId: string) {
await closeVote(voteId)
loadDetail()
}
if (loading || !detail) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-muted-foreground">Laden...</p>
</div>
)
}
const { assembly, agendaItems, attendees, votes, quorum } = detail
const quorumPercent =
quorum.totalMembers > 0
? Math.round((quorum.attendees / quorum.totalMembers) * 100)
: 0
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={() => router.push("/assemblies")}
>
<ArrowLeft className="h-5 w-5" />
</Button>
<div className="flex-1">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">{assembly.title}</h1>
<Badge className={statusColors[assembly.status]}>
{statusLabels[assembly.status]}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{new Date(assembly.scheduledAt).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
{assembly.location && `${assembly.location}`}
</p>
</div>
<div className="flex gap-2">
{assembly.status === "PLANNED" && (
<Button onClick={handleSendInvitations}>
<Send className="mr-2 h-4 w-4" />
Einladen
</Button>
)}
{(assembly.status === "PLANNED" || assembly.status === "INVITED") && (
<Button onClick={handleStart} variant="default">
<Play className="mr-2 h-4 w-4" />
Starten
</Button>
)}
{assembly.status === "IN_PROGRESS" && (
<Button onClick={handleComplete} variant="default">
<Square className="mr-2 h-4 w-4" />
Beenden
</Button>
)}
{assembly.status === "COMPLETED" && (
<Button onClick={handleDownloadProtocol} variant="outline">
<FileDown className="mr-2 h-4 w-4" />
Protokoll
</Button>
)}
</div>
</div>
{/* Quorum Card */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Users className="h-4 w-4" />
Beschlussfähigkeit
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<Progress value={quorumPercent} className="flex-1" />
<span className="text-sm font-medium">
{quorum.attendees} / {quorum.totalMembers}
</span>
<Badge
className={
quorum.quorumMet
? "bg-green-500/20 text-green-400"
: "bg-red-500/20 text-red-400"
}
>
{quorum.quorumMet
? "Beschlussfähig"
: `${quorum.required} benötigt`}
</Badge>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Agenda */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Vote className="h-5 w-5" />
Tagesordnung
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{agendaItems.map((item) => (
<div
key={item.id}
className="flex items-start gap-3 p-3 rounded-md bg-muted/50"
>
<span className="text-sm font-bold text-muted-foreground min-w-[40px]">
TOP {item.position}
</span>
<div className="flex-1">
<p className="font-medium">{item.title}</p>
{item.description && (
<p className="text-sm text-muted-foreground mt-1">
{item.description}
</p>
)}
<Badge variant="outline" className="mt-1 text-xs">
{item.itemType}
</Badge>
</div>
</div>
))}
{agendaItems.length === 0 && (
<p className="text-muted-foreground text-sm">
Keine Tagesordnungspunkte
</p>
)}
</CardContent>
</Card>
{/* Votes */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Vote className="h-5 w-5" />
Abstimmungen
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{votes.map((vote) => (
<div
key={vote.id}
className="p-3 rounded-md bg-muted/50 space-y-2"
>
<div className="flex items-center justify-between">
<p className="font-medium">{vote.title}</p>
{vote.result ? (
<Badge
className={
vote.result === "ACCEPTED"
? "bg-green-500/20 text-green-400"
: "bg-red-500/20 text-red-400"
}
>
{vote.result === "ACCEPTED" ? "Angenommen" : "Abgelehnt"}
</Badge>
) : (
<Button
size="sm"
variant="outline"
onClick={() => handleCloseVote(vote.id)}
>
Schließen
</Button>
)}
</div>
<div className="flex gap-4 text-sm">
<span className="text-green-400"> {vote.yesCount} Ja</span>
<span className="text-red-400"> {vote.noCount} Nein</span>
<span className="text-muted-foreground">
{vote.abstainCount} Enthaltung
</span>
</div>
</div>
))}
{votes.length === 0 && (
<p className="text-muted-foreground text-sm">
Keine Abstimmungen
</p>
)}
</CardContent>
</Card>
</div>
{/* Attendees */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
Anwesende ({attendees.length})
</CardTitle>
</CardHeader>
<CardContent>
{attendees.length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{attendees.map((a) => (
<div
key={a.id}
className="flex items-center gap-2 p-2 rounded bg-muted/50 text-sm"
>
<CheckCircle className="h-4 w-4 text-green-400" />
<span>{a.memberId.slice(0, 8)}...</span>
{a.proxyForMemberId && (
<Badge variant="outline" className="text-xs">
Vollmacht
</Badge>
)}
</div>
))}
</div>
) : (
<p className="text-muted-foreground text-sm">
Noch keine Anwesenden eingecheckt
</p>
)}
</CardContent>
</Card>
</div>
)
}