diff --git a/docker-compose.local.yml b/docker-compose.local.yml index af5b4e3..5725c15 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -31,6 +31,7 @@ services: - ./supabase/migrations/14-email-requested-trigger.sql:/sql/14-email-requested-trigger.sql:ro - ./supabase/migrations/15-individuell-vat-subtotal-fix.sql:/sql/15-individuell-vat-subtotal-fix.sql:ro - ./supabase/migrations/16-rental-type-weekend-gap-fix.sql:/sql/16-rental-type-weekend-gap-fix.sql:ro + - ./supabase/migrations/17-vehicle-photos.sql:/sql/17-vehicle-photos.sql:ro kong: volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 60eaa29..a806b35 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -224,6 +224,7 @@ services: - /mnt/user/appdata/mc-cars/supabase/migrations/14-email-requested-trigger.sql:/sql/14-email-requested-trigger.sql:ro - /mnt/user/appdata/mc-cars/supabase/migrations/15-individuell-vat-subtotal-fix.sql:/sql/15-individuell-vat-subtotal-fix.sql:ro - /mnt/user/appdata/mc-cars/supabase/migrations/16-rental-type-weekend-gap-fix.sql:/sql/16-rental-type-weekend-gap-fix.sql:ro + - /mnt/user/appdata/mc-cars/supabase/migrations/17-vehicle-photos.sql:/sql/17-vehicle-photos.sql:ro entrypoint: ["sh","-c"] command: - | @@ -256,6 +257,7 @@ services: psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/14-email-requested-trigger.sql psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/15-individuell-vat-subtotal-fix.sql psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/16-rental-type-weekend-gap-fix.sql + psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/17-vehicle-photos.sql echo "post-init done." restart: "no" networks: [mccars] diff --git a/frontend/admin.html b/frontend/admin.html index b569349..d688560 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -172,11 +172,19 @@
+
+ +
+
+
diff --git a/frontend/admin.js b/frontend/admin.js index 5cfb203..bdd997c 100644 --- a/frontend/admin.js +++ b/frontend/admin.js @@ -53,6 +53,8 @@ const saveBtn = document.querySelector("#saveBtn"); const resetBtn = document.querySelector("#resetBtn"); const photoInput = document.querySelector("#photoInput"); const photoPreview = document.querySelector("#photoPreview"); +const extraPhotoInput = document.querySelector("#extraPhotoInput"); +const extraPhotoGallery = document.querySelector("#extraPhotoGallery"); const tableBody = document.querySelector("#adminTable tbody"); // ----- State ----- @@ -66,6 +68,7 @@ const state = { vehicles: [], vehicleMap: new Map(), currentPhotoPath: null, + vehiclePhotos: [], realtimeChannel: null, forcedRotation: false, }; @@ -322,6 +325,7 @@ function loadForEdit(id) { vehicleForm.is_active.checked = v.is_active; state.currentPhotoPath = v.photo_path || null; updatePreview(v.photo_url); + loadVehiclePhotos(v.id); window.scrollTo({ top: 0, behavior: "smooth" }); } @@ -337,6 +341,7 @@ resetBtn.addEventListener("click", () => { vehicleForm.kaution_eur.value = 5000; vehicleForm.price_per_km_eur.value = 1.50; state.currentPhotoPath = null; + state.vehiclePhotos = []; updatePreview(""); formTitle.textContent = "Neues Fahrzeug"; formFeedback.textContent = ""; @@ -390,7 +395,13 @@ async function deleteVehicle(id) { const v = state.vehicleMap.get(id); if (!v) return; if (!confirm(`Delete ${v.brand} ${v.model}?`)) return; + // Delete old main photo if (v.photo_path) await supabase.storage.from("vehicle-photos").remove([v.photo_path]); + // Delete gallery photos from storage + const { data: photos } = await supabase.from("vehicle_photos").select("photo_path").eq("vehicle_id", id); + if (photos?.length) { + await supabase.storage.from("vehicle-photos").remove(photos.map(p => p.photo_path)); + } const { error } = await supabase.from("vehicles").delete().eq("id", id); if (error) { alert(error.message); return; } await loadVehicles(); @@ -426,6 +437,120 @@ photoInput.addEventListener("change", async () => { }); function updatePreview(url) { photoPreview.style.backgroundImage = url ? `url('${url}')` : ""; } +// ----- Vehicle Photo Gallery ----- +async function loadVehiclePhotos(vehicleId) { + if (!vehicleId) { + state.vehiclePhotos = []; + renderExtraPhotoGallery(); + return; + } + const { data, error } = await supabase + .from("vehicle_photos") + .select("*") + .eq("vehicle_id", vehicleId) + .order("display_order", { ascending: true }); + if (error) { console.error("Failed to load vehicle photos:", error); return; } + state.vehiclePhotos = data || []; + renderExtraPhotoGallery(); +} + +function renderExtraPhotoGallery() { + if (!extraPhotoGallery) return; + extraPhotoGallery.innerHTML = ""; + for (const ph of state.vehiclePhotos) { + const wrapper = document.createElement("div"); + wrapper.style.cssText = "position:relative;border-radius:8px;overflow:hidden;aspect-ratio:16/10;background:#1a1a1a;"; + wrapper.innerHTML = ` + +
+ ${!ph.is_primary ? `` : ''} + +
+ ${ph.is_primary ? 'Hauptfoto' : ''} + `; + extraPhotoGallery.appendChild(wrapper); + } + + // Event listeners + extraPhotoGallery.querySelectorAll("[data-delete-photo]").forEach(btn => { + btn.addEventListener("click", async () => { + const phId = btn.dataset.deletePhoto; + await deleteVehiclePhoto(phId); + }); + }); + extraPhotoGallery.querySelectorAll("[data-set-primary]").forEach(btn => { + btn.addEventListener("click", async () => { + const phId = btn.dataset.setPrimary; + await setPrimaryPhoto(phId); + }); + }); +} + +async function deleteVehiclePhoto(photoId) { + const ph = state.vehiclePhotos.find(p => p.id === photoId); + if (!ph) return; + try { + if (ph.photo_path) { + await supabase.storage.from("vehicle-photos").remove([ph.photo_path]); + } + const { error } = await supabase.from("vehicle_photos").delete().eq("id", photoId); + if (error) throw error; + state.vehiclePhotos = state.vehiclePhotos.filter(p => p.id !== photoId); + renderExtraPhotoGallery(); + } catch (err) { + console.error("Failed to delete photo:", err); + } +} + +async function setPrimaryPhoto(photoId) { + const vid = vehicleForm.vid?.value; + if (!vid) return; + try { + await supabase.rpc("set_primary_vehicle_photo", { p_vehicle_id: vid, p_photo_id: photoId }); + await loadVehiclePhotos(vid); + } catch (err) { + console.error("Failed to set primary photo:", err); + } +} + +// Extra photos upload +extraPhotoInput?.addEventListener("change", async () => { + const files = extraPhotoInput.files; + if (!files.length) return; + const vid = vehicleForm.vid?.value; + if (!vid) { + formFeedback.className = "form-feedback error"; + formFeedback.textContent = "Bitte zuerst Fahrzeug speichern, dann Fotos hinzufügen."; + return; + } + formFeedback.className = "form-feedback"; + formFeedback.textContent = "Uploading photos..."; + for (const file of files) { + try { + const ext = (file.name.split(".").pop() || "jpg").toLowerCase(); + const path = `${vid}/${crypto.randomUUID()}.${ext}`; + const { error: upErr } = await supabase.storage + .from("vehicle-photos") + .upload(path, file, { contentType: file.type, upsert: true }); + if (upErr) throw upErr; + const { data: pub } = supabase.storage.from("vehicle-photos").getPublicUrl(path); + const maxOrder = state.vehiclePhotos.reduce((m, p) => Math.max(m, p.display_order), -1); + await supabase.from("vehicle_photos").insert({ + vehicle_id: vid, + photo_url: pub.publicUrl, + photo_path: path, + display_order: maxOrder + 1, + is_primary: state.vehiclePhotos.length === 0, + }); + } catch (err) { + console.error("Upload failed:", err); + } + } + await loadVehiclePhotos(vid); + formFeedback.textContent = `${files.length} Foto(s) hochgeladen.`; + extraPhotoInput.value = ""; +}); + // ========================================================================= // LEADS // ========================================================================= diff --git a/frontend/agb.html b/frontend/agb.html index fd2fcd2..5f5b1b8 100644 --- a/frontend/agb.html +++ b/frontend/agb.html @@ -4,8 +4,8 @@ AGB · MC Cars - - + + @@ -51,13 +51,12 @@