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:
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user