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,69 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Board member assignment — links a member to a board position for a term.
|
||||
* Legal basis: §27 BGB (Bestellung/Abberufung des Vorstands).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "board_members", indexes = {
|
||||
@Index(name = "idx_board_members_club", columnList = "club_id"),
|
||||
@Index(name = "idx_board_members_tenant", columnList = "tenant_id")
|
||||
})
|
||||
public class BoardMember extends AbstractTenantEntity {
|
||||
|
||||
@Column(name = "club_id", nullable = false)
|
||||
private UUID clubId;
|
||||
|
||||
@Column(name = "position_id", nullable = false)
|
||||
private UUID positionId;
|
||||
|
||||
@Column(name = "member_id", nullable = false)
|
||||
private UUID memberId;
|
||||
|
||||
@Column(name = "elected_at", nullable = false)
|
||||
private LocalDate electedAt;
|
||||
|
||||
@Column(name = "term_start", nullable = false)
|
||||
private LocalDate termStart;
|
||||
|
||||
@Column(name = "term_end")
|
||||
private LocalDate termEnd;
|
||||
|
||||
@Column(name = "is_current", nullable = false)
|
||||
private Boolean isCurrent = true;
|
||||
|
||||
@Column(name = "elected_in_assembly_id")
|
||||
private UUID electedInAssemblyId;
|
||||
|
||||
// Getters and setters
|
||||
|
||||
public UUID getClubId() { return clubId; }
|
||||
public void setClubId(UUID clubId) { this.clubId = clubId; }
|
||||
|
||||
public UUID getPositionId() { return positionId; }
|
||||
public void setPositionId(UUID positionId) { this.positionId = positionId; }
|
||||
|
||||
public UUID getMemberId() { return memberId; }
|
||||
public void setMemberId(UUID memberId) { this.memberId = memberId; }
|
||||
|
||||
public LocalDate getElectedAt() { return electedAt; }
|
||||
public void setElectedAt(LocalDate electedAt) { this.electedAt = electedAt; }
|
||||
|
||||
public LocalDate getTermStart() { return termStart; }
|
||||
public void setTermStart(LocalDate termStart) { this.termStart = termStart; }
|
||||
|
||||
public LocalDate getTermEnd() { return termEnd; }
|
||||
public void setTermEnd(LocalDate termEnd) { this.termEnd = termEnd; }
|
||||
|
||||
public Boolean getIsCurrent() { return isCurrent; }
|
||||
public void setIsCurrent(Boolean isCurrent) { this.isCurrent = isCurrent; }
|
||||
|
||||
public UUID getElectedInAssemblyId() { return electedInAssemblyId; }
|
||||
public void setElectedInAssemblyId(UUID electedInAssemblyId) { this.electedInAssemblyId = electedInAssemblyId; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Board position definition (e.g., "1. Vorsitzender", "Kassenwart", "Präventionsbeauftragter").
|
||||
* Legal basis: §26 BGB (Vorstand), §30 BGB (Besonderer Vertreter), §23 KCanG (Präventionsbeauftragter).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "board_positions", indexes = {
|
||||
@Index(name = "idx_board_positions_club", columnList = "club_id"),
|
||||
@Index(name = "idx_board_positions_tenant", columnList = "tenant_id")
|
||||
})
|
||||
public class BoardPosition extends AbstractTenantEntity {
|
||||
|
||||
@Column(name = "club_id", nullable = false)
|
||||
private UUID clubId;
|
||||
|
||||
@Column(name = "title", nullable = false, length = 100)
|
||||
private String title;
|
||||
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Boolean isActive = true;
|
||||
|
||||
// Getters and setters
|
||||
|
||||
public UUID getClubId() { return clubId; }
|
||||
public void setClubId(UUID clubId) { this.clubId = clubId; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public Integer getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||
|
||||
public Boolean getIsActive() { return isActive; }
|
||||
public void setIsActive(Boolean isActive) { this.isActive = isActive; }
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import de.cannamanage.domain.enums.DocumentAccessLevel;
|
||||
import de.cannamanage.domain.enums.DocumentCategory;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Club document entity for the document archive.
|
||||
* Legal basis: §22 KCanG (documentation requirements), §147 AO (retention).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "documents", indexes = {
|
||||
@Index(name = "idx_documents_club", columnList = "club_id"),
|
||||
@Index(name = "idx_documents_tenant", columnList = "tenant_id"),
|
||||
@Index(name = "idx_documents_category", columnList = "club_id, category")
|
||||
})
|
||||
public class Document extends AbstractTenantEntity {
|
||||
|
||||
@Column(name = "club_id", nullable = false)
|
||||
private UUID clubId;
|
||||
|
||||
@Column(name = "title", nullable = false, length = 300)
|
||||
private String title;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "category", nullable = false, length = 50)
|
||||
private DocumentCategory category;
|
||||
|
||||
@Column(name = "filename", nullable = false, length = 255)
|
||||
private String filename;
|
||||
|
||||
@Column(name = "content_type", nullable = false, length = 100)
|
||||
private String contentType;
|
||||
|
||||
@Column(name = "file_size", nullable = false)
|
||||
private Long fileSize;
|
||||
|
||||
@Column(name = "storage_path", nullable = false, length = 500)
|
||||
private String storagePath;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "access_level", nullable = false, length = 20)
|
||||
private DocumentAccessLevel accessLevel = DocumentAccessLevel.ALL_MEMBERS;
|
||||
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(name = "uploaded_by", nullable = false)
|
||||
private UUID uploadedBy;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private Instant updatedAt;
|
||||
|
||||
@PreUpdate
|
||||
void onUpdate() {
|
||||
this.updatedAt = Instant.now();
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
|
||||
public UUID getClubId() { return clubId; }
|
||||
public void setClubId(UUID clubId) { this.clubId = clubId; }
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public DocumentCategory getCategory() { return category; }
|
||||
public void setCategory(DocumentCategory category) { this.category = category; }
|
||||
|
||||
public String getFilename() { return filename; }
|
||||
public void setFilename(String filename) { this.filename = filename; }
|
||||
|
||||
public String getContentType() { return contentType; }
|
||||
public void setContentType(String contentType) { this.contentType = contentType; }
|
||||
|
||||
public Long getFileSize() { return fileSize; }
|
||||
public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
|
||||
|
||||
public String getStoragePath() { return storagePath; }
|
||||
public void setStoragePath(String storagePath) { this.storagePath = storagePath; }
|
||||
|
||||
public DocumentAccessLevel getAccessLevel() { return accessLevel; }
|
||||
public void setAccessLevel(DocumentAccessLevel accessLevel) { this.accessLevel = accessLevel; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public UUID getUploadedBy() { return uploadedBy; }
|
||||
public void setUploadedBy(UUID uploadedBy) { this.uploadedBy = uploadedBy; }
|
||||
|
||||
public Instant getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
@@ -78,5 +78,13 @@ public enum AuditEventType {
|
||||
ASSEMBLY_INVITED,
|
||||
ASSEMBLY_STARTED,
|
||||
ASSEMBLY_COMPLETED,
|
||||
ASSEMBLY_VOTE_RECORDED
|
||||
ASSEMBLY_VOTE_RECORDED,
|
||||
|
||||
// Sprint 8 — Document events
|
||||
DOCUMENT_UPLOADED,
|
||||
DOCUMENT_DELETED,
|
||||
|
||||
// Sprint 8 — Board events
|
||||
BOARD_MEMBER_ELECTED,
|
||||
BOARD_MEMBER_REMOVED
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.cannamanage.domain.enums;
|
||||
|
||||
/**
|
||||
* Access levels for documents in the document archive.
|
||||
* Controls visibility: ALL_MEMBERS = everyone, BOARD_ONLY = Vorstand only.
|
||||
*/
|
||||
public enum DocumentAccessLevel {
|
||||
ALL_MEMBERS,
|
||||
BOARD_ONLY
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.cannamanage.domain.enums;
|
||||
|
||||
/**
|
||||
* Categories for club documents in the document archive.
|
||||
* Based on typical Vereinsverwaltung document types.
|
||||
*/
|
||||
public enum DocumentCategory {
|
||||
SATZUNG,
|
||||
PROTOKOLL,
|
||||
VERTRAG,
|
||||
VERSICHERUNG,
|
||||
GENEHMIGUNG,
|
||||
SONSTIGES
|
||||
}
|
||||
@@ -23,5 +23,7 @@ public enum NotificationType {
|
||||
PAYMENT_RECEIVED,
|
||||
// Sprint 8 — Assembly:
|
||||
ASSEMBLY_INVITATION,
|
||||
ASSEMBLY_REMINDER
|
||||
ASSEMBLY_REMINDER,
|
||||
// Sprint 8 — Board:
|
||||
BOARD_TERM_EXPIRING
|
||||
}
|
||||
|
||||
@@ -21,5 +21,6 @@ public enum StaffPermission {
|
||||
// Sprint 8:
|
||||
MANAGE_FINANCES,
|
||||
VIEW_FINANCES,
|
||||
MANAGE_ASSEMBLIES
|
||||
MANAGE_ASSEMBLIES,
|
||||
MANAGE_DOCUMENTS
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user