fix: open booking edit modal via fetch (HTMX wasn't bound)#41
Merged
Conversation
The booking modal is injected with vanilla fetch + innerHTML, so HTMX never processed the hx-get on the Edit button — clicking it did nothing. Switch to a plain onclick + openBookingEditModal helper that mirrors openBookingModal, keeping all modal swaps on the same code path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 tasks
Ceesaxp
added a commit
that referenced
this pull request
May 7, 2026
… + E) (#47) * refactor(calendar): extract calendar fan-out into CalendarEventSyncer Phase A of the host-driven event scheduling work (see plan in /root/.claude/plans/here-is-a-draft-cryptic-crane.md). Pure refactor; no behavioural change for bookings. - Introduce CalendarEventInput as a provider-agnostic event shape and refactor CreateEventForHost / UpdateEvent / the private Google + CalDAV helpers to take it. CreateEvent(*BookingWithDetails) stays as a thin booking-side adapter; new BuildCalendarEventInputForBooking composes description + reschedule URL + attendees from the booking shape. - Add the calendarEventWriter interface so the syncer can be tested without hitting the network. - New CalendarEventSyncer.Create / Update / Delete fans out across all host targets and persists tracking rows in booking_calendar_events. The Update path explicitly persists any replacement event_id returned by the calendar layer (CalDAV delete+create or fall-through to create when the prior ID was empty), so subsequent updates and deletes target the correct provider event. - BookingService loses its three duplicate fan-out helpers (createPooledHostCalendarEvents, updateAllCalendarEvents, deleteAllCalendarEvents) and now goes through the syncer. The legacy pre-pooled-host fallback (booking.CalendarEventID with template.CalendarID) is preserved for old rows. - Regression tests cover one-host create, pooled-host fan-out, per-host failure isolation, the CalDAV replacement-id and prior-id-empty persistence invariants, delete fan-out, and Google Meet conference link surfacing. https://claude.ai/code/session_01YWosUoHN5qrc7bTrTv1hzM * feat(hosted-events): schema, models, and repositories Phase B of the host-driven event scheduling work. DB-only — no service or handler wiring yet. - Migration 014 adds hosted_events, hosted_event_attendees, and hosted_event_calendar_events for both postgres (root migrations/) and sqlite (migrations/sqlite/). status enum is intentionally narrow for v1 (scheduled | cancelled). - Models HostedEvent, HostedEventAttendee, HostedEventCalendarEvent, and HostedEventStatus mirror the booking shapes' SQLiteTime / db tag / json tag style. - HostedEventRepository: Create, GetByID, Update, ListByHost, GetByHostIDAndTimeRange (with excludeEventID for self-collision filtering on edits), GetUpcomingForReminders, MarkReminderSent. - HostedEventAttendeeRepository.ReplaceForEvent: atomic delete-and-insert in a transaction. Source of truth for the attendee diff is whatever this method commits. - HostedEventCalendarEventRepository: Create / GetByHostedEventID / Update / DeleteByHostedEventID — same shape as BookingCalendarEventRepository. - Register the hostedEventTrackingStore on CalendarEventSyncer so the ItemKindHostedEvent dispatch is now wired (was a stub in Phase A). - Round-trip tests for the new repos under sqlite. Note on placeholders: q(driver, sql) does plain regex substitution of $N to ?, so under sqlite the bind order follows the order placeholders appear in the query — not their numeric value. Out-of-order $1/$3/$2 silently mis-binds. New time-range methods order placeholders strictly ascending and document this constraint inline. https://claude.ai/code/session_01YWosUoHN5qrc7bTrTv1hzM * feat(hosted-events): HostedEventService + email + reminder integration Phase C + F of the host-driven event scheduling work, building on PR #43's CalendarEventSyncer + repos. No UI yet — that's the next PR. Service (internal/services/hosted_event.go): - HostedEventService with Create / Update / Cancel / Get / List / Archive / Unarchive / RetryCalendarEvent / DetectBusyConflicts. - Calendar resolution: explicit input → template → host default → first writable (the syncer's resolveWritableCalendarID handles the last fallback). - Conferencing-link write happens twice: once after CreateMeetingForHostedEvent (Zoom path) and again after the syncer surfaces a Google Meet link from the calendar create response — matches the booking flow's coupling. - ErrConferencingReauthRequired on create logs and continues; the event still persists. On Update it surfaces (parity with booking.go:425). - Update flow computes field + attendee diffs (lowercased email keys), resets reminder_sent when Start changes, and only upserts contacts for newly added attendees so retained attendees don't get meeting_count re-incremented on a benign edit. - Email diff: removed → SendHostedEventCancelledForAttendee, added → SendHostedEventInvited, retained (when material field changed) → SendHostedEventUpdated. - DetectBusyConflicts merges three sources (provider busy, confirmed bookings, scheduled hosted events) with excludeEventID for self-edit. - Service depends on a narrow hostedEventEmailSender interface so tests can inject a spy without dialling SMTP. Conferencing (internal/services/conferencing.go): - Extracted createZoomMeetingRaw(ctx, hostID, topic, start, durationMin) so both bookings and hosted events share the Zoom API call. - New CreateMeetingForHostedEvent(ctx, hostID, title, start, duration, locationType, customLocation). CreateMeeting(details) keeps its existing signature and now delegates to createZoomMeetingRaw internally. Contacts (internal/services/contact.go): - New UpsertFromHostedEventAttendee. Callers invoke this only for newly added attendees so meeting_count and last_met don't drift on edits. Email (internal/services/email.go): - SendHostedEventInvited / Updated / Cancelled / CancelledForAttendee / Reminder, plus generateICSForHostedEvent. Inline-Go-string pattern matching SendBookingConfirmed at email.go:184. Cancellation paths emit METHOD:CANCEL ICS so calendar clients remove the event. Reminder (internal/services/reminder.go): - checkAndSendReminders now splits into processBookingReminders + processHostedEventReminders so a partial failure in one source doesn't abort the other. Tests: - internal/services/hosted_event_test.go: 13 cases covering persistence, calendar resolution priority, ErrConferencingReauthRequired handling, Google Meet link surfacing through the syncer, invited-email fan-out, contact upsert, attendee-diff email routing (added/removed/retained), reminder_sent reset on Start change, no-op-update sends no email, Update.PatchesAllTrackedCalendarEvents, retained-attendee contact not re-upserted, Cancel deletes tracking rows + cancellation emails, DetectBusyConflicts excludes self when editing. - internal/services/reminder_test.go: end-to-end pass through processHostedEventReminders flips reminder_sent and prevents re-fire. go vet clean. All previously-passing tests still pass. Build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(hosted-events): dashboard handler, routes, and UI templates (Phase D + E) Phase D + E of host-driven event scheduling. Builds on the HostedEventService introduced in #44 and exposes it through 12 dashboard routes plus 6 HTML templates. Sidebar gets a "Schedule Event" item between Templates and Bookings. ## Routes (cmd/server/main.go) - GET /dashboard/events — list (?archived=true to include) - GET /dashboard/events/new — create form (?template=ID prefills) - POST /dashboard/events — create - GET /dashboard/events/check-conflicts — HTMX partial; busy-time merge - GET /dashboard/events/attendee-search — HTMX partial; contact autocomplete - GET /dashboard/events/{id}/details — modal partial - GET /dashboard/events/{id}/edit — full edit form - POST /dashboard/events/{id}/edit — update - POST /dashboard/events/{id}/cancel — cancel - POST /dashboard/events/{id}/archive — archive - POST /dashboard/events/{id}/unarchive — unarchive - POST /dashboard/events/{id}/retry-calendar — retry calendar event creation The HTMX partial routes use the /events/check-conflicts and /events/attendee-search paths so they don't shadow {id}. ## Handler (internal/handlers/dashboard_events.go) DashboardEventsHandler wired on the Handlers struct alongside Dashboard. Form parsing uses parallel-indexed repeated form fields (attendee_email/_name/_contact_id), matching the existing pattern used by the templates handler. parseDateTimeInTZ combines the form's date + time + timezone fields into a UTC time.Time before handing to the service. Update flow uses pointer fields on UpdateHostedEventInput so the form can express "unchanged" vs "set to empty". ## Templates (Phase E) All six new templates reuse the existing primitives — `.section`, `.section-header`, `.form-input`, `.form-select`, `.form-textarea`, `.form-label`, `.form-hint`, `.btn .btn-primary/.btn-secondary/.btn-outline`, `.btn-sm`, `.badge`, `.table-container/.table`, `.duration-chips`, `.radio-cards`, `.modal-overlay/.modal-content/.modal-header/.modal-body`, `.empty-state`, `.alert/.alert-warning/.alert-error`. No new CSS files; the attendee chip styling is a small inline block confined to the form template. - templates/pages/dashboard_events.html — list + filter view - templates/pages/dashboard_event_form.html — create + edit form - templates/partials/events_table_partial.html — table body - templates/partials/event_attendee_picker_results.html — autocomplete results - templates/partials/event_conflict_warning.html — soft conflict banner - templates/partials/dashboard_event_detail_partial.html — modal detail The form has 7 sections (Template prefill (new only) → Basics → When → Location → Calendar → Attendees) with HTMX-driven conflict warnings and contact autocomplete. Modal pattern matches the booking flow's recent fetch-based approach (#41 pattern). ## Tests internal/handlers/dashboard_events_test.go — 9 cases: - TestEventAttendeePickerResults_RendersContacts - TestEventAttendeePickerResults_EmptyShowsFreeFormHint - TestEventAttendeePickerResults_EmptyAndNoQuery_RendersNothing - TestEventConflictWarning_RendersConflicts (asserts pluralisation + soft-warning copy) - TestEventConflictWarning_NoConflicts_RendersEmpty - TestEventsTablePartial_RendersScheduledAndCancelled (status-routed badges + actions) - TestEventForm_NewMode_RendersTitleAndAttendeePicker - TestEventForm_EditMode_PrefillsAndCarriesExcludeEventID (zoom + 45-min + exclude_event_id) - TestEventForm_FormErrorRendersHumanCopy (at_least_one_attendee → human copy) Pattern matches internal/handlers/template_test.go: load real template files, render to bytes.Buffer, assert strings. Tests render the {{define "content"}} blocks directly to avoid the layout-loading dependency. `make test`, `go vet ./...`, `go build ./cmd/server` all clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Edit button on the booking details modal did nothing when clicked.
The modal is loaded by `openBookingModal` in `dashboard_bookings.html` via vanilla `fetch` + `innerHTML` — HTMX is not invoked, so the `hx-get` attribute I added to the Edit button was never processed and the click was a no-op.
Switched the Edit button to a plain `onclick` calling a new `openBookingEditModal` helper that mirrors the existing `openBookingModal` exactly. Keeps both modal transitions on the same code path.
Test plan
🤖 Generated with Claude Code