diff --git a/src/app/(applicant)/applicant/page.tsx b/src/app/(applicant)/applicant/page.tsx
index 0f5fee8..19ba69b 100644
--- a/src/app/(applicant)/applicant/page.tsx
+++ b/src/app/(applicant)/applicant/page.tsx
@@ -20,6 +20,7 @@ import { MentoringRequestCard } from '@/components/applicant/mentoring-request-c
import { MentorConversationCard } from '@/components/applicant/mentor-conversation-card'
import { AttendingMembersCard } from '@/components/applicant/attending-members-card'
import { LunchBanner } from '@/components/applicant/lunch-banner'
+import { ExternalAttendeesStrip } from '@/components/applicant/external-attendees-strip'
import { AnimatedCard } from '@/components/shared/animated-container'
import { ProjectLogoUpload } from '@/components/shared/project-logo-upload'
import { Progress } from '@/components/ui/progress'
@@ -407,6 +408,9 @@ export default function ApplicantDashboardPage() {
{/* Lunch banner (auto-hides when lunch event disabled or unconfigured) */}
+ {/* External lunch attendees attached to this team (auto-hides if none) */}
+
+
{/* Grand finale attendee roster (auto-hides until confirmation status is CONFIRMED) */}
diff --git a/src/components/applicant/external-attendees-strip.tsx b/src/components/applicant/external-attendees-strip.tsx
new file mode 100644
index 0000000..fae22a5
--- /dev/null
+++ b/src/components/applicant/external-attendees-strip.tsx
@@ -0,0 +1,25 @@
+'use client'
+
+import { trpc } from '@/lib/trpc/client'
+import { Card, CardContent } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { UsersRound } from 'lucide-react'
+
+export function ExternalAttendeesStrip({ projectId }: { projectId: string }) {
+ const { data } = trpc.lunch.getProjectExternals.useQuery({ projectId })
+ if (!data || data.length === 0) return null
+ return (
+
+
+
+ External attendees joining your team:
+ {data.map((e) => (
+
+ {e.name}
+ {e.roleNote ? ` (${e.roleNote})` : ''}
+
+ ))}
+
+
+ )
+}
diff --git a/src/server/routers/lunch.ts b/src/server/routers/lunch.ts
index d58a74f..e4c2c94 100644
--- a/src/server/routers/lunch.ts
+++ b/src/server/routers/lunch.ts
@@ -398,6 +398,29 @@ export const lunchRouter = router({
return { ...event, changeDeadline }
}),
+ /**
+ * Read-only list of project-attached externals for a project. Visible to
+ * any team member of the project (so they know who's joining their lunch).
+ */
+ getProjectExternals: protectedProcedure
+ .input(z.object({ projectId: z.string() }))
+ .query(async ({ ctx, input }) => {
+ const userId = ctx.user.id
+ const role = ctx.user.role
+ const isAdmin = role === 'SUPER_ADMIN' || role === 'PROGRAM_ADMIN'
+ if (!isAdmin) {
+ const tm = await ctx.prisma.teamMember.findFirst({
+ where: { projectId: input.projectId, userId },
+ })
+ if (!tm) throw new TRPCError({ code: 'FORBIDDEN' })
+ }
+ return ctx.prisma.externalAttendee.findMany({
+ where: { projectId: input.projectId },
+ include: { dish: true },
+ orderBy: { createdAt: 'asc' },
+ })
+ }),
+
/**
* All picks for the caller's team. Within-team transparency: every team
* member sees their teammates' picks (lunch picks aren't sensitive).
diff --git a/tests/unit/lunch-router.test.ts b/tests/unit/lunch-router.test.ts
index 501806e..4b95aaf 100644
--- a/tests/unit/lunch-router.test.ts
+++ b/tests/unit/lunch-router.test.ts
@@ -414,6 +414,49 @@ describe('lunch.exportManifestCsv', () => {
})
})
+describe('lunch.getProjectExternals', () => {
+ it('returns project-attached externals to a team member', async () => {
+ const program = await createTestProgram({ name: `pe-ok-${uid()}` })
+ programIds.push(program.id)
+ const lead = await createTestUser('APPLICANT')
+ userIds.push(lead.id)
+ const project = await createTestProject(program.id, {
+ title: `pe-${uid()}`, competitionCategory: 'STARTUP',
+ })
+ await prisma.teamMember.create({
+ data: { projectId: project.id, userId: lead.id, role: 'LEAD' },
+ })
+ const event = await prisma.lunchEvent.create({
+ data: { programId: program.id, enabled: true },
+ })
+ await prisma.externalAttendee.create({
+ data: { lunchEventId: event.id, projectId: project.id, name: 'Sponsor X' },
+ })
+ const caller = createCaller(lunchRouter, {
+ id: lead.id, email: lead.email, role: 'APPLICANT',
+ })
+ const result = await caller.getProjectExternals({ projectId: project.id })
+ expect(result).toHaveLength(1)
+ expect(result[0].name).toBe('Sponsor X')
+ })
+
+ it('rejects strangers', async () => {
+ const program = await createTestProgram({ name: `pe-rej-${uid()}` })
+ programIds.push(program.id)
+ const stranger = await createTestUser('APPLICANT')
+ userIds.push(stranger.id)
+ const project = await createTestProject(program.id, {
+ title: `pe-rej-${uid()}`, competitionCategory: 'STARTUP',
+ })
+ const caller = createCaller(lunchRouter, {
+ id: stranger.id, email: stranger.email, role: 'APPLICANT',
+ })
+ await expect(
+ caller.getProjectExternals({ projectId: project.id }),
+ ).rejects.toThrow()
+ })
+})
+
describe('lunch.updateEvent', () => {
it('patches an arbitrary subset of fields', async () => {
const program = await createTestProgram({ name: `lunch-upd-${uid()}` })