diff --git a/.github/prompts/14-admin-booking-management.prompt.md b/.github/prompts/14-admin-booking-management.prompt.md new file mode 100644 index 0000000..c70a984 --- /dev/null +++ b/.github/prompts/14-admin-booking-management.prompt.md @@ -0,0 +1,77 @@ +--- +agent: agent +description: Manage booked dates in the authenticated Date Wizard admin page, including newest-first display, activity and booking metadata, and confirmation-gated deletion. +--- + +Implement or review booked-date management for the Date Wizard application. Preserve the existing Go server, JSON booking persistence, authenticated admin behavior, German user-facing language, and visual conventions. + +## Admin navigation + +Add a separate booking-management view to the authenticated `/admin` page. Keep it between the existing `Termine` and `Aktivitäten` navigation items. The view is reached through a visible navigation button and must remain responsive on narrow screens. + +The booking view is initially read-only except for deleting a booking. Do not add editing controls for the booking date, duration, name, activities, or booking timestamp. + +## Booking list + +Load bookings through an authenticated admin endpoint and display them newest first using the booking creation timestamp `bookedAt`. + +Each booking entry must: + +- Display the selected date as the primary information, formatted in German. +- Display every selected activity when one or more activities are present. +- Display each activity's stored icon and name, including the single-activity case and multiple-activity case. +- Display the booker's name only when a non-empty name is available. +- Display the date and time at which the booking was created from `bookedAt`. +- Display a useful empty state when no bookings exist. +- Render all values as text content rather than injecting untrusted booking data as HTML. + +Keep legacy records with missing optional values displayable. Do not fabricate a booking timestamp when `bookedAt` is empty. + +## Backend API + +Use the existing authenticated Go HTTP API and booking storage helpers. + +- Add or preserve an authenticated `GET /api/admin/bookings` endpoint. +- Return structured booking records containing `bookedAt`, `date`, `dateType`, `name`, and `activities`. +- Sort records newest first by `bookedAt`, while keeping records with missing timestamps valid. +- Do not expose the full booking records through the public `/api/booked-dates` endpoint; that endpoint may continue returning only date strings. +- Reject unauthenticated requests with the existing JSON authentication response. +- Return appropriate errors for malformed requests and storage failures. + +## Booking deletion + +Provide one `Termin löschen` button for every booking entry. + +- Ask for explicit confirmation before sending a delete request. +- Include the formatted booking date in the confirmation question. +- Add an authenticated `DELETE /api/admin/bookings` endpoint accepting `{ "date": "YYYY-MM-DD" }`. +- Delete only the matching booking record from `config/dates_booked.json`. +- Return a not-found response when the requested booking does not exist. +- Keep the delete button disabled while the request is in progress. +- Reload the booking list after successful deletion. +- Remove the deleted date from the admin calendar's booked-date state and refresh the affected date displays. +- Keep confirmation and deletion errors visible to the administrator. +- Do not delete or modify any other booking record. + +## Persistence and safety + +Preserve the existing structured booking format and booking uniqueness rule: at most one booking record exists for a date. Use the existing booking mutex and atomic JSON write helper for deletion. Do not modify `config/appsettings.json` or expose the admin password. + +## Implementation and validation + +Before editing, inspect the existing admin HTML, JavaScript, CSS, Go handlers, booking model, storage helpers, public booked-date endpoint, and prompt conventions. Reuse existing request helpers and styling patterns. + +After editing: + +1. Run `gofmt` on changed Go files. +2. Run the available Go test or build command. +3. Run a JavaScript syntax check for changed scripts when Node.js is available. +4. Check diagnostics for changed files. +5. Verify unauthenticated access still shows the login view. +6. Verify the booking tab appears between `Termine` and `Aktivitäten`. +7. Verify newest-first ordering, date display, single and multiple activity display with icons, optional name display, and booking timestamp display. +8. Verify the delete confirmation prevents deletion when cancelled and deletes only the confirmed booking when accepted. +9. Verify the calendar booked state updates after deletion. +10. Do not alter `config/appsettings.json` or unrelated fixtures when documenting or committing this feature. + +When reporting the result, summarize changed files, user-visible behavior, API behavior, persistence effects, and validation commands that passed or failed. diff --git a/config/activities.json b/config/activities.json index bec039f..b575d9f 100644 --- a/config/activities.json +++ b/config/activities.json @@ -1,7 +1,7 @@ [ { "name": "Cocktailabend zu zweit", - "icon": "🚲", + "icon": "🍸", "type": "long" }, { @@ -11,11 +11,11 @@ }, { "name": "Ausgedehnte Wanderung", - "icon": "🛋️", + "icon": "🌲", "type": "long" }, { - "name": "Kinofilm", + "name": "Kinofilm gucken", "icon": "🎬", "type": "short" }, @@ -52,7 +52,8 @@ { "name": "Chillen im Freibad", "icon": "🏊", - "type": "short" + "type": "short", + "disabled": true }, { "name": "Film zu Hause + Essen bestellen", @@ -80,13 +81,8 @@ "type": "short" }, { - "name": "l", + "name": "Etwas eigenes", "icon": "✏️", "type": "custom" - }, - { - "name": "asdss", - "icon": "📚", - "type": "short" } ] \ No newline at end of file diff --git a/config/dates_available.json b/config/dates_available.json index 57ae781..438e187 100644 --- a/config/dates_available.json +++ b/config/dates_available.json @@ -92,23 +92,135 @@ "type": "short" }, { - "date": "2026-10-03", - "type": "long" - }, - { - "date": "2026-10-04", - "type": "long" - }, - { - "date": "2026-10-10", - "type": "long" - }, - { - "date": "2026-10-21", + "date": "2026-10-02", "type": "short" }, { - "date": "2026-10-22", + "date": "2026-10-06", + "type": "short" + }, + { + "date": "2026-10-09", + "type": "short" + }, + { + "date": "2026-10-11", + "type": "short" + }, + { + "date": "2026-10-12", + "type": "short" + }, + { + "date": "2026-10-13", + "type": "short" + }, + { + "date": "2026-10-14", + "type": "short" + }, + { + "date": "2026-10-16", + "type": "short" + }, + { + "date": "2026-10-26", + "type": "short" + }, + { + "date": "2026-10-27", + "type": "short" + }, + { + "date": "2026-10-28", + "type": "short" + }, + { + "date": "2026-10-29", + "type": "short" + }, + { + "date": "2026-10-30", + "type": "short" + }, + { + "date": "2026-11-02", + "type": "short" + }, + { + "date": "2026-11-03", + "type": "short" + }, + { + "date": "2026-11-04", + "type": "short" + }, + { + "date": "2026-11-05", + "type": "short" + }, + { + "date": "2026-11-06", + "type": "short" + }, + { + "date": "2026-11-08", "type": "long" + }, + { + "date": "2026-11-09", + "type": "short" + }, + { + "date": "2026-11-10", + "type": "short" + }, + { + "date": "2026-11-11", + "type": "short" + }, + { + "date": "2026-11-12", + "type": "short" + }, + { + "date": "2026-11-17", + "type": "short" + }, + { + "date": "2026-11-18", + "type": "short" + }, + { + "date": "2026-11-23", + "type": "short" + }, + { + "date": "2026-11-24", + "type": "short" + }, + { + "date": "2026-11-25", + "type": "short" + }, + { + "date": "2026-11-26", + "type": "short" + }, + { + "date": "2026-11-27", + "type": "short" + }, + { + "date": "2026-11-28", + "type": "long" + }, + { + "date": "2026-11-29", + "type": "long" + }, + { + "date": "2026-11-30", + "type": "short" } -] +] \ No newline at end of file diff --git a/config/dates_booked.json b/config/dates_booked.json index ae78a3c..0637a08 100644 --- a/config/dates_booked.json +++ b/config/dates_booked.json @@ -1,27 +1 @@ -[ - { - "bookedAt": "", - "date": "2026-10-10", - "dateType": "long", - "name": "", - "activities": [] - }, - { - "bookedAt": "2026-10-01T16:10:50Z", - "date": "2026-10-22", - "dateType": "long", - "name": "Du weißt, wer ich bin", - "activities": [ - { - "name": "Chillen im Freibad", - "icon": "🏊", - "type": "short" - }, - { - "name": "Sushi selber machen", - "icon": "🍣", - "type": "short" - } - ] - } -] +[] \ No newline at end of file diff --git a/main.go b/main.go index c98a2cc..b52829a 100644 --- a/main.go +++ b/main.go @@ -101,6 +101,7 @@ func main() { http.HandleFunc("/api/admin/dates", handleAdminDates) http.HandleFunc("/api/admin/dates/bulk", handleAdminBulkDates) http.HandleFunc("/api/admin/activities", handleAdminActivities) + http.HandleFunc("/api/admin/bookings", handleAdminBookings) http.HandleFunc("/admin", handleAdminPage) // Serve static files from webDir directory @@ -287,6 +288,65 @@ func handleAdminActivities(w http.ResponseWriter, r *http.Request) { } } +func handleAdminBookings(w http.ResponseWriter, r *http.Request) { + if !isAdminRequest(r) { + writeJSON(w, http.StatusUnauthorized, Response{Success: false, Message: "Anmeldung erforderlich."}) + return + } + switch r.Method { + case http.MethodGet: + bookings, err := getBookings() + if err != nil { + log.Printf("Error reading bookings: %v", err) + http.Error(w, "Bookings could not be loaded", http.StatusInternalServerError) + return + } + sort.SliceStable(bookings, func(first, second int) bool { + return bookings[first].BookedAt > bookings[second].BookedAt + }) + writeJSON(w, http.StatusOK, bookings) + case http.MethodDelete: + var request struct { + Date string `json:"date"` + } + if err := json.NewDecoder(r.Body).Decode(&request); err != nil || strings.TrimSpace(request.Date) == "" { + writeJSON(w, http.StatusBadRequest, Response{Success: false, Message: "Bitte gib ein Datum an."}) + return + } + request.Date = strings.TrimSpace(request.Date) + + bookingsMu.Lock() + defer bookingsMu.Unlock() + bookings, err := readBookings() + if err != nil { + log.Printf("Error reading bookings: %v", err) + http.Error(w, "Bookings could not be loaded", http.StatusInternalServerError) + return + } + filtered := make([]Booking, 0, len(bookings)) + deleted := false + for _, booking := range bookings { + if booking.Date == request.Date { + deleted = true + continue + } + filtered = append(filtered, booking) + } + if !deleted { + writeJSON(w, http.StatusNotFound, Response{Success: false, Message: "Gebuchter Termin wurde nicht gefunden."}) + return + } + if err := writeJSONFile(getBookedDatesFilePath(), filtered); err != nil { + log.Printf("Error deleting booking: %v", err) + http.Error(w, "Booking could not be deleted", http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, Response{Success: true, Message: "Gebuchter Termin gelöscht."}) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + func handleAdminLogin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) diff --git a/public/admin.css b/public/admin.css index 6864efe..5dba85f 100644 --- a/public/admin.css +++ b/public/admin.css @@ -262,6 +262,68 @@ color: var(--color-text-light); } +.bookings-list { + display: grid; + gap: 14px; + margin: 18px 0 0; + padding: 0; + list-style: none; +} + +.booking-row { + display: grid; + gap: 8px; + padding: 14px 0; + border-bottom: 1px solid #e5d8ce; +} + +.booking-date { + font-family: var(--font-heading); + font-size: 1.08rem; +} + +.booking-activities { + display: flex; + flex-wrap: wrap; + gap: 6px 12px; + color: var(--color-text); +} + +.booking-activity { + font-weight: 600; +} + +.booking-metadata { + display: flex; + flex-wrap: wrap; + gap: 4px 14px; + color: var(--color-text-light); + font-size: 0.88rem; +} + +.delete-booking-btn { + justify-self: start; + min-height: 36px; + padding: 6px 10px; + border: 1px solid #c84a62; + border-radius: 5px; + background: transparent; + color: #a83049; + font: inherit; + font-weight: 700; + cursor: pointer; +} + +.delete-booking-btn:hover, +.delete-booking-btn:focus-visible { + background: #fff0f2; +} + +.delete-booking-btn:disabled { + opacity: 0.55; + cursor: wait; +} + .admin-calendar { margin-top: 20px; padding: 16px; diff --git a/public/admin.html b/public/admin.html index 1a66dec..98c6df4 100644 --- a/public/admin.html +++ b/public/admin.html @@ -35,6 +35,7 @@ @@ -73,6 +74,17 @@ + +