import { PrismaClient } from "@prisma/client"; import { defaultCategories, type CategoryDefinition } from "../lib/defaults"; const prisma = new PrismaClient(); async function main() { console.log("🏷️ Synchronisation des catΓ©gories hiΓ©rarchiques..."); console.log(` ${defaultCategories.length} catΓ©gories Γ  synchroniser\n`); // ═══════════════════════════════════════════════════════════════════════════ // PHASE 0: Nettoyage des doublons existants // ═══════════════════════════════════════════════════════════════════════════ console.log("═".repeat(50)); console.log("PHASE 0: Nettoyage des doublons"); console.log("═".repeat(50)); const allExisting = await prisma.category.findMany(); const byNormalizedName = new Map(); for (const cat of allExisting) { const normalized = normalizeName(cat.name); if (!byNormalizedName.has(normalized)) { byNormalizedName.set(normalized, []); } byNormalizedName.get(normalized)!.push(cat); } let merged = 0; for (const [_normalized, cats] of byNormalizedName) { if (cats.length > 1) { // Garder celui avec emoji let keeper = cats[0]; for (const cat of cats) { if (/[\u{1F300}-\u{1F9FF}]/u.test(cat.name)) { keeper = cat; break; } } const toDelete: typeof cats = []; for (const cat of cats) { if (cat.id !== keeper.id) toDelete.push(cat); } for (const dup of toDelete) { // TransfΓ©rer transactions await prisma.transaction.updateMany({ where: { categoryId: dup.id }, data: { categoryId: keeper.id }, }); // TransfΓ©rer enfants await prisma.category.updateMany({ where: { parentId: dup.id }, data: { parentId: keeper.id }, }); // Supprimer doublon await prisma.category.delete({ where: { id: dup.id } }); console.log(`πŸ—‘οΈ FusionnΓ©: "${dup.name}" β†’ "${keeper.name}"`); merged++; } } } console.log( merged > 0 ? ` ${merged} doublons fusionnΓ©s` : " Aucun doublon βœ“", ); // SΓ©parer parents et enfants const parentCategories = defaultCategories.filter( (c) => c.parentSlug === null, ); const childCategories = defaultCategories.filter( (c) => c.parentSlug !== null, ); console.log(`\nπŸ“ ${parentCategories.length} catΓ©gories parentes`); console.log(` └─ ${childCategories.length} sous-catΓ©gories\n`); // Map slug -> id (pour rΓ©soudre les parentId) const slugToId = new Map(); let created = 0; let updated = 0; let unchanged = 0; // ═══════════════════════════════════════════════════════════════════════════ // PHASE 1: CrΓ©er/MAJ les catΓ©gories parentes // ═══════════════════════════════════════════════════════════════════════════ console.log("═".repeat(50)); console.log("PHASE 1: CatΓ©gories parentes"); console.log("═".repeat(50)); for (const category of parentCategories) { const result = await upsertCategory(category, null); slugToId.set(category.slug, result.id); if (result.action === "created") created++; else if (result.action === "updated") updated++; else unchanged++; } // ═══════════════════════════════════════════════════════════════════════════ // PHASE 2: CrΓ©er/MAJ les sous-catΓ©gories // ═══════════════════════════════════════════════════════════════════════════ console.log("\n" + "═".repeat(50)); console.log("PHASE 2: Sous-catΓ©gories"); console.log("═".repeat(50)); for (const category of childCategories) { const parentId = slugToId.get(category.parentSlug!); if (!parentId) { console.log( `⚠️ Parent introuvable pour: ${category.name} (parentSlug: ${category.parentSlug})`, ); continue; } const result = await upsertCategory(category, parentId); slugToId.set(category.slug, result.id); if (result.action === "created") created++; else if (result.action === "updated") updated++; else unchanged++; } // ═══════════════════════════════════════════════════════════════════════════ // RΓ‰SUMΓ‰ // ═══════════════════════════════════════════════════════════════════════════ console.log("\n" + "═".repeat(50)); console.log("πŸ“Š RΓ‰SUMΓ‰ CATΓ‰GORIES:"); console.log("═".repeat(50)); console.log(` βœ… Créées: ${created}`); console.log(` ✏️ Mises Γ  jour: ${updated}`); console.log(` ⏭️ InchangΓ©es: ${unchanged}`); // Stats finales const totalCategories = await prisma.category.count(); const parentCount = await prisma.category.count({ where: { parentId: null }, }); const childCount = await prisma.category.count({ where: { NOT: { parentId: null } }, }); const totalKeywords = defaultCategories.reduce( (sum, c) => sum + c.keywords.length, 0, ); console.log("\nπŸ“ˆ Base de donnΓ©es:"); console.log( ` Total catΓ©gories: ${totalCategories} (${parentCount} parents, ${childCount} enfants)`, ); console.log(` Total keywords: ${totalKeywords}`); } // Normaliser un nom (enlever emojis, espaces multiples, lowercase) function normalizeName(name: string): string { return name .replace(/[\u{1F300}-\u{1F9FF}]/gu, "") // Remove emojis .replace(/[^\w\sΓ€-ΓΏ]/g, "") // Keep only alphanumeric and accents .replace(/\s+/g, " ") .toLowerCase() .trim(); } async function upsertCategory( category: CategoryDefinition, parentId: string | null, ): Promise<{ id: string; action: "created" | "updated" | "unchanged" }> { // Chercher par nom exact d'abord let existing = await prisma.category.findFirst({ where: { name: category.name }, }); // Si pas trouvΓ©, chercher par nom normalisΓ© (sans emoji) dans TOUTES les catΓ©gories if (!existing) { const allCategories = await prisma.category.findMany(); const normalizedTarget = normalizeName(category.name); for (const cat of allCategories) { if (normalizeName(cat.name) === normalizedTarget) { existing = cat; console.log( ` πŸ”— Match normalisΓ©: "${cat.name}" β†’ "${category.name}"`, ); break; } } } if (existing) { // Comparer pour voir si mise Γ  jour nΓ©cessaire const existingKeywords = JSON.parse(existing.keywords) as string[]; const keywordsChanged = JSON.stringify(existingKeywords.sort()) !== JSON.stringify([...category.keywords].sort()); const nameChanged = existing.name !== category.name; const colorChanged = existing.color !== category.color; const iconChanged = existing.icon !== category.icon; const parentChanged = existing.parentId !== parentId; if ( nameChanged || keywordsChanged || colorChanged || iconChanged || parentChanged ) { await prisma.category.update({ where: { id: existing.id }, data: { name: category.name, // Met Γ  jour le nom aussi (ajout emoji) color: category.color, icon: category.icon, keywords: JSON.stringify(category.keywords), parentId: parentId, }, }); console.log( `✏️ MAJ: ${existing.name}${nameChanged ? ` β†’ ${category.name}` : ""}`, ); if (keywordsChanged) { console.log( ` └─ Keywords: ${existingKeywords.length} β†’ ${category.keywords.length}`, ); } if (parentChanged) { console.log(` └─ Parent modifiΓ©`); } return { id: existing.id, action: "updated" }; } return { id: existing.id, action: "unchanged" }; } // CrΓ©er nouvelle catΓ©gorie const created = await prisma.category.create({ data: { name: category.name, color: category.color, icon: category.icon, keywords: JSON.stringify(category.keywords), parentId: parentId, }, }); console.log( `βœ… Créée: ${category.name}${category.keywords.length > 0 ? ` (${category.keywords.length} keywords)` : ""}`, ); return { id: created.id, action: "created" }; } main() .catch((e) => { console.error("❌ Erreur:", e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });