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 @@