espace-paie-odentas/scripts/migrate-avenants-from-airtable.ts
odentas da17ca6ef2 feat: Amélioration de la page staff/avenants avec pagination et filtres
- Ajout de filtres sophistiqués : organisation, statut, type, signature, élément, dates
- Tri par colonne : date d'effet, date d'avenant, n° avenant, n° contrat
- Pagination avec 25/50/100 éléments par page
- Ordre par défaut : date d'effet décroissant (plus récent en premier)
- Compteur de filtres actifs avec bouton de réinitialisation
- Affichage du matricule salarié, n° avenant et type d'avenant dans le tableau
- Recherche étendue : inclut matricule, production et n° avenant
- Interface cohérente avec les pages staff/contrats et staff/payslips
2025-11-05 18:28:40 +01:00

307 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Script de migration des avenants depuis Airtable vers Supabase
*
* Ce script lit le fichier CSV exporté depuis Airtable et crée les entrées
* dans la table `avenants` de Supabase.
*
* Utilisation:
* npx tsx scripts/migrate-avenants-from-airtable.ts
*/
import { createClient } from '@supabase/supabase-js';
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';
import { config } from 'dotenv';
// Charger les variables d'environnement
config({ path: path.join(process.cwd(), '.env.local') });
// Configuration
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const CSV_FILE_PATH = path.join(process.cwd(), 'Contrats de travail-Tous les CDDU.csv');
if (!SUPABASE_URL || !SUPABASE_SERVICE_KEY) {
console.error('❌ Variables d\'environnement manquantes');
console.error('Assurez-vous que NEXT_PUBLIC_SUPABASE_URL et SUPABASE_SERVICE_ROLE_KEY sont définies');
process.exit(1);
}
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY);
interface AirtableAvenantRow {
reference: string; // Référence du contrat (ex: DK1M4LEQ, RG4168801)
structure_api: string; // Nom de l'organisation (ex: Association Atelier Moz)
date_avenant: string; // Date de l'avenant (format: D/M/YYYY)
effet_avenant: string; // Date d'effet (format: D/M/YYYY)
elements_avenantes: string; // Éléments modifiés (ex: "Durée de l'engagement,Rémunération")
}
interface ContractMatch {
id: string;
contract_number: string;
org_id: string;
organization_name: string;
}
/**
* Parse une date au format D/M/YYYY ou DD/MM/YYYY vers YYYY-MM-DD
*/
function parseDate(dateStr: string): string | null {
if (!dateStr || dateStr.trim() === '') return null;
const parts = dateStr.split('/');
if (parts.length !== 3) return null;
const day = parts[0].padStart(2, '0');
const month = parts[1].padStart(2, '0');
const year = parts[2];
return `${year}-${month}-${day}`;
}
/**
* Parse les éléments avenantés depuis le CSV
* Ex: "Durée de l'engagement,Rémunération" => ["duree", "remuneration"]
*/
function parseElements(elementsStr: string): string[] {
if (!elementsStr || elementsStr.trim() === '') return [];
const elements: string[] = [];
const parts = elementsStr.split(',').map(s => s.trim());
for (const part of parts) {
if (part.includes('Durée')) {
elements.push('duree');
} else if (part.includes('Rémunération')) {
elements.push('remuneration');
} else if (part.includes('Objet')) {
elements.push('objet');
} else if (part.includes('Lieu') || part.includes('horaire')) {
elements.push('lieu_horaire');
}
}
return elements;
}
/**
* Recherche un contrat par sa référence
*/
async function findContract(reference: string): Promise<ContractMatch | null> {
// Chercher dans la table cddu_contracts
const { data, error } = await supabase
.from('cddu_contracts')
.select(`
id,
contract_number,
org_id,
organizations!inner(name)
`)
.or(`contract_number.eq.${reference},reference.eq.${reference}`)
.maybeSingle();
if (error) {
console.error(` ⚠️ Erreur recherche contrat ${reference}:`, error.message);
return null;
}
if (!data) {
return null;
}
return {
id: data.id,
contract_number: data.contract_number,
org_id: data.org_id,
organization_name: (data.organizations as any)?.name || 'Unknown'
};
}
/**
* Génère un numéro d'avenant pour une organisation
*/
async function generateNumeroAvenant(orgId: string): Promise<string> {
// Compter les avenants existants pour cette organisation
const { count } = await supabase
.from('avenants')
.select('contract_id, cddu_contracts!inner(org_id)', { count: 'exact', head: true })
.eq('cddu_contracts.org_id', orgId);
const numero = (count || 0) + 1;
return `AVE-${String(numero).padStart(3, '0')}`;
}
/**
* Lit et parse le fichier CSV
*/
async function readCSV(): Promise<AirtableAvenantRow[]> {
return new Promise((resolve, reject) => {
const rows: AirtableAvenantRow[] = [];
const fileStream = fs.createReadStream(CSV_FILE_PATH);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
let isFirstLine = true;
rl.on('line', (line) => {
// Ignorer la première ligne (header)
if (isFirstLine) {
isFirstLine = false;
return;
}
// Parser le CSV (attention aux virgules dans les guillemets)
const matches = line.match(/(?:^|,)("(?:[^"]|"")*"|[^,]*)/g);
if (!matches || matches.length < 5) return;
const cols = matches.map(m => {
let val = m.replace(/^,/, ''); // Supprimer virgule de début
if (val.startsWith('"') && val.endsWith('"')) {
val = val.slice(1, -1).replace(/""/g, '"');
}
return val.trim();
});
rows.push({
reference: cols[0] || '',
structure_api: cols[1] || '',
date_avenant: cols[2] || '',
effet_avenant: cols[3] || '',
elements_avenantes: cols[4] || ''
});
});
rl.on('close', () => resolve(rows));
rl.on('error', reject);
});
}
/**
* Fonction principale de migration
*/
async function migrate() {
console.log('🚀 Début de la migration des avenants depuis Airtable\n');
// Vérifier que le fichier CSV existe
if (!fs.existsSync(CSV_FILE_PATH)) {
console.error(`❌ Fichier CSV non trouvé: ${CSV_FILE_PATH}`);
console.error('📝 Placez le fichier "Contrats de travail-Tous les CDDU.csv" à la racine du projet');
process.exit(1);
}
console.log(`📄 Lecture du fichier: ${CSV_FILE_PATH}\n`);
const rows = await readCSV();
console.log(`📊 Nombre total de lignes: ${rows.length}\n`);
const stats = {
total: rows.length,
skipped: 0,
contractNotFound: 0,
success: 0,
errors: 0
};
const contractsNotFound: string[] = [];
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const index = i + 1;
console.log(`\n[${index}/${rows.length}] Traitement de ${row.reference}`);
// Ignorer les lignes sans date d'avenant
if (!row.date_avenant || row.date_avenant.trim() === '') {
console.log(` ⏭️ Aucune date d'avenant - Ignoré`);
stats.skipped++;
continue;
}
// Rechercher le contrat
const contract = await findContract(row.reference);
if (!contract) {
console.log(` ❌ Contrat non trouvé: ${row.reference}`);
contractsNotFound.push(row.reference);
stats.contractNotFound++;
continue;
}
console.log(` ✅ Contrat trouvé: ${contract.contract_number} (${contract.organization_name})`);
// Parser les dates
const dateAvenant = parseDate(row.date_avenant);
const dateEffet = parseDate(row.effet_avenant);
if (!dateAvenant || !dateEffet) {
console.log(` ⚠️ Dates invalides - Ignoré`);
stats.skipped++;
continue;
}
// Parser les éléments
const elements = parseElements(row.elements_avenantes);
// Générer le numéro d'avenant
const numeroAvenant = await generateNumeroAvenant(contract.org_id);
// Créer l'avenant
const avenantData = {
contract_id: contract.id,
numero_avenant: numeroAvenant,
date_avenant: dateAvenant,
date_effet: dateEffet,
type_avenant: 'modification',
motif_avenant: `Migration depuis Airtable - Éléments: ${row.elements_avenantes}`,
elements_avenantes: elements,
statut: 'signed', // On considère les avenants Airtable comme signés
signature_status: 'signed',
// Pas de PDF ni de données détaillées pour l'instant
objet_data: null,
duree_data: null,
lieu_horaire_data: null,
remuneration_data: null,
pdf_url: null,
pdf_s3_key: null
};
const { data, error } = await supabase
.from('avenants')
.insert(avenantData)
.select()
.single();
if (error) {
console.log(` ❌ Erreur création: ${error.message}`);
stats.errors++;
} else {
console.log(` ✅ Avenant créé: ${numeroAvenant} (Date: ${dateAvenant})`);
stats.success++;
}
}
// Résumé
console.log('\n\n' + '='.repeat(60));
console.log('📊 RÉSUMÉ DE LA MIGRATION');
console.log('='.repeat(60));
console.log(`Total de lignes: ${stats.total}`);
console.log(`Avenants créés: ${stats.success}`);
console.log(`Lignes ignorées: ${stats.skipped} ⏭️`);
console.log(`Contrats non trouvés: ${stats.contractNotFound}`);
console.log(`Erreurs: ${stats.errors}`);
console.log('='.repeat(60));
if (contractsNotFound.length > 0) {
console.log('\n⚠ Contrats non trouvés dans Supabase:');
contractsNotFound.forEach(ref => console.log(` - ${ref}`));
console.log('\n💡 Ces contrats doivent être migrés avant de créer leurs avenants');
}
console.log('\n✅ Migration terminée!\n');
}
// Exécuter la migration
migrate().catch(console.error);