From e85b319c9399087b2395976be51a154f5f18a562 Mon Sep 17 00:00:00 2001 From: LagoESP Date: Wed, 29 Apr 2026 16:00:06 +0200 Subject: [PATCH] feat: add deposit and weekend km allowance to vehicles, update migrations and booking flow Co-authored-by: Copilot --- docker-compose.local.yml | 1 + docker-compose.yml | 3 ++ frontend/admin.html | 10 +++-- frontend/admin.js | 12 +++++- frontend/app.js | 14 ++++++- frontend/i18n.js | 6 ++- supabase/migrations/01-init.sql | 38 ++++--------------- supabase/migrations/03-booking-flow.sql | 6 +++ supabase/migrations/04-kaution-weekend-km.sql | 27 +++++++++++++ supabase/migrations/post-boot.sql | 5 +++ 10 files changed, 83 insertions(+), 39 deletions(-) create mode 100644 supabase/migrations/04-kaution-weekend-km.sql diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 8b0183f..0469f94 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -18,6 +18,7 @@ services: - ./supabase/migrations/post-boot.sql:/sql/post-boot.sql:ro - ./supabase/migrations/02-leads.sql:/sql/02-leads.sql:ro - ./supabase/migrations/03-booking-flow.sql:/sql/03-booking-flow.sql:ro + - ./supabase/migrations/04-kaution-weekend-km.sql:/sql/04-kaution-weekend-km.sql:ro kong: volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 08bbb4d..e9067ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -107,6 +107,7 @@ services: PGRST_DB_ANON_ROLE: anon PGRST_JWT_SECRET: ${JWT_SECRET} PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_DB_CHANNEL_ENABLED: "true" networks: [mccars] logging: { driver: json-file, options: { max-size: "10m", max-file: "3" } } @@ -210,6 +211,7 @@ services: - /mnt/user/appdata/mc-cars/supabase/migrations/post-boot.sql:/sql/post-boot.sql:ro - /mnt/user/appdata/mc-cars/supabase/migrations/02-leads.sql:/sql/02-leads.sql:ro - /mnt/user/appdata/mc-cars/supabase/migrations/03-booking-flow.sql:/sql/03-booking-flow.sql:ro + - /mnt/user/appdata/mc-cars/supabase/migrations/04-kaution-weekend-km.sql:/sql/04-kaution-weekend-km.sql:ro entrypoint: ["sh","-c"] command: - | @@ -229,6 +231,7 @@ services: -f /sql/post-boot.sql psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/02-leads.sql psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/03-booking-flow.sql + psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/04-kaution-weekend-km.sql echo "post-init done." restart: "no" networks: [mccars] diff --git a/frontend/admin.html b/frontend/admin.html index a240f37..b2624f0 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -139,10 +139,7 @@ Foto hochladen (JPG/PNG/WebP, max 50 MB) - +
@@ -163,6 +160,11 @@
+ + +
+ +
diff --git a/frontend/admin.js b/frontend/admin.js index abf38ec..242eb1d 100644 --- a/frontend/admin.js +++ b/frontend/admin.js @@ -230,6 +230,8 @@ function loadForEdit(id) { vehicleForm.daily_price_eur.value = v.daily_price_eur; vehicleForm.weekend_price_eur.value = v.weekend_price_eur || 0; vehicleForm.max_daily_km.value = v.max_daily_km || 150; + vehicleForm.kaution_eur.value = v.kaution_eur || 5000; + vehicleForm.max_km_weekend.value = v.max_km_weekend || ''; vehicleForm.sort_order.value = v.sort_order; vehicleForm.location.value = v.location; vehicleForm.description_de.value = v.description_de; @@ -250,6 +252,8 @@ resetBtn.addEventListener("click", () => { vehicleForm.seats.value = 2; vehicleForm.max_daily_km.value = 150; vehicleForm.weekend_price_eur.value = 0; + vehicleForm.kaution_eur.value = 5000; + vehicleForm.max_km_weekend.value = ''; state.currentPhotoPath = null; updatePreview(""); formTitle.textContent = "Neues Fahrzeug"; @@ -273,6 +277,8 @@ vehicleForm.addEventListener("submit", async (e) => { daily_price_eur: +fd.get("daily_price_eur") || 0, weekend_price_eur: +fd.get("weekend_price_eur") || 0, max_daily_km: +fd.get("max_daily_km") || 150, + kaution_eur: +fd.get("kaution_eur") || 5000, + max_km_weekend: fd.get("max_km_weekend") ? +fd.get("max_km_weekend") : null, sort_order: +fd.get("sort_order") || 100, location: fd.get("location") || "Steiermark (TBD)", description_de: fd.get("description_de") || "", @@ -316,11 +322,15 @@ photoInput.addEventListener("change", async () => { formFeedback.className = "form-feedback"; formFeedback.textContent = "Uploading photo..."; try { + // Delete old photo if exists + if (state.currentPhotoPath) { + await supabase.storage.from("vehicle-photos").remove([state.currentPhotoPath]); + } const ext = (file.name.split(".").pop() || "jpg").toLowerCase(); const path = `${crypto.randomUUID()}.${ext}`; const { error: upErr } = await supabase.storage .from("vehicle-photos") - .upload(path, file, { contentType: file.type, upsert: false }); + .upload(path, file, { contentType: file.type, upsert: true }); if (upErr) throw upErr; const { data: pub } = supabase.storage.from("vehicle-photos").getPublicUrl(path); state.currentPhotoPath = path; diff --git a/frontend/app.js b/frontend/app.js index 508e2a5..b0b177b 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -196,6 +196,14 @@ function openDetails(id) {
${v.top_speed_kmh}${t("kmh")}
${escapeHtml(v.acceleration)}${t("accel")}
+
+
${v.seats}${t("seats")}
+
€ ${v.weekend_price_eur || v.daily_price_eur}${t("bpfWeekendRate")}
+
${v.max_daily_km || 150}${t("bpfMaxKm")}
+
+
+
€ ${(v.kaution_eur || 5000).toLocaleString("de-DE")}${t("bpfDeposit")}
+
€ ${v.daily_price_eur} / ${t("perDay")}
@@ -364,8 +372,10 @@ function updateSidebar() { const subtotal = weekdayCost + weekendCost; const vat = Math.round(subtotal * 0.20); const total = subtotal + vat; - const deposit = Math.round(v.daily_price_eur * 2.5); - const includedKm = (v.max_daily_km || 150) * totalDays; + const deposit = v.kaution_eur || 5000; + const kmPerWeekendDay = v.max_km_weekend || v.max_daily_km || 150; + const kmPerWeekday = v.max_daily_km || 150; + const includedKm = (weekdays * kmPerWeekday) + (weekendDays * kmPerWeekendDay); bpfSidebarPlaceholder.style.display = "none"; bpfSidebarContent.style.display = "block"; diff --git a/frontend/i18n.js b/frontend/i18n.js index e58c931..661f684 100644 --- a/frontend/i18n.js +++ b/frontend/i18n.js @@ -133,7 +133,6 @@ export const translations = { adminNewVehicle: "Neues Fahrzeug", adminAllVehicles: "Alle Fahrzeuge", adminPhotoUpload: "Foto hochladen (JPG/PNG/WebP, max 50 MB)", - adminPhotoUrl: "Foto-URL (wird automatisch gesetzt nach Upload)", adminBrand: "Marke", adminModel: "Modell", adminPower: "PS", @@ -169,6 +168,8 @@ export const translations = { adminReceived: "Eingang", adminVehicleTab: "Fahrzeug", adminPeriod: "Zeitraum", + adminKaution: "Kaution (€)", + adminMaxKmWeekend: "Max. km/Wochenendtag", }, en: { navCars: "Fleet", @@ -303,7 +304,6 @@ export const translations = { adminNewVehicle: "New vehicle", adminAllVehicles: "All vehicles", adminPhotoUpload: "Upload photo (JPG/PNG/WebP, max 50 MB)", - adminPhotoUrl: "Photo URL (auto-set after upload)", adminBrand: "Brand", adminModel: "Model", adminPower: "HP", @@ -339,6 +339,8 @@ export const translations = { adminReceived: "Received", adminVehicleTab: "Vehicle", adminPeriod: "Period", + adminKaution: "Deposit (€)", + adminMaxKmWeekend: "Max. km/weekend day", }, }; diff --git a/supabase/migrations/01-init.sql b/supabase/migrations/01-init.sql index 7378b4e..c90dac4 100644 --- a/supabase/migrations/01-init.sql +++ b/supabase/migrations/01-init.sql @@ -65,6 +65,9 @@ begin -- Let authenticator impersonate app roles. execute 'grant anon, authenticated, service_role to authenticator'; + + -- Let storage_admin impersonate app roles (needed for RLS on storage.objects). + execute 'grant anon, authenticated, service_role to supabase_storage_admin'; end $roles$; @@ -165,34 +168,9 @@ insert into public.vehicles (brand, model, power_hp, top_speed_kmh, acceleration, seats, daily_price_eur, location, description_de, description_en, photo_url, sort_order) values - ('Porsche','911 GT3', 510, 318, '3.4s', 2, 890, 'Steiermark (TBD)', - 'Puristischer Hochdrehzahl-Saugmotor und kompromissloser Motorsport-Charakter.', - 'Pure high-revving naturally aspirated engine with uncompromising motorsport character.', - 'https://images.unsplash.com/photo-1611821064430-0d40291d0f0b?auto=format&fit=crop&w=1400&q=80', - 10), - ('Lamborghini','Huracan EVO', 640, 325, '2.9s', 2, 990, 'Steiermark (TBD)', - 'V10 mit 640 PS, scharfes Design und kompromisslose Performance auf Strasse und Rennstrecke.', - 'V10 with 640 hp, sharp design and uncompromising performance on road and track.', - 'https://images.unsplash.com/photo-1544636331-e26879cd4d9b?auto=format&fit=crop&w=1400&q=80', - 20), - ('Audi','RS6 Performance', 630, 305, '3.4s', 5, 540, 'Steiermark (TBD)', - 'Alltagstauglicher Kombi mit brutaler V8-Biturbo-Power und Allradantrieb.', - 'Everyday-ready estate with brutal twin-turbo V8 power and quattro AWD.', - 'https://images.unsplash.com/photo-1606664515524-ed2f786a0bd6?auto=format&fit=crop&w=1400&q=80', - 30), - ('BMW','M4 Competition', 530, 290, '3.5s', 4, 430, 'Steiermark (TBD)', - 'Reihensechszylinder-Biturbo mit praeziser Lenkung und sportlichem Fahrwerk.', - 'Twin-turbo inline-six with precise steering and sporty chassis.', - 'https://images.unsplash.com/photo-1555215695-3004980ad54e?auto=format&fit=crop&w=1400&q=80', - 40), - ('Nissan','GT-R R35', 570, 315, '2.8s', 4, 510, 'Steiermark (TBD)', - 'Ikonischer Allrad-Supersportler mit Twin-Turbo V6 und brutalem Antritt.', - 'Iconic AWD supercar with twin-turbo V6 and ferocious launch.', - 'https://images.unsplash.com/photo-1626668893632-6f3a4466d22f?auto=format&fit=crop&w=1400&q=80', - 50), - ('Mercedes-AMG','G63', 585, 220, '4.5s', 5, 620, 'Steiermark (TBD)', - 'Legendaere G-Klasse mit V8-Biturbo-Performance und unverkennbarem Design.', - 'Legendary G-Class with V8 biturbo performance and unmistakable design.', - 'https://images.unsplash.com/photo-1606611013016-969c19ba27bb?auto=format&fit=crop&w=1400&q=80', - 60) + ('Ferrari','296 GTB', 830, 330, '2.9s', 2, 850, 'Steiermark (TBD)', + 'V6-Hybrid mit 830 PS, atemberaubendes Design und kompromisslose Performance auf Strasse und Rennstrecke.', + 'V6 hybrid with 830 hp, breathtaking design and uncompromising performance on road and track.', + '/images/ferrari-main-car.png', + 10) on conflict do nothing; diff --git a/supabase/migrations/03-booking-flow.sql b/supabase/migrations/03-booking-flow.sql index bdac3a5..89da155 100644 --- a/supabase/migrations/03-booking-flow.sql +++ b/supabase/migrations/03-booking-flow.sql @@ -16,6 +16,12 @@ update public.vehicles max_daily_km = 150 where weekend_price_eur = 0; +-- Ferrari gets specific pricing +update public.vehicles + set weekend_price_eur = 1100, + max_daily_km = 200 + where brand = 'Ferrari' and model = '296 GTB'; + -- ----------------------------------------------------------------------------- -- 2. Lead attachments: documents uploaded during booking flow -- ----------------------------------------------------------------------------- diff --git a/supabase/migrations/04-kaution-weekend-km.sql b/supabase/migrations/04-kaution-weekend-km.sql new file mode 100644 index 0000000..54c82ce --- /dev/null +++ b/supabase/migrations/04-kaution-weekend-km.sql @@ -0,0 +1,27 @@ +-- ============================================================================= +-- MC Cars - Add per-vehicle kaution (deposit) and max_km_weekend columns. +-- Idempotent. +-- ============================================================================= + +-- ----------------------------------------------------------------------------- +-- 1. kaution_eur: deposit per vehicle, NOT NULL, default 5000, must be > 0 +-- ----------------------------------------------------------------------------- +alter table public.vehicles add column if not exists kaution_eur integer not null default 5000; + +do $$ +begin + if not exists ( + select 1 from information_schema.check_constraints + where constraint_name = 'vehicles_kaution_positive' + ) then + alter table public.vehicles add constraint vehicles_kaution_positive check (kaution_eur > 0); + end if; +end $$; + +-- ----------------------------------------------------------------------------- +-- 2. max_km_weekend: optional per-weekend-day km allowance (NULL = use max_daily_km) +-- ----------------------------------------------------------------------------- +alter table public.vehicles add column if not exists max_km_weekend integer default null; + +-- Signal PostgREST to reload its schema cache +notify pgrst, 'reload schema'; diff --git a/supabase/migrations/post-boot.sql b/supabase/migrations/post-boot.sql index 27b1cb5..917d2b6 100644 --- a/supabase/migrations/post-boot.sql +++ b/supabase/migrations/post-boot.sql @@ -60,9 +60,14 @@ on conflict (id) do update file_size_limit = excluded.file_size_limit, allowed_mime_types = excluded.allowed_mime_types; +-- Let storage_admin assume app roles for RLS evaluation (idempotent). +grant anon, authenticated, service_role to supabase_storage_admin; + grant select on storage.buckets to anon, authenticated; +grant all on storage.buckets to service_role; grant select on storage.objects to anon; grant select, insert, update, delete on storage.objects to authenticated; +grant all on storage.objects to service_role; drop policy if exists "vehicle_photos_public_read" on storage.objects; drop policy if exists "vehicle_photos_admin_insert" on storage.objects;