feat(sprint8): Phase 4 — Dokumentenarchiv + Vorstandsverwaltung
Backend: - V20 migration: documents table with category, access_level, file storage - V21 migration: board_positions + board_members with term tracking - Document entity + DocumentCategory/DocumentAccessLevel enums - BoardPosition + BoardMember entities - Extended AuditEventType (DOCUMENT_UPLOADED/DELETED, BOARD_MEMBER_ELECTED/REMOVED) - Extended StaffPermission (MANAGE_DOCUMENTS) - Extended NotificationType (BOARD_TERM_EXPIRING) - DocumentService: upload, list, download, delete, storage usage - BoardService: positions CRUD, elect/remove members, current/history - DocumentController: multipart upload, filtered list, download, delete, portal - BoardController: positions, elect, remove, current board, history, portal Frontend: - documents.ts + board.ts service layers - Admin /documents page: grouped by category, upload dialog, filter, download/delete - Admin /board page: current board cards, position management, elect member dialog - Navigation: added Dokumente + Vorstand to sidebar - i18n: documents.* + board.* keys in de.json + en.json
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { apiClient } from "@/lib/api-client"
|
||||
|
||||
export type DocumentCategory =
|
||||
| "SATZUNG"
|
||||
| "PROTOKOLL"
|
||||
| "VERTRAG"
|
||||
| "VERSICHERUNG"
|
||||
| "GENEHMIGUNG"
|
||||
| "SONSTIGES"
|
||||
|
||||
export type DocumentAccessLevel = "ALL_MEMBERS" | "BOARD_ONLY"
|
||||
|
||||
export interface ClubDocument {
|
||||
id: string
|
||||
title: string
|
||||
category: DocumentCategory
|
||||
filename: string
|
||||
contentType: string
|
||||
fileSize: number
|
||||
accessLevel: DocumentAccessLevel
|
||||
description: string | null
|
||||
uploadedBy: string
|
||||
createdAt: string
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
export interface StorageUsage {
|
||||
bytesUsed: number
|
||||
}
|
||||
|
||||
export async function uploadDocument(
|
||||
clubId: string,
|
||||
title: string,
|
||||
category: DocumentCategory,
|
||||
accessLevel: DocumentAccessLevel,
|
||||
description: string | null,
|
||||
file: File
|
||||
): Promise<ClubDocument> {
|
||||
const formData = new FormData()
|
||||
formData.append("file", file)
|
||||
|
||||
const params = new URLSearchParams({
|
||||
clubId,
|
||||
title,
|
||||
category,
|
||||
accessLevel,
|
||||
})
|
||||
if (description) params.append("description", description)
|
||||
|
||||
// Multipart upload — use raw fetch since apiClient assumes JSON
|
||||
const res = await fetch(
|
||||
`/api/backend/documents/upload?${params.toString()}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
if (!res.ok) throw new Error("Upload failed")
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export function listDocuments(
|
||||
clubId: string,
|
||||
category?: DocumentCategory,
|
||||
accessLevel?: DocumentAccessLevel
|
||||
): Promise<ClubDocument[]> {
|
||||
const params = new URLSearchParams({ clubId })
|
||||
if (category) params.append("category", category)
|
||||
if (accessLevel) params.append("accessLevel", accessLevel)
|
||||
return apiClient<ClubDocument[]>(`/documents?${params.toString()}`)
|
||||
}
|
||||
|
||||
export async function downloadDocument(id: string): Promise<Blob> {
|
||||
const res = await fetch(`/api/backend/documents/${id}/download`)
|
||||
if (!res.ok) throw new Error("Download failed")
|
||||
return res.blob()
|
||||
}
|
||||
|
||||
export function deleteDocument(id: string, clubId: string): Promise<void> {
|
||||
return apiClient<void>(`/documents/${id}?clubId=${clubId}`, {
|
||||
method: "DELETE",
|
||||
})
|
||||
}
|
||||
|
||||
export function getStorageUsage(clubId: string): Promise<StorageUsage> {
|
||||
return apiClient<StorageUsage>(`/documents/usage?clubId=${clubId}`)
|
||||
}
|
||||
|
||||
export function getPortalDocuments(clubId: string): Promise<ClubDocument[]> {
|
||||
return apiClient<ClubDocument[]>(`/portal/documents?clubId=${clubId}`)
|
||||
}
|
||||
|
||||
// Helper: format file size
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
// Category labels
|
||||
export const categoryLabels: Record<DocumentCategory, string> = {
|
||||
SATZUNG: "Satzung",
|
||||
PROTOKOLL: "Protokoll",
|
||||
VERTRAG: "Vertrag",
|
||||
VERSICHERUNG: "Versicherung",
|
||||
GENEHMIGUNG: "Genehmigung",
|
||||
SONSTIGES: "Sonstiges",
|
||||
}
|
||||
Reference in New Issue
Block a user