diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index 33e9a6f..7186d54 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -84,6 +83,18 @@ module.exports = class UsersDBApi { || null , + + matriculePaie: data.data.matriculePaie || null, + workdayId: data.data.workdayId || null, + productionSite: data.data.productionSite || null, + remoteWork: data.data.remoteWork || null, + hiringDate: data.data.hiringDate || null, + positionEntryDate: data.data.positionEntryDate || null, + departureDate: data.data.departureDate || null, + service: data.data.service || null, + position: data.data.position || null, + team: data.data.team || null, + departmentId: data.data.department || data.data.departmentId || null, importHash: data.data.importHash || null, createdById: currentUser.id, @@ -204,6 +215,18 @@ module.exports = class UsersDBApi { || null , + + matriculePaie: item.matriculePaie || null, + workdayId: item.workdayId || null, + productionSite: item.productionSite || null, + remoteWork: item.remoteWork || null, + hiringDate: item.hiringDate || null, + positionEntryDate: item.positionEntryDate || null, + departureDate: item.departureDate || null, + service: item.service || null, + position: item.position || null, + team: item.team || null, + departmentId: item.department || item.departmentId || null, importHash: item.importHash || null, createdById: currentUser.id, @@ -297,6 +320,19 @@ module.exports = class UsersDBApi { if (data.provider !== undefined) updatePayload.provider = data.provider; + + if (data.matriculePaie !== undefined) updatePayload.matriculePaie = data.matriculePaie; + if (data.workdayId !== undefined) updatePayload.workdayId = data.workdayId; + if (data.productionSite !== undefined) updatePayload.productionSite = data.productionSite; + if (data.remoteWork !== undefined) updatePayload.remoteWork = data.remoteWork; + if (data.hiringDate !== undefined) updatePayload.hiringDate = data.hiringDate; + if (data.positionEntryDate !== undefined) updatePayload.positionEntryDate = data.positionEntryDate; + if (data.departureDate !== undefined) updatePayload.departureDate = data.departureDate; + if (data.service !== undefined) updatePayload.service = data.service; + if (data.position !== undefined) updatePayload.position = data.position; + if (data.team !== undefined) updatePayload.team = data.team; + if (data.department !== undefined) updatePayload.departmentId = data.department; + if (data.departmentId !== undefined) updatePayload.departmentId = data.departmentId; updatePayload.updatedById = currentUser.id; @@ -427,6 +463,10 @@ module.exports = class UsersDBApi { output.avatar = await users.getAvatar({ transaction }); + + output.department = await users.getDepartment({ + transaction + }); output.app_role = await users.getApp_role({ @@ -500,6 +540,11 @@ module.exports = class UsersDBApi { as: 'avatar', }, + { + model: db.departments, + as: 'department', + }, + ]; if (filter) { @@ -554,6 +599,28 @@ module.exports = class UsersDBApi { ), }; } + + if (filter.matriculePaie) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'matriculePaie', + filter.matriculePaie, + ), + }; + } + + if (filter.workdayId) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'workdayId', + filter.workdayId, + ), + }; + } if (filter.password) { where = { @@ -598,6 +665,13 @@ module.exports = class UsersDBApi { ), }; } + + if (filter.department) { + where = { + ...where, + departmentId: Utils.uuid(filter.department), + }; + } @@ -775,12 +849,17 @@ module.exports = class UsersDBApi { 'firstName', query, ), + Utils.ilike( + 'users', + 'lastName', + query, + ), ], }; } const records = await db.users.findAll({ - attributes: [ 'id', 'firstName' ], + attributes: [ 'id', 'firstName', 'lastName' ], where, limit: limit ? Number(limit) : undefined, offset: offset ? Number(offset) : undefined, @@ -789,7 +868,7 @@ module.exports = class UsersDBApi { return records.map((record) => ({ id: record.id, - label: record.firstName, + label: `${record.firstName} ${record.lastName}`, })); } @@ -943,5 +1022,4 @@ module.exports = class UsersDBApi { -}; - +}; \ No newline at end of file diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js index 07741b3..79cff95 100644 --- a/backend/src/db/db.config.js +++ b/backend/src/db/db.config.js @@ -1,5 +1,3 @@ - - module.exports = { production: { dialect: 'postgres', @@ -12,11 +10,12 @@ module.exports = { seederStorage: 'sequelize', }, development: { - username: 'postgres', + username: process.env.DB_USER || 'postgres', dialect: 'postgres', - password: '', - database: 'db_gestion_entr_es_sorties', + password: process.env.DB_PASS || '', + database: process.env.DB_NAME || 'db_gestion_entr_es_sorties', host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 5432, logging: console.log, seederStorage: 'sequelize', }, @@ -30,4 +29,4 @@ module.exports = { logging: console.log, seederStorage: 'sequelize', } -}; +}; \ No newline at end of file diff --git a/backend/src/db/migrations/20260203100000-public-permissions.js b/backend/src/db/migrations/20260203100000-public-permissions.js new file mode 100644 index 0000000..69c735d --- /dev/null +++ b/backend/src/db/migrations/20260203100000-public-permissions.js @@ -0,0 +1,37 @@ + +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.sequelize.query(` + INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId") + SELECT NOW(), NOW(), r.id, p.id + FROM roles r, permissions p + WHERE r.name = 'Public' + AND p.name IN ('READ_USERS', 'CREATE_TIME_ENTRIES') + ON CONFLICT ("roles_permissionsId", "permissionId") DO NOTHING; + `, { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.sequelize.query(` + DELETE FROM "rolesPermissionsPermissions" + WHERE "roles_permissionsId" IN (SELECT id FROM roles WHERE name = 'Public') + AND "permissionId" IN (SELECT id FROM permissions WHERE name IN ('READ_USERS', 'CREATE_TIME_ENTRIES')); + `, { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/20260203110000-add-user-fields.js b/backend/src/db/migrations/20260203110000-add-user-fields.js new file mode 100644 index 0000000..1fe09a8 --- /dev/null +++ b/backend/src/db/migrations/20260203110000-add-user-fields.js @@ -0,0 +1,60 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn('users', 'matriculePaie', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('users', 'workdayId', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('users', 'productionSite', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('users', 'remoteWork', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('users', 'hiringDate', { type: Sequelize.DataTypes.DATE }, { transaction }); + await queryInterface.addColumn('users', 'positionEntryDate', { type: Sequelize.DataTypes.DATE }, { transaction }); + await queryInterface.addColumn('users', 'departureDate', { type: Sequelize.DataTypes.DATE }, { transaction }); + await queryInterface.addColumn('users', 'service', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('users', 'position', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('users', 'team', { type: Sequelize.DataTypes.TEXT }, { transaction }); + await queryInterface.addColumn('users', 'departmentId', { + type: Sequelize.DataTypes.UUID, + references: { + model: 'departments', + key: 'id', + }, + allowNull: true, + }, { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('users', 'matriculePaie', { transaction }); + await queryInterface.removeColumn('users', 'workdayId', { transaction }); + await queryInterface.removeColumn('users', 'productionSite', { transaction }); + await queryInterface.removeColumn('users', 'remoteWork', { transaction }); + await queryInterface.removeColumn('users', 'hiringDate', { transaction }); + await queryInterface.removeColumn('users', 'positionEntryDate', { transaction }); + await queryInterface.removeColumn('users', 'departureDate', { transaction }); + await queryInterface.removeColumn('users', 'service', { transaction }); + await queryInterface.removeColumn('users', 'position', { transaction }); + await queryInterface.removeColumn('users', 'team', { transaction }); + await queryInterface.removeColumn('users', 'departmentId', { transaction }); + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index 77d37b7..99424f0 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -14,94 +14,96 @@ module.exports = function(sequelize, DataTypes) { primaryKey: true, }, -firstName: { + firstName: { type: DataTypes.TEXT, - - - }, -lastName: { + lastName: { type: DataTypes.TEXT, - - - }, -phoneNumber: { + phoneNumber: { type: DataTypes.TEXT, - - - }, -email: { + email: { type: DataTypes.TEXT, - - - }, -disabled: { + disabled: { type: DataTypes.BOOLEAN, - allowNull: false, defaultValue: false, - - - }, -password: { + password: { type: DataTypes.TEXT, - - - }, -emailVerified: { + emailVerified: { type: DataTypes.BOOLEAN, - allowNull: false, defaultValue: false, - - - }, -emailVerificationToken: { + emailVerificationToken: { type: DataTypes.TEXT, - - - }, -emailVerificationTokenExpiresAt: { + emailVerificationTokenExpiresAt: { type: DataTypes.DATE, - - - }, -passwordResetToken: { + passwordResetToken: { type: DataTypes.TEXT, - - - }, -passwordResetTokenExpiresAt: { + passwordResetTokenExpiresAt: { type: DataTypes.DATE, - - - }, -provider: { + provider: { type: DataTypes.TEXT, - - + }, + matriculePaie: { + type: DataTypes.TEXT, + }, + + workdayId: { + type: DataTypes.TEXT, + }, + + productionSite: { + type: DataTypes.TEXT, + }, + + remoteWork: { + type: DataTypes.TEXT, + }, + + hiringDate: { + type: DataTypes.DATE, + }, + + positionEntryDate: { + type: DataTypes.DATE, + }, + + departureDate: { + type: DataTypes.DATE, + }, + + service: { + type: DataTypes.TEXT, + }, + + position: { + type: DataTypes.TEXT, + }, + + team: { + type: DataTypes.TEXT, }, importHash: { @@ -137,13 +139,6 @@ provider: { through: 'usersCustom_permissionsPermissions', }); - -/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - - - - db.users.hasMany(db.departments, { as: 'departments_manager', foreignKey: { @@ -152,6 +147,13 @@ provider: { constraints: false, }); + db.users.belongsTo(db.departments, { + as: 'department', + foreignKey: { + name: 'departmentId', + }, + constraints: false, + }); db.users.hasMany(db.time_entries, { as: 'time_entries_employee', @@ -179,12 +181,6 @@ provider: { constraints: false, }); - - -//end loop - - - db.users.belongsTo(db.roles, { as: 'app_role', foreignKey: { @@ -193,8 +189,6 @@ provider: { constraints: false, }); - - db.users.hasMany(db.file, { as: 'avatar', foreignKey: 'belongsToId', @@ -247,7 +241,7 @@ provider: { function trimStringFields(users) { - users.email = users.email.trim(); + if (users.email) users.email = users.email.trim(); users.firstName = users.firstName ? users.firstName.trim() @@ -258,5 +252,4 @@ function trimStringFields(users) { : null; return users; -} - +} \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js index dcd6683..51d9d09 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,4 +1,3 @@ - const express = require('express'); const cors = require('cors'); const app = express(); @@ -93,7 +92,7 @@ app.use('/api/pexels', pexelsRoutes); app.enable('trust proxy'); -app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes); +app.use('/api/users', usersRoutes); app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoutes); @@ -101,7 +100,7 @@ app.use('/api/permissions', passport.authenticate('jwt', {session: false}), perm app.use('/api/departments', passport.authenticate('jwt', {session: false}), departmentsRoutes); -app.use('/api/time_entries', passport.authenticate('jwt', {session: false}), time_entriesRoutes); +app.use('/api/time_entries', time_entriesRoutes); app.use('/api/import_jobs', passport.authenticate('jwt', {session: false}), import_jobsRoutes); @@ -151,4 +150,4 @@ db.sequelize.sync().then(function () { }); }); -module.exports = app; +module.exports = app; \ No newline at end of file diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..4ced3eb 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, {useEffect, useRef, useState} from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' @@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) { } return
{NavBarItemComponentContents}
-} +} \ No newline at end of file diff --git a/frontend/src/components/Users/configureUsersCols.tsx b/frontend/src/components/Users/configureUsersCols.tsx index 37cebd3..f1bf28e 100644 --- a/frontend/src/components/Users/configureUsersCols.tsx +++ b/frontend/src/components/Users/configureUsersCols.tsx @@ -20,27 +20,22 @@ type Params = (id: string) => void; export const loadColumns = async ( onDelete: Params, entityName: string, - user - ) => { async function callOptionsApi(entityName: string) { - if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; - try { - const data = await axios(`/${entityName}/autocomplete?limit=100`); - return data.data; + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; } catch (error) { - console.log(error); - return []; + console.log(error); + return []; } } const hasUpdatePermission = hasPermission(user, 'UPDATE_USERS') return [ - { field: 'firstName', headerName: 'First Name', @@ -49,13 +44,8 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - }, - { field: 'lastName', headerName: 'Last Name', @@ -64,122 +54,96 @@ export const loadColumns = async ( filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - }, - - { - field: 'phoneNumber', - headerName: 'Phone Number', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - - }, - { field: 'email', headerName: 'E-Mail', flex: 1, + minWidth: 150, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + }, + { + field: 'matriculePaie', + headerName: 'Matricule Paie', + flex: 1, minWidth: 120, filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - }, - + { + field: 'workdayId', + headerName: 'WD ID', + flex: 1, + minWidth: 100, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + }, + { + field: 'department', + headerName: 'Département', + flex: 1, + minWidth: 150, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.name, + valueOptions: await callOptionsApi('departments'), + valueGetter: (params: GridValueGetterParams) => + params?.row?.department?.name || params?.row?.departmentId, + }, + { + field: 'service', + headerName: 'Service', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + }, + { + field: 'position', + headerName: 'Poste', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + }, + { + field: 'productionSite', + headerName: 'Site de production', + flex: 1, + minWidth: 150, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + }, { field: 'disabled', headerName: 'Disabled', flex: 1, - minWidth: 120, + minWidth: 100, filterable: false, headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - type: 'boolean', - }, - - { - field: 'avatar', - headerName: 'Avatar', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, - sortable: false, - renderCell: (params: GridValueGetterParams) => ( - - ), - - }, - - { - field: 'app_role', - headerName: 'App Role', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('roles'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - - }, - - { - field: 'custom_permissions', - headerName: 'Custom Permissions', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, - sortable: false, - type: 'singleSelect', - valueFormatter: ({ value }) => - dataFormatter.permissionsManyListFormatter(value).join(', '), - renderEditCell: (params) => ( - - ), - - }, - { field: 'actions', type: 'actions', @@ -187,7 +151,6 @@ export const loadColumns = async ( headerClassName: 'datagrid--header', cellClassName: 'datagrid--cell', getActions: (params: GridRowParams) => { - return [
, ] }, }, ]; -}; +}; \ No newline at end of file diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..26c3572 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' @@ -126,4 +125,4 @@ export default function LayoutAuthenticated({ ) -} +} \ No newline at end of file diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 5b1b6bc..1366712 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -7,15 +7,28 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiViewDashboardOutline, label: 'Dashboard', }, - + { + href: '/check-in', + label: 'Pointage Entrée', + icon: icon.mdiClockIn, + permissions: 'CREATE_TIME_ENTRIES' + }, { href: '/users/users-list', - label: '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: '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', @@ -40,14 +53,6 @@ const menuAside: MenuAsideItem[] = [ icon: 'mdiOfficeBuilding' in icon ? icon['mdiOfficeBuilding' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, permissions: 'READ_DEPARTMENTS' }, - { - href: '/time_entries/time_entries-list', - label: 'Time entries', - // 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: '/import_jobs/import_jobs-list', label: 'Import jobs', @@ -69,8 +74,6 @@ const menuAside: MenuAsideItem[] = [ label: 'Profile', icon: icon.mdiAccountCircle, }, - - { href: '/api-docs', target: '_blank', @@ -80,4 +83,4 @@ const menuAside: MenuAsideItem[] = [ }, ] -export default menuAside +export default menuAside \ No newline at end of file diff --git a/frontend/src/pages/check-in.tsx b/frontend/src/pages/check-in.tsx new file mode 100644 index 0000000..788527c --- /dev/null +++ b/frontend/src/pages/check-in.tsx @@ -0,0 +1,213 @@ +import { mdiClockIn, mdiAccountSearch, mdiCalendarClock, mdiCardAccountDetailsOutline, mdiCheckCircle, mdiCardAccountDetails } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, useState } from 'react' +import CardBox from '../components/CardBox' +import LayoutGuest from '../layouts/Guest' +import SectionMain from '../components/SectionMain' +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' +import { getPageTitle } from '../config' + +import { Field, Form, Formik, useFormikContext } from 'formik' +import FormField from '../components/FormField' +import BaseDivider from '../components/BaseDivider' +import BaseButtons from '../components/BaseButtons' +import BaseButton from '../components/BaseButton' +import { SelectField } from '../components/SelectField' +import NotificationBar from '../components/NotificationBar' +import BaseIcon from '../components/BaseIcon' + +import { create } from '../stores/time_entries/time_entriesSlice' +import { useAppDispatch } from '../stores/hooks' +import moment from 'moment' +import axios from 'axios' + +const initialValues = { + employee: '', + start_time: moment().format('YYYY-MM-DDTHH:mm'), + badge_number: '', + status: 'pending', +} + +const EmployeeDetails = () => { + const { values } = useFormikContext() + const [employeeData, setEmployeeData] = useState(null) + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (values.employee) { + setLoading(true) + axios.get(`/users/${values.employee}`) + .then(res => { + setEmployeeData(res.data) + }) + .catch(err => { + console.error(err) + setEmployeeData(null) + }) + .finally(() => { + setLoading(false) + }) + } else { + setEmployeeData(null) + } + }, [values.employee]) + + if (loading) return
Chargement des informations du collaborateur...
+ if (!employeeData) return null + + return ( +
+
+ + Informations Collaborateur +
+
+
+

Nom & Prénom:

+

{employeeData.firstName} {employeeData.lastName}

+
+
+

Département:

+

{employeeData.department?.name || 'Non défini'}

+
+
+

Matricule Paie:

+

{employeeData.matriculePaie || '-'}

+
+
+

Workday ID:

+

{employeeData.workdayId || '-'}

+
+
+

Site de production:

+

{employeeData.productionSite || '-'}

+
+
+

Télétravail:

+

{employeeData.remoteWork || '-'}

+
+
+

Service:

+

{employeeData.service || '-'}

+
+
+

Poste:

+

{employeeData.position || '-'}

+
+
+

Équipe (N+1):

+

{employeeData.team || '-'}

+
+
+
+ ) +} + +const CheckIn = () => { + const dispatch = useAppDispatch() + const [isSuccess, setIsSuccess] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = async (data, { resetForm }) => { + setIsSuccess(false) + setError(null) + try { + const resultAction = await dispatch(create(data)) + if (create.fulfilled.match(resultAction)) { + setIsSuccess(true) + resetForm() + } else { + setError("Une erreur est survenue lors de l'enregistrement.") + } + } catch (err) { + setError("Une erreur est survenue lors de l'enregistrement.") + } + } + + return ( + <> + + {getPageTitle('Enregistrement Entrée')} + + + + {''} + + + {isSuccess && ( + + L'entrée a été enregistrée avec succès. + + )} + + {error && ( + + {error} + + )} + + + + {({ isSubmitting }) => ( +
+ + + + + + + + + + + + + + + + + + + + + + + + )} +
+
+
+ + ) +} + +CheckIn.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default CheckIn diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index fbdfd2f..2571bf6 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,4 +1,3 @@ - import React, { useEffect, useState } from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; @@ -7,13 +6,12 @@ import BaseButton from '../components/BaseButton'; import CardBox from '../components/CardBox'; import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; import { useAppSelector } from '../stores/hooks'; import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; - +import { mdiClockIn, mdiLogin } from '@mdi/js'; export default function Starter() { const [illustrationImage, setIllustrationImage] = useState({ @@ -111,7 +109,7 @@ export default function Starter() { } > - {getPageTitle('Starter Page')} + {getPageTitle('Gestion Entrées Sorties')} @@ -128,22 +126,31 @@ export default function Starter() { : null}
- + -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

+
+

+ Suivez facilement les entrées et sorties de vos collaborateurs, + gérez les badges provisoires et consultez les rapports d'heures en temps réel. +

- + - +
@@ -163,4 +170,3 @@ export default function Starter() { Starter.getLayout = function getLayout(page: ReactElement) { return {page}; }; - diff --git a/frontend/src/pages/users/users-edit.tsx b/frontend/src/pages/users/users-edit.tsx index b0e9244..c48aadf 100644 --- a/frontend/src/pages/users/users-edit.tsx +++ b/frontend/src/pages/users/users-edit.tsx @@ -1,9 +1,7 @@ import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js' import Head from 'next/head' import React, { ReactElement, useEffect, useState } from 'react' -import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; -import dayjs from "dayjs"; import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' @@ -16,282 +14,69 @@ import FormField from '../../components/FormField' import BaseDivider from '../../components/BaseDivider' import BaseButtons from '../../components/BaseButtons' import BaseButton from '../../components/BaseButton' -import FormCheckRadio from '../../components/FormCheckRadio' -import FormCheckRadioGroup from '../../components/FormCheckRadioGroup' import FormFilePicker from '../../components/FormFilePicker' import FormImagePicker from '../../components/FormImagePicker' import { SelectField } from "../../components/SelectField"; import { SelectFieldMany } from "../../components/SelectFieldMany"; import { SwitchField } from '../../components/SwitchField' -import {RichTextField} from "../../components/RichTextField"; import { update, fetch } from '../../stores/users/usersSlice' import { useAppDispatch, useAppSelector } from '../../stores/hooks' import { useRouter } from 'next/router' -import {saveFile} from "../../helpers/fileSaver"; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from "../../components/ImageField"; - - const EditUsersPage = () => { const router = useRouter() const dispatch = useAppDispatch() const initVals = { - - - 'firstName': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'lastName': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'phoneNumber': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'email': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + firstName: '', + lastName: '', + phoneNumber: '', + email: '', disabled: false, - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - avatar: [], - - - - - - - - - - - - - - - - - - - - - - - - - - - - - app_role: null, - - - - - - - - - - - - - - - - - - - - - - - - - - - - - custom_permissions: [], - - - - password: '' - + password: '', + matriculePaie: '', + workdayId: '', + productionSite: '', + remoteWork: false, + hiringDate: '', + positionEntryDate: '', + departureDate: '', + service: '', + position: '', + team: '', + departmentId: '', } const [initialValues, setInitialValues] = useState(initVals) const { users } = useAppSelector((state) => state.users) - - const { id } = router.query useEffect(() => { - dispatch(fetch({ id: id })) + if (id) { + dispatch(fetch({ id: id })) + } }, [id]) useEffect(() => { - if (typeof users === 'object') { - setInitialValues(users) + if (typeof users === 'object' && users !== null) { + const newInitialVal = {...initVals}; + Object.keys(initVals).forEach(el => { + if (users[el] !== undefined) { + newInitialVal[el] = users[el] + } + }) + // Format dates for input type="date" + if (newInitialVal.hiringDate) newInitialVal.hiringDate = newInitialVal.hiringDate.split('T')[0]; + if (newInitialVal.positionEntryDate) newInitialVal.positionEntryDate = newInitialVal.positionEntryDate.split('T')[0]; + if (newInitialVal.departureDate) newInitialVal.departureDate = newInitialVal.departureDate.split('T')[0]; + + setInitialValues(newInitialVal); } }, [users]) - useEffect(() => { - if (typeof users === 'object') { - const newInitialVal = {...initVals}; - Object.keys(initVals).forEach(el => newInitialVal[el] = (users)[el]) - setInitialValues(newInitialVal); - } - }, [users]) - const handleSubmit = async (data) => { await dispatch(update({ id: id, data })) await router.push('/users/users-list') @@ -300,10 +85,10 @@ const EditUsersPage = () => { return ( <> - {getPageTitle('Edit users')} + {getPageTitle('Edit User')} - + {''} @@ -313,358 +98,120 @@ const EditUsersPage = () => { onSubmit={(values) => handleSubmit(values)} >
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + @@ -683,13 +230,11 @@ const EditUsersPage = () => { EditUsersPage.getLayout = function getLayout(page: ReactElement) { return ( {page} ) } -export default EditUsersPage +export default EditUsersPage \ No newline at end of file diff --git a/frontend/src/pages/users/users-new.tsx b/frontend/src/pages/users/users-new.tsx index 510a85b..187f19f 100644 --- a/frontend/src/pages/users/users-new.tsx +++ b/frontend/src/pages/users/users-new.tsx @@ -28,135 +28,25 @@ import { useRouter } from 'next/router' import moment from 'moment'; const initialValues = { - - firstName: '', - - - - - - - - - - - - - - - lastName: '', - - - - - - - - - - - - - - - phoneNumber: '', - - - - - - - - - - - - - - - email: '', - - - - - - - - - - - - - - - - - - - - - disabled: false, - - - - - - - - - - - - - - - - - - - avatar: [], - - - - - - - - - - - - - - - - app_role: '', - - - - - - - - - - - - - - - - custom_permissions: [], - - + matriculePaie: '', + workdayId: '', + productionSite: '', + remoteWork: false, + hiringDate: '', + positionEntryDate: '', + departureDate: '', + service: '', + position: '', + team: '', + departmentId: '', } @@ -164,9 +54,6 @@ const UsersNew = () => { const router = useRouter() const dispatch = useAppDispatch() - - - const handleSubmit = async (data) => { await dispatch(create(data)) await router.push('/users/users-list') @@ -174,220 +61,84 @@ const UsersNew = () => { return ( <> - {getPageTitle('New Item')} + {getPageTitle('New User')} - + {''} handleSubmit(values)} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{ > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { - - - - @@ -490,13 +187,11 @@ const UsersNew = () => { UsersNew.getLayout = function getLayout(page: ReactElement) { return ( {page} ) } -export default UsersNew +export default UsersNew \ No newline at end of file diff --git a/frontend/src/pages/users/users-view.tsx b/frontend/src/pages/users/users-view.tsx index a32e0b8..8cd11e6 100644 --- a/frontend/src/pages/users/users-view.tsx +++ b/frontend/src/pages/users/users-view.tsx @@ -1,12 +1,8 @@ import React, { ReactElement, useEffect } from 'react'; import Head from 'next/head' -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -import dayjs from "dayjs"; import {useAppDispatch, useAppSelector} from "../../stores/hooks"; import {useRouter} from "next/router"; import { fetch } from '../../stores/users/usersSlice' -import {saveFile} from "../../helpers/fileSaver"; import dataFormatter from '../../helpers/dataFormatter'; import ImageField from "../../components/ImageField"; import LayoutAuthenticated from "../../layouts/Authenticated"; @@ -25,27 +21,21 @@ const UsersView = () => { const router = useRouter() const dispatch = useAppDispatch() const { users } = useAppSelector((state) => state.users) - - const { id } = router.query; - function removeLastCharacter(str) { - console.log(str,`str`) - return str.slice(0, -1); - } - useEffect(() => { - dispatch(fetch({ id })); + if (id) { + dispatch(fetch({ id })); + } }, [dispatch, id]); - return ( <> - {getPageTitle('View users')} + {getPageTitle('View User')} - + { /> - - - +

First Name

{users?.firstName}

- - - - - - - - - - - - - - - - - - - - - - - - - - -

Last Name

{users?.lastName}

- - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Phone Number

-

{users?.phoneNumber}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -

E-Mail

{users?.email}

- - +
+

Phone Number

+

{users?.phoneNumber}

+
- +
+

Matricule Paie

+

{users?.matriculePaie}

+
- +
+

WD ID

+

{users?.workdayId}

+
- +
+

Site de production

+

{users?.productionSite}

+
- +
+

Service

+

{users?.service}

+
- +
+

Poste

+

{users?.position}

+
- +
+

Équipe (N+1)

+

{users?.team}

+
- +
+

Département

+

{users?.department?.name ?? 'No data'}

+
- +
+

Date d'embauche

+

{dataFormatter.dateFormatter(users?.hiringDate)}

+
- +
+

Date d'entrée poste

+

{dataFormatter.dateFormatter(users?.positionEntryDate)}

+
- +
+

Date de départ

+

{dataFormatter.dateFormatter(users?.departureDate)}

+
- - + + null}} + disabled + /> + - - - - - - - - - - - - - - - - - { disabled /> - +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Avatar

{users?.avatar?.length @@ -253,83 +143,13 @@ const UsersView = () => { ) :

No Avatar

}
- - - - - - - - - - - - - - - - - - - - - - - -

App Role

- - - - -

{users?.app_role?.name ?? 'No data'}

- - - - - - - - - - - - +

{users?.app_role?.name ?? 'No data'}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <> +

Custom Permissions

{ - - - - - - - - - - - - - - + {users.custom_permissions && Array.isArray(users.custom_permissions) && users.custom_permissions.map((item: any) => ( router.push(`/permissions/permissions-view/?id=${item.id}`)}> - - - - - - - - - - - - - ))} @@ -382,310 +176,7 @@ const UsersView = () => { {!users?.custom_permissions?.length &&
No data
} - - - - - - - - - - - - - <> -

Departments Manager

- -
-
NameName
{ item.name }
- - - - - - - - - - - - - - - - - - - - - {users.departments_manager && Array.isArray(users.departments_manager) && - users.departments_manager.map((item: any) => ( - router.push(`/departments/departments-view/?id=${item.id}`)}> - - - - - - - - - - - - - - - - - ))} - -
NomCodeLocalisation
- { item.name } - - { item.code } - - { item.location } -
-
- {!users?.departments_manager?.length &&
No data
} -
- - - - <> -

Time_entries Collaborateur

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {users.time_entries_employee && Array.isArray(users.time_entries_employee) && - users.time_entries_employee.map((item: any) => ( - router.push(`/time_entries/time_entries-view/?id=${item.id}`)}> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ))} - -
HeureentréeHeuresortieNumérobadgeprovisoireStatutRésuméNote
- { dataFormatter.dateTimeFormatter(item.start_time) } - - { dataFormatter.dateTimeFormatter(item.end_time) } - - { item.badge_number } - - { item.status } - - { item.summary } - - { item.note } -
-
- {!users?.time_entries_employee?.length &&
No data
} -
- - - - <> -

Import_jobs Importépar

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - {users.import_jobs_imported_by && Array.isArray(users.import_jobs_imported_by) && - users.import_jobs_imported_by.map((item: any) => ( - router.push(`/import_jobs/import_jobs-view/?id=${item.id}`)}> - - - - - - - - - - - - - - - - - - - - - - - ))} - -
NomfichierDateimportStatutimportLignestraitées
- { item.filename } - - { dataFormatter.dateTimeFormatter(item.imported_at) } - - { item.status } - - { item.rows_processed } -
-
- {!users?.import_jobs_imported_by?.length &&
No data
} -
- - - - <> -

Badges Affectéà

- -
- - - - - - - - - - - - - - - - - - - - - - {users.badges_assigned_to && Array.isArray(users.badges_assigned_to) && - users.badges_assigned_to.map((item: any) => ( - router.push(`/badges/badges-view/?id=${item.id}`)}> - - - - - - - - - - - - - - - - - ))} - -
NumérobadgeDateémissionRestitué
- { item.number } - - { dataFormatter.dateTimeFormatter(item.issued_at) } - - { dataFormatter.booleanFormatter(item.returned) } -
-
- {!users?.badges_assigned_to?.length &&
No data
} -
- - - +
@@ -703,9 +194,7 @@ const UsersView = () => { UsersView.getLayout = function getLayout(page: ReactElement) { return ( {page}