/// /** * Collections métier PocketBase (v0.23+). * Ne recrée PAS `users` : elle existe déjà (auth par défaut). * Idempotent : si une collection existe déjà, on la réutilise (évite "name must be unique" au redémarrage). */ migrate( (app) => { const usersId = app.findCollectionByNameOrId("users").id; /** Retrouve une collection même si findCollectionByNameOrId échoue (casse, cache, image Docker). */ function findExistingCollection(name) { try { return app.findCollectionByNameOrId(name); } catch (_) {} try { const all = app.findAllCollections(); const want = String(name).toLowerCase(); for (let i = 0; i < all.length; i++) { const c = all[i]; if (c && c.name && String(c.name).toLowerCase() === want) { return c; } } } catch (_) {} return null; } function loadOrCreate(name, factory) { const existing = findExistingCollection(name); if (existing != null) { return existing; } try { const col = factory(); app.save(col); return col; } catch (err) { const msg = String(err && err.value ? err.value : err && err.message ? err.message : err); if (msg.includes("unique") || msg.includes("Unique")) { const again = findExistingCollection(name); if (again != null) { return again; } } throw err; } } // ── 1. Étapes pipeline ─────────────────────────────────── const etapes = loadOrCreate("etapes_pipeline", () => new Collection({ name: "etapes_pipeline", type: "base", fields: [ { name: "user", type: "relation", required: true, options: { collectionId: usersId, maxSelect: 1, cascadeDelete: true } }, { name: "nom", type: "text", required: true }, { name: "ordre", type: "number", required: true }, { name: "couleur", type: "text" }, { name: "is_terminal", type: "bool" }, ], listRule: "@request.auth.id != \"\" && user = @request.auth.id", viewRule: "@request.auth.id != \"\" && user = @request.auth.id", createRule: "@request.auth.id != \"\"", updateRule: "@request.auth.id != \"\" && user = @request.auth.id", deleteRule: "@request.auth.id != \"\" && user = @request.auth.id", })); // ── 2. Contacts ────────────────────────────────────────── const contacts = loadOrCreate("contacts", () => new Collection({ name: "contacts", type: "base", fields: [ { name: "user", type: "relation", required: true, options: { collectionId: usersId, maxSelect: 1, cascadeDelete: true } }, { name: "nom", type: "text", required: true }, { name: "prenom", type: "text" }, { name: "societe", type: "text" }, { name: "categorie", type: "select", required: true, options: { maxSelect: 1, values: ["notaire", "agent_immo", "artisan_gros_oeuvre", "artisan_second_oeuvre", "artisan_finitions", "banquier", "courtier", "diagnostiqueur", "geometre", "avocat", "comptable", "vendeur", "acheteur", "autre"] } }, { name: "specialite", type: "text" }, { name: "telephone", type: "text" }, { name: "telephone_2", type: "text" }, { name: "email", type: "email" }, { name: "ville", type: "text" }, { name: "zone_intervention", type: "text" }, { name: "note", type: "number", options: { min: 1, max: 5 } }, { name: "recommande", type: "bool" }, { name: "taux_horaire", type: "number" }, { name: "notes", type: "text" }, { name: "is_favori", type: "bool" }, ], listRule: "@request.auth.id != \"\" && user = @request.auth.id", viewRule: "@request.auth.id != \"\" && user = @request.auth.id", createRule: "@request.auth.id != \"\"", updateRule: "@request.auth.id != \"\" && user = @request.auth.id", deleteRule: "@request.auth.id != \"\" && user = @request.auth.id", })); // ── 3. Biens ───────────────────────────────────────────── const biens = loadOrCreate("biens", () => new Collection({ name: "biens", type: "base", fields: [ { name: "user", type: "relation", required: true, options: { collectionId: usersId, maxSelect: 1, cascadeDelete: true } }, { name: "etape", type: "relation", options: { collectionId: etapes.id, maxSelect: 1 } }, { name: "source_contact", type: "relation", options: { collectionId: contacts.id, maxSelect: 1 } }, { name: "titre", type: "text" }, { name: "type_bien", type: "select", options: { maxSelect: 1, values: ["appartement", "maison", "immeuble", "terrain", "local_commercial", "parking", "cave", "autre"] } }, { name: "adresse", type: "text" }, { name: "code_postal", type: "text" }, { name: "ville", type: "text" }, { name: "latitude", type: "number" }, { name: "longitude", type: "number" }, { name: "surface_habitable", type: "number" }, { name: "surface_totale", type: "number" }, { name: "nb_pieces", type: "number" }, { name: "nb_chambres", type: "number" }, { name: "annee_construction", type: "number" }, { name: "dpe_lettre", type: "select", options: { maxSelect: 1, values: ["A", "B", "C", "D", "E", "F", "G"] } }, { name: "dpe_valeur", type: "number" }, { name: "source", type: "select", options: { maxSelect: 1, values: ["particulier", "agence", "notaire", "tribunal", "succession", "reseau", "autre"] } }, { name: "url_annonce", type: "url" }, { name: "statut", type: "select", options: { maxSelect: 1, values: ["actif", "abandonne", "vendu"] } }, { name: "priorite", type: "number" }, { name: "is_off_market", type: "bool" }, { name: "date_premiere_visite", type: "text" }, { name: "date_offre", type: "text" }, { name: "date_compromis", type: "text" }, { name: "date_acte", type: "text" }, { name: "description", type: "text" }, { name: "points_forts", type: "text" }, { name: "points_faibles", type: "text" }, { name: "photo_principale", type: "file", options: { maxSelect: 1, maxSize: 10485760, mimeTypes: ["image/jpeg", "image/png", "image/webp"] } }, ], listRule: "@request.auth.id != \"\" && user = @request.auth.id", viewRule: "@request.auth.id != \"\" && user = @request.auth.id", createRule: "@request.auth.id != \"\"", updateRule: "@request.auth.id != \"\" && user = @request.auth.id", deleteRule: "@request.auth.id != \"\" && user = @request.auth.id", })); // ── 4. Analyses financières ─────────────────────────────── loadOrCreate("analyses_financieres", () => new Collection({ name: "analyses_financieres", type: "base", fields: [ { name: "user", type: "relation", required: true, options: { collectionId: usersId, maxSelect: 1, cascadeDelete: true } }, { name: "bien", type: "relation", required: true, options: { collectionId: biens.id, maxSelect: 1, cascadeDelete: true } }, { name: "prix_achat", type: "number" }, { name: "type_bien_fiscal", type: "select", options: { maxSelect: 1, values: ["ancien", "neuf"] } }, { name: "frais_notaire", type: "number" }, { name: "frais_agence_achat", type: "number" }, { name: "budget_travaux", type: "number" }, { name: "reserve_imprevus_pct", type: "number" }, { name: "duree_portage_mois", type: "number" }, { name: "taux_credit", type: "number" }, { name: "taxe_fonciere_annuelle", type: "number" }, { name: "charges_copropriete_mensuelle", type: "number" }, { name: "prix_revente_cible", type: "number" }, { name: "frais_agence_vente_pct", type: "number" }, { name: "taux_impot", type: "number" }, { name: "marge_brute", type: "number" }, { name: "marge_brute_pct", type: "number" }, { name: "marge_nette", type: "number" }, { name: "marge_nette_pct", type: "number" }, { name: "notes", type: "text" }, ], listRule: "@request.auth.id != \"\" && user = @request.auth.id", viewRule: "@request.auth.id != \"\" && user = @request.auth.id", createRule: "@request.auth.id != \"\"", updateRule: "@request.auth.id != \"\" && user = @request.auth.id", deleteRule: "@request.auth.id != \"\" && user = @request.auth.id", })); // ── 5. Visites ──────────────────────────────────────────── loadOrCreate("visites", () => new Collection({ name: "visites", type: "base", fields: [ { name: "user", type: "relation", required: true, options: { collectionId: usersId, maxSelect: 1, cascadeDelete: true } }, { name: "bien", type: "relation", required: true, options: { collectionId: biens.id, maxSelect: 1, cascadeDelete: true } }, { name: "date_visite", type: "date", required: true }, { name: "duree_minutes", type: "number" }, { name: "type_visite", type: "select", options: { maxSelect: 1, values: ["premiere", "seconde", "expert", "contradiction"] } }, { name: "notes_brutes", type: "text" }, { name: "rapport_genere", type: "text" }, { name: "checklist_reponses", type: "json" }, { name: "estimation_travaux_min", type: "number" }, { name: "estimation_travaux_max", type: "number" }, { name: "avis_global", type: "select", options: { maxSelect: 1, values: ["coup_de_coeur", "interessant", "neutre", "a_eviter"] } }, { name: "score_opportunite", type: "number", options: { min: 1, max: 10 } }, { name: "photos", type: "file", options: { maxSelect: 20, maxSize: 10485760, mimeTypes: ["image/jpeg", "image/png", "image/webp"] } }, ], listRule: "@request.auth.id != \"\" && user = @request.auth.id", viewRule: "@request.auth.id != \"\" && user = @request.auth.id", createRule: "@request.auth.id != \"\"", updateRule: "@request.auth.id != \"\" && user = @request.auth.id", deleteRule: "@request.auth.id != \"\" && user = @request.auth.id", })); // ── 6. Tâches ───────────────────────────────────────────── loadOrCreate("taches", () => new Collection({ name: "taches", type: "base", fields: [ { name: "user", type: "relation", required: true, options: { collectionId: usersId, maxSelect: 1, cascadeDelete: true } }, { name: "bien", type: "relation", options: { collectionId: biens.id, maxSelect: 1 } }, { name: "contact", type: "relation", options: { collectionId: contacts.id, maxSelect: 1 } }, { name: "titre", type: "text", required: true }, { name: "description", type: "text" }, { name: "type_tache", type: "select", options: { maxSelect: 1, values: ["visite", "appel", "email", "relance", "administratif", "travaux", "banque", "autre"] } }, { name: "priorite", type: "number" }, { name: "statut", type: "select", options: { maxSelect: 1, values: ["a_faire", "en_cours", "fait", "annule"] } }, { name: "date_echeance", type: "date" }, { name: "date_rappel", type: "date" }, { name: "is_urgent", type: "bool" }, ], listRule: "@request.auth.id != \"\" && user = @request.auth.id", viewRule: "@request.auth.id != \"\" && user = @request.auth.id", createRule: "@request.auth.id != \"\"", updateRule: "@request.auth.id != \"\" && user = @request.auth.id", deleteRule: "@request.auth.id != \"\" && user = @request.auth.id", })); // ── 7. Notes biens ──────────────────────────────────────── loadOrCreate("notes_biens", () => new Collection({ name: "notes_biens", type: "base", fields: [ { name: "user", type: "relation", required: true, options: { collectionId: usersId, maxSelect: 1, cascadeDelete: true } }, { name: "bien", type: "relation", required: true, options: { collectionId: biens.id, maxSelect: 1, cascadeDelete: true } }, { name: "contenu", type: "text", required: true }, { name: "type_note", type: "select", options: { maxSelect: 1, values: ["libre", "contact_vendeur", "negociation", "info_marche"] } }, ], listRule: "@request.auth.id != \"\" && user = @request.auth.id", viewRule: "@request.auth.id != \"\" && user = @request.auth.id", createRule: "@request.auth.id != \"\"", updateRule: "@request.auth.id != \"\" && user = @request.auth.id", deleteRule: "@request.auth.id != \"\" && user = @request.auth.id", })); // ── 8. Documents biens ──────────────────────────────────── loadOrCreate("documents_biens", () => new Collection({ name: "documents_biens", type: "base", fields: [ { name: "user", type: "relation", required: true, options: { collectionId: usersId, maxSelect: 1, cascadeDelete: true } }, { name: "bien", type: "relation", required: true, options: { collectionId: biens.id, maxSelect: 1, cascadeDelete: true } }, { name: "nom", type: "text", required: true }, { name: "type_document", type: "select", options: { maxSelect: 1, values: ["compromis", "acte", "dpe", "diagnostics", "devis", "facture", "titre_propriete", "autre"] } }, { name: "date_document", type: "date" }, { name: "notes", type: "text" }, { name: "fichier", type: "file", required: true, options: { maxSelect: 1, maxSize: 52428800, mimeTypes: ["application/pdf", "image/jpeg", "image/png"] } }, ], listRule: "@request.auth.id != \"\" && user = @request.auth.id", viewRule: "@request.auth.id != \"\" && user = @request.auth.id", createRule: "@request.auth.id != \"\"", updateRule: "@request.auth.id != \"\" && user = @request.auth.id", deleteRule: "@request.auth.id != \"\" && user = @request.auth.id", })); // ── 9. Devis travaux ───────────────────────────────────── loadOrCreate("devis_travaux", () => new Collection({ name: "devis_travaux", type: "base", fields: [ { name: "user", type: "relation", required: true, options: { collectionId: usersId, maxSelect: 1, cascadeDelete: true } }, { name: "bien", type: "relation", required: true, options: { collectionId: biens.id, maxSelect: 1, cascadeDelete: true } }, { name: "contact", type: "relation", options: { collectionId: contacts.id, maxSelect: 1 } }, { name: "lot", type: "text", required: true }, { name: "description", type: "text" }, { name: "montant_ht", type: "number" }, { name: "taux_tva", type: "number" }, { name: "montant_ttc", type: "number" }, { name: "statut", type: "select", options: { maxSelect: 1, values: ["en_attente", "refuse", "accepte", "en_cours", "termine", "paye"] } }, { name: "date_devis", type: "date" }, { name: "date_debut", type: "date" }, { name: "date_fin_prevu", type: "date" }, { name: "notes", type: "text" }, { name: "fichier_pdf", type: "file", options: { maxSelect: 1, maxSize: 10485760, mimeTypes: ["application/pdf"] } }, ], listRule: "@request.auth.id != \"\" && user = @request.auth.id", viewRule: "@request.auth.id != \"\" && user = @request.auth.id", createRule: "@request.auth.id != \"\"", updateRule: "@request.auth.id != \"\" && user = @request.auth.id", deleteRule: "@request.auth.id != \"\" && user = @request.auth.id", })); }, (app) => { for (const name of [ "devis_travaux", "documents_biens", "notes_biens", "taches", "visites", "analyses_financieres", "biens", "contacts", "etapes_pipeline", ]) { try { app.delete(app.findCollectionByNameOrId(name)); } catch (_) {} } }, );