0ffdc362b5
- Event + EventRsvp models + migration (RsvpStatus enum: GOING/MAYBE/NOT_GOING) - EventsModule: admin CRUD (create/update/delete/publish) + RSVP list - GET /my/events + POST /my/events/:id/rsvp in MyController/MyService - Admin BFF routes: GET/POST /api/admin/events, PATCH/DELETE /api/admin/events/[id] - Member BFF routes: GET /api/my/events, POST /api/my/events/[id]/rsvp - /my/events page: upcoming/past split, RSVP button (client component, optimistic) - Register DigestModule, OrgModule, ThreadsModule, EventsModule in AppModule - Add EVENT_CREATED/DELETED/UPDATED and DIGEST_SENT to AuditAction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
109 lines
3.3 KiB
TypeScript
109 lines
3.3 KiB
TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
import { PrismaService } from '../../prisma/prisma.service';
|
|
import { AuditService } from '../audit/audit.service';
|
|
import { AuditAction } from '../audit/audit.types';
|
|
|
|
@Injectable()
|
|
export class EventsService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly audit: AuditService,
|
|
) {}
|
|
|
|
async list(tenantId: string, includeUnpublished = false) {
|
|
return this.prisma.event.findMany({
|
|
where: {
|
|
tenantId,
|
|
...(includeUnpublished ? {} : { isPublished: true }),
|
|
},
|
|
orderBy: { startsAt: 'asc' },
|
|
include: { _count: { select: { rsvps: true } } },
|
|
});
|
|
}
|
|
|
|
async create(tenantId: string, adminId: string, dto: {
|
|
title: string;
|
|
description?: string;
|
|
location?: string;
|
|
startsAt: string;
|
|
endsAt?: string;
|
|
isPublished?: boolean;
|
|
}) {
|
|
const event = await this.prisma.event.create({
|
|
data: {
|
|
tenantId,
|
|
title: dto.title,
|
|
description: dto.description ?? null,
|
|
location: dto.location ?? null,
|
|
startsAt: new Date(dto.startsAt),
|
|
endsAt: dto.endsAt ? new Date(dto.endsAt) : null,
|
|
createdBy: adminId,
|
|
isPublished: dto.isPublished ?? false,
|
|
},
|
|
});
|
|
|
|
await this.audit.log({
|
|
tenantId,
|
|
actorId: adminId,
|
|
action: AuditAction.EVENT_CREATED,
|
|
resourceType: 'Event',
|
|
resourceId: event.id,
|
|
payload: { title: event.title, isPublished: event.isPublished },
|
|
});
|
|
|
|
return event;
|
|
}
|
|
|
|
async update(tenantId: string, adminId: string, eventId: string, dto: {
|
|
title?: string;
|
|
description?: string;
|
|
location?: string;
|
|
startsAt?: string;
|
|
endsAt?: string;
|
|
isPublished?: boolean;
|
|
}) {
|
|
const event = await this.prisma.event.findFirst({ where: { id: eventId, tenantId } });
|
|
if (!event) throw new NotFoundException('Event not found');
|
|
|
|
return this.prisma.event.update({
|
|
where: { id: eventId },
|
|
data: {
|
|
...(dto.title !== undefined && { title: dto.title }),
|
|
...(dto.description !== undefined && { description: dto.description }),
|
|
...(dto.location !== undefined && { location: dto.location }),
|
|
...(dto.startsAt !== undefined && { startsAt: new Date(dto.startsAt) }),
|
|
...(dto.endsAt !== undefined && { endsAt: new Date(dto.endsAt) }),
|
|
...(dto.isPublished !== undefined && { isPublished: dto.isPublished }),
|
|
},
|
|
});
|
|
}
|
|
|
|
async remove(tenantId: string, adminId: string, eventId: string) {
|
|
const event = await this.prisma.event.findFirst({ where: { id: eventId, tenantId } });
|
|
if (!event) throw new NotFoundException('Event not found');
|
|
await this.prisma.event.delete({ where: { id: eventId } });
|
|
|
|
await this.audit.log({
|
|
tenantId,
|
|
actorId: adminId,
|
|
action: AuditAction.EVENT_DELETED,
|
|
resourceType: 'Event',
|
|
resourceId: eventId,
|
|
payload: { deleted: true },
|
|
});
|
|
|
|
return { ok: true };
|
|
}
|
|
|
|
async getRsvps(tenantId: string, eventId: string) {
|
|
const event = await this.prisma.event.findFirst({ where: { id: eventId, tenantId } });
|
|
if (!event) throw new NotFoundException('Event not found');
|
|
|
|
return this.prisma.eventRsvp.findMany({
|
|
where: { eventId },
|
|
include: { user: { select: { id: true, displayName: true, jid: true } } },
|
|
orderBy: { createdAt: 'asc' },
|
|
});
|
|
}
|
|
}
|