feat: add deposit and weekend km allowance to vehicles, update migrations and booking flow

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
LagoESP
2026-04-29 16:00:06 +02:00
parent d960e37aa8
commit e85b319c93
10 changed files with 83 additions and 39 deletions
+1
View File
@@ -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:
+3
View File
@@ -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]
+6 -4
View File
@@ -139,10 +139,7 @@
<span data-i18n="adminPhotoUpload">Foto hochladen (JPG/PNG/WebP, max 50 MB)</span>
<input type="file" id="photoInput" accept="image/*" />
</label>
<label>
<span data-i18n="adminPhotoUrl">Foto-URL (wird automatisch gesetzt nach Upload)</span>
<input type="url" name="photo_url" placeholder="https://..." />
</label>
<input type="hidden" name="photo_url" />
<div class="row2">
<label><span data-i18n="adminBrand">Marke</span><input name="brand" required /></label>
@@ -163,6 +160,11 @@
<div class="row3">
<label><span>Max. km/Tag</span><input type="number" name="max_daily_km" min="0" value="150" /></label>
<label><span data-i18n="adminKaution">Kaution (€)</span><input type="number" name="kaution_eur" min="1" value="5000" required /></label>
<label><span data-i18n="adminMaxKmWeekend">Max. km/Wochenendtag</span><input type="number" name="max_km_weekend" min="0" placeholder="wie km/Tag" /></label>
</div>
<div class="row2">
<label><span data-i18n="adminSort">Reihenfolge</span><input type="number" name="sort_order" value="100" /></label>
<label><span data-i18n="adminLocation">Standort</span><input name="location" value="Steiermark (TBD)" /></label>
</div>
+11 -1
View File
@@ -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;
+12 -2
View File
@@ -196,6 +196,14 @@ function openDetails(id) {
<div><strong>${v.top_speed_kmh}</strong><span>${t("kmh")}</span></div>
<div><strong>${escapeHtml(v.acceleration)}</strong><span>${t("accel")}</span></div>
</div>
<div class="spec-row" style="margin:1rem 0;">
<div><strong>${v.seats}</strong><span>${t("seats")}</span></div>
<div><strong>€ ${v.weekend_price_eur || v.daily_price_eur}</strong><span>${t("bpfWeekendRate")}</span></div>
<div><strong>${v.max_daily_km || 150}</strong><span>${t("bpfMaxKm")}</span></div>
</div>
<div class="spec-row" style="margin:1rem 0;grid-template-columns:1fr;">
<div><strong>€ ${(v.kaution_eur || 5000).toLocaleString("de-DE")}</strong><span>${t("bpfDeposit")}</span></div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:1rem;">
<div class="vehicle-price">€ ${v.daily_price_eur}<span> / ${t("perDay")}</span></div>
<button class="btn" id="dialogBook">${t("bookNow")}</button>
@@ -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";
+4 -2
View File
@@ -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",
},
};
+8 -30
View File
@@ -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;
+6
View File
@@ -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
-- -----------------------------------------------------------------------------
@@ -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';
+5
View File
@@ -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;