fix(explore): expert booking calendar — viewport-fit dialog, nav z-stack, per-day availability dots - #1229
fix(explore): expert booking calendar — viewport-fit dialog, nav z-stack, per-day availability dots#1229teetangh wants to merge 11 commits into
Conversation
…ay availability dots - Book Now dialog: inline zIndex 1002 so the fixed navbar (z-[1000]) and announcement bar (z-[1001]) can no longer paint above it; compact header/footer chrome; slot list capped (30dvh mobile / 350px desktop) with internal scroll so the dialog never grows past the viewport. - Calendar: cell size driven by a --cell clamp(26px, 5.2dvh, 42px) var so all rows + legend fit on screen at any height; weekday header and grid center cells; description and schedule note hidden on tight screens. - Calendar now shows which dates have slots: month-wide rollup from the existing availability range endpoint (today -> month end, lazy per month flip) rendered as emerald/amber/rose/zinc dots with a legend; past and zero-slot days disabled; prev-month arrow disabled on the current month. - Sidebar pricing card sticks below the real header stack via xl:top-[calc(var(--header-height)+1rem)] instead of top-24.
✅ Deploy Preview for familiarise ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Warning
|
…olling Dialog is now a fixed-height (85dvh) flex column with container-type: size — header/footer stay fixed, the two panes flex, and only the slot list scrolls internally. Calendar cells size off the dialog's real height via cqh container units (--cell clamp), so the grid can never push the dialog past the screen. Dropped the duplicate calendar legend; the slot list legend doubles as the dot key.
…insic width, w-auto collapsed it
8cqh-of-dialog cells ignored the chrome share of the pane, so the last week row spilled outside the card on shorter screens. The card is now the size container at md+ and cells derive from its real height (clamp(22px, 16.6cqh - 30px, 48px)); card clips overflow; mobile stack scrolls the body and caps the slot list at 40dvh.
…h variant) At short viewport heights the side-by-side dialog left the calendar card ~217px — six week rows can never fit. New 'mdh' custom variant (min-width 768px + min-height 700px) gates the two-column, no-scroll layout; shorter screens use the stacked layout with body scroll. Chrome slimmed (header/desc/footer/card padding) and cell formula retuned to the card's real chrome.
… mdh layout flex-basis:0 children of auto-height parents collapsed the stacked fallback to ~97px, clipping the calendar. flex-1 now applies only at mdh where the dialog has a fixed height; stacked mode sizes by content.
… full-screen on phones Per the SaaS scheduling pattern (Cal.com/Calendly): the booking flow is the task, so the sheet takes 92dvh and up to 1200px wide on desktop and goes full-bleed below sm. The mdh two-column threshold drops to 620px height — at the previously-cramped 662px viewports the side-by-side no-scroll layout now fits with ~36px cells.
…l constant to -22px
…size by content min-h-0 let flex-shrink crush the card below its content when the body grid's auto rows resolved small on mobile; the calendar clipped at ~4 rows. Panes/card/list now keep natural height outside mdh.
|
…y everywhere, and day segments are half-open (#1343, #1342, #1326, #1348, #1415, #1416) (#1512) ## Summary **The one rule this PR establishes:** `SlotOfAvailabilityWeekly.startDay` is the day the **consultant** published, in their own local calendar, and the UTC weekday is always derived from it through the row's own frozen `utcOffsetMinutes`. Four surfaces had each answered that question for themselves, and the disagreements are the six issues below. | Issue | What was wrong | Fix | | --- | --- | --- | | **#1343** | The settings save path shifted the day forward or back to the UTC day the converted instant landed on, while onboarding stored the local day. An Asia/Kolkata row starting before 05:30 local walked back one weekday on every save — Monday 01:00 saved as Sunday, reloaded into Sunday's form row, saved again as Saturday. | `shiftDayOfWeek` and the `startDayOffset` block are gone. `utils/schedule/formatting.ts` now has one builder, `weeklySlotForSave`, that both save paths use; `formatWeeklySlot` is a thin adapter over it and keeps the #1125 all-or-nothing throw contract. | | **#1342** | The calendar grid bucketed rows on the **viewer's** weekday and did not even select `utcOffsetMinutes`, so an overseas customer was shown an IST pre-05:30 row a day away from the day checkout would accept. | New `utils/schedule/weekly-projection.ts` owns `utcStartDayIndex`, `weeklyRowDurationMinutes` and `weeklyRowOccurrencesInRange` — the single generator the grid, `isMinuteWithinWeeklySlot`, `getNextOccurrenceWeekly` and `matchWeeklySlotToDay` all share. The grid endpoint carries the stored columns through instead of flattening them onto a 1970 reference date, and `processWeeklySlots` loses its `timezone` parameter because display zoning belongs downstream in `splitSlotsByDay`. | | **#1326**, **#1348 item 1** | Onboarding and the per-row PATCH defaulted a consultant with no profile timezone to UTC 0 rather than to the launch offset, and nothing checked a caller who supplied an offset of their own. | New `lib/scheduling/weeklyUtcOffset.ts` is the only answer: derived from `User.timezone`, 330 when the profile carries no usable zone (ADR 17), `WeeklyOffsetConflictError` → 400 `UTC_OFFSET_CONFLICT` when a caller contradicts it, and one Sentry warning per write (never per row) when the consultant publishes from outside Asia/Kolkata. `PIN_TO_LAUNCH_OFFSET` is the one-line switch to hard-pin every row to IST. Onboarding also stamped the offset *after* `mergeAdjacentWeeklyRows`, so that fold's cross-offset guard was comparing `undefined` with `undefined`; it is stamped before the fold now. | | **#1415** | `splitSlotsByDay` cut day segments at `endOfDay` (23:59:59.999). A 23:30–23:59:59.999 remainder is not a thirty-minute atom, so a block published up to local midnight silently lost its last bookable slot on every surface. | Segments are half-open: a segment ends at the next local day's midnight. | | **#1416** | The expert page's display merge carried a 60-second tolerance where booking requires exact adjacency, so the availability card advertised a window whose seam no row publishes and checkout's per-atom union coverage then refused the booking. | `mergeConsecutiveSlotsForDisplay` requires exact adjacency, and its docblock now states the truth: the only difference from `mergeConsecutiveSlots` is which atoms are eligible (same-status versus available-only). | Per **user decision 6 / #872**, all four weekly write paths additionally **dual-write** the five DST columns (`timezone`, `localStartMinutes`, `localEndMinutes`, `localStartDay`, `localEndDay`) from the same resolver, computed after the merge so they describe the row actually stored. Nothing reads them. `coalesceConsultantWeeklyRows` deletes and recreates rows, so it recomputes them for the merged row rather than losing them on the next coalesce. The `schema.prisma` doc-comment changes from "unwritten" to "written from 2026-09-05, read by nothing until the reader flip" — comment only, **no schema change, no `db push` needed**. **No data repair.** The rows the old settings path wrote are pre-MVP mock data; the no-backfill rule applies and the pre-MVP reset wipes them. ### Rebands worth recording - **#1415's headline is false.** `availability-window-merge.test.ts:240-245` already proves a two-hour block yields one two-hour window across rows. The real residual was the `endOfDay` bound described above, and that is what this PR fixes. - **#1348 is a three-item bucket.** Item 1 (the zero-offset default) is fixed here, together with the regression test across the write paths that the issue asks for. Item 2 is explicitly scope-hygiene only and defers to #1206, which is now built. Item 3 asks for three tests, of which only the `mergeAdjacentWeeklyRows` cross-offset bullet is addressed (by the ordering fix above); see follow-ups. ## Files touched **New:** `utils/schedule/weekly-projection.ts`, `lib/scheduling/weeklyUtcOffset.ts`, `__tests__/booking-algorithm/weekly-day-semantics.test.ts`. **Changed:** `utils/schedule/formatting.ts`, `utils/timeSlotsProcessing.ts`, `utils/slotAllocation/slotTimeUtils.ts`, `utils/slotAllocation/SlotAllocationService.ts`, `utils/slotAllocation/mergeAdjacentWeeklyRows.ts`, `utils/onboarding-server.ts`, `app/api/slots/availability-with-allocation/[consultantId]/route.ts`, `app/api/slots/availability/weekly/route.ts`, `app/api/slots/availability/weekly/[id]/route.ts`, `app/api/user/consultants/[id]/route.ts`, `app/explore/experts/[consultantId]/utils/mergeSlots.ts`, `prisma/schema.prisma` (comment only), `__tests__/schedule/format-slots-for-api-throws.test.ts`. **Docs:** `docs/booking/00-architecture-decisions.md` (ADR B4 rewritten, register row and status updated), `docs/booking/02-event-types-and-validation.md`, `docs/booking/03-slot-math-and-calculations.md` (new section "Projecting a weekly row onto real dates"), `docs/booking/19-dst-and-timezone-posture.md` (rules 1–4 corrected — the local columns are written now and the drift warning exists), `.claude/skills/booking/references/availability.md` §1/§2/§4. `docs/booking/05-troubleshooting-and-changelog.md` is deliberately untouched; PR-D writes one consolidated section. ## Verification | Check | Command | Result | | --- | --- | --- | | Types | `rm tsconfig.tsbuildinfo && NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit` | Clean, zero diagnostics (run twice: before and after the Prettier pass). `npx prisma generate` deliberately not run — a sibling worktree owns the shared client. | | Lint | `npx eslint` over all 15 changed/new TypeScript files | **0 errors, 2 warnings**, both pre-existing `no-explicit-any` at `SlotAllocationService.ts:3445` and `:3498` (verified identical on `origin/dev` at lines 3446/3499, only shifted by edits above them). Nothing new introduced. | | Format | `npx prettier --check` over all 21 changed files | All matched files use Prettier code style. | | Tests | `npx jest __tests__/booking-algorithm __tests__/booking __tests__/schedule` | **84 suites passed, 1294 tests passed**, exit 0. | | A12.1 grid ⇔ validator walk | Throwaway script, not committed: every 30-minute atom of the week 2026-09-06 → 2026-09-13 for the IST Monday 01:00–05:00 row (`{MONDAY, 1170–1410, offset 330}`), asserting `weeklyRowOccurrencesInRange` coverage ⇔ `isMinuteWithinWeeklySlot`. | `ATOMS=336 GRID=8 VALIDATOR=8 MISMATCHES=0` — the eight atoms are the four hours the row publishes, and the grid and the validator agree on every one of the 336. | The committed pin, `__tests__/booking-algorithm/weekly-day-semantics.test.ts`, is table-driven, node-environment and Prisma-free, and covers all six required areas: the two save paths agreeing on `startDay` (including re-save idempotency), the offset resolver's table plus a source assertion that the four write paths call `resolveWeeklyUtcOffsetMinutes` and no longer call `getTimezoneOffsetMinutes` directly, grid-equals-validator for the IST pre-dawn row seen from both an `Asia/Kolkata` and an `America/New_York` viewer, the midnight block keeping its 23:30 window, display-merge-equals-booking-merge over gaps of 0/1,000/60,000 ms, and the dual-written local columns. `slotTimeUtils.test.ts`, `availability-window-merge.test.ts`, `availability-grid-conditional-get.test.ts`, `availability-window-scan.test.ts` and `slot-session-fix-pins.test.ts` were run and are green, and were not edited. ## Not done / follow-ups - **Server-side `validateTimeIncrements`** is out of scope. It is a private helper on a separate rejection surface, so the server write paths still do not run it and a minute-level seam remains creatable through the API even though neither merge function will now advertise one. - **#1229** (draft, `fix/explore-booking-responsive`) overlaps this area textually only. Per the guardrail, `ConsultationPricingToggle.tsx`, `ExpertPricing.tsx`, `WeeklyAvailability.tsx` and `CustomAvailability.tsx` are untouched; the only explore file changed here is `utils/mergeSlots.ts`. - **No data repair** for rows already written by the old settings path — pre-MVP mock data, no-backfill rule, the reset wipes them. - **#1348 item 3** still wants two tests this PR does not add: preference-scoring tie-break determinism under equal scores, and confirmation that a manual allocation is not silently overwritten by a later auto pass. Neither is a behaviour change, and both are test-budget items that belong with the allocator work rather than here. - **#872 reader flip** remains post-MVP. This PR only makes the dual-write correct and documents the reader-flip approach in `19-dst-and-timezone-posture.md`. Closes #1343 Closes #1342 Closes #1326 Closes #1348 Closes #1415 Closes #1416 Part of #872 Part of #1433 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01EyngsXG829TRTBof4CSGT1




Problem
On the Explore Experts details page (
/explore/experts/[consultantId]):z-50while the fixed navbar isz-[1000]and the announcement barz-[1001], so the nav painted above the modal.sticky top-24(96px) sat under the real chrome stack (80px nav + ~40px announcement bar ≈ 120px).Fix
zIndex: 1002on the Book NowDialogContent(merge-order-proof vs thez-50base class). Matches thez-[1002]pattern already used byTrialBookingModal/SubscriptionPricingToggle.--cell: clamp(26px, 5.2dvh, 42px)CSS var so 6 rows + legend always fit; compact header/footer chrome; slot list capped (30dvhmobile /350pxdesktop) with internal scroll; description + schedule note hidden on tight screens. The whole dialog now shrinks with the screen instead of overflowing it./api/slots/availability-with-allocationrange endpoint (today → month end, lazy re-fetch on month flip, stale-response guard) rendered as emerald (open) / amber (partially booked) / rose (fully booked) / zinc (no slots) dots under each date, with a legend. Past and zero-slot days are disabled; fully-booked days stay clickable to show the rose slot list; prev-month arrow disabled on the current month. No backend changes.xl:sticky xl:top-[calc(var(--header-height,5rem)+1rem)]— header-aware and only sticky where the two-column layout exists.Verification
tsc --noEmit+ ESLint clean on touched files.