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.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;