From 60d25175fb1330abacd1c19161dc7e279f2d354b Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 16 Feb 2026 22:50:16 +0000 Subject: [PATCH] Autosave: 20260216-225015 --- backend/src/db/api/pto_journal_entries.js | 44 +- backend/src/db/api/users.js | 69 +- backend/src/db/api/yearly_leave_summaries.js | 55 +- backend/src/db/migrations/1771278926211.js | 107 +++ backend/src/db/models/users.js | 104 ++- backend/src/services/email/index.js | 40 +- frontend/src/components/NavBarItem.tsx | 5 +- frontend/src/components/NavBarMenuList.tsx | 13 +- frontend/src/components/PTOStats.tsx | 60 ++ frontend/src/layouts/Authenticated.tsx | 56 +- frontend/src/menuNavBar.ts | 114 ++- frontend/src/pages/dashboard.tsx | 590 +++++--------- frontend/src/pages/employee-summary.tsx | 124 +++ frontend/src/pages/users/users-edit.tsx | 778 ++++--------------- frontend/src/pages/users/users-new.tsx | 536 +++---------- frontend/src/stores/users/usersSlice.ts | 11 +- 16 files changed, 1061 insertions(+), 1645 deletions(-) create mode 100644 backend/src/db/migrations/1771278926211.js create mode 100644 frontend/src/components/PTOStats.tsx create mode 100644 frontend/src/pages/employee-summary.tsx diff --git a/backend/src/db/api/pto_journal_entries.js b/backend/src/db/api/pto_journal_entries.js index bc29915..03ece1b 100644 --- a/backend/src/db/api/pto_journal_entries.js +++ b/backend/src/db/api/pto_journal_entries.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -321,18 +320,6 @@ module.exports = class Pto_journal_entriesDBApi { const output = pto_journal_entries.get({plain: true}); - - - - - - - - - - - - output.user = await pto_journal_entries.getUser({ transaction }); @@ -356,21 +343,34 @@ module.exports = class Pto_journal_entriesDBApi { filter, options ) { + const currentUser = (options && options.currentUser) || { id: null }; const limit = filter.limit || 0; let offset = 0; let where = {}; const currentPage = +filter.page; - - - - offset = currentPage * limit; - const orderBy = null; - const transaction = (options && options.transaction) || undefined; + // Role-based filtering + if (currentUser.app_role?.name === 'User') { + where.userId = currentUser.id; + } else if (currentUser.app_role?.name === 'Manager') { + const managedUsers = await db.users.findAll({ + where: { + [Op.or]: [ + { managerId: currentUser.id }, + { id: currentUser.id } + ] + }, + attributes: ['id'], + transaction + }); + const managedUserIds = managedUsers.map(u => u.id); + where.userId = { [Op.in]: managedUserIds }; + } + let include = [ { @@ -423,9 +423,6 @@ module.exports = class Pto_journal_entriesDBApi { } : {}, }, - - - ]; if (filter) { @@ -710,5 +707,4 @@ module.exports = class Pto_journal_entriesDBApi { } -}; - +}; \ No newline at end of file diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index d11dcbc..dd5aa59 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'); @@ -85,6 +84,16 @@ module.exports = class UsersDBApi { null , + work_hours_per_week: data.data.work_hours_per_week || null, + leave_policy_type: data.data.leave_policy_type || 'pto', + paid_pto_per_year: data.data.paid_pto_per_year || null, + medical_leave_per_year: data.data.medical_leave_per_year || null, + bereavement_per_year: data.data.bereavement_per_year || null, + vacation_pay_rate: data.data.vacation_pay_rate || null, + hiring_year: data.data.hiring_year || null, + position: data.data.position || null, + managerId: data.data.manager || null, + importHash: data.data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -116,6 +125,9 @@ module.exports = class UsersDBApi { transaction, }); + await users.setNotification_recipients(data.data.notification_recipients || [], { + transaction, + }); await FileDBApi.replaceRelationFiles( @@ -205,6 +217,16 @@ module.exports = class UsersDBApi { null , + work_hours_per_week: item.work_hours_per_week || null, + leave_policy_type: item.leave_policy_type || 'pto', + paid_pto_per_year: item.paid_pto_per_year || null, + medical_leave_per_year: item.medical_leave_per_year || null, + bereavement_per_year: item.bereavement_per_year || null, + vacation_pay_rate: item.vacation_pay_rate || null, + hiring_year: item.hiring_year || null, + position: item.position || null, + managerId: item.manager || null, + importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -298,7 +320,16 @@ module.exports = class UsersDBApi { if (data.provider !== undefined) updatePayload.provider = data.provider; - + if (data.work_hours_per_week !== undefined) updatePayload.work_hours_per_week = data.work_hours_per_week; + if (data.leave_policy_type !== undefined) updatePayload.leave_policy_type = data.leave_policy_type; + if (data.paid_pto_per_year !== undefined) updatePayload.paid_pto_per_year = data.paid_pto_per_year; + if (data.medical_leave_per_year !== undefined) updatePayload.medical_leave_per_year = data.medical_leave_per_year; + if (data.bereavement_per_year !== undefined) updatePayload.bereavement_per_year = data.bereavement_per_year; + if (data.vacation_pay_rate !== undefined) updatePayload.vacation_pay_rate = data.vacation_pay_rate; + if (data.hiring_year !== undefined) updatePayload.hiring_year = data.hiring_year; + if (data.position !== undefined) updatePayload.position = data.position; + if (data.manager !== undefined) updatePayload.managerId = data.manager; + updatePayload.updatedById = currentUser.id; await users.update(updatePayload, {transaction}); @@ -320,6 +351,10 @@ module.exports = class UsersDBApi { if (data.custom_permissions !== undefined) { await users.setCustom_permissions(data.custom_permissions, { transaction }); } + + if (data.notification_recipients !== undefined) { + await users.setNotification_recipients(data.notification_recipients, { transaction }); + } @@ -447,6 +482,9 @@ module.exports = class UsersDBApi { output.app_role = await users.getApp_role({ transaction }); + + output.manager = await users.getManager({ transaction }); + output.notification_recipients = await users.getNotification_recipients({ transaction }); if (output.app_role) { output.app_role_permissions = await output.app_role.getPermissions({ @@ -515,6 +553,11 @@ module.exports = class UsersDBApi { as: 'avatar', }, + { + model: db.users, + as: 'manager', + }, + ]; if (filter) { @@ -613,6 +656,17 @@ module.exports = class UsersDBApi { ), }; } + + if (filter.position) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'users', + 'position', + filter.position, + ), + }; + } @@ -795,7 +849,7 @@ module.exports = class UsersDBApi { } const records = await db.users.findAll({ - attributes: [ 'id', 'firstName' ], + attributes: [ 'id', 'firstName', 'lastName' ], where, limit: limit ? Number(limit) : undefined, offset: offset ? Number(offset) : undefined, @@ -804,7 +858,7 @@ module.exports = class UsersDBApi { return records.map((record) => ({ id: record.id, - label: record.firstName, + label: `${record.firstName} ${record.lastName}`, })); } @@ -936,6 +990,10 @@ module.exports = class UsersDBApi { }, ); + if (users && users.disabled) { + throw new Error('User is disabled'); + } + const token = crypto .randomBytes(20) .toString('hex'); @@ -958,5 +1016,4 @@ module.exports = class UsersDBApi { -}; - +}; \ No newline at end of file diff --git a/backend/src/db/api/yearly_leave_summaries.js b/backend/src/db/api/yearly_leave_summaries.js index 10985cb..23d96b5 100644 --- a/backend/src/db/api/yearly_leave_summaries.js +++ b/backend/src/db/api/yearly_leave_summaries.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -280,18 +279,6 @@ module.exports = class Yearly_leave_summariesDBApi { const output = yearly_leave_summaries.get({plain: true}); - - - - - - - - - - - - output.user = await yearly_leave_summaries.getUser({ transaction }); @@ -305,21 +292,34 @@ module.exports = class Yearly_leave_summariesDBApi { filter, options ) { + const currentUser = (options && options.currentUser) || { id: null }; const limit = filter.limit || 0; let offset = 0; let where = {}; const currentPage = +filter.page; - - - - offset = currentPage * limit; - const orderBy = null; - const transaction = (options && options.transaction) || undefined; + // Role-based filtering + if (currentUser.app_role?.name === 'User') { + where.userId = currentUser.id; + } else if (currentUser.app_role?.name === 'Manager') { + const managedUsers = await db.users.findAll({ + where: { + [Op.or]: [ + { managerId: currentUser.id }, + { id: currentUser.id } + ] + }, + attributes: ['id'], + transaction + }); + const managedUserIds = managedUsers.map(u => u.id); + where.userId = { [Op.in]: managedUserIds }; + } + let include = [ { @@ -352,9 +352,19 @@ module.exports = class Yearly_leave_summariesDBApi { } + if (filter.userId) { + where = { + ...where, + userId: filter.userId + }; + } - - + if (filter.calendar_year) { + where = { + ...where, + calendar_year: filter.calendar_year + }; + } if (filter.calendar_yearRange) { @@ -678,5 +688,4 @@ module.exports = class Yearly_leave_summariesDBApi { } -}; - +}; \ No newline at end of file diff --git a/backend/src/db/migrations/1771278926211.js b/backend/src/db/migrations/1771278926211.js new file mode 100644 index 0000000..b4dfa07 --- /dev/null +++ b/backend/src/db/migrations/1771278926211.js @@ -0,0 +1,107 @@ + +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn('users', 'work_hours_per_week', { + type: Sequelize.DataTypes.DECIMAL, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('users', 'leave_policy_type', { + type: Sequelize.DataTypes.ENUM('pto', 'paid_vacation_pay'), + defaultValue: 'pto', + allowNull: false, + }, { transaction }); + + await queryInterface.addColumn('users', 'paid_pto_per_year', { + type: Sequelize.DataTypes.DECIMAL, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('users', 'medical_leave_per_year', { + type: Sequelize.DataTypes.DECIMAL, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('users', 'bereavement_per_year', { + type: Sequelize.DataTypes.DECIMAL, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('users', 'vacation_pay_rate', { + type: Sequelize.DataTypes.DECIMAL, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('users', 'hiring_year', { + type: Sequelize.DataTypes.INTEGER, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('users', 'position', { + type: Sequelize.DataTypes.TEXT, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('users', 'managerId', { + type: Sequelize.DataTypes.UUID, + references: { + model: 'users', + key: 'id', + }, + allowNull: true, + }, { transaction }); + + // Create a junction table for notifications if needed, + // but for now let's just add the basic fields. + // The user mentioned "Notifications - multiselect of users who get notifications when away" + // This is likely a many-to-many relationship: User -> Notification Recipients (Users) + + await queryInterface.createTable('user_notification_recipients', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + userId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'users', key: 'id' }, + onDelete: 'CASCADE', + }, + recipientId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'users', key: 'id' }, + onDelete: 'CASCADE', + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + }, { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.dropTable('user_notification_recipients', { transaction }); + await queryInterface.removeColumn('users', 'managerId', { transaction }); + await queryInterface.removeColumn('users', 'position', { transaction }); + await queryInterface.removeColumn('users', 'hiring_year', { transaction }); + await queryInterface.removeColumn('users', 'vacation_pay_rate', { transaction }); + await queryInterface.removeColumn('users', 'bereavement_per_year', { transaction }); + await queryInterface.removeColumn('users', 'medical_leave_per_year', { transaction }); + await queryInterface.removeColumn('users', 'paid_pto_per_year', { transaction }); + await queryInterface.removeColumn('users', 'leave_policy_type', { transaction }); + await queryInterface.removeColumn('users', 'work_hours_per_week', { 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 9bd3880..7518a3a 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -16,92 +16,87 @@ module.exports = function(sequelize, DataTypes) { firstName: { type: DataTypes.TEXT, - - - }, lastName: { type: DataTypes.TEXT, - - - }, phoneNumber: { type: DataTypes.TEXT, - - - }, email: { type: DataTypes.TEXT, - - - }, disabled: { type: DataTypes.BOOLEAN, - allowNull: false, defaultValue: false, - - - }, password: { type: DataTypes.TEXT, - - - }, emailVerified: { type: DataTypes.BOOLEAN, - allowNull: false, defaultValue: false, - - - }, emailVerificationToken: { type: DataTypes.TEXT, - - - }, emailVerificationTokenExpiresAt: { type: DataTypes.DATE, - - - }, passwordResetToken: { type: DataTypes.TEXT, - - - }, passwordResetTokenExpiresAt: { type: DataTypes.DATE, - - - }, provider: { type: DataTypes.TEXT, - - + }, + work_hours_per_week: { + type: DataTypes.DECIMAL, + }, + + leave_policy_type: { + type: DataTypes.ENUM('pto', 'paid_vacation_pay'), + defaultValue: 'pto', + }, + + paid_pto_per_year: { + type: DataTypes.DECIMAL, + }, + + medical_leave_per_year: { + type: DataTypes.DECIMAL, + }, + + bereavement_per_year: { + type: DataTypes.DECIMAL, + }, + + vacation_pay_rate: { + type: DataTypes.DECIMAL, + }, + + hiring_year: { + type: DataTypes.INTEGER, + }, + + position: { + type: DataTypes.TEXT, }, importHash: { @@ -137,15 +132,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.time_off_requests, { as: 'time_off_requests_requester', foreignKey: { @@ -162,7 +148,6 @@ provider: { constraints: false, }); - db.users.hasMany(db.pto_journal_entries, { as: 'pto_journal_entries_user', foreignKey: { @@ -179,7 +164,6 @@ provider: { constraints: false, }); - db.users.hasMany(db.yearly_leave_summaries, { as: 'yearly_leave_summaries_user', foreignKey: { @@ -188,7 +172,6 @@ provider: { constraints: false, }); - db.users.hasMany(db.office_calendar_events, { as: 'office_calendar_events_user', foreignKey: { @@ -197,7 +180,6 @@ provider: { constraints: false, }); - db.users.hasMany(db.approval_tasks, { as: 'approval_tasks_assigned_manager', foreignKey: { @@ -206,12 +188,6 @@ provider: { constraints: false, }); - - -//end loop - - - db.users.belongsTo(db.roles, { as: 'app_role', foreignKey: { @@ -220,7 +196,19 @@ provider: { constraints: false, }); + db.users.belongsTo(db.users, { + as: 'manager', + foreignKey: 'managerId', + constraints: false, + }); + db.users.belongsToMany(db.users, { + as: 'notification_recipients', + through: 'user_notification_recipients', + foreignKey: 'userId', + otherKey: 'recipientId', + constraints: false, + }); db.users.hasMany(db.file, { as: 'avatar', @@ -232,7 +220,6 @@ provider: { }, }); - db.users.belongsTo(db.users, { as: 'createdBy', }); @@ -285,5 +272,4 @@ function trimStringFields(users) { : null; return users; -} - +} \ No newline at end of file diff --git a/backend/src/services/email/index.js b/backend/src/services/email/index.js index bc97a3d..7a6c174 100644 --- a/backend/src/services/email/index.js +++ b/backend/src/services/email/index.js @@ -1,6 +1,7 @@ const config = require('../../config'); const assert = require('assert'); const nodemailer = require('nodemailer'); +const db = require('../../db/models'); module.exports = class EmailSender { constructor(email) { @@ -13,6 +14,43 @@ module.exports = class EmailSender { assert(this.email.subject, 'email.subject is required'); assert(this.email.html, 'email.html is required'); + // Check if user is disabled + if (this.email.to) { + const recipients = Array.isArray(this.email.to) ? this.email.to : this.email.to.split(',').map(e => e.trim()); + + // If single recipient (common case), check and return if disabled + if (recipients.length === 1) { + const user = await db.users.findOne({ where: { email: recipients[0] } }); + if (user && user.disabled) { + console.log(`Email to ${recipients[0]} suppressed because user is disabled.`); + return; + } + } else { + // If multiple, strictly we should filter? + // But nodemailer takes the string/array in mailOptions. + // For safety/simplicity in this specific task "prevents notices", + // I will assume strictly 1-to-1 notices for things like "password reset". + // If there's a bulk email, we might want to filter, but that requires rewriting the `to` field. + // Given the requirement "mark inactive which prevents notices", filtering the list is safer. + + const enabledRecipients = []; + for (const email of recipients) { + const user = await db.users.findOne({ where: { email } }); + if (!user || !user.disabled) { + enabledRecipients.push(email); + } else { + console.log(`Email to ${email} suppressed because user is disabled.`); + } + } + + if (enabledRecipients.length === 0) { + return; + } + + this.email.to = enabledRecipients; + } + } + const htmlContent = await this.email.html(); const transporter = nodemailer.createTransport(this.transportConfig); @@ -41,4 +79,4 @@ module.exports = class EmailSender { get from() { return config.email.from; } -}; +}; \ 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/NavBarMenuList.tsx b/frontend/src/components/NavBarMenuList.tsx index 0896428..2fcf90b 100644 --- a/frontend/src/components/NavBarMenuList.tsx +++ b/frontend/src/components/NavBarMenuList.tsx @@ -1,19 +1,28 @@ import React from 'react' import { MenuNavBarItem } from '../interfaces' import NavBarItem from './NavBarItem' +import { useAppSelector } from '../stores/hooks' +import { hasPermission } from '../helpers/userPermissions' type Props = { menu: MenuNavBarItem[] } export default function NavBarMenuList({ menu }: Props) { + const { currentUser } = useAppSelector((state) => state.auth) + + const filteredMenu = menu.filter((item) => { + if (!item.permissions) return true + return hasPermission(currentUser, item.permissions) + }) + return ( <> - {menu.map((item, index) => ( + {filteredMenu.map((item, index) => (
))} ) -} +} \ No newline at end of file diff --git a/frontend/src/components/PTOStats.tsx b/frontend/src/components/PTOStats.tsx new file mode 100644 index 0000000..0ced6e0 --- /dev/null +++ b/frontend/src/components/PTOStats.tsx @@ -0,0 +1,60 @@ +import React from 'react' +import { mdiClockOutline, mdiCalendarCheck, mdiCalendarBlank, mdiMedicalBag } from '@mdi/js' +import CardBox from './CardBox' +import BaseIcon from './BaseIcon' + +type Props = { + summary: { + pto_pending_days: number | string + pto_scheduled_days: number | string + pto_available_days: number | string + medical_taken_days: number | string + } +} + +const PTOStats = ({ summary }: Props) => { + const stats = [ + { + label: 'Pending PTO', + value: summary?.pto_pending_days || 0, + icon: mdiClockOutline, + color: 'text-yellow-500', + }, + { + label: 'Scheduled PTO', + value: summary?.pto_scheduled_days || 0, + icon: mdiCalendarCheck, + color: 'text-blue-500', + }, + { + label: 'Available PTO', + value: summary?.pto_available_days || 0, + icon: mdiCalendarBlank, + color: 'text-green-500', + }, + { + label: 'Medical Leave Taken', + value: summary?.medical_taken_days || 0, + icon: mdiMedicalBag, + color: 'text-red-500', + }, + ] + + return ( +
+ {stats.map((stat, index) => ( + +
+
+

{stat.label}

+

{stat.value} Days

+
+ +
+
+ ))} +
+ ) +} + +export default PTOStats \ No newline at end of file diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..4fda5eb 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,13 +1,7 @@ import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' import jwt from 'jsonwebtoken'; -import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' -import menuAside from '../menuAside' import menuNavBar from '../menuNavBar' -import BaseIcon from '../components/BaseIcon' import NavBar from '../components/NavBar' -import NavBarItemPlain from '../components/NavBarItemPlain' -import AsideMenu from '../components/AsideMenu' import FooterBar from '../components/FooterBar' import { useAppDispatch, useAppSelector } from '../stores/hooks' import Search from '../components/Search'; @@ -15,6 +9,7 @@ import { useRouter } from 'next/router' import {findMe, logoutUser} from "../stores/authSlice"; import {hasPermission} from "../helpers/userPermissions"; +import NavBarItemPlain from '../components/NavBarItemPlain'; type Props = { @@ -67,61 +62,22 @@ export default function LayoutAuthenticated({ const darkMode = useAppSelector((state) => state.style.darkMode) - const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false) - const [isAsideLgActive, setIsAsideLgActive] = useState(false) - - useEffect(() => { - const handleRouteChangeStart = () => { - setIsAsideMobileExpanded(false) - setIsAsideLgActive(false) - } - - router.events.on('routeChangeStart', handleRouteChangeStart) - - // If the component is unmounted, unsubscribe - // from the event with the `off` method: - return () => { - router.events.off('routeChangeStart', handleRouteChangeStart) - } - }, [router.events, dispatch]) - - - const layoutAsidePadding = 'xl:pl-60' - return (
- setIsAsideMobileExpanded(!isAsideMobileExpanded)} - > - - - setIsAsideLgActive(true)} - > - - - setIsAsideLgActive(false)} - /> - {children} +
+ {children} +
Hand-crafted & Made with ❤️
diff --git a/frontend/src/menuNavBar.ts b/frontend/src/menuNavBar.ts index a5dd956..f2a59c8 100644 --- a/frontend/src/menuNavBar.ts +++ b/frontend/src/menuNavBar.ts @@ -1,19 +1,117 @@ import { - mdiMenu, - mdiClockOutline, - mdiCloud, - mdiCrop, mdiAccount, - mdiCogOutline, - mdiEmail, mdiLogout, mdiThemeLightDark, - mdiGithub, - mdiVuejs, + mdiViewDashboardOutline, + mdiAccountGroup, + mdiShieldAccountVariantOutline, + mdiShieldAccountOutline, + mdiCalendarStar, + mdiCalendarRange, + mdiClipboardTextClock, + mdiBookOpenPageVariant, + mdiChartBox, + mdiCalendarMonth, + mdiCheckDecagram, + mdiFileCode, + mdiAccountCircle, + mdiTable } from '@mdi/js' import { MenuNavBarItem } from './interfaces' const menuNavBar: MenuNavBarItem[] = [ + { + href: '/dashboard', + icon: mdiViewDashboardOutline, + label: 'Home', + }, + { + label: 'Management', + menu: [ + { + href: '/users/users-list', + label: 'Users', + icon: mdiAccountGroup, + permissions: 'READ_USERS' + }, + { + href: '/roles/roles-list', + label: 'Roles', + icon: mdiShieldAccountVariantOutline, + permissions: 'READ_ROLES' + }, + { + href: '/permissions/permissions-list', + label: 'Permissions', + icon: mdiShieldAccountOutline, + permissions: 'READ_PERMISSIONS' + }, + { + href: '/api-docs', + label: 'Swagger API', + icon: mdiFileCode, + permissions: 'READ_API_DOCS' + }, + ] + }, + { + label: 'PTO & Leaves', + menu: [ + { + href: '/employee-summary', + label: 'Employee Summary', + icon: mdiAccountGroup, + permissions: 'READ_YEARLY_LEAVE_SUMMARIES' + }, + { + href: '/time_off_requests/time_off_requests-list', + label: 'Time off requests', + icon: mdiClipboardTextClock, + permissions: 'READ_TIME_OFF_REQUESTS' + }, + { + href: '/pto_journal_entries/pto_journal_entries-list', + label: 'PTO Log', + icon: mdiBookOpenPageVariant, + permissions: 'READ_PTO_JOURNAL_ENTRIES' + }, + { + href: '/yearly_leave_summaries/yearly_leave_summaries-list', + label: 'Yearly Summaries', + icon: mdiChartBox, + permissions: 'READ_YEARLY_LEAVE_SUMMARIES' + }, + ] + }, + { + label: 'Calendar & Tasks', + menu: [ + { + href: '/office_calendar_events/office_calendar_events-list', + label: 'Office Calendar', + icon: mdiCalendarMonth, + permissions: 'READ_OFFICE_CALENDAR_EVENTS' + }, + { + href: '/holidays/holidays-list', + label: 'Holidays', + icon: mdiCalendarRange, + permissions: 'READ_HOLIDAYS' + }, + { + href: '/holiday_calendars/holiday_calendars-list', + label: 'Holiday Calendars', + icon: mdiCalendarStar, + permissions: 'READ_HOLIDAY_CALENDARS' + }, + { + href: '/approval_tasks/approval_tasks-list', + label: 'Approval Tasks', + icon: mdiCheckDecagram, + permissions: 'READ_APPROVAL_TASKS' + }, + ] + }, { isCurrentUser: true, menu: [ diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 950ae7e..8d23206 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -1,431 +1,215 @@ import * as icon from '@mdi/js'; import Head from 'next/head' -import React from 'react' +import React, { useState, useEffect } from 'react' import axios from 'axios'; import type { ReactElement } from 'react' import LayoutAuthenticated from '../layouts/Authenticated' import SectionMain from '../components/SectionMain' import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' -import BaseIcon from "../components/BaseIcon"; +import CardBox from '../components/CardBox' +import BaseButton from '../components/BaseButton' import { getPageTitle } from '../config' -import Link from "next/link"; +import { useAppSelector } from '../stores/hooks' +import PTOStats from '../components/PTOStats' +import moment from 'moment' +import Link from 'next/link' -import { hasPermission } from "../helpers/userPermissions"; -import { fetchWidgets } from '../stores/roles/rolesSlice'; -import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; -import { SmartWidget } from '../components/SmartWidget/SmartWidget'; - -import { useAppDispatch, useAppSelector } from '../stores/hooks'; const Dashboard = () => { - const dispatch = useAppDispatch(); - const iconsColor = useAppSelector((state) => state.style.iconsColor); - const corners = useAppSelector((state) => state.style.corners); - const cardsStyle = useAppSelector((state) => state.style.cardsStyle); + const { currentUser } = useAppSelector((state) => state.auth) + const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()) + const [summary, setSummary] = useState(null) + const [approvals, setApprovals] = useState([]) + const [upcomingTimeOff, setUpcomingTimeOff] = useState([]) + const [holidays, setHolidays] = useState([]) + const [loading, setLoading] = useState(true) - const loadingMessage = 'Loading...'; + const fetchDashboardData = async () => { + setLoading(true) + try { + // Fetch PTO Summary for selected year + const summaryRes = await axios.get(`/yearly_leave_summaries`, { + params: { + filter: JSON.stringify({ + userId: currentUser?.id, + calendar_year: selectedYear + }) + } + }) + setSummary(summaryRes.data.rows[0] || null) - - const [users, setUsers] = React.useState(loadingMessage); - const [roles, setRoles] = React.useState(loadingMessage); - const [permissions, setPermissions] = React.useState(loadingMessage); - const [holiday_calendars, setHoliday_calendars] = React.useState(loadingMessage); - const [holidays, setHolidays] = React.useState(loadingMessage); - const [time_off_requests, setTime_off_requests] = React.useState(loadingMessage); - const [pto_journal_entries, setPto_journal_entries] = React.useState(loadingMessage); - const [yearly_leave_summaries, setYearly_leave_summaries] = React.useState(loadingMessage); - const [office_calendar_events, setOffice_calendar_events] = React.useState(loadingMessage); - const [approval_tasks, setApproval_tasks] = React.useState(loadingMessage); + // Fetch Pending Approvals if manager/admin + const approvalsRes = await axios.get(`/approval_tasks`, { + params: { + filter: JSON.stringify({ + status: 'pending' + }) + } + }) + setApprovals(approvalsRes.data.rows) - - const [widgetsRole, setWidgetsRole] = React.useState({ - role: { value: '', label: '' }, - }); - const { currentUser } = useAppSelector((state) => state.auth); - const { isFetchingQuery } = useAppSelector((state) => state.openAi); - - const { rolesWidgets, loading } = useAppSelector((state) => state.roles); - - - async function loadData() { - const entities = ['users','roles','permissions','holiday_calendars','holidays','time_off_requests','pto_journal_entries','yearly_leave_summaries','office_calendar_events','approval_tasks',]; - const fns = [setUsers,setRoles,setPermissions,setHoliday_calendars,setHolidays,setTime_off_requests,setPto_journal_entries,setYearly_leave_summaries,setOffice_calendar_events,setApproval_tasks,]; + // Fetch Upcoming Time Off + const upcomingRes = await axios.get(`/office_calendar_events`, { + params: { + limit: 5, + offset: 0, + sort: 'start_date_ASC' + } + }) + setUpcomingTimeOff(upcomingRes.data.rows.filter(e => moment(e.start_date).isSameOrAfter(moment(), 'day'))) - const requests = entities.map((entity, index) => { - - if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { - return axios.get(`/${entity.toLowerCase()}/count`); - } else { - fns[index](null); - return Promise.resolve({data: {count: null}}); - } - - }); + // Fetch Holidays for selected year + const holidaysRes = await axios.get(`/holidays`, { + params: { + filter: JSON.stringify({ + calendar_year: selectedYear + }) + } + }) + setHolidays(holidaysRes.data.rows) - Promise.allSettled(requests).then((results) => { - results.forEach((result, i) => { - if (result.status === 'fulfilled') { - fns[i](result.value.data.count); - } else { - fns[i](result.reason.message); - } - }); - }); + } catch (error) { + console.error('Error fetching dashboard data:', error) + } finally { + setLoading(false) } - - async function getWidgets(roleId) { - await dispatch(fetchWidgets(roleId)); - } - React.useEffect(() => { - if (!currentUser) return; - loadData().then(); - setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } }); - }, [currentUser]); + } + + useEffect(() => { + if (currentUser) { + fetchDashboardData() + } + }, [currentUser, selectedYear]) + + const years = [selectedYear - 1, selectedYear, selectedYear + 1, selectedYear + 2] - React.useEffect(() => { - if (!currentUser || !widgetsRole?.role?.value) return; - getWidgets(widgetsRole?.role?.value || '').then(); - }, [widgetsRole?.role?.value]); - return ( <> - - {getPageTitle('Overview')} - + {getPageTitle('Home')} - - {''} + +
+ Year: + +
- - {hasPermission(currentUser, 'CREATE_ROLES') && } - {!!rolesWidgets.length && - hasPermission(currentUser, 'CREATE_ROLES') && ( -

- {`${widgetsRole?.role?.label || 'Users'}'s widgets`} -

- )} -
- {(isFetchingQuery || loading) && ( -
- {' '} - Loading widgets... -
- )} + {/* PTO Summary Stats */} + - { rolesWidgets && - rolesWidgets.map((widget) => ( - - ))} -
+
+ {/* Action Items (Approvals) */} + +
+

Action Items (Pending Approvals)

+ + View All + +
+
+ + + + + + + + + + + {approvals.length > 0 ? ( + approvals.map((task) => ( + + + + + + + )) + ) : ( + + + + )} + +
RequesterTypeDatesActions
{task.time_off_request?.requester?.firstName} {task.time_off_request?.requester?.lastName}{task.task_type?.replace(/_/g, ' ')} + {moment(task.time_off_request?.starts_at).format('MMM D')} - {moment(task.time_off_request?.ends_at).format('MMM D')} + + +
No pending approvals
+
+
- {!!rolesWidgets.length &&
} - -
- - - {hasPermission(currentUser, 'READ_USERS') && -
-
-
-
- Users -
-
- {users} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_ROLES') && -
-
-
-
- Roles -
-
- {roles} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_PERMISSIONS') && -
-
-
-
- Permissions -
-
- {permissions} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_HOLIDAY_CALENDARS') && -
-
-
-
- Holiday calendars -
-
- {holiday_calendars} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_HOLIDAYS') && -
-
-
-
- Holidays -
-
- {holidays} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_TIME_OFF_REQUESTS') && -
-
-
-
- Time off requests -
-
- {time_off_requests} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_PTO_JOURNAL_ENTRIES') && -
-
-
-
- Pto journal entries -
-
- {pto_journal_entries} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_YEARLY_LEAVE_SUMMARIES') && -
-
-
-
- Yearly leave summaries -
-
- {yearly_leave_summaries} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_OFFICE_CALENDAR_EVENTS') && -
-
-
-
- Office calendar events -
-
- {office_calendar_events} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_APPROVAL_TASKS') && -
-
-
-
- Approval tasks -
-
- {approval_tasks} -
-
-
- -
-
-
- } - - + {/* Upcoming Time Off & Holidays */} + +
+

Upcoming Time Off & Holidays

+
+
+ + + + + + + + + + {[...holidays, ...upcomingTimeOff] + .sort((a, b) => { + const dateA = a.holiday_date || a.start_date + const dateB = b.holiday_date || b.start_date + return moment(dateA).diff(moment(dateB)) + }) + .slice(0, 10) + .map((item, idx) => { + const isHoliday = !!item.holiday_date + return ( + + + + + + ) + })} + {holidays.length === 0 && upcomingTimeOff.length === 0 && ( + + + + )} + +
DateEvent / NameType
+ {moment(item.holiday_date || item.start_date).format('MMM D, YYYY')} + + {isHoliday ? item.name : `${item.user?.firstName} ${item.user?.lastName}`} + + + {isHoliday ? 'Holiday' : 'PTO'} + +
No upcoming events
+
+
@@ -436,4 +220,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) { return {page} } -export default Dashboard +export default Dashboard \ No newline at end of file diff --git a/frontend/src/pages/employee-summary.tsx b/frontend/src/pages/employee-summary.tsx new file mode 100644 index 0000000..d8408cb --- /dev/null +++ b/frontend/src/pages/employee-summary.tsx @@ -0,0 +1,124 @@ +import { mdiAccountGroup, mdiHome } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useState, useEffect } from 'react' +import axios from 'axios' +import CardBox from '../components/CardBox' +import LayoutAuthenticated from '../layouts/Authenticated' +import SectionMain from '../components/SectionMain' +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' +import { getPageTitle } from '../config' +import { useAppSelector } from '../stores/hooks' +import LoadingSpinner from '../components/LoadingSpinner' + +const EmployeeSummary = () => { + const { currentUser } = useAppSelector((state) => state.auth) + const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()) + const [summaries, setSummaries] = useState([]) + const [loading, setLoading] = useState(true) + + const fetchSummaries = async () => { + setLoading(true) + try { + // For now fetching all summaries for the year. + // In a real app we might filter by manager if not admin. + const res = await axios.get('/yearly_leave_summaries', { + params: { + limit: 100, + filter: JSON.stringify({ + calendar_year: selectedYear + }) + } + }) + setSummaries(res.data.rows) + } catch (error) { + console.error('Error fetching summaries:', error) + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchSummaries() + }, [selectedYear]) + + const years = [selectedYear - 1, selectedYear, selectedYear + 1, selectedYear + 2] + + return ( + <> + + {getPageTitle('Employee Summary')} + + + +
+ Year: + +
+
+ + + {loading ? ( +
+ +
+ ) : ( +
+ + + + + + + + + + + + + {summaries.length > 0 ? ( + summaries.map((summary) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
Employee NamePendingScheduledPTO TakenAvailableMedical Taken
+ {summary.user?.firstName} {summary.user?.lastName} + {summary.pto_pending_days || 0}{summary.pto_scheduled_days || 0}{summary.pto_taken_days || 0} + {summary.pto_available_days || 0} + + {summary.medical_taken_days || 0} +
+ No summaries found for {selectedYear} +
+
+ )} +
+
+ + ) +} + +EmployeeSummary.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default EmployeeSummary diff --git a/frontend/src/pages/users/users-edit.tsx b/frontend/src/pages/users/users-edit.tsx index da18a30..23d8760 100644 --- a/frontend/src/pages/users/users-edit.tsx +++ b/frontend/src/pages/users/users-edit.tsx @@ -1,10 +1,6 @@ -import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js' +import { mdiAccount, mdiChartTimelineVariant, mdiMail, 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' import SectionMain from '../../components/SectionMain' @@ -16,280 +12,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 initVals = { + firstName: '', + lastName: '', + phoneNumber: '', + email: '', + disabled: false, + avatar: [], + app_role: null, + custom_permissions: [], + password: '', + work_hours_per_week: '', + leave_policy_type: '', + paid_pto_per_year: '', + medical_leave_per_year: '', + bereavement_per_year: '', + vacation_pay_rate: '', + hiring_year: '', + position: '', + manager: null, + notification_recipients: [] +} +const emptyOptions = []; const EditUsersPage = () => { const router = useRouter() const dispatch = useAppDispatch() - const initVals = { - - - 'firstName': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'lastName': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'phoneNumber': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'email': '', - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - disabled: false, - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - avatar: [], - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - app_role: null, - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - custom_permissions: [], - - - - password: '' - - } - const [initialValues, setInitialValues] = useState(initVals) - - const { users } = useAppSelector((state) => state.users) - + const [initialValues, setInitialValues] = useState(initVals) + const { users } = useAppSelector((state) => state.users) const { id } = router.query useEffect(() => { - dispatch(fetch({ id: id })) - }, [id]) - - useEffect(() => { - if (typeof users === 'object') { - setInitialValues(users) + if (id) { + dispatch(fetch({ id: id })) } - }, [users]) + }, [id, dispatch]) useEffect(() => { - if (typeof users === 'object') { - const newInitialVal = {...initVals}; - Object.keys(initVals).forEach(el => newInitialVal[el] = (users)[el]) - setInitialValues(newInitialVal); - } + if (users && typeof users === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach(el => { + if (users[el] !== undefined) { + if (el === 'app_role' || el === 'manager') { + newInitialVal[el] = users[el]?.id || users[el]; + } else if (el === 'custom_permissions' || el === 'notification_recipients') { + newInitialVal[el] = users[el]?.map(item => item.id || item) || []; + } else { + newInitialVal[el] = users[el]; + } + } + }) + setInitialValues(newInitialVal); + } }, [users]) const handleSubmit = async (data) => { @@ -300,11 +85,11 @@ const EditUsersPage = () => { return ( <> - {getPageTitle('Edit users')} + {getPageTitle('Edit User')} - - {''} + + {''} { initialValues={initialValues} onSubmit={(values) => handleSubmit(values)} > + {({ values }) => (
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {values.leave_policy_type === 'pto' && ( + + + + )} + + + + + + + + + + {values.leave_policy_type === 'paid_vacation_pay' && ( + + + + )} + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + - router.push('/users/users-list')}/> + router.push('/users/users-list')} /> + )}
@@ -694,14 +208,10 @@ const EditUsersPage = () => { EditUsersPage.getLayout = function getLayout(page: ReactElement) { return ( - - {page} - + + {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..8d87f3e 100644 --- a/frontend/src/pages/users/users-new.tsx +++ b/frontend/src/pages/users/users-new.tsx @@ -1,4 +1,4 @@ -import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js' +import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload, mdiClockOutline, mdiAccountStar, mdiBriefcase, mdiCalendar } from '@mdi/js' import Head from 'next/head' import React, { ReactElement } from 'react' import CardBox from '../../components/CardBox' @@ -12,474 +12,156 @@ 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 { SwitchField } from '../../components/SwitchField' import { SelectField } from '../../components/SelectField' import { SelectFieldMany } from "../../components/SelectFieldMany"; -import {RichTextField} from "../../components/RichTextField"; import { create } from '../../stores/users/usersSlice' import { useAppDispatch } from '../../stores/hooks' import { useRouter } from 'next/router' -import moment from 'moment'; const initialValues = { - - firstName: '', - - - - - - - - - - - - - - - lastName: '', - - - - - - - - - - - - - - - phoneNumber: '', - - - - - - - - - - - - - - - email: '', - - - - - - - - - - - - - - - - - - - - - disabled: false, - - - - - - - - - - - - - - - - - - - avatar: [], - - - - - - - - - - - - - - - - app_role: '', - - - - - - - - - - - - - - - - custom_permissions: [], - - + work_hours_per_week: 40, + leave_policy_type: 'pto', + paid_pto_per_year: 15, + medical_leave_per_year: 10, + bereavement_per_year: 3, + vacation_pay_rate: 0, + hiring_year: new Date().getFullYear(), + position: '', + manager: '', + notification_recipients: [] } +const emptyOptions = []; const UsersNew = () => { const router = useRouter() const dispatch = useAppDispatch() - - - const handleSubmit = async (data) => { await dispatch(create(data)) await router.push('/users/users-list') } + return ( <> - {getPageTitle('New Item')} + {getPageTitle('New User')} - - {''} + + {''} handleSubmit(values)} > + {({ values }) => (
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {values.leave_policy_type === 'pto' && ( + + + + )} + + + + + + + + + + {values.leave_policy_type === 'paid_vacation_pay' && ( + + + + )} + + + + + + + + +
+ + + + + + + + + + + + - router.push('/users/users-list')}/> + router.push('/users/users-list')} /> + )}
@@ -489,14 +171,10 @@ const UsersNew = () => { UsersNew.getLayout = function getLayout(page: ReactElement) { return ( - - {page} - + + {page} + ) } -export default UsersNew +export default UsersNew \ No newline at end of file diff --git a/frontend/src/stores/users/usersSlice.ts b/frontend/src/stores/users/usersSlice.ts index 96013aa..6a568b4 100644 --- a/frontend/src/stores/users/usersSlice.ts +++ b/frontend/src/stores/users/usersSlice.ts @@ -158,6 +158,7 @@ export const usersSlice = createSlice({ builder.addCase(deleteItemsByIds.fulfilled, (state) => { state.loading = false; + state.refetch = true; fulfilledNotify(state, 'Users has been deleted'); }); @@ -173,13 +174,14 @@ export const usersSlice = createSlice({ builder.addCase(deleteItem.fulfilled, (state) => { state.loading = false + state.refetch = true; fulfilledNotify(state, `${'Users'.slice(0, -1)} has been deleted`); }) builder.addCase(deleteItem.rejected, (state, action) => { - state.loading = false + state.loading = false; rejectNotify(state, action); - }) + }); builder.addCase(create.pending, (state) => { state.loading = true @@ -192,6 +194,7 @@ export const usersSlice = createSlice({ builder.addCase(create.fulfilled, (state) => { state.loading = false + // state.refetch = true; // Removed to fix infinite loop fulfilledNotify(state, `${'Users'.slice(0, -1)} has been created`); }) @@ -201,6 +204,7 @@ export const usersSlice = createSlice({ }) builder.addCase(update.fulfilled, (state) => { state.loading = false + // state.refetch = true; // Removed to fix infinite loop fulfilledNotify(state, `${'Users'.slice(0, -1)} has been updated`); }) builder.addCase(update.rejected, (state, action) => { @@ -214,6 +218,7 @@ export const usersSlice = createSlice({ }) builder.addCase(uploadCsv.fulfilled, (state) => { state.loading = false; + state.refetch = true; fulfilledNotify(state, 'Users has been uploaded'); }) builder.addCase(uploadCsv.rejected, (state, action) => { @@ -228,4 +233,4 @@ export const usersSlice = createSlice({ // Action creators are generated for each case reducer function export const { setRefetch } = usersSlice.actions -export default usersSlice.reducer +export default usersSlice.reducer \ No newline at end of file