Autosave: 20260216-225015
This commit is contained in:
parent
4b6bdc53b6
commit
60d25175fb
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -321,18 +320,6 @@ module.exports = class Pto_journal_entriesDBApi {
|
|||||||
|
|
||||||
const output = pto_journal_entries.get({plain: true});
|
const output = pto_journal_entries.get({plain: true});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.user = await pto_journal_entries.getUser({
|
output.user = await pto_journal_entries.getUser({
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
@ -356,21 +343,34 @@ module.exports = class Pto_journal_entriesDBApi {
|
|||||||
filter,
|
filter,
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
|
const currentUser = (options && options.currentUser) || { id: null };
|
||||||
const limit = filter.limit || 0;
|
const limit = filter.limit || 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let where = {};
|
let where = {};
|
||||||
const currentPage = +filter.page;
|
const currentPage = +filter.page;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
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 = [
|
let include = [
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -423,9 +423,6 @@ module.exports = class Pto_journal_entriesDBApi {
|
|||||||
} : {},
|
} : {},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
@ -710,5 +707,4 @@ module.exports = class Pto_journal_entriesDBApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -85,6 +84,16 @@ module.exports = class UsersDBApi {
|
|||||||
null
|
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,
|
importHash: data.data.importHash || null,
|
||||||
createdById: currentUser.id,
|
createdById: currentUser.id,
|
||||||
updatedById: currentUser.id,
|
updatedById: currentUser.id,
|
||||||
@ -116,6 +125,9 @@ module.exports = class UsersDBApi {
|
|||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await users.setNotification_recipients(data.data.notification_recipients || [], {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
await FileDBApi.replaceRelationFiles(
|
await FileDBApi.replaceRelationFiles(
|
||||||
@ -205,6 +217,16 @@ module.exports = class UsersDBApi {
|
|||||||
null
|
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,
|
importHash: item.importHash || null,
|
||||||
createdById: currentUser.id,
|
createdById: currentUser.id,
|
||||||
updatedById: currentUser.id,
|
updatedById: currentUser.id,
|
||||||
@ -298,7 +320,16 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
if (data.provider !== undefined) updatePayload.provider = data.provider;
|
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;
|
updatePayload.updatedById = currentUser.id;
|
||||||
|
|
||||||
await users.update(updatePayload, {transaction});
|
await users.update(updatePayload, {transaction});
|
||||||
@ -320,6 +351,10 @@ module.exports = class UsersDBApi {
|
|||||||
if (data.custom_permissions !== undefined) {
|
if (data.custom_permissions !== undefined) {
|
||||||
await users.setCustom_permissions(data.custom_permissions, { transaction });
|
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({
|
output.app_role = await users.getApp_role({
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
|
|
||||||
|
output.manager = await users.getManager({ transaction });
|
||||||
|
output.notification_recipients = await users.getNotification_recipients({ transaction });
|
||||||
|
|
||||||
if (output.app_role) {
|
if (output.app_role) {
|
||||||
output.app_role_permissions = await output.app_role.getPermissions({
|
output.app_role_permissions = await output.app_role.getPermissions({
|
||||||
@ -515,6 +553,11 @@ module.exports = class UsersDBApi {
|
|||||||
as: 'avatar',
|
as: 'avatar',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
model: db.users,
|
||||||
|
as: 'manager',
|
||||||
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (filter) {
|
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({
|
const records = await db.users.findAll({
|
||||||
attributes: [ 'id', 'firstName' ],
|
attributes: [ 'id', 'firstName', 'lastName' ],
|
||||||
where,
|
where,
|
||||||
limit: limit ? Number(limit) : undefined,
|
limit: limit ? Number(limit) : undefined,
|
||||||
offset: offset ? Number(offset) : undefined,
|
offset: offset ? Number(offset) : undefined,
|
||||||
@ -804,7 +858,7 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
return records.map((record) => ({
|
return records.map((record) => ({
|
||||||
id: record.id,
|
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
|
const token = crypto
|
||||||
.randomBytes(20)
|
.randomBytes(20)
|
||||||
.toString('hex');
|
.toString('hex');
|
||||||
@ -958,5 +1016,4 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -280,18 +279,6 @@ module.exports = class Yearly_leave_summariesDBApi {
|
|||||||
|
|
||||||
const output = yearly_leave_summaries.get({plain: true});
|
const output = yearly_leave_summaries.get({plain: true});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.user = await yearly_leave_summaries.getUser({
|
output.user = await yearly_leave_summaries.getUser({
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
@ -305,21 +292,34 @@ module.exports = class Yearly_leave_summariesDBApi {
|
|||||||
filter,
|
filter,
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
|
const currentUser = (options && options.currentUser) || { id: null };
|
||||||
const limit = filter.limit || 0;
|
const limit = filter.limit || 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let where = {};
|
let where = {};
|
||||||
const currentPage = +filter.page;
|
const currentPage = +filter.page;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
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 = [
|
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) {
|
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: {
|
firstName: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
lastName: {
|
lastName: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
phoneNumber: {
|
phoneNumber: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
email: {
|
email: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
disabled: {
|
disabled: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
|
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
password: {
|
password: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
emailVerified: {
|
emailVerified: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
|
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
emailVerificationToken: {
|
emailVerificationToken: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
emailVerificationTokenExpiresAt: {
|
emailVerificationTokenExpiresAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
passwordResetToken: {
|
passwordResetToken: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
passwordResetTokenExpiresAt: {
|
passwordResetTokenExpiresAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
provider: {
|
provider: {
|
||||||
type: DataTypes.TEXT,
|
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: {
|
importHash: {
|
||||||
@ -137,15 +132,6 @@ provider: {
|
|||||||
through: 'usersCustom_permissionsPermissions',
|
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, {
|
db.users.hasMany(db.time_off_requests, {
|
||||||
as: 'time_off_requests_requester',
|
as: 'time_off_requests_requester',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -162,7 +148,6 @@ provider: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
db.users.hasMany(db.pto_journal_entries, {
|
db.users.hasMany(db.pto_journal_entries, {
|
||||||
as: 'pto_journal_entries_user',
|
as: 'pto_journal_entries_user',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -179,7 +164,6 @@ provider: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
db.users.hasMany(db.yearly_leave_summaries, {
|
db.users.hasMany(db.yearly_leave_summaries, {
|
||||||
as: 'yearly_leave_summaries_user',
|
as: 'yearly_leave_summaries_user',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -188,7 +172,6 @@ provider: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
db.users.hasMany(db.office_calendar_events, {
|
db.users.hasMany(db.office_calendar_events, {
|
||||||
as: 'office_calendar_events_user',
|
as: 'office_calendar_events_user',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -197,7 +180,6 @@ provider: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
db.users.hasMany(db.approval_tasks, {
|
db.users.hasMany(db.approval_tasks, {
|
||||||
as: 'approval_tasks_assigned_manager',
|
as: 'approval_tasks_assigned_manager',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -206,12 +188,6 @@ provider: {
|
|||||||
constraints: false,
|
constraints: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//end loop
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.users.belongsTo(db.roles, {
|
db.users.belongsTo(db.roles, {
|
||||||
as: 'app_role',
|
as: 'app_role',
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
@ -220,7 +196,19 @@ provider: {
|
|||||||
constraints: false,
|
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, {
|
db.users.hasMany(db.file, {
|
||||||
as: 'avatar',
|
as: 'avatar',
|
||||||
@ -232,7 +220,6 @@ provider: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
db.users.belongsTo(db.users, {
|
db.users.belongsTo(db.users, {
|
||||||
as: 'createdBy',
|
as: 'createdBy',
|
||||||
});
|
});
|
||||||
@ -285,5 +272,4 @@ function trimStringFields(users) {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
const config = require('../../config');
|
const config = require('../../config');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const nodemailer = require('nodemailer');
|
const nodemailer = require('nodemailer');
|
||||||
|
const db = require('../../db/models');
|
||||||
|
|
||||||
module.exports = class EmailSender {
|
module.exports = class EmailSender {
|
||||||
constructor(email) {
|
constructor(email) {
|
||||||
@ -13,6 +14,43 @@ module.exports = class EmailSender {
|
|||||||
assert(this.email.subject, 'email.subject is required');
|
assert(this.email.subject, 'email.subject is required');
|
||||||
assert(this.email.html, 'email.html 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 htmlContent = await this.email.html();
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport(this.transportConfig);
|
const transporter = nodemailer.createTransport(this.transportConfig);
|
||||||
@ -41,4 +79,4 @@ module.exports = class EmailSender {
|
|||||||
get from() {
|
get from() {
|
||||||
return config.email.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 Link from 'next/link'
|
||||||
import { useState } from 'react'
|
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
|
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
|
||||||
}
|
}
|
||||||
@ -1,19 +1,28 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { MenuNavBarItem } from '../interfaces'
|
import { MenuNavBarItem } from '../interfaces'
|
||||||
import NavBarItem from './NavBarItem'
|
import NavBarItem from './NavBarItem'
|
||||||
|
import { useAppSelector } from '../stores/hooks'
|
||||||
|
import { hasPermission } from '../helpers/userPermissions'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
menu: MenuNavBarItem[]
|
menu: MenuNavBarItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NavBarMenuList({ menu }: Props) {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{menu.map((item, index) => (
|
{filteredMenu.map((item, index) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<NavBarItem item={item} />
|
<NavBarItem item={item} />
|
||||||
</div>
|
</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 React, { ReactNode, useEffect } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
|
||||||
import menuAside from '../menuAside'
|
|
||||||
import menuNavBar from '../menuNavBar'
|
import menuNavBar from '../menuNavBar'
|
||||||
import BaseIcon from '../components/BaseIcon'
|
|
||||||
import NavBar from '../components/NavBar'
|
import NavBar from '../components/NavBar'
|
||||||
import NavBarItemPlain from '../components/NavBarItemPlain'
|
|
||||||
import AsideMenu from '../components/AsideMenu'
|
|
||||||
import FooterBar from '../components/FooterBar'
|
import FooterBar from '../components/FooterBar'
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||||
import Search from '../components/Search';
|
import Search from '../components/Search';
|
||||||
@ -15,6 +9,7 @@ import { useRouter } from 'next/router'
|
|||||||
import {findMe, logoutUser} from "../stores/authSlice";
|
import {findMe, logoutUser} from "../stores/authSlice";
|
||||||
|
|
||||||
import {hasPermission} from "../helpers/userPermissions";
|
import {hasPermission} from "../helpers/userPermissions";
|
||||||
|
import NavBarItemPlain from '../components/NavBarItemPlain';
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -67,61 +62,22 @@ export default function LayoutAuthenticated({
|
|||||||
|
|
||||||
const darkMode = useAppSelector((state) => state.style.darkMode)
|
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 (
|
return (
|
||||||
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
||||||
<div
|
<div
|
||||||
className={`${layoutAsidePadding} ${
|
className={`pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
|
||||||
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`}
|
|
||||||
>
|
>
|
||||||
<NavBar
|
<NavBar
|
||||||
menu={menuNavBar}
|
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>
|
<NavBarItemPlain useMargin>
|
||||||
<Search />
|
<Search />
|
||||||
</NavBarItemPlain>
|
</NavBarItemPlain>
|
||||||
</NavBar>
|
</NavBar>
|
||||||
<AsideMenu
|
<div className="max-w-7xl mx-auto">
|
||||||
isAsideMobileExpanded={isAsideMobileExpanded}
|
{children}
|
||||||
isAsideLgActive={isAsideLgActive}
|
</div>
|
||||||
menu={menuAside}
|
|
||||||
onAsideLgClose={() => setIsAsideLgActive(false)}
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
<FooterBar>Hand-crafted & Made with ❤️</FooterBar>
|
<FooterBar>Hand-crafted & Made with ❤️</FooterBar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,19 +1,117 @@
|
|||||||
import {
|
import {
|
||||||
mdiMenu,
|
|
||||||
mdiClockOutline,
|
|
||||||
mdiCloud,
|
|
||||||
mdiCrop,
|
|
||||||
mdiAccount,
|
mdiAccount,
|
||||||
mdiCogOutline,
|
|
||||||
mdiEmail,
|
|
||||||
mdiLogout,
|
mdiLogout,
|
||||||
mdiThemeLightDark,
|
mdiThemeLightDark,
|
||||||
mdiGithub,
|
mdiViewDashboardOutline,
|
||||||
mdiVuejs,
|
mdiAccountGroup,
|
||||||
|
mdiShieldAccountVariantOutline,
|
||||||
|
mdiShieldAccountOutline,
|
||||||
|
mdiCalendarStar,
|
||||||
|
mdiCalendarRange,
|
||||||
|
mdiClipboardTextClock,
|
||||||
|
mdiBookOpenPageVariant,
|
||||||
|
mdiChartBox,
|
||||||
|
mdiCalendarMonth,
|
||||||
|
mdiCheckDecagram,
|
||||||
|
mdiFileCode,
|
||||||
|
mdiAccountCircle,
|
||||||
|
mdiTable
|
||||||
} from '@mdi/js'
|
} from '@mdi/js'
|
||||||
import { MenuNavBarItem } from './interfaces'
|
import { MenuNavBarItem } from './interfaces'
|
||||||
|
|
||||||
const menuNavBar: MenuNavBarItem[] = [
|
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,
|
isCurrentUser: true,
|
||||||
menu: [
|
menu: [
|
||||||
|
|||||||
@ -1,431 +1,215 @@
|
|||||||
import * as icon from '@mdi/js';
|
import * as icon from '@mdi/js';
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated'
|
import LayoutAuthenticated from '../layouts/Authenticated'
|
||||||
import SectionMain from '../components/SectionMain'
|
import SectionMain from '../components/SectionMain'
|
||||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
||||||
import BaseIcon from "../components/BaseIcon";
|
import CardBox from '../components/CardBox'
|
||||||
|
import BaseButton from '../components/BaseButton'
|
||||||
import { getPageTitle } from '../config'
|
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 Dashboard = () => {
|
||||||
const dispatch = useAppDispatch();
|
const { currentUser } = useAppSelector((state) => state.auth)
|
||||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear())
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
const [summary, setSummary] = useState(null)
|
||||||
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
|
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)
|
||||||
|
|
||||||
|
// Fetch Pending Approvals if manager/admin
|
||||||
const [users, setUsers] = React.useState(loadingMessage);
|
const approvalsRes = await axios.get(`/approval_tasks`, {
|
||||||
const [roles, setRoles] = React.useState(loadingMessage);
|
params: {
|
||||||
const [permissions, setPermissions] = React.useState(loadingMessage);
|
filter: JSON.stringify({
|
||||||
const [holiday_calendars, setHoliday_calendars] = React.useState(loadingMessage);
|
status: 'pending'
|
||||||
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);
|
setApprovals(approvalsRes.data.rows)
|
||||||
const [office_calendar_events, setOffice_calendar_events] = React.useState(loadingMessage);
|
|
||||||
const [approval_tasks, setApproval_tasks] = React.useState(loadingMessage);
|
|
||||||
|
|
||||||
|
// Fetch Upcoming Time Off
|
||||||
const [widgetsRole, setWidgetsRole] = React.useState({
|
const upcomingRes = await axios.get(`/office_calendar_events`, {
|
||||||
role: { value: '', label: '' },
|
params: {
|
||||||
});
|
limit: 5,
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
offset: 0,
|
||||||
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
sort: 'start_date_ASC'
|
||||||
|
}
|
||||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
})
|
||||||
|
setUpcomingTimeOff(upcomingRes.data.rows.filter(e => moment(e.start_date).isSameOrAfter(moment(), 'day')))
|
||||||
|
|
||||||
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,];
|
|
||||||
|
|
||||||
const requests = entities.map((entity, index) => {
|
// Fetch Holidays for selected year
|
||||||
|
const holidaysRes = await axios.get(`/holidays`, {
|
||||||
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
params: {
|
||||||
return axios.get(`/${entity.toLowerCase()}/count`);
|
filter: JSON.stringify({
|
||||||
} else {
|
calendar_year: selectedYear
|
||||||
fns[index](null);
|
})
|
||||||
return Promise.resolve({data: {count: null}});
|
}
|
||||||
}
|
})
|
||||||
|
setHolidays(holidaysRes.data.rows)
|
||||||
});
|
|
||||||
|
|
||||||
Promise.allSettled(requests).then((results) => {
|
} catch (error) {
|
||||||
results.forEach((result, i) => {
|
console.error('Error fetching dashboard data:', error)
|
||||||
if (result.status === 'fulfilled') {
|
} finally {
|
||||||
fns[i](result.value.data.count);
|
setLoading(false)
|
||||||
} else {
|
|
||||||
fns[i](result.reason.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
async function getWidgets(roleId) {
|
|
||||||
await dispatch(fetchWidgets(roleId));
|
useEffect(() => {
|
||||||
}
|
if (currentUser) {
|
||||||
React.useEffect(() => {
|
fetchDashboardData()
|
||||||
if (!currentUser) return;
|
}
|
||||||
loadData().then();
|
}, [currentUser, selectedYear])
|
||||||
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
|
|
||||||
}, [currentUser]);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>{getPageTitle('Home')}</title>
|
||||||
{getPageTitle('Overview')}
|
|
||||||
</title>
|
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton
|
<SectionTitleLineWithButton icon={icon.mdiHome} title="Home" main>
|
||||||
icon={icon.mdiChartTimelineVariant}
|
<div className="flex items-center space-x-2">
|
||||||
title='Overview'
|
<span className="text-sm text-gray-500">Year:</span>
|
||||||
main>
|
<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>
|
</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'>
|
{/* PTO Summary Stats */}
|
||||||
{(isFetchingQuery || loading) && (
|
<PTOStats
|
||||||
<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`}>
|
summary={summary || {
|
||||||
<BaseIcon
|
pto_pending_days: 0,
|
||||||
className={`${iconsColor} animate-spin mr-5`}
|
pto_scheduled_days: 0,
|
||||||
w='w-16'
|
pto_available_days: 0,
|
||||||
h='h-16'
|
medical_taken_days: 0
|
||||||
size={48}
|
}}
|
||||||
path={icon.mdiLoading}
|
/>
|
||||||
/>{' '}
|
|
||||||
Loading widgets...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ rolesWidgets &&
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
rolesWidgets.map((widget) => (
|
{/* Action Items (Approvals) */}
|
||||||
<SmartWidget
|
<CardBox className="flex-1" hasTable>
|
||||||
key={widget.id}
|
<div className="p-4 border-b dark:border-dark-700 flex justify-between items-center">
|
||||||
userId={currentUser?.id}
|
<h3 className="font-bold">Action Items (Pending Approvals)</h3>
|
||||||
widget={widget}
|
<Link href="/approval_tasks/approval_tasks-list" className="text-sm text-blue-500 hover:underline">
|
||||||
roleId={widgetsRole?.role?.value || ''}
|
View All
|
||||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
</Link>
|
||||||
/>
|
</div>
|
||||||
))}
|
<div className="overflow-x-auto">
|
||||||
</div>
|
<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 ' />}
|
{/* Upcoming Time Off & Holidays */}
|
||||||
|
<CardBox className="flex-1" hasTable>
|
||||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
<div className="p-4 border-b dark:border-dark-700">
|
||||||
|
<h3 className="font-bold">Upcoming Time Off & Holidays</h3>
|
||||||
|
</div>
|
||||||
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
<div className="overflow-x-auto">
|
||||||
<div
|
<table className="w-full text-sm text-left">
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
<thead>
|
||||||
>
|
<tr className="border-b dark:border-dark-700">
|
||||||
<div className="flex justify-between align-center">
|
<th className="p-4">Date</th>
|
||||||
<div>
|
<th className="p-4">Event / Name</th>
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
<th className="p-4">Type</th>
|
||||||
Users
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
<tbody>
|
||||||
{users}
|
{[...holidays, ...upcomingTimeOff]
|
||||||
</div>
|
.sort((a, b) => {
|
||||||
</div>
|
const dateA = a.holiday_date || a.start_date
|
||||||
<div>
|
const dateB = b.holiday_date || b.start_date
|
||||||
<BaseIcon
|
return moment(dateA).diff(moment(dateB))
|
||||||
className={`${iconsColor}`}
|
})
|
||||||
w="w-16"
|
.slice(0, 10)
|
||||||
h="h-16"
|
.map((item, idx) => {
|
||||||
size={48}
|
const isHoliday = !!item.holiday_date
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
return (
|
||||||
// @ts-ignore
|
<tr key={idx} className="border-b dark:border-dark-700">
|
||||||
path={icon.mdiAccountGroup || icon.mdiTable}
|
<td className="p-4">
|
||||||
/>
|
{moment(item.holiday_date || item.start_date).format('MMM D, YYYY')}
|
||||||
</div>
|
</td>
|
||||||
</div>
|
<td className="p-4">
|
||||||
</div>
|
{isHoliday ? item.name : `${item.user?.firstName} ${item.user?.lastName}`}
|
||||||
</Link>}
|
</td>
|
||||||
|
<td className="p-4">
|
||||||
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
|
<span className={`px-2 py-1 rounded-full text-xs ${isHoliday ? 'bg-purple-100 text-purple-700' : 'bg-blue-100 text-blue-700'}`}>
|
||||||
<div
|
{isHoliday ? 'Holiday' : 'PTO'}
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
</span>
|
||||||
>
|
</td>
|
||||||
<div className="flex justify-between align-center">
|
</tr>
|
||||||
<div>
|
)
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
})}
|
||||||
Roles
|
{holidays.length === 0 && upcomingTimeOff.length === 0 && (
|
||||||
</div>
|
<tr>
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
<td colSpan={3} className="p-4 text-center text-gray-500">No upcoming events</td>
|
||||||
{roles}
|
</tr>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</tbody>
|
||||||
<div>
|
</table>
|
||||||
<BaseIcon
|
</div>
|
||||||
className={`${iconsColor}`}
|
</CardBox>
|
||||||
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>}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
@ -436,4 +220,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
|
|||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
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 Head from 'next/head'
|
||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
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 CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
@ -16,280 +12,69 @@ import FormField from '../../components/FormField'
|
|||||||
import BaseDivider from '../../components/BaseDivider'
|
import BaseDivider from '../../components/BaseDivider'
|
||||||
import BaseButtons from '../../components/BaseButtons'
|
import BaseButtons from '../../components/BaseButtons'
|
||||||
import BaseButton from '../../components/BaseButton'
|
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 FormImagePicker from '../../components/FormImagePicker'
|
||||||
import { SelectField } from "../../components/SelectField";
|
import { SelectField } from "../../components/SelectField";
|
||||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
||||||
import { SwitchField } from '../../components/SwitchField'
|
import { SwitchField } from '../../components/SwitchField'
|
||||||
import {RichTextField} from "../../components/RichTextField";
|
|
||||||
|
|
||||||
import { update, fetch } from '../../stores/users/usersSlice'
|
import { update, fetch } from '../../stores/users/usersSlice'
|
||||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
import { useRouter } from 'next/router'
|
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 EditUsersPage = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
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
|
const { id } = router.query
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetch({ id: id }))
|
if (id) {
|
||||||
}, [id])
|
dispatch(fetch({ id: id }))
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof users === 'object') {
|
|
||||||
setInitialValues(users)
|
|
||||||
}
|
}
|
||||||
}, [users])
|
}, [id, dispatch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof users === 'object') {
|
if (users && typeof users === 'object') {
|
||||||
const newInitialVal = {...initVals};
|
const newInitialVal = { ...initVals };
|
||||||
Object.keys(initVals).forEach(el => newInitialVal[el] = (users)[el])
|
Object.keys(initVals).forEach(el => {
|
||||||
setInitialValues(newInitialVal);
|
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])
|
}, [users])
|
||||||
|
|
||||||
const handleSubmit = async (data) => {
|
const handleSubmit = async (data) => {
|
||||||
@ -300,11 +85,11 @@ const EditUsersPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Edit users')}</title>
|
<title>{getPageTitle('Edit User')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit users'} main>
|
<SectionTitleLineWithButton icon={mdiAccount} title={'Edit User'} main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
<Formik
|
<Formik
|
||||||
@ -312,379 +97,108 @@ const EditUsersPage = () => {
|
|||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
|
{({ values }) => (
|
||||||
<Form>
|
<Form>
|
||||||
|
<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="First Name"
|
|
||||||
>
|
<FormField label="Last Name">
|
||||||
<Field
|
<Field name="lastName" placeholder="Last Name" />
|
||||||
name="firstName"
|
</FormField>
|
||||||
placeholder="First Name"
|
|
||||||
/>
|
<FormField label="E-Mail">
|
||||||
</FormField>
|
<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="Last Name"
|
|
||||||
>
|
<FormField label="Medical Leave Per Year">
|
||||||
<Field
|
<Field name="medical_leave_per_year" placeholder="Medical Leave Per Year" type="number" />
|
||||||
name="lastName"
|
</FormField>
|
||||||
placeholder="Last Name"
|
|
||||||
/>
|
<FormField label="Bereavement Per Year">
|
||||||
</FormField>
|
<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
|
<FormField label='Disabled' labelFor='disabled'>
|
||||||
label="Phone Number"
|
<Field name='disabled' id='disabled' component={SwitchField} />
|
||||||
>
|
</FormField>
|
||||||
<Field
|
|
||||||
name="phoneNumber"
|
<FormField label="Avatar">
|
||||||
placeholder="Phone Number"
|
<Field
|
||||||
/>
|
color='info'
|
||||||
</FormField>
|
icon={mdiUpload}
|
||||||
|
path={'users/avatar'}
|
||||||
|
name='avatar'
|
||||||
|
id='avatar'
|
||||||
|
component={FormImagePicker}
|
||||||
|
/>
|
||||||
|
</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>
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
<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>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
@ -694,14 +208,10 @@ const EditUsersPage = () => {
|
|||||||
|
|
||||||
EditUsersPage.getLayout = function getLayout(page: ReactElement) {
|
EditUsersPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated
|
<LayoutAuthenticated permission={'UPDATE_USERS'}>
|
||||||
|
{page}
|
||||||
permission={'UPDATE_USERS'}
|
</LayoutAuthenticated>
|
||||||
|
|
||||||
>
|
|
||||||
{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 Head from 'next/head'
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
@ -12,474 +12,156 @@ import FormField from '../../components/FormField'
|
|||||||
import BaseDivider from '../../components/BaseDivider'
|
import BaseDivider from '../../components/BaseDivider'
|
||||||
import BaseButtons from '../../components/BaseButtons'
|
import BaseButtons from '../../components/BaseButtons'
|
||||||
import BaseButton from '../../components/BaseButton'
|
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 FormImagePicker from '../../components/FormImagePicker'
|
||||||
import { SwitchField } from '../../components/SwitchField'
|
import { SwitchField } from '../../components/SwitchField'
|
||||||
|
|
||||||
import { SelectField } from '../../components/SelectField'
|
import { SelectField } from '../../components/SelectField'
|
||||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
||||||
import {RichTextField} from "../../components/RichTextField";
|
|
||||||
|
|
||||||
import { create } from '../../stores/users/usersSlice'
|
import { create } from '../../stores/users/usersSlice'
|
||||||
import { useAppDispatch } from '../../stores/hooks'
|
import { useAppDispatch } from '../../stores/hooks'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
|
||||||
|
|
||||||
firstName: '',
|
firstName: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
lastName: '',
|
lastName: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
phoneNumber: '',
|
phoneNumber: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
email: '',
|
email: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
avatar: [],
|
avatar: [],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app_role: '',
|
app_role: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
custom_permissions: [],
|
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 UsersNew = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async (data) => {
|
const handleSubmit = async (data) => {
|
||||||
await dispatch(create(data))
|
await dispatch(create(data))
|
||||||
await router.push('/users/users-list')
|
await router.push('/users/users-list')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('New Item')}</title>
|
<title>{getPageTitle('New User')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
<SectionTitleLineWithButton icon={mdiAccount} title="New User" main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={
|
initialValues={initialValues}
|
||||||
|
|
||||||
initialValues
|
|
||||||
|
|
||||||
}
|
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
|
{({ values }) => (
|
||||||
<Form>
|
<Form>
|
||||||
|
<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="First Name"
|
|
||||||
>
|
<FormField label="Last Name">
|
||||||
<Field
|
<Field name="lastName" placeholder="Last Name" />
|
||||||
name="firstName"
|
</FormField>
|
||||||
placeholder="First Name"
|
|
||||||
/>
|
<FormField label="E-Mail">
|
||||||
</FormField>
|
<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">
|
||||||
<FormField
|
<Field name="paid_pto_per_year" placeholder="Paid PTO Per Year" type="number" />
|
||||||
label="Last Name"
|
</FormField>
|
||||||
>
|
)}
|
||||||
<Field
|
|
||||||
name="lastName"
|
<FormField label="Medical Leave Per Year">
|
||||||
placeholder="Last Name"
|
<Field name="medical_leave_per_year" placeholder="Medical Leave Per Year" type="number" />
|
||||||
/>
|
</FormField>
|
||||||
</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
|
<FormField label="Avatar">
|
||||||
label="Phone Number"
|
<Field
|
||||||
>
|
color='info'
|
||||||
<Field
|
icon={mdiUpload}
|
||||||
name="phoneNumber"
|
path={'users/avatar'}
|
||||||
placeholder="Phone Number"
|
name='avatar'
|
||||||
/>
|
id='avatar'
|
||||||
</FormField>
|
component={FormImagePicker}
|
||||||
|
/>
|
||||||
|
</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>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
<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>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
@ -489,14 +171,10 @@ const UsersNew = () => {
|
|||||||
|
|
||||||
UsersNew.getLayout = function getLayout(page: ReactElement) {
|
UsersNew.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated
|
<LayoutAuthenticated permission={'CREATE_USERS'}>
|
||||||
|
{page}
|
||||||
permission={'CREATE_USERS'}
|
</LayoutAuthenticated>
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</LayoutAuthenticated>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UsersNew
|
export default UsersNew
|
||||||
@ -158,6 +158,7 @@ export const usersSlice = createSlice({
|
|||||||
|
|
||||||
builder.addCase(deleteItemsByIds.fulfilled, (state) => {
|
builder.addCase(deleteItemsByIds.fulfilled, (state) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refetch = true;
|
||||||
fulfilledNotify(state, 'Users has been deleted');
|
fulfilledNotify(state, 'Users has been deleted');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -173,13 +174,14 @@ export const usersSlice = createSlice({
|
|||||||
|
|
||||||
builder.addCase(deleteItem.fulfilled, (state) => {
|
builder.addCase(deleteItem.fulfilled, (state) => {
|
||||||
state.loading = false
|
state.loading = false
|
||||||
|
state.refetch = true;
|
||||||
fulfilledNotify(state, `${'Users'.slice(0, -1)} has been deleted`);
|
fulfilledNotify(state, `${'Users'.slice(0, -1)} has been deleted`);
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.addCase(deleteItem.rejected, (state, action) => {
|
builder.addCase(deleteItem.rejected, (state, action) => {
|
||||||
state.loading = false
|
state.loading = false;
|
||||||
rejectNotify(state, action);
|
rejectNotify(state, action);
|
||||||
})
|
});
|
||||||
|
|
||||||
builder.addCase(create.pending, (state) => {
|
builder.addCase(create.pending, (state) => {
|
||||||
state.loading = true
|
state.loading = true
|
||||||
@ -192,6 +194,7 @@ export const usersSlice = createSlice({
|
|||||||
|
|
||||||
builder.addCase(create.fulfilled, (state) => {
|
builder.addCase(create.fulfilled, (state) => {
|
||||||
state.loading = false
|
state.loading = false
|
||||||
|
// state.refetch = true; // Removed to fix infinite loop
|
||||||
fulfilledNotify(state, `${'Users'.slice(0, -1)} has been created`);
|
fulfilledNotify(state, `${'Users'.slice(0, -1)} has been created`);
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -201,6 +204,7 @@ export const usersSlice = createSlice({
|
|||||||
})
|
})
|
||||||
builder.addCase(update.fulfilled, (state) => {
|
builder.addCase(update.fulfilled, (state) => {
|
||||||
state.loading = false
|
state.loading = false
|
||||||
|
// state.refetch = true; // Removed to fix infinite loop
|
||||||
fulfilledNotify(state, `${'Users'.slice(0, -1)} has been updated`);
|
fulfilledNotify(state, `${'Users'.slice(0, -1)} has been updated`);
|
||||||
})
|
})
|
||||||
builder.addCase(update.rejected, (state, action) => {
|
builder.addCase(update.rejected, (state, action) => {
|
||||||
@ -214,6 +218,7 @@ export const usersSlice = createSlice({
|
|||||||
})
|
})
|
||||||
builder.addCase(uploadCsv.fulfilled, (state) => {
|
builder.addCase(uploadCsv.fulfilled, (state) => {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.refetch = true;
|
||||||
fulfilledNotify(state, 'Users has been uploaded');
|
fulfilledNotify(state, 'Users has been uploaded');
|
||||||
})
|
})
|
||||||
builder.addCase(uploadCsv.rejected, (state, action) => {
|
builder.addCase(uploadCsv.rejected, (state, action) => {
|
||||||
@ -228,4 +233,4 @@ export const usersSlice = createSlice({
|
|||||||
// Action creators are generated for each case reducer function
|
// Action creators are generated for each case reducer function
|
||||||
export const { setRefetch } = usersSlice.actions
|
export const { setRefetch } = usersSlice.actions
|
||||||
|
|
||||||
export default usersSlice.reducer
|
export default usersSlice.reducer
|
||||||
Loading…
x
Reference in New Issue
Block a user