feat: enriched edit request system with side-by-side snapshots, calendar preloading, and admin review UI
Backend: - Add enriched response types (EditSnapshot, EnrichedEditRequest) with original vs proposed snapshots - Add 4 new GET endpoints for viewing edit requests (user and admin scoped) - Remove github.com/lib/pq dependency — use native PostgreSQL array scanning - Clean up edit requests, time blockers, and notifications on booking cancellation - Validate exceptional closed hours on admin approve (409 Conflict) - Notification upsert on edit request replace (no duplicate admin notifications) Frontend: - New user EditRequestModal with time/services/both modes and lunch protection - New admin EditRequestModal with side-by-side diff (date/time, services, notes) - Integrate edit requests into PendingApprovals card and notifications page - Preload 3 months of availability to prevent calendar snap-back - Apply lunch protection to isDateUnavailable in BookingFlow and BookingCreateModal - Fix accessibility: card list items use <button> instead of <div> Dev & Docs: - Seed edit requests in local-dev-2.sh - Update all Obsidian manuals with enriched edit request documentation - 42 new tests (438/441 passing)
This commit is contained in:
@@ -541,6 +541,12 @@ The request appears in the **Pending Approvals** section on the Today page. You'
|
||||
- Any notes they've added
|
||||
- Any services they want to change
|
||||
|
||||
The system shows a **side-by-side comparison** of the original booking versus the proposed changes:
|
||||
- **Original snapshot**: current start time, end time, services (with prices and durations), and notes
|
||||
- **Proposed snapshot**: the new start time, recalculated end time, updated services, and new notes
|
||||
|
||||
This lets you see exactly what will change before you approve or decline.
|
||||
|
||||
### What You Can Do
|
||||
|
||||
**Approve** — The booking is updated to the new time and services. The customer's request is cleared.
|
||||
@@ -551,12 +557,17 @@ The request appears in the **Pending Approvals** section on the Today page. You'
|
||||
|
||||
- The new time doesn't clash with another appointment
|
||||
- The new time falls within your working hours
|
||||
- **The new time doesn't fall during a holiday/closed period** — the system will block approval if the proposed time is during exceptional closed hours
|
||||
- If the customer is changing services, the new total duration fits in the slot
|
||||
|
||||
### Can the Customer Withdraw Their Request?
|
||||
|
||||
Yes — a customer can cancel their own reschedule request at any time before you've reviewed it.
|
||||
|
||||
### What Happens When a Booking is Cancelled
|
||||
|
||||
If a customer cancels their booking entirely, any pending reschedule request for that booking is automatically removed, along with the associated time block and notification.
|
||||
|
||||
---
|
||||
|
||||
## The Deposit System (Admin View)
|
||||
|
||||
@@ -97,6 +97,7 @@ flowchart TD
|
||||
- Anonymous reservation cap (50 per 10-minute rolling window)
|
||||
- Auto-status transitions: confirmed → in_progress → completed
|
||||
- Booking edit requests (customers can request reschedule, admin approves/denies)
|
||||
- **Enriched edit requests**: side-by-side original vs proposed snapshots with service details, end-time calculation, and user info
|
||||
- Admin booking service editing with overlap detection and price/duration overrides
|
||||
- Idempotency keys for booking deduplication
|
||||
|
||||
@@ -195,12 +196,12 @@ All flows integrate with holiday/exceptional hours and time blockers.
|
||||
|
||||
## Test Coverage
|
||||
|
||||
**396/399 tests passing** (3 skipped) across 12+ test packages. Comprehensive coverage of online payments (deposit, full, partial, balance), saved card operations, tip payments, terminal payments, refunds, idempotency, webhook handling, and cash/gift card payments.
|
||||
**438/441 tests passing** (3 skipped) across 12+ test packages. Comprehensive coverage of online payments (deposit, full, partial, balance), saved card operations, tip payments, terminal payments, refunds, idempotency, webhook handling, cash/gift card payments, and enriched edit request workflows.
|
||||
|
||||
| Package | Coverage Area |
|
||||
|---------|--------------|
|
||||
| `handlers/auth` | Authentication (login, register, refresh, verification) |
|
||||
| `handlers/bookings` | User booking flow, guest bookings, reservations, edit requests, discounts, closing hours validation, active booking limits |
|
||||
| `handlers/bookings` | User booking flow, guest bookings, reservations, edit requests (create/delete/view/enriched), approval/rejection, time-blocker lifecycle, exceptional hours validation, cancellation cleanup, cross-user isolation |
|
||||
| `handlers/payments` | Square payments (terminal, online, refunds, tips, saved cards) |
|
||||
| `internal/square` | Square client interface, dev mock, prod stub |
|
||||
| `handlers/admin` | Admin bookings, today view, users, services |
|
||||
|
||||
@@ -209,6 +209,8 @@ src/lib/components/
|
||||
| DELETE | `/api/bookings/{id}` | Cancel booking (with forgiveness option) |
|
||||
| POST | `/api/bookings/{id}/edit-request` | Request booking reschedule |
|
||||
| DELETE | `/api/bookings/{id}/edit-request` | Cancel edit request |
|
||||
| GET | `/api/bookings/{id}/edit-request` | View own pending edit request (enriched) |
|
||||
| GET | `/api/bookings/edit-requests` | List all own pending edit requests (enriched) |
|
||||
| POST | `/api/bookings/{id}/payment` | Create online payment (deposit, full, partial, balance) |
|
||||
| POST | `/api/bookings/{id}/tip` | Add tip to completed booking |
|
||||
| GET | `/api/bookings/{id}/payment-summary` | Get payment summary for booking |
|
||||
@@ -235,7 +237,9 @@ src/lib/components/
|
||||
| POST | `/api/admin/bookings/{id}/confirm` | Confirm booking |
|
||||
| POST | `/api/admin/bookings/{id}/cancel` | Cancel booking |
|
||||
| POST | `/api/admin/bookings/reserve` | Reserve slot (walkin=5min, callin=1h) |
|
||||
| GET | `/api/admin/bookings/{id}/edit-requests` | List edit requests |
|
||||
| GET | `/api/admin/bookings/{id}/edit-requests` | List edit requests (paginated, with total) |
|
||||
| GET | `/api/admin/bookings/edit-requests` | List ALL edit requests across all bookings (enriched) |
|
||||
| GET | `/api/admin/bookings/{id}/edit-request` | View pending edit request for specific booking (enriched) |
|
||||
| POST | `/api/admin/bookings/{id}/edit-requests/{request_id}/approve` | Approve edit request |
|
||||
| POST | `/api/admin/bookings/{id}/edit-requests/{request_id}/deny` | Deny edit request |
|
||||
| GET | `/api/admin/users` | List users |
|
||||
@@ -516,6 +520,58 @@ Users manage their preferred notification channels via `/account` → Admin tab
|
||||
- `GET /api/user/notification-preferences` — Returns `{emailEnabled, smsEnabled, browserPushEnabled}`. Defaults to all `true` if no row exists.
|
||||
- `PUT /api/user/notification-preferences` — Accepts partial updates (only provided fields change, unset fields retain current value). Upserts on first call.
|
||||
|
||||
### Enriched Edit Request System
|
||||
|
||||
**How it works:** When a user requests a booking edit (time change, notes, or services), the system creates a `booking_edit_requests` row and returns an **enriched response** with side-by-side `original` and `proposed` snapshots. Each snapshot includes start/end times, full service details (name, price, duration), and notes.
|
||||
|
||||
**Enriched Response Types:**
|
||||
|
||||
```go
|
||||
type EditServiceDetail struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
DurationMinutes int `json:"duration_minutes"`
|
||||
}
|
||||
|
||||
type EditSnapshot struct {
|
||||
StartTime *time.Time `json:"start_time"`
|
||||
EndTime *time.Time `json:"end_time"`
|
||||
Services []EditServiceDetail `json:"services"`
|
||||
Notes *string `json:"notes"`
|
||||
}
|
||||
|
||||
type EnrichedEditRequest struct {
|
||||
ID string `json:"id"`
|
||||
BookingID string `json:"booking_id"`
|
||||
RequestedBy string `json:"requested_by"`
|
||||
RequestedAt time.Time `json:"requested_at"`
|
||||
Notes *string `json:"notes"`
|
||||
Original *EditSnapshot `json:"original"`
|
||||
Proposed *EditSnapshot `json:"proposed"`
|
||||
User *EditUserSummary `json:"user,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**End-time calculation:** `end_time = start_time + sum(service durations)`. If total duration is 0, falls back to 60 minutes.
|
||||
|
||||
**`has_overrides` branch:** When a booking has override prices/durations on its services, the `proposed` snapshot uses the original booking services (not the `new_services` array) since service changes are blocked for overridden bookings.
|
||||
|
||||
**New endpoints:**
|
||||
|
||||
| Endpoint | Auth | Response |
|
||||
|----------|------|----------|
|
||||
| `GET /api/bookings/{id}/edit-request` | User (owner only) | `{"edit_request": EnrichedEditRequest}` |
|
||||
| `GET /api/bookings/edit-requests` | User (own only) | `{"edit_requests": [EnrichedEditRequest]}` |
|
||||
| `GET /api/admin/bookings/edit-requests` | Admin | `{"edit_requests": [EnrichedEditRequest]}` |
|
||||
| `GET /api/admin/bookings/{id}/edit-request` | Admin | `{"edit_request": EnrichedEditRequest}` |
|
||||
|
||||
**Cancellation cleanup:** When a user cancels their booking (`UserCancelBookingHandler`), any pending edit request, associated `RESERVATION:edit_request` time_blocker, and `edit_requested` admin_notification are all deleted.
|
||||
|
||||
**Notification upsert:** When a user submits a second edit request (upsert), the old `edit_requested` notification is deleted and a fresh one is created — admins see a single refreshed notification with an updated timestamp, never duplicates.
|
||||
|
||||
**Exceptional hours validation:** When admin approves an edit request, the proposed time is checked against `exceptional_working_hours`. If the time falls during a closed period, approval is rejected with 409 Conflict.
|
||||
|
||||
### Loyalty & Discount System
|
||||
|
||||
**Loyalty Stamps:**
|
||||
@@ -640,7 +696,7 @@ go test -tags "test,dev" -v -p 1 -count=2 ./... # Run twice for flaky detection
|
||||
|
||||
### Test Coverage
|
||||
|
||||
**396/399 tests passing** (3 skipped) across 12+ packages. Comprehensive coverage of online payments (deposit, full, partial, balance), saved card operations, tip payments, terminal payments, refunds, idempotency, webhook handling, and cash/gift card payments.
|
||||
**438/441 tests passing** (3 skipped) across 12+ packages. Comprehensive coverage of online payments (deposit, full, partial, balance), saved card operations, tip payments, terminal payments, refunds, idempotency, webhook handling, and cash/gift card payments.
|
||||
- `handlers/auth` — Authentication
|
||||
- `handlers/bookings` — User booking flow, guest bookings, reservations, edit requests, discounts, closing hours validation, active booking limits
|
||||
- `handlers/payments` — Square payments (terminal, online, refunds, tips, saved cards)
|
||||
|
||||
@@ -234,19 +234,23 @@ If you need to change your appointment time:
|
||||
1. Go to your **Account** page and find the booking
|
||||
2. Select **Reschedule**
|
||||
3. Pick a new date and time (the same availability rules apply — the new slot must be open)
|
||||
4. Submit your reschedule request
|
||||
4. Add any notes about the change (optional)
|
||||
5. Submit your reschedule request
|
||||
|
||||
**What happens next:**
|
||||
- Your request goes to the salon for review
|
||||
- The salon sees a side-by-side comparison of your original booking versus the proposed changes
|
||||
- The salon can either **approve** or **decline** it
|
||||
- If approved, your appointment time is updated to the new slot
|
||||
- If declined, your original appointment time stays the same
|
||||
- You can cancel your reschedule request at any time before the salon reviews it
|
||||
- You can view all your pending reschedule requests from your account
|
||||
|
||||
**Things to know:**
|
||||
- You can't reschedule a completed or cancelled appointment
|
||||
- The new time must not clash with any of your other existing appointments
|
||||
- If the salon has already adjusted the price or duration of your booking, those adjustments are respected in the reschedule
|
||||
- If you cancel your booking entirely, any pending reschedule request is automatically removed
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user