9.6 KiB
Wave 4 — Team-facing "My Logistics" + travel/visa UX fixes
For agentic workers: REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use
- [ ].
Goal: Close the loop for finalist teams. Today a confirmed team can see only their attendee roster + a visa badge; they have no view of their flights or hotel, can't self-enter passport nationality, and the submitter (non-TeamMember lead) can't even see the card. This wave adds a team-facing "My Logistics" view (flights + hotel + visa + nationality self-entry), fixes the submitter gap, links the confirm-success page to the dashboard, and adds the admin-side travel/visa correctness fixes (departure-after-arrival validation, CSV export).
Architecture: New read procedures on the applicant router (getMyLogistics) reusing the same caller→project resolution as the existing finalist procedures, plus a self-service updateMyVisaNationality. A new applicant dashboard card. Admin-side: input validation on logistics.upsertFlightDetail + CSV export buttons mirroring the lunch manifest.
Tech Stack: Next.js 15, tRPC 11, Prisma 6, shadcn/ui. No schema change.
Key facts (verified 2026-06-04):
applicant.getMyFinalistConfirmation(src/server/routers/applicant.ts:2748) resolves the project viateamMembers: { some: { userId } }— MISSES a lead who submitted but has no TeamMember row, and has no role guard.applicant.getMyVisaApplications(:2819) returns visa rows whenprogram.visaStatusVisibleToMembers, else null. Reuse its visibility logic.- Admin-only:
logistics.getHotel,logistics.listFlightDetails(src/server/routers/logistics.ts). No team-facing equivalents → teams never see hotel/flights. - Applicant dashboard composes cards in
src/app/(applicant)/applicant/page.tsx(~line 409):LunchBanner,ExternalAttendeesStrip,AttendingMembersCard. Add the new card here. - Confirm-success copy at
src/app/(public)/finalist/confirm/[token]/page.tsx:183promises "your project page" with no link. logistics.upsertFlightDetailinput (:145) has no departure-after-arrival check. Lunch CSV export pattern:src/components/admin/logistics/lunch-manifest.tsx:28-57.- Models:
Hotel{ name, address?, link?, notes? }(programId 1:1);FlightDetail{ arrival/departure At/FlightNumber/Airport, status, adminNotes }(per AttendingMember);VisaApplication{ status, nationality?, ... }.
Task 1: Team-facing read endpoint + submitter fix
Files: src/server/routers/applicant.ts; test tests/unit/applicant-my-logistics.test.ts.
-
Step 1: Fix
getMyFinalistConfirmationresolution: change thewhereto{ OR: [{ submittedByUserId: ctx.user.id }, { teamMembers: { some: { userId: ctx.user.id } } }], finalistConfirmation: { isNot: null } }(verifyProject.submittedByUserIdis the correct field name in schema). Keep the rest. (No role guard added — APPLICANT/lead may not have an explicit role; the project-scoping is the guard. If other procedures in this router use a role check, match it; otherwise leave project-scoping as the guard and note it.) -
Step 2: Add
getMyLogistics: protectedProcedure.query:- Resolve the caller's confirmed-finalist project (same OR-resolution as above; return
nullif none or confirmation not CONFIRMED). - Return:
{ projectTitle: string, confirmationStatus: FinalistConfirmationStatus, hotel: { name, address, link, notes } | null, // program Hotel (1:1) myFlight: { arrivalAt, arrivalFlightNumber, arrivalAirport, departureAt, departureFlightNumber, departureAirport, status } | null, // the caller's own AttendingMember.flightDetail visaVisible: boolean, // program.visaStatusVisibleToMembers myVisa: { status, nationality } | null, // caller's own VisaApplication, only when visaVisible } myFlight: find the caller'sAttendingMemberfor this confirmation (where confirmationId + userId), includeflightDetail.hotel:prisma.hotel.findUnique({ where: { programId } }).myVisa: only whenvisaVisibleand the caller's AttendingMember has avisaApplication.
- Resolve the caller's confirmed-finalist project (same OR-resolution as above; return
-
Step 3: Test — set up a CONFIRMED finalist with the caller as an AttendingMember, a Hotel, a FlightDetail (CONFIRMED),
visaStatusVisibleToMembers:true, a VisaApplication GRANTED. CallgetMyLogisticsas that user → asserthotel.name,myFlight.arrivalFlightNumber,visaVisible:true,myVisa.status==='GRANTED'. Also: a non-finalist user →null. (Caller viacreateCaller(applicantRouter, { id, email, role:'APPLICANT' }).) -
Step 4:
npx vitest run tests/unit/applicant-my-logistics.test.ts→ pass; re-run any applicant tests touching these procedures.npm run typecheck→ clean. -
Step 5: Commit —
git commit -am "feat(applicant): getMyLogistics (hotel+flight+visa) + submitter-match fix"
Task 2: Self-service nationality entry
Files: src/server/routers/applicant.ts; test in the same file.
- Step 1: Add
updateMyVisaNationality: protectedProcedure.input(z.object({ nationality: z.string().max(100) })).mutation:- Find the caller's
AttendingMemberwhose program hasvisaStatusVisibleToMembers:trueand which has aVisaApplication; if none →TRPCError NOT_FOUND("No visa application to update"). - Update that
VisaApplication.nationality. AuditVISA_NATIONALITY_SELF_SET. Return{ ok: true }. - (Optional nicety: also copy to
User.nationalityif empty.)
- Find the caller's
- Step 2: Test — caller with a visible VisaApplication sets nationality → assert it persisted. Caller without one → rejects.
- Step 3: Run → pass.
npm run typecheck→ clean. - Step 4: Commit —
git commit -am "feat(applicant): self-service visa nationality entry"
Task 3: "My Logistics" card + confirm-page link
Files: create src/components/applicant/my-logistics-card.tsx; modify src/app/(applicant)/applicant/page.tsx, src/app/(public)/finalist/confirm/[token]/page.tsx.
- Step 1: Build
MyLogisticsCard('use client'):trpc.applicant.getMyLogistics.useQuery(). Ifnullor loading → render nothing / Skeleton. Otherwise a Card "Your grand-finale logistics" with sections:- Hotel — name + address + link (if present), else "Hotel details coming soon."
- Flights — the caller's arrival/departure (flight no, airport, formatted date/time in Europe/Paris) + a status badge, else "Your flight details will appear here once arranged."
- Visa (only if
visaVisible) — status badge + a nationality field: shows currentmyVisa.nationalityor an inline editable input →trpc.applicant.updateMyVisaNationality(sonner toast + invalidate). Follow the visual pattern ofattending-members-card.tsx. Visible affordances only.
- Step 2: Render
<MyLogisticsCard />insrc/app/(applicant)/applicant/page.tsxnearAttendingMembersCard(~line 415). - Step 3: In the confirm-success block (
finalist/confirm/[token]/page.tsx:168-185), replace the dead "your project page" sentence with a real link/button to/applicant("Go to my dashboard"). (The confirm page is public; the link will hit auth and land them on their dashboard after login.) - Step 4:
npm run typecheck→ clean. - Step 5: Commit —
git commit -am "feat(applicant): My Logistics card (hotel/flights/visa+nationality) + confirm-page dashboard link"
Task 4: Admin travel/visa UX — validation + CSV export
Files: src/server/routers/logistics.ts, src/components/admin/logistics/travel-tab.tsx, src/components/admin/logistics/visas-tab.tsx; test tests/unit/logistics-flight.test.ts (extend).
- Step 1: Server validation in
logistics.upsertFlightDetail: after buildingdata, if botharrivalAtanddepartureAtare present anddepartureAt < arrivalAt, throwTRPCError BAD_REQUEST("Departure must be after arrival"). Add a test asserting the rejection. - Step 2: CSV export — add a "Download CSV" button to
travel-tab.tsx(columns: Project, Attendee, Email, Arrival date/time, Arrival flight, Arrival airport, Departure date/time, Departure flight, Departure airport, Status, Needs visa) and tovisas-tab.tsx(columns: Project, Attendee, Email, Nationality, Status, Invitation sent, Appointment, Decision, Notes). MIRROR the CSV builder insrc/components/admin/logistics/lunch-manifest.tsx:28-57(Blob + anchor download; escape commas/quotes). Build from the data already loaded by each tab's query. - Step 3:
npx vitest run tests/unit/logistics-flight.test.ts→ pass.npm run typecheck→ clean. - Step 4: Commit —
git commit -am "feat(logistics): departure-after-arrival validation + travel/visa CSV export"
Task 5: Verify
- Step 1:
npx vitest run— full suite green. - Step 2:
npm run typecheck— clean. Stop dev,rm -rf .next,npm run build— clean. - Step 3: Restart dev on :3001. Dev smoke: as admin, enroll a team (ADMIN_CONFIRM) and add flight + hotel + set a visa; then log in as that team's lead (seed user
matt@letsbe.solutionsis a MEMBER of "Revamp Flips" — or use the lead) and confirm the "My Logistics" card shows the hotel + flight + visa + nationality field. Verify the CSV export downloads. Clean up. - Step 4: Summarize.
Notes
- Deep timezone overhaul of the admin flight
datetime-localinputs (storing explicit Europe/Paris) is a separate design decision — NOT in this wave; the validation + Europe/Paris display labels are the pragmatic improvement. Flag as remaining polish. - This completes the 4-wave logistics overhaul.