7.8 KiB
Multiple Hotels + Room Assignments — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use
- [ ].
Goal: Replace the one-hotel-per-edition model with many hotels + per-attendee room assignments (hotel, room number, check-in/out), assignable per-member or per-team, surfaced to teams and in the travel email.
Architecture: New HotelStay 1:1 detail record per AttendingMember (mirrors FlightDetail); Hotel.programId becomes non-unique. Logistics router gains list/CRUD + rooming/assignment procedures; the Hotels tab is reworked into Hotels + Rooming sections. Spec: docs/superpowers/specs/2026-06-04-multi-hotel-rooming-design.md.
Tech Stack: Next.js 15, tRPC 11, Prisma 6, shadcn/ui, Vitest 4. One schema migration (additive + drop one unique constraint).
Verified facts: Hotel (schema.prisma) is programId @unique; FlightDetail is the 1:1-detail pattern to mirror. 1:1 hotel callers to update: logistics.ts:13 (getHotel), :17/33 (upsertHotel), :231 (travel email program-hotel lookup), applicant.ts:2899 (getMyLogistics), hotels-tab.tsx:20/37/40.
Task 1: Schema — many hotels + HotelStay
Files: prisma/schema.prisma, migration.
- Step 1: Edit
Hotel: remove@uniquefromprogramId; addstays HotelStay[]and@@index([programId]). - Step 2: Add
HotelStaymodel exactly as in the spec (1:1attendingMemberId @unique, requiredhotelId,roomNumber?,checkInAt?,checkOutAt?,notes?, timestamps;attendingMemberrelationonDelete: Cascade;hotelrelationonDelete: Restrict;@@index([hotelId])). AddhotelStay HotelStay?toAttendingMember. - Step 3:
npx prisma migrate dev --name multi_hotel_and_hotel_stay→ migration created + applied + client regenerated. Confirm the generated SQL dropsHotel_programId_key, adds the index, and createsHotelStaywith both FKs. - Step 4:
npm run typecheck(existinggetHotel/upsertHotelstill compile until Task 2). Commit:git add prisma/ && git commit -m "feat(hotel): many hotels per edition + HotelStay (room assignment)"
Task 2: Logistics router — hotels CRUD + rooming + assignment + travel email
Files: src/server/routers/logistics.ts; test tests/unit/logistics-hotels.test.ts.
Replace getHotel/upsertHotel with the full set (read the spec table for exact inputs):
listHotels({ programId })— hotels +_count: { stays }.createHotel/updateHotel/deleteHotel(deleteHotel: pre-count stays; if >0 throwBAD_REQUEST"Reassign N occupant(s) first"). All audited.listRooming({ programId })— one row per CONFIRMEDAttendingMemberin the program:{ attendingMemberId, projectId, projectTitle, user{id,name,email}, stay: { hotelId, roomNumber, checkInAt, checkOutAt } | null }, sorted by project title then user name.assignStay({ attendingMemberId, hotelId, roomNumber?, checkInAt?, checkOutAt?, notes? })— upsertHotelStay(validate the hotel belongs to the same program as the attendee). AuditHOTEL_STAY_ASSIGN.assignTeamToHotel({ confirmationId, hotelId, checkInAt?, checkOutAt? })— for eachAttendingMemberof the confirmation, upsertHotelStaywithhotelId(+ optional dates), preserving existingroomNumber. AuditHOTEL_TEAM_ASSIGN.unassignStay({ attendingMemberId })—deleteMany(no-op safe). AuditHOTEL_STAY_UNASSIGN.
Then update setFlightStatus (the TRAVEL_CONFIRMED path, ~line 231): instead of the program 1:1 hotel, load the attendee's hotelStay (with hotel) and pass hotel + room/dates into the notification metadata (keys: hotel: { name, address, link }, roomNumber, checkInAt, checkOutAt). Update getTravelConfirmedTemplate (src/lib/email.ts) + its NOTIFICATION_EMAIL_TEMPLATES entry to render room/dates if present (additive — keep existing fields working).
- Step 1: Tests (
tests/unit/logistics-hotels.test.ts, mirrorlogistics-hotel.test.ts): create 2 hotels for one program;deleteHotelon an occupied hotel rejects, on an empty one succeeds;assignStayupsert (create→update room);assignTeamToHotelassigns all of a 2-attendee team;unassignStayremoves;listRoomingreturns the confirmed attendees with/without stays. - Step 2: Run → fail → implement → pass (
npx vitest run tests/unit/logistics-hotels.test.ts). Re-runtests/unit/logistics-flight.test.ts tests/unit/logistics-comms.test.ts(travel-email change). - Step 3:
npm run typecheck. Commit:feat(logistics): hotels CRUD + rooming + assignment procedures + travel email
Task 3: Applicant getMyLogistics + My Logistics card → assigned hotel/room
Files: src/server/routers/applicant.ts, src/components/applicant/my-logistics-card.tsx; test tests/unit/applicant-my-logistics.test.ts (extend).
- Step 1: In
getMyLogistics(~line 2899), replace theprisma.hotel.findUnique({ where: { programId } })with the caller'sAttendingMember.hotelStay(includehotel). Returnhotel: { name, address, link, notes } | nullandroom: { roomNumber, checkInAt, checkOutAt } | null. Extend the test to seed aHotelStayand asserthotel.name+room.roomNumber. - Step 2: Update
MyLogisticsCardHotel section to also show Room (number) + check-in/out (Monaco-time labels) whenroomis present. - Step 3: Run the applicant test → pass;
npm run typecheck. Commit:feat(applicant): My Logistics shows assigned hotel + room
Task 4: Hotels tab rework — Hotels + Rooming UI
Files: src/components/admin/logistics/hotels-tab.tsx (rework); optionally split a rooming-section.tsx.
- Step 1: Hotels section —
trpc.logistics.listHotels; list cards with name/address/link/notes + occupancy badge (_count.stays); Add / Edit (dialog) / Delete (AlertDialog; show the "reassign first" error toast on rejection) wired to create/update/deleteHotel. Invalidate on success. - Step 2: Rooming section —
trpc.logistics.listRooming; group rows by project (team). Per team header: an "Assign whole team to…" Select →assignTeamToHotel. Per attendee row:HotelSelect (→assignStay, orunassignStaywhen cleared),Room #input (blur →assignStay),Check-in/Check-outdate inputs (blur →assignStay). Hotel options fromlistHotels. Skeleton while loading; empty state "No confirmed attendees yet." Download CSV button (Team, Member, Email, Hotel, Room, Check-in, Check-out) mirroring the travel/visa export. Visible affordances only. - Step 3:
npm run typecheck. Commit:feat(logistics): Hotels tab — multi-hotel management + rooming assignment
Task 5: Verify + deploy
- Step 1:
npx vitest run— full suite green;npm run typecheckclean. - Step 2: Stop dev,
rm -rf .next,npm run build— clean. - Step 3: Restart dev on :3001. Dev smoke (admin): Logistics → Hotels tab → add 2 hotels; enroll a team (ADMIN_CONFIRM) so attendees exist; in Rooming, "assign whole team" to hotel A, override one member to hotel B + a room number; verify occupancy counts; check the team-member dashboard shows their assigned hotel + room. Clean up.
- Step 4: Merge to
main(fast-forward if possible), push, watch Gitea build #N succeed, then redeploy on prod (ssh stefan@89.58.5.223:22022,/opt/letsbe/stacks/mopc-portal,docker compose pull && docker compose down && docker compose up -d— NO -v), verify migration applied + app healthy +GET /login200. - Step 5: Summarize.
Notes
- All comms/assignment writes best-effort where they sit inside other mutations (travel email try/catch already in place).
- Prod migration is additive + one dropped unique constraint (safe; no
HotelStaydata exists yet).