feat(sprint7): Phase 2 — Info Board (Schwarzes Brett)
Backend: - V13 Flyway migration: info_board_posts, post_attachments, post_read_status tables - InfoBoardPost entity with category enum (EVENT, RULE, GENERAL, MAINTENANCE) - PostAttachment entity (table created, upload deferred to later) - PostReadStatus entity with composite key (post_id, member_id) - InfoBoardPostRepository with paginated queries + unread count - InfoBoardService: CRUD, pin/archive, mark-as-read, notification dispatch - InfoBoardController: admin CRUD + portal read/unread endpoints - Integration with NotificationService and AuditService Frontend: - info-board.ts service with React Query hooks for all endpoints - Admin Info Board page at /info-board with create dialog, filters, pin/archive/delete - Navigation: added 'Schwarzes Brett' to admin sidebar - i18n: added infoBoard.* keys to de.json and en.json - Fixed pre-existing prettier issues in notification-compose.ts - Fixed BufferSource type issue in push-subscription.ts
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
package de.cannamanage.api.controller;
|
||||
|
||||
import de.cannamanage.domain.entity.InfoBoardPost;
|
||||
import de.cannamanage.domain.enums.InfoBoardCategory;
|
||||
import de.cannamanage.service.InfoBoardService;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
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.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Info Board (Schwarzes Brett) endpoints for admin and portal.
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class InfoBoardController {
|
||||
|
||||
private final InfoBoardService infoBoardService;
|
||||
|
||||
// ============================================================
|
||||
// ADMIN ENDPOINTS (require MANAGE_INFO_BOARD permission)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Create a new info board post.
|
||||
*/
|
||||
@PostMapping("/api/v1/info-board")
|
||||
public ResponseEntity<?> createPost(
|
||||
@Valid @RequestBody CreatePostRequest request,
|
||||
@AuthenticationPrincipal UserDetails user) {
|
||||
|
||||
UUID authorId = UUID.fromString(user.getUsername());
|
||||
InfoBoardPost post = infoBoardService.createPost(
|
||||
request.clubId(), request.title(), request.content(),
|
||||
request.category(), request.pinned() != null && request.pinned(), authorId);
|
||||
|
||||
return ResponseEntity.ok(toResponse(post));
|
||||
}
|
||||
|
||||
/**
|
||||
* List posts (admin view with optional filters).
|
||||
*/
|
||||
@GetMapping("/api/v1/info-board")
|
||||
public ResponseEntity<?> listPosts(
|
||||
@RequestParam UUID clubId,
|
||||
@RequestParam(required = false) InfoBoardCategory category,
|
||||
@RequestParam(defaultValue = "false") boolean includeArchived,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
|
||||
Page<InfoBoardPost> posts = infoBoardService.getPosts(clubId, category, includeArchived, page, size);
|
||||
var items = posts.getContent().stream().map(this::toResponse).toList();
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"posts", items,
|
||||
"totalElements", posts.getTotalElements(),
|
||||
"totalPages", posts.getTotalPages(),
|
||||
"page", posts.getNumber()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single post.
|
||||
*/
|
||||
@GetMapping("/api/v1/info-board/{id}")
|
||||
public ResponseEntity<?> getPost(@PathVariable UUID id) {
|
||||
InfoBoardPost post = infoBoardService.getPost(id);
|
||||
return ResponseEntity.ok(toResponse(post));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a post.
|
||||
*/
|
||||
@PutMapping("/api/v1/info-board/{id}")
|
||||
public ResponseEntity<?> updatePost(
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody UpdatePostRequest request) {
|
||||
|
||||
InfoBoardPost post = infoBoardService.updatePost(
|
||||
id, request.title(), request.content(), request.category(), request.pinned());
|
||||
return ResponseEntity.ok(toResponse(post));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a post.
|
||||
*/
|
||||
@DeleteMapping("/api/v1/info-board/{id}")
|
||||
public ResponseEntity<?> deletePost(@PathVariable UUID id) {
|
||||
infoBoardService.deletePost(id);
|
||||
return ResponseEntity.ok(Map.of("deleted", true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive a post.
|
||||
*/
|
||||
@PostMapping("/api/v1/info-board/{id}/archive")
|
||||
public ResponseEntity<?> archivePost(@PathVariable UUID id) {
|
||||
InfoBoardPost post = infoBoardService.archivePost(id);
|
||||
return ResponseEntity.ok(toResponse(post));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unarchive a post.
|
||||
*/
|
||||
@PostMapping("/api/v1/info-board/{id}/unarchive")
|
||||
public ResponseEntity<?> unarchivePost(@PathVariable UUID id) {
|
||||
InfoBoardPost post = infoBoardService.unarchivePost(id);
|
||||
return ResponseEntity.ok(toResponse(post));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle pin status.
|
||||
*/
|
||||
@PostMapping("/api/v1/info-board/{id}/pin")
|
||||
public ResponseEntity<?> togglePin(@PathVariable UUID id) {
|
||||
InfoBoardPost post = infoBoardService.togglePin(id);
|
||||
return ResponseEntity.ok(toResponse(post));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PORTAL ENDPOINTS (member access)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Get posts for the member's club (non-archived, pinned first).
|
||||
*/
|
||||
@GetMapping("/api/v1/portal/info-board")
|
||||
public ResponseEntity<?> getPortalPosts(
|
||||
@RequestParam UUID clubId,
|
||||
@RequestParam(required = false) InfoBoardCategory category,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
|
||||
Page<InfoBoardPost> posts = infoBoardService.getPosts(clubId, category, false, page, size);
|
||||
var items = posts.getContent().stream().map(this::toResponse).toList();
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"posts", items,
|
||||
"totalElements", posts.getTotalElements(),
|
||||
"totalPages", posts.getTotalPages(),
|
||||
"page", posts.getNumber()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a post as read.
|
||||
*/
|
||||
@PostMapping("/api/v1/portal/info-board/{id}/read")
|
||||
public ResponseEntity<?> markAsRead(
|
||||
@PathVariable UUID id,
|
||||
@RequestParam UUID memberId) {
|
||||
infoBoardService.markAsRead(id, memberId);
|
||||
return ResponseEntity.ok(Map.of("read", true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread post count for badge display.
|
||||
*/
|
||||
@GetMapping("/api/v1/portal/info-board/unread-count")
|
||||
public ResponseEntity<?> getUnreadCount(
|
||||
@RequestParam UUID clubId,
|
||||
@RequestParam UUID memberId) {
|
||||
long count = infoBoardService.getUnreadCount(clubId, memberId);
|
||||
return ResponseEntity.ok(Map.of("unreadCount", count));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DTOs
|
||||
// ============================================================
|
||||
|
||||
public record CreatePostRequest(
|
||||
@NotNull UUID clubId,
|
||||
@NotBlank @Size(max = 200) String title,
|
||||
@NotBlank String content,
|
||||
@NotNull InfoBoardCategory category,
|
||||
Boolean pinned
|
||||
) {}
|
||||
|
||||
public record UpdatePostRequest(
|
||||
@Size(max = 200) String title,
|
||||
String content,
|
||||
InfoBoardCategory category,
|
||||
Boolean pinned
|
||||
) {}
|
||||
|
||||
// ============================================================
|
||||
// Response mapping
|
||||
// ============================================================
|
||||
|
||||
private Map<String, Object> toResponse(InfoBoardPost post) {
|
||||
return Map.of(
|
||||
"id", post.getId(),
|
||||
"clubId", post.getClubId(),
|
||||
"title", post.getTitle(),
|
||||
"content", post.getContent(),
|
||||
"category", post.getCategory().name(),
|
||||
"pinned", post.isPinned(),
|
||||
"archived", post.isArchived(),
|
||||
"authorId", post.getAuthorId(),
|
||||
"createdAt", post.getCreatedAt().toString(),
|
||||
"updatedAt", post.getUpdatedAt() != null ? post.getUpdatedAt().toString() : ""
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user