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,100 @@
|
||||
package de.cannamanage.api.controller;
|
||||
|
||||
import de.cannamanage.domain.entity.BoardMember;
|
||||
import de.cannamanage.domain.entity.BoardPosition;
|
||||
import de.cannamanage.service.BoardService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
public class BoardController {
|
||||
|
||||
private final BoardService boardService;
|
||||
|
||||
public BoardController(BoardService boardService) {
|
||||
this.boardService = boardService;
|
||||
}
|
||||
|
||||
// --- Positions ---
|
||||
|
||||
@PostMapping("/board/positions")
|
||||
public ResponseEntity<BoardPosition> createPosition(
|
||||
@RequestParam UUID clubId,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
String title = (String) body.get("title");
|
||||
String description = (String) body.get("description");
|
||||
Integer sortOrder = body.containsKey("sortOrder") ? (Integer) body.get("sortOrder") : 0;
|
||||
BoardPosition pos = boardService.createPosition(clubId, title, description, sortOrder);
|
||||
return ResponseEntity.ok(pos);
|
||||
}
|
||||
|
||||
@GetMapping("/board/positions")
|
||||
public ResponseEntity<List<BoardPosition>> getPositions(@RequestParam UUID clubId) {
|
||||
return ResponseEntity.ok(boardService.getPositions(clubId));
|
||||
}
|
||||
|
||||
@PutMapping("/board/positions/{id}")
|
||||
public ResponseEntity<BoardPosition> updatePosition(
|
||||
@PathVariable UUID id,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
String title = (String) body.get("title");
|
||||
String description = (String) body.get("description");
|
||||
Integer sortOrder = body.containsKey("sortOrder") ? (Integer) body.get("sortOrder") : null;
|
||||
Boolean isActive = body.containsKey("isActive") ? (Boolean) body.get("isActive") : null;
|
||||
BoardPosition pos = boardService.updatePosition(id, title, description, sortOrder, isActive);
|
||||
return ResponseEntity.ok(pos);
|
||||
}
|
||||
|
||||
// --- Board Members ---
|
||||
|
||||
@PostMapping("/board/members")
|
||||
public ResponseEntity<BoardMember> electBoardMember(
|
||||
@RequestParam UUID clubId,
|
||||
@RequestBody Map<String, Object> body,
|
||||
Principal principal) {
|
||||
UUID positionId = UUID.fromString((String) body.get("positionId"));
|
||||
UUID memberId = UUID.fromString((String) body.get("memberId"));
|
||||
LocalDate electedAt = LocalDate.parse((String) body.get("electedAt"));
|
||||
LocalDate termStart = LocalDate.parse((String) body.get("termStart"));
|
||||
LocalDate termEnd = body.get("termEnd") != null ? LocalDate.parse((String) body.get("termEnd")) : null;
|
||||
UUID assemblyId = body.get("assemblyId") != null ? UUID.fromString((String) body.get("assemblyId")) : null;
|
||||
UUID userId = UUID.fromString(principal.getName());
|
||||
|
||||
BoardMember bm = boardService.electBoardMember(clubId, positionId, memberId,
|
||||
electedAt, termStart, termEnd, assemblyId, userId);
|
||||
return ResponseEntity.ok(bm);
|
||||
}
|
||||
|
||||
@GetMapping("/board")
|
||||
public ResponseEntity<List<BoardMember>> getCurrentBoard(@RequestParam UUID clubId) {
|
||||
return ResponseEntity.ok(boardService.getCurrentBoard(clubId));
|
||||
}
|
||||
|
||||
@GetMapping("/board/history")
|
||||
public ResponseEntity<List<BoardMember>> getBoardHistory(@RequestParam UUID clubId) {
|
||||
return ResponseEntity.ok(boardService.getBoardHistory(clubId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/board/members/{id}")
|
||||
public ResponseEntity<Void> removeBoardMember(
|
||||
@PathVariable UUID id,
|
||||
@RequestParam UUID clubId,
|
||||
Principal principal) {
|
||||
UUID userId = UUID.fromString(principal.getName());
|
||||
boardService.removeBoardMember(id, userId, clubId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// Portal endpoint
|
||||
@GetMapping("/portal/board")
|
||||
public ResponseEntity<List<BoardMember>> getPortalBoard(@RequestParam UUID clubId) {
|
||||
return ResponseEntity.ok(boardService.getCurrentBoard(clubId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.cannamanage.api.controller;
|
||||
|
||||
import de.cannamanage.domain.entity.Document;
|
||||
import de.cannamanage.domain.enums.DocumentAccessLevel;
|
||||
import de.cannamanage.domain.enums.DocumentCategory;
|
||||
import de.cannamanage.service.DocumentService;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
public class DocumentController {
|
||||
|
||||
private final DocumentService documentService;
|
||||
|
||||
public DocumentController(DocumentService documentService) {
|
||||
this.documentService = documentService;
|
||||
}
|
||||
|
||||
@PostMapping("/documents/upload")
|
||||
public ResponseEntity<Document> uploadDocument(
|
||||
@RequestParam UUID clubId,
|
||||
@RequestParam String title,
|
||||
@RequestParam DocumentCategory category,
|
||||
@RequestParam(defaultValue = "ALL_MEMBERS") DocumentAccessLevel accessLevel,
|
||||
@RequestParam(required = false) String description,
|
||||
@RequestParam("file") MultipartFile file,
|
||||
Principal principal) throws IOException {
|
||||
UUID userId = UUID.fromString(principal.getName());
|
||||
Document doc = documentService.uploadDocument(clubId, title, category, accessLevel, description, file, userId);
|
||||
return ResponseEntity.ok(doc);
|
||||
}
|
||||
|
||||
@GetMapping("/documents")
|
||||
public ResponseEntity<List<Document>> listDocuments(
|
||||
@RequestParam UUID clubId,
|
||||
@RequestParam(required = false) DocumentCategory category,
|
||||
@RequestParam(required = false) DocumentAccessLevel accessLevel) {
|
||||
List<Document> docs = documentService.listDocuments(clubId, category, accessLevel);
|
||||
return ResponseEntity.ok(docs);
|
||||
}
|
||||
|
||||
@GetMapping("/documents/{id}/download")
|
||||
public ResponseEntity<byte[]> downloadDocument(@PathVariable UUID id) throws IOException {
|
||||
Document doc = documentService.getDocument(id);
|
||||
byte[] content = documentService.downloadDocument(id);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + doc.getFilename() + "\"")
|
||||
.contentType(MediaType.parseMediaType(doc.getContentType()))
|
||||
.body(content);
|
||||
}
|
||||
|
||||
@DeleteMapping("/documents/{id}")
|
||||
public ResponseEntity<Void> deleteDocument(
|
||||
@PathVariable UUID id,
|
||||
@RequestParam UUID clubId,
|
||||
Principal principal) throws IOException {
|
||||
UUID userId = UUID.fromString(principal.getName());
|
||||
documentService.deleteDocument(id, userId, clubId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@GetMapping("/documents/usage")
|
||||
public ResponseEntity<Map<String, Long>> getStorageUsage(@RequestParam UUID clubId) {
|
||||
long usage = documentService.getStorageUsage(clubId);
|
||||
return ResponseEntity.ok(Map.of("bytesUsed", usage));
|
||||
}
|
||||
|
||||
// Portal endpoint — only ALL_MEMBERS documents
|
||||
@GetMapping("/portal/documents")
|
||||
public ResponseEntity<List<Document>> getPortalDocuments(@RequestParam UUID clubId) {
|
||||
List<Document> docs = documentService.listDocuments(clubId, null, DocumentAccessLevel.ALL_MEMBERS);
|
||||
return ResponseEntity.ok(docs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
-- V20: Document archive for club documents (Satzung, Protokolle, Verträge, etc.)
|
||||
-- Legal basis: §22 KCanG (Dokumentationspflichten), §147 AO (Aufbewahrungspflichten)
|
||||
|
||||
CREATE TABLE documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
club_id UUID NOT NULL REFERENCES clubs(id),
|
||||
title VARCHAR(300) NOT NULL,
|
||||
category VARCHAR(50) NOT NULL,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
content_type VARCHAR(100) NOT NULL,
|
||||
file_size BIGINT NOT NULL,
|
||||
storage_path VARCHAR(500) NOT NULL,
|
||||
access_level VARCHAR(20) NOT NULL DEFAULT 'ALL_MEMBERS',
|
||||
description TEXT,
|
||||
uploaded_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_documents_club ON documents(club_id);
|
||||
CREATE INDEX idx_documents_category ON documents(club_id, category);
|
||||
CREATE INDEX idx_documents_tenant ON documents(tenant_id);
|
||||
@@ -0,0 +1,33 @@
|
||||
-- V21: Board management (Vorstandsverwaltung)
|
||||
-- Legal basis: §26 BGB (Vorstand), §27 BGB (Bestellung/Abberufung), §23 KCanG (Präventionsbeauftragter)
|
||||
|
||||
CREATE TABLE board_positions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
club_id UUID NOT NULL REFERENCES clubs(id),
|
||||
title VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE board_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
club_id UUID NOT NULL REFERENCES clubs(id),
|
||||
position_id UUID NOT NULL REFERENCES board_positions(id),
|
||||
member_id UUID NOT NULL REFERENCES members(id),
|
||||
elected_at DATE NOT NULL,
|
||||
term_start DATE NOT NULL,
|
||||
term_end DATE,
|
||||
is_current BOOLEAN DEFAULT TRUE,
|
||||
elected_in_assembly_id UUID REFERENCES assemblies(id),
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_board_positions_club ON board_positions(club_id);
|
||||
CREATE INDEX idx_board_positions_tenant ON board_positions(tenant_id);
|
||||
CREATE INDEX idx_board_members_club ON board_members(club_id);
|
||||
CREATE INDEX idx_board_members_tenant ON board_members(tenant_id);
|
||||
CREATE INDEX idx_board_members_current ON board_members(club_id, is_current) WHERE is_current = TRUE;
|
||||
Reference in New Issue
Block a user