|
|
|
@@ -0,0 +1,330 @@
|
|
|
|
|
package de.cannamanage.api.controller;
|
|
|
|
|
|
|
|
|
|
import de.cannamanage.api.security.StaffPermissionChecker;
|
|
|
|
|
import de.cannamanage.domain.entity.*;
|
|
|
|
|
import de.cannamanage.domain.enums.*;
|
|
|
|
|
import de.cannamanage.service.AssemblyProtocolService;
|
|
|
|
|
import de.cannamanage.service.AssemblyService;
|
|
|
|
|
import de.cannamanage.service.AssemblyService.AgendaItemInput;
|
|
|
|
|
import de.cannamanage.service.repository.MemberRepository;
|
|
|
|
|
import jakarta.validation.Valid;
|
|
|
|
|
import jakarta.validation.constraints.NotBlank;
|
|
|
|
|
import jakarta.validation.constraints.NotNull;
|
|
|
|
|
import org.springframework.http.HttpHeaders;
|
|
|
|
|
import org.springframework.http.MediaType;
|
|
|
|
|
import org.springframework.http.ResponseEntity;
|
|
|
|
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
|
|
|
|
import org.springframework.security.core.userdetails.UserDetails;
|
|
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
|
|
|
|
import java.time.Instant;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* REST controller for general assembly (Mitgliederversammlung) management.
|
|
|
|
|
* Admin endpoints require MANAGE_ASSEMBLIES permission.
|
|
|
|
|
* Portal endpoints allow members to view assemblies they're invited to.
|
|
|
|
|
*/
|
|
|
|
|
@RestController
|
|
|
|
|
@RequestMapping("/api/v1")
|
|
|
|
|
public class AssemblyController {
|
|
|
|
|
|
|
|
|
|
private final AssemblyService assemblyService;
|
|
|
|
|
private final AssemblyProtocolService protocolService;
|
|
|
|
|
private final StaffPermissionChecker permissionChecker;
|
|
|
|
|
private final MemberRepository memberRepository;
|
|
|
|
|
|
|
|
|
|
public AssemblyController(AssemblyService assemblyService,
|
|
|
|
|
AssemblyProtocolService protocolService,
|
|
|
|
|
StaffPermissionChecker permissionChecker,
|
|
|
|
|
MemberRepository memberRepository) {
|
|
|
|
|
this.assemblyService = assemblyService;
|
|
|
|
|
this.protocolService = protocolService;
|
|
|
|
|
this.permissionChecker = permissionChecker;
|
|
|
|
|
this.memberRepository = memberRepository;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// === Admin Endpoints ===
|
|
|
|
|
|
|
|
|
|
@PostMapping("/assemblies")
|
|
|
|
|
public ResponseEntity<AssemblyResponse> createAssembly(
|
|
|
|
|
@Valid @RequestBody CreateAssemblyRequest request,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
var userId = permissionChecker.getUserId(user);
|
|
|
|
|
var clubId = permissionChecker.getClubId(user);
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var agendaItems = request.agendaItems() != null
|
|
|
|
|
? request.agendaItems().stream()
|
|
|
|
|
.map(a -> new AgendaItemInput(a.title(), a.description(), a.itemType()))
|
|
|
|
|
.toList()
|
|
|
|
|
: List.<AgendaItemInput>of();
|
|
|
|
|
|
|
|
|
|
var assembly = assemblyService.createAssembly(clubId, request.title(), request.assemblyType(),
|
|
|
|
|
request.scheduledAt(), request.location(), request.quorumRequired(), userId, agendaItems);
|
|
|
|
|
|
|
|
|
|
return ResponseEntity.ok(toResponse(assembly));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping("/assemblies")
|
|
|
|
|
public ResponseEntity<List<AssemblyResponse>> listAssemblies(@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
var clubId = permissionChecker.getClubId(user);
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var assemblies = assemblyService.getAssemblies(clubId);
|
|
|
|
|
return ResponseEntity.ok(assemblies.stream().map(this::toResponse).toList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping("/assemblies/{id}")
|
|
|
|
|
public ResponseEntity<AssemblyDetailResponse> getAssemblyDetail(
|
|
|
|
|
@PathVariable UUID id,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var assembly = assemblyService.getAssemblyDetail(id);
|
|
|
|
|
var agendaItems = assemblyService.getAgendaItems(id);
|
|
|
|
|
var attendees = assemblyService.getAttendees(id);
|
|
|
|
|
var votes = assemblyService.getVotes(id);
|
|
|
|
|
var quorum = assemblyService.calculateQuorum(id);
|
|
|
|
|
|
|
|
|
|
return ResponseEntity.ok(new AssemblyDetailResponse(
|
|
|
|
|
toResponse(assembly),
|
|
|
|
|
agendaItems.stream().map(this::toAgendaResponse).toList(),
|
|
|
|
|
attendees.stream().map(this::toAttendeeResponse).toList(),
|
|
|
|
|
votes.stream().map(this::toVoteResponse).toList(),
|
|
|
|
|
new QuorumResponse(quorum.attendees(), quorum.totalMembers(), quorum.required(), quorum.quorumMet())
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PutMapping("/assemblies/{id}")
|
|
|
|
|
public ResponseEntity<AssemblyResponse> updateAssembly(
|
|
|
|
|
@PathVariable UUID id,
|
|
|
|
|
@Valid @RequestBody UpdateAssemblyRequest request,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var assembly = assemblyService.updateAssembly(id, request.title(), request.scheduledAt(),
|
|
|
|
|
request.location(), request.quorumRequired());
|
|
|
|
|
return ResponseEntity.ok(toResponse(assembly));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/assemblies/{id}/invite")
|
|
|
|
|
public ResponseEntity<AssemblyResponse> sendInvitations(
|
|
|
|
|
@PathVariable UUID id,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
var userId = permissionChecker.getUserId(user);
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var assembly = assemblyService.sendInvitations(id, userId);
|
|
|
|
|
return ResponseEntity.ok(toResponse(assembly));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/assemblies/{id}/cancel")
|
|
|
|
|
public ResponseEntity<AssemblyResponse> cancelAssembly(
|
|
|
|
|
@PathVariable UUID id,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
var userId = permissionChecker.getUserId(user);
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var assembly = assemblyService.cancelAssembly(id, userId);
|
|
|
|
|
return ResponseEntity.ok(toResponse(assembly));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/assemblies/{id}/start")
|
|
|
|
|
public ResponseEntity<AssemblyResponse> startAssembly(
|
|
|
|
|
@PathVariable UUID id,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
var userId = permissionChecker.getUserId(user);
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var assembly = assemblyService.startAssembly(id, userId);
|
|
|
|
|
return ResponseEntity.ok(toResponse(assembly));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/assemblies/{id}/complete")
|
|
|
|
|
public ResponseEntity<AssemblyResponse> completeAssembly(
|
|
|
|
|
@PathVariable UUID id,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
var userId = permissionChecker.getUserId(user);
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var assembly = assemblyService.completeAssembly(id, userId);
|
|
|
|
|
return ResponseEntity.ok(toResponse(assembly));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/assemblies/{id}/attendees")
|
|
|
|
|
public ResponseEntity<AttendeeResponse> checkInAttendee(
|
|
|
|
|
@PathVariable UUID id,
|
|
|
|
|
@Valid @RequestBody CheckInRequest request,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var attendee = assemblyService.checkInAttendee(id, request.memberId(), request.proxyForMemberId());
|
|
|
|
|
return ResponseEntity.ok(toAttendeeResponse(attendee));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping("/assemblies/{id}/attendees")
|
|
|
|
|
public ResponseEntity<List<AttendeeResponse>> listAttendees(
|
|
|
|
|
@PathVariable UUID id,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var attendees = assemblyService.getAttendees(id);
|
|
|
|
|
return ResponseEntity.ok(attendees.stream().map(this::toAttendeeResponse).toList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/assemblies/{id}/votes")
|
|
|
|
|
public ResponseEntity<VoteResponse> createVote(
|
|
|
|
|
@PathVariable UUID id,
|
|
|
|
|
@Valid @RequestBody CreateVoteRequest request,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var vote = assemblyService.createVote(id, request.agendaItemId(), request.title(),
|
|
|
|
|
request.description(), request.voteType());
|
|
|
|
|
return ResponseEntity.ok(toVoteResponse(vote));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/assemblies/votes/{voteId}/cast")
|
|
|
|
|
public ResponseEntity<VoteResponse> castVote(
|
|
|
|
|
@PathVariable UUID voteId,
|
|
|
|
|
@Valid @RequestBody CastVoteRequest request,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
var userId = permissionChecker.getUserId(user);
|
|
|
|
|
|
|
|
|
|
var vote = assemblyService.castVote(voteId, request.memberId(), request.decision(), userId);
|
|
|
|
|
return ResponseEntity.ok(toVoteResponse(vote));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/assemblies/votes/{voteId}/close")
|
|
|
|
|
public ResponseEntity<VoteResponse> closeVote(
|
|
|
|
|
@PathVariable UUID voteId,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
var vote = assemblyService.closeVote(voteId);
|
|
|
|
|
return ResponseEntity.ok(toVoteResponse(vote));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping("/assemblies/{id}/protocol")
|
|
|
|
|
public ResponseEntity<byte[]> downloadProtocol(
|
|
|
|
|
@PathVariable UUID id,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
permissionChecker.requirePermission(user, StaffPermission.MANAGE_ASSEMBLIES);
|
|
|
|
|
|
|
|
|
|
byte[] pdf = protocolService.generateProtocol(id);
|
|
|
|
|
return ResponseEntity.ok()
|
|
|
|
|
.contentType(MediaType.APPLICATION_PDF)
|
|
|
|
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=protokoll-" + id + ".pdf")
|
|
|
|
|
.body(pdf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// === Portal Endpoints ===
|
|
|
|
|
|
|
|
|
|
@GetMapping("/portal/assemblies")
|
|
|
|
|
public ResponseEntity<List<AssemblyResponse>> portalListAssemblies(
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
var tenantId = permissionChecker.getTenantId(user);
|
|
|
|
|
var assemblies = assemblyService.getUpcomingAssemblies(tenantId);
|
|
|
|
|
return ResponseEntity.ok(assemblies.stream().map(this::toResponse).toList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@GetMapping("/portal/assemblies/{id}")
|
|
|
|
|
public ResponseEntity<AssemblyDetailResponse> portalGetAssemblyDetail(
|
|
|
|
|
@PathVariable UUID id,
|
|
|
|
|
@AuthenticationPrincipal UserDetails user) {
|
|
|
|
|
var assembly = assemblyService.getAssemblyDetail(id);
|
|
|
|
|
var agendaItems = assemblyService.getAgendaItems(id);
|
|
|
|
|
var attendees = assemblyService.getAttendees(id);
|
|
|
|
|
var votes = assemblyService.getVotes(id);
|
|
|
|
|
var quorum = assemblyService.calculateQuorum(id);
|
|
|
|
|
|
|
|
|
|
return ResponseEntity.ok(new AssemblyDetailResponse(
|
|
|
|
|
toResponse(assembly),
|
|
|
|
|
agendaItems.stream().map(this::toAgendaResponse).toList(),
|
|
|
|
|
attendees.stream().map(this::toAttendeeResponse).toList(),
|
|
|
|
|
votes.stream().map(this::toVoteResponse).toList(),
|
|
|
|
|
new QuorumResponse(quorum.attendees(), quorum.totalMembers(), quorum.required(), quorum.quorumMet())
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// === DTOs ===
|
|
|
|
|
|
|
|
|
|
record CreateAssemblyRequest(
|
|
|
|
|
@NotBlank String title,
|
|
|
|
|
@NotNull AssemblyType assemblyType,
|
|
|
|
|
@NotNull Instant scheduledAt,
|
|
|
|
|
String location,
|
|
|
|
|
Integer quorumRequired,
|
|
|
|
|
List<AgendaItemRequest> agendaItems
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
record AgendaItemRequest(
|
|
|
|
|
@NotBlank String title,
|
|
|
|
|
String description,
|
|
|
|
|
@NotNull AgendaItemType itemType
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
record UpdateAssemblyRequest(
|
|
|
|
|
String title,
|
|
|
|
|
Instant scheduledAt,
|
|
|
|
|
String location,
|
|
|
|
|
Integer quorumRequired
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
record CheckInRequest(@NotNull UUID memberId, UUID proxyForMemberId) {}
|
|
|
|
|
|
|
|
|
|
record CreateVoteRequest(
|
|
|
|
|
@NotNull UUID agendaItemId,
|
|
|
|
|
@NotBlank String title,
|
|
|
|
|
String description,
|
|
|
|
|
@NotNull VoteType voteType
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
record CastVoteRequest(@NotNull UUID memberId, @NotNull VoteDecision decision) {}
|
|
|
|
|
|
|
|
|
|
record AssemblyResponse(
|
|
|
|
|
UUID id, String title, AssemblyType assemblyType, Instant scheduledAt,
|
|
|
|
|
String location, AssemblyStatus status, Instant invitationSentAt,
|
|
|
|
|
Integer quorumRequired, Instant openedAt, Instant closedAt, Instant createdAt
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
record AssemblyDetailResponse(
|
|
|
|
|
AssemblyResponse assembly,
|
|
|
|
|
List<AgendaItemResponse> agendaItems,
|
|
|
|
|
List<AttendeeResponse> attendees,
|
|
|
|
|
List<VoteResponse> votes,
|
|
|
|
|
QuorumResponse quorum
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
record AgendaItemResponse(UUID id, int position, String title, String description, AgendaItemType itemType) {}
|
|
|
|
|
|
|
|
|
|
record AttendeeResponse(UUID id, UUID memberId, Instant checkedInAt, UUID proxyForMemberId) {}
|
|
|
|
|
|
|
|
|
|
record VoteResponse(UUID id, UUID agendaItemId, String title, String description, VoteType voteType,
|
|
|
|
|
int yesCount, int noCount, int abstainCount, VoteResult result, Instant votedAt) {}
|
|
|
|
|
|
|
|
|
|
record QuorumResponse(long attendees, long totalMembers, int required, boolean quorumMet) {}
|
|
|
|
|
|
|
|
|
|
// === Mappers ===
|
|
|
|
|
|
|
|
|
|
private AssemblyResponse toResponse(Assembly a) {
|
|
|
|
|
return new AssemblyResponse(a.getId(), a.getTitle(), a.getAssemblyType(), a.getScheduledAt(),
|
|
|
|
|
a.getLocation(), a.getStatus(), a.getInvitationSentAt(), a.getQuorumRequired(),
|
|
|
|
|
a.getOpenedAt(), a.getClosedAt(), a.getCreatedAt());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private AgendaItemResponse toAgendaResponse(AssemblyAgendaItem i) {
|
|
|
|
|
return new AgendaItemResponse(i.getId(), i.getPosition(), i.getTitle(), i.getDescription(), i.getItemType());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private AttendeeResponse toAttendeeResponse(AssemblyAttendee a) {
|
|
|
|
|
return new AttendeeResponse(a.getId(), a.getMemberId(), a.getCheckedInAt(), a.getProxyForMemberId());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private VoteResponse toVoteResponse(AssemblyVote v) {
|
|
|
|
|
return new VoteResponse(v.getId(), v.getAgendaItemId(), v.getTitle(), v.getDescription(),
|
|
|
|
|
v.getVoteType(), v.getYesCount(), v.getNoCount(), v.getAbstainCount(),
|
|
|
|
|
v.getResult(), v.getVotedAt());
|
|
|
|
|
}
|
|
|
|
|
}
|