271 lines
7.1 KiB
TypeScript
271 lines
7.1 KiB
TypeScript
// src/scripts/import-employees-from-csv.ts
|
||
import { PrismaClient, Roles } from '@prisma/client';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
// ⚙️ Chemin vers ton CSV employees
|
||
const CSV_PATH = path.resolve(__dirname, '../../data/export_new_employee_table.csv');
|
||
|
||
// Rôles éligibles pour la table Employees
|
||
const ELIGIBLE_ROLES: Roles[] = [
|
||
Roles.EMPLOYEE,
|
||
Roles.SUPERVISOR,
|
||
Roles.HR,
|
||
Roles.ACCOUNTING,
|
||
Roles.ADMIN,
|
||
];
|
||
|
||
// Type correspondant EXACT aux colonnes de ton CSV
|
||
type EmployeeCsvRow = {
|
||
employee_number: string;
|
||
email: string;
|
||
job_title: string;
|
||
company: string; // sera converti en number
|
||
is_supervisor: string; // "True"/"False" (ou variantes)
|
||
onboarding: string; // millis
|
||
offboarding: string; // millis ou "NULL"
|
||
};
|
||
|
||
// Représentation minimale d'un user
|
||
type UserSummary = {
|
||
id: string; // UUID
|
||
email: string;
|
||
role: Roles;
|
||
};
|
||
|
||
// ============ Helpers CSV ============
|
||
|
||
function splitCsvLine(line: string): string[] {
|
||
const result: string[] = [];
|
||
let current = '';
|
||
let inQuotes = false;
|
||
|
||
for (let i = 0; i < line.length; i++) {
|
||
const char = line[i];
|
||
|
||
if (char === '"') {
|
||
// guillemet échappé ""
|
||
if (inQuotes && line[i + 1] === '"') {
|
||
current += '"';
|
||
i++;
|
||
} else {
|
||
inQuotes = !inQuotes;
|
||
}
|
||
} else if (char === ',' && !inQuotes) {
|
||
result.push(current);
|
||
current = '';
|
||
} else {
|
||
current += char;
|
||
}
|
||
}
|
||
|
||
result.push(current);
|
||
return result.map((v) => v.trim());
|
||
}
|
||
|
||
// ============ Helpers de parsing ============
|
||
|
||
function parseBoolean(value: string): boolean {
|
||
const v = value.trim().toLowerCase();
|
||
return v === 'true' || v === '1' || v === 'yes' || v === 'y' || v === 'oui';
|
||
}
|
||
|
||
function parseIntSafe(value: string, fieldName: string): number | null {
|
||
const trimmed = value.trim();
|
||
if (!trimmed || trimmed.toUpperCase() === 'NULL') return null;
|
||
|
||
const n = Number.parseInt(trimmed, 10);
|
||
if (Number.isNaN(n)) {
|
||
console.warn(`⚠️ Impossible de parser "${value}" en entier pour le champ ${fieldName}`);
|
||
return null;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
function millisToDate(value: string): Date | null {
|
||
const trimmed = value.trim().toUpperCase();
|
||
if (!trimmed || trimmed === 'NULL') return null;
|
||
|
||
const ms = Number(trimmed);
|
||
if (!Number.isFinite(ms)) {
|
||
console.warn(`⚠️ Impossible de parser "${value}" en millis pour une Date`);
|
||
return null;
|
||
}
|
||
|
||
const d = new Date(ms);
|
||
// On normalise au jour (minuit UTC)
|
||
const normalized = new Date(Date.UTC(
|
||
d.getUTCFullYear(),
|
||
d.getUTCMonth(),
|
||
d.getUTCDate(),
|
||
));
|
||
|
||
return normalized;
|
||
}
|
||
|
||
// ============ MAIN ============
|
||
|
||
async function main() {
|
||
// 1. Lecture du CSV
|
||
const fileContent = fs.readFileSync(CSV_PATH, 'utf-8');
|
||
|
||
const lines = fileContent
|
||
.split(/\r?\n/)
|
||
.map((l) => l.trim())
|
||
.filter((l) => l.length > 0);
|
||
|
||
if (lines.length <= 1) {
|
||
console.error('CSV vide ou seulement un header');
|
||
return;
|
||
}
|
||
|
||
const header = splitCsvLine(lines[0]); // ["employee_number","email",...]
|
||
const dataLines = lines.slice(1);
|
||
|
||
const csvRows: EmployeeCsvRow[] = dataLines.map((line) => {
|
||
const values = splitCsvLine(line);
|
||
const row: any = {};
|
||
|
||
header.forEach((col, idx) => {
|
||
row[col] = values[idx] ?? '';
|
||
});
|
||
|
||
return row as EmployeeCsvRow;
|
||
});
|
||
|
||
console.log(`➡️ ${csvRows.length} lignes trouvées dans le CSV employees`);
|
||
|
||
// 2. Récupérer tous les emails du CSV
|
||
const emails = Array.from(
|
||
new Set(
|
||
csvRows
|
||
.map((r) => r.email.trim())
|
||
.filter((e) => e.length > 0),
|
||
),
|
||
);
|
||
|
||
console.log(`➡️ ${emails.length} emails uniques trouvés dans le CSV`);
|
||
|
||
// 3. Charger les users correspondants avec les bons rôles
|
||
const users = (await prisma.users.findMany({
|
||
where: {
|
||
email: { in: emails },
|
||
role: { in: ELIGIBLE_ROLES },
|
||
},
|
||
select: {
|
||
id: true,
|
||
email: true,
|
||
role: true,
|
||
},
|
||
})) as UserSummary[];
|
||
|
||
console.log(`➡️ ${users.length} users éligibles trouvés dans la DB`);
|
||
|
||
// Map email → user
|
||
const userByEmail = new Map<string, UserSummary>();
|
||
for (const user of users) {
|
||
const key = user.email.trim().toLowerCase();
|
||
userByEmail.set(key, user);
|
||
}
|
||
|
||
// 4. Construire les données pour employees.createMany
|
||
const employeesToCreate: {
|
||
user_id: string;
|
||
external_payroll_id: number;
|
||
company_code: number;
|
||
first_work_day: Date;
|
||
last_work_day: Date | null;
|
||
job_title: string | null;
|
||
is_supervisor: boolean;
|
||
supervisor_id?: number | null;
|
||
}[] = [];
|
||
|
||
const rowsWithoutUser: EmployeeCsvRow[] = [];
|
||
const rowsWithInvalidNumbers: EmployeeCsvRow[] = [];
|
||
|
||
for (const row of csvRows) {
|
||
const emailKey = row.email.trim().toLowerCase();
|
||
const user = userByEmail.get(emailKey);
|
||
|
||
if (!user) {
|
||
rowsWithoutUser.push(row);
|
||
continue;
|
||
}
|
||
|
||
const external_payroll_id = parseIntSafe(row.employee_number, 'external_payroll_id');
|
||
const company_code = parseIntSafe(String(row.company), 'company_code');
|
||
|
||
if (external_payroll_id === null || company_code === null) {
|
||
rowsWithInvalidNumbers.push(row);
|
||
continue;
|
||
}
|
||
|
||
const first_work_day = millisToDate(row.onboarding);
|
||
const last_work_day = millisToDate(row.offboarding);
|
||
const is_supervisor = parseBoolean(row.is_supervisor);
|
||
const job_title = row.job_title?.trim() || null;
|
||
|
||
if (!first_work_day) {
|
||
console.warn(
|
||
`⚠️ Date d'onboarding invalide pour ${row.email} (employee_number=${row.employee_number})`,
|
||
);
|
||
continue;
|
||
}
|
||
|
||
employeesToCreate.push({
|
||
user_id: user.id,
|
||
external_payroll_id,
|
||
company_code,
|
||
first_work_day,
|
||
last_work_day,
|
||
job_title,
|
||
is_supervisor,
|
||
supervisor_id: null, // on pourra gérer ça plus tard si tu as les infos
|
||
});
|
||
}
|
||
|
||
console.log(`➡️ ${employeesToCreate.length} entrées Employees prêtes à être insérées`);
|
||
|
||
if (rowsWithoutUser.length > 0) {
|
||
console.warn(`⚠️ ${rowsWithoutUser.length} lignes CSV sans user correspondant (email / rôle) :`);
|
||
for (const row of rowsWithoutUser) {
|
||
console.warn(
|
||
` - email=${row.email}, employee_number=${row.employee_number}, company=${row.company}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
if (rowsWithInvalidNumbers.length > 0) {
|
||
console.warn(`⚠️ ${rowsWithInvalidNumbers.length} lignes CSV avec ids/compagnies invalides :`);
|
||
for (const row of rowsWithInvalidNumbers) {
|
||
console.warn(
|
||
` - email=${row.email}, employee_number="${row.employee_number}", company="${row.company}"`,
|
||
);
|
||
}
|
||
}
|
||
|
||
if (employeesToCreate.length === 0) {
|
||
console.warn('⚠️ Aucun Employees à créer, arrêt.');
|
||
return;
|
||
}
|
||
|
||
// 5. Insert en batch
|
||
const result = await prisma.employees.createMany({
|
||
data: employeesToCreate,
|
||
skipDuplicates: true, // évite les erreurs si tu relances le script
|
||
});
|
||
|
||
console.log(`✅ ${result.count} employees insérés dans la DB`);
|
||
}
|
||
|
||
main()
|
||
.catch((err) => {
|
||
console.error('❌ Erreur pendant l’import CSV → Employees', err);
|
||
process.exit(1);
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect();
|
||
});
|