feat(sprint8): Phase 3 — Mitgliederversammlung (assemblies, voting, protocol PDF)

Backend:
- V19 migration: assemblies, assembly_agenda_items, assembly_attendees, assembly_votes, assembly_vote_records
- Enums: AssemblyType, AssemblyStatus, AgendaItemType, VoteType, VoteDecision, VoteResult
- Entities: Assembly, AssemblyAgendaItem, AssemblyAttendee, AssemblyVote, AssemblyVoteRecord
- Repositories: Assembly, AgendaItem, Attendee, Vote, VoteRecord
- AssemblyService: full lifecycle (create, invite, start, attend, vote, quorum, complete)
- AssemblyProtocolService: OpenPDF protocol generation (§147 AO compliant)
- AssemblyController: admin + portal endpoints
- Extended: AuditEventType, NotificationType, StaffPermission

Frontend:
- Assembly service with full API client and TypeScript types
- Admin assemblies list page with create dialog (agenda builder)
- Admin assembly detail page (quorum, agenda, votes, attendees)
- Navigation: Versammlungen with Gavel icon (after Finanzen)

Legal basis: §32-§40 BGB (Mitgliederversammlung), §147 AO (retention)
This commit is contained in:
Patrick Plate
2026-06-15 08:39:10 +02:00
parent 3211ade5be
commit b22702317a
57 changed files with 6338 additions and 55 deletions
@@ -1,6 +1,6 @@
"use client"
import { useEffect, useCallback, useRef } from "react"
import { useEffect, useRef } from "react"
/**
* Hook to subscribe to forum WebSocket events for a specific club.
@@ -12,6 +12,7 @@ export function useForumSubscription(
onNewTopic?: (data: ForumTopicEvent) => void,
onNewReply?: (data: ForumReplyEvent) => void
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stompClientRef = useRef<any>(null)
useEffect(() => {
@@ -41,12 +42,15 @@ export function useForumSubscription(
// Subscribe to specific topic if provided
if (topicId) {
client.subscribe(`/topic/club.${clubId}.forum.${topicId}`, (message) => {
const data = JSON.parse(message.body)
if (data.type === "NEW_REPLY" && onNewReply) {
onNewReply(data as ForumReplyEvent)
client.subscribe(
`/topic/club.${clubId}.forum.${topicId}`,
(message) => {
const data = JSON.parse(message.body)
if (data.type === "NEW_REPLY" && onNewReply) {
onNewReply(data as ForumReplyEvent)
}
}
})
)
}
}
@@ -10,6 +10,7 @@ export function useInfoBoardSubscription(
clubId: string | undefined,
onNewPost?: (data: InfoBoardPostEvent) => void
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stompClientRef = useRef<any>(null)
useEffect(() => {