Autosave: 20260216-225015
This commit is contained in:
parent
4b6bdc53b6
commit
60d25175fb
@ -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 {
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
@ -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 {
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
@ -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 {
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
107
backend/src/db/migrations/1771278926211.js
Normal file
107
backend/src/db/migrations/1771278926211.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -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 <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
|
||||
}
|
||||
}
|
||||
@ -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) => (
|
||||
<div key={index}>
|
||||
<NavBarItem item={item} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
60
frontend/src/components/PTOStats.tsx
Normal file
60
frontend/src/components/PTOStats.tsx
Normal file
@ -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 (
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6">
|
||||
{stats.map((stat, index) => (
|
||||
<CardBox key={index}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-500 dark:text-slate-400 text-sm">{stat.label}</p>
|
||||
<p className="text-2xl font-bold">{stat.value} Days</p>
|
||||
</div>
|
||||
<BaseIcon path={stat.icon} size={48} className={stat.color} />
|
||||
</div>
|
||||
</CardBox>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PTOStats
|
||||
@ -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 (
|
||||
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
||||
<div
|
||||
className={`${layoutAsidePadding} ${
|
||||
isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
|
||||
} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
|
||||
className={`pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
|
||||
>
|
||||
<NavBar
|
||||
menu={menuNavBar}
|
||||
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`}
|
||||
className={``}
|
||||
>
|
||||
<NavBarItemPlain
|
||||
display="flex lg:hidden"
|
||||
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
|
||||
>
|
||||
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
|
||||
</NavBarItemPlain>
|
||||
<NavBarItemPlain
|
||||
display="hidden lg:flex xl:hidden"
|
||||
onClick={() => setIsAsideLgActive(true)}
|
||||
>
|
||||
<BaseIcon path={mdiMenu} size="24" />
|
||||
</NavBarItemPlain>
|
||||
<NavBarItemPlain useMargin>
|
||||
<Search />
|
||||
</NavBarItemPlain>
|
||||
</NavBar>
|
||||
<AsideMenu
|
||||
isAsideMobileExpanded={isAsideMobileExpanded}
|
||||
isAsideLgActive={isAsideLgActive}
|
||||
menu={menuAside}
|
||||
onAsideLgClose={() => setIsAsideLgActive(false)}
|
||||
/>
|
||||
{children}
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{children}
|
||||
</div>
|
||||
<FooterBar>Hand-crafted & Made with ❤️</FooterBar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{getPageTitle('Overview')}
|
||||
</title>
|
||||
<title>{getPageTitle('Home')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={icon.mdiChartTimelineVariant}
|
||||
title='Overview'
|
||||
main>
|
||||
{''}
|
||||
<SectionTitleLineWithButton icon={icon.mdiHome} title="Home" main>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-500">Year:</span>
|
||||
<select
|
||||
value={selectedYear}
|
||||
onChange={(e) => setSelectedYear(parseInt(e.target.value))}
|
||||
className="px-2 py-1 border rounded dark:bg-dark-800 dark:border-dark-700"
|
||||
>
|
||||
{years.map(y => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
||||
currentUser={currentUser}
|
||||
isFetchingQuery={isFetchingQuery}
|
||||
setWidgetsRole={setWidgetsRole}
|
||||
widgetsRole={widgetsRole}
|
||||
/>}
|
||||
{!!rolesWidgets.length &&
|
||||
hasPermission(currentUser, 'CREATE_ROLES') && (
|
||||
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
|
||||
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
|
||||
{(isFetchingQuery || loading) && (
|
||||
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
|
||||
<BaseIcon
|
||||
className={`${iconsColor} animate-spin mr-5`}
|
||||
w='w-16'
|
||||
h='h-16'
|
||||
size={48}
|
||||
path={icon.mdiLoading}
|
||||
/>{' '}
|
||||
Loading widgets...
|
||||
</div>
|
||||
)}
|
||||
{/* PTO Summary Stats */}
|
||||
<PTOStats
|
||||
summary={summary || {
|
||||
pto_pending_days: 0,
|
||||
pto_scheduled_days: 0,
|
||||
pto_available_days: 0,
|
||||
medical_taken_days: 0
|
||||
}}
|
||||
/>
|
||||
|
||||
{ rolesWidgets &&
|
||||
rolesWidgets.map((widget) => (
|
||||
<SmartWidget
|
||||
key={widget.id}
|
||||
userId={currentUser?.id}
|
||||
widget={widget}
|
||||
roleId={widgetsRole?.role?.value || ''}
|
||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
{/* Action Items (Approvals) */}
|
||||
<CardBox className="flex-1" hasTable>
|
||||
<div className="p-4 border-b dark:border-dark-700 flex justify-between items-center">
|
||||
<h3 className="font-bold">Action Items (Pending Approvals)</h3>
|
||||
<Link href="/approval_tasks/approval_tasks-list" className="text-sm text-blue-500 hover:underline">
|
||||
View All
|
||||
</Link>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-dark-700">
|
||||
<th className="p-4">Requester</th>
|
||||
<th className="p-4">Type</th>
|
||||
<th className="p-4">Dates</th>
|
||||
<th className="p-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{approvals.length > 0 ? (
|
||||
approvals.map((task) => (
|
||||
<tr key={task.id} className="border-b dark:border-dark-700">
|
||||
<td className="p-4">{task.time_off_request?.requester?.firstName} {task.time_off_request?.requester?.lastName}</td>
|
||||
<td className="p-4 capitalize">{task.task_type?.replace(/_/g, ' ')}</td>
|
||||
<td className="p-4">
|
||||
{moment(task.time_off_request?.starts_at).format('MMM D')} - {moment(task.time_off_request?.ends_at).format('MMM D')}
|
||||
</td>
|
||||
<td className="p-4 whitespace-nowrap">
|
||||
<BaseButton
|
||||
color="info"
|
||||
label="Review"
|
||||
small
|
||||
href={`/approval_tasks/approval_tasks-edit?id=${task.id}`}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="p-4 text-center text-gray-500">No pending approvals</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
||||
|
||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
||||
|
||||
|
||||
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Users
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{users}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiAccountGroup || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Roles
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{roles}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Permissions
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{permissions}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={icon.mdiShieldAccountOutline || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_HOLIDAY_CALENDARS') && <Link href={'/holiday_calendars/holiday_calendars-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Holiday calendars
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{holiday_calendars}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiCalendarStar' in icon ? icon['mdiCalendarStar' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_HOLIDAYS') && <Link href={'/holidays/holidays-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Holidays
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{holidays}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiCalendarHoliday' in icon ? icon['mdiCalendarHoliday' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_TIME_OFF_REQUESTS') && <Link href={'/time_off_requests/time_off_requests-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Time off requests
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{time_off_requests}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiClipboardTextClock' in icon ? icon['mdiClipboardTextClock' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_PTO_JOURNAL_ENTRIES') && <Link href={'/pto_journal_entries/pto_journal_entries-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Pto journal entries
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{pto_journal_entries}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiBookOpenPageVariant' in icon ? icon['mdiBookOpenPageVariant' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_YEARLY_LEAVE_SUMMARIES') && <Link href={'/yearly_leave_summaries/yearly_leave_summaries-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Yearly leave summaries
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{yearly_leave_summaries}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiChartBox' in icon ? icon['mdiChartBox' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_OFFICE_CALENDAR_EVENTS') && <Link href={'/office_calendar_events/office_calendar_events-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Office calendar events
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{office_calendar_events}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiCalendarMonth' in icon ? icon['mdiCalendarMonth' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
{hasPermission(currentUser, 'READ_APPROVAL_TASKS') && <Link href={'/approval_tasks/approval_tasks-list'}>
|
||||
<div
|
||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||
>
|
||||
<div className="flex justify-between align-center">
|
||||
<div>
|
||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||
Approval tasks
|
||||
</div>
|
||||
<div className="text-3xl leading-tight font-semibold">
|
||||
{approval_tasks}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseIcon
|
||||
className={`${iconsColor}`}
|
||||
w="w-16"
|
||||
h="h-16"
|
||||
size={48}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
path={'mdiCheckDecagram' in icon ? icon['mdiCheckDecagram' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>}
|
||||
|
||||
|
||||
{/* Upcoming Time Off & Holidays */}
|
||||
<CardBox className="flex-1" hasTable>
|
||||
<div className="p-4 border-b dark:border-dark-700">
|
||||
<h3 className="font-bold">Upcoming Time Off & Holidays</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-dark-700">
|
||||
<th className="p-4">Date</th>
|
||||
<th className="p-4">Event / Name</th>
|
||||
<th className="p-4">Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...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 (
|
||||
<tr key={idx} className="border-b dark:border-dark-700">
|
||||
<td className="p-4">
|
||||
{moment(item.holiday_date || item.start_date).format('MMM D, YYYY')}
|
||||
</td>
|
||||
<td className="p-4">
|
||||
{isHoliday ? item.name : `${item.user?.firstName} ${item.user?.lastName}`}
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className={`px-2 py-1 rounded-full text-xs ${isHoliday ? 'bg-purple-100 text-purple-700' : 'bg-blue-100 text-blue-700'}`}>
|
||||
{isHoliday ? 'Holiday' : 'PTO'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{holidays.length === 0 && upcomingTimeOff.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="p-4 text-center text-gray-500">No upcoming events</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
</SectionMain>
|
||||
</>
|
||||
@ -436,4 +220,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
export default Dashboard
|
||||
124
frontend/src/pages/employee-summary.tsx
Normal file
124
frontend/src/pages/employee-summary.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Employee Summary')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiAccountGroup} title="Employee Summary" main>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-500">Year:</span>
|
||||
<select
|
||||
value={selectedYear}
|
||||
onChange={(e) => setSelectedYear(parseInt(e.target.value))}
|
||||
className="px-2 py-1 border rounded dark:bg-dark-800 dark:border-dark-700"
|
||||
>
|
||||
{years.map(y => (
|
||||
<option key={y} value={y}>{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<CardBox hasTable>
|
||||
{loading ? (
|
||||
<div className="p-10 flex justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-dark-700 bg-gray-50 dark:bg-dark-900">
|
||||
<th className="p-4">Employee Name</th>
|
||||
<th className="p-4">Pending</th>
|
||||
<th className="p-4">Scheduled</th>
|
||||
<th className="p-4">PTO Taken</th>
|
||||
<th className="p-4">Available</th>
|
||||
<th className="p-4">Medical Taken</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{summaries.length > 0 ? (
|
||||
summaries.map((summary) => (
|
||||
<tr key={summary.id} className="border-b dark:border-dark-700 hover:bg-gray-50 dark:hover:bg-dark-800 transition-colors">
|
||||
<td className="p-4 font-medium">
|
||||
{summary.user?.firstName} {summary.user?.lastName}
|
||||
</td>
|
||||
<td className="p-4">{summary.pto_pending_days || 0}</td>
|
||||
<td className="p-4">{summary.pto_scheduled_days || 0}</td>
|
||||
<td className="p-4">{summary.pto_taken_days || 0}</td>
|
||||
<td className="p-4 font-bold text-green-600 dark:text-green-400">
|
||||
{summary.pto_available_days || 0}
|
||||
</td>
|
||||
<td className="p-4 text-red-600 dark:text-red-400">
|
||||
{summary.medical_taken_days || 0}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={6} className="p-8 text-center text-gray-500">
|
||||
No summaries found for {selectedYear}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
EmployeeSummary.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default EmployeeSummary
|
||||
@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Edit users')}</title>
|
||||
<title>{getPageTitle('Edit User')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit users'} main>
|
||||
{''}
|
||||
<SectionTitleLineWithButton icon={mdiAccount} title={'Edit User'} main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
@ -312,379 +97,108 @@ const EditUsersPage = () => {
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
{({ values }) => (
|
||||
<Form>
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="First Name"
|
||||
>
|
||||
<Field
|
||||
name="firstName"
|
||||
placeholder="First Name"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Last Name"
|
||||
>
|
||||
<Field
|
||||
name="lastName"
|
||||
placeholder="Last Name"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Phone Number"
|
||||
>
|
||||
<Field
|
||||
name="phoneNumber"
|
||||
placeholder="Phone Number"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="E-Mail"
|
||||
>
|
||||
<Field
|
||||
name="email"
|
||||
placeholder="E-Mail"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Disabled' labelFor='disabled'>
|
||||
<Field
|
||||
name='disabled'
|
||||
id='disabled'
|
||||
component={SwitchField}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Avatar'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'users/avatar'}
|
||||
name='avatar'
|
||||
id='avatar'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormImagePicker}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='App Role' labelFor='app_role'>
|
||||
<Field
|
||||
name='app_role'
|
||||
id='app_role'
|
||||
component={SelectField}
|
||||
options={initialValues.app_role}
|
||||
itemRef={'roles'}
|
||||
|
||||
|
||||
|
||||
|
||||
showField={'name'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Custom Permissions' labelFor='custom_permissions'>
|
||||
<Field
|
||||
name='custom_permissions'
|
||||
id='custom_permissions'
|
||||
component={SelectFieldMany}
|
||||
options={initialValues.custom_permissions}
|
||||
itemRef={'permissions'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
showField={'name'}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Password"
|
||||
>
|
||||
<Field
|
||||
name="password"
|
||||
placeholder="password"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<FormField label="First Name">
|
||||
<Field name="firstName" placeholder="First Name" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Last Name">
|
||||
<Field name="lastName" placeholder="Last Name" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="E-Mail">
|
||||
<Field name="email" placeholder="E-Mail" type="email" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Phone Number">
|
||||
<Field name="phoneNumber" placeholder="Phone Number" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Position">
|
||||
<Field name="position" placeholder="Position" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Hiring Year">
|
||||
<Field name="hiring_year" placeholder="Hiring Year" type="number" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Work Hours Per Week">
|
||||
<Field name="work_hours_per_week" placeholder="Work Hours Per Week" type="number" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Leave Policy Type">
|
||||
<Field name="leave_policy_type" component={SelectField} options={[
|
||||
{ value: 'pto', label: 'PTO' },
|
||||
{ value: 'paid_vacation_pay', label: 'Earned Vacation Pay' }
|
||||
]} />
|
||||
</FormField>
|
||||
|
||||
{values.leave_policy_type === 'pto' && (
|
||||
<FormField label="Paid PTO Per Year">
|
||||
<Field name="paid_pto_per_year" placeholder="Paid PTO Per Year" type="number" />
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField label="Medical Leave Per Year">
|
||||
<Field name="medical_leave_per_year" placeholder="Medical Leave Per Year" type="number" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Bereavement Per Year">
|
||||
<Field name="bereavement_per_year" placeholder="Bereavement Per Year" type="number" />
|
||||
</FormField>
|
||||
|
||||
{values.leave_policy_type === 'paid_vacation_pay' && (
|
||||
<FormField label="Vacation Pay Rate (%)">
|
||||
<Field name="vacation_pay_rate" placeholder="Vacation Pay Rate" type="number" />
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField label="Manager">
|
||||
<Field name="manager" component={SelectField} options={emptyOptions} itemRef={'users'} showField="firstName" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="App Role">
|
||||
<Field name="app_role" component={SelectField} options={emptyOptions} itemRef={'roles'} showField="name" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Password">
|
||||
<Field name="password" placeholder="Leave empty to keep current" type="password" />
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField label="Notification Recipients">
|
||||
<Field name="notification_recipients" component={SelectFieldMany} options={emptyOptions} itemRef={'users'} showField="firstName" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Custom Permissions">
|
||||
<Field name="custom_permissions" component={SelectFieldMany} options={emptyOptions} itemRef={'permissions'} showField="name" />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Disabled' labelFor='disabled'>
|
||||
<Field name='disabled' id='disabled' component={SwitchField} />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Avatar">
|
||||
<Field
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'users/avatar'}
|
||||
name='avatar'
|
||||
id='avatar'
|
||||
component={FormImagePicker}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Submit" />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/users/users-list')}/>
|
||||
<BaseButton type='button' color='danger' outline label='Cancel' onClick={() => router.push('/users/users-list')} />
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
@ -694,14 +208,10 @@ const EditUsersPage = () => {
|
||||
|
||||
EditUsersPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'UPDATE_USERS'}
|
||||
|
||||
>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
<LayoutAuthenticated permission={'UPDATE_USERS'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditUsersPage
|
||||
export default EditUsersPage
|
||||
@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('New Item')}</title>
|
||||
<title>{getPageTitle('New User')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
||||
{''}
|
||||
<SectionTitleLineWithButton icon={mdiAccount} title="New User" main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
initialValues={
|
||||
|
||||
initialValues
|
||||
|
||||
}
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
{({ values }) => (
|
||||
<Form>
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="First Name"
|
||||
>
|
||||
<Field
|
||||
name="firstName"
|
||||
placeholder="First Name"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Last Name"
|
||||
>
|
||||
<Field
|
||||
name="lastName"
|
||||
placeholder="Last Name"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Phone Number"
|
||||
>
|
||||
<Field
|
||||
name="phoneNumber"
|
||||
placeholder="Phone Number"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="E-Mail"
|
||||
>
|
||||
<Field
|
||||
name="email"
|
||||
placeholder="E-Mail"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Disabled' labelFor='disabled'>
|
||||
<Field
|
||||
name='disabled'
|
||||
id='disabled'
|
||||
component={SwitchField}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Avatar'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'users/avatar'}
|
||||
name='avatar'
|
||||
id='avatar'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormImagePicker}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="App Role" labelFor="app_role">
|
||||
<Field name="app_role" id="app_role" component={SelectField} options={[]} itemRef={'roles'}></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='Custom Permissions' labelFor='custom_permissions'>
|
||||
<Field
|
||||
name='custom_permissions'
|
||||
id='custom_permissions'
|
||||
itemRef={'permissions'}
|
||||
options={[]}
|
||||
component={SelectFieldMany}>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<FormField label="First Name">
|
||||
<Field name="firstName" placeholder="First Name" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Last Name">
|
||||
<Field name="lastName" placeholder="Last Name" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="E-Mail">
|
||||
<Field name="email" placeholder="E-Mail" type="email" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Phone Number">
|
||||
<Field name="phoneNumber" placeholder="Phone Number" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Position">
|
||||
<Field name="position" placeholder="Position" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Hiring Year">
|
||||
<Field name="hiring_year" placeholder="Hiring Year" type="number" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Work Hours Per Week">
|
||||
<Field name="work_hours_per_week" placeholder="Work Hours Per Week" type="number" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Leave Policy Type">
|
||||
<Field name="leave_policy_type" component={SelectField} options={[
|
||||
{ value: 'pto', label: 'PTO' },
|
||||
{ value: 'paid_vacation_pay', label: 'Earned Vacation Pay' }
|
||||
]} />
|
||||
</FormField>
|
||||
|
||||
{values.leave_policy_type === 'pto' && (
|
||||
<FormField label="Paid PTO Per Year">
|
||||
<Field name="paid_pto_per_year" placeholder="Paid PTO Per Year" type="number" />
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField label="Medical Leave Per Year">
|
||||
<Field name="medical_leave_per_year" placeholder="Medical Leave Per Year" type="number" />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Bereavement Per Year">
|
||||
<Field name="bereavement_per_year" placeholder="Bereavement Per Year" type="number" />
|
||||
</FormField>
|
||||
|
||||
{values.leave_policy_type === 'paid_vacation_pay' && (
|
||||
<FormField label="Vacation Pay Rate (%)">
|
||||
<Field name="vacation_pay_rate" placeholder="Vacation Pay Rate" type="number" />
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField label="Manager">
|
||||
<Field name="manager" component={SelectField} options={emptyOptions} itemRef={'users'} />
|
||||
</FormField>
|
||||
|
||||
<FormField label="App Role">
|
||||
<Field name="app_role" component={SelectField} options={emptyOptions} itemRef={'roles'} />
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField label="Notification Recipients">
|
||||
<Field name="notification_recipients" component={SelectFieldMany} options={emptyOptions} itemRef={'users'} />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Disabled' labelFor='disabled'>
|
||||
<Field name='disabled' id='disabled' component={SwitchField} />
|
||||
</FormField>
|
||||
|
||||
<FormField label="Avatar">
|
||||
<Field
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'users/avatar'}
|
||||
name='avatar'
|
||||
id='avatar'
|
||||
component={FormImagePicker}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Submit" />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/users/users-list')}/>
|
||||
<BaseButton type='button' color='danger' outline label='Cancel' onClick={() => router.push('/users/users-list')} />
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
@ -489,14 +171,10 @@ const UsersNew = () => {
|
||||
|
||||
UsersNew.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'CREATE_USERS'}
|
||||
|
||||
>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
<LayoutAuthenticated permission={'CREATE_USERS'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
}
|
||||
|
||||
export default UsersNew
|
||||
export default UsersNew
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user