- {meeting.partAssignments
+ {meeting.eventParts
.filter(p => meeting.userPartIds.includes(p.id))
.map(part => {
const roleLabel = part.viewerRole === 'reader' ? partReaderLabel(part) : partSpeakerLabel(part)
@@ -365,8 +365,8 @@ function NextMeetingCard({ meeting }: { meeting: Awaited
)
})}
- {meeting.serviceRoleAssignments
- .filter(r => meeting.userServiceRoleIds.includes(r.id))
+ {meeting.eventServiceParts
+ .filter(r => meeting.userServicePartIds.includes(r.id))
.map(role => (
{role.name}
diff --git a/app/features/dashboard/server/dashboard.integration.test.ts b/app/features/dashboard/server/dashboard.integration.test.ts
index 426ca695..687c1fc5 100644
--- a/app/features/dashboard/server/dashboard.integration.test.ts
+++ b/app/features/dashboard/server/dashboard.integration.test.ts
@@ -134,7 +134,7 @@ beforeAll(async () => {
})
// Future event with programme assignments
- const eventKind = await tx.programmeTemplate.create({
+ const eventKind = await tx.eventTemplate.create({
data: { name: 'Midweek', key: `midweek-${ts}`, color: '#00aa00', congregationId },
})
@@ -150,7 +150,7 @@ beforeAll(async () => {
},
})
- await tx.programmePartAssignment.create({
+ await tx.eventPart.create({
data: {
eventId: futureEvent.id,
assigneeId: aliceId,
@@ -163,7 +163,7 @@ beforeAll(async () => {
},
})
- await tx.programmePartAssignment.create({
+ await tx.eventPart.create({
data: {
eventId: futureEvent.id,
assigneeId: bobId,
@@ -174,7 +174,7 @@ beforeAll(async () => {
},
})
- await tx.programmeServiceRoleAssignment.create({
+ await tx.eventServicePart.create({
data: {
eventId: futureEvent.id,
assigneeId: bobId,
@@ -200,10 +200,10 @@ beforeAll(async () => {
afterAll(async () => {
await withScope(congregationId, async tx => {
- await tx.programmeServiceRoleAssignment.deleteMany({ where: { congregationId } })
- await tx.programmePartAssignment.deleteMany({ where: { congregationId } })
+ await tx.eventServicePart.deleteMany({ where: { congregationId } })
+ await tx.eventPart.deleteMany({ where: { congregationId } })
await tx.event.deleteMany({ where: { congregationId } })
- await tx.programmeTemplate.deleteMany({ where: { congregationId } })
+ await tx.eventTemplate.deleteMany({ where: { congregationId } })
await tx.boardDocument.deleteMany({ where: { congregationId } })
await tx.boardSection.deleteMany({ where: { congregationId } })
await tx.attribution.deleteMany({ where: { congregationId } })
@@ -220,7 +220,7 @@ afterAll(async () => {
const { getUserTerritories, getRecentDocuments, getUnreadDocumentCount, getNextMeeting, getConflictingAssignments } =
await import('./dashboard.server')
const { getResponsibleConflicts } = await import('./get-responsible-conflicts.server')
-const { refreshConflictFlags } = await import('~/features/events/server/programme-assignments.server')
+const { refreshConflictFlags } = await import('~/features/events/server/event-part-assignments.server')
// --- Tests ---
@@ -268,14 +268,14 @@ describe('getNextMeeting (integration)', () => {
const result = await withScope(congregationId, tx => getNextMeeting(tx, aliceId))
expect(result).not.toBeNull()
expect(result?.name).toContain('Future Meeting')
- expect(result?.partAssignments.length).toBeGreaterThanOrEqual(2)
- expect(result?.serviceRoleAssignments.length).toBeGreaterThanOrEqual(1)
+ expect(result?.eventParts.length).toBeGreaterThanOrEqual(2)
+ expect(result?.eventServiceParts.length).toBeGreaterThanOrEqual(1)
})
it('identifies parts assigned to the user (as assignee)', async () => {
const result = await withScope(congregationId, tx => getNextMeeting(tx, aliceId))
expect(result?.userPartIds).toHaveLength(1)
- const userPart = result?.partAssignments.find(p => result.userPartIds.includes(p.id))
+ const userPart = result?.eventParts.find(p => result.userPartIds.includes(p.id))
expect(userPart?.name).toBe('Talk')
})
@@ -283,7 +283,7 @@ describe('getNextMeeting (integration)', () => {
const result = await withScope(congregationId, tx => getNextMeeting(tx, bobId))
// Bob is assistant on Talk and assignee on Reading
expect(result?.userPartIds).toHaveLength(2)
- expect(result?.userServiceRoleIds).toHaveLength(1)
+ expect(result?.userServicePartIds).toHaveLength(1)
})
it('does not return past events', async () => {
@@ -293,7 +293,7 @@ describe('getNextMeeting (integration)', () => {
it("ignores another user's day off when picking the next meeting", async () => {
const offEventId = await withScope(congregationId, async tx => {
- const offKind = await tx.programmeTemplate.create({
+ const offKind = await tx.eventTemplate.create({
data: { name: 'Absence', key: 'day-off', color: '#888888', congregationId },
})
const off = await tx.event.create({
@@ -316,7 +316,7 @@ describe('getNextMeeting (integration)', () => {
} finally {
await withScope(congregationId, async tx => {
await tx.event.delete({ where: { id_congregationId: { id: offEventId, congregationId } } })
- await tx.programmeTemplate.deleteMany({ where: { key: 'day-off', congregationId } })
+ await tx.eventTemplate.deleteMany({ where: { key: 'day-off', congregationId } })
})
}
})
@@ -327,8 +327,8 @@ describe('getConflictingAssignments (integration)', () => {
name: string
startDate: Date
endDate?: Date
- parts?: { assigneeId?: number | null; assistantId?: number | null; hasConflict?: boolean; name?: string }[]
- serviceRoles?: { assigneeId: number; hasConflict?: boolean; name?: string }[]
+ eventParts?: { assigneeId?: number | null; assistantId?: number | null; hasConflict?: boolean; name?: string }[]
+ eventServiceParts?: { assigneeId: number; hasConflict?: boolean; name?: string }[]
cong?: number
createdById?: number
}
@@ -336,7 +336,7 @@ describe('getConflictingAssignments (integration)', () => {
function seedEvent(opts: SeedOpts) {
const cong = opts.cong ?? congregationId
return withScope(cong, async tx => {
- const eventKind = await tx.programmeTemplate.findFirstOrThrow({
+ const eventKind = await tx.eventTemplate.findFirstOrThrow({
where: { congregationId: cong, key: { not: 'day-off' } },
})
const event = await tx.event.create({
@@ -354,8 +354,8 @@ describe('getConflictingAssignments (integration)', () => {
},
})
const partIds = await Promise.all(
- (opts.parts ?? []).map(async (p, i) => {
- const created = await tx.programmePartAssignment.create({
+ (opts.eventParts ?? []).map(async (p, i) => {
+ const created = await tx.eventPart.create({
data: {
eventId: event.id,
assigneeId: p.assigneeId ?? null,
@@ -370,9 +370,9 @@ describe('getConflictingAssignments (integration)', () => {
return created.id
}),
)
- const serviceRoleIds = await Promise.all(
- (opts.serviceRoles ?? []).map(async sr => {
- const created = await tx.programmeServiceRoleAssignment.create({
+ const servicePartIds = await Promise.all(
+ (opts.eventServiceParts ?? []).map(async sr => {
+ const created = await tx.eventServicePart.create({
data: {
eventId: event.id,
assigneeId: sr.assigneeId,
@@ -384,14 +384,14 @@ describe('getConflictingAssignments (integration)', () => {
return created.id
}),
)
- return { eventId: event.id, partIds, serviceRoleIds }
+ return { eventId: event.id, partIds, servicePartIds }
})
}
async function cleanupEvent(eventId: number, cong: number = congregationId) {
await withScope(cong, async tx => {
- await tx.programmeServiceRoleAssignment.deleteMany({ where: { eventId, congregationId: cong } })
- await tx.programmePartAssignment.deleteMany({ where: { eventId, congregationId: cong } })
+ await tx.eventServicePart.deleteMany({ where: { eventId, congregationId: cong } })
+ await tx.eventPart.deleteMany({ where: { eventId, congregationId: cong } })
await tx.event.delete({ where: { id_congregationId: { id: eventId, congregationId: cong } } })
})
}
@@ -400,7 +400,7 @@ describe('getConflictingAssignments (integration)', () => {
const seeded = await seedEvent({
name: 'Assignee Conflict',
startDate: new Date('2027-08-01T19:00:00Z'),
- parts: [{ assigneeId: aliceId, hasConflict: true, name: 'Discours' }],
+ eventParts: [{ assigneeId: aliceId, hasConflict: true, name: 'Discours' }],
})
try {
const result = await withScope(congregationId, tx => getConflictingAssignments(tx, aliceId))
@@ -417,7 +417,7 @@ describe('getConflictingAssignments (integration)', () => {
const seeded = await seedEvent({
name: 'Assistant Conflict',
startDate: new Date('2027-08-02T19:00:00Z'),
- parts: [{ assigneeId: bobId, assistantId: aliceId, hasConflict: true, name: 'Lecture' }],
+ eventParts: [{ assigneeId: bobId, assistantId: aliceId, hasConflict: true, name: 'Lecture' }],
})
try {
const result = await withScope(congregationId, tx => getConflictingAssignments(tx, aliceId))
@@ -432,12 +432,12 @@ describe('getConflictingAssignments (integration)', () => {
const seeded = await seedEvent({
name: 'Service Role Conflict',
startDate: new Date('2027-08-03T19:00:00Z'),
- serviceRoles: [{ assigneeId: aliceId, hasConflict: true, name: 'Son' }],
+ eventServiceParts: [{ assigneeId: aliceId, hasConflict: true, name: 'Son' }],
})
try {
const result = await withScope(congregationId, tx => getConflictingAssignments(tx, aliceId))
expect(result?.kind).toBe('service-role')
- expect(result?.id).toBe(seeded.serviceRoleIds[0])
+ expect(result?.id).toBe(seeded.servicePartIds[0])
expect(result?.name).toBe('Son')
} finally {
await cleanupEvent(seeded.eventId)
@@ -448,18 +448,18 @@ describe('getConflictingAssignments (integration)', () => {
const later = await seedEvent({
name: 'Later Part',
startDate: new Date('2027-09-15T19:00:00Z'),
- parts: [{ assigneeId: aliceId, hasConflict: true }],
+ eventParts: [{ assigneeId: aliceId, hasConflict: true }],
})
const earlier = await seedEvent({
name: 'Earlier Service Role',
startDate: new Date('2027-09-01T19:00:00Z'),
- serviceRoles: [{ assigneeId: aliceId, hasConflict: true, name: 'Early Sound' }],
+ eventServiceParts: [{ assigneeId: aliceId, hasConflict: true, name: 'Early Sound' }],
})
try {
const result = await withScope(congregationId, tx => getConflictingAssignments(tx, aliceId))
expect(result?.kind).toBe('service-role')
expect(result?.name).toBe('Early Sound')
- expect(result?.id).toBe(earlier.serviceRoleIds[0])
+ expect(result?.id).toBe(earlier.servicePartIds[0])
} finally {
await cleanupEvent(earlier.eventId)
await cleanupEvent(later.eventId)
@@ -470,8 +470,8 @@ describe('getConflictingAssignments (integration)', () => {
const seeded = await seedEvent({
name: 'No Conflict',
startDate: new Date('2027-10-01T19:00:00Z'),
- parts: [{ assigneeId: aliceId, hasConflict: false }],
- serviceRoles: [{ assigneeId: aliceId, hasConflict: false }],
+ eventParts: [{ assigneeId: aliceId, hasConflict: false }],
+ eventServiceParts: [{ assigneeId: aliceId, hasConflict: false }],
})
try {
const result = await withScope(congregationId, tx => getConflictingAssignments(tx, aliceId))
@@ -486,7 +486,7 @@ describe('getConflictingAssignments (integration)', () => {
name: 'Past Conflict',
startDate: new Date('2024-01-01T19:00:00Z'),
endDate: new Date('2024-01-01T21:00:00Z'),
- parts: [{ assigneeId: aliceId, hasConflict: true }],
+ eventParts: [{ assigneeId: aliceId, hasConflict: true }],
})
try {
const result = await withScope(congregationId, tx => getConflictingAssignments(tx, aliceId))
@@ -503,7 +503,7 @@ describe('getConflictingAssignments (integration)', () => {
})
const otherSeed = await withScope(otherCong.id, async tx => {
- const otherKind = await tx.programmeTemplate.create({
+ const otherKind = await tx.eventTemplate.create({
data: { name: 'Midweek', key: `midweek-other-${otherTs}`, color: '#00aa00', congregationId: otherCong.id },
})
const otherMember = await tx.member.create({
@@ -530,7 +530,7 @@ describe('getConflictingAssignments (integration)', () => {
},
})
// Same numeric id range as Alice — guarantees that without RLS the other-cong row would match
- const part = await tx.programmePartAssignment.create({
+ const part = await tx.eventPart.create({
data: {
eventId: event.id,
assigneeId: otherMember.id,
@@ -561,7 +561,7 @@ describe('getConflictingAssignments (integration)', () => {
expect(aliceFromHerCong).toBeNull()
} finally {
await withScope(otherCong.id, async tx => {
- await tx.programmePartAssignment.delete({
+ await tx.eventPart.delete({
where: { id_congregationId: { id: otherSeed.partId, congregationId: otherCong.id } },
})
await tx.event.delete({
@@ -569,7 +569,7 @@ describe('getConflictingAssignments (integration)', () => {
})
await tx.userAccount.delete({ where: { id: otherSeed.otherAccountId } })
await tx.member.delete({ where: { id: otherSeed.otherMemberId } })
- await tx.programmeTemplate.delete({
+ await tx.eventTemplate.delete({
where: { id_congregationId: { id: otherSeed.otherKindId, congregationId: otherCong.id } },
})
})
@@ -584,7 +584,7 @@ describe('getConflictingAssignments (integration)', () => {
// for templated events or stops touching one of the two assignment tables.
it('refreshConflictFlags clears stale hasConflict and the alert disappears', async () => {
const setup = await withScope(congregationId, async tx => {
- const template = await tx.programmeTemplate.create({
+ const template = await tx.eventTemplate.create({
data: {
name: 'Invariant Template',
key: `invariant-template-${ts}`,
@@ -603,7 +603,7 @@ describe('getConflictingAssignments (integration)', () => {
},
})
// Pre-set the flag to true to simulate a stale conflict left over from a now-deleted day-off.
- const part = await tx.programmePartAssignment.create({
+ const part = await tx.eventPart.create({
data: {
eventId: event.id,
assigneeId: aliceId,
@@ -614,7 +614,7 @@ describe('getConflictingAssignments (integration)', () => {
congregationId,
},
})
- const serviceRole = await tx.programmeServiceRoleAssignment.create({
+ const servicePart = await tx.eventServicePart.create({
data: {
eventId: event.id,
assigneeId: aliceId,
@@ -623,7 +623,7 @@ describe('getConflictingAssignments (integration)', () => {
congregationId,
},
})
- return { templateId: template.id, eventId: event.id, partId: part.id, serviceRoleId: serviceRole.id }
+ return { templateId: template.id, eventId: event.id, partId: part.id, servicePartId: servicePart.id }
})
try {
@@ -641,14 +641,14 @@ describe('getConflictingAssignments (integration)', () => {
)
const partFlag = await withScope(congregationId, tx =>
- tx.programmePartAssignment.findUniqueOrThrow({
+ tx.eventPart.findUniqueOrThrow({
where: { id_congregationId: { id: setup.partId, congregationId } },
select: { hasConflict: true },
}),
)
const serviceFlag = await withScope(congregationId, tx =>
- tx.programmeServiceRoleAssignment.findUniqueOrThrow({
- where: { id_congregationId: { id: setup.serviceRoleId, congregationId } },
+ tx.eventServicePart.findUniqueOrThrow({
+ where: { id_congregationId: { id: setup.servicePartId, congregationId } },
select: { hasConflict: true },
}),
)
@@ -659,14 +659,14 @@ describe('getConflictingAssignments (integration)', () => {
expect(after).toBeNull()
} finally {
await withScope(congregationId, async tx => {
- await tx.programmeServiceRoleAssignment.delete({
- where: { id_congregationId: { id: setup.serviceRoleId, congregationId } },
+ await tx.eventServicePart.delete({
+ where: { id_congregationId: { id: setup.servicePartId, congregationId } },
})
- await tx.programmePartAssignment.delete({
+ await tx.eventPart.delete({
where: { id_congregationId: { id: setup.partId, congregationId } },
})
await tx.event.delete({ where: { id_congregationId: { id: setup.eventId, congregationId } } })
- await tx.programmeTemplate.delete({
+ await tx.eventTemplate.delete({
where: { id_congregationId: { id: setup.templateId, congregationId } },
})
})
@@ -680,7 +680,7 @@ describe('getConflictingAssignments (integration)', () => {
// stale hasConflict and the alert disappears" pin earlier in this file.
it('getResponsibleConflicts drops the entry when hasConflict clears on the underlying assignment', async () => {
const setup = await withScope(congregationId, async tx => {
- const template = await tx.programmeTemplate.create({
+ const template = await tx.eventTemplate.create({
data: {
name: 'Responsible Invariant Template',
key: `resp-invariant-template-${ts}`,
@@ -688,7 +688,7 @@ describe('getConflictingAssignments (integration)', () => {
},
})
// Bob is the responsible for this template; Alice is the absentee.
- await tx.programmeTemplateResponsible.create({
+ await tx.templateResponsible.create({
data: {
templateId: template.id,
userId: bobAccountId,
@@ -706,7 +706,7 @@ describe('getConflictingAssignments (integration)', () => {
status: 'released',
},
})
- const part = await tx.programmePartAssignment.create({
+ const part = await tx.eventPart.create({
data: {
eventId: event.id,
assigneeId: aliceId,
@@ -729,7 +729,7 @@ describe('getConflictingAssignments (integration)', () => {
// Simulate resolution: the underlying assignment is no longer in conflict
// (either the absence went away or the assignment was reassigned).
await withScope(congregationId, tx =>
- tx.programmePartAssignment.update({
+ tx.eventPart.update({
where: { id_congregationId: { id: setup.partId, congregationId } },
data: { hasConflict: false },
}),
@@ -739,12 +739,12 @@ describe('getConflictingAssignments (integration)', () => {
expect(after).toEqual({ count: 0, absenteeNames: [], totalAbsenteesCount: 0 })
} finally {
await withScope(congregationId, async tx => {
- await tx.programmePartAssignment.delete({
+ await tx.eventPart.delete({
where: { id_congregationId: { id: setup.partId, congregationId } },
})
await tx.event.delete({ where: { id_congregationId: { id: setup.eventId, congregationId } } })
- await tx.programmeTemplateResponsible.deleteMany({ where: { templateId: setup.templateId } })
- await tx.programmeTemplate.delete({
+ await tx.templateResponsible.deleteMany({ where: { templateId: setup.templateId } })
+ await tx.eventTemplate.delete({
where: { id_congregationId: { id: setup.templateId, congregationId } },
})
})
@@ -768,7 +768,7 @@ describe('getConflictingAssignments (integration)', () => {
status: 'released',
},
})
- const part = await tx.programmePartAssignment.create({
+ const part = await tx.eventPart.create({
data: {
eventId: event.id,
assigneeId: aliceId,
@@ -793,7 +793,7 @@ describe('getConflictingAssignments (integration)', () => {
expect(asManager.absenteeNames).toEqual(['Alice Dupont'])
} finally {
await withScope(congregationId, async tx => {
- await tx.programmePartAssignment.delete({
+ await tx.eventPart.delete({
where: { id_congregationId: { id: setup.partId, congregationId } },
})
await tx.event.delete({ where: { id_congregationId: { id: setup.eventId, congregationId } } })
diff --git a/app/features/dashboard/server/dashboard.server.test.ts b/app/features/dashboard/server/dashboard.server.test.ts
index d3a9fce4..4fb23ad2 100644
--- a/app/features/dashboard/server/dashboard.server.test.ts
+++ b/app/features/dashboard/server/dashboard.server.test.ts
@@ -6,8 +6,8 @@ vi.mock('~/shared/infra/db.server', () => ({
boardDocument: { findMany: vi.fn(), count: vi.fn() },
boardDynamicDocumentSettings: { findMany: vi.fn(), count: vi.fn() },
event: { findFirst: vi.fn(), findMany: vi.fn() },
- programmePartAssignment: { findMany: vi.fn() },
- programmeServiceRoleAssignment: { findMany: vi.fn() },
+ eventPart: { findMany: vi.fn() },
+ eventServicePart: { findMany: vi.fn() },
role: { findMany: vi.fn() },
},
}))
@@ -22,7 +22,6 @@ const {
getUnreadDocumentCount,
getNextMeeting,
getUpcomingAbsences,
- getUpcomingAssignments,
getConflictingAssignments,
} = await import('./dashboard.server')
const { unscopedDb: db } = await import('~/shared/infra/db.server')
@@ -147,7 +146,7 @@ describe('getNextMeeting', () => {
startDate: new Date(2026, 3, 25),
endDate: new Date(2026, 3, 25),
template: { name: 'Midweek', color: '#000' },
- partAssignments: [
+ eventParts: [
{
id: 10,
name: 'Talk',
@@ -171,7 +170,7 @@ describe('getNextMeeting', () => {
assistant: null,
},
],
- serviceRoleAssignments: [
+ eventServiceParts: [
{ id: 20, name: 'Sound', assignee: { id: 42, firstname: 'John', lastname: 'Doe' } },
{ id: 21, name: 'Stage', assignee: { id: 50, firstname: 'Bob', lastname: 'Brown' } },
],
@@ -180,10 +179,10 @@ describe('getNextMeeting', () => {
const result = await getNextMeeting(db, 42)
expect(result).not.toBeNull()
expect(result?.userPartIds).toEqual([10])
- expect(result?.userServiceRoleIds).toEqual([20])
+ expect(result?.userServicePartIds).toEqual([20])
// Viewer is the assignee on part 10 → speaker. Part 11 belongs to someone
// else so the viewer has no role there.
- const parts = result?.partAssignments ?? []
+ const parts = result?.eventParts ?? []
expect(parts.find(p => p.id === 10)?.viewerRole).toBe('speaker')
expect(parts.find(p => p.id === 11)?.viewerRole).toBeNull()
})
@@ -195,7 +194,7 @@ describe('getNextMeeting', () => {
startDate: new Date(2026, 3, 25),
endDate: new Date(2026, 3, 25),
template: null,
- partAssignments: [
+ eventParts: [
{
id: 10,
name: 'Study',
@@ -211,12 +210,12 @@ describe('getNextMeeting', () => {
assistant: { id: 42, firstname: 'John', lastname: 'Doe' },
},
],
- serviceRoleAssignments: [],
+ eventServiceParts: [],
} as never)
const result = await getNextMeeting(db, 42)
expect(result?.userPartIds).toEqual([10])
- expect(result?.partAssignments[0].viewerRole).toBe('reader')
+ expect(result?.eventParts[0].viewerRole).toBe('reader')
})
// Locks the shape: the Prisma select MUST project speakerLabel and readerLabel
@@ -231,7 +230,7 @@ describe('getNextMeeting', () => {
startDate: new Date(2026, 3, 25),
endDate: new Date(2026, 3, 25),
template: null,
- partAssignments: [
+ eventParts: [
{
id: 10,
name: 'Bible reading',
@@ -255,13 +254,13 @@ describe('getNextMeeting', () => {
assistant: null,
},
],
- serviceRoleAssignments: [],
+ eventServiceParts: [],
} as never)
const result = await getNextMeeting(db, 42)
- expect(result?.partAssignments[0]).toMatchObject({ speakerLabel: 'STUDENT-SENTINEL-42', readerLabel: null })
- expect(result?.partAssignments[1]).toMatchObject({
+ expect(result?.eventParts[0]).toMatchObject({ speakerLabel: 'STUDENT-SENTINEL-42', readerLabel: null })
+ expect(result?.eventParts[1]).toMatchObject({
speakerLabel: 'STUDENT-SENTINEL-99',
readerLabel: 'HOUSEHOLDER-SENTINEL-99',
})
@@ -269,8 +268,8 @@ describe('getNextMeeting', () => {
// Also assert the Prisma select requested the fields — a fixture that
// happened to include the sentinels would pass without this.
const call = vi.mocked(db.event.findFirst).mock.calls[0][0]
- const select = call?.select as { partAssignments?: { select?: Record } }
- expect(select.partAssignments?.select).toMatchObject({ speakerLabel: true, readerLabel: true })
+ const select = call?.select as { eventParts?: { select?: Record } }
+ expect(select.eventParts?.select).toMatchObject({ speakerLabel: true, readerLabel: true })
})
it('returns empty arrays when user has no assignments', async () => {
@@ -280,7 +279,7 @@ describe('getNextMeeting', () => {
startDate: new Date(2026, 3, 25),
endDate: new Date(2026, 3, 25),
template: null,
- partAssignments: [
+ eventParts: [
{
id: 10,
name: 'Talk',
@@ -291,12 +290,12 @@ describe('getNextMeeting', () => {
assistant: null,
},
],
- serviceRoleAssignments: [],
+ eventServiceParts: [],
} as never)
const result = await getNextMeeting(db, 42)
expect(result?.userPartIds).toEqual([])
- expect(result?.userServiceRoleIds).toEqual([])
+ expect(result?.userServicePartIds).toEqual([])
})
// The dashboard is publisher-facing. Drafts must not surface — same
@@ -368,37 +367,17 @@ describe('getUpcomingAbsences', () => {
})
})
-// --- getUpcomingAssignments: draft events hidden ---
-//
-// The publisher dashboard is a public view of the schedule; draft assignments
-// must not preview here or a publisher sees a mid-edit programme.
-
-describe('getUpcomingAssignments', () => {
- it('filters part and service-role assignments to released events', async () => {
- vi.mocked(db.programmePartAssignment.findMany).mockResolvedValue([] as never)
- vi.mocked(db.programmeServiceRoleAssignment.findMany).mockResolvedValue([] as never)
-
- await getUpcomingAssignments(db, 42)
-
- const [partCall] = vi.mocked(db.programmePartAssignment.findMany).mock.calls[0]
- expect((partCall as { where: { event: unknown } }).where.event).toMatchObject({ status: 'released' })
-
- const [serviceCall] = vi.mocked(db.programmeServiceRoleAssignment.findMany).mock.calls[0]
- expect((serviceCall as { where: { event: unknown } }).where.event).toMatchObject({ status: 'released' })
- })
-})
-
describe('getConflictingAssignments', () => {
it('only surfaces conflicts on released events', async () => {
- vi.mocked(db.programmePartAssignment.findMany).mockResolvedValue([] as never)
- vi.mocked(db.programmeServiceRoleAssignment.findMany).mockResolvedValue([] as never)
+ vi.mocked(db.eventPart.findMany).mockResolvedValue([] as never)
+ vi.mocked(db.eventServicePart.findMany).mockResolvedValue([] as never)
await getConflictingAssignments(db, 42)
- const [partCall] = vi.mocked(db.programmePartAssignment.findMany).mock.calls[0]
+ const [partCall] = vi.mocked(db.eventPart.findMany).mock.calls[0]
expect((partCall as { where: { event: unknown } }).where.event).toMatchObject({ status: 'released' })
- const [serviceCall] = vi.mocked(db.programmeServiceRoleAssignment.findMany).mock.calls[0]
+ const [serviceCall] = vi.mocked(db.eventServicePart.findMany).mock.calls[0]
expect((serviceCall as { where: { event: unknown } }).where.event).toMatchObject({ status: 'released' })
})
})
diff --git a/app/features/dashboard/server/dashboard.server.ts b/app/features/dashboard/server/dashboard.server.ts
index e2e7a711..ee2ddded 100644
--- a/app/features/dashboard/server/dashboard.server.ts
+++ b/app/features/dashboard/server/dashboard.server.ts
@@ -1,5 +1,5 @@
// Intentional cross-feature import: dashboard aggregates data from events and the board for the overview
-import { EventStatus, ProgrammeTemplateKey } from '~/features/events'
+import { EventStatus, EventTemplateKey } from '~/features/events'
import { getNextDaysOffs } from '~/features/events/index.server'
import { resolveEffectiveRoleIds } from '~/shared/auth/permissions.server'
import { TWO_WEEKS_MS } from '~/shared/constants/limits'
@@ -165,96 +165,11 @@ export async function getUpcomingAbsences(db: TransactionClient, userId: number,
return { upcoming, shouldNudge }
}
-export async function getUpcomingAssignments(db: TransactionClient, userId: number) {
- const now = new Date()
-
- const [partAssignments, serviceRoleAssignments] = await Promise.all([
- db.programmePartAssignment.findMany({
- where: {
- OR: [{ assigneeId: userId }, { assistantId: userId }],
- // Drafts are the manager's scratch space — never previewed to
- // publishers.
- event: { startDate: { gte: now }, status: EventStatus.Released },
- },
- select: {
- id: true,
- name: true,
- topic: true,
- assigneeId: true,
- assistantId: true,
- event: {
- select: {
- name: true,
- startDate: true,
- },
- },
- },
- orderBy: { event: { startDate: 'asc' } },
- take: DASHBOARD_RECENT_ITEMS_LIMIT,
- }),
- db.programmeServiceRoleAssignment.findMany({
- where: {
- assigneeId: userId,
- event: { startDate: { gte: now }, status: EventStatus.Released },
- },
- select: {
- id: true,
- name: true,
- event: {
- select: {
- name: true,
- startDate: true,
- },
- },
- },
- orderBy: { event: { startDate: 'asc' } },
- take: DASHBOARD_RECENT_ITEMS_LIMIT,
- }),
- ])
-
- type Assignment = {
- kind: 'part' | 'service-role'
- id: number
- name: string
- topic?: string | null
- role: 'speaker' | 'assistant' | 'service'
- eventName: string
- eventDate: Date
- }
-
- const assignments: Assignment[] = [
- ...partAssignments.map(
- (a): Assignment => ({
- kind: 'part',
- id: a.id,
- name: a.name,
- topic: a.topic,
- role: a.assigneeId === userId ? 'speaker' : 'assistant',
- eventName: a.event.name,
- eventDate: a.event.startDate,
- }),
- ),
- ...serviceRoleAssignments.map(
- (a): Assignment => ({
- kind: 'service-role',
- id: a.id,
- name: a.name,
- role: 'service',
- eventName: a.event.name,
- eventDate: a.event.startDate,
- }),
- ),
- ]
-
- assignments.sort((a, b) => a.eventDate.getTime() - b.eventDate.getTime())
- return assignments.slice(0, 5)
-}
-
export async function getConflictingAssignments(db: TransactionClient, userId: number) {
const now = new Date()
const [partConflicts, serviceConflicts] = await Promise.all([
- db.programmePartAssignment.findMany({
+ db.eventPart.findMany({
where: {
hasConflict: true,
OR: [{ assigneeId: userId }, { assistantId: userId }],
@@ -271,7 +186,7 @@ export async function getConflictingAssignments(db: TransactionClient, userId: n
orderBy: { event: { startDate: 'asc' } },
take: 1,
}),
- db.programmeServiceRoleAssignment.findMany({
+ db.eventServicePart.findMany({
where: {
hasConflict: true,
assigneeId: userId,
@@ -318,7 +233,7 @@ export async function getNextMeeting(db: TransactionClient, userId: number) {
// NOT: { template: {...} } instead of template: { key: { not } } — the
// second form inner-joins through template and silently drops null-
// template rows, which older legacy events might still carry.
- NOT: { template: { key: ProgrammeTemplateKey.DayOff } },
+ NOT: { template: { key: EventTemplateKey.DayOff } },
// Publisher-facing dashboard — drafts must stay hidden.
status: EventStatus.Released,
},
@@ -328,7 +243,7 @@ export async function getNextMeeting(db: TransactionClient, userId: number) {
startDate: true,
endDate: true,
template: { select: { name: true, color: true } },
- partAssignments: {
+ eventParts: {
select: {
id: true,
name: true,
@@ -342,7 +257,7 @@ export async function getNextMeeting(db: TransactionClient, userId: number) {
},
orderBy: { order: 'asc' },
},
- serviceRoleAssignments: {
+ eventServiceParts: {
select: {
id: true,
name: true,
@@ -359,19 +274,19 @@ export async function getNextMeeting(db: TransactionClient, userId: number) {
// engineer it. `null` means "viewer has no role on this part" — the UI
// filters on userPartIds so nulls never render, but keeping the field
// present makes the shape uniform and typed.
- const partAssignments = event.partAssignments.map(p => ({
+ const eventParts = event.eventParts.map(p => ({
...p,
viewerRole:
p.assignee?.id === userId ? ('speaker' as const) : p.assistant?.id === userId ? ('reader' as const) : null,
}))
- const userPartIds = new Set(partAssignments.filter(p => p.viewerRole !== null).map(p => p.id))
- const userServiceRoleIds = new Set(event.serviceRoleAssignments.filter(r => r.assignee?.id === userId).map(r => r.id))
+ const userPartIds = new Set(eventParts.filter(p => p.viewerRole !== null).map(p => p.id))
+ const userServicePartIds = new Set(event.eventServiceParts.filter(r => r.assignee?.id === userId).map(r => r.id))
return {
...event,
- partAssignments,
+ eventParts,
userPartIds: [...userPartIds],
- userServiceRoleIds: [...userServiceRoleIds],
+ userServicePartIds: [...userServicePartIds],
}
}
diff --git a/app/features/dashboard/server/get-responsible-conflicts.server.test.ts b/app/features/dashboard/server/get-responsible-conflicts.server.test.ts
index a0cd9261..b3015a51 100644
--- a/app/features/dashboard/server/get-responsible-conflicts.server.test.ts
+++ b/app/features/dashboard/server/get-responsible-conflicts.server.test.ts
@@ -2,8 +2,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('~/shared/infra/db.server', () => ({
unscopedDb: {
- programmePartAssignment: { findMany: vi.fn() },
- programmeServiceRoleAssignment: { findMany: vi.fn() },
+ eventPart: { findMany: vi.fn() },
+ eventServicePart: { findMany: vi.fn() },
},
}))
@@ -12,8 +12,8 @@ const { unscopedDb: db } = await import('~/shared/infra/db.server')
beforeEach(() => {
vi.resetAllMocks()
- vi.mocked(db.programmePartAssignment.findMany).mockResolvedValue([] as never)
- vi.mocked(db.programmeServiceRoleAssignment.findMany).mockResolvedValue([] as never)
+ vi.mocked(db.eventPart.findMany).mockResolvedValue([] as never)
+ vi.mocked(db.eventServicePart.findMany).mockResolvedValue([] as never)
})
describe('getResponsibleConflicts', () => {
@@ -29,7 +29,7 @@ describe('getResponsibleConflicts', () => {
const userId = 100
await getResponsibleConflicts(db, userId, false)
- const partCall = vi.mocked(db.programmePartAssignment.findMany).mock.calls[0][0]
+ const partCall = vi.mocked(db.eventPart.findMany).mock.calls[0][0]
const partWhere = partCall?.where as Record
expect(partWhere.hasConflict).toBe(true)
expect(partWhere.event).toEqual({
@@ -43,7 +43,7 @@ describe('getResponsibleConflicts', () => {
const userId = 100
await getResponsibleConflicts(db, userId, false)
- const serviceCall = vi.mocked(db.programmeServiceRoleAssignment.findMany).mock.calls[0][0]
+ const serviceCall = vi.mocked(db.eventServicePart.findMany).mock.calls[0][0]
const serviceWhere = serviceCall?.where as Record
expect(serviceWhere.event).toEqual({
startDate: { gte: expect.any(Date) },
@@ -59,7 +59,7 @@ describe('getResponsibleConflicts', () => {
it('drops the template filter for ProgramManager users but keeps the released filter', async () => {
await getResponsibleConflicts(db, 100, true)
- const partCall = vi.mocked(db.programmePartAssignment.findMany).mock.calls[0][0]
+ const partCall = vi.mocked(db.eventPart.findMany).mock.calls[0][0]
const partWhere = partCall?.where as Record
expect(partWhere.event).toEqual({ startDate: { gte: expect.any(Date) }, status: 'released' })
expect(partWhere.event).not.toHaveProperty('template')
@@ -68,7 +68,7 @@ describe('getResponsibleConflicts', () => {
it('only considers upcoming events (startDate >= now)', async () => {
await getResponsibleConflicts(db, 100, true)
- const [partCall] = vi.mocked(db.programmePartAssignment.findMany).mock.calls[0]
+ const [partCall] = vi.mocked(db.eventPart.findMany).mock.calls[0]
const event = (partCall as { where: { event: { startDate: { gte: Date } } } }).where.event
expect(event.startDate.gte.getTime()).toBeGreaterThan(Date.now() - 5000)
})
@@ -78,7 +78,7 @@ describe('getResponsibleConflicts', () => {
// the same event) collapse to one conflict. Otherwise a member appearing
// twice double-counts the badge.
it('dedupes by (memberId, eventId) when computing count', async () => {
- vi.mocked(db.programmePartAssignment.findMany).mockResolvedValue([
+ vi.mocked(db.eventPart.findMany).mockResolvedValue([
{
eventId: 1,
assigneeId: 100,
@@ -101,7 +101,7 @@ describe('getResponsibleConflicts', () => {
})
it('counts one conflict per (member × event) — same member on two events counts twice', async () => {
- vi.mocked(db.programmePartAssignment.findMany).mockResolvedValue([
+ vi.mocked(db.eventPart.findMany).mockResolvedValue([
{
eventId: 1,
assigneeId: 100,
@@ -124,7 +124,7 @@ describe('getResponsibleConflicts', () => {
})
it('merges names across part and service assignments (deduped)', async () => {
- vi.mocked(db.programmePartAssignment.findMany).mockResolvedValue([
+ vi.mocked(db.eventPart.findMany).mockResolvedValue([
{
eventId: 1,
assigneeId: 100,
@@ -133,7 +133,7 @@ describe('getResponsibleConflicts', () => {
assistant: null,
},
] as never)
- vi.mocked(db.programmeServiceRoleAssignment.findMany).mockResolvedValue([
+ vi.mocked(db.eventServicePart.findMany).mockResolvedValue([
{ eventId: 2, assigneeId: 100, assignee: { firstname: 'Alice', lastname: 'Dupont' } },
] as never)
@@ -143,7 +143,7 @@ describe('getResponsibleConflicts', () => {
})
it('caps absenteeNames at 3, sorted alphabetically; count still reflects all conflicts', async () => {
- vi.mocked(db.programmePartAssignment.findMany).mockResolvedValue([
+ vi.mocked(db.eventPart.findMany).mockResolvedValue([
{
eventId: 1,
assigneeId: 100,
@@ -192,7 +192,7 @@ describe('getResponsibleConflicts', () => {
// related member could not be joined. Either way it must not corrupt the
// aggregation. This pin guards the `record()` null-skip at line 66.
it('silently skips rows whose assignee is null (no assignee booked or join failed)', async () => {
- vi.mocked(db.programmePartAssignment.findMany).mockResolvedValue([
+ vi.mocked(db.eventPart.findMany).mockResolvedValue([
{
eventId: 1,
assigneeId: null,
@@ -219,7 +219,7 @@ describe('getResponsibleConflicts', () => {
// potentially the absentee, so both are enumerated. Downstream the
// responsible sees them and can click through for detail.
it('includes both assignee and assistant of a part row', async () => {
- vi.mocked(db.programmePartAssignment.findMany).mockResolvedValue([
+ vi.mocked(db.eventPart.findMany).mockResolvedValue([
{
eventId: 1,
assigneeId: 100,
diff --git a/app/features/dashboard/server/get-responsible-conflicts.server.ts b/app/features/dashboard/server/get-responsible-conflicts.server.ts
index 26573c92..6f2976d5 100644
--- a/app/features/dashboard/server/get-responsible-conflicts.server.ts
+++ b/app/features/dashboard/server/get-responsible-conflicts.server.ts
@@ -18,7 +18,7 @@ const MAX_ABSENTEE_NAMES = 3
// Scoping:
// - non-manager → only events on templates where the user is the documented
-// responsible (ProgrammeTemplateResponsible.userId);
+// responsible (TemplateResponsible.userId);
// - ProgramManager → all events, including untemplated ones which have
// no responsible relation at all.
export async function getResponsibleConflicts(
@@ -35,7 +35,7 @@ export async function getResponsibleConflicts(
: { startDate: { gte: now }, status: EventStatus.Released, template: { responsibles: { some: { userId } } } }
const [partRows, serviceRows] = await Promise.all([
- db.programmePartAssignment.findMany({
+ db.eventPart.findMany({
where: { hasConflict: true, event: eventFilter },
select: {
eventId: true,
@@ -45,7 +45,7 @@ export async function getResponsibleConflicts(
assistant: { select: { firstname: true, lastname: true } },
},
}),
- db.programmeServiceRoleAssignment.findMany({
+ db.eventServicePart.findMany({
where: { hasConflict: true, event: eventFilter },
select: {
eventId: true,
diff --git a/app/features/dashboard/ui/build-urgent-items.test.ts b/app/features/dashboard/ui/build-urgent-items.test.ts
index f12ff5b6..ac80c521 100644
--- a/app/features/dashboard/ui/build-urgent-items.test.ts
+++ b/app/features/dashboard/ui/build-urgent-items.test.ts
@@ -20,7 +20,7 @@ const {
buildUrgentItems,
urgentTerritoriesItems,
urgentPartAssignmentItems,
- urgentServiceRoleItems,
+ urgentServicePartItems,
urgentDayoffConflictItems,
urgentResponsibleConflictItems,
urgentDocumentsItem,
@@ -56,15 +56,15 @@ type PartAssignment = {
assistant: Person | null
viewerRole: 'speaker' | 'reader' | null
}
-type ServiceRoleAssignment = { id: number; name: string; assignee: Person | null }
+type ServicePartAssignment = { id: number; name: string; assignee: Person | null }
function makeNextMeeting(
startDate: Date,
{
userPartIds = [] as number[],
- userServiceRoleIds = [] as number[],
- partAssignments = [] as PartAssignment[],
- serviceRoleAssignments = [] as ServiceRoleAssignment[],
+ userServicePartIds = [] as number[],
+ eventParts = [] as PartAssignment[],
+ eventServiceParts = [] as ServicePartAssignment[],
} = {},
) {
return {
@@ -73,10 +73,10 @@ function makeNextMeeting(
startDate,
endDate: new Date(startDate.getTime() + 2 * 60 * 60 * 1000), // +2h
template: { name: 'Midweek', color: '#000' } as { name: string; color: string } | null,
- partAssignments,
- serviceRoleAssignments,
+ eventParts,
+ eventServiceParts,
userPartIds,
- userServiceRoleIds,
+ userServicePartIds,
}
}
@@ -142,7 +142,7 @@ describe('urgentPartAssignmentItems', () => {
it('returns empty array when meeting is more than 3 days away', () => {
const meeting = makeNextMeeting(new Date(2026, 3, 28), {
userPartIds: [10],
- partAssignments: [
+ eventParts: [
{
id: 10,
name: 'Discours',
@@ -163,7 +163,7 @@ describe('urgentPartAssignmentItems', () => {
it('returns item when meeting is within 3 days', () => {
const meeting = makeNextMeeting(new Date(2026, 3, 26, 19, 0), {
userPartIds: [10],
- partAssignments: [
+ eventParts: [
{
id: 10,
name: 'Discours',
@@ -188,7 +188,7 @@ describe('urgentPartAssignmentItems', () => {
it('returns item when meeting is today', () => {
const meeting = makeNextMeeting(new Date(2026, 3, 24, 19, 0), {
userPartIds: [10],
- partAssignments: [
+ eventParts: [
{
id: 10,
name: 'Discours',
@@ -207,32 +207,32 @@ describe('urgentPartAssignmentItems', () => {
})
})
-// --- urgentServiceRoleItems ---
+// --- urgentServicePartItems ---
-describe('urgentServiceRoleItems', () => {
+describe('urgentServicePartItems', () => {
it('returns empty array for null meeting', () => {
- expect(urgentServiceRoleItems(null)).toEqual([])
+ expect(urgentServicePartItems(null)).toEqual([])
})
it('returns empty array when user has no service roles', () => {
- const meeting = makeNextMeeting(new Date(2026, 3, 25), { userServiceRoleIds: [] })
- expect(urgentServiceRoleItems(meeting)).toEqual([])
+ const meeting = makeNextMeeting(new Date(2026, 3, 25), { userServicePartIds: [] })
+ expect(urgentServicePartItems(meeting)).toEqual([])
})
it('returns empty array when meeting is more than 3 days away', () => {
const meeting = makeNextMeeting(new Date(2026, 3, 28), {
- userServiceRoleIds: [5],
- serviceRoleAssignments: [{ id: 5, name: 'Son', assignee: { id: 1, firstname: 'John', lastname: 'Doe' } }],
+ userServicePartIds: [5],
+ eventServiceParts: [{ id: 5, name: 'Son', assignee: { id: 1, firstname: 'John', lastname: 'Doe' } }],
})
- expect(urgentServiceRoleItems(meeting)).toEqual([])
+ expect(urgentServicePartItems(meeting)).toEqual([])
})
it('returns item with priority 3 when meeting is within 3 days', () => {
const meeting = makeNextMeeting(new Date(2026, 3, 25, 19, 0), {
- userServiceRoleIds: [5],
- serviceRoleAssignments: [{ id: 5, name: 'Son', assignee: { id: 1, firstname: 'John', lastname: 'Doe' } }],
+ userServicePartIds: [5],
+ eventServiceParts: [{ id: 5, name: 'Son', assignee: { id: 1, firstname: 'John', lastname: 'Doe' } }],
})
- const items = urgentServiceRoleItems(meeting)
+ const items = urgentServicePartItems(meeting)
expect(items).toHaveLength(1)
expect(items[0].priority).toBe(3)
expect(items[0].to).toBe('/board')
@@ -389,7 +389,7 @@ describe('buildUrgentItems', () => {
const territories = [makeTerritory(1, 'T-1', 'overdue', new Date(2026, 3, 20))]
const meeting = makeNextMeeting(new Date(2026, 3, 25, 19, 0), {
userPartIds: [10],
- partAssignments: [
+ eventParts: [
{
id: 10,
name: 'Discours',
@@ -413,8 +413,8 @@ describe('buildUrgentItems', () => {
const meetingDate = new Date(2026, 3, 25, 19, 0)
const meeting = makeNextMeeting(meetingDate, {
userPartIds: [10],
- userServiceRoleIds: [5],
- partAssignments: [
+ userServicePartIds: [5],
+ eventParts: [
{
id: 10,
name: 'Discours',
@@ -428,7 +428,7 @@ describe('buildUrgentItems', () => {
readerLabel: null,
},
],
- serviceRoleAssignments: [{ id: 5, name: 'Son', assignee: null }],
+ eventServiceParts: [{ id: 5, name: 'Son', assignee: null }],
})
const conflict = makeConflict(7, 'Discours public', meetingDate)
const items = buildUrgentItems(null, null, meeting, conflict, null)
diff --git a/app/features/dashboard/ui/build-urgent-items.ts b/app/features/dashboard/ui/build-urgent-items.ts
index 6681e5b1..f419467b 100644
--- a/app/features/dashboard/ui/build-urgent-items.ts
+++ b/app/features/dashboard/ui/build-urgent-items.ts
@@ -59,7 +59,7 @@ export function urgentPartAssignmentItems(nextMeeting: NextMeeting): UrgentItem[
if (!nextMeeting || nextMeeting.userPartIds.length === 0) return []
if (new Date(nextMeeting.startDate).getTime() - Date.now() > THREE_DAYS_MS) return []
- const userPart = nextMeeting.partAssignments.find(p => nextMeeting.userPartIds.includes(p.id))
+ const userPart = nextMeeting.eventParts.find(p => nextMeeting.userPartIds.includes(p.id))
if (!userPart) return []
return [
@@ -76,11 +76,11 @@ export function urgentPartAssignmentItems(nextMeeting: NextMeeting): UrgentItem[
]
}
-export function urgentServiceRoleItems(nextMeeting: NextMeeting): UrgentItem[] {
- if (!nextMeeting || nextMeeting.userServiceRoleIds.length === 0) return []
+export function urgentServicePartItems(nextMeeting: NextMeeting): UrgentItem[] {
+ if (!nextMeeting || nextMeeting.userServicePartIds.length === 0) return []
if (new Date(nextMeeting.startDate).getTime() - Date.now() > THREE_DAYS_MS) return []
- const userRole = nextMeeting.serviceRoleAssignments.find(r => nextMeeting.userServiceRoleIds.includes(r.id))
+ const userRole = nextMeeting.eventServiceParts.find(r => nextMeeting.userServicePartIds.includes(r.id))
if (!userRole) return []
return [
@@ -170,7 +170,7 @@ export function buildUrgentItems(
const items = [
...urgentTerritoriesItems(territories),
...urgentPartAssignmentItems(nextMeeting),
- ...urgentServiceRoleItems(nextMeeting),
+ ...urgentServicePartItems(nextMeeting),
...urgentDayoffConflictItems(dayoffConflict),
...urgentResponsibleConflictItems(responsibleConflicts),
...urgentDocumentsItem(unreadDocumentCount),
diff --git a/app/features/display-board/index.server.ts b/app/features/display-board/index.server.ts
index 330c70c3..860fc4cf 100644
--- a/app/features/display-board/index.server.ts
+++ b/app/features/display-board/index.server.ts
@@ -1,4 +1,4 @@
// Public server-only surface of the display-board feature.
+export { resolveProgrammeLink } from './server/event-link.server'
export { boardNotifications } from './server/notifications.server'
-export { resolveProgrammeLink } from './server/programme-link.server'
diff --git a/app/features/display-board/model/programme-display.test.ts b/app/features/display-board/model/event-display.test.ts
similarity index 99%
rename from app/features/display-board/model/programme-display.test.ts
rename to app/features/display-board/model/event-display.test.ts
index e6585870..0591709e 100644
--- a/app/features/display-board/model/programme-display.test.ts
+++ b/app/features/display-board/model/event-display.test.ts
@@ -5,7 +5,7 @@ vi.mock('~/i18n/paraglide/messages', () => ({
}))
const { formatName, formatAssigneeWithAssistant, nameMatches, getPartDisplay, partMatchesQuery } = await import(
- './programme-display'
+ './event-display'
)
function makeUser(firstname: string | null, lastname: string | null, anonymizedAt: Date | null = null) {
diff --git a/app/features/display-board/model/programme-display.ts b/app/features/display-board/model/event-display.ts
similarity index 100%
rename from app/features/display-board/model/programme-display.ts
rename to app/features/display-board/model/event-display.ts
diff --git a/app/features/display-board/routes/documents/new-dynamic.tsx b/app/features/display-board/routes/documents/new-dynamic.tsx
index 55aae8bf..630a1eb6 100644
--- a/app/features/display-board/routes/documents/new-dynamic.tsx
+++ b/app/features/display-board/routes/documents/new-dynamic.tsx
@@ -137,7 +137,7 @@ export async function action({ request, context }: Route.ActionArgs) {
// For programme documents, build default config with all templates selected
let dynamicConfig: ProgrammeDynamicConfig | undefined
if (dynamicType === DynamicType.Programme) {
- const templates = await db.programmeTemplate.findMany({
+ const templates = await db.eventTemplate.findMany({
where: { congregationId },
select: { id: true },
})
diff --git a/app/features/display-board/routes/dynamic/viewer.tsx b/app/features/display-board/routes/dynamic/viewer.tsx
index 46957b07..b4a23e9e 100644
--- a/app/features/display-board/routes/dynamic/viewer.tsx
+++ b/app/features/display-board/routes/dynamic/viewer.tsx
@@ -7,7 +7,7 @@ import {
getDynamicDocumentData,
markDynamicDocumentViewed,
} from '~/features/display-board/server/dynamic-documents.server'
-import { filterDynamicDataToEvent } from '~/features/display-board/server/programme-event-filter.server'
+import { filterDynamicDataToEvent } from '~/features/display-board/server/event-filter.server'
import { PioneersView } from '~/features/display-board/ui/dynamic/PioneersView'
import { ProgrammeView } from '~/features/display-board/ui/dynamic/ProgrammeView'
import { PublisherGroupsView } from '~/features/display-board/ui/dynamic/PublisherGroupsView'
diff --git a/app/features/display-board/schemas/dynamic-config.schema.ts b/app/features/display-board/schemas/dynamic-config.schema.ts
index 212b602b..06f2639a 100644
--- a/app/features/display-board/schemas/dynamic-config.schema.ts
+++ b/app/features/display-board/schemas/dynamic-config.schema.ts
@@ -1,13 +1,13 @@
import { z } from 'zod'
-const programmeTemplateConfigSchema = z.object({
+const eventTemplateConfigSchema = z.object({
templateId: z.number().int().positive(),
parts: z.boolean(),
services: z.boolean(),
})
export const programmeDynamicConfigSchema = z.object({
- templates: z.array(programmeTemplateConfigSchema),
+ templates: z.array(eventTemplateConfigSchema),
groupBy: z.enum(['date', 'template']).default('date'),
})
diff --git a/app/features/display-board/server/dynamic-documents.integration.test.ts b/app/features/display-board/server/dynamic-documents.integration.test.ts
index 70e17ea0..7f8051d8 100644
--- a/app/features/display-board/server/dynamic-documents.integration.test.ts
+++ b/app/features/display-board/server/dynamic-documents.integration.test.ts
@@ -80,7 +80,7 @@ beforeAll(async () => {
})
// Create programme template and future event
- const template = await tx.programmeTemplate.create({
+ const template = await tx.eventTemplate.create({
data: { name: `Midweek ${ts}`, key: `midweek-${ts}`, congregationId },
})
@@ -118,9 +118,9 @@ afterAll(async () => {
await tx.boardDynamicDocumentView.deleteMany({ where: { settings: { congregationId } } })
await tx.boardDynamicDocumentSettings.deleteMany({ where: { congregationId } })
await tx.boardSection.deleteMany({ where: { congregationId } })
- await tx.programmePartAssignment.deleteMany({ where: { congregationId } })
+ await tx.eventPart.deleteMany({ where: { congregationId } })
await tx.event.deleteMany({ where: { congregationId } })
- await tx.programmeTemplate.deleteMany({ where: { congregationId } })
+ await tx.eventTemplate.deleteMany({ where: { congregationId } })
await tx.publisherGroup.deleteMany({ where: { congregationId } })
await tx.userAccount.deleteMany({ where: { congregationId } })
await tx.member.deleteMany({ where: { congregationId } })
diff --git a/app/features/display-board/server/dynamic-documents.server.test.ts b/app/features/display-board/server/dynamic-documents.server.test.ts
index 621795ec..bfd35c69 100644
--- a/app/features/display-board/server/dynamic-documents.server.test.ts
+++ b/app/features/display-board/server/dynamic-documents.server.test.ts
@@ -20,7 +20,7 @@ const mockDb = {
findFirst: vi.fn(),
findMany: vi.fn(),
},
- programmePartAssignment: {
+ eventPart: {
findFirst: vi.fn(),
},
boardDynamicDocumentView: {
@@ -160,7 +160,7 @@ describe('getContentVersion', () => {
const eventDate = new Date('2026-04-10')
const assignmentDate = new Date('2026-04-18')
mockDb.event.findFirst.mockResolvedValue({ updatedAt: eventDate })
- mockDb.programmePartAssignment.findFirst.mockResolvedValue({ updatedAt: assignmentDate })
+ mockDb.eventPart.findFirst.mockResolvedValue({ updatedAt: assignmentDate })
const result = await getContentVersion(mockDb as never, 'programme', 'midweek', 10)
@@ -224,13 +224,13 @@ describe('getDynamicPreview programme draft filter', () => {
describe('getContentVersion programme draft filter', () => {
it('reads latest event / assignment updatedAt from released events only', async () => {
mockDb.event.findFirst.mockResolvedValue(null)
- mockDb.programmePartAssignment.findFirst.mockResolvedValue(null)
+ mockDb.eventPart.findFirst.mockResolvedValue(null)
await getContentVersion(mockDb as never, 'programme', 'midweek', 10)
const eventCall = mockDb.event.findFirst.mock.calls[0][0]
expect(eventCall.where).toMatchObject({ status: 'released' })
- const assignmentCall = mockDb.programmePartAssignment.findFirst.mock.calls[0][0]
+ const assignmentCall = mockDb.eventPart.findFirst.mock.calls[0][0]
expect(assignmentCall.where.event).toMatchObject({ status: 'released' })
})
})
diff --git a/app/features/display-board/server/dynamic-documents.server.ts b/app/features/display-board/server/dynamic-documents.server.ts
index 49aadfd1..8a2aaff4 100644
--- a/app/features/display-board/server/dynamic-documents.server.ts
+++ b/app/features/display-board/server/dynamic-documents.server.ts
@@ -65,7 +65,7 @@ export async function listAvailableDynamicTypes(
}
// Programme: always available (users can create multiple with different configs)
- const templateCount = await db.programmeTemplate.count({ where: { congregationId } })
+ const templateCount = await db.eventTemplate.count({ where: { congregationId } })
if (templateCount > 0) {
available.push({
dynamicType: DynamicType.Programme,
@@ -132,7 +132,7 @@ export async function getContentVersion(
orderBy: { updatedAt: 'desc' },
select: { updatedAt: true },
}),
- db.programmePartAssignment.findFirst({
+ db.eventPart.findFirst({
where: {
congregationId,
event: { templateId: { in: templateIds }, startDate: { gte: fromDate }, status: EventStatus.Released },
@@ -157,7 +157,7 @@ export async function getContentVersion(
orderBy: { updatedAt: 'desc' },
select: { updatedAt: true },
}),
- db.programmePartAssignment.findFirst({
+ db.eventPart.findFirst({
where: {
congregationId,
event: { template: { key: dynamicRef }, startDate: { gte: fromDate }, status: EventStatus.Released },
@@ -346,7 +346,7 @@ function fetchProgrammeByIds(
},
include: {
template: true,
- partAssignments: {
+ eventParts: {
orderBy: [{ order: 'asc' }, { trackOrder: { sort: 'asc', nulls: 'last' } }],
include: {
assignee: { select: userSelect },
@@ -354,7 +354,7 @@ function fetchProgrammeByIds(
externalSpeaker: { select: { name: true } },
},
},
- serviceRoleAssignments: includeServices ? { include: { assignee: { select: userSelect } } } : false,
+ eventServiceParts: includeServices ? { include: { assignee: { select: userSelect } } } : false,
},
orderBy: { startDate: 'asc' },
})
@@ -376,7 +376,7 @@ function fetchProgrammeByKey(
status: EventStatus.Released,
},
include: {
- partAssignments: {
+ eventParts: {
orderBy: [{ order: 'asc' }, { trackOrder: { sort: 'asc', nulls: 'last' } }],
include: {
assignee: { select: userSelect },
@@ -384,7 +384,7 @@ function fetchProgrammeByKey(
externalSpeaker: { select: { name: true } },
},
},
- serviceRoleAssignments: showServices ? { include: { assignee: { select: userSelect } } } : false,
+ eventServiceParts: showServices ? { include: { assignee: { select: userSelect } } } : false,
},
orderBy: { startDate: 'asc' },
})
diff --git a/app/features/display-board/server/programme-event-filter.server.test.ts b/app/features/display-board/server/event-filter.server.test.ts
similarity index 80%
rename from app/features/display-board/server/programme-event-filter.server.test.ts
rename to app/features/display-board/server/event-filter.server.test.ts
index 2b9f4e40..655622f3 100644
--- a/app/features/display-board/server/programme-event-filter.server.test.ts
+++ b/app/features/display-board/server/event-filter.server.test.ts
@@ -1,13 +1,13 @@
import { describe, expect, it } from 'vitest'
import { DynamicType } from '~/features/display-board/model/dynamic-document.type'
-import { filterDynamicDataToEvent } from './programme-event-filter.server'
+import { filterDynamicDataToEvent } from './event-filter.server'
const PROGRAMME_DATA = {
type: DynamicType.Programme,
events: [
- { id: 100, name: 'Meeting A', partAssignments: [], serviceRoleAssignments: [] },
- { id: 200, name: 'Meeting B', partAssignments: [], serviceRoleAssignments: [] },
- { id: 300, name: 'Meeting C', partAssignments: [], serviceRoleAssignments: [] },
+ { id: 100, name: 'Meeting A', parts: [], serviceParts: [] },
+ { id: 200, name: 'Meeting B', parts: [], serviceParts: [] },
+ { id: 300, name: 'Meeting C', parts: [], serviceParts: [] },
],
config: null,
templateKey: null,
diff --git a/app/features/display-board/server/programme-event-filter.server.ts b/app/features/display-board/server/event-filter.server.ts
similarity index 100%
rename from app/features/display-board/server/programme-event-filter.server.ts
rename to app/features/display-board/server/event-filter.server.ts
diff --git a/app/features/display-board/server/programme-link.server.integration.test.ts b/app/features/display-board/server/event-link.server.integration.test.ts
similarity index 95%
rename from app/features/display-board/server/programme-link.server.integration.test.ts
rename to app/features/display-board/server/event-link.server.integration.test.ts
index f64b6c27..2d8a10ca 100644
--- a/app/features/display-board/server/programme-link.server.integration.test.ts
+++ b/app/features/display-board/server/event-link.server.integration.test.ts
@@ -23,7 +23,7 @@ function withScope(congregationId: number, fn: (tx: Tx) => Promise): Promi
})
}
-const { resolveProgrammeLink } = await import('./programme-link.server')
+const { resolveProgrammeLink } = await import('./event-link.server')
const ts = Date.now()
let congId: number
@@ -57,11 +57,11 @@ beforeAll(async () => {
})
sectionId = section.id
- const templateA = await tx.programmeTemplate.create({
+ const templateA = await tx.eventTemplate.create({
data: { name: 'Weekly meeting', key: `weekly-meeting-${ts}`, congregationId: congId },
})
templateAId = templateA.id
- const templateB = await tx.programmeTemplate.create({
+ const templateB = await tx.eventTemplate.create({
data: { name: 'Public talk', key: `public-talk-${ts}`, congregationId: congId },
})
templateBId = templateB.id
@@ -96,7 +96,7 @@ afterAll(async () => {
await withScope(congId, async tx => {
await tx.boardDynamicDocumentSettings.deleteMany({})
await tx.event.deleteMany({})
- await tx.programmeTemplate.deleteMany({})
+ await tx.eventTemplate.deleteMany({})
await tx.boardSection.deleteMany({})
await tx.userAccount.deleteMany({})
})
diff --git a/app/features/display-board/server/programme-link.server.test.ts b/app/features/display-board/server/event-link.server.test.ts
similarity index 99%
rename from app/features/display-board/server/programme-link.server.test.ts
rename to app/features/display-board/server/event-link.server.test.ts
index d9f37c9b..3290c454 100644
--- a/app/features/display-board/server/programme-link.server.test.ts
+++ b/app/features/display-board/server/event-link.server.test.ts
@@ -12,7 +12,7 @@ vi.mock('~/shared/infra/logger.server', () => ({
createLogger: () => ({ debug: vi.fn(), info: vi.fn(), warn: warnSpy, error: vi.fn() }),
}))
-const { resolveProgrammeLink } = await import('./programme-link.server')
+const { resolveProgrammeLink } = await import('./event-link.server')
const { unscopedDb: db } = await import('~/shared/infra/db.server')
beforeEach(() => {
diff --git a/app/features/display-board/server/programme-link.server.ts b/app/features/display-board/server/event-link.server.ts
similarity index 98%
rename from app/features/display-board/server/programme-link.server.ts
rename to app/features/display-board/server/event-link.server.ts
index bc182541..fbbb7d12 100644
--- a/app/features/display-board/server/programme-link.server.ts
+++ b/app/features/display-board/server/event-link.server.ts
@@ -2,7 +2,7 @@ import { parseProgrammeConfig } from '~/features/display-board/model/dynamic-doc
import type { TransactionClient } from '~/shared/infra/db.server'
import { createLogger } from '~/shared/infra/logger.server'
-const logger = createLogger('programme-link')
+const logger = createLogger('event-link')
// Resolves the best-effort public URL where an assignee can view their upcoming
// event. Preferred target is a Programme dynamic document on the board that
diff --git a/app/features/display-board/ui/dynamic/ProgrammeView.tsx b/app/features/display-board/ui/dynamic/ProgrammeView.tsx
index 9e827c20..8e49dd3b 100644
--- a/app/features/display-board/ui/dynamic/ProgrammeView.tsx
+++ b/app/features/display-board/ui/dynamic/ProgrammeView.tsx
@@ -2,12 +2,7 @@ import type { LucideIcon } from 'lucide-react'
import { BookOpen, Calendar, ChevronRight, Gem, HeartHandshake } from 'lucide-react'
import { type RefObject, useEffect, useMemo, useRef, useState } from 'react'
import type { ProgrammeDynamicConfig } from '~/features/display-board/model/dynamic-document.type'
-import {
- formatName,
- getPartDisplay,
- nameMatches,
- partMatchesQuery,
-} from '~/features/display-board/model/programme-display'
+import { formatName, getPartDisplay, nameMatches, partMatchesQuery } from '~/features/display-board/model/event-display'
import { groupPartsBySlot } from '~/features/events'
import * as m from '~/i18n/paraglide/messages'
import { Badge } from '~/shared/ui/badge'
@@ -74,7 +69,7 @@ interface PartAssignment {
} | null
}
-interface ServiceRoleAssignment {
+interface ServicePartAssignment {
id: number
name: string
assignee?: {
@@ -90,8 +85,8 @@ interface ProgrammeEvent {
name: string
startDate: Date
templateId?: number | null
- partAssignments: PartAssignment[]
- serviceRoleAssignments?: ServiceRoleAssignment[]
+ eventParts: PartAssignment[]
+ eventServiceParts?: ServicePartAssignment[]
}
export interface ProgrammeViewData {
@@ -248,7 +243,7 @@ function ServiceSection({
hasParts,
query,
}: {
- services: ServiceRoleAssignment[]
+ services: ServicePartAssignment[]
hasParts: boolean
query: string
}) {
@@ -451,7 +446,7 @@ export function ProgrammeView({ events, showServices, config, highlightQuery, sc
const eventShowParts = templateConfig?.parts ?? true
const eventShowServices = templateConfig?.services ?? showServices
- const sections = groupPartsBySlot(event.partAssignments)
+ const sections = groupPartsBySlot(event.eventParts)
const isFirstOfWeek = firstEventIdPerWeek.has(event.id)
const weekKey = getWeekKey(new Date(event.startDate))
@@ -524,8 +519,8 @@ export function ProgrammeView({ events, showServices, config, highlightQuery, sc
})}
{/* Services */}
- {eventShowServices && event.serviceRoleAssignments && event.serviceRoleAssignments.length > 0 && (
-
+ {eventShowServices && event.eventServiceParts && event.eventServiceParts.length > 0 && (
+
)}