feat: update upload functionality and permissions for document handling
- Removed the `upsert` option from the file upload in `uploadDoc` function to prevent unintended overwrites. - Enhanced German translations in `i18n.js` for better clarity and consistency in the admin interface. - Added new CSS styles for link interactions to improve user experience in `styles.css`. - Updated Supabase SQL migration to grant additional permissions for anonymous users to insert and update storage objects, ensuring proper functionality during the booking flow. Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
-- 07-sales-orders.sql
|
||||
-- Sales Orders, customer private notes, lead IP tracking, attachment relations
|
||||
|
||||
alter table public.leads add column if not exists ip_address text default '';
|
||||
alter table public.leads add column if not exists ip_country text default '';
|
||||
|
||||
create table if not exists public.sales_orders (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
customer_id uuid not null references public.customers(id) on delete cascade,
|
||||
lead_id uuid not null references public.leads(id) on delete restrict,
|
||||
order_number text not null default '',
|
||||
private_notes text not null default '',
|
||||
kaution_paid boolean not null default false,
|
||||
rental_paid boolean not null default false,
|
||||
rental_complete boolean not null default false,
|
||||
kaution_paid_at timestamptz,
|
||||
rental_paid_at timestamptz,
|
||||
rental_complete_at timestamptz,
|
||||
daily_subtotal integer not null default 0,
|
||||
weekend_subtotal integer not null default 0,
|
||||
subtotal_eur integer not null default 0,
|
||||
vat_eur integer not null default 0,
|
||||
total_eur integer not null default 0,
|
||||
deposit_eur integer not null default 0,
|
||||
total_days integer not null default 0,
|
||||
weekday_count integer not null default 0,
|
||||
weekend_day_count integer not null default 0,
|
||||
date_from date,
|
||||
date_to date,
|
||||
vehicle_label text not null default '',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists sales_orders_customer_idx on public.sales_orders (customer_id);
|
||||
create index if not exists sales_orders_lead_idx on public.sales_orders (lead_id);
|
||||
create index if not exists sales_orders_order_number on public.sales_orders (order_number);
|
||||
|
||||
drop trigger if exists sales_orders_touch on public.sales_orders;
|
||||
create trigger sales_orders_touch
|
||||
before update on public.sales_orders
|
||||
for each row execute function public.tg_touch_updated_at();
|
||||
|
||||
alter table public.sales_orders enable row level security;
|
||||
|
||||
drop policy if exists "sales_orders_admin_select" on public.sales_orders;
|
||||
drop policy if exists "sales_orders_admin_insert" on public.sales_orders;
|
||||
drop policy if exists "sales_orders_admin_update" on public.sales_orders;
|
||||
|
||||
create policy "sales_orders_admin_select"
|
||||
on public.sales_orders for select to authenticated using (true);
|
||||
create policy "sales_orders_admin_insert"
|
||||
on public.sales_orders for insert to authenticated with check (true);
|
||||
create policy "sales_orders_admin_update"
|
||||
on public.sales_orders for update to authenticated using (true) with check (true);
|
||||
|
||||
grant select, insert, update on public.sales_orders to authenticated;
|
||||
grant all on public.sales_orders to service_role;
|
||||
|
||||
create table if not exists public.sales_order_attachments (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
sales_order_id uuid not null references public.sales_orders(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 sales_order_attachments_so_idx on public.sales_order_attachments (sales_order_id);
|
||||
|
||||
alter table public.sales_order_attachments enable row level security;
|
||||
|
||||
drop policy if exists "so_attach_admin_all" on public.sales_order_attachments;
|
||||
create policy "so_attach_admin_all"
|
||||
on public.sales_order_attachments for all to authenticated
|
||||
using (true) with check (true);
|
||||
|
||||
grant all on public.sales_order_attachments to authenticated;
|
||||
grant all on public.sales_order_attachments to service_role;
|
||||
|
||||
alter table public.customers add column if not exists private_notes text not null default '';
|
||||
create unique index if not exists customers_email_lower_unique on public.customers ((lower(email)));
|
||||
|
||||
alter table public.customer_attachments add column if not exists sales_order_id uuid references public.sales_orders(id) on delete set null;
|
||||
create index if not exists customer_attachments_so_idx on public.customer_attachments (sales_order_id);
|
||||
|
||||
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_sales_order public.sales_orders;
|
||||
v_user uuid := auth.uid();
|
||||
v_order_num text;
|
||||
v_year integer;
|
||||
v_count integer;
|
||||
begin
|
||||
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 v_lead.status = 'qualified' then
|
||||
select * into v_customer from public.customers where lower(email) = lower(v_lead.email) limit 1;
|
||||
return v_customer;
|
||||
end if;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
v_year := extract(year from now())::integer;
|
||||
select coalesce(count(*), 0) + 1 into v_count
|
||||
from public.sales_orders
|
||||
where extract(year from created_at)::integer = v_year;
|
||||
v_order_num := 'SO-' || v_year || '-' || lpad(v_count::text, 4, '0');
|
||||
|
||||
insert into public.sales_orders (
|
||||
customer_id, lead_id, order_number, private_notes,
|
||||
daily_subtotal, weekend_subtotal, subtotal_eur, vat_eur, total_eur, deposit_eur,
|
||||
total_days, weekday_count, weekend_day_count,
|
||||
date_from, date_to, vehicle_label
|
||||
) values (
|
||||
v_customer.id, v_lead.id, v_order_num, coalesce(v_lead.admin_notes, ''),
|
||||
coalesce(v_lead.daily_subtotal, 0), coalesce(v_lead.weekend_subtotal, 0),
|
||||
coalesce(v_lead.subtotal_eur, 0), coalesce(v_lead.vat_eur, 0),
|
||||
coalesce(v_lead.total_eur, 0), coalesce(v_lead.deposit_eur, 0),
|
||||
coalesce(v_lead.total_days, 0), coalesce(v_lead.weekday_count, 0),
|
||||
coalesce(v_lead.weekend_day_count, 0),
|
||||
v_lead.date_from, v_lead.date_to, v_lead.vehicle_label
|
||||
) returning * into v_sales_order;
|
||||
|
||||
insert into public.customer_attachments (customer_id, lead_id, sales_order_id, bucket, file_path, file_name, mime_type, kind, created_at)
|
||||
select v_customer.id, la.lead_id, v_sales_order.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
|
||||
);
|
||||
|
||||
insert into public.sales_order_attachments (sales_order_id, bucket, file_path, file_name, mime_type, kind, created_at)
|
||||
select v_sales_order.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;
|
||||
|
||||
return v_customer;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sales_order_toggle_kaution(p_so_id uuid)
|
||||
returns public.sales_orders
|
||||
language plpgsql
|
||||
security invoker
|
||||
as $$
|
||||
declare
|
||||
v_so public.sales_orders;
|
||||
begin
|
||||
select * into v_so from public.sales_orders where id = p_so_id for update;
|
||||
if not found then
|
||||
raise exception 'sales order % not found', p_so_id;
|
||||
end if;
|
||||
|
||||
if v_so.kaution_paid then
|
||||
update public.sales_orders
|
||||
set kaution_paid = false, kaution_paid_at = null, updated_at = now()
|
||||
where id = p_so_id
|
||||
returning * into v_so;
|
||||
else
|
||||
if v_so.rental_complete then
|
||||
raise exception 'cannot set kaution paid after rental is complete';
|
||||
end if;
|
||||
update public.sales_orders
|
||||
set kaution_paid = true, kaution_paid_at = now(), updated_at = now()
|
||||
where id = p_so_id
|
||||
returning * into v_so;
|
||||
end if;
|
||||
return v_so;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sales_order_toggle_rental(p_so_id uuid)
|
||||
returns public.sales_orders
|
||||
language plpgsql
|
||||
security invoker
|
||||
as $$
|
||||
declare
|
||||
v_so public.sales_orders;
|
||||
begin
|
||||
select * into v_so from public.sales_orders where id = p_so_id for update;
|
||||
if not found then
|
||||
raise exception 'sales order % not found', p_so_id;
|
||||
end if;
|
||||
|
||||
if v_so.rental_paid then
|
||||
update public.sales_orders
|
||||
set rental_paid = false, rental_paid_at = null, updated_at = now()
|
||||
where id = p_so_id
|
||||
returning * into v_so;
|
||||
else
|
||||
if v_so.kaution_paid = false or v_so.rental_complete then
|
||||
raise exception 'can only set rental paid after kaution is paid and before rental is complete';
|
||||
end if;
|
||||
update public.sales_orders
|
||||
set rental_paid = true, rental_paid_at = now(), updated_at = now()
|
||||
where id = p_so_id
|
||||
returning * into v_so;
|
||||
end if;
|
||||
return v_so;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sales_order_toggle_complete(p_so_id uuid)
|
||||
returns public.sales_orders
|
||||
language plpgsql
|
||||
security invoker
|
||||
as $$
|
||||
declare
|
||||
v_so public.sales_orders;
|
||||
begin
|
||||
select * into v_so from public.sales_orders where id = p_so_id for update;
|
||||
if not found then
|
||||
raise exception 'sales order % not found', p_so_id;
|
||||
end if;
|
||||
|
||||
if v_so.rental_complete then
|
||||
update public.sales_orders
|
||||
set rental_complete = false, rental_complete_at = null, updated_at = now()
|
||||
where id = p_so_id
|
||||
returning * into v_so;
|
||||
else
|
||||
if v_so.kaution_paid = false or v_so.rental_paid = false then
|
||||
raise exception 'can only set rental complete after kaution and rental are paid';
|
||||
end if;
|
||||
update public.sales_orders
|
||||
set rental_complete = true, rental_complete_at = now(), updated_at = now()
|
||||
where id = p_so_id
|
||||
returning * into v_so;
|
||||
end if;
|
||||
return v_so;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.customer_update_private_notes(p_customer_id uuid, p_notes text)
|
||||
returns void
|
||||
language plpgsql
|
||||
security invoker
|
||||
as $$
|
||||
begin
|
||||
update public.customers
|
||||
set private_notes = coalesce(p_notes, ''), updated_at = now()
|
||||
where id = p_customer_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sales_order_update_private_notes(p_so_id uuid, p_notes text)
|
||||
returns void
|
||||
language plpgsql
|
||||
security invoker
|
||||
as $$
|
||||
begin
|
||||
update public.sales_orders
|
||||
set private_notes = coalesce(p_notes, ''), updated_at = now()
|
||||
where id = p_so_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.sales_order_upload_attachment(
|
||||
p_sales_order_id uuid,
|
||||
p_file_path text,
|
||||
p_file_name text,
|
||||
p_mime_type text,
|
||||
p_kind text default 'other'
|
||||
)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
v_id uuid;
|
||||
begin
|
||||
insert into public.sales_order_attachments (sales_order_id, bucket, file_path, file_name, mime_type, kind)
|
||||
values (p_sales_order_id, 'customer-documents', p_file_path, p_file_name, p_mime_type, p_kind)
|
||||
returning id into v_id;
|
||||
return v_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.create_lead(
|
||||
p_name text,
|
||||
p_email text,
|
||||
p_phone text default '',
|
||||
p_vehicle_id uuid default null,
|
||||
p_vehicle_label text default '',
|
||||
p_date_from date default null,
|
||||
p_date_to date default null,
|
||||
p_message text default '',
|
||||
p_source text default 'website',
|
||||
p_daily_subtotal integer default 0,
|
||||
p_weekend_subtotal integer default 0,
|
||||
p_subtotal_eur integer default 0,
|
||||
p_vat_eur integer default 0,
|
||||
p_total_eur integer default 0,
|
||||
p_deposit_eur integer default 0,
|
||||
p_total_days integer default 0,
|
||||
p_weekday_count integer default 0,
|
||||
p_weekend_day_count integer default 0,
|
||||
p_ip_address text default '',
|
||||
p_ip_country text default ''
|
||||
)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
v_lead_id uuid;
|
||||
begin
|
||||
insert into public.leads (
|
||||
name, email, phone, vehicle_id, vehicle_label, date_from, date_to,
|
||||
message, source,
|
||||
daily_subtotal, weekend_subtotal, subtotal_eur, vat_eur, total_eur, deposit_eur,
|
||||
total_days, weekday_count, weekend_day_count, ip_address, ip_country
|
||||
) values (
|
||||
p_name, p_email, p_phone, p_vehicle_id, p_vehicle_label, p_date_from, p_date_to,
|
||||
p_message, p_source,
|
||||
p_daily_subtotal, p_weekend_subtotal, p_subtotal_eur, p_vat_eur, p_total_eur, p_deposit_eur,
|
||||
p_total_days, p_weekday_count, p_weekend_day_count, p_ip_address, p_ip_country
|
||||
)
|
||||
returning id into v_lead_id;
|
||||
return v_lead_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop function if exists public.create_lead(
|
||||
text, text, text, uuid, text, date, date, text, text
|
||||
);
|
||||
|
||||
drop function if exists public.create_lead(
|
||||
text, text, text, uuid, text, date, date, text, text,
|
||||
integer, integer, integer, integer, integer, integer, integer, integer, integer
|
||||
);
|
||||
|
||||
create or replace function public.create_lead(
|
||||
p_name text,
|
||||
p_email text,
|
||||
p_phone text default '',
|
||||
p_vehicle_id uuid default null,
|
||||
p_vehicle_label text default '',
|
||||
p_date_from date default null,
|
||||
p_date_to date default null,
|
||||
p_message text default '',
|
||||
p_source text default 'website',
|
||||
p_daily_subtotal integer default 0,
|
||||
p_weekend_subtotal integer default 0,
|
||||
p_subtotal_eur integer default 0,
|
||||
p_vat_eur integer default 0,
|
||||
p_total_eur integer default 0,
|
||||
p_deposit_eur integer default 0,
|
||||
p_total_days integer default 0,
|
||||
p_weekday_count integer default 0,
|
||||
p_weekend_day_count integer default 0,
|
||||
p_ip_address text default '',
|
||||
p_ip_country text default ''
|
||||
)
|
||||
returns uuid
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
v_lead_id uuid;
|
||||
begin
|
||||
insert into public.leads (
|
||||
name, email, phone, vehicle_id, vehicle_label, date_from, date_to,
|
||||
message, source,
|
||||
daily_subtotal, weekend_subtotal, subtotal_eur, vat_eur, total_eur, deposit_eur,
|
||||
total_days, weekday_count, weekend_day_count, ip_address, ip_country
|
||||
) values (
|
||||
p_name, p_email, p_phone, p_vehicle_id, p_vehicle_label, p_date_from, p_date_to,
|
||||
p_message, p_source,
|
||||
p_daily_subtotal, p_weekend_subtotal, p_subtotal_eur, p_vat_eur, p_total_eur, p_deposit_eur,
|
||||
p_total_days, p_weekday_count, p_weekend_day_count, p_ip_address, p_ip_country
|
||||
)
|
||||
returning id into v_lead_id;
|
||||
return v_lead_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.sales_order_toggle_kaution(uuid) to authenticated;
|
||||
grant execute on function public.sales_order_toggle_rental(uuid) to authenticated;
|
||||
grant execute on function public.sales_order_toggle_complete(uuid) to authenticated;
|
||||
grant execute on function public.customer_update_private_notes(uuid, text) to authenticated;
|
||||
grant execute on function public.sales_order_update_private_notes(uuid, text) to authenticated;
|
||||
grant execute on function public.sales_order_upload_attachment(uuid, text, text, text, text) to authenticated;
|
||||
grant execute on function public.create_lead(
|
||||
text, text, text, uuid, text, date, date, text, text,
|
||||
integer, integer, integer, integer, integer, integer, integer, integer, integer,
|
||||
text, text
|
||||
) to anon, authenticated, service_role;
|
||||
|
||||
notify pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user