feat: manage waitlist and remove group members
- Add group-waitlist module (list/promote/remove entries) - Remove attendee with optional atomic promotion from waitlist - Block removal when attendee has payments (attendee_has_payments) - Waitlist detail modal matching members UI; optimistic add-to-waitlist - Responsive tweaks for the members/waitlist tabs
This commit is contained in:
83
apps/backend/src/modules/group-waitlist/lib/promote.ts
Normal file
83
apps/backend/src/modules/group-waitlist/lib/promote.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { PrismaClient } from '@generated/prisma/client';
|
||||
import type { AttendeeDto, ProblemDetails, Result } from '@gruperly/shared';
|
||||
import { err, ok } from '@gruperly/shared';
|
||||
import { capacityReachedProblem, conflictProblem } from '@/http/problem-builders';
|
||||
import { type AttendeeRecord, toAttendeeDto } from '@/modules/attendees/lib/helpers';
|
||||
import { type GroupWaitlistEntryRecord } from './helpers';
|
||||
|
||||
export type PromoteDb = Pick<PrismaClient, 'attendee' | 'groupWaitlistEntry'>;
|
||||
|
||||
type PromoteGroup = {
|
||||
id: string;
|
||||
capacity: number | null;
|
||||
};
|
||||
|
||||
export async function findFirstPendingWaitlistEntry(
|
||||
db: PromoteDb,
|
||||
groupId: string,
|
||||
): Promise<GroupWaitlistEntryRecord | null> {
|
||||
const record = await db.groupWaitlistEntry.findFirst({
|
||||
where: { groupId, status: 'PENDING' },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
return record as GroupWaitlistEntryRecord | null;
|
||||
}
|
||||
|
||||
export async function promoteWaitlistEntryRecord(
|
||||
db: PromoteDb,
|
||||
group: PromoteGroup,
|
||||
entry: GroupWaitlistEntryRecord,
|
||||
): Promise<Result<AttendeeDto, ProblemDetails>> {
|
||||
if (group.capacity !== null) {
|
||||
const currentCount = await db.attendee.count({ where: { groupId: group.id } });
|
||||
if (currentCount >= group.capacity) {
|
||||
return err(capacityReachedProblem(group.capacity));
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await db.attendee.findFirst({
|
||||
where: { groupId: group.id, phone: entry.phone },
|
||||
});
|
||||
if (existing) {
|
||||
return err(
|
||||
conflictProblem({
|
||||
detail: 'Ya existe un alumno registrado con este número de teléfono en este grupo.',
|
||||
code: 'attendee_already_registered',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const attendee = await db.attendee.create({
|
||||
data: {
|
||||
groupId: group.id,
|
||||
fullName: entry.fullName,
|
||||
phone: entry.phone,
|
||||
email: entry.email,
|
||||
notes: entry.notes,
|
||||
},
|
||||
});
|
||||
|
||||
return ok(toAttendeeDto(attendee as unknown as AttendeeRecord));
|
||||
}
|
||||
|
||||
export type PromoteFirstResult = {
|
||||
attendee: AttendeeDto;
|
||||
entry: GroupWaitlistEntryRecord;
|
||||
};
|
||||
|
||||
export async function promoteFirstPendingWaitlistEntry(
|
||||
db: PromoteDb,
|
||||
group: PromoteGroup,
|
||||
): Promise<Result<PromoteFirstResult | null, ProblemDetails>> {
|
||||
const entry = await findFirstPendingWaitlistEntry(db, group.id);
|
||||
if (!entry) {
|
||||
return ok(null);
|
||||
}
|
||||
|
||||
const result = await promoteWaitlistEntryRecord(db, group, entry);
|
||||
if (!result.ok) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return ok({ attendee: result.value, entry });
|
||||
}
|
||||
Reference in New Issue
Block a user