feat(sprint7): Phase 2.5 — Club Event Calendar
- Flyway V14: club_events + event_rsvps tables with reminder_sent tracking - Enums: EventType, RsvpStatus, RecurrenceRule + extend AuditEventType/NotificationType - Entities: ClubEvent (extends AbstractTenantEntity), EventRsvp (unique event+member) - Repositories: ClubEventRepository, EventRsvpRepository with date-range and status queries - EventService: CRUD, RSVP with maxAttendees enforcement (409 if full), iCal RFC 5545 generation, recurring event virtual expansion, notifications on create/cancel, auto-post to Info Board - EventReminderScheduler: hourly check, 24h reminder to ACCEPTED/MAYBE attendees - EventController: admin CRUD (MANAGE_INFO_BOARD permission), portal upcoming events, RSVP endpoint, iCal download (text/calendar), attendee list - Frontend: events.ts service (React Query hooks matching apiClient pattern), admin calendar page (month grid with event dots, create dialog, event cards), portal events page (RSVP buttons, capacity display) - Navigation: added Kalender with Calendar icon - i18n: events.* keys in de.json and en.json - UI: added @radix-ui/react-switch + Switch component
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package de.cannamanage.service;
|
||||
|
||||
import de.cannamanage.domain.enums.NotificationType;
|
||||
import de.cannamanage.domain.enums.RsvpStatus;
|
||||
import de.cannamanage.service.repository.ClubEventRepository;
|
||||
import de.cannamanage.service.repository.EventRsvpRepository;
|
||||
import de.cannamanage.service.repository.MemberRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Scheduler that sends event reminders ~24h before an event starts.
|
||||
* Runs every hour, finds events in the 24-25h window, sends notifications to ACCEPTED/MAYBE attendees.
|
||||
*/
|
||||
@Service
|
||||
public class EventReminderScheduler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EventReminderScheduler.class);
|
||||
|
||||
private final ClubEventRepository eventRepository;
|
||||
private final EventRsvpRepository rsvpRepository;
|
||||
private final MemberRepository memberRepository;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
public EventReminderScheduler(ClubEventRepository eventRepository,
|
||||
EventRsvpRepository rsvpRepository,
|
||||
MemberRepository memberRepository,
|
||||
NotificationService notificationService) {
|
||||
this.eventRepository = eventRepository;
|
||||
this.rsvpRepository = rsvpRepository;
|
||||
this.memberRepository = memberRepository;
|
||||
this.notificationService = notificationService;
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 3600000) // Every hour
|
||||
@Transactional
|
||||
public void sendUpcomingEventReminders() {
|
||||
var now = Instant.now();
|
||||
var in24h = now.plus(24, ChronoUnit.HOURS);
|
||||
var in25h = now.plus(25, ChronoUnit.HOURS);
|
||||
|
||||
var upcomingEvents = eventRepository.findByStartAtBetweenAndReminderSentFalse(in24h, in25h);
|
||||
|
||||
for (var event : upcomingEvents) {
|
||||
var attendees = rsvpRepository.findByEventIdAndStatusIn(
|
||||
event.getId(), List.of(RsvpStatus.ACCEPTED, RsvpStatus.MAYBE));
|
||||
|
||||
for (var rsvp : attendees) {
|
||||
var member = memberRepository.findById(rsvp.getMemberId());
|
||||
member.ifPresent(m -> {
|
||||
if (m.getUserId() != null) {
|
||||
notificationService.sendNotification(
|
||||
m.getUserId(),
|
||||
NotificationType.EVENT_REMINDER,
|
||||
"Erinnerung: " + event.getTitle(),
|
||||
"Morgen um " + formatTime(event.getStartAt()) +
|
||||
(event.getLocation() != null ? " — " + event.getLocation() : ""),
|
||||
"/portal/events/" + event.getId()
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Mark reminder as sent
|
||||
event.setReminderSent(true);
|
||||
eventRepository.save(event);
|
||||
log.info("Sent {} reminders for event '{}'", attendees.size(), event.getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
private String formatTime(Instant instant) {
|
||||
return DateTimeFormatter.ofPattern("HH:mm")
|
||||
.format(instant.atZone(ZoneId.of("Europe/Berlin")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
package de.cannamanage.service;
|
||||
|
||||
import de.cannamanage.domain.entity.ClubEvent;
|
||||
import de.cannamanage.domain.entity.EventRsvp;
|
||||
import de.cannamanage.domain.entity.TenantContext;
|
||||
import de.cannamanage.domain.enums.*;
|
||||
import de.cannamanage.service.repository.ClubEventRepository;
|
||||
import de.cannamanage.service.repository.EventRsvpRepository;
|
||||
import de.cannamanage.service.repository.MemberRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Service for club event management — CRUD, RSVP, iCal generation, and recurring event expansion.
|
||||
*/
|
||||
@Service
|
||||
@Transactional
|
||||
public class EventService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EventService.class);
|
||||
private static final DateTimeFormatter ICAL_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");
|
||||
|
||||
private final ClubEventRepository eventRepository;
|
||||
private final EventRsvpRepository rsvpRepository;
|
||||
private final MemberRepository memberRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final InfoBoardService infoBoardService;
|
||||
private final AuditService auditService;
|
||||
|
||||
public EventService(ClubEventRepository eventRepository,
|
||||
EventRsvpRepository rsvpRepository,
|
||||
MemberRepository memberRepository,
|
||||
NotificationService notificationService,
|
||||
InfoBoardService infoBoardService,
|
||||
AuditService auditService) {
|
||||
this.eventRepository = eventRepository;
|
||||
this.rsvpRepository = rsvpRepository;
|
||||
this.memberRepository = memberRepository;
|
||||
this.notificationService = notificationService;
|
||||
this.infoBoardService = infoBoardService;
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new club event and notify all members.
|
||||
*/
|
||||
public ClubEvent createEvent(UUID clubId, String title, String description, EventType eventType,
|
||||
Instant startAt, Instant endAt, String location, Integer maxAttendees,
|
||||
boolean recurring, RecurrenceRule recurrenceRule, LocalDate recurrenceEndDate,
|
||||
UUID createdBy, boolean postToInfoBoard) {
|
||||
var event = new ClubEvent(clubId, title, description, eventType, startAt, endAt, location, maxAttendees, createdBy);
|
||||
event.setRecurring(recurring);
|
||||
event.setRecurrenceRule(recurrenceRule);
|
||||
event.setRecurrenceEndDate(recurrenceEndDate);
|
||||
|
||||
ClubEvent saved = eventRepository.save(event);
|
||||
log.info("Club event created: {} '{}' in club {}", saved.getId(), title, clubId);
|
||||
|
||||
// Notify all club members
|
||||
try {
|
||||
var members = memberRepository.findByClubId(clubId);
|
||||
members.forEach(member -> {
|
||||
if (member.getUserId() != null) {
|
||||
notificationService.sendNotification(
|
||||
member.getUserId(),
|
||||
NotificationType.EVENT_CREATED,
|
||||
title,
|
||||
formatEventNotification(saved),
|
||||
"/portal/events/" + saved.getId()
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to send event notifications: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Auto-post to info board
|
||||
if (postToInfoBoard) {
|
||||
try {
|
||||
String infoContent = formatEventInfoBoardContent(saved);
|
||||
infoBoardService.createPost(clubId, "📅 " + title, infoContent,
|
||||
InfoBoardCategory.EVENT, false, createdBy);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to auto-post event to info board: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Audit
|
||||
auditService.log(AuditEventType.EVENT_CREATED, "ClubEvent", saved.getId().toString(),
|
||||
"Event created: " + title);
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update event details.
|
||||
*/
|
||||
public ClubEvent updateEvent(UUID eventId, String title, String description, EventType eventType,
|
||||
Instant startAt, Instant endAt, String location, Integer maxAttendees,
|
||||
boolean recurring, RecurrenceRule recurrenceRule, LocalDate recurrenceEndDate) {
|
||||
var event = eventRepository.findById(eventId)
|
||||
.orElseThrow(() -> new NoSuchElementException("Event not found: " + eventId));
|
||||
|
||||
event.setTitle(title);
|
||||
event.setDescription(description);
|
||||
event.setEventType(eventType);
|
||||
event.setStartAt(startAt);
|
||||
event.setEndAt(endAt);
|
||||
event.setLocation(location);
|
||||
event.setMaxAttendees(maxAttendees);
|
||||
event.setRecurring(recurring);
|
||||
event.setRecurrenceRule(recurrenceRule);
|
||||
event.setRecurrenceEndDate(recurrenceEndDate);
|
||||
|
||||
ClubEvent saved = eventRepository.save(event);
|
||||
auditService.log(AuditEventType.EVENT_UPDATED, "ClubEvent", saved.getId().toString(),
|
||||
"Event updated: " + title);
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel (delete) an event and notify attendees.
|
||||
*/
|
||||
public void cancelEvent(UUID eventId) {
|
||||
var event = eventRepository.findById(eventId)
|
||||
.orElseThrow(() -> new NoSuchElementException("Event not found: " + eventId));
|
||||
|
||||
// Notify ACCEPTED and MAYBE attendees
|
||||
var attendees = rsvpRepository.findByEventIdAndStatusIn(eventId,
|
||||
List.of(RsvpStatus.ACCEPTED, RsvpStatus.MAYBE));
|
||||
for (var rsvp : attendees) {
|
||||
var member = memberRepository.findById(rsvp.getMemberId());
|
||||
member.ifPresent(m -> {
|
||||
if (m.getUserId() != null) {
|
||||
notificationService.sendNotification(
|
||||
m.getUserId(),
|
||||
NotificationType.EVENT_CANCELLED,
|
||||
"Veranstaltung abgesagt: " + event.getTitle(),
|
||||
"Die Veranstaltung \"" + event.getTitle() + "\" wurde abgesagt.",
|
||||
null
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auditService.log(AuditEventType.EVENT_CANCELLED, "ClubEvent", event.getId().toString(),
|
||||
"Event cancelled: " + event.getTitle());
|
||||
eventRepository.delete(event);
|
||||
log.info("Event cancelled and deleted: {}", eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single event by ID.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public ClubEvent getEvent(UUID eventId) {
|
||||
return eventRepository.findById(eventId)
|
||||
.orElseThrow(() -> new NoSuchElementException("Event not found: " + eventId));
|
||||
}
|
||||
|
||||
/**
|
||||
* List events in a date range for calendar view.
|
||||
* Includes recurring event expansion.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<ClubEvent> listEvents(Instant from, Instant to) {
|
||||
UUID tenantId = TenantContext.getCurrentTenant();
|
||||
var events = eventRepository.findByTenantIdAndStartAtBetweenOrderByStartAtAsc(tenantId, from, to);
|
||||
|
||||
// Also find recurring events that started before 'from' but may have occurrences in range
|
||||
var allEvents = eventRepository.findByTenantIdAndStartAtBetweenOrderByStartAtAsc(
|
||||
tenantId, Instant.EPOCH, to);
|
||||
|
||||
List<ClubEvent> result = new ArrayList<>(events);
|
||||
for (var event : allEvents) {
|
||||
if (event.isRecurring() && event.getStartAt().isBefore(from)) {
|
||||
result.addAll(expandRecurring(event, from, to));
|
||||
}
|
||||
}
|
||||
|
||||
result.sort(Comparator.comparing(ClubEvent::getStartAt));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* List upcoming events (next 30 days) for portal widget.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<ClubEvent> listUpcomingEvents(int limit) {
|
||||
UUID tenantId = TenantContext.getCurrentTenant();
|
||||
return eventRepository.findByTenantIdAndStartAtAfterOrderByStartAtAsc(
|
||||
tenantId, Instant.now(), PageRequest.of(0, limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update an RSVP. Enforces maxAttendees for ACCEPTED status.
|
||||
* Returns the RSVP or throws if event is full.
|
||||
*/
|
||||
public EventRsvp rsvp(UUID eventId, UUID memberId, RsvpStatus status) {
|
||||
var event = eventRepository.findById(eventId)
|
||||
.orElseThrow(() -> new NoSuchElementException("Event not found: " + eventId));
|
||||
|
||||
// Enforce max attendees for ACCEPTED
|
||||
if (status == RsvpStatus.ACCEPTED && event.getMaxAttendees() != null) {
|
||||
long currentAccepted = rsvpRepository.countByEventIdAndStatus(eventId, RsvpStatus.ACCEPTED);
|
||||
// Check if this member already had ACCEPTED (update case)
|
||||
var existing = rsvpRepository.findByEventIdAndMemberId(eventId, memberId);
|
||||
boolean alreadyAccepted = existing.isPresent() && existing.get().getStatus() == RsvpStatus.ACCEPTED;
|
||||
if (!alreadyAccepted && currentAccepted >= event.getMaxAttendees()) {
|
||||
throw new IllegalStateException("EVENT_FULL");
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert RSVP
|
||||
var rsvp = rsvpRepository.findByEventIdAndMemberId(eventId, memberId)
|
||||
.orElseGet(() -> new EventRsvp(event, memberId, status));
|
||||
rsvp.setStatus(status);
|
||||
rsvp.setRespondedAt(Instant.now());
|
||||
|
||||
return rsvpRepository.save(rsvp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all RSVPs for an event.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<EventRsvp> getAttendees(UUID eventId) {
|
||||
return rsvpRepository.findByEventId(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get RSVP counts by status for an event.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Map<RsvpStatus, Long> getAttendeeCounts(UUID eventId) {
|
||||
Map<RsvpStatus, Long> counts = new HashMap<>();
|
||||
for (RsvpStatus status : RsvpStatus.values()) {
|
||||
counts.put(status, rsvpRepository.countByEventIdAndStatus(eventId, status));
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate RFC 5545 iCal (.ics) content for an event.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public String generateIcal(UUID eventId) {
|
||||
var event = eventRepository.findById(eventId)
|
||||
.orElseThrow(() -> new NoSuchElementException("Event not found: " + eventId));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.append("BEGIN:VCALENDAR\r\n");
|
||||
sb.append("VERSION:2.0\r\n");
|
||||
sb.append("PRODID:-//CannaManage//Events//EN\r\n");
|
||||
sb.append("BEGIN:VEVENT\r\n");
|
||||
sb.append("UID:").append(event.getId()).append("@cannamanage.de\r\n");
|
||||
sb.append("DTSTART:").append(formatIcalDate(event.getStartAt())).append("\r\n");
|
||||
if (event.getEndAt() != null) {
|
||||
sb.append("DTEND:").append(formatIcalDate(event.getEndAt())).append("\r\n");
|
||||
}
|
||||
sb.append("SUMMARY:").append(escapeIcal(event.getTitle())).append("\r\n");
|
||||
if (event.getDescription() != null) {
|
||||
sb.append("DESCRIPTION:").append(escapeIcal(event.getDescription())).append("\r\n");
|
||||
}
|
||||
if (event.getLocation() != null) {
|
||||
sb.append("LOCATION:").append(escapeIcal(event.getLocation())).append("\r\n");
|
||||
}
|
||||
if (event.isRecurring() && event.getRecurrenceRule() != null) {
|
||||
sb.append("RRULE:FREQ=").append(toIcalFreq(event.getRecurrenceRule()));
|
||||
if (event.getRecurrenceRule() == RecurrenceRule.BIWEEKLY) {
|
||||
sb.append(";INTERVAL=2");
|
||||
}
|
||||
if (event.getRecurrenceEndDate() != null) {
|
||||
sb.append(";UNTIL=").append(event.getRecurrenceEndDate().format(DateTimeFormatter.BASIC_ISO_DATE)).append("T235959Z");
|
||||
}
|
||||
sb.append("\r\n");
|
||||
}
|
||||
sb.append("DTSTAMP:").append(formatIcalDate(Instant.now())).append("\r\n");
|
||||
sb.append("END:VEVENT\r\n");
|
||||
sb.append("END:VCALENDAR\r\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a recurring event into virtual occurrences within the given date range.
|
||||
*/
|
||||
List<ClubEvent> expandRecurring(ClubEvent event, Instant from, Instant to) {
|
||||
if (!event.isRecurring() || event.getRecurrenceRule() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<ClubEvent> occurrences = new ArrayList<>();
|
||||
ZoneId zone = ZoneId.of("Europe/Berlin");
|
||||
LocalDateTime baseStart = LocalDateTime.ofInstant(event.getStartAt(), zone);
|
||||
Duration duration = event.getEndAt() != null
|
||||
? Duration.between(event.getStartAt(), event.getEndAt())
|
||||
: Duration.ZERO;
|
||||
|
||||
LocalDate endDate = event.getRecurrenceEndDate() != null
|
||||
? event.getRecurrenceEndDate()
|
||||
: baseStart.toLocalDate().plusYears(1);
|
||||
|
||||
LocalDateTime cursor = baseStart;
|
||||
while (true) {
|
||||
cursor = advanceCursor(cursor, event.getRecurrenceRule());
|
||||
Instant occurrenceStart = cursor.atZone(zone).toInstant();
|
||||
|
||||
if (occurrenceStart.isAfter(to)) break;
|
||||
if (cursor.toLocalDate().isAfter(endDate)) break;
|
||||
|
||||
if (occurrenceStart.isAfter(from) || occurrenceStart.equals(from)) {
|
||||
// Create a virtual (non-persisted) event representing this occurrence
|
||||
ClubEvent virtual = new ClubEvent(
|
||||
event.getClubId(), event.getTitle(), event.getDescription(),
|
||||
event.getEventType(), occurrenceStart,
|
||||
duration.isZero() ? null : occurrenceStart.plus(duration),
|
||||
event.getLocation(), event.getMaxAttendees(), event.getCreatedBy()
|
||||
);
|
||||
virtual.setId(event.getId()); // Same ID for reference
|
||||
virtual.setRecurring(true);
|
||||
virtual.setRecurrenceRule(event.getRecurrenceRule());
|
||||
occurrences.add(virtual);
|
||||
}
|
||||
}
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
// --- Private helpers ---
|
||||
|
||||
private LocalDateTime advanceCursor(LocalDateTime cursor, RecurrenceRule rule) {
|
||||
return switch (rule) {
|
||||
case WEEKLY -> cursor.plusWeeks(1);
|
||||
case BIWEEKLY -> cursor.plusWeeks(2);
|
||||
case MONTHLY -> cursor.plusMonths(1);
|
||||
};
|
||||
}
|
||||
|
||||
private String formatIcalDate(Instant instant) {
|
||||
return ICAL_DATE_FORMAT.format(instant.atZone(ZoneOffset.UTC));
|
||||
}
|
||||
|
||||
private String escapeIcal(String text) {
|
||||
return text.replace("\\", "\\\\")
|
||||
.replace(",", "\\,")
|
||||
.replace(";", "\\;")
|
||||
.replace("\n", "\\n");
|
||||
}
|
||||
|
||||
private String toIcalFreq(RecurrenceRule rule) {
|
||||
return switch (rule) {
|
||||
case WEEKLY, BIWEEKLY -> "WEEKLY";
|
||||
case MONTHLY -> "MONTHLY";
|
||||
};
|
||||
}
|
||||
|
||||
private String formatEventNotification(ClubEvent event) {
|
||||
var sb = new StringBuilder("Neue Veranstaltung: ");
|
||||
sb.append(event.getTitle());
|
||||
if (event.getLocation() != null) {
|
||||
sb.append(" — ").append(event.getLocation());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String formatEventInfoBoardContent(ClubEvent event) {
|
||||
var sb = new StringBuilder();
|
||||
sb.append("<p><strong>").append(event.getTitle()).append("</strong></p>");
|
||||
sb.append("<p>📅 ").append(formatGermanDate(event.getStartAt()));
|
||||
if (event.getEndAt() != null) {
|
||||
sb.append(" – ").append(formatGermanDate(event.getEndAt()));
|
||||
}
|
||||
sb.append("</p>");
|
||||
if (event.getLocation() != null) {
|
||||
sb.append("<p>📍 ").append(event.getLocation()).append("</p>");
|
||||
}
|
||||
if (event.getDescription() != null) {
|
||||
sb.append("<p>").append(event.getDescription()).append("</p>");
|
||||
}
|
||||
if (event.getMaxAttendees() != null) {
|
||||
sb.append("<p>Max. Teilnehmer: ").append(event.getMaxAttendees()).append("</p>");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String formatGermanDate(Instant instant) {
|
||||
return DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
|
||||
.format(instant.atZone(ZoneId.of("Europe/Berlin")));
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package de.cannamanage.service.repository;
|
||||
|
||||
import de.cannamanage.domain.entity.ClubEvent;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface ClubEventRepository extends JpaRepository<ClubEvent, UUID> {
|
||||
|
||||
List<ClubEvent> findByTenantIdAndStartAtBetweenOrderByStartAtAsc(UUID tenantId, Instant from, Instant to);
|
||||
|
||||
List<ClubEvent> findByTenantIdAndStartAtAfterOrderByStartAtAsc(UUID tenantId, Instant after, Pageable pageable);
|
||||
|
||||
List<ClubEvent> findByStartAtBetweenAndReminderSentFalse(Instant from, Instant to);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package de.cannamanage.service.repository;
|
||||
|
||||
import de.cannamanage.domain.entity.EventRsvp;
|
||||
import de.cannamanage.domain.enums.RsvpStatus;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface EventRsvpRepository extends JpaRepository<EventRsvp, UUID> {
|
||||
|
||||
List<EventRsvp> findByEventId(UUID eventId);
|
||||
|
||||
Optional<EventRsvp> findByEventIdAndMemberId(UUID eventId, UUID memberId);
|
||||
|
||||
long countByEventIdAndStatus(UUID eventId, RsvpStatus status);
|
||||
|
||||
List<EventRsvp> findByEventIdAndStatusIn(UUID eventId, List<RsvpStatus> statuses);
|
||||
}
|
||||
Reference in New Issue
Block a user