Auto commit: 2026-02-03T14:34:33.216Z
This commit is contained in:
parent
6ee47994e6
commit
fadb1454c6
BIN
assets/pasted-20260203-130644-02f333fa.jpg
Normal file
BIN
assets/pasted-20260203-130644-02f333fa.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@ -226,8 +226,8 @@ module.exports = class UsersDBApi {
|
|||||||
service: item.service || null,
|
service: item.service || null,
|
||||||
position: item.position || null,
|
position: item.position || null,
|
||||||
team: item.team || null,
|
team: item.team || null,
|
||||||
departmentId: item.department || item.departmentId || null,
|
departmentId: item.departmentId || (item.department && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(item.department) ? item.department : null),
|
||||||
app_roleId: item.app_role || item.app_roleId || null,
|
app_roleId: item.app_roleId || (item.app_role && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(item.app_role) ? item.app_role : null),
|
||||||
|
|
||||||
importHash: item.importHash || null,
|
importHash: item.importHash || null,
|
||||||
createdById: currentUser.id,
|
createdById: currentUser.id,
|
||||||
|
|||||||
@ -84,7 +84,9 @@ app.use('/api-docs', function (req, res, next) {
|
|||||||
app.use(cors({origin: true}));
|
app.use(cors({origin: true}));
|
||||||
require('./auth/auth');
|
require('./auth/auth');
|
||||||
|
|
||||||
app.use(bodyParser.json());
|
// Increase JSON body limit to 50MB to match file upload limits and prevent Broken Pipe
|
||||||
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
|
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/file', fileRoutes);
|
app.use('/api/file', fileRoutes);
|
||||||
|
|||||||
@ -3,7 +3,8 @@ const express = require('express');
|
|||||||
const UsersService = require('../services/users');
|
const UsersService = require('../services/users');
|
||||||
const UsersDBApi = require('../db/api/users');
|
const UsersDBApi = require('../db/api/users');
|
||||||
const wrapAsync = require('../helpers').wrapAsync;
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
|
const db = require('../db/models');
|
||||||
|
const processFile = require("../middlewares/upload");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@ -85,7 +86,7 @@ router.use(checkCrudPermissions('users'));
|
|||||||
router.post('/', wrapAsync(async (req, res) => {
|
router.post('/', wrapAsync(async (req, res) => {
|
||||||
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||||
const link = new URL(referer);
|
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;
|
const payload = true;
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}));
|
}));
|
||||||
@ -126,11 +127,40 @@ router.post('/', wrapAsync(async (req, res) => {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
router.post('/bulk-import', 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 referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||||
const link = new URL(referer);
|
const link = new URL(referer);
|
||||||
await UsersService.bulkImport(req, res, true, link.host);
|
|
||||||
const payload = true;
|
// Respond immediately to prevent timeout
|
||||||
res.status(200).send(payload);
|
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));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -52,20 +52,29 @@ module.exports = class UsersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
static async bulkImport(req, res, sendInvitationEmails = false, host) {
|
||||||
console.log('Starting bulk import...');
|
console.log('Starting bulk import (legacy called)...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await processFile(req, res);
|
if (!req.file) { await processFile(req, res); }
|
||||||
|
|
||||||
if (!req.file || !req.file.buffer) {
|
if (!req.file || !req.file.buffer) {
|
||||||
throw new ValidationError('importer.errors.fileRequired');
|
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
|
// Detect separator
|
||||||
const content = req.file.buffer.toString('utf-8');
|
const content = fileBuffer.toString('utf-8');
|
||||||
const firstLine = content.split('\n')[0];
|
const firstLine = content.split('\n')[0];
|
||||||
let separator = ',';
|
let separator = ',';
|
||||||
if (firstLine.includes(';')) separator = ';';
|
if (firstLine.includes(';')) separator = ';';
|
||||||
@ -88,177 +97,185 @@ module.exports = class UsersService {
|
|||||||
const defaultRoleName = config.roles?.user || 'Employee';
|
const defaultRoleName = config.roles?.user || 'Employee';
|
||||||
const userRole = await db.roles.findOne({ where: { name: defaultRoleName } });
|
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 = {
|
const headerMapping = {
|
||||||
'email': 'email',
|
'email': 'email',
|
||||||
'e-mail': 'email',
|
'mailprofessionnel': 'email',
|
||||||
'mail professionnel': 'email',
|
'courriel': 'email',
|
||||||
'prénom': 'firstName',
|
'mail': 'email',
|
||||||
|
'adressemail': 'email',
|
||||||
'prenom': 'firstName',
|
'prenom': 'firstName',
|
||||||
|
'firstname': 'firstName',
|
||||||
'nom': 'lastName',
|
'nom': 'lastName',
|
||||||
'téléphone': 'phoneNumber',
|
'lastname': 'lastName',
|
||||||
'telephone': 'phoneNumber',
|
'telephone': 'phoneNumber',
|
||||||
'n° tel': 'phoneNumber',
|
'ntel': 'phoneNumber',
|
||||||
|
'numerotel': 'phoneNumber',
|
||||||
|
'phonenumber': 'phoneNumber',
|
||||||
'matricule': 'matriculePaie',
|
'matricule': 'matriculePaie',
|
||||||
'matricule paie': 'matriculePaie',
|
'matriculepaie': 'matriculePaie',
|
||||||
'wd id': 'workdayId',
|
'wdid': 'workdayId',
|
||||||
'workday': 'workdayId',
|
'workday': 'workdayId',
|
||||||
|
'workdayid': 'workdayId',
|
||||||
'site': 'productionSite',
|
'site': 'productionSite',
|
||||||
'site de production': 'productionSite',
|
'sitedeproduction': 'productionSite',
|
||||||
'télétravail': 'remoteWork',
|
'productionsite': 'productionSite',
|
||||||
'teletravail': 'remoteWork',
|
'teletravail': 'remoteWork',
|
||||||
'date d\'embauche': 'hiringDate',
|
'remotework': 'remoteWork',
|
||||||
|
'datedembauche': 'hiringDate',
|
||||||
'embauche': 'hiringDate',
|
'embauche': 'hiringDate',
|
||||||
'date d\'entrée': 'positionEntryDate',
|
'hiringdate': 'hiringDate',
|
||||||
'date d\'entrée poste': 'positionEntryDate',
|
'datedentree': 'positionEntryDate',
|
||||||
'entrée poste': 'positionEntryDate',
|
'datedentreeposte': 'positionEntryDate',
|
||||||
'date de départ': 'departureDate',
|
'entreeposte': 'positionEntryDate',
|
||||||
'départ': 'departureDate',
|
'positionentrydate': 'positionEntryDate',
|
||||||
|
'datededepart': 'departureDate',
|
||||||
|
'depart': 'departureDate',
|
||||||
|
'departuredate': 'departureDate',
|
||||||
'service': 'service',
|
'service': 'service',
|
||||||
'poste': 'position',
|
'poste': 'position',
|
||||||
'équipe': 'team',
|
'position': 'position',
|
||||||
'equipe': 'team',
|
'equipe': 'team',
|
||||||
'équipe (n+1)': 'team',
|
'equipen1': 'team',
|
||||||
'département': 'department',
|
'team': 'team',
|
||||||
'departement': 'department'
|
'departement': 'department',
|
||||||
|
'department': 'department'
|
||||||
};
|
};
|
||||||
|
|
||||||
const bufferStream = new stream.PassThrough();
|
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 = [];
|
let currentBatch = [];
|
||||||
const batchSize = 1000;
|
const batchSize = 500;
|
||||||
let totalProcessed = 0;
|
|
||||||
let emailsToInvite = [];
|
let emailsToInvite = [];
|
||||||
|
|
||||||
const processBatch = async (batch) => {
|
const processBatch = async (batch) => {
|
||||||
if (batch.length === 0) return;
|
if (batch.length === 0) return;
|
||||||
|
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await UsersDBApi.bulkImport(batch, {
|
await UsersDBApi.bulkImport(batch, {
|
||||||
transaction,
|
transaction,
|
||||||
ignoreDuplicates: true,
|
ignoreDuplicates: true,
|
||||||
validate: false, // Disable validation for speed in large imports
|
validate: false,
|
||||||
currentUser: req.currentUser
|
currentUser: currentUser
|
||||||
});
|
});
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
totalProcessed += batch.length;
|
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}`);
|
console.log(`Processed batch of ${batch.length}. Total processed: ${totalProcessed}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
console.error('Batch processing error:', error);
|
console.error('Batch processing error:', error);
|
||||||
// Continue with next batch? For now, we stop on first error in batch
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsePromise = new Promise((resolve, reject) => {
|
for await (const data of parser) {
|
||||||
bufferStream
|
Object.keys(data).forEach(key => {
|
||||||
.pipe(csv({
|
if (typeof data[key] === 'string') {
|
||||||
separator: separator,
|
data[key] = data[key].trim();
|
||||||
mapHeaders: ({ header }) => {
|
if (data[key] === '') data[key] = null;
|
||||||
const lowerHeader = header.toLowerCase().trim();
|
}
|
||||||
const cleanHeader = lowerHeader.replace(/^\uFEFF/, '');
|
});
|
||||||
return headerMapping[cleanHeader] || cleanHeader;
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}))
|
if (data.remoteWork) {
|
||||||
.on('data', async (data) => {
|
const val = data.remoteWork.toLowerCase().trim();
|
||||||
// Clean up data
|
if (['oui', 'yes', 'y', 'true'].includes(val)) data.remoteWork = 'Oui';
|
||||||
Object.keys(data).forEach(key => {
|
else if (['non', 'no', 'n', 'false'].includes(val)) data.remoteWork = 'Non';
|
||||||
if (typeof data[key] === 'string') {
|
}
|
||||||
data[key] = data[key].trim();
|
['hiringDate', 'positionEntryDate', 'departureDate'].forEach(field => {
|
||||||
if (data[key] === '') data[key] = null;
|
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) {
|
||||||
const email = data.email?.toLowerCase().trim();
|
data.app_roleId = userRole.id;
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.on('end', async () => {
|
currentBatch.push(data);
|
||||||
try {
|
existingEmails.add(email);
|
||||||
if (currentBatch.length > 0) {
|
emailsToInvite.push(email);
|
||||||
await processBatch(currentBatch);
|
|
||||||
}
|
if (currentBatch.length >= batchSize) {
|
||||||
console.log('CSV parsing and batch processing finished. Total:', totalProcessed);
|
await processBatch(currentBatch);
|
||||||
resolve();
|
currentBatch = [];
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
.on('error', (error) => {
|
totalSkipped++;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Bulk import error:', 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,82 +5,87 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
{
|
{
|
||||||
href: '/dashboard',
|
href: '/dashboard',
|
||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'Tableau de bord',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/check-in',
|
href: '/check-in',
|
||||||
label: 'Pointage Entrée',
|
label: 'Enregistrement Entrée',
|
||||||
icon: icon.mdiClockIn,
|
icon: icon.mdiClockIn,
|
||||||
permissions: 'CREATE_TIME_ENTRIES'
|
permissions: 'CREATE_TIME_ENTRIES'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
label: 'Données',
|
||||||
label: 'Collaborateurs',
|
icon: icon.mdiDatabase,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
menu: [
|
||||||
// @ts-ignore
|
{
|
||||||
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
href: '/users/users-list',
|
||||||
permissions: 'READ_USERS'
|
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: 'Configuration',
|
||||||
label: 'Présences',
|
icon: icon.mdiCog,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
menu: [
|
||||||
// @ts-ignore
|
{
|
||||||
icon: 'mdiClock' in icon ? icon['mdiClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
href: '/import_jobs/import_jobs-list',
|
||||||
permissions: 'READ_TIME_ENTRIES'
|
label: 'Imports CSV',
|
||||||
},
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
{
|
// @ts-ignore
|
||||||
href: '/roles/roles-list',
|
icon: 'mdiFileImport' in icon ? icon['mdiFileImport' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
label: 'Roles',
|
permissions: 'READ_IMPORT_JOBS'
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
},
|
||||||
// @ts-ignore
|
{
|
||||||
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
href: '/departments/departments-list',
|
||||||
permissions: 'READ_ROLES'
|
label: 'Départements',
|
||||||
},
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
{
|
// @ts-ignore
|
||||||
href: '/permissions/permissions-list',
|
icon: 'mdiOfficeBuilding' in icon ? icon['mdiOfficeBuilding' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
label: 'Permissions',
|
permissions: 'READ_DEPARTMENTS'
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
},
|
||||||
// @ts-ignore
|
{
|
||||||
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
href: '/roles/roles-list',
|
||||||
permissions: 'READ_PERMISSIONS'
|
label: 'Rôles',
|
||||||
},
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
{
|
// @ts-ignore
|
||||||
href: '/departments/departments-list',
|
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
||||||
label: 'Departments',
|
permissions: 'READ_ROLES'
|
||||||
// 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,
|
href: '/permissions/permissions-list',
|
||||||
permissions: 'READ_DEPARTMENTS'
|
label: 'Permissions',
|
||||||
},
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
{
|
// @ts-ignore
|
||||||
href: '/import_jobs/import_jobs-list',
|
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
||||||
label: 'Import jobs',
|
permissions: 'READ_PERMISSIONS'
|
||||||
// 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'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/profile',
|
href: '/profile',
|
||||||
label: 'Profile',
|
label: 'Mon Profil',
|
||||||
icon: icon.mdiAccountCircle,
|
icon: icon.mdiAccountCircle,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
href: '/api-docs',
|
|
||||||
target: '_blank',
|
|
||||||
label: 'Swagger API',
|
|
||||||
icon: icon.mdiFileCode,
|
|
||||||
permissions: 'READ_API_DOCS'
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export default menuAside
|
export default menuAside
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { mdiClockIn, mdiAccountSearch, mdiCalendarClock, mdiCardAccountDetailsOu
|
|||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import CardBox from '../components/CardBox'
|
import CardBox from '../components/CardBox'
|
||||||
import LayoutGuest from '../layouts/Guest'
|
import LayoutAuthenticated from '../layouts/Authenticated'
|
||||||
import SectionMain from '../components/SectionMain'
|
import SectionMain from '../components/SectionMain'
|
||||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
||||||
import { getPageTitle } from '../config'
|
import { getPageTitle } from '../config'
|
||||||
@ -64,7 +64,7 @@ const EmployeeDetails = () => {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-500">Nom & Prénom:</p>
|
<p className="text-gray-500">Nom & Prénom:</p>
|
||||||
<p className="font-semibold">{employeeData.firstName} {employeeData.lastName}</p>
|
<p className="font-semibold text-lg">{employeeData.firstName} {employeeData.lastName}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-500">Département:</p>
|
<p className="text-gray-500">Département:</p>
|
||||||
@ -75,7 +75,7 @@ const EmployeeDetails = () => {
|
|||||||
<p className="font-semibold">{employeeData.matriculePaie || '-'}</p>
|
<p className="font-semibold">{employeeData.matriculePaie || '-'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-500">Workday ID:</p>
|
<p className="text-gray-500">WD ID:</p>
|
||||||
<p className="font-semibold">{employeeData.workdayId || '-'}</p>
|
<p className="font-semibold">{employeeData.workdayId || '-'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -106,7 +106,7 @@ const EmployeeDetails = () => {
|
|||||||
const CheckIn = () => {
|
const CheckIn = () => {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const [isSuccess, setIsSuccess] = useState(false)
|
const [isSuccess, setIsSuccess] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
const handleSubmit = async (data, { resetForm }) => {
|
const handleSubmit = async (data, { resetForm }) => {
|
||||||
setIsSuccess(false)
|
setIsSuccess(false)
|
||||||
@ -116,6 +116,8 @@ const CheckIn = () => {
|
|||||||
if (create.fulfilled.match(resultAction)) {
|
if (create.fulfilled.match(resultAction)) {
|
||||||
setIsSuccess(true)
|
setIsSuccess(true)
|
||||||
resetForm()
|
resetForm()
|
||||||
|
// Keep the start_time current
|
||||||
|
// data.start_time = moment().format('YYYY-MM-DDTHH:mm')
|
||||||
} else {
|
} else {
|
||||||
setError("Une erreur est survenue lors de l'enregistrement.")
|
setError("Une erreur est survenue lors de l'enregistrement.")
|
||||||
}
|
}
|
||||||
@ -153,14 +155,14 @@ const CheckIn = () => {
|
|||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<FormField label="Collaborateur" labelFor="employee" icons={[mdiAccountSearch]}>
|
<FormField label="Rechercher un collaborateur (Nom)" labelFor="employee" icons={[mdiAccountSearch]}>
|
||||||
<Field
|
<Field
|
||||||
name="employee"
|
name="employee"
|
||||||
id="employee"
|
id="employee"
|
||||||
component={SelectField}
|
component={SelectField}
|
||||||
options={[]}
|
options={[]}
|
||||||
itemRef={'users'}
|
itemRef={'users'}
|
||||||
showField={'lastName'}
|
showField={'lastName'} // Not critical for autocomplete
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@ -168,13 +170,15 @@ const CheckIn = () => {
|
|||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
|
|
||||||
<FormField label="Heure d'entrée" icons={[mdiCalendarClock]}>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<Field type="datetime-local" name="start_time" placeholder="Heure d'entrée" />
|
<FormField label="Date et Heure d'entrée" icons={[mdiCalendarClock]}>
|
||||||
</FormField>
|
<Field type="datetime-local" name="start_time" placeholder="Heure d'entrée" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
<FormField label="Numéro de badge provisoire" icons={[mdiCardAccountDetailsOutline]}>
|
<FormField label="Numéro de badge provisoire" icons={[mdiCardAccountDetailsOutline]}>
|
||||||
<Field name="badge_number" placeholder="Ex: 12345" />
|
<Field name="badge_number" placeholder="Ex: 12345" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
|
|
||||||
@ -182,7 +186,7 @@ const CheckIn = () => {
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
type="submit"
|
type="submit"
|
||||||
color="info"
|
color="info"
|
||||||
label={isSubmitting ? "Enregistrement..." : "Enregistrer l'entrée"}
|
label={isSubmitting ? "Enregistrement..." : "Valider l'entrée"}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
@ -204,10 +208,10 @@ const CheckIn = () => {
|
|||||||
|
|
||||||
CheckIn.getLayout = function getLayout(page: ReactElement) {
|
CheckIn.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutGuest>
|
<LayoutAuthenticated>
|
||||||
{page}
|
{page}
|
||||||
</LayoutGuest>
|
</LayoutAuthenticated>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CheckIn
|
export default CheckIn
|
||||||
Loading…
x
Reference in New Issue
Block a user