diff --git a/src/components/applicant/attending-members-card.tsx b/src/components/applicant/attending-members-card.tsx index 4951d37..620c5ff 100644 --- a/src/components/applicant/attending-members-card.tsx +++ b/src/components/applicant/attending-members-card.tsx @@ -12,9 +12,42 @@ import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { PlaneTakeoff, ShieldCheck, AlertTriangle } from 'lucide-react' import { EditAttendeesDialog } from './edit-attendees-dialog' +import type { VisaStatus } from '@prisma/client' + +const VISA_BADGE: Record< + VisaStatus, + { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline' } +> = { + NOT_NEEDED: { label: 'Visa not needed', variant: 'outline' }, + REQUESTED: { label: 'Visa requested', variant: 'secondary' }, + INVITATION_SENT: { label: 'Invitation sent', variant: 'secondary' }, + APPOINTMENT_BOOKED: { label: 'Appointment booked', variant: 'default' }, + GRANTED: { label: 'Visa granted', variant: 'default' }, + DENIED: { label: 'Visa denied', variant: 'destructive' }, +} + +function formatDateOnly(d: Date | string): string { + return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(new Date(d)) +} + +function nextVisaDate(v: { + invitationSentAt: Date | string | null + appointmentAt: Date | string | null + decisionAt: Date | string | null + status: VisaStatus +}): { label: string; date: Date | string } | null { + if (v.status === 'GRANTED' || v.status === 'DENIED') { + if (v.decisionAt) return { label: 'Decision', date: v.decisionAt } + return null + } + if (v.appointmentAt) return { label: 'Appointment', date: v.appointmentAt } + if (v.invitationSentAt) return { label: 'Invitation sent', date: v.invitationSentAt } + return null +} export function AttendingMembersCard() { const { data, isLoading } = trpc.applicant.getMyFinalistConfirmation.useQuery() + const { data: myVisas } = trpc.applicant.getMyVisaApplications.useQuery() if (isLoading) { return ( @@ -34,6 +67,9 @@ export function AttendingMembersCard() { const cutoffAt = data.cutoffAt ? new Date(data.cutoffAt) : null const userById = new Map(data.project.teamMembers.map((tm) => [tm.userId, tm.user])) const attendees = data.confirmation.attendingMembers + const visaByUser = new Map( + (myVisas ?? []).map((v) => [v.userId, v] as const), + ) const editDisabled = !data.editableNow const editDisabledReason = !data.editableNow @@ -92,6 +128,9 @@ export function AttendingMembersCard() { {attendees.map((a) => { const user = userById.get(a.userId) if (!user) return null + const visa = visaByUser.get(a.userId) + const visaBadge = visa ? VISA_BADGE[visa.status] : null + const next = visa ? nextVisaDate(visa) : null return (
  • {user.name ?? user.email}
    {user.email}
    - {a.needsVisa && ( - - - Visa support - - )} +
    + {visa && visaBadge ? ( + <> + + + {visaBadge.label} + + {next && ( + + {next.label}: {formatDateOnly(next.date)} + + )} + + ) : ( + a.needsVisa && ( + + + Visa support + + ) + )} +
  • ) })} diff --git a/src/server/routers/applicant.ts b/src/server/routers/applicant.ts index 28a7b5e..9f372b5 100644 --- a/src/server/routers/applicant.ts +++ b/src/server/routers/applicant.ts @@ -2814,6 +2814,7 @@ export const applicantRouter = router({ .map((a) => ({ id: a.visaApplication!.id, attendingMemberId: a.id, + userId: a.userId, status: a.visaApplication!.status, nationality: a.visaApplication!.nationality, invitationSentAt: a.visaApplication!.invitationSentAt,