-- ===================================================================== -- Migration 0038 — Shop-scoped service titles and icons. -- -- Lets owners/managers customize how transaction services appear in the -- UI without changing canonical service codes used by accounting logic. -- ===================================================================== set search_path = app, public; create table if not exists app.service_ui_settings ( shop_id uuid not null references app.shops(id) on delete cascade, service_code text not null references app.services(code) on delete cascade, display_name text, icon text, updated_at timestamptz not null default now(), updated_by uuid references auth.users(id), primary key (shop_id, service_code), constraint service_ui_display_name_len check (display_name is null or length(display_name) between 1 and 80), constraint service_ui_icon_len check (icon is null or length(icon) between 1 and 16) ); alter table app.service_ui_settings enable row level security; alter table app.service_ui_settings force row level security; revoke insert, update, delete on app.service_ui_settings from authenticated; grant select on app.service_ui_settings to authenticated; drop policy if exists service_ui_select on app.service_ui_settings; create policy service_ui_select on app.service_ui_settings for select using ( app.has_any_role_in_shop(shop_id, array['owner','manager','cashier','auditor']::app.business_role[]) ); create or replace view app.v_service_ui_settings as select s.id as shop_id, svc.code as service_code, svc.name as default_name, svc.category, svc.is_active, ui.display_name, ui.icon, ui.updated_at, ui.updated_by from app.shops s join app.services svc on svc.is_active = true left join app.service_ui_settings ui on ui.shop_id = s.id and ui.service_code = svc.code where app.has_any_role_in_shop(s.id, array['owner','manager','cashier','auditor']::app.business_role[]); grant select on app.v_service_ui_settings to authenticated; create or replace function app.set_service_ui_setting( p_shop uuid, p_service_code text, p_display_name text, p_icon text ) returns void language plpgsql security definer set search_path = app, public as $$ declare v_display text := nullif(btrim(coalesce(p_display_name, '')), ''); v_icon text := nullif(btrim(coalesce(p_icon, '')), ''); begin if not app.has_any_role_in_shop(p_shop, array['owner','manager']::app.business_role[]) then raise exception 'manager or owner role required'; end if; if not exists (select 1 from app.services where code = p_service_code and is_active) then raise exception 'unknown or inactive service %', p_service_code; end if; if v_display is null and v_icon is null then delete from app.service_ui_settings where shop_id = p_shop and service_code = p_service_code; return; end if; insert into app.service_ui_settings(shop_id, service_code, display_name, icon, updated_by) values (p_shop, p_service_code, v_display, v_icon, auth.uid()) on conflict (shop_id, service_code) do update set display_name = excluded.display_name, icon = excluded.icon, updated_at = now(), updated_by = auth.uid(); end; $$; revoke all on function app.set_service_ui_setting(uuid, text, text, text) from public; grant execute on function app.set_service_ui_setting(uuid, text, text, text) to authenticated; -- Recent transaction rows should use the shop's display title when set. drop view if exists app.v_my_recent_transactions; create view app.v_my_recent_transactions as select t.id, t.reference_no, t.shop_id, t.till_id, t.shift_id, t.service_code, coalesce(ui.display_name, s.name) as service_name, s.category, t.payment_method, t.gross_usd, t.gross_lbp, t.fee_usd + t.commission_usd as revenue_usd, t.fee_lbp + t.commission_lbp as revenue_lbp, t.external_ref, t.external_ref_provider, t.beneficiary_name, t.beneficiary_phone, t.status, t.occurred_at, t.user_id from app.transactions t join app.services s on s.code = t.service_code left join app.service_ui_settings ui on ui.shop_id = t.shop_id and ui.service_code = t.service_code where t.user_id = auth.uid() or app.has_any_role_in_shop(t.shop_id, array['owner','manager','auditor']::app.business_role[]); grant select on app.v_my_recent_transactions to authenticated;