package de.oaa.xxx.gruppe; import de.oaa.xxx.gruppe.dto.*; import de.oaa.xxx.gruppe.entity.*; import de.oaa.xxx.gruppe.repository.*; import de.oaa.xxx.social.entity.KommentarEntity; import de.oaa.xxx.social.repository.KommentarRepository; import de.oaa.xxx.user.UserEntity; import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserService; import de.oaa.xxx.util.BaseController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.security.Principal; import java.time.LocalDateTime; import java.util.*; @RestController @RequestMapping("/gruppen") public class GruppeController extends BaseController { private static final Logger LOGGER = LoggerFactory.getLogger(GruppeController.class); private final GruppeRepository gruppeRepository; private final GruppenmitgliedRepository mitgliedRepository; private final BeitrittsanfrageRepository anfrageRepository; private final GruppenbeitragRepository beitragRepository; private final UmfrageOptionRepository optionRepository; private final UmfrageStimmeRepository stimmeRepository; private final GruppenbeitragLikeRepository likeRepository; private final BeitragMeldungRepository meldungRepository; private final KommentarRepository kommentarRepository; private final UserRepository userRepository; public GruppeController(GruppeRepository gruppeRepository, GruppenmitgliedRepository mitgliedRepository, BeitrittsanfrageRepository anfrageRepository, GruppenbeitragRepository beitragRepository, UmfrageOptionRepository optionRepository, UmfrageStimmeRepository stimmeRepository, GruppenbeitragLikeRepository likeRepository, BeitragMeldungRepository meldungRepository, KommentarRepository kommentarRepository, UserRepository userRepository, UserService userService) { super(userService); this.gruppeRepository = gruppeRepository; this.mitgliedRepository = mitgliedRepository; this.anfrageRepository = anfrageRepository; this.beitragRepository = beitragRepository; this.optionRepository = optionRepository; this.stimmeRepository = stimmeRepository; this.likeRepository = likeRepository; this.meldungRepository = meldungRepository; this.kommentarRepository = kommentarRepository; this.userRepository = userRepository; } record CreateGruppeRequest(String name, String beschreibung, String bild, boolean isPrivate) {} record JoinRequest(String nachricht) {} record UpdateGruppeRequest(String name, String beschreibung, String bild, Boolean isPrivate) {} // ── GET /gruppe/search?q= ── @GetMapping("/search") public ResponseEntity> search(@RequestParam String q, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); List gruppen = gruppeRepository.findByNameContainingIgnoreCase(q) .stream().limit(30).toList(); List gruppeIds = gruppen.stream().map(GruppeEntity::getGruppeId).toList(); Map latestActivity = buildLatestActivityMap(gruppeIds); List result = gruppen.stream() .map(g -> toDto(g, myId)) .sorted((a, b) -> { LocalDateTime la = latestActivity.getOrDefault(a.gruppeId(), a.createdAt()); LocalDateTime lb = latestActivity.getOrDefault(b.gruppeId(), b.createdAt()); return lb.compareTo(la); }) .toList(); return ResponseEntity.ok(result); } // ── GET /gruppe/mine ── @GetMapping("/mine") public ResponseEntity> mine(Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); List gruppen = mitgliedRepository.findByUserId(myId) .stream() .map(m -> gruppeRepository.findById(m.getGruppeId()).orElse(null)) .filter(Objects::nonNull) .toList(); List gruppeIds = gruppen.stream().map(GruppeEntity::getGruppeId).toList(); Map latestActivity = buildLatestActivityMap(gruppeIds); List result = gruppen.stream() .map(g -> toDto(g, myId)) .sorted((a, b) -> { LocalDateTime la = latestActivity.getOrDefault(a.gruppeId(), a.createdAt()); LocalDateTime lb = latestActivity.getOrDefault(b.gruppeId(), b.createdAt()); return lb.compareTo(la); }) .toList(); return ResponseEntity.ok(result); } // ── GET /gruppe/{id} ── @GetMapping("/{id}") public ResponseEntity getGruppe(@PathVariable("id") UUID id, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); return gruppeRepository.findById(id) .map(g -> ResponseEntity.ok(toDto(g, myId))) .orElse(ResponseEntity.notFound().build()); } // ── POST /gruppe ── @PostMapping public ResponseEntity createGruppe(@RequestBody CreateGruppeRequest req, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); if (req.name() == null || req.name().isBlank()) return ResponseEntity.badRequest().build(); GruppeEntity gruppe = new GruppeEntity(); gruppe.setGruppeId(UUID.randomUUID()); gruppe.setName(req.name().trim()); gruppe.setBeschreibung(req.beschreibung()); gruppe.setBild(req.bild()); gruppe.setPrivate(req.isPrivate()); gruppe.setCreatedAt(LocalDateTime.now()); gruppe.setCreatedByUserId(myId); gruppeRepository.save(gruppe); GruppenmitgliedEntity admin = new GruppenmitgliedEntity(); admin.setMitgliedId(UUID.randomUUID()); admin.setGruppeId(gruppe.getGruppeId()); admin.setUserId(myId); admin.setRolle(GruppenRolle.ADMIN); admin.setJoinedAt(LocalDateTime.now()); mitgliedRepository.save(admin); LOGGER.info("User {} hat Gruppe '{}' ({}) erstellt", myId, gruppe.getName(), gruppe.getGruppeId()); return ResponseEntity.status(201).body(toDto(gruppe, myId)); } // ── PUT /gruppe/{id} ── @PutMapping("/{id}") public ResponseEntity updateGruppe(@PathVariable("id") UUID id, @RequestBody UpdateGruppeRequest req, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); var gruppeOpt = gruppeRepository.findById(id); if (gruppeOpt.isEmpty()) return ResponseEntity.notFound().build(); GruppeEntity gruppe = gruppeOpt.get(); if (!isAdmin(id, myId)) return ResponseEntity.status(403).build(); if (req.name() != null && !req.name().isBlank()) gruppe.setName(req.name().trim()); if (req.beschreibung() != null) gruppe.setBeschreibung(req.beschreibung()); if (req.bild() != null) gruppe.setBild(req.bild()); if (req.isPrivate() != null) gruppe.setPrivate(req.isPrivate()); gruppeRepository.save(gruppe); LOGGER.debug("User {} hat Gruppe {} aktualisiert", myId, id); return ResponseEntity.ok(toDto(gruppe, myId)); } // ── DELETE /gruppe/{id} ── @DeleteMapping("/{id}") public ResponseEntity deleteGruppe(@PathVariable("id") UUID id, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); var gruppeOpt = gruppeRepository.findById(id); if (gruppeOpt.isEmpty()) return ResponseEntity.notFound().build(); if (!isAdmin(id, myId)) return ResponseEntity.status(403).build(); // Cascade delete List beitraege = beitragRepository.findByGruppeIdOrderByCreatedAtDesc(id); for (GruppenbeitragEntity b : beitraege) { UUID bid = b.getBeitragId(); meldungRepository.deleteByBeitragId(bid); stimmeRepository.deleteByBeitragId(bid); optionRepository.deleteByBeitragId(bid); likeRepository.deleteByBeitragId(bid); // Kommentare on GROUP_POST List kommentare = kommentarRepository .findByTargetTypeAndTargetIdOrderByCreatedAtAsc("GROUP_POST", bid); for (KommentarEntity k : kommentare) { kommentarRepository.delete(k); } } beitragRepository.deleteByGruppeId(id); anfrageRepository.deleteByGruppeId(id); mitgliedRepository.deleteByGruppeId(id); gruppeRepository.deleteById(id); LOGGER.info("User {} hat Gruppe {} gelöscht", myId, id); return ResponseEntity.noContent().build(); } // ── POST /gruppe/{id}/join ── @PostMapping("/{id}/join") public ResponseEntity join(@PathVariable("id") UUID id, @RequestBody(required = false) JoinRequest req, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); var gruppeOpt = gruppeRepository.findById(id); if (gruppeOpt.isEmpty()) return ResponseEntity.notFound().build(); GruppeEntity gruppe = gruppeOpt.get(); if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId).isPresent()) return ResponseEntity.status(409).build(); if (gruppe.isPrivate()) { // Check no pending request already var existingReq = anfrageRepository.findByGruppeIdAndUserId(id, myId); if (existingReq.isPresent() && existingReq.get().getStatus() == AnfrageStatus.AUSSTEHEND) return ResponseEntity.status(409).build(); BeitrittsanfrageEntity anfrage = new BeitrittsanfrageEntity(); anfrage.setAnfrageId(UUID.randomUUID()); anfrage.setGruppeId(id); anfrage.setUserId(myId); anfrage.setNachricht(req != null ? req.nachricht() : null); anfrage.setAngefragtAt(LocalDateTime.now()); anfrage.setStatus(AnfrageStatus.AUSSTEHEND); anfrageRepository.save(anfrage); LOGGER.info("User {} hat Beitrittsanfrage für private Gruppe {} gestellt", myId, id); return ResponseEntity.status(201).build(); } else { GruppenmitgliedEntity mitglied = new GruppenmitgliedEntity(); mitglied.setMitgliedId(UUID.randomUUID()); mitglied.setGruppeId(id); mitglied.setUserId(myId); mitglied.setRolle(GruppenRolle.MITGLIED); mitglied.setJoinedAt(LocalDateTime.now()); mitgliedRepository.save(mitglied); LOGGER.info("User {} ist Gruppe {} beigetreten", myId, id); return ResponseEntity.status(201).build(); } } // ── DELETE /gruppe/{id}/leave ── @DeleteMapping("/{id}/leave") public ResponseEntity leave(@PathVariable("id") UUID id, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); mitgliedRepository.deleteByGruppeIdAndUserId(id, myId); LOGGER.info("User {} hat Gruppe {} verlassen", myId, id); return ResponseEntity.noContent().build(); } // ── GET /gruppe/{id}/members ── @GetMapping("/{id}/members") public ResponseEntity>> getMembers(@PathVariable("id") UUID id, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, myId).isEmpty()) return ResponseEntity.status(403).build(); List> result = mitgliedRepository.findByGruppeId(id).stream() .map(m -> { Map map = new LinkedHashMap<>(); map.put("mitgliedId", m.getMitgliedId()); map.put("userId", m.getUserId()); map.put("rolle", m.getRolle().name()); map.put("joinedAt", m.getJoinedAt()); userRepository.findById(m.getUserId()).ifPresent(u -> { map.put("userName", u.getName()); map.put("userPicture", u.getProfilePicture()); }); return map; }) .toList(); return ResponseEntity.ok(result); } // ── DELETE /gruppe/{id}/members/{userId} ── @DeleteMapping("/{id}/members/{userId}") public ResponseEntity removeMember(@PathVariable("id") UUID id, @PathVariable("userId") UUID userId, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); if (!isAdmin(id, myId)) return ResponseEntity.status(403).build(); mitgliedRepository.deleteByGruppeIdAndUserId(id, userId); LOGGER.warn("Admin {} hat User {} aus Gruppe {} entfernt", myId, userId, id); return ResponseEntity.noContent().build(); } // ── POST /gruppe/{id}/members/{userId}/promote ── @PostMapping("/{id}/members/{userId}/promote") public ResponseEntity promote(@PathVariable("id") UUID id, @PathVariable("userId") UUID userId, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); if (!isAdmin(id, myId)) return ResponseEntity.status(403).build(); var m = mitgliedRepository.findFirstByGruppeIdAndUserId(id, userId); if (m.isEmpty()) return ResponseEntity.notFound().build(); m.get().setRolle(GruppenRolle.ADMIN); mitgliedRepository.save(m.get()); LOGGER.info("Admin {} hat User {} in Gruppe {} zum Admin befördert", myId, userId, id); return ResponseEntity.ok().build(); } // ── GET /gruppe/{id}/requests ── @GetMapping("/{id}/requests") public ResponseEntity> getRequests(@PathVariable("id") UUID id, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); if (!isAdmin(id, myId)) return ResponseEntity.status(403).build(); List dtos = anfrageRepository .findByGruppeIdAndStatus(id, AnfrageStatus.AUSSTEHEND) .stream() .map(a -> { UserEntity u = userRepository.findById(a.getUserId()).orElse(null); return new BeitrittsanfrageDto(a.getAnfrageId(), a.getGruppeId(), a.getUserId(), u != null ? u.getName() : "Unbekannt", u != null ? u.getProfilePicture() : null, a.getNachricht(), a.getAngefragtAt()); }) .toList(); return ResponseEntity.ok(dtos); } // ── POST /gruppe/{id}/requests/{reqId}/approve ── @PostMapping("/{id}/requests/{reqId}/approve") public ResponseEntity approveRequest(@PathVariable("id") UUID id, @PathVariable("reqId") UUID reqId, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); if (!isAdmin(id, myId)) return ResponseEntity.status(403).build(); var anfOpt = anfrageRepository.findById(reqId); if (anfOpt.isEmpty()) return ResponseEntity.notFound().build(); BeitrittsanfrageEntity anfrage = anfOpt.get(); anfrage.setStatus(AnfrageStatus.GENEHMIGT); anfrageRepository.save(anfrage); LOGGER.info("Admin {} hat Beitrittsanfrage {} (User: {}) für Gruppe {} genehmigt", myId, reqId, anfrage.getUserId(), id); if (mitgliedRepository.findFirstByGruppeIdAndUserId(id, anfrage.getUserId()).isEmpty()) { GruppenmitgliedEntity mitglied = new GruppenmitgliedEntity(); mitglied.setMitgliedId(UUID.randomUUID()); mitglied.setGruppeId(id); mitglied.setUserId(anfrage.getUserId()); mitglied.setRolle(GruppenRolle.MITGLIED); mitglied.setJoinedAt(LocalDateTime.now()); mitgliedRepository.save(mitglied); } return ResponseEntity.ok().build(); } // ── DELETE /gruppe/{id}/requests/{reqId} ── @DeleteMapping("/{id}/requests/{reqId}") public ResponseEntity rejectRequest(@PathVariable("id") UUID id, @PathVariable("reqId") UUID reqId, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); if (!isAdmin(id, myId)) return ResponseEntity.status(403).build(); var anfOpt = anfrageRepository.findById(reqId); if (anfOpt.isEmpty()) return ResponseEntity.notFound().build(); BeitrittsanfrageEntity anfrage = anfOpt.get(); anfrage.setStatus(AnfrageStatus.ABGELEHNT); anfrageRepository.save(anfrage); LOGGER.debug("Admin {} hat Beitrittsanfrage {} für Gruppe {} abgelehnt", myId, reqId, id); return ResponseEntity.noContent().build(); } // ── GET /gruppen/reports/pending/count ── @GetMapping("/reports/pending/count") public ResponseEntity pendingReportsCount(Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); long count = mitgliedRepository.findByUserId(myId).stream() .filter(m -> m.getRolle() == GruppenRolle.ADMIN) .mapToLong(m -> { List beitraege = beitragRepository.findByGruppeIdOrderByCreatedAtDesc(m.getGruppeId()); List ids = beitraege.stream().map(GruppenbeitragEntity::getBeitragId).toList(); return ids.isEmpty() ? 0L : meldungRepository.findByBeitragIdIn(ids).size(); }) .sum(); return ResponseEntity.ok(count); } // ── GET /gruppen/requests/pending/count ── @GetMapping("/requests/pending/count") public ResponseEntity pendingRequestCount(Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); long count = mitgliedRepository.findByUserId(myId).stream() .filter(m -> m.getRolle() == GruppenRolle.ADMIN) .mapToLong(m -> anfrageRepository.findByGruppeIdAndStatus(m.getGruppeId(), AnfrageStatus.AUSSTEHEND).size()) .sum(); return ResponseEntity.ok(count); } // ── GET /gruppe/requests/mine ── @GetMapping("/requests/mine") public ResponseEntity>> myRequests(Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); List> result = anfrageRepository.findByUserIdAndStatus(myId, AnfrageStatus.AUSSTEHEND) .stream() .map(a -> { Map map = new LinkedHashMap<>(); map.put("anfrageId", a.getAnfrageId()); map.put("gruppeId", a.getGruppeId()); map.put("nachricht", a.getNachricht()); map.put("angefragtAt", a.getAngefragtAt()); gruppeRepository.findById(a.getGruppeId()).ifPresent(g -> map.put("gruppeName", g.getName())); return map; }) .toList(); return ResponseEntity.ok(result); } // ── DELETE /gruppe/{id}/requests/mine (withdraw own request) ── @DeleteMapping("/{id}/requests/mine") public ResponseEntity withdrawRequest(@PathVariable("id") UUID id, Principal principal) { UUID myId = resolveMyId(principal); if (myId == null) return ResponseEntity.status(401).build(); anfrageRepository.findByGruppeIdAndUserId(id, myId).ifPresent(a -> { if (a.getStatus() == AnfrageStatus.AUSSTEHEND) { anfrageRepository.delete(a); LOGGER.debug("User {} hat eigene Beitrittsanfrage für Gruppe {} zurückgezogen", myId, id); } }); return ResponseEntity.noContent().build(); } // ── Helpers ── private Map buildLatestActivityMap(List gruppeIds) { if (gruppeIds.isEmpty()) return Map.of(); Map map = new HashMap<>(); beitragRepository.findLatestCreatedAtByGruppeIds(gruppeIds).forEach(row -> { UUID gId = (UUID) row[0]; LocalDateTime latest = (LocalDateTime) row[1]; map.put(gId, latest); }); return map; } private boolean isAdmin(UUID gruppeId, UUID userId) { return mitgliedRepository.findFirstByGruppeIdAndUserId(gruppeId, userId) .map(m -> m.getRolle() == GruppenRolle.ADMIN) .orElse(false); } GruppeDto toDto(GruppeEntity g, UUID myId) { long memberCount = mitgliedRepository.countByGruppeId(g.getGruppeId()); long postCount = beitragRepository.findByGruppeIdOrderByCreatedAtDesc(g.getGruppeId()).size(); String myRole = mitgliedRepository.findFirstByGruppeIdAndUserId(g.getGruppeId(), myId) .map(m -> m.getRolle().name()) .orElse(null); String myRequestStatus = null; if (myRole == null) { myRequestStatus = anfrageRepository.findByGruppeIdAndUserId(g.getGruppeId(), myId) .filter(a -> a.getStatus() == AnfrageStatus.AUSSTEHEND) .map(a -> a.getStatus().name()) .orElse(null); } return new GruppeDto(g.getGruppeId(), g.getName(), g.getBeschreibung(), g.getBild(), g.isPrivate(), g.getCreatedAt(), memberCount, postCount, myRole, myRequestStatus); } }