From fadb1454c6baccf17895672a47b2eb24a3b6e85b Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 3 Feb 2026 14:34:33 +0000 Subject: [PATCH] Auto commit: 2026-02-03T14:34:33.216Z --- assets/pasted-20260203-130644-02f333fa.jpg | Bin 0 -> 51114 bytes backend/src/db/api/users.js | 4 +- backend/src/index.js | 4 +- backend/src/routes/users.js | 40 ++- backend/src/services/users.js | 279 +++++++++++---------- frontend/src/menuAside.ts | 131 +++++----- frontend/src/pages/check-in.tsx | 38 +-- 7 files changed, 277 insertions(+), 219 deletions(-) create mode 100644 assets/pasted-20260203-130644-02f333fa.jpg diff --git a/assets/pasted-20260203-130644-02f333fa.jpg b/assets/pasted-20260203-130644-02f333fa.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e8d19ec626a8d57792a29819c54c1c63178d73f4 GIT binary patch literal 51114 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 { const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); - await UsersService.create(req.body.data, req.currentUser, true, link.host); + await UsersService.create(req.body.data, req.currentUser, false, link.host); const payload = true; res.status(200).send(payload); })); @@ -126,11 +127,40 @@ router.post('/', wrapAsync(async (req, res) => { * */ router.post('/bulk-import', wrapAsync(async (req, res) => { + console.log('Received bulk import request'); + + // Process file upload first + try { + await processFile(req, res); + } catch (e) { + console.error('File upload error:', e); + return res.status(400).send({ message: 'Error uploading file' }); + } + + if (!req.file || !req.file.buffer) { + return res.status(400).send({ message: 'File is required' }); + } + + // Create a job entry + const job = await db.import_jobs.create({ + filename: req.file.originalname, + status: 'pending', + imported_byId: req.currentUser.id, + imported_at: new Date(), + rows_processed: 0 + }); + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); - await UsersService.bulkImport(req, res, true, link.host); - const payload = true; - res.status(200).send(payload); + + // Respond immediately to prevent timeout + res.status(200).send({ success: true, jobId: job.id }); + + // Process in background + console.log(`Starting background processing for job ${job.id}`); + UsersService.processBulkImport(req.file.buffer, req.currentUser, false, link.host, job.id) + .then(() => console.log(`Job ${job.id} finished successfully`)) + .catch(err => console.error(`Job ${job.id} failed:`, err)); })); /** diff --git a/backend/src/services/users.js b/backend/src/services/users.js index b2ad4d2..fc7eead 100644 --- a/backend/src/services/users.js +++ b/backend/src/services/users.js @@ -52,20 +52,29 @@ module.exports = class UsersService { } } - static async bulkImport(req, res, sendInvitationEmails = true, host) { - console.log('Starting bulk import...'); - + static async bulkImport(req, res, sendInvitationEmails = false, host) { + console.log('Starting bulk import (legacy called)...'); try { - await processFile(req, res); - + if (!req.file) { await processFile(req, res); } if (!req.file || !req.file.buffer) { throw new ValidationError('importer.errors.fileRequired'); } + return await this.processBulkImport(req.file.buffer, req.currentUser, sendInvitationEmails, host); + } catch (error) { + console.error('Bulk import error:', error); + throw error; + } + } - console.log('File received, size:', req.file.size); + static async processBulkImport(fileBuffer, currentUser, sendInvitationEmails = false, host, jobId = null) { + console.log(`Processing bulk import${jobId ? ` for job ${jobId}` : ''}...`); + + let totalProcessed = 0; + let totalSkipped = 0; + try { // Detect separator - const content = req.file.buffer.toString('utf-8'); + const content = fileBuffer.toString('utf-8'); const firstLine = content.split('\n')[0]; let separator = ','; if (firstLine.includes(';')) separator = ';'; @@ -88,177 +97,185 @@ module.exports = class UsersService { const defaultRoleName = config.roles?.user || 'Employee'; const userRole = await db.roles.findOne({ where: { name: defaultRoleName } }); + const normalizeHeader = (h) => { + if (!h) return ''; + return h.toLowerCase() + .normalize("NFD") + .replace(/[̀-ͯ]/g, "") // Remove diacritics + .replace(/[^a-z0-9]/g, ""); // Remove non-alphanumeric + }; + const headerMapping = { 'email': 'email', - 'e-mail': 'email', - 'mail professionnel': 'email', - 'prénom': 'firstName', + 'mailprofessionnel': 'email', + 'courriel': 'email', + 'mail': 'email', + 'adressemail': 'email', 'prenom': 'firstName', + 'firstname': 'firstName', 'nom': 'lastName', - 'téléphone': 'phoneNumber', + 'lastname': 'lastName', 'telephone': 'phoneNumber', - 'n° tel': 'phoneNumber', + 'ntel': 'phoneNumber', + 'numerotel': 'phoneNumber', + 'phonenumber': 'phoneNumber', 'matricule': 'matriculePaie', - 'matricule paie': 'matriculePaie', - 'wd id': 'workdayId', + 'matriculepaie': 'matriculePaie', + 'wdid': 'workdayId', 'workday': 'workdayId', + 'workdayid': 'workdayId', 'site': 'productionSite', - 'site de production': 'productionSite', - 'télétravail': 'remoteWork', + 'sitedeproduction': 'productionSite', + 'productionsite': 'productionSite', 'teletravail': 'remoteWork', - 'date d\'embauche': 'hiringDate', + 'remotework': 'remoteWork', + 'datedembauche': 'hiringDate', 'embauche': 'hiringDate', - 'date d\'entrée': 'positionEntryDate', - 'date d\'entrée poste': 'positionEntryDate', - 'entrée poste': 'positionEntryDate', - 'date de départ': 'departureDate', - 'départ': 'departureDate', + 'hiringdate': 'hiringDate', + 'datedentree': 'positionEntryDate', + 'datedentreeposte': 'positionEntryDate', + 'entreeposte': 'positionEntryDate', + 'positionentrydate': 'positionEntryDate', + 'datededepart': 'departureDate', + 'depart': 'departureDate', + 'departuredate': 'departureDate', 'service': 'service', 'poste': 'position', - 'équipe': 'team', + 'position': 'position', 'equipe': 'team', - 'équipe (n+1)': 'team', - 'département': 'department', - 'departement': 'department' + 'equipen1': 'team', + 'team': 'team', + 'departement': 'department', + 'department': 'department' }; const bufferStream = new stream.PassThrough(); + bufferStream.end(fileBuffer); + + const parser = bufferStream.pipe(csv({ + separator: separator, + mapHeaders: ({ header }) => { + const cleanHeader = normalizeHeader(header); + if (headerMapping[cleanHeader]) { + return headerMapping[cleanHeader]; + } + return null; + } + })); + let currentBatch = []; - const batchSize = 1000; - let totalProcessed = 0; + const batchSize = 500; let emailsToInvite = []; const processBatch = async (batch) => { if (batch.length === 0) return; - const transaction = await db.sequelize.transaction(); try { await UsersDBApi.bulkImport(batch, { transaction, ignoreDuplicates: true, - validate: false, // Disable validation for speed in large imports - currentUser: req.currentUser + validate: false, + currentUser: currentUser }); await transaction.commit(); totalProcessed += batch.length; + if (jobId) { + await db.import_jobs.update( + { rows_processed: totalProcessed }, + { where: { id: jobId } } + ); + } console.log(`Processed batch of ${batch.length}. Total processed: ${totalProcessed}`); } catch (error) { await transaction.rollback(); console.error('Batch processing error:', error); - // Continue with next batch? For now, we stop on first error in batch throw error; } }; - const parsePromise = new Promise((resolve, reject) => { - bufferStream - .pipe(csv({ - separator: separator, - mapHeaders: ({ header }) => { - const lowerHeader = header.toLowerCase().trim(); - const cleanHeader = lowerHeader.replace(/^\uFEFF/, ''); - return headerMapping[cleanHeader] || cleanHeader; + for await (const data of parser) { + Object.keys(data).forEach(key => { + if (typeof data[key] === 'string') { + data[key] = data[key].trim(); + if (data[key] === '') data[key] = null; + } + }); + + const email = data.email?.toLowerCase().trim(); + if (email && email.includes('@')) { + if (!existingEmails.has(email)) { + if (data.department) { + const deptName = data.department.toLowerCase().trim(); + if (departmentMap[deptName]) { + data.departmentId = departmentMap[deptName]; + } } - })) - .on('data', async (data) => { - // Clean up data - Object.keys(data).forEach(key => { - if (typeof data[key] === 'string') { - data[key] = data[key].trim(); - if (data[key] === '') data[key] = null; + if (data.remoteWork) { + const val = data.remoteWork.toLowerCase().trim(); + if (['oui', 'yes', 'y', 'true'].includes(val)) data.remoteWork = 'Oui'; + else if (['non', 'no', 'n', 'false'].includes(val)) data.remoteWork = 'Non'; + } + ['hiringDate', 'positionEntryDate', 'departureDate'].forEach(field => { + if (data[field]) { + const parsedDate = moment(data[field], ['DD/MM/YYYY', 'YYYY-MM-DD', 'MM/DD/YYYY'], true); + data[field] = parsedDate.isValid() ? parsedDate.toDate() : null; } }); - - const email = data.email?.toLowerCase().trim(); - - if (email && email.includes('@') && !existingEmails.has(email)) { - if (data.department) { - const deptName = data.department.toLowerCase().trim(); - if (departmentMap[deptName]) { - data.departmentId = departmentMap[deptName]; - } - } - - if (data.remoteWork) { - const val = data.remoteWork.toLowerCase().trim(); - if (['oui', 'yes', 'y', 'true'].includes(val)) data.remoteWork = 'Oui'; - else if (['non', 'no', 'n', 'false'].includes(val)) data.remoteWork = 'Non'; - } - - ['hiringDate', 'positionEntryDate', 'departureDate'].forEach(field => { - if (data[field]) { - const parsedDate = moment(data[field], ['DD/MM/YYYY', 'YYYY-MM-DD', 'MM/DD/YYYY'], true); - data[field] = parsedDate.isValid() ? parsedDate.toDate() : null; - } - }); - - if (!data.app_roleId && userRole) { - data.app_roleId = userRole.id; - } - - currentBatch.push(data); - existingEmails.add(email); - emailsToInvite.push(email); - - if (currentBatch.length >= batchSize) { - const batchToProcess = [...currentBatch]; - currentBatch = []; - // We need to pause the stream to wait for the batch to be processed - bufferStream.pause(); - processBatch(batchToProcess) - .then(() => bufferStream.resume()) - .catch(err => { - bufferStream.destroy(err); - reject(err); - }); - } + if (!data.app_roleId && userRole) { + data.app_roleId = userRole.id; } - }) - .on('end', async () => { - try { - if (currentBatch.length > 0) { - await processBatch(currentBatch); - } - console.log('CSV parsing and batch processing finished. Total:', totalProcessed); - resolve(); - } catch (err) { - reject(err); + + currentBatch.push(data); + existingEmails.add(email); + emailsToInvite.push(email); + + if (currentBatch.length >= batchSize) { + await processBatch(currentBatch); + currentBatch = []; } - }) - .on('error', (error) => { - console.error('CSV parsing error:', error); - reject(error); - }); - }); - - bufferStream.end(req.file.buffer); - await parsePromise; - - if (totalProcessed === 0) { - throw new ValidationError('importer.errors.noRowsFound'); - } - - // Send emails in background to avoid blocking the response further - if (emailsToInvite.length > 0 && sendInvitationEmails) { - console.log(`Starting background email sending for ${emailsToInvite.length} users...`); - // Use a simple background loop with delays to avoid overwhelming SMTP - const sendEmailsInBackground = async (emails) => { - const batchSize = 50; - for (let i = 0; i < emails.length; i += batchSize) { - const batch = emails.slice(i, i + batchSize); - await Promise.all(batch.map(email => - AuthService.sendPasswordResetEmail(email, 'invitation', host).catch(err => console.error(`Failed to send email to ${email}:`, err)) - )); - console.log(`Sent email batch ${i / batchSize + 1}. Total sent: ${Math.min(i + batchSize, emails.length)}`); - // Small delay between batches - await new Promise(resolve => setTimeout(resolve, 1000)); + } else { + totalSkipped++; } - }; - sendEmailsInBackground(emailsToInvite); + } } + if (currentBatch.length > 0) { + await processBatch(currentBatch); + } + + if (jobId) { + await db.import_jobs.update( + { status: 'completed', rows_processed: totalProcessed }, + { where: { id: jobId } } + ); + } + + console.log('Import finished. Total:', totalProcessed, 'Skipped:', totalSkipped); + + // Send emails in background + if (emailsToInvite.length > 0 && sendInvitationEmails) { + (async () => { + const batchSize = 50; + for (let i = 0; i < emailsToInvite.length; i += batchSize) { + const batch = emailsToInvite.slice(i, i + batchSize); + await Promise.all(batch.map(email => + AuthService.sendPasswordResetEmail(email, 'invitation', host).catch(err => console.error(`Email error for ${email}:`, err)) + )); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + })().catch(err => console.error('Background email error:', err)); + } + + return { totalProcessed, totalSkipped }; + } catch (error) { console.error('Bulk import error:', error); + if (jobId) { + await db.import_jobs.update( + { status: 'failed' }, + { where: { id: jobId } } + ).catch(e => console.error('Failed to update job status:', e)); + } throw error; } } diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 1366712..1bdc8a5 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -5,82 +5,87 @@ const menuAside: MenuAsideItem[] = [ { href: '/dashboard', icon: icon.mdiViewDashboardOutline, - label: 'Dashboard', + label: 'Tableau de bord', }, { href: '/check-in', - label: 'Pointage Entrée', + label: 'Enregistrement Entrée', icon: icon.mdiClockIn, permissions: 'CREATE_TIME_ENTRIES' }, { - href: '/users/users-list', - label: 'Collaborateurs', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiAccountGroup ?? icon.mdiTable, - permissions: 'READ_USERS' + label: 'Données', + icon: icon.mdiDatabase, + menu: [ + { + href: '/users/users-list', + label: 'Collaborateurs', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiAccountGroup ?? icon.mdiTable, + permissions: 'READ_USERS' + }, + { + href: '/time_entries/time_entries-list', + label: 'Historique Entrées', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiClock' in icon ? icon['mdiClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_TIME_ENTRIES' + }, + { + href: '/badges/badges-list', + label: 'Badges', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiBadgeAccount' in icon ? icon['mdiBadgeAccount' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_BADGES' + }, + ] }, { - href: '/time_entries/time_entries-list', - label: 'Présences', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiClock' in icon ? icon['mdiClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_TIME_ENTRIES' - }, - { - href: '/roles/roles-list', - label: 'Roles', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable, - permissions: 'READ_ROLES' - }, - { - href: '/permissions/permissions-list', - label: 'Permissions', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, - permissions: 'READ_PERMISSIONS' - }, - { - href: '/departments/departments-list', - label: 'Departments', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiOfficeBuilding' in icon ? icon['mdiOfficeBuilding' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_DEPARTMENTS' - }, - { - href: '/import_jobs/import_jobs-list', - label: 'Import jobs', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFileImport' in icon ? icon['mdiFileImport' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_IMPORT_JOBS' - }, - { - href: '/badges/badges-list', - label: 'Badges', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiBadgeAccount' in icon ? icon['mdiBadgeAccount' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_BADGES' + label: 'Configuration', + icon: icon.mdiCog, + menu: [ + { + href: '/import_jobs/import_jobs-list', + label: 'Imports CSV', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiFileImport' in icon ? icon['mdiFileImport' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_IMPORT_JOBS' + }, + { + href: '/departments/departments-list', + label: 'Départements', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: 'mdiOfficeBuilding' in icon ? icon['mdiOfficeBuilding' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_DEPARTMENTS' + }, + { + href: '/roles/roles-list', + label: 'Rôles', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable, + permissions: 'READ_ROLES' + }, + { + href: '/permissions/permissions-list', + label: 'Permissions', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, + permissions: 'READ_PERMISSIONS' + }, + ] }, { href: '/profile', - label: 'Profile', + label: 'Mon Profil', icon: icon.mdiAccountCircle, }, - { - href: '/api-docs', - target: '_blank', - label: 'Swagger API', - icon: icon.mdiFileCode, - permissions: 'READ_API_DOCS' - }, ] -export default menuAside \ No newline at end of file +export default menuAside diff --git a/frontend/src/pages/check-in.tsx b/frontend/src/pages/check-in.tsx index 788527c..6be3cf6 100644 --- a/frontend/src/pages/check-in.tsx +++ b/frontend/src/pages/check-in.tsx @@ -2,7 +2,7 @@ import { mdiClockIn, mdiAccountSearch, mdiCalendarClock, mdiCardAccountDetailsOu import Head from 'next/head' import React, { ReactElement, useEffect, useState } from 'react' import CardBox from '../components/CardBox' -import LayoutGuest from '../layouts/Guest' +import LayoutAuthenticated from '../layouts/Authenticated' import SectionMain from '../components/SectionMain' import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' import { getPageTitle } from '../config' @@ -64,7 +64,7 @@ const EmployeeDetails = () => {

Nom & Prénom:

-

{employeeData.firstName} {employeeData.lastName}

+

{employeeData.firstName} {employeeData.lastName}

Département:

@@ -75,7 +75,7 @@ const EmployeeDetails = () => {

{employeeData.matriculePaie || '-'}

-

Workday ID:

+

WD ID:

{employeeData.workdayId || '-'}

@@ -106,7 +106,7 @@ const EmployeeDetails = () => { const CheckIn = () => { const dispatch = useAppDispatch() const [isSuccess, setIsSuccess] = useState(false) - const [error, setError] = useState(null) + const [error, setError] = useState(null) const handleSubmit = async (data, { resetForm }) => { setIsSuccess(false) @@ -116,6 +116,8 @@ const CheckIn = () => { if (create.fulfilled.match(resultAction)) { setIsSuccess(true) resetForm() + // Keep the start_time current + // data.start_time = moment().format('YYYY-MM-DDTHH:mm') } else { setError("Une erreur est survenue lors de l'enregistrement.") } @@ -153,14 +155,14 @@ const CheckIn = () => { > {({ isSubmitting }) => (
- + @@ -168,13 +170,15 @@ const CheckIn = () => { - - - +
+ + + - - - + + + +
@@ -182,7 +186,7 @@ const CheckIn = () => { { CheckIn.getLayout = function getLayout(page: ReactElement) { return ( - + {page} - + ) } -export default CheckIn +export default CheckIn \ No newline at end of file