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
@@ -0,0 +1,232 @@
package de.cannamanage.service;
import com.lowagie.text.*;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfWriter;
import de.cannamanage.domain.entity.*;
import de.cannamanage.domain.enums.VoteResult;
import de.cannamanage.service.repository.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
/**
* Generates meeting protocol (Protokoll) PDF for general assemblies.
* Legal basis: §67 BGB (MV-Protokolle for Vereinsregister).
* Retention: 10 years per §147 AO.
*/
@Service
public class AssemblyProtocolService {
private static final Logger log = LoggerFactory.getLogger(AssemblyProtocolService.class);
private static final Font TITLE_FONT = new Font(Font.HELVETICA, 16, Font.BOLD);
private static final Font SUBTITLE_FONT = new Font(Font.HELVETICA, 12, Font.BOLD);
private static final Font HEADER_FONT = new Font(Font.HELVETICA, 11, Font.BOLD);
private static final Font NORMAL_FONT = new Font(Font.HELVETICA, 10, Font.NORMAL);
private static final Font SMALL_FONT = new Font(Font.HELVETICA, 9, Font.NORMAL, Color.GRAY);
private static final Font RESULT_ACCEPTED = new Font(Font.HELVETICA, 10, Font.BOLD, new Color(0, 128, 0));
private static final Font RESULT_REJECTED = new Font(Font.HELVETICA, 10, Font.BOLD, Color.RED);
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMAN);
private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm", Locale.GERMAN);
private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm", Locale.GERMAN);
private final AssemblyRepository assemblyRepository;
private final AssemblyAgendaItemRepository agendaItemRepository;
private final AssemblyAttendeeRepository attendeeRepository;
private final AssemblyVoteRepository voteRepository;
private final MemberRepository memberRepository;
private final ClubRepository clubRepository;
public AssemblyProtocolService(AssemblyRepository assemblyRepository,
AssemblyAgendaItemRepository agendaItemRepository,
AssemblyAttendeeRepository attendeeRepository,
AssemblyVoteRepository voteRepository,
MemberRepository memberRepository,
ClubRepository clubRepository) {
this.assemblyRepository = assemblyRepository;
this.agendaItemRepository = agendaItemRepository;
this.attendeeRepository = attendeeRepository;
this.voteRepository = voteRepository;
this.memberRepository = memberRepository;
this.clubRepository = clubRepository;
}
public byte[] generateProtocol(UUID assemblyId) {
var assembly = assemblyRepository.findById(assemblyId)
.orElseThrow(() -> new IllegalArgumentException("Assembly not found: " + assemblyId));
var club = clubRepository.findById(assembly.getClubId())
.orElseThrow(() -> new IllegalArgumentException("Club not found"));
var agendaItems = agendaItemRepository.findByAssemblyIdOrderByPosition(assemblyId);
var attendees = attendeeRepository.findByAssemblyId(assemblyId);
var votes = voteRepository.findByAssemblyId(assemblyId);
long totalMembers = memberRepository.countByTenantIdAndStatus(assembly.getTenantId(),
de.cannamanage.domain.enums.MemberStatus.ACTIVE);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
try {
PdfWriter.getInstance(document, baos);
document.open();
// Title
String typeLabel = assembly.getAssemblyType() == de.cannamanage.domain.enums.AssemblyType.ORDINARY
? "ordentlichen" : "außerordentlichen";
Paragraph title = new Paragraph("Protokoll der " + typeLabel + " Mitgliederversammlung", TITLE_FONT);
title.setAlignment(Element.ALIGN_CENTER);
title.setSpacingAfter(10);
document.add(title);
// Club name
Paragraph clubParagraph = new Paragraph(club.getName(), SUBTITLE_FONT);
clubParagraph.setAlignment(Element.ALIGN_CENTER);
clubParagraph.setSpacingAfter(20);
document.add(clubParagraph);
// Meeting details table
PdfPTable detailsTable = new PdfPTable(2);
detailsTable.setWidthPercentage(80);
detailsTable.setWidths(new float[]{30, 70});
detailsTable.setSpacingAfter(15);
addDetailRow(detailsTable, "Datum:", formatDate(assembly.getScheduledAt()));
addDetailRow(detailsTable, "Uhrzeit:", formatTime(assembly.getOpenedAt()) + " " +
(assembly.getClosedAt() != null ? formatTime(assembly.getClosedAt()) : ""));
addDetailRow(detailsTable, "Ort:", assembly.getLocation() != null ? assembly.getLocation() : "");
addDetailRow(detailsTable, "Anwesend:", attendees.size() + " von " + totalMembers + " Mitgliedern");
addDetailRow(detailsTable, "Beschlussfähig:",
assembly.getQuorumRequired() != null && attendees.size() >= assembly.getQuorumRequired()
? "Ja" : "Nein");
document.add(detailsTable);
// Separator
document.add(new Paragraph(" "));
// Agenda (Tagesordnung)
Paragraph agendaTitle = new Paragraph("Tagesordnung", SUBTITLE_FONT);
agendaTitle.setSpacingAfter(8);
document.add(agendaTitle);
for (var item : agendaItems) {
Paragraph agendaLine = new Paragraph(
"TOP " + item.getPosition() + ": " + item.getTitle(), HEADER_FONT);
agendaLine.setSpacingAfter(3);
document.add(agendaLine);
if (item.getDescription() != null && !item.getDescription().isBlank()) {
Paragraph desc = new Paragraph(item.getDescription(), NORMAL_FONT);
desc.setIndentationLeft(20);
desc.setSpacingAfter(5);
document.add(desc);
}
// Votes for this agenda item
var itemVotes = votes.stream()
.filter(v -> v.getAgendaItemId().equals(item.getId()))
.toList();
for (var vote : itemVotes) {
addVoteResult(document, vote);
}
document.add(new Paragraph(" "));
}
// Signatures
document.add(new Paragraph(" "));
document.add(new Paragraph(" "));
Paragraph sigTitle = new Paragraph("Unterschriften", SUBTITLE_FONT);
sigTitle.setSpacingAfter(30);
document.add(sigTitle);
PdfPTable sigTable = new PdfPTable(2);
sigTable.setWidthPercentage(80);
sigTable.setSpacingBefore(20);
addSignatureLine(sigTable, "Versammlungsleiter/in");
addSignatureLine(sigTable, "Protokollführer/in");
document.add(sigTable);
// Footer
document.add(new Paragraph(" "));
Paragraph footer = new Paragraph(
"Erstellt am " + DATE_FMT.format(java.time.LocalDate.now()) +
", Aufbewahrungspflicht gemäß §147 AO (10 Jahre)", SMALL_FONT);
footer.setAlignment(Element.ALIGN_CENTER);
footer.setSpacingBefore(30);
document.add(footer);
document.close();
} catch (Exception e) {
log.error("Error generating assembly protocol PDF for {}", assemblyId, e);
throw new RuntimeException("Failed to generate protocol PDF", e);
}
log.info("Protocol PDF generated for assembly: {}", assembly.getTitle());
return baos.toByteArray();
}
private void addDetailRow(PdfPTable table, String label, String value) {
PdfPCell labelCell = new PdfPCell(new Phrase(label, HEADER_FONT));
labelCell.setBorder(0);
labelCell.setPaddingBottom(4);
table.addCell(labelCell);
PdfPCell valueCell = new PdfPCell(new Phrase(value, NORMAL_FONT));
valueCell.setBorder(0);
valueCell.setPaddingBottom(4);
table.addCell(valueCell);
}
private void addVoteResult(Document document, AssemblyVote vote) throws DocumentException {
Paragraph voteParagraph = new Paragraph();
voteParagraph.setIndentationLeft(20);
voteParagraph.setSpacingAfter(5);
voteParagraph.add(new Chunk("Abstimmung: ", HEADER_FONT));
voteParagraph.add(new Chunk(vote.getTitle(), NORMAL_FONT));
document.add(voteParagraph);
String resultText = String.format("Ergebnis: %d Ja, %d Nein, %d Enthaltung → ",
vote.getYesCount(), vote.getNoCount(), vote.getAbstainCount());
Paragraph resultParagraph = new Paragraph();
resultParagraph.setIndentationLeft(30);
resultParagraph.add(new Chunk(resultText, NORMAL_FONT));
if (vote.getResult() == VoteResult.ACCEPTED) {
resultParagraph.add(new Chunk("Angenommen", RESULT_ACCEPTED));
} else {
resultParagraph.add(new Chunk("Abgelehnt", RESULT_REJECTED));
}
document.add(resultParagraph);
}
private void addSignatureLine(PdfPTable table, String role) {
PdfPCell cell = new PdfPCell();
cell.setBorder(0);
cell.setPaddingTop(30);
Paragraph p = new Paragraph();
p.add(new Chunk("_______________________________\n", NORMAL_FONT));
p.add(new Chunk(role, SMALL_FONT));
cell.addElement(p);
table.addCell(cell);
}
private String formatDate(java.time.Instant instant) {
if (instant == null) return "";
return DATE_FMT.format(instant.atZone(ZoneId.of("Europe/Berlin")).toLocalDate());
}
private String formatTime(java.time.Instant instant) {
if (instant == null) return "";
return TIME_FMT.format(instant.atZone(ZoneId.of("Europe/Berlin")).toLocalTime());
}
}
@@ -0,0 +1,332 @@
package de.cannamanage.service;
import de.cannamanage.domain.entity.*;
import de.cannamanage.domain.enums.*;
import de.cannamanage.service.repository.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.*;
/**
* Service for general assembly (Mitgliederversammlung) management.
* Handles the full lifecycle: create → invite → start → vote → complete.
* Legal basis: §32 BGB (MV as decision-making organ), §33 BGB (vote majorities).
*/
@Service
@Transactional
public class AssemblyService {
private static final Logger log = LoggerFactory.getLogger(AssemblyService.class);
private final AssemblyRepository assemblyRepository;
private final AssemblyAgendaItemRepository agendaItemRepository;
private final AssemblyAttendeeRepository attendeeRepository;
private final AssemblyVoteRepository voteRepository;
private final AssemblyVoteRecordRepository voteRecordRepository;
private final MemberRepository memberRepository;
private final NotificationService notificationService;
private final AuditService auditService;
public AssemblyService(AssemblyRepository assemblyRepository,
AssemblyAgendaItemRepository agendaItemRepository,
AssemblyAttendeeRepository attendeeRepository,
AssemblyVoteRepository voteRepository,
AssemblyVoteRecordRepository voteRecordRepository,
MemberRepository memberRepository,
NotificationService notificationService,
AuditService auditService) {
this.assemblyRepository = assemblyRepository;
this.agendaItemRepository = agendaItemRepository;
this.attendeeRepository = attendeeRepository;
this.voteRepository = voteRepository;
this.voteRecordRepository = voteRecordRepository;
this.memberRepository = memberRepository;
this.notificationService = notificationService;
this.auditService = auditService;
}
// === Assembly CRUD ===
public Assembly createAssembly(UUID clubId, String title, AssemblyType type, Instant scheduledAt,
String location, Integer quorumRequired, UUID createdBy,
List<AgendaItemInput> agendaItems) {
var assembly = new Assembly();
assembly.setClubId(clubId);
assembly.setTitle(title);
assembly.setAssemblyType(type);
assembly.setScheduledAt(scheduledAt);
assembly.setLocation(location);
assembly.setQuorumRequired(quorumRequired);
assembly.setCreatedBy(createdBy);
assembly.setStatus(AssemblyStatus.PLANNED);
assembly.setUpdatedAt(Instant.now());
assembly = assemblyRepository.save(assembly);
if (agendaItems != null) {
for (int i = 0; i < agendaItems.size(); i++) {
var input = agendaItems.get(i);
var item = new AssemblyAgendaItem();
item.setAssemblyId(assembly.getId());
item.setPosition(i + 1);
item.setTitle(input.title());
item.setDescription(input.description());
item.setItemType(input.itemType());
agendaItemRepository.save(item);
}
}
auditService.log(AuditEventType.ASSEMBLY_CREATED, createdBy, assembly.getId().toString(),
"Mitgliederversammlung erstellt: " + title);
log.info("Assembly created: {} ({})", title, type);
return assembly;
}
public Assembly updateAssembly(UUID assemblyId, String title, Instant scheduledAt,
String location, Integer quorumRequired) {
var assembly = assemblyRepository.findById(assemblyId)
.orElseThrow(() -> new IllegalArgumentException("Assembly not found: " + assemblyId));
if (assembly.getStatus() != AssemblyStatus.PLANNED) {
throw new IllegalStateException("Cannot update assembly in status: " + assembly.getStatus());
}
if (title != null) assembly.setTitle(title);
if (scheduledAt != null) assembly.setScheduledAt(scheduledAt);
if (location != null) assembly.setLocation(location);
if (quorumRequired != null) assembly.setQuorumRequired(quorumRequired);
assembly.setUpdatedAt(Instant.now());
return assemblyRepository.save(assembly);
}
// === Invitations ===
public Assembly sendInvitations(UUID assemblyId, UUID sentBy) {
var assembly = assemblyRepository.findById(assemblyId)
.orElseThrow(() -> new IllegalArgumentException("Assembly not found: " + assemblyId));
if (assembly.getStatus() != AssemblyStatus.PLANNED) {
throw new IllegalStateException("Invitations can only be sent for PLANNED assemblies");
}
// Send notification to all active members
notificationService.sendToAllMembers(assembly.getClubId(),
NotificationType.ASSEMBLY_INVITATION,
"Einladung zur Mitgliederversammlung: " + assembly.getTitle(),
"Am " + assembly.getScheduledAt() + " findet eine Mitgliederversammlung statt. Ort: " + assembly.getLocation());
assembly.setInvitationSentAt(Instant.now());
assembly.setStatus(AssemblyStatus.INVITED);
assembly.setUpdatedAt(Instant.now());
auditService.log(AuditEventType.ASSEMBLY_INVITED, sentBy, assembly.getId().toString(),
"Einladungen versendet für: " + assembly.getTitle());
log.info("Invitations sent for assembly: {}", assembly.getTitle());
return assemblyRepository.save(assembly);
}
// === Assembly Lifecycle ===
public Assembly cancelAssembly(UUID assemblyId, UUID cancelledBy) {
var assembly = assemblyRepository.findById(assemblyId)
.orElseThrow(() -> new IllegalArgumentException("Assembly not found: " + assemblyId));
assembly.setStatus(AssemblyStatus.CANCELLED);
assembly.setUpdatedAt(Instant.now());
if (assembly.getInvitationSentAt() != null) {
notificationService.sendToAllMembers(assembly.getClubId(),
NotificationType.ASSEMBLY_REMINDER,
"Mitgliederversammlung abgesagt: " + assembly.getTitle(),
"Die geplante Mitgliederversammlung wurde abgesagt.");
}
return assemblyRepository.save(assembly);
}
public Assembly startAssembly(UUID assemblyId, UUID startedBy) {
var assembly = assemblyRepository.findById(assemblyId)
.orElseThrow(() -> new IllegalArgumentException("Assembly not found: " + assemblyId));
if (assembly.getStatus() != AssemblyStatus.INVITED && assembly.getStatus() != AssemblyStatus.PLANNED) {
throw new IllegalStateException("Cannot start assembly in status: " + assembly.getStatus());
}
assembly.setStatus(AssemblyStatus.IN_PROGRESS);
assembly.setOpenedAt(Instant.now());
assembly.setUpdatedAt(Instant.now());
auditService.log(AuditEventType.ASSEMBLY_STARTED, startedBy, assembly.getId().toString(),
"Mitgliederversammlung eröffnet: " + assembly.getTitle());
log.info("Assembly started: {}", assembly.getTitle());
return assemblyRepository.save(assembly);
}
public Assembly completeAssembly(UUID assemblyId, UUID completedBy) {
var assembly = assemblyRepository.findById(assemblyId)
.orElseThrow(() -> new IllegalArgumentException("Assembly not found: " + assemblyId));
if (assembly.getStatus() != AssemblyStatus.IN_PROGRESS) {
throw new IllegalStateException("Cannot complete assembly in status: " + assembly.getStatus());
}
assembly.setStatus(AssemblyStatus.COMPLETED);
assembly.setClosedAt(Instant.now());
assembly.setUpdatedAt(Instant.now());
auditService.log(AuditEventType.ASSEMBLY_COMPLETED, completedBy, assembly.getId().toString(),
"Mitgliederversammlung geschlossen: " + assembly.getTitle());
log.info("Assembly completed: {}", assembly.getTitle());
return assemblyRepository.save(assembly);
}
// === Attendance ===
public AssemblyAttendee checkInAttendee(UUID assemblyId, UUID memberId, UUID proxyForMemberId) {
if (attendeeRepository.existsByAssemblyIdAndMemberId(assemblyId, memberId)) {
throw new IllegalStateException("Member already checked in");
}
var attendee = new AssemblyAttendee();
attendee.setAssemblyId(assemblyId);
attendee.setMemberId(memberId);
attendee.setCheckedInAt(Instant.now());
attendee.setProxyForMemberId(proxyForMemberId);
return attendeeRepository.save(attendee);
}
public QuorumInfo calculateQuorum(UUID assemblyId) {
var assembly = assemblyRepository.findById(assemblyId)
.orElseThrow(() -> new IllegalArgumentException("Assembly not found: " + assemblyId));
long attendeeCount = attendeeRepository.countByAssemblyId(assemblyId);
long totalMembers = memberRepository.countByTenantIdAndStatus(
assembly.getTenantId(), MemberStatus.ACTIVE);
int required = assembly.getQuorumRequired() != null ? assembly.getQuorumRequired() : (int)(totalMembers / 2) + 1;
boolean quorumMet = attendeeCount >= required;
return new QuorumInfo(attendeeCount, totalMembers, required, quorumMet);
}
// === Voting ===
public AssemblyVote createVote(UUID assemblyId, UUID agendaItemId, String title,
String description, VoteType voteType) {
var vote = new AssemblyVote();
vote.setAssemblyId(assemblyId);
vote.setAgendaItemId(agendaItemId);
vote.setTitle(title);
vote.setDescription(description);
vote.setVoteType(voteType);
return voteRepository.save(vote);
}
public AssemblyVote castVote(UUID voteId, UUID memberId, VoteDecision decision, UUID actingUser) {
if (voteRecordRepository.existsByVoteIdAndMemberId(voteId, memberId)) {
throw new IllegalStateException("Member has already voted");
}
var vote = voteRepository.findById(voteId)
.orElseThrow(() -> new IllegalArgumentException("Vote not found: " + voteId));
if (vote.getResult() != null) {
throw new IllegalStateException("Vote is already closed");
}
var record = new AssemblyVoteRecord();
record.setVoteId(voteId);
record.setMemberId(memberId);
record.setDecision(decision);
record.setVotedAt(Instant.now());
voteRecordRepository.save(record);
// Update counts
switch (decision) {
case YES -> vote.setYesCount(vote.getYesCount() + 1);
case NO -> vote.setNoCount(vote.getNoCount() + 1);
case ABSTAIN -> vote.setAbstainCount(vote.getAbstainCount() + 1);
}
auditService.log(AuditEventType.ASSEMBLY_VOTE_RECORDED, actingUser, voteId.toString(),
"Stimme abgegeben: " + decision + " für " + vote.getTitle());
return voteRepository.save(vote);
}
public AssemblyVote closeVote(UUID voteId) {
var vote = voteRepository.findById(voteId)
.orElseThrow(() -> new IllegalArgumentException("Vote not found: " + voteId));
if (vote.getResult() != null) {
throw new IllegalStateException("Vote is already closed");
}
int totalVotes = vote.getYesCount() + vote.getNoCount(); // abstentions don't count toward majority
VoteResult result = calculateResult(vote.getVoteType(), vote.getYesCount(), totalVotes);
vote.setResult(result);
vote.setVotedAt(Instant.now());
log.info("Vote closed: {} — Result: {} (Yes: {}, No: {}, Abstain: {})",
vote.getTitle(), result, vote.getYesCount(), vote.getNoCount(), vote.getAbstainCount());
return voteRepository.save(vote);
}
private VoteResult calculateResult(VoteType voteType, int yesCount, int totalVotes) {
if (totalVotes == 0) return VoteResult.REJECTED;
double ratio = (double) yesCount / totalVotes;
return switch (voteType) {
case SIMPLE_MAJORITY -> ratio > 0.5 ? VoteResult.ACCEPTED : VoteResult.REJECTED;
case TWO_THIRDS -> ratio >= (2.0 / 3.0) ? VoteResult.ACCEPTED : VoteResult.REJECTED;
case THREE_QUARTERS -> ratio >= 0.75 ? VoteResult.ACCEPTED : VoteResult.REJECTED;
case UNANIMOUS -> yesCount == totalVotes ? VoteResult.ACCEPTED : VoteResult.REJECTED;
};
}
// === Queries ===
public List<Assembly> getAssemblies(UUID clubId) {
return assemblyRepository.findByClubIdOrderByScheduledAtDesc(clubId);
}
public Assembly getAssemblyDetail(UUID assemblyId) {
return assemblyRepository.findById(assemblyId)
.orElseThrow(() -> new IllegalArgumentException("Assembly not found: " + assemblyId));
}
public List<AssemblyAgendaItem> getAgendaItems(UUID assemblyId) {
return agendaItemRepository.findByAssemblyIdOrderByPosition(assemblyId);
}
public List<AssemblyAttendee> getAttendees(UUID assemblyId) {
return attendeeRepository.findByAssemblyId(assemblyId);
}
public List<AssemblyVote> getVotes(UUID assemblyId) {
return voteRepository.findByAssemblyId(assemblyId);
}
public List<AssemblyVoteRecord> getVoteRecords(UUID voteId) {
return voteRecordRepository.findByVoteId(voteId);
}
public List<Assembly> getUpcomingAssemblies(UUID tenantId) {
return assemblyRepository.findByTenantIdOrderByScheduledAtDesc(tenantId).stream()
.filter(a -> a.getStatus() == AssemblyStatus.PLANNED || a.getStatus() == AssemblyStatus.INVITED)
.toList();
}
// === DTOs ===
public record AgendaItemInput(String title, String description, AgendaItemType itemType) {}
public record QuorumInfo(long attendees, long totalMembers, int required, boolean quorumMet) {}
}
@@ -0,0 +1,14 @@
package de.cannamanage.service.repository;
import de.cannamanage.domain.entity.AssemblyAgendaItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
@Repository
public interface AssemblyAgendaItemRepository extends JpaRepository<AssemblyAgendaItem, UUID> {
List<AssemblyAgendaItem> findByAssemblyIdOrderByPosition(UUID assemblyId);
}
@@ -0,0 +1,18 @@
package de.cannamanage.service.repository;
import de.cannamanage.domain.entity.AssemblyAttendee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
@Repository
public interface AssemblyAttendeeRepository extends JpaRepository<AssemblyAttendee, UUID> {
List<AssemblyAttendee> findByAssemblyId(UUID assemblyId);
long countByAssemblyId(UUID assemblyId);
boolean existsByAssemblyIdAndMemberId(UUID assemblyId, UUID memberId);
}
@@ -0,0 +1,19 @@
package de.cannamanage.service.repository;
import de.cannamanage.domain.entity.Assembly;
import de.cannamanage.domain.enums.AssemblyStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
@Repository
public interface AssemblyRepository extends JpaRepository<Assembly, UUID> {
List<Assembly> findByClubIdOrderByScheduledAtDesc(UUID clubId);
List<Assembly> findByClubIdAndStatus(UUID clubId, AssemblyStatus status);
List<Assembly> findByTenantIdOrderByScheduledAtDesc(UUID tenantId);
}
@@ -0,0 +1,16 @@
package de.cannamanage.service.repository;
import de.cannamanage.domain.entity.AssemblyVoteRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
@Repository
public interface AssemblyVoteRecordRepository extends JpaRepository<AssemblyVoteRecord, UUID> {
List<AssemblyVoteRecord> findByVoteId(UUID voteId);
boolean existsByVoteIdAndMemberId(UUID voteId, UUID memberId);
}
@@ -0,0 +1,16 @@
package de.cannamanage.service.repository;
import de.cannamanage.domain.entity.AssemblyVote;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
@Repository
public interface AssemblyVoteRepository extends JpaRepository<AssemblyVote, UUID> {
List<AssemblyVote> findByAssemblyId(UUID assemblyId);
List<AssemblyVote> findByAgendaItemId(UUID agendaItemId);
}