An den Verantaltungen und Locations gearbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
@@ -71,11 +71,15 @@ public class SecurityConfig {
|
||||
.requestMatchers("/games/chastity/joinlock.html").authenticated()
|
||||
.requestMatchers("/community/benachrichtigungen.html").authenticated()
|
||||
.requestMatchers("/community/abonnements.html").authenticated()
|
||||
.requestMatchers("/community/locations.html").authenticated()
|
||||
.requestMatchers("/community/location-detail.html").authenticated()
|
||||
.requestMatchers("/community/events.html").authenticated()
|
||||
.requestMatchers("/community/event-detail.html").authenticated()
|
||||
.requestMatchers("/gruppen/**").authenticated()
|
||||
.requestMatchers("/feed/**").authenticated()
|
||||
.requestMatchers("/notifications/**").authenticated()
|
||||
.requestMatchers("/events/**").authenticated()
|
||||
.requestMatchers("/*.html").permitAll()
|
||||
.requestMatchers("/*.html").permitAll()
|
||||
.requestMatchers("/**/*.html").permitAll()
|
||||
.requestMatchers("/help/*.html").permitAll()
|
||||
.requestMatchers("/css/**").permitAll()
|
||||
|
||||
425
src/main/java/de/oaa/xxx/location/LocationController.java
Normal file
425
src/main/java/de/oaa/xxx/location/LocationController.java
Normal file
@@ -0,0 +1,425 @@
|
||||
package de.oaa.xxx.location;
|
||||
|
||||
import de.oaa.xxx.location.entity.*;
|
||||
import de.oaa.xxx.location.repository.*;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
import jakarta.transaction.Transactional;
|
||||
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.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/locations")
|
||||
public class LocationController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LocationController.class);
|
||||
private static final int MAX_GALLERY_IMAGES = 20;
|
||||
private static final int MAX_BATCH_SIZE = 50;
|
||||
|
||||
private final LocationRepository locationRepo;
|
||||
private final LocationImageRepository imageRepo;
|
||||
private final LocationOpeningHoursRepository hoursRepo;
|
||||
private final LocationEventRepository eventRepo;
|
||||
private final LocationEventAttendeeRepository attendeeRepo;
|
||||
private final LocationFollowRepository followRepo;
|
||||
private final UserService userService;
|
||||
|
||||
public LocationController(LocationRepository locationRepo,
|
||||
LocationImageRepository imageRepo,
|
||||
LocationOpeningHoursRepository hoursRepo,
|
||||
LocationEventRepository eventRepo,
|
||||
LocationEventAttendeeRepository attendeeRepo,
|
||||
LocationFollowRepository followRepo,
|
||||
UserService userService) {
|
||||
this.locationRepo = locationRepo;
|
||||
this.imageRepo = imageRepo;
|
||||
this.hoursRepo = hoursRepo;
|
||||
this.eventRepo = eventRepo;
|
||||
this.attendeeRepo = attendeeRepo;
|
||||
this.followRepo = followRepo;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
// ── DTOs ─────────────────────────────────────────────────────────────────
|
||||
|
||||
record IdsResult(List<UUID> ids, int total) {}
|
||||
|
||||
record LocationPreviewDto(UUID locationId, String name, String profilePictureLq, double distanzKm) {}
|
||||
|
||||
record OpeningHourDto(int dayOfWeek, String openTime, String closeTime, boolean closed) {}
|
||||
|
||||
record GalleryImageDto(UUID imageId, String imageData) {}
|
||||
|
||||
record LocationDetailDto(
|
||||
UUID locationId,
|
||||
UUID ownerId,
|
||||
String name,
|
||||
String description,
|
||||
String profilePictureHq,
|
||||
String profilePictureLq,
|
||||
Double lat,
|
||||
Double lon,
|
||||
String street,
|
||||
String city,
|
||||
boolean ownershipConfirmed,
|
||||
LocalDateTime createdAt,
|
||||
List<GalleryImageDto> gallery,
|
||||
List<OpeningHourDto> openingHours,
|
||||
boolean following
|
||||
) {}
|
||||
|
||||
record CreateRequest(
|
||||
String name,
|
||||
String description,
|
||||
String profilePictureLq,
|
||||
String profilePictureHq,
|
||||
Double lat,
|
||||
Double lon,
|
||||
String street,
|
||||
String city,
|
||||
boolean ownershipConfirmed
|
||||
) {}
|
||||
|
||||
record UpdateRequest(
|
||||
String name,
|
||||
String description,
|
||||
String profilePictureLq,
|
||||
String profilePictureHq,
|
||||
Double lat,
|
||||
Double lon,
|
||||
String street,
|
||||
String city
|
||||
) {}
|
||||
|
||||
record GalleryUploadRequest(String imageData) {}
|
||||
|
||||
// ── Suche / IDs ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Gibt alle Location-IDs zurück, sortiert nach Entfernung vom angegebenen Punkt.
|
||||
*/
|
||||
@GetMapping("/ids")
|
||||
public ResponseEntity<IdsResult> getIds(
|
||||
@RequestParam double lat,
|
||||
@RequestParam double lon,
|
||||
@RequestParam(defaultValue = "50") int maxDistanceKm,
|
||||
Principal principal) {
|
||||
|
||||
userService.requireUser(principal);
|
||||
|
||||
List<UUID> sorted = locationRepo.findByLatIsNotNullAndLonIsNotNull().stream()
|
||||
.filter(l -> haversineKm(lat, lon, l.getLat(), l.getLon()) <= maxDistanceKm)
|
||||
.sorted(Comparator.comparingDouble(l -> haversineKm(lat, lon, l.getLat(), l.getLon())))
|
||||
.map(LocationEntity::getLocationId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(new IdsResult(sorted, sorted.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch-Laden von Location-Previews (Name + LQ-Bild + Distanz).
|
||||
* Benötigt lat/lon für die Distanzberechnung.
|
||||
*/
|
||||
@PostMapping("/batch")
|
||||
public ResponseEntity<List<LocationPreviewDto>> getBatch(
|
||||
@RequestBody BatchRequest request,
|
||||
Principal principal) {
|
||||
|
||||
userService.requireUser(principal);
|
||||
if (request.ids() == null || request.ids().isEmpty()) return ResponseEntity.ok(List.of());
|
||||
|
||||
List<UUID> ids = request.ids().stream().filter(Objects::nonNull).limit(MAX_BATCH_SIZE).toList();
|
||||
Map<UUID, LocationEntity> byId = locationRepo.findAllById(ids).stream()
|
||||
.collect(Collectors.toMap(LocationEntity::getLocationId, l -> l));
|
||||
|
||||
double refLat = request.lat() != null ? request.lat() : 0;
|
||||
double refLon = request.lon() != null ? request.lon() : 0;
|
||||
|
||||
List<LocationPreviewDto> result = ids.stream()
|
||||
.map(byId::get)
|
||||
.filter(Objects::nonNull)
|
||||
.map(l -> new LocationPreviewDto(
|
||||
l.getLocationId(),
|
||||
l.getName(),
|
||||
l.getProfilePictureLq(),
|
||||
l.getLat() != null && l.getLon() != null
|
||||
? Math.round(haversineKm(refLat, refLon, l.getLat(), l.getLon()) * 10.0) / 10.0
|
||||
: -1))
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
record BatchRequest(List<UUID> ids, Double lat, Double lon) {}
|
||||
|
||||
/**
|
||||
* Meine eigenen Locations (IDs, neueste zuerst).
|
||||
*/
|
||||
@GetMapping("/mine")
|
||||
public ResponseEntity<IdsResult> getMine(Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
List<UUID> ids = locationRepo.findByOwnerIdOrderByCreatedAtDesc(myId).stream()
|
||||
.map(LocationEntity::getLocationId)
|
||||
.toList();
|
||||
return ResponseEntity.ok(new IdsResult(ids, ids.size()));
|
||||
}
|
||||
|
||||
// ── Detail ───────────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/{locationId}")
|
||||
public ResponseEntity<LocationDetailDto> getDetail(
|
||||
@PathVariable UUID locationId,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
return locationRepo.findById(locationId)
|
||||
.map(l -> ResponseEntity.ok(toDetail(l, myId)))
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
// ── CRUD ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<LocationDetailDto> create(
|
||||
@RequestBody CreateRequest req,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
|
||||
if (req.name() == null || req.name().isBlank()) return ResponseEntity.badRequest().build();
|
||||
|
||||
LocationEntity loc = new LocationEntity();
|
||||
loc.setLocationId(UUID.randomUUID());
|
||||
loc.setOwnerId(myId);
|
||||
loc.setName(req.name().trim());
|
||||
loc.setDescription(req.description() != null ? req.description().trim() : null);
|
||||
loc.setProfilePictureLq(req.profilePictureLq());
|
||||
loc.setProfilePictureHq(req.profilePictureHq());
|
||||
loc.setLat(req.lat());
|
||||
loc.setLon(req.lon());
|
||||
loc.setStreet(req.street());
|
||||
loc.setCity(req.city());
|
||||
loc.setOwnershipConfirmed(req.ownershipConfirmed());
|
||||
loc.setCreatedAt(LocalDateTime.now());
|
||||
locationRepo.save(loc);
|
||||
|
||||
LOGGER.info("User {} hat Location {} angelegt", myId, loc.getLocationId());
|
||||
return ResponseEntity.status(201).body(toDetail(loc, myId));
|
||||
}
|
||||
|
||||
@PutMapping("/{locationId}")
|
||||
public ResponseEntity<LocationDetailDto> update(
|
||||
@PathVariable UUID locationId,
|
||||
@RequestBody UpdateRequest req,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
var opt = locationRepo.findById(locationId);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
LocationEntity loc = opt.get();
|
||||
if (!loc.getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
if (req.name() != null && !req.name().isBlank()) loc.setName(req.name().trim());
|
||||
if (req.description() != null) loc.setDescription(req.description().trim());
|
||||
if (req.profilePictureLq() != null) loc.setProfilePictureLq(req.profilePictureLq());
|
||||
if (req.profilePictureHq() != null) loc.setProfilePictureHq(req.profilePictureHq());
|
||||
if (req.lat() != null) loc.setLat(req.lat());
|
||||
if (req.lon() != null) loc.setLon(req.lon());
|
||||
if (req.street() != null) loc.setStreet(req.street());
|
||||
if (req.city() != null) loc.setCity(req.city());
|
||||
locationRepo.save(loc);
|
||||
|
||||
return ResponseEntity.ok(toDetail(loc, myId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@DeleteMapping("/{locationId}")
|
||||
public ResponseEntity<Void> delete(
|
||||
@PathVariable UUID locationId,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
var opt = locationRepo.findById(locationId);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!opt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
// Kaskade: Events + Attendees + Bilder + Öffnungszeiten + Follows löschen
|
||||
eventRepo.findByLocationIdOrderByStartAtAsc(locationId).forEach(e -> {
|
||||
attendeeRepo.deleteByEventId(e.getEventId());
|
||||
});
|
||||
eventRepo.deleteByLocationId(locationId);
|
||||
imageRepo.deleteByLocationId(locationId);
|
||||
hoursRepo.deleteByLocationId(locationId);
|
||||
followRepo.deleteByLocationId(locationId);
|
||||
locationRepo.deleteById(locationId);
|
||||
|
||||
LOGGER.info("User {} hat Location {} gelöscht", myId, locationId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Galerie ───────────────────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/{locationId}/gallery")
|
||||
public ResponseEntity<GalleryImageDto> uploadGalleryImage(
|
||||
@PathVariable UUID locationId,
|
||||
@RequestBody GalleryUploadRequest req,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
var opt = locationRepo.findById(locationId);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!opt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
if (req.imageData() == null || req.imageData().isBlank()) return ResponseEntity.badRequest().build();
|
||||
if (imageRepo.countByLocationId(locationId) >= MAX_GALLERY_IMAGES) return ResponseEntity.status(422).build();
|
||||
|
||||
LocationImageEntity img = new LocationImageEntity();
|
||||
img.setImageId(UUID.randomUUID());
|
||||
img.setLocationId(locationId);
|
||||
img.setImageData(req.imageData());
|
||||
img.setUploadedAt(LocalDateTime.now());
|
||||
imageRepo.save(img);
|
||||
|
||||
return ResponseEntity.status(201).body(new GalleryImageDto(img.getImageId(), img.getImageData()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@DeleteMapping("/{locationId}/gallery/{imageId}")
|
||||
public ResponseEntity<Void> deleteGalleryImage(
|
||||
@PathVariable UUID locationId,
|
||||
@PathVariable UUID imageId,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
var locOpt = locationRepo.findById(locationId);
|
||||
if (locOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!locOpt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
var imgOpt = imageRepo.findById(imageId);
|
||||
if (imgOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!imgOpt.get().getLocationId().equals(locationId)) return ResponseEntity.status(400).build();
|
||||
|
||||
imageRepo.delete(imgOpt.get());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Öffnungszeiten ────────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/{locationId}/opening-hours")
|
||||
public ResponseEntity<List<OpeningHourDto>> getOpeningHours(
|
||||
@PathVariable UUID locationId,
|
||||
Principal principal) {
|
||||
|
||||
userService.requireUser(principal);
|
||||
if (!locationRepo.existsById(locationId)) return ResponseEntity.notFound().build();
|
||||
|
||||
List<OpeningHourDto> dtos = hoursRepo.findByLocationIdOrderByDayOfWeek(locationId).stream()
|
||||
.map(h -> new OpeningHourDto(h.getDayOfWeek(), h.getOpenTime(), h.getCloseTime(), h.isClosed()))
|
||||
.toList();
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Öffnungszeiten einer Location vollständig neu (kompletter Ersatz).
|
||||
*/
|
||||
@Transactional
|
||||
@PutMapping("/{locationId}/opening-hours")
|
||||
public ResponseEntity<List<OpeningHourDto>> setOpeningHours(
|
||||
@PathVariable UUID locationId,
|
||||
@RequestBody List<OpeningHourDto> hours,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
var opt = locationRepo.findById(locationId);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!opt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
hoursRepo.deleteByLocationId(locationId);
|
||||
|
||||
List<LocationOpeningHoursEntity> saved = new ArrayList<>();
|
||||
for (OpeningHourDto dto : hours) {
|
||||
if (dto.dayOfWeek() < 1 || dto.dayOfWeek() > 7) continue;
|
||||
LocationOpeningHoursEntity e = new LocationOpeningHoursEntity();
|
||||
e.setHoursId(UUID.randomUUID());
|
||||
e.setLocationId(locationId);
|
||||
e.setDayOfWeek(dto.dayOfWeek());
|
||||
e.setOpenTime(dto.openTime());
|
||||
e.setCloseTime(dto.closeTime());
|
||||
e.setClosed(dto.closed());
|
||||
saved.add(hoursRepo.save(e));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(saved.stream()
|
||||
.map(h -> new OpeningHourDto(h.getDayOfWeek(), h.getOpenTime(), h.getCloseTime(), h.isClosed()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
// ── Follow ────────────────────────────────────────────────────────────────
|
||||
|
||||
@PostMapping("/{locationId}/follow")
|
||||
public ResponseEntity<Map<String, Object>> toggleFollow(
|
||||
@PathVariable UUID locationId,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
if (!locationRepo.existsById(locationId)) return ResponseEntity.notFound().build();
|
||||
|
||||
var existing = followRepo.findByUserIdAndLocationId(myId, locationId);
|
||||
boolean following;
|
||||
if (existing.isPresent()) {
|
||||
followRepo.delete(existing.get());
|
||||
following = false;
|
||||
} else {
|
||||
var f = new de.oaa.xxx.location.entity.LocationFollowEntity();
|
||||
f.setFollowId(UUID.randomUUID());
|
||||
f.setLocationId(locationId);
|
||||
f.setUserId(myId);
|
||||
f.setFollowedAt(LocalDateTime.now());
|
||||
followRepo.save(f);
|
||||
following = true;
|
||||
}
|
||||
return ResponseEntity.ok(Map.of("following", following));
|
||||
}
|
||||
|
||||
@GetMapping("/followed")
|
||||
public ResponseEntity<IdsResult> getFollowed(Principal principal) {
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
List<UUID> ids = followRepo.findByUserId(myId).stream()
|
||||
.map(de.oaa.xxx.location.entity.LocationFollowEntity::getLocationId)
|
||||
.toList();
|
||||
return ResponseEntity.ok(new IdsResult(ids, ids.size()));
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private LocationDetailDto toDetail(LocationEntity l, UUID myId) {
|
||||
List<GalleryImageDto> gallery = imageRepo.findByLocationIdOrderByUploadedAtAsc(l.getLocationId()).stream()
|
||||
.map(i -> new GalleryImageDto(i.getImageId(), i.getImageData()))
|
||||
.toList();
|
||||
List<OpeningHourDto> hours = hoursRepo.findByLocationIdOrderByDayOfWeek(l.getLocationId()).stream()
|
||||
.map(h -> new OpeningHourDto(h.getDayOfWeek(), h.getOpenTime(), h.getCloseTime(), h.isClosed()))
|
||||
.toList();
|
||||
boolean following = followRepo.findByUserIdAndLocationId(myId, l.getLocationId()).isPresent();
|
||||
return new LocationDetailDto(
|
||||
l.getLocationId(), l.getOwnerId(), l.getName(), l.getDescription(),
|
||||
l.getProfilePictureHq(), l.getProfilePictureLq(),
|
||||
l.getLat(), l.getLon(), l.getStreet(), l.getCity(),
|
||||
l.isOwnershipConfirmed(), l.getCreatedAt(),
|
||||
gallery, hours, following);
|
||||
}
|
||||
|
||||
static double haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
final double R = 6371.0;
|
||||
double dLat = Math.toRadians(lat2 - lat1);
|
||||
double dLon = Math.toRadians(lon2 - lon1);
|
||||
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
|
||||
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
|
||||
* Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
}
|
||||
384
src/main/java/de/oaa/xxx/location/LocationEventController.java
Normal file
384
src/main/java/de/oaa/xxx/location/LocationEventController.java
Normal file
@@ -0,0 +1,384 @@
|
||||
package de.oaa.xxx.location;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import de.oaa.xxx.location.entity.LocationEventAttendeeEntity;
|
||||
import de.oaa.xxx.location.entity.LocationEventEntity;
|
||||
import de.oaa.xxx.location.entity.LocationFollowEntity;
|
||||
import de.oaa.xxx.location.repository.LocationEventAttendeeRepository;
|
||||
import de.oaa.xxx.location.repository.LocationEventRepository;
|
||||
import de.oaa.xxx.location.repository.LocationFollowRepository;
|
||||
import de.oaa.xxx.location.repository.LocationRepository;
|
||||
import de.oaa.xxx.user.UserEntity;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
@RestController
|
||||
public class LocationEventController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LocationEventController.class);
|
||||
private static final int MAX_BATCH_SIZE = 50;
|
||||
|
||||
private final LocationRepository locationRepo;
|
||||
private final LocationEventRepository eventRepo;
|
||||
private final LocationEventAttendeeRepository attendeeRepo;
|
||||
private final LocationFollowRepository followRepo;
|
||||
private final UserRepository userRepo;
|
||||
private final UserService userService;
|
||||
|
||||
public LocationEventController(LocationRepository locationRepo,
|
||||
LocationEventRepository eventRepo,
|
||||
LocationEventAttendeeRepository attendeeRepo,
|
||||
LocationFollowRepository followRepo,
|
||||
UserRepository userRepo,
|
||||
UserService userService) {
|
||||
this.locationRepo = locationRepo;
|
||||
this.eventRepo = eventRepo;
|
||||
this.attendeeRepo = attendeeRepo;
|
||||
this.followRepo = followRepo;
|
||||
this.userRepo = userRepo;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
// ── DTOs ─────────────────────────────────────────────────────────────────
|
||||
|
||||
record IdsResult(List<UUID> ids, int total) {}
|
||||
|
||||
record EventPreviewDto(
|
||||
UUID eventId,
|
||||
UUID locationId,
|
||||
String locationName,
|
||||
String title,
|
||||
String imageData,
|
||||
LocalDateTime startAt,
|
||||
double distanzKm,
|
||||
long attendeeCount,
|
||||
boolean attendingMe
|
||||
) {}
|
||||
|
||||
record AttendeeDto(UUID userId, String name, String profilePictureLq, String geschlecht) {}
|
||||
|
||||
record EventDetailDto(
|
||||
UUID eventId,
|
||||
UUID locationId,
|
||||
String locationName,
|
||||
String title,
|
||||
String description,
|
||||
String imageData,
|
||||
LocalDateTime startAt,
|
||||
LocalDateTime createdAt,
|
||||
boolean attendingMe,
|
||||
List<AttendeeDto> attendees
|
||||
) {}
|
||||
|
||||
record CreateEventRequest(String title, String description, String imageData, LocalDateTime startAt) {}
|
||||
|
||||
record UpdateEventRequest(String title, String description, String imageData, LocalDateTime startAt) {}
|
||||
|
||||
record BatchRequest(List<UUID> ids, Double lat, Double lon) {}
|
||||
|
||||
// ── Events einer Location ─────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/locations/{locationId}/events")
|
||||
public ResponseEntity<List<EventPreviewDto>> getEventsForLocation(
|
||||
@PathVariable UUID locationId,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
if (!locationRepo.existsById(locationId)) return ResponseEntity.notFound().build();
|
||||
|
||||
var location = locationRepo.findById(locationId).orElseThrow();
|
||||
double refLat = location.getLat() != null ? location.getLat() : 0;
|
||||
double refLon = location.getLon() != null ? location.getLon() : 0;
|
||||
|
||||
List<EventPreviewDto> dtos = eventRepo.findByLocationIdOrderByStartAtAsc(locationId).stream()
|
||||
.map(e -> toPreview(e, location.getName(), refLat, refLon, myId))
|
||||
.toList();
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
@PostMapping("/locations/{locationId}/events")
|
||||
public ResponseEntity<EventDetailDto> createEvent(
|
||||
@PathVariable UUID locationId,
|
||||
@RequestBody CreateEventRequest req,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
var locOpt = locationRepo.findById(locationId);
|
||||
if (locOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!locOpt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
if (req.title() == null || req.title().isBlank()) return ResponseEntity.badRequest().build();
|
||||
if (req.startAt() == null) return ResponseEntity.badRequest().build();
|
||||
|
||||
LocationEventEntity event = new LocationEventEntity();
|
||||
event.setEventId(UUID.randomUUID());
|
||||
event.setLocationId(locationId);
|
||||
event.setTitle(req.title().trim());
|
||||
event.setDescription(req.description() != null ? req.description().trim() : null);
|
||||
event.setImageData(req.imageData());
|
||||
event.setStartAt(req.startAt());
|
||||
event.setCreatedAt(LocalDateTime.now());
|
||||
eventRepo.save(event);
|
||||
|
||||
LOGGER.info("Location {} hat Event {} angelegt", locationId, event.getEventId());
|
||||
return ResponseEntity.status(201).body(toDetail(event, locOpt.get().getName(), myId));
|
||||
}
|
||||
|
||||
@PutMapping("/locations/{locationId}/events/{eventId}")
|
||||
public ResponseEntity<EventDetailDto> updateEvent(
|
||||
@PathVariable UUID locationId,
|
||||
@PathVariable UUID eventId,
|
||||
@RequestBody UpdateEventRequest req,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
var locOpt = locationRepo.findById(locationId);
|
||||
if (locOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!locOpt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
var evtOpt = eventRepo.findById(eventId);
|
||||
if (evtOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!evtOpt.get().getLocationId().equals(locationId)) return ResponseEntity.status(400).build();
|
||||
|
||||
LocationEventEntity event = evtOpt.get();
|
||||
if (req.title() != null && !req.title().isBlank()) event.setTitle(req.title().trim());
|
||||
if (req.description() != null) event.setDescription(req.description().trim());
|
||||
if (req.imageData() != null) event.setImageData(req.imageData());
|
||||
if (req.startAt() != null) event.setStartAt(req.startAt());
|
||||
eventRepo.save(event);
|
||||
|
||||
return ResponseEntity.ok(toDetail(event, locOpt.get().getName(), myId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@DeleteMapping("/locations/{locationId}/events/{eventId}")
|
||||
public ResponseEntity<Void> deleteEvent(
|
||||
@PathVariable UUID locationId,
|
||||
@PathVariable UUID eventId,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
var locOpt = locationRepo.findById(locationId);
|
||||
if (locOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!locOpt.get().getOwnerId().equals(myId)) return ResponseEntity.status(403).build();
|
||||
|
||||
var evtOpt = eventRepo.findById(eventId);
|
||||
if (evtOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
if (!evtOpt.get().getLocationId().equals(locationId)) return ResponseEntity.status(400).build();
|
||||
|
||||
attendeeRepo.deleteByEventId(eventId);
|
||||
eventRepo.delete(evtOpt.get());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// ── Event-Detail und Teilnahme ────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/location-events/{eventId}")
|
||||
public ResponseEntity<EventDetailDto> getEvent(
|
||||
@PathVariable UUID eventId,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
var evtOpt = eventRepo.findById(eventId);
|
||||
if (evtOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
|
||||
String locationName = locationRepo.findById(evtOpt.get().getLocationId())
|
||||
.map(l -> l.getName()).orElse("");
|
||||
return ResponseEntity.ok(toDetail(evtOpt.get(), locationName, myId));
|
||||
}
|
||||
|
||||
@PostMapping("/location-events/{eventId}/attend")
|
||||
public ResponseEntity<Map<String, Object>> toggleAttend(
|
||||
@PathVariable UUID eventId,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
if (!eventRepo.existsById(eventId)) return ResponseEntity.notFound().build();
|
||||
|
||||
var existing = attendeeRepo.findByEventIdAndUserId(eventId, myId);
|
||||
boolean attending;
|
||||
if (existing.isPresent()) {
|
||||
attendeeRepo.delete(existing.get());
|
||||
attending = false;
|
||||
} else {
|
||||
LocationEventAttendeeEntity a = new LocationEventAttendeeEntity();
|
||||
a.setAttendeeId(UUID.randomUUID());
|
||||
a.setEventId(eventId);
|
||||
a.setUserId(myId);
|
||||
a.setRegisteredAt(LocalDateTime.now());
|
||||
attendeeRepo.save(a);
|
||||
attending = true;
|
||||
}
|
||||
long count = attendeeRepo.countByEventId(eventId);
|
||||
return ResponseEntity.ok(Map.of("attending", attending, "attendeeCount", count));
|
||||
}
|
||||
|
||||
// ── Event-Suche (IDs + Batch) ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Liefert Event-IDs sortiert nach Datum (nächste zuerst).
|
||||
* Immer enthalten: Events von abonnierten Locations.
|
||||
* Optional: Events im Umkreis von lat/lon (wenn angegeben).
|
||||
*/
|
||||
@GetMapping("/location-events/ids")
|
||||
public ResponseEntity<IdsResult> searchIds(
|
||||
@RequestParam(required = false) Double lat,
|
||||
@RequestParam(required = false) Double lon,
|
||||
@RequestParam(defaultValue = "50") int maxDistanceKm,
|
||||
@RequestParam(required = false) String from,
|
||||
@RequestParam(required = false) String to,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
|
||||
LocalDateTime fromDt = from != null ? LocalDateTime.parse(from) : LocalDateTime.now();
|
||||
LocalDateTime toDt = to != null ? LocalDateTime.parse(to) : fromDt.plusMonths(3);
|
||||
|
||||
// Abonnierte Locations – deren Events werden immer eingeschlossen
|
||||
Set<UUID> followedLocationIds = followRepo.findByUserId(myId).stream()
|
||||
.map(LocationFollowEntity::getLocationId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<UUID, double[]> locationCoords = new HashMap<>();
|
||||
List<UUID> sorted = eventRepo.findInTimeRange(fromDt, toDt).stream()
|
||||
.filter(e -> {
|
||||
// Immer einschließen wenn Location abonniert
|
||||
if (followedLocationIds.contains(e.getLocationId())) return true;
|
||||
// Ohne Koordinaten kein Umkreis-Filter
|
||||
if (lat == null || lon == null) return false;
|
||||
double[] coords = locationCoords.computeIfAbsent(e.getLocationId(), locId ->
|
||||
locationRepo.findById(locId)
|
||||
.filter(l -> l.getLat() != null && l.getLon() != null)
|
||||
.map(l -> new double[]{l.getLat(), l.getLon()})
|
||||
.orElse(null));
|
||||
if (coords == null) return false;
|
||||
return LocationController.haversineKm(lat, lon, coords[0], coords[1]) <= maxDistanceKm;
|
||||
})
|
||||
.sorted(Comparator.comparing(LocationEventEntity::getStartAt))
|
||||
.map(LocationEventEntity::getEventId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(new IdsResult(sorted, sorted.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch-Laden von Event-Previews.
|
||||
*/
|
||||
@PostMapping("/location-events/batch")
|
||||
public ResponseEntity<List<EventPreviewDto>> getBatch(
|
||||
@RequestBody BatchRequest request,
|
||||
Principal principal) {
|
||||
|
||||
UUID myId = userService.requireUser(principal).getUserId();
|
||||
if (request.ids() == null || request.ids().isEmpty()) return ResponseEntity.ok(List.of());
|
||||
|
||||
List<UUID> ids = request.ids().stream().filter(Objects::nonNull).limit(MAX_BATCH_SIZE).toList();
|
||||
Map<UUID, LocationEventEntity> byId = eventRepo.findAllById(ids).stream()
|
||||
.collect(Collectors.toMap(LocationEventEntity::getEventId, Function.identity()));
|
||||
|
||||
double refLat = request.lat() != null ? request.lat() : 0;
|
||||
double refLon = request.lon() != null ? request.lon() : 0;
|
||||
|
||||
// Locations für Namens-Lookup und Distanzberechnung
|
||||
Set<UUID> locationIds = byId.values().stream()
|
||||
.map(LocationEventEntity::getLocationId).collect(Collectors.toSet());
|
||||
Map<UUID, de.oaa.xxx.location.entity.LocationEntity> locationById =
|
||||
locationRepo.findAllById(locationIds).stream()
|
||||
.collect(Collectors.toMap(
|
||||
de.oaa.xxx.location.entity.LocationEntity::getLocationId, Function.identity()));
|
||||
|
||||
List<EventPreviewDto> result = ids.stream()
|
||||
.map(byId::get)
|
||||
.filter(Objects::nonNull)
|
||||
.map(e -> {
|
||||
var loc = locationById.get(e.getLocationId());
|
||||
String locName = loc != null ? loc.getName() : "";
|
||||
double dist = (loc != null && loc.getLat() != null && loc.getLon() != null)
|
||||
? Math.round(LocationController.haversineKm(refLat, refLon, loc.getLat(), loc.getLon()) * 10.0) / 10.0
|
||||
: -1;
|
||||
return toPreview(e, locName, refLat, refLon, myId);
|
||||
})
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private EventPreviewDto toPreview(LocationEventEntity e, String locationName,
|
||||
double refLat, double refLon, UUID myId) {
|
||||
var loc = locationRepo.findById(e.getLocationId()).orElse(null);
|
||||
double dist = (loc != null && loc.getLat() != null && loc.getLon() != null)
|
||||
? Math.round(LocationController.haversineKm(refLat, refLon, loc.getLat(), loc.getLon()) * 10.0) / 10.0
|
||||
: -1;
|
||||
long count = attendeeRepo.countByEventId(e.getEventId());
|
||||
boolean mine = attendeeRepo.findByEventIdAndUserId(e.getEventId(), myId).isPresent();
|
||||
return new EventPreviewDto(e.getEventId(), e.getLocationId(), locationName,
|
||||
e.getTitle(), e.getImageData(), e.getStartAt(), dist, count, mine);
|
||||
}
|
||||
|
||||
private EventDetailDto toDetail(LocationEventEntity e, String locationName, UUID myId) {
|
||||
List<LocationEventAttendeeEntity> attendeeEntities =
|
||||
attendeeRepo.findByEventIdOrderByRegisteredAtAsc(e.getEventId());
|
||||
|
||||
Set<UUID> attendeeUserIds = attendeeEntities.stream()
|
||||
.map(LocationEventAttendeeEntity::getUserId).collect(Collectors.toSet());
|
||||
Map<UUID, UserEntity> userById = attendeeUserIds.isEmpty()
|
||||
? Map.of()
|
||||
: userRepo.findAllById(attendeeUserIds).stream()
|
||||
.collect(Collectors.toMap(UserEntity::getUserId, Function.identity()));
|
||||
|
||||
// Sortierung: Alphabetisch nach Geschlecht-Name, dann nach Registrierungszeit
|
||||
List<AttendeeDto> attendees = attendeeEntities.stream()
|
||||
.map(a -> {
|
||||
UserEntity u = userById.get(a.getUserId());
|
||||
if (u == null) return null;
|
||||
String geschlecht = u.getGeschlecht() != null ? u.getGeschlecht().name() : "UNBEKANNT";
|
||||
return new AttendeeDto(u.getUserId(), u.getName(), u.getProfilePicture(), geschlecht);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator
|
||||
.comparing((AttendeeDto a) -> {
|
||||
// Sortierreihenfolge: WEIBLICH → MAENNLICH → DIVERS → rest
|
||||
return switch (a.geschlecht()) {
|
||||
case "WEIBLICH" -> 0;
|
||||
case "MAENNLICH" -> 1;
|
||||
case "DIVERS" -> 2;
|
||||
default -> 3;
|
||||
};
|
||||
})
|
||||
.thenComparing(AttendeeDto::name))
|
||||
.toList();
|
||||
|
||||
boolean attendingMe = attendeeRepo.findByEventIdAndUserId(e.getEventId(), myId).isPresent();
|
||||
|
||||
return new EventDetailDto(
|
||||
e.getEventId(), e.getLocationId(), locationName,
|
||||
e.getTitle(), e.getDescription(), e.getImageData(),
|
||||
e.getStartAt(), e.getCreatedAt(),
|
||||
attendingMe, attendees);
|
||||
}
|
||||
}
|
||||
55
src/main/java/de/oaa/xxx/location/entity/LocationEntity.java
Normal file
55
src/main/java/de/oaa/xxx/location/entity/LocationEntity.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package de.oaa.xxx.location.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "location")
|
||||
public class LocationEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID locationId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID ownerId;
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String name;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
/** LQ-Profilbild (klein, für Listenansicht) – base64 */
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String profilePictureLq;
|
||||
|
||||
/** HQ-Profilbild (max 1024×1024) – base64 */
|
||||
@Column(columnDefinition = "MEDIUMTEXT")
|
||||
private String profilePictureHq;
|
||||
|
||||
@Column
|
||||
private Double lat;
|
||||
|
||||
@Column
|
||||
private Double lon;
|
||||
|
||||
@Column(length = 200)
|
||||
private String street;
|
||||
|
||||
@Column(length = 200)
|
||||
private String city;
|
||||
|
||||
/** Eigentümerschaft wurde vom Anleger bestätigt */
|
||||
@Column(nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||
private boolean ownershipConfirmed = false;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.oaa.xxx.location.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "location_event_attendee",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"event_id", "user_id"}))
|
||||
public class LocationEventAttendeeEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID attendeeId;
|
||||
|
||||
@Column(nullable = false, name = "event_id")
|
||||
private UUID eventId;
|
||||
|
||||
@Column(nullable = false, name = "user_id")
|
||||
private UUID userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime registeredAt;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.oaa.xxx.location.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "location_event")
|
||||
public class LocationEventEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID eventId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID locationId;
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String title;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
/** base64-Profilbild der Veranstaltung, max 1024px */
|
||||
@Column(columnDefinition = "MEDIUMTEXT")
|
||||
private String imageData;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime startAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package de.oaa.xxx.location.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "location_follow")
|
||||
public class LocationFollowEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID followId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID locationId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime followedAt;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.oaa.xxx.location.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "location_image")
|
||||
public class LocationImageEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID imageId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID locationId;
|
||||
|
||||
/** base64-kodiertes Bild, max 1024px */
|
||||
@Column(nullable = false, columnDefinition = "MEDIUMTEXT")
|
||||
private String imageData;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime uploadedAt;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.oaa.xxx.location.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "location_opening_hours",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"location_id", "day_of_week"}))
|
||||
public class LocationOpeningHoursEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID hoursId;
|
||||
|
||||
@Column(nullable = false, name = "location_id")
|
||||
private UUID locationId;
|
||||
|
||||
/**
|
||||
* Wochentag: 1 = Montag, 2 = Dienstag, ..., 7 = Sonntag
|
||||
*/
|
||||
@Column(nullable = false, name = "day_of_week")
|
||||
private int dayOfWeek;
|
||||
|
||||
/** Öffnungszeit als "HH:mm"-String, null = kein Eintrag */
|
||||
@Column(length = 5)
|
||||
private String openTime;
|
||||
|
||||
/** Schließzeit als "HH:mm"-String, null = kein Eintrag */
|
||||
@Column(length = 5)
|
||||
private String closeTime;
|
||||
|
||||
/** true = an diesem Tag geschlossen */
|
||||
@Column(nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0")
|
||||
private boolean closed = false;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.oaa.xxx.location.repository;
|
||||
|
||||
import de.oaa.xxx.location.entity.LocationEventAttendeeEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface LocationEventAttendeeRepository extends JpaRepository<LocationEventAttendeeEntity, UUID> {
|
||||
|
||||
List<LocationEventAttendeeEntity> findByEventIdOrderByRegisteredAtAsc(UUID eventId);
|
||||
|
||||
Optional<LocationEventAttendeeEntity> findByEventIdAndUserId(UUID eventId, UUID userId);
|
||||
|
||||
long countByEventId(UUID eventId);
|
||||
|
||||
void deleteByEventId(UUID eventId);
|
||||
|
||||
void deleteByUserId(UUID userId);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.oaa.xxx.location.repository;
|
||||
|
||||
import de.oaa.xxx.location.entity.LocationEventEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface LocationEventRepository extends JpaRepository<LocationEventEntity, UUID> {
|
||||
|
||||
List<LocationEventEntity> findByLocationIdOrderByStartAtAsc(UUID locationId);
|
||||
|
||||
/** Alle zukünftigen Events mit Koordinaten ihrer Location (für Umkreis-Suche) */
|
||||
@Query("""
|
||||
SELECT e FROM LocationEventEntity e
|
||||
WHERE e.startAt >= :from
|
||||
AND e.startAt <= :to
|
||||
""")
|
||||
List<LocationEventEntity> findInTimeRange(LocalDateTime from, LocalDateTime to);
|
||||
|
||||
void deleteByLocationId(UUID locationId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.oaa.xxx.location.repository;
|
||||
|
||||
import de.oaa.xxx.location.entity.LocationFollowEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface LocationFollowRepository extends JpaRepository<LocationFollowEntity, UUID> {
|
||||
Optional<LocationFollowEntity> findByUserIdAndLocationId(UUID userId, UUID locationId);
|
||||
List<LocationFollowEntity> findByUserId(UUID userId);
|
||||
void deleteByLocationId(UUID locationId);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.oaa.xxx.location.repository;
|
||||
|
||||
import de.oaa.xxx.location.entity.LocationImageEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface LocationImageRepository extends JpaRepository<LocationImageEntity, UUID> {
|
||||
|
||||
List<LocationImageEntity> findByLocationIdOrderByUploadedAtAsc(UUID locationId);
|
||||
|
||||
long countByLocationId(UUID locationId);
|
||||
|
||||
void deleteByLocationId(UUID locationId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.oaa.xxx.location.repository;
|
||||
|
||||
import de.oaa.xxx.location.entity.LocationOpeningHoursEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface LocationOpeningHoursRepository extends JpaRepository<LocationOpeningHoursEntity, UUID> {
|
||||
|
||||
List<LocationOpeningHoursEntity> findByLocationIdOrderByDayOfWeek(UUID locationId);
|
||||
|
||||
void deleteByLocationId(UUID locationId);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.oaa.xxx.location.repository;
|
||||
|
||||
import de.oaa.xxx.location.entity.LocationEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface LocationRepository extends JpaRepository<LocationEntity, UUID> {
|
||||
|
||||
List<LocationEntity> findByOwnerIdOrderByCreatedAtDesc(UUID ownerId);
|
||||
|
||||
/** Alle Locations mit gesetzten Koordinaten (für Umkreissuche) */
|
||||
List<LocationEntity> findByLatIsNotNullAndLonIsNotNull();
|
||||
}
|
||||
@@ -33,6 +33,10 @@ public class User {
|
||||
private Integer datingMaxDistanzKm;
|
||||
private Integer datingMinAlter;
|
||||
private Integer datingMaxAlter;
|
||||
private String filterCity;
|
||||
private Double filterLat;
|
||||
private Double filterLon;
|
||||
private Integer filterMaxDistKm;
|
||||
|
||||
public Integer getAlter() {
|
||||
return geburtsdatum != null ? Period.between(geburtsdatum, LocalDate.now()).getYears() : null;
|
||||
|
||||
@@ -500,4 +500,17 @@ public class UserController {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
record LocationFilterRequest(String filterCity, Double filterLat, Double filterLon, Integer filterMaxDistKm) {}
|
||||
|
||||
@PutMapping("/me/location-filter")
|
||||
public ResponseEntity<Void> updateLocationFilter(@RequestBody LocationFilterRequest request, Principal principal) {
|
||||
var user = userService.requireUser(principal);
|
||||
if (request.filterCity() != null) user.setFilterCity(request.filterCity());
|
||||
if (request.filterLat() != null) user.setFilterLat(request.filterLat());
|
||||
if (request.filterLon() != null) user.setFilterLon(request.filterLon());
|
||||
if (request.filterMaxDistKm() != null) user.setFilterMaxDistKm(Math.max(1, Math.min(500, request.filterMaxDistKm())));
|
||||
userRepository.save(user);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,19 @@ public class UserEntity {
|
||||
@Column
|
||||
private Integer datingMaxAlter;
|
||||
|
||||
// ── Locations/Events-Filter ──
|
||||
@Column(length = 200)
|
||||
private String filterCity;
|
||||
|
||||
@Column
|
||||
private Double filterLat;
|
||||
|
||||
@Column
|
||||
private Double filterLon;
|
||||
|
||||
@Column
|
||||
private Integer filterMaxDistKm;
|
||||
|
||||
public Integer getAlter() {
|
||||
return geburtsdatum != null ? Period.between(geburtsdatum, LocalDate.now()).getYears() : null;
|
||||
}
|
||||
@@ -167,6 +180,10 @@ public class UserEntity {
|
||||
user.setDatingMaxDistanzKm(datingMaxDistanzKm);
|
||||
user.setDatingMinAlter(datingMinAlter);
|
||||
user.setDatingMaxAlter(datingMaxAlter);
|
||||
user.setFilterCity(filterCity);
|
||||
user.setFilterLat(filterLat);
|
||||
user.setFilterLon(filterLon);
|
||||
user.setFilterMaxDistKm(filterMaxDistKm);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user