Add admin booking management prompt and deletion
This commit is contained in:
@@ -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.
|
||||||
+6
-10
@@ -1,7 +1,7 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "Cocktailabend zu zweit",
|
"name": "Cocktailabend zu zweit",
|
||||||
"icon": "🚲",
|
"icon": "🍸",
|
||||||
"type": "long"
|
"type": "long"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Ausgedehnte Wanderung",
|
"name": "Ausgedehnte Wanderung",
|
||||||
"icon": "🛋️",
|
"icon": "🌲",
|
||||||
"type": "long"
|
"type": "long"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Kinofilm",
|
"name": "Kinofilm gucken",
|
||||||
"icon": "🎬",
|
"icon": "🎬",
|
||||||
"type": "short"
|
"type": "short"
|
||||||
},
|
},
|
||||||
@@ -52,7 +52,8 @@
|
|||||||
{
|
{
|
||||||
"name": "Chillen im Freibad",
|
"name": "Chillen im Freibad",
|
||||||
"icon": "🏊",
|
"icon": "🏊",
|
||||||
"type": "short"
|
"type": "short",
|
||||||
|
"disabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Film zu Hause + Essen bestellen",
|
"name": "Film zu Hause + Essen bestellen",
|
||||||
@@ -80,13 +81,8 @@
|
|||||||
"type": "short"
|
"type": "short"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "l",
|
"name": "Etwas eigenes",
|
||||||
"icon": "✏️",
|
"icon": "✏️",
|
||||||
"type": "custom"
|
"type": "custom"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "asdss",
|
|
||||||
"icon": "📚",
|
|
||||||
"type": "short"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
+127
-15
@@ -92,23 +92,135 @@
|
|||||||
"type": "short"
|
"type": "short"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2026-10-03",
|
"date": "2026-10-02",
|
||||||
"type": "long"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-10-04",
|
|
||||||
"type": "long"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-10-10",
|
|
||||||
"type": "long"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2026-10-21",
|
|
||||||
"type": "short"
|
"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"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -101,6 +101,7 @@ func main() {
|
|||||||
http.HandleFunc("/api/admin/dates", handleAdminDates)
|
http.HandleFunc("/api/admin/dates", handleAdminDates)
|
||||||
http.HandleFunc("/api/admin/dates/bulk", handleAdminBulkDates)
|
http.HandleFunc("/api/admin/dates/bulk", handleAdminBulkDates)
|
||||||
http.HandleFunc("/api/admin/activities", handleAdminActivities)
|
http.HandleFunc("/api/admin/activities", handleAdminActivities)
|
||||||
|
http.HandleFunc("/api/admin/bookings", handleAdminBookings)
|
||||||
http.HandleFunc("/admin", handleAdminPage)
|
http.HandleFunc("/admin", handleAdminPage)
|
||||||
|
|
||||||
// Serve static files from webDir directory
|
// 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) {
|
func handleAdminLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
|||||||
@@ -262,6 +262,68 @@
|
|||||||
color: var(--color-text-light);
|
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 {
|
.admin-calendar {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
|
|
||||||
<nav class="admin-nav" aria-label="Verwaltung">
|
<nav class="admin-nav" aria-label="Verwaltung">
|
||||||
<button class="admin-nav-btn is-active" type="button" data-view="datesView">Termine</button>
|
<button class="admin-nav-btn is-active" type="button" data-view="datesView">Termine</button>
|
||||||
|
<button class="admin-nav-btn" type="button" data-view="bookingsView">Gebuchte Termine</button>
|
||||||
<button class="admin-nav-btn" type="button" data-view="activitiesView">Aktivitäten</button>
|
<button class="admin-nav-btn" type="button" data-view="activitiesView">Aktivitäten</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -73,6 +74,17 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="bookingsView" class="admin-panel hidden">
|
||||||
|
<section class="admin-section" aria-labelledby="bookingsTitle">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h2 id="bookingsTitle">Gebuchte Termine</h2>
|
||||||
|
<p>Neueste Buchungen stehen oben.</p>
|
||||||
|
</div>
|
||||||
|
<ul id="bookingsList" class="bookings-list" aria-live="polite"></ul>
|
||||||
|
<p id="emptyBookings" class="empty-dates hidden">Noch keine Termine gebucht.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="activitiesView" class="admin-panel hidden">
|
<div id="activitiesView" class="admin-panel hidden">
|
||||||
<section class="admin-section" aria-labelledby="activitiesTitle">
|
<section class="admin-section" aria-labelledby="activitiesTitle">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
|
|||||||
+74
-1
@@ -19,6 +19,8 @@
|
|||||||
const emptyActivities = document.getElementById("emptyActivities");
|
const emptyActivities = document.getElementById("emptyActivities");
|
||||||
const activityMessage = document.getElementById("activityMessage");
|
const activityMessage = document.getElementById("activityMessage");
|
||||||
const activityAddRow = document.getElementById("activityAddRow");
|
const activityAddRow = document.getElementById("activityAddRow");
|
||||||
|
const bookingsList = document.getElementById("bookingsList");
|
||||||
|
const emptyBookings = document.getElementById("emptyBookings");
|
||||||
const newActivityIcon = document.getElementById("newActivityIcon");
|
const newActivityIcon = document.getElementById("newActivityIcon");
|
||||||
const newActivityName = document.getElementById("newActivityName");
|
const newActivityName = document.getElementById("newActivityName");
|
||||||
const newActivityType = document.getElementById("newActivityType");
|
const newActivityType = document.getElementById("newActivityType");
|
||||||
@@ -33,6 +35,7 @@
|
|||||||
let viewYear = today.getFullYear();
|
let viewYear = today.getFullYear();
|
||||||
let viewMonth = today.getMonth();
|
let viewMonth = today.getMonth();
|
||||||
let activities = [];
|
let activities = [];
|
||||||
|
let bookings = [];
|
||||||
|
|
||||||
function setMessage(element, message, success) {
|
function setMessage(element, message, success) {
|
||||||
element.textContent = message;
|
element.textContent = message;
|
||||||
@@ -149,6 +152,76 @@
|
|||||||
prevMonthBtn.disabled = viewYear === today.getFullYear() && viewMonth === today.getMonth();
|
prevMonthBtn.disabled = viewYear === today.getFullYear() && viewMonth === today.getMonth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderBookings() {
|
||||||
|
bookingsList.innerHTML = "";
|
||||||
|
emptyBookings.classList.toggle("hidden", bookings.length > 0);
|
||||||
|
bookings.forEach((booking) => {
|
||||||
|
const row = document.createElement("li");
|
||||||
|
row.className = "booking-row";
|
||||||
|
|
||||||
|
const date = document.createElement("strong");
|
||||||
|
date.className = "booking-date";
|
||||||
|
date.textContent = formatDate(booking.date);
|
||||||
|
row.appendChild(date);
|
||||||
|
|
||||||
|
if (booking.activities && booking.activities.length > 0) {
|
||||||
|
const activityList = document.createElement("div");
|
||||||
|
activityList.className = "booking-activities";
|
||||||
|
booking.activities.forEach((activity) => {
|
||||||
|
const activityItem = document.createElement("span");
|
||||||
|
activityItem.className = "booking-activity";
|
||||||
|
activityItem.textContent = `${activity.icon || ""} ${activity.name}`.trim();
|
||||||
|
activityList.appendChild(activityItem);
|
||||||
|
});
|
||||||
|
row.appendChild(activityList);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = document.createElement("div");
|
||||||
|
metadata.className = "booking-metadata";
|
||||||
|
if (booking.name && booking.name.trim()) {
|
||||||
|
const name = document.createElement("span");
|
||||||
|
name.textContent = booking.name;
|
||||||
|
metadata.appendChild(name);
|
||||||
|
}
|
||||||
|
if (booking.bookedAt) {
|
||||||
|
const created = document.createElement("time");
|
||||||
|
created.dateTime = booking.bookedAt;
|
||||||
|
created.textContent = `Gebucht am ${new Date(booking.bookedAt).toLocaleString("de-DE", { dateStyle: "medium", timeStyle: "short" })}`;
|
||||||
|
metadata.appendChild(created);
|
||||||
|
}
|
||||||
|
if (metadata.childElementCount > 0) row.appendChild(metadata);
|
||||||
|
|
||||||
|
const deleteButton = document.createElement("button");
|
||||||
|
deleteButton.className = "delete-booking-btn";
|
||||||
|
deleteButton.type = "button";
|
||||||
|
deleteButton.textContent = "Termin löschen";
|
||||||
|
deleteButton.addEventListener("click", () => deleteBooking(booking, deleteButton));
|
||||||
|
row.appendChild(deleteButton);
|
||||||
|
bookingsList.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBooking(booking, deleteButton) {
|
||||||
|
if (!window.confirm(`Den gebuchten Termin am ${formatDate(booking.date)} wirklich löschen?`)) return;
|
||||||
|
deleteButton.disabled = true;
|
||||||
|
try {
|
||||||
|
await request("/api/admin/bookings", { method: "DELETE", body: JSON.stringify({ date: booking.date }) });
|
||||||
|
bookedDates.delete(booking.date);
|
||||||
|
await loadBookings();
|
||||||
|
renderCalendar();
|
||||||
|
renderDates();
|
||||||
|
} catch (error) {
|
||||||
|
deleteButton.disabled = false;
|
||||||
|
if (error.status === 401) showLogin("Deine Anmeldung ist abgelaufen.");
|
||||||
|
else window.alert(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBookings() {
|
||||||
|
bookings = await request("/api/admin/bookings", { method: "GET" });
|
||||||
|
renderBookings();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadDates() {
|
async function loadDates() {
|
||||||
try {
|
try {
|
||||||
const [loadedDates, loadedBookedDates] = await Promise.all([
|
const [loadedDates, loadedBookedDates] = await Promise.all([
|
||||||
@@ -161,7 +234,7 @@
|
|||||||
draftDates = new Map(dates.map((date) => [date.date, date.type]));
|
draftDates = new Map(dates.map((date) => [date.date, date.type]));
|
||||||
loginView.classList.add("hidden");
|
loginView.classList.add("hidden");
|
||||||
adminView.classList.remove("hidden");
|
adminView.classList.remove("hidden");
|
||||||
await loadActivities();
|
await Promise.all([loadActivities(), loadBookings()]);
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
renderDates();
|
renderDates();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user