- 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
307 lines
9.1 KiB
TypeScript
307 lines
9.1 KiB
TypeScript
/**
|
||
* 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);
|