The 'Unassigned' rooming option used value="" which throws at runtime
(blank tab behind the error boundary). Use a sentinel value mapped to unassign.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrites hotels-tab.tsx from the removed single-hotel getHotel/upsertHotel
pattern to the new multi-hotel API: Hotels section (list/add/edit/delete with
occupancy badge) + Rooming section (per-attendee hotel+room+dates assignment,
team-assign shortcut, CSV export).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the program-level hotel.findUnique (broken after removing @unique)
with the caller's HotelStay (include hotel) on their AttendingMember.
Returns hotel: {name,address,link,notes}|null and room: {roomNumber,
checkInAt,checkOutAt}|null. MyLogisticsCard renders the Room section
(number + Monaco-time check-in/out) when room is present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace getHotel/upsertHotel with listHotels/createHotel/updateHotel/deleteHotel
(multi-hotel per edition). Add listRooming, assignStay, assignTeamToHotel, and
unassignStay procedures for per-attendee room assignments. Update setFlightStatus
to include attendee's HotelStay in TRAVEL_CONFIRMED notification metadata.
Extend getTravelConfirmedTemplate to render room number and check-in/out dates.
All procedures are adminProcedure and audit-logged. 10 new unit tests green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Hotel.programId no longer unique (many hotels per edition)
- New HotelStay 1:1 with AttendingMember (hotelId, room, check-in/out)
- Program.hotel -> hotels[]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add updateMyVisaNationality mutation: finds the caller's AttendingMember where the program has visaStatusVisibleToMembers=true and a VisaApplication exists, updates VisaApplication.nationality, and emits a VISA_NATIONALITY_SELF_SET audit log. Throws NOT_FOUND when no eligible application exists. Tests: persists update; rejects caller without a visible visa app.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix getMyFinalistConfirmation to resolve project via OR [submittedByUserId, teamMembers] so a lead who submitted but has no TeamMember row can see their card.
- Add getMyLogistics query: returns projectTitle, confirmationStatus, hotel (program 1:1), myFlight (caller's AttendingMember.flightDetail), visaVisible flag, and myVisa when visible. Returns null for non-confirmed or unrelated callers.
- Tests: confirmed finalist sees hotel/flight/visa; non-finalist gets null; PENDING confirmation gets null.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract selectUnpickedAttendees helper with OR filter (is null OR pickedAt null)
to fix cron missing attendees with no MemberLunchPick row at all
- Update cron route to use the helper
- sendRecap now throws TRPCError on email failure instead of silently stamping success
- Add lunch.sendReminders adminProcedure for manual on-demand reminder sends
- Add "Send reminders now" AlertDialog button to LunchRecapActions
- Tests: lunch-reminder-filter.test.ts (2 new), all 5 lunch test files pass (40 tests)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a flight is set to CONFIRMED, fire a TRAVEL_CONFIRMED in-app
notification (+ email via the existing NotificationEmailSetting pipeline)
to the attending member. When a visa status changes to one of
INVITATION_SENT|APPOINTMENT_BOOKED|GRANTED|DENIED, fire a
VISA_STATUS_UPDATE notification. Both are best-effort (try/catch, never
throw inside the mutation). Admin notes are not forwarded to metadata.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add sendDueConfirmationReminders() to finalist-confirmation.ts: queries
PENDING confirmations with no reminderSentAt whose deadline is within the
per-program LIVE_FINAL round reminderHoursBeforeDeadline window (default 12h),
sends a FINALIST_REMINDER in-app notification (+ email via pipeline) to the
team LEAD, then stamps reminderSentAt for idempotency.
Wire into the finalist-confirmations cron route alongside expirePendingPastDeadline.
Also clear reminderSentAt on re-invite in resetOrCreatePendingConfirmation so
re-invited teams get a fresh reminder window.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Notifies the team LEAD with FINALIST_WITHDRAWN (in-app + email) when an admin
withdraws a grand-finale slot via adminDecline, unconfirm, or unenroll.
For unenroll, only notifies when a CONFIRMED confirmation existed before deletion.
Adds finalist-withdrawal.test.ts (4 tests) covering all three paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire notifyAdmins() into finalist lifecycle events so admins receive
in-app (+ email when enabled) notifications on each key transition:
- finalist.confirm → FINALIST_CONFIRMED
- finalist.decline / adminDecline → FINALIST_DECLINED
- expirePendingPastDeadline (per row) → FINALIST_EXPIRED
- promoteNextWaitlistEntry + manualPromote → FINALIST_WAITLIST_PROMOTED
All calls are wrapped in try/catch — comms never throw inside a mutation
or cron (CLAUDE.md: "round notifications never throw"). Added project
title to the project select where it was missing.
Tests: tests/unit/finalist-comms.test.ts — 3 tests passing (TDD).
Regression: finalist-confirmation, finalist-admin-confirm,
finalist-enrollment — 30 tests all passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add 8 constants to NotificationTypes (FINALIST_CONFIRMED/DECLINED/EXPIRED/
WAITLIST_PROMOTED/REMINDER/WITHDRAWN, TRAVEL_CONFIRMED, VISA_STATUS_UPDATE)
with matching icons and priorities in NotificationIcons/NotificationPriorities
- Add 4 branded email templates: getFinalistReminderTemplate,
getFinalistWithdrawnTemplate, getTravelConfirmedTemplate,
getVisaStatusTemplate — registered in NOTIFICATION_EMAIL_TEMPLATES
(admin-alert types use generic fallback)
- Add 8 logistics seed rows to seed-notification-settings.ts; upserted to
dev DB (idempotent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- enrollFinalists: reject a roundId whose competition belongs to a
different program than input.programId.
- unenroll: reject a project/round pair from different programs before
any delete.
- Hoist ADMIN_CONFIRM attendee validation to a pre-pass so a bad entry
in a multi-team batch fails before any project is partially written.
- Add regression tests for both cross-program guards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds EnrollAttendeesDialog and FinalistEnrollmentCard components and
wires the card above FinalistSlotsCard on the LIVE_FINAL round Overview,
giving admins the missing UI entry point to enroll mentoring-round teams
into the Grand Final via EMAIL or ADMIN_CONFIRM mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Returns mentoring-round candidates grouped by category with status,
team members, quota and confirmed/pending counts; inLiveFinal flag and
attendeeCap for the enrollment UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds finalist.unenroll(projectId, roundId) which deletes the
FinalistConfirmation (cascading AttendingMember/FlightDetail/
VisaApplication/MemberLunchPick) and the LIVE_FINAL ProjectRoundState,
then logs a FINALIST_UNENROLL audit entry. Safe no-op when no rows exist.
Tests cover ADMIN_CONFIRM enrolled teardown and the no-rows path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add `finalist.enrollFinalists` adminProcedure: creates ProjectRoundState in
LIVE_FINAL round (skipDuplicates) + resets/creates FinalistConfirmation in
one step, with EMAIL and ADMIN_CONFIRM attendee modes.
- Extract `confirmAttendanceInTx` helper into finalist-enrollment.ts; reuse
from both adminConfirm and enrollFinalists (DRY refactor, all tests green).
- Add 4 tests covering EMAIL mode, ADMIN_CONFIRM mode, re-enroll safety, and
over-cap rejection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Member/mentor/teammate tables used width:100%, stretching the two
columns apart and forcing names to wrap; wrapped names then misaligned
with their email (default vertical-align: middle).
Drop width:100% so tables hug content, add 16px column gap via name-cell
padding, and set vertical-align: top so emails align to the first line
of the name. Applied to getMentorBulkAssignmentTemplate and
getTeamMentorIntroductionTemplate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All user-creation paths (admin create, bulk invite import, public application
contact + team members, project team members, jury-group + special-award
invites) now set roles=[role] so the invariant role in roles[] holds for new
users, matching seed.ts and the role-change mutations. Prevents the empty
roles[] inconsistency that hid primary-role mentors from the mentor picker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getCandidates, getMentorPool and bulkAssign matched MENTOR via roles[] only, so
a user with role=MENTOR but an empty roles[] array (legacy/seeded records, e.g.
Arnaud Blandin on prod) was excluded from the mentor picker and rejected by
bulk assign. Match MENTOR as primary role OR in roles[], mirroring userHasRole.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
user.list and user.listInvitableIds filtered on the singular User.role column,
so the type tabs (Jury/Mentor/…) omitted users holding that role as a secondary
role (User.roles[]). Match the role as primary OR secondary (roles hasSome),
combined with search via AND, mirroring userHasRole / hasRole middleware.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate getMentorBulkAssignmentTemplate + getTeamMentorIntroductionTemplate to
getEmailWrapper() so they match the other ~40 platform emails: MOPC logo header,
ocean background, big-logo footer, and UTF-8 charset (fixes accent/em-dash
rendering). Body now uses sectionTitle/paragraph/infoBox/ctaButton helpers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a sky-accented "Send Welcome / Reminder" button to the Notifications
grid in the round page, visible only on MENTORING rounds. Wires into
trpc.mentor.previewMentorshipWelcome / sendMentorshipWelcome via the
shared EmailPreviewDialog with optional custom note support.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sendMentorBulkAssignmentEmail now accepts optional teamMembers per project
and a customNote, forwards both to the template, switches to getBaseUrl(),
and returns Promise<boolean> (true on success, false on empty/error).
sendTeamMentorIntroductionEmail now accepts optional teammates and customNote,
forwards both to the template, switches to getBaseUrl(), and returns
Promise<boolean> (true on success, false on empty/error).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>