feat: add parent announcements and family or child event responses

This commit is contained in:
t.indorf
2026-10-09 21:44:19 +02:00
parent 2d3b82eb80
commit 979f795341
29 changed files with 2031 additions and 472 deletions
+143
View File
@@ -0,0 +1,143 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import {
AnnouncementAudience as A,
EventParticipationStatus as S,
RsvpMode,
TerminType,
UserRole,
} from "@prisma/client";
import {
announcementRecipientWhere,
visibleAnnouncementAudiences,
} from "../src/lib/announcement-access";
import {
hasMissingRsvp,
rsvpSubmissionSchema,
suggestedRsvpMode,
type RsvpEventDto,
} from "../src/lib/rsvp";
const family = {
eventId: "event",
mode: RsvpMode.FAMILY,
status: S.ATTENDING,
adultCount: 2,
};
test("Familienzusage braucht eine positive ganze Erwachsenenanzahl", () => {
assert.equal(rsvpSubmissionSchema.safeParse(family).success, true);
for (const adultCount of [0, -1, 1.5, 2147483648, NaN]) {
assert.equal(
rsvpSubmissionSchema.safeParse({ ...family, adultCount }).success,
false,
);
}
});
test("Familienabsage setzt Erwachsenenanzahl auf null", () => {
assert.equal(
rsvpSubmissionSchema.safeParse({
...family,
status: S.NOT_ATTENDING,
adultCount: 0,
}).success,
true,
);
assert.equal(
rsvpSubmissionSchema.safeParse({ ...family, status: S.NOT_ATTENDING })
.success,
false,
);
});
test("Kinderantworten dürfen verschiedene Zusagen enthalten, keine Duplikate oder leere Liste", () => {
const payload = {
eventId: "event",
mode: RsvpMode.CHILD,
responses: [
{ childId: "a", status: S.ATTENDING },
{ childId: "b", status: S.NOT_ATTENDING },
],
};
assert.equal(rsvpSubmissionSchema.safeParse(payload).success, true);
assert.equal(
rsvpSubmissionSchema.safeParse({ ...payload, responses: [] }).success,
false,
);
assert.equal(
rsvpSubmissionSchema.safeParse({
...payload,
responses: [payload.responses[0], payload.responses[0]],
}).success,
false,
);
});
test("Eltern sehen Elternmeldungen und gemeinsame Meldungen", () => {
assert.deepEqual(
visibleAnnouncementAudiences({ role: UserRole.ELTERN, familyId: "f" }),
[A.ALLE, A.ELTERN],
);
assert.deepEqual(
visibleAnnouncementAudiences({ role: UserRole.KOORDINATOR, familyId: "f" }),
[A.ALLE, A.ELTERN],
);
});
test("Erzieherkonto erhält auch mit Haushaltszuordnung keine Elternmeldungen", () => {
assert.deepEqual(
visibleAnnouncementAudiences({ role: UserRole.ERZIEHER, familyId: "f" }),
[A.ALLE, A.ERZIEHER],
);
});
test("Admin darf alle Meldungen verwalten", () => {
assert.deepEqual(
new Set(visibleAnnouncementAudiences({ role: UserRole.ADMIN })),
new Set(Object.values(A)),
);
});
test("E-Mail-Gruppen schließen Erzieher aus Elternempfängern aus", () => {
assert.deepEqual(announcementRecipientWhere(A.ERZIEHER), {
role: UserRole.ERZIEHER,
});
assert.deepEqual(announcementRecipientWhere(A.ELTERN), {
role: { not: UserRole.ERZIEHER },
OR: [{ familyId: { not: null } }, { role: UserRole.ELTERN }],
});
});
test("Die Rückmeldekarte verschwindet erst, wenn jedes Kind geantwortet hat", () => {
const event: RsvpEventDto = {
id: "e",
title: "Ausflug",
startDate: "2030-01-01",
rsvpDeadline: null,
mode: RsvpMode.CHILD,
canReply: true,
familyResponse: null,
children: [
{ id: "a", name: "A", status: S.ATTENDING },
{ id: "b", name: "B", status: null },
],
};
assert.equal(hasMissingRsvp(event), true);
assert.equal(
hasMissingRsvp({
...event,
children: event.children.map((child) => ({
...child,
status: S.NOT_ATTENDING,
})),
}),
false,
);
assert.equal(hasMissingRsvp({ ...event, mode: RsvpMode.FAMILY }), true);
assert.equal(
hasMissingRsvp({
...event,
mode: RsvpMode.FAMILY,
familyResponse: { status: S.NOT_ATTENDING, adultCount: 0 },
}),
false,
);
});
test("Antwortart wird passend zur Terminart vorgeschlagen", () => {
assert.equal(suggestedRsvpMode(TerminType.ELTERNABEND), RsvpMode.FAMILY);
assert.equal(suggestedRsvpMode(TerminType.SONSTIGES), RsvpMode.CHILD);
});