-- MDB-Turbo : cœur métier dossiers + checklist visite + base investisseurs -- Exécuter après création du projet Supabase (auth.users existe déjà). -- --------------------------------------------------------------------------- -- Profil utilisateur (lien auth) -- --------------------------------------------------------------------------- create table if not exists public.profiles ( id uuid primary key references auth.users (id) on delete cascade, full_name text, company_name text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); -- --------------------------------------------------------------------------- -- Dossier = opportunité / deal en cours -- --------------------------------------------------------------------------- create type public.dossier_status as enum ( 'draft', 'sourcing', 'analysis', 'visit', 'offer', 'under_promise', 'resale', 'closed_won', 'closed_lost' ); create table if not exists public.dossiers ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users (id) on delete cascade, title text not null default 'Nouveau dossier', status public.dossier_status not null default 'draft', -- Localisation address_line text, city text, postal_code text, insee_code text, latitude double precision, longitude double precision, -- Physique surface_m2 numeric(12, 2), land_surface_m2 numeric(12, 2), rooms_count smallint, dpe_class text check (dpe_class is null or dpe_class ~ '^[ABCDEFG]$'), -- Financier (saisie terrain / desk) purchase_price_target numeric(14, 2), resale_price_estimate numeric(14, 2), dvf_reference_price_m2 numeric(14, 2), works_estimate_total numeric(14, 2) not null default 0, works_visit_adjustment numeric(14, 2) not null default 0, notary_fee_rate numeric(6, 5) not null default 0.077, sale_agency_fee_rate numeric(6, 5) not null default 0.05, misc_acquisition_cost numeric(14, 2) not null default 0, misc_sale_cost numeric(14, 2) not null default 0, carrying_months smallint not null default 6, carrying_annual_rate numeric(6, 5) not null default 0.05, carrying_principal numeric(14, 2), -- Urbanisme & stratégies "quick win" plu_zone_code text, plu_notes text, parcel_subdivision_candidate boolean not null default false, deficit_foncier_candidate boolean not null default false, -- IA (agents) — JSON pour itérer vite sans migrations à chaque prompt sourcing_agent_output jsonb, tech_estimator_output jsonb, financier_agent_output jsonb, deal_maker_output jsonb, under_promise_at timestamptz, teaser_pdf_url text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists dossiers_user_id_idx on public.dossiers (user_id); create index if not exists dossiers_status_idx on public.dossiers (status); -- --------------------------------------------------------------------------- -- Catalogue "points noirs" visite (anti-erreur) -- --------------------------------------------------------------------------- create table if not exists public.visit_finding_definitions ( code text primary key, label text not null, default_works_delta_eur numeric(14, 2) not null default 0, severity smallint not null default 1, sort_order int not null default 0 ); insert into public.visit_finding_definitions (code, label, default_works_delta_eur, severity, sort_order) values ('structural_crack', 'Fissure structurelle / désordre porteur', 15000, 5, 10), ('roof_full_replace', 'Toiture à refaire (complète)', 35000, 5, 20), ('roof_partial', 'Toiture partielle / zinguerie lourde', 8000, 3, 30), ('humidity_basement', 'Infiltrations cave / vide sanitaire', 12000, 3, 40), ('electrical_rewire', 'Rénovation électrique complète', 15000, 4, 50), ('asbestos', 'Présence amiante / désamiantage à prévoir', 10000, 4, 60), ('septic_non_conform', 'Assainissement non conforme', 12000, 3, 70), ('facade_insulation', 'ITE / ravalement lourd', 25000, 3, 80), ('heat_pump_full', 'Chauffage à refaire (pompe à chaleur + réseau)', 18000, 2, 90) on conflict (code) do nothing; create table if not exists public.dossier_visit_findings ( id uuid primary key default gen_random_uuid(), dossier_id uuid not null references public.dossiers (id) on delete cascade, finding_code text not null references public.visit_finding_definitions (code), checked boolean not null default false, works_delta_override_eur numeric(14, 2), checked_at timestamptz, unique (dossier_id, finding_code) ); create index if not exists dossier_visit_findings_dossier_idx on public.dossier_visit_findings (dossier_id); -- --------------------------------------------------------------------------- -- Investisseurs (module "Flash" — critères en JSON) -- --------------------------------------------------------------------------- create table if not exists public.investisseurs ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users (id) on delete cascade, display_name text not null, email text, phone text, min_margin_pct numeric(5, 2) not null default 12, max_ticket_eur numeric(14, 2), zones jsonb, strategies jsonb, notes text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index if not exists investisseurs_user_id_idx on public.investisseurs (user_id); -- --------------------------------------------------------------------------- -- updated_at trigger -- --------------------------------------------------------------------------- create or replace function public.set_updated_at() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$; drop trigger if exists set_profiles_updated_at on public.profiles; create trigger set_profiles_updated_at before update on public.profiles for each row execute function public.set_updated_at(); drop trigger if exists set_dossiers_updated_at on public.dossiers; create trigger set_dossiers_updated_at before update on public.dossiers for each row execute function public.set_updated_at(); drop trigger if exists set_investisseurs_updated_at on public.investisseurs; create trigger set_investisseurs_updated_at before update on public.investisseurs for each row execute function public.set_updated_at(); -- --------------------------------------------------------------------------- -- RLS -- --------------------------------------------------------------------------- alter table public.profiles enable row level security; alter table public.dossiers enable row level security; alter table public.dossier_visit_findings enable row level security; alter table public.investisseurs enable row level security; -- lecture / écriture : uniquement son user_id create policy "profiles_self" on public.profiles for all using (auth.uid() = id) with check (auth.uid() = id); create policy "dossiers_self" on public.dossiers for all using (auth.uid() = user_id) with check (auth.uid() = user_id); create policy "visit_findings_via_dossier" on public.dossier_visit_findings for all using ( exists ( select 1 from public.dossiers d where d.id = dossier_id and d.user_id = auth.uid() ) ) with check ( exists ( select 1 from public.dossiers d where d.id = dossier_id and d.user_id = auth.uid() ) ); create policy "investisseurs_self" on public.investisseurs for all using (auth.uid() = user_id) with check (auth.uid() = user_id); -- Catalogue visites : lecture pour tous utilisateurs authentifiés alter table public.visit_finding_definitions enable row level security; create policy "visit_finding_definitions_read" on public.visit_finding_definitions for select to authenticated using (true); -- --------------------------------------------------------------------------- -- Auto-création profil à l'inscription (optionnel) -- --------------------------------------------------------------------------- create or replace function public.handle_new_user() returns trigger language plpgsql security definer set search_path = public as $$ begin insert into public.profiles (id, full_name) values (new.id, coalesce(new.raw_user_meta_data->>'full_name', '')) on conflict (id) do nothing; return new; end; $$; drop trigger if exists on_auth_user_created on auth.users; create trigger on_auth_user_created after insert on auth.users for each row execute function public.handle_new_user();