TidyLinker.com
TypeScript
Pull Request Merged
PR #161 merged: feat: multi-day range bookings
Summary
Adele books Jamie 5am–9am Mon–Fri for 2 weeks in one form, sends one request, Jamie accepts once. Implements Option 1 (Booking series) from scratch/plans/plan-multi-day-range-bookings.md — parent BookingSeries row + N child JobPosting/Quote rows. Single-day booking flow is untouched.
What shipped
Backend
booking_seriestable +job_postings.booking_series_idFK migrationsBookingSeriesmodel (usesHasUid+SoftDeletes) withclient/cleaner/jobPostingsrelations and aBookingSeriesStatusenum (pending/accepted/declined/partially_cancelled/completed/cancelled)- Actions:
CreateBookingSeries,AcceptBookingSeries(accept all or passaccept_job_idsto partially accept and decline the rest),DeclineBookingSeries,CancelBookingSeriesRemaining CreateDirectBookingrefactored — newcreateJobAndQuote(...)helper so single-day and series share rate/description/services logic without duplicating itCancelJobPostingnow rolls up to the parent series after a per-day cancel (Pending/Accepted →partially_cancelledwhile children remain, →cancelledwhen none do)ReleaseHeldBookingsnow handles held series (oneBookingSeriesRequested+ one notification per series) alongside held single bookings- New
BookingSeriesRequestedandBookingSeriesAcceptedevents (broadcast) and matchingBookingSeriesRequestedNotification/BookingSeriesAcceptedNotification BookingSeriesRequestform request with date-range cap (90 days), day-of-week mask validation,unavailable_datessurfaced as a validation error so the UI can show which specific days clashed, and a series-scoped duplicate-pending check (single-day pending no longer blocks a series request, per plan §6)- Client + Cleaner
BookingSeriesControllers, policy,BookingSeriesDataDTO, routes (client.booking-series.{store,show,destroy}andcleaner.booking-series.{show,accept,decline}) —book-seriesendpoint isthrottle:10,1to discourage abuse from the loosened pending guard
Frontend
booking-dialog.tsxgains a Single day / Multiple days tab. Multi-day mode shows aDateRangePicker(new component usingreact-day-pickermode="range"), seven Mon–Sun chips (defaulting to weekdays), a live "N bookings, £total" preview, and a 24h time field that applies to every day<AlertError>summary at the top of the dialog so 422s are surfaced (per the recently-shipped form-error pattern — the project's component isalert-error.tsx, not theFormErrorSurfacename in the brief)- New
Client/BookingSeries/Show.tsxandCleaner/BookingSeries/Show.tsxpages — cleaner side has Accept all / Pick days / Decline all Client/Jobs/Index.tsxnow groups jobs sharing abooking_series_idinto one "Series" row that links through to the series page; single-day jobs render as beforeStatusBadgegains apartially_cancelledorange variant
Notifications
- One
BookingSeriesRequestedNotificationper series (not N), with deep-link to/cleaner/booking-series/{uid} BookingSeriesAcceptedNotificationcovers both full and partial acceptance; per-jobDirectBookingAcceptedlisteners still fire so reminders/payment authorisation keep working
Tests
tests/Feature/BookingSeries/CreateBookingSeriesTest.php(6 tests): happy-path 10-child Mon–Fri series, day-of-week mask honoured when start is a Saturday, zero-date range → 422, >60 days → 422, unverified client → series held + no notifications, cleaner unavailable on subset of dates →unavailable_dateserror with no rows createdtests/Feature/BookingSeries/AcceptBookingSeriesTest.php(5 tests): accept-all sets every quote to accepted and fires bothBookingSeriesAccepted(×1) andDirectBookingAccepted(×10); partial accept declines the rest and flips series topartially_cancelled; non-owner cleaner gets 403; client cannot accept their own series; decline cascades to every childtests/Feature/BookingSeries/CancelBookingDayTest.php(3 tests): cancelling one day leaves the other 9 intact and flips series topartially_cancelled; "cancel remaining" skips in-progress children but cancels every other live day; cancelling every live day → seriescancelledtests/Feature/BookingSeries/BookingSeriesAvailabilityTest.php(1 test):getBookedSlotscontinues to return per-day rows for series children — confirms no calendar regression
All 15 new tests pass plus all 37 existing DirectBooking tests still pass.
What's deferred (open questions from plan §10)
- Acceptance default UX: shipped as "Accept all in one click, with a Pick days expander", per plan §10 (1). Confirm with Adele + Jamie before/after Stu sees it.
- Cancellation refunds (§10.2): per-day cancel reuses the existing single-day path — no series-level "refund all in one click". Plan §9 flagged this as out of scope; if Adele or Stu want bulk refund, that's a follow-up.
- Payment timing (§10.3): per-day on completion, same as today. The plan keeps the door open to upfront series payment but explicitly defers it; no Stripe changes here.
- Message thread scope (§10.4): one thread per series, anchored on the first child job (the cheap option). A future
MessageThread.subjectpolymorphism would let the series own the thread directly. - Series-level edits (§10.5): not supported — client cancels the relevant days and re-books.
- Naming (§10.7): copy currently uses "Booking series" and "Series" pill — easy to change once Stu picks a final term.
- Notification fatigue (§9): per-job reminders still fan out N times for a series. Should honour a digest preference; out of scope here.
- Calendar collisions after acceptance (§9): if a cleaner becomes unavailable on day N after accepting, day N's job stays Pending — same gap as today's single-day flow.
Test plan
-
php artisan migrate:fresh --seedruns cleanly - As a client, open a cleaner profile, click Request Booking, toggle Multiple days, pick a 2-week range, untick Sat/Sun, fill the form, send — series row appears on
/client/jobswith "10 bookings" - Try a Sat-only range with the mask set to weekdays — see the "no dates fall inside that range" error in the dialog
- As the cleaner, open
/cleaner/booking-series/{uid}, click Accept all — every day flips to confirmed and aBookingSeriesAcceptednotification lands on the client - On the cleaner side, hit Pick days, untick a few, hit Accept-and-decline-rest — verify the picked days are accepted and the rest are cancelled
- As the client, cancel one day from
/client/jobs/{day}— series row shows "Partially cancelled" and other days stay live - As the client, click Cancel remaining on the series page — every still-cancellable day is cancelled; in-progress days stay
- As an unverified client, submit a series — see "verify your email" notice, no cleaner notification fires; after verifying, series is released and one notification arrives
Notes for review
- Wayfinder TypeScript files (
resources/js/actions/...) are git-ignored and regenerated on build/php artisan wayfinder:generate. The PR includesphp artisan wayfinder:generate-ready route definitions inroutes/web.php. - The booking series duplicate-pending guard is intentionally narrower than the single-day one (per plan §6). The
book-seriesroute is throttled at 10/min to compensate.
+3412
additions
-132
deletions
36
files changed