feat: enhance booking flow with BPF wizard, weekend pricing, and document uploads
- Updated translations for German and English to reflect changes in deposit terminology. - Modified index.html to implement a new booking flow with a step-by-step wizard for vehicle selection, contact details, and ID verification. - Added CSS styles for the new booking flow interface, including responsive design and improved input styles. - Created new database policies and tables for handling customer and lead attachments during the booking process. - Introduced weekend pricing and daily KM limits for vehicles in the database schema. - Implemented email-based customer upsert functionality to streamline lead qualification and attachment transfer. Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
-- =============================================================================
|
||||
-- MC Cars - Booking flow enhancements: weekend pricing, daily KM limits,
|
||||
-- lead attachments, customer attachments, email-based customer upsert.
|
||||
-- Idempotent.
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. Add weekend_price_eur and max_daily_km to vehicles
|
||||
-- -----------------------------------------------------------------------------
|
||||
alter table public.vehicles add column if not exists weekend_price_eur integer not null default 0;
|
||||
alter table public.vehicles add column if not exists max_daily_km integer not null default 0;
|
||||
|
||||
-- Backfill existing vehicles with sensible defaults (weekend = daily * 1.2)
|
||||
update public.vehicles
|
||||
set weekend_price_eur = ceil(daily_price_eur * 1.2),
|
||||
max_daily_km = 150
|
||||
where weekend_price_eur = 0;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. Lead attachments: documents uploaded during booking flow
|
||||
-- -----------------------------------------------------------------------------
|
||||
create table if not exists public.lead_attachments (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
lead_id uuid not null references public.leads(id) on delete cascade,
|
||||
bucket text not null default 'customer-documents',
|
||||
file_path text not null,
|
||||
file_name text not null default '',
|
||||
mime_type text not null default 'application/octet-stream',
|
||||
kind text not null default 'other'
|
||||
check (kind in ('id_document', 'income_proof', 'other')),
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists lead_attachments_lead_idx on public.lead_attachments (lead_id);
|
||||
|
||||
alter table public.lead_attachments enable row level security;
|
||||
|
||||
drop policy if exists "lead_attach_anon_insert" on public.lead_attachments;
|
||||
drop policy if exists "lead_attach_admin_all" on public.lead_attachments;
|
||||
|
||||
-- Anon can insert (they upload during booking)
|
||||
create policy "lead_attach_anon_insert"
|
||||
on public.lead_attachments for insert to anon
|
||||
with check (true);
|
||||
|
||||
-- Authenticated admins can do everything
|
||||
create policy "lead_attach_admin_all"
|
||||
on public.lead_attachments for all to authenticated
|
||||
using (true) with check (true);
|
||||
|
||||
grant insert on public.lead_attachments to anon;
|
||||
grant all on public.lead_attachments to authenticated;
|
||||
grant all on public.lead_attachments to service_role;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3. Customer attachments: transferred from leads on qualification
|
||||
-- -----------------------------------------------------------------------------
|
||||
create table if not exists public.customer_attachments (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
customer_id uuid not null references public.customers(id) on delete cascade,
|
||||
lead_id uuid references public.leads(id) on delete set null,
|
||||
bucket text not null default 'customer-documents',
|
||||
file_path text not null,
|
||||
file_name text not null default '',
|
||||
mime_type text not null default 'application/octet-stream',
|
||||
kind text not null default 'other'
|
||||
check (kind in ('id_document', 'income_proof', 'other')),
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists customer_attachments_cust_idx on public.customer_attachments (customer_id);
|
||||
|
||||
alter table public.customer_attachments enable row level security;
|
||||
|
||||
drop policy if exists "cust_attach_admin_all" on public.customer_attachments;
|
||||
|
||||
create policy "cust_attach_admin_all"
|
||||
on public.customer_attachments for all to authenticated
|
||||
using (true) with check (true);
|
||||
|
||||
grant all on public.customer_attachments to authenticated;
|
||||
grant all on public.customer_attachments to service_role;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4. Allow multiple leads per customer: drop UNIQUE on lead_id, add email index
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Drop the unique index on customers.lead_id so multiple leads can map to one customer
|
||||
drop index if exists customers_lead_unique;
|
||||
|
||||
-- Create unique index on email for upsert
|
||||
create unique index if not exists customers_email_unique on public.customers (lower(email));
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 5. Replace qualify_lead() with email-based upsert + attachment transfer
|
||||
-- -----------------------------------------------------------------------------
|
||||
create or replace function public.qualify_lead(p_lead_id uuid, p_notes text default '')
|
||||
returns public.customers
|
||||
language plpgsql
|
||||
security invoker
|
||||
as $$
|
||||
declare
|
||||
v_lead public.leads;
|
||||
v_customer public.customers;
|
||||
v_user uuid := auth.uid();
|
||||
begin
|
||||
-- Lock the lead row
|
||||
select * into v_lead from public.leads where id = p_lead_id for update;
|
||||
if not found then
|
||||
raise exception 'lead % not found', p_lead_id;
|
||||
end if;
|
||||
|
||||
-- If already qualified, just return the associated customer
|
||||
if v_lead.status = 'qualified' then
|
||||
select * into v_customer from public.customers where lead_id = v_lead.id limit 1;
|
||||
if not found then
|
||||
select * into v_customer from public.customers where lower(email) = lower(v_lead.email) limit 1;
|
||||
end if;
|
||||
return v_customer;
|
||||
end if;
|
||||
|
||||
-- Mark lead as qualified
|
||||
update public.leads
|
||||
set status = 'qualified',
|
||||
is_active = false,
|
||||
qualified_at = now(),
|
||||
qualified_by = v_user,
|
||||
admin_notes = coalesce(nullif(p_notes, ''), admin_notes)
|
||||
where id = v_lead.id;
|
||||
|
||||
-- Upsert customer by email
|
||||
insert into public.customers (lead_id, name, email, phone, notes, created_by)
|
||||
values (v_lead.id, v_lead.name, v_lead.email, v_lead.phone, coalesce(p_notes,''), v_user)
|
||||
on conflict ((lower(email))) do update
|
||||
set name = excluded.name,
|
||||
phone = excluded.phone,
|
||||
notes = case when excluded.notes <> '' then excluded.notes else public.customers.notes end,
|
||||
updated_at = now()
|
||||
returning * into v_customer;
|
||||
|
||||
-- Transfer lead attachments to customer
|
||||
insert into public.customer_attachments (customer_id, lead_id, bucket, file_path, file_name, mime_type, kind, created_at)
|
||||
select v_customer.id, la.lead_id, la.bucket, la.file_path, la.file_name, la.mime_type, la.kind, la.created_at
|
||||
from public.lead_attachments la
|
||||
where la.lead_id = v_lead.id
|
||||
and not exists (
|
||||
select 1 from public.customer_attachments ca
|
||||
where ca.customer_id = v_customer.id
|
||||
and ca.file_path = la.file_path
|
||||
);
|
||||
|
||||
return v_customer;
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function public.qualify_lead(uuid, text) from public;
|
||||
grant execute on function public.qualify_lead(uuid, text) to authenticated;
|
||||
@@ -83,3 +83,32 @@ create policy "vehicle_photos_admin_update"
|
||||
create policy "vehicle_photos_admin_delete"
|
||||
on storage.objects for delete to authenticated
|
||||
using (bucket_id = 'vehicle-photos');
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Private bucket for booking documents (ID, payslip)
|
||||
-- -----------------------------------------------------------------------------
|
||||
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
values ('customer-documents','customer-documents', false, 10485760,
|
||||
array['image/jpeg','image/png','application/pdf'])
|
||||
on conflict (id) do update
|
||||
set public = excluded.public,
|
||||
file_size_limit = excluded.file_size_limit,
|
||||
allowed_mime_types = excluded.allowed_mime_types;
|
||||
|
||||
drop policy if exists "custdocs_anon_upload" on storage.objects;
|
||||
drop policy if exists "custdocs_admin_read" on storage.objects;
|
||||
drop policy if exists "custdocs_admin_delete" on storage.objects;
|
||||
|
||||
-- Anon can upload during booking flow
|
||||
create policy "custdocs_anon_upload"
|
||||
on storage.objects for insert to anon
|
||||
with check (bucket_id = 'customer-documents');
|
||||
|
||||
-- Only authenticated admins can read/delete
|
||||
create policy "custdocs_admin_read"
|
||||
on storage.objects for select to authenticated
|
||||
using (bucket_id = 'customer-documents');
|
||||
|
||||
create policy "custdocs_admin_delete"
|
||||
on storage.objects for delete to authenticated
|
||||
using (bucket_id = 'customer-documents');
|
||||
|
||||
Reference in New Issue
Block a user