01
This commit is contained in:
parent
d130092f78
commit
25962a2c05
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,3 +1,8 @@
|
||||
node_modules/
|
||||
*/node_modules/
|
||||
*/build/
|
||||
|
||||
**/node_modules/
|
||||
**/build/
|
||||
.DS_Store
|
||||
.env
|
||||
File diff suppressed because one or more lines are too long
@ -18,6 +18,7 @@ module.exports = class EmployeesDBApi {
|
||||
name: data.name || null,
|
||||
email: data.email || null,
|
||||
phone: data.phone || null,
|
||||
password_hash: data.password_hash || null,
|
||||
importHash: data.importHash || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
@ -29,6 +30,10 @@ module.exports = class EmployeesDBApi {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await employees.setRole(data.role || null, {
|
||||
transaction,
|
||||
});
|
||||
|
||||
return employees;
|
||||
}
|
||||
|
||||
@ -43,6 +48,7 @@ module.exports = class EmployeesDBApi {
|
||||
name: item.name || null,
|
||||
email: item.email || null,
|
||||
phone: item.phone || null,
|
||||
password_hash: item.password_hash || null,
|
||||
importHash: item.importHash || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
@ -73,6 +79,9 @@ module.exports = class EmployeesDBApi {
|
||||
|
||||
if (data.phone !== undefined) updatePayload.phone = data.phone;
|
||||
|
||||
if (data.password_hash !== undefined)
|
||||
updatePayload.password_hash = data.password_hash;
|
||||
|
||||
updatePayload.updatedById = currentUser.id;
|
||||
|
||||
await employees.update(updatePayload, { transaction });
|
||||
@ -85,6 +94,14 @@ module.exports = class EmployeesDBApi {
|
||||
);
|
||||
}
|
||||
|
||||
if (data.role !== undefined) {
|
||||
await employees.setRole(
|
||||
data.role,
|
||||
|
||||
{ transaction },
|
||||
);
|
||||
}
|
||||
|
||||
return employees;
|
||||
}
|
||||
|
||||
@ -150,10 +167,19 @@ module.exports = class EmployeesDBApi {
|
||||
transaction,
|
||||
});
|
||||
|
||||
output.notification_logs_employee =
|
||||
await employees.getNotification_logs_employee({
|
||||
transaction,
|
||||
});
|
||||
|
||||
output.department = await employees.getDepartment({
|
||||
transaction,
|
||||
});
|
||||
|
||||
output.role = await employees.getRole({
|
||||
transaction,
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@ -195,6 +221,32 @@ module.exports = class EmployeesDBApi {
|
||||
}
|
||||
: {},
|
||||
},
|
||||
|
||||
{
|
||||
model: db.roles,
|
||||
as: 'role',
|
||||
|
||||
where: filter.role
|
||||
? {
|
||||
[Op.or]: [
|
||||
{
|
||||
id: {
|
||||
[Op.in]: filter.role
|
||||
.split('|')
|
||||
.map((term) => Utils.uuid(term)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: {
|
||||
[Op.or]: filter.role
|
||||
.split('|')
|
||||
.map((term) => ({ [Op.iLike]: `%${term}%` })),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {},
|
||||
},
|
||||
];
|
||||
|
||||
if (filter) {
|
||||
@ -226,6 +278,17 @@ module.exports = class EmployeesDBApi {
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.password_hash) {
|
||||
where = {
|
||||
...where,
|
||||
[Op.and]: Utils.ilike(
|
||||
'employees',
|
||||
'password_hash',
|
||||
filter.password_hash,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.active !== undefined) {
|
||||
where = {
|
||||
...where,
|
||||
|
||||
289
backend/src/db/api/notification_logs.js
Normal file
289
backend/src/db/api/notification_logs.js
Normal file
@ -0,0 +1,289 @@
|
||||
const db = require('../models');
|
||||
const FileDBApi = require('./file');
|
||||
const crypto = require('crypto');
|
||||
const Utils = require('../utils');
|
||||
|
||||
const Sequelize = db.Sequelize;
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
module.exports = class Notification_logsDBApi {
|
||||
static async create(data, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const notification_logs = await db.notification_logs.create(
|
||||
{
|
||||
id: data.id || undefined,
|
||||
|
||||
importHash: data.importHash || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await notification_logs.setEmployee(data.employee || null, {
|
||||
transaction,
|
||||
});
|
||||
|
||||
return notification_logs;
|
||||
}
|
||||
|
||||
static async bulkImport(data, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
// Prepare data - wrapping individual data transformations in a map() method
|
||||
const notification_logsData = data.map((item, index) => ({
|
||||
id: item.id || undefined,
|
||||
|
||||
importHash: item.importHash || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
createdAt: new Date(Date.now() + index * 1000),
|
||||
}));
|
||||
|
||||
// Bulk create items
|
||||
const notification_logs = await db.notification_logs.bulkCreate(
|
||||
notification_logsData,
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
// For each item created, replace relation files
|
||||
|
||||
return notification_logs;
|
||||
}
|
||||
|
||||
static async update(id, data, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const notification_logs = await db.notification_logs.findByPk(
|
||||
id,
|
||||
{},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
const updatePayload = {};
|
||||
|
||||
updatePayload.updatedById = currentUser.id;
|
||||
|
||||
await notification_logs.update(updatePayload, { transaction });
|
||||
|
||||
if (data.employee !== undefined) {
|
||||
await notification_logs.setEmployee(
|
||||
data.employee,
|
||||
|
||||
{ transaction },
|
||||
);
|
||||
}
|
||||
|
||||
return notification_logs;
|
||||
}
|
||||
|
||||
static async deleteByIds(ids, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const notification_logs = await db.notification_logs.findAll({
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: ids,
|
||||
},
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
|
||||
await db.sequelize.transaction(async (transaction) => {
|
||||
for (const record of notification_logs) {
|
||||
await record.update({ deletedBy: currentUser.id }, { transaction });
|
||||
}
|
||||
for (const record of notification_logs) {
|
||||
await record.destroy({ transaction });
|
||||
}
|
||||
});
|
||||
|
||||
return notification_logs;
|
||||
}
|
||||
|
||||
static async remove(id, options) {
|
||||
const currentUser = (options && options.currentUser) || { id: null };
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const notification_logs = await db.notification_logs.findByPk(id, options);
|
||||
|
||||
await notification_logs.update(
|
||||
{
|
||||
deletedBy: currentUser.id,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
await notification_logs.destroy({
|
||||
transaction,
|
||||
});
|
||||
|
||||
return notification_logs;
|
||||
}
|
||||
|
||||
static async findBy(where, options) {
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
const notification_logs = await db.notification_logs.findOne(
|
||||
{ where },
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
if (!notification_logs) {
|
||||
return notification_logs;
|
||||
}
|
||||
|
||||
const output = notification_logs.get({ plain: true });
|
||||
|
||||
output.employee = await notification_logs.getEmployee({
|
||||
transaction,
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static async findAll(filter, options) {
|
||||
const limit = filter.limit || 0;
|
||||
let offset = 0;
|
||||
let where = {};
|
||||
const currentPage = +filter.page;
|
||||
|
||||
offset = currentPage * limit;
|
||||
|
||||
const orderBy = null;
|
||||
|
||||
const transaction = (options && options.transaction) || undefined;
|
||||
|
||||
let include = [
|
||||
{
|
||||
model: db.employees,
|
||||
as: 'employee',
|
||||
|
||||
where: filter.employee
|
||||
? {
|
||||
[Op.or]: [
|
||||
{
|
||||
id: {
|
||||
[Op.in]: filter.employee
|
||||
.split('|')
|
||||
.map((term) => Utils.uuid(term)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: {
|
||||
[Op.or]: filter.employee
|
||||
.split('|')
|
||||
.map((term) => ({ [Op.iLike]: `%${term}%` })),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {},
|
||||
},
|
||||
];
|
||||
|
||||
if (filter) {
|
||||
if (filter.id) {
|
||||
where = {
|
||||
...where,
|
||||
['id']: Utils.uuid(filter.id),
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.active !== undefined) {
|
||||
where = {
|
||||
...where,
|
||||
active: filter.active === true || filter.active === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.createdAtRange) {
|
||||
const [start, end] = filter.createdAtRange;
|
||||
|
||||
if (start !== undefined && start !== null && start !== '') {
|
||||
where = {
|
||||
...where,
|
||||
['createdAt']: {
|
||||
...where.createdAt,
|
||||
[Op.gte]: start,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (end !== undefined && end !== null && end !== '') {
|
||||
where = {
|
||||
...where,
|
||||
['createdAt']: {
|
||||
...where.createdAt,
|
||||
[Op.lte]: end,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const queryOptions = {
|
||||
where,
|
||||
include,
|
||||
distinct: true,
|
||||
order:
|
||||
filter.field && filter.sort
|
||||
? [[filter.field, filter.sort]]
|
||||
: [['createdAt', 'desc']],
|
||||
transaction: options?.transaction,
|
||||
logging: console.log,
|
||||
};
|
||||
|
||||
if (!options?.countOnly) {
|
||||
queryOptions.limit = limit ? Number(limit) : undefined;
|
||||
queryOptions.offset = offset ? Number(offset) : undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows, count } = await db.notification_logs.findAndCountAll(
|
||||
queryOptions,
|
||||
);
|
||||
|
||||
return {
|
||||
rows: options?.countOnly ? [] : rows,
|
||||
count: count,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error executing query:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async findAllAutocomplete(query, limit, offset) {
|
||||
let where = {};
|
||||
|
||||
if (query) {
|
||||
where = {
|
||||
[Op.or]: [
|
||||
{ ['id']: Utils.uuid(query) },
|
||||
Utils.ilike('notification_logs', 'id', query),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const records = await db.notification_logs.findAll({
|
||||
attributes: ['id', 'id'],
|
||||
where,
|
||||
limit: limit ? Number(limit) : undefined,
|
||||
offset: offset ? Number(offset) : undefined,
|
||||
orderBy: [['id', 'ASC']],
|
||||
});
|
||||
|
||||
return records.map((record) => ({
|
||||
id: record.id,
|
||||
label: record.id,
|
||||
}));
|
||||
}
|
||||
};
|
||||
@ -141,6 +141,10 @@ module.exports = class RolesDBApi {
|
||||
transaction,
|
||||
});
|
||||
|
||||
output.employees_role = await roles.getEmployees_role({
|
||||
transaction,
|
||||
});
|
||||
|
||||
output.permissions = await roles.getPermissions({
|
||||
transaction,
|
||||
});
|
||||
|
||||
49
backend/src/db/migrations/1746445548702.js
Normal file
49
backend/src/db/migrations/1746445548702.js
Normal file
@ -0,0 +1,49 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async up(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.addColumn(
|
||||
'employees',
|
||||
'password_hash',
|
||||
{
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async down(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.removeColumn('employees', 'password_hash', {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
};
|
||||
52
backend/src/db/migrations/1746445584795.js
Normal file
52
backend/src/db/migrations/1746445584795.js
Normal file
@ -0,0 +1,52 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async up(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.addColumn(
|
||||
'employees',
|
||||
'roleId',
|
||||
{
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
|
||||
references: {
|
||||
model: 'roles',
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async down(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.removeColumn('employees', 'roleId', { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
};
|
||||
54
backend/src/db/migrations/1746445630402.js
Normal file
54
backend/src/db/migrations/1746445630402.js
Normal file
@ -0,0 +1,54 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async up(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.addColumn(
|
||||
'notification_logs',
|
||||
'employeeId',
|
||||
{
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
|
||||
references: {
|
||||
model: 'employees',
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {QueryInterface} queryInterface
|
||||
* @param {Sequelize} Sequelize
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async down(queryInterface, Sequelize) {
|
||||
/**
|
||||
* @type {Transaction}
|
||||
*/
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.removeColumn('notification_logs', 'employeeId', {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -26,6 +26,10 @@ module.exports = function (sequelize, DataTypes) {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
password_hash: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
importHash: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
@ -50,6 +54,14 @@ module.exports = function (sequelize, DataTypes) {
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
db.employees.hasMany(db.notification_logs, {
|
||||
as: 'notification_logs_employee',
|
||||
foreignKey: {
|
||||
name: 'employeeId',
|
||||
},
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
//end loop
|
||||
|
||||
db.employees.belongsTo(db.departments, {
|
||||
@ -60,6 +72,14 @@ module.exports = function (sequelize, DataTypes) {
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
db.employees.belongsTo(db.roles, {
|
||||
as: 'role',
|
||||
foreignKey: {
|
||||
name: 'roleId',
|
||||
},
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
db.employees.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
|
||||
53
backend/src/db/models/notification_logs.js
Normal file
53
backend/src/db/models/notification_logs.js
Normal file
@ -0,0 +1,53 @@
|
||||
const config = require('../../config');
|
||||
const providers = config.providers;
|
||||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcrypt');
|
||||
const moment = require('moment');
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
const notification_logs = sequelize.define(
|
||||
'notification_logs',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
importHash: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
paranoid: true,
|
||||
freezeTableName: true,
|
||||
},
|
||||
);
|
||||
|
||||
notification_logs.associate = (db) => {
|
||||
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
|
||||
|
||||
//end loop
|
||||
|
||||
db.notification_logs.belongsTo(db.employees, {
|
||||
as: 'employee',
|
||||
foreignKey: {
|
||||
name: 'employeeId',
|
||||
},
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
db.notification_logs.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
|
||||
db.notification_logs.belongsTo(db.users, {
|
||||
as: 'updatedBy',
|
||||
});
|
||||
};
|
||||
|
||||
return notification_logs;
|
||||
};
|
||||
@ -64,6 +64,14 @@ module.exports = function (sequelize, DataTypes) {
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
db.roles.hasMany(db.employees, {
|
||||
as: 'employees_role',
|
||||
foreignKey: {
|
||||
name: 'roleId',
|
||||
},
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
//end loop
|
||||
|
||||
db.roles.belongsTo(db.users, {
|
||||
|
||||
@ -9,6 +9,8 @@ const PreRegistrations = db.pre_registrations;
|
||||
|
||||
const Visitors = db.visitors;
|
||||
|
||||
const NotificationLogs = db.notification_logs;
|
||||
|
||||
const DepartmentsData = [
|
||||
{
|
||||
name: 'HR',
|
||||
@ -21,6 +23,14 @@ const DepartmentsData = [
|
||||
{
|
||||
name: 'Admin',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Security',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'James Clerk Maxwell',
|
||||
},
|
||||
];
|
||||
|
||||
const EmployeesData = [
|
||||
@ -32,6 +42,10 @@ const EmployeesData = [
|
||||
phone: '5551234567',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
password_hash: 'Louis Pasteur',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
@ -42,6 +56,10 @@ const EmployeesData = [
|
||||
phone: '5552345678',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
password_hash: 'Emil Kraepelin',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
@ -52,6 +70,38 @@ const EmployeesData = [
|
||||
phone: '5553456789',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
password_hash: 'Max Born',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
name: 'David Red',
|
||||
|
||||
email: 'david.red@company.com',
|
||||
|
||||
phone: '5554567890',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
password_hash: 'Edward Teller',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Laura Yellow',
|
||||
|
||||
email: 'laura.yellow@company.com',
|
||||
|
||||
phone: '5555678901',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
password_hash: 'Isaac Newton',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
];
|
||||
|
||||
@ -61,7 +111,7 @@ const PreRegistrationsData = [
|
||||
|
||||
expected_check_in: new Date('2023-10-06T09:00:00Z'),
|
||||
|
||||
status: 'pending',
|
||||
status: 'cancelled',
|
||||
},
|
||||
|
||||
{
|
||||
@ -69,7 +119,7 @@ const PreRegistrationsData = [
|
||||
|
||||
expected_check_in: new Date('2023-10-07T10:00:00Z'),
|
||||
|
||||
status: 'checked-in',
|
||||
status: 'cancelled',
|
||||
},
|
||||
|
||||
{
|
||||
@ -77,6 +127,22 @@ const PreRegistrationsData = [
|
||||
|
||||
expected_check_in: new Date('2023-10-08T11:00:00Z'),
|
||||
|
||||
status: 'checked-in',
|
||||
},
|
||||
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
|
||||
expected_check_in: new Date('2023-10-09T12:00:00Z'),
|
||||
|
||||
status: 'cancelled',
|
||||
},
|
||||
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
|
||||
expected_check_in: new Date('2023-10-10T13:00:00Z'),
|
||||
|
||||
status: 'cancelled',
|
||||
},
|
||||
];
|
||||
@ -141,12 +207,78 @@ const VisitorsData = [
|
||||
|
||||
check_out_time: new Date('2023-10-03T15:30:00Z'),
|
||||
|
||||
status: 'checked-in',
|
||||
status: 'checked-out',
|
||||
|
||||
// type code here for "images" field
|
||||
|
||||
badge_id: 'V12347',
|
||||
},
|
||||
|
||||
{
|
||||
full_name: 'Bob Brown',
|
||||
|
||||
email: 'bob.brown@example.com',
|
||||
|
||||
phone: '2233445566',
|
||||
|
||||
purpose: 'Maintenance',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
check_in_time: new Date('2023-10-04T08:30:00Z'),
|
||||
|
||||
check_out_time: new Date('2023-10-04T09:30:00Z'),
|
||||
|
||||
status: 'checked-in',
|
||||
|
||||
// type code here for "images" field
|
||||
|
||||
badge_id: 'V12348',
|
||||
},
|
||||
|
||||
{
|
||||
full_name: 'Charlie Green',
|
||||
|
||||
email: 'charlie.green@example.com',
|
||||
|
||||
phone: '3344556677',
|
||||
|
||||
purpose: 'Training',
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
check_in_time: new Date('2023-10-05T13:00:00Z'),
|
||||
|
||||
check_out_time: new Date('2023-10-05T14:00:00Z'),
|
||||
|
||||
status: 'checked-in',
|
||||
|
||||
// type code here for "images" field
|
||||
|
||||
badge_id: 'V12349',
|
||||
},
|
||||
];
|
||||
|
||||
const NotificationLogsData = [
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
|
||||
{
|
||||
// type code here for "relation_one" field
|
||||
},
|
||||
];
|
||||
|
||||
// Similar logic for "relation_many"
|
||||
@ -184,6 +316,28 @@ async function associateEmployeeWithDepartment() {
|
||||
if (Employee2?.setDepartment) {
|
||||
await Employee2.setDepartment(relatedDepartment2);
|
||||
}
|
||||
|
||||
const relatedDepartment3 = await Departments.findOne({
|
||||
offset: Math.floor(Math.random() * (await Departments.count())),
|
||||
});
|
||||
const Employee3 = await Employees.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
});
|
||||
if (Employee3?.setDepartment) {
|
||||
await Employee3.setDepartment(relatedDepartment3);
|
||||
}
|
||||
|
||||
const relatedDepartment4 = await Departments.findOne({
|
||||
offset: Math.floor(Math.random() * (await Departments.count())),
|
||||
});
|
||||
const Employee4 = await Employees.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
});
|
||||
if (Employee4?.setDepartment) {
|
||||
await Employee4.setDepartment(relatedDepartment4);
|
||||
}
|
||||
}
|
||||
|
||||
async function associatePreRegistrationWithVisitor() {
|
||||
@ -219,6 +373,28 @@ async function associatePreRegistrationWithVisitor() {
|
||||
if (PreRegistration2?.setVisitor) {
|
||||
await PreRegistration2.setVisitor(relatedVisitor2);
|
||||
}
|
||||
|
||||
const relatedVisitor3 = await Visitors.findOne({
|
||||
offset: Math.floor(Math.random() * (await Visitors.count())),
|
||||
});
|
||||
const PreRegistration3 = await PreRegistrations.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
});
|
||||
if (PreRegistration3?.setVisitor) {
|
||||
await PreRegistration3.setVisitor(relatedVisitor3);
|
||||
}
|
||||
|
||||
const relatedVisitor4 = await Visitors.findOne({
|
||||
offset: Math.floor(Math.random() * (await Visitors.count())),
|
||||
});
|
||||
const PreRegistration4 = await PreRegistrations.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
});
|
||||
if (PreRegistration4?.setVisitor) {
|
||||
await PreRegistration4.setVisitor(relatedVisitor4);
|
||||
}
|
||||
}
|
||||
|
||||
async function associateVisitorWithHost() {
|
||||
@ -254,6 +430,85 @@ async function associateVisitorWithHost() {
|
||||
if (Visitor2?.setHost) {
|
||||
await Visitor2.setHost(relatedHost2);
|
||||
}
|
||||
|
||||
const relatedHost3 = await Employees.findOne({
|
||||
offset: Math.floor(Math.random() * (await Employees.count())),
|
||||
});
|
||||
const Visitor3 = await Visitors.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
});
|
||||
if (Visitor3?.setHost) {
|
||||
await Visitor3.setHost(relatedHost3);
|
||||
}
|
||||
|
||||
const relatedHost4 = await Employees.findOne({
|
||||
offset: Math.floor(Math.random() * (await Employees.count())),
|
||||
});
|
||||
const Visitor4 = await Visitors.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
});
|
||||
if (Visitor4?.setHost) {
|
||||
await Visitor4.setHost(relatedHost4);
|
||||
}
|
||||
}
|
||||
|
||||
async function associateNotificationLogWithEmployee() {
|
||||
const relatedEmployee0 = await Employees.findOne({
|
||||
offset: Math.floor(Math.random() * (await Employees.count())),
|
||||
});
|
||||
const NotificationLog0 = await NotificationLogs.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 0,
|
||||
});
|
||||
if (NotificationLog0?.setEmployee) {
|
||||
await NotificationLog0.setEmployee(relatedEmployee0);
|
||||
}
|
||||
|
||||
const relatedEmployee1 = await Employees.findOne({
|
||||
offset: Math.floor(Math.random() * (await Employees.count())),
|
||||
});
|
||||
const NotificationLog1 = await NotificationLogs.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 1,
|
||||
});
|
||||
if (NotificationLog1?.setEmployee) {
|
||||
await NotificationLog1.setEmployee(relatedEmployee1);
|
||||
}
|
||||
|
||||
const relatedEmployee2 = await Employees.findOne({
|
||||
offset: Math.floor(Math.random() * (await Employees.count())),
|
||||
});
|
||||
const NotificationLog2 = await NotificationLogs.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 2,
|
||||
});
|
||||
if (NotificationLog2?.setEmployee) {
|
||||
await NotificationLog2.setEmployee(relatedEmployee2);
|
||||
}
|
||||
|
||||
const relatedEmployee3 = await Employees.findOne({
|
||||
offset: Math.floor(Math.random() * (await Employees.count())),
|
||||
});
|
||||
const NotificationLog3 = await NotificationLogs.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 3,
|
||||
});
|
||||
if (NotificationLog3?.setEmployee) {
|
||||
await NotificationLog3.setEmployee(relatedEmployee3);
|
||||
}
|
||||
|
||||
const relatedEmployee4 = await Employees.findOne({
|
||||
offset: Math.floor(Math.random() * (await Employees.count())),
|
||||
});
|
||||
const NotificationLog4 = await NotificationLogs.findOne({
|
||||
order: [['id', 'ASC']],
|
||||
offset: 4,
|
||||
});
|
||||
if (NotificationLog4?.setEmployee) {
|
||||
await NotificationLog4.setEmployee(relatedEmployee4);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@ -266,6 +521,8 @@ module.exports = {
|
||||
|
||||
await Visitors.bulkCreate(VisitorsData);
|
||||
|
||||
await NotificationLogs.bulkCreate(NotificationLogsData);
|
||||
|
||||
await Promise.all([
|
||||
// Similar logic for "relation_many"
|
||||
|
||||
@ -274,6 +531,8 @@ module.exports = {
|
||||
await associatePreRegistrationWithVisitor(),
|
||||
|
||||
await associateVisitorWithHost(),
|
||||
|
||||
await associateNotificationLogWithEmployee(),
|
||||
]);
|
||||
},
|
||||
|
||||
@ -285,5 +544,7 @@ module.exports = {
|
||||
await queryInterface.bulkDelete('pre_registrations', null, {});
|
||||
|
||||
await queryInterface.bulkDelete('visitors', null, {});
|
||||
|
||||
await queryInterface.bulkDelete('notification_logs', null, {});
|
||||
},
|
||||
};
|
||||
|
||||
@ -18,6 +18,7 @@ const pexelsRoutes = require('./routes/pexels');
|
||||
const openaiRoutes = require('./routes/openai');
|
||||
|
||||
const contactFormRoutes = require('./routes/contactForm');
|
||||
const staffRoutes = require('./routes/staff');
|
||||
|
||||
const usersRoutes = require('./routes/users');
|
||||
|
||||
@ -147,6 +148,7 @@ app.use(
|
||||
);
|
||||
|
||||
app.use('/api/contact-form', contactFormRoutes);
|
||||
app.use('/api/staff', staffRoutes);
|
||||
|
||||
app.use(
|
||||
'/api/search',
|
||||
|
||||
@ -29,6 +29,9 @@ router.use(checkCrudPermissions('employees'));
|
||||
* phone:
|
||||
* type: string
|
||||
* default: phone
|
||||
* password_hash:
|
||||
* type: string
|
||||
* default: password_hash
|
||||
|
||||
*/
|
||||
|
||||
@ -310,7 +313,7 @@ router.get(
|
||||
const currentUser = req.currentUser;
|
||||
const payload = await EmployeesDBApi.findAll(req.query, { currentUser });
|
||||
if (filetype && filetype === 'csv') {
|
||||
const fields = ['id', 'name', 'email', 'phone'];
|
||||
const fields = ['id', 'name', 'email', 'phone', 'password_hash'];
|
||||
const opts = { fields };
|
||||
try {
|
||||
const csv = parse(payload.rows, opts);
|
||||
|
||||
97
backend/src/routes/staff.js
Normal file
97
backend/src/routes/staff.js
Normal file
@ -0,0 +1,97 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { Employee } = require('../db/models');
|
||||
const { Visitor } = require('../db/models');
|
||||
const { NotificationLog } = require('../db/models');
|
||||
const authMiddleware = require('../middlewares/auth');
|
||||
|
||||
// Helper to generate JWT
|
||||
function generateToken(employee) {
|
||||
const payload = { id: employee.id, role: employee.role };
|
||||
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '8h' });
|
||||
}
|
||||
|
||||
// POST /staff/login
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
const employee = await Employee.findOne({ where: { email } });
|
||||
if (!employee) return res.status(401).json({ message: 'Invalid credentials' });
|
||||
const valid = await bcrypt.compare(password, employee.password_hash);
|
||||
if (!valid) return res.status(401).json({ message: 'Invalid credentials' });
|
||||
const token = generateToken(employee);
|
||||
res.json({ token, employee: { id: employee.id, name: employee.name, email: employee.email, role: employee.role } });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Protect below routes
|
||||
router.use(authMiddleware);
|
||||
|
||||
// GET /staff/visitors
|
||||
router.get('/visitors', async (req, res) => {
|
||||
try {
|
||||
const visitors = await Visitor.findAll({ where: { assigned_employee_id: req.user.id } });
|
||||
res.json(visitors);
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /staff/visitors/:id/approve
|
||||
router.post('/visitors/:id/approve', async (req, res) => {
|
||||
try {
|
||||
const visitor = await Visitor.findByPk(req.params.id);
|
||||
if (!visitor) return res.status(404).json({ message: 'Not found' });
|
||||
visitor.status = 'approved';
|
||||
await visitor.save();
|
||||
// Log notification
|
||||
await NotificationLog.create({ employee_id: req.user.id, message: `Visitor ${visitor.full_name} approved.` });
|
||||
res.json(visitor);
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /staff/visitors/:id/reject
|
||||
router.post('/visitors/:id/reject', async (req, res) => {
|
||||
try {
|
||||
const visitor = await Visitor.findByPk(req.params.id);
|
||||
if (!visitor) return res.status(404).json({ message: 'Not found' });
|
||||
visitor.status = 'rejected';
|
||||
await visitor.save();
|
||||
await NotificationLog.create({ employee_id: req.user.id, message: `Visitor ${visitor.full_name} rejected.` });
|
||||
res.json(visitor);
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /staff/history
|
||||
router.get('/history', async (req, res) => {
|
||||
try {
|
||||
const history = await Visitor.findAll({
|
||||
where: { assigned_employee_id: req.user.id, status: ['approved', 'rejected', 'checked-out'] },
|
||||
order: [['check_in_time', 'DESC']],
|
||||
});
|
||||
res.json(history);
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /staff/notifications
|
||||
router.get('/notifications', async (req, res) => {
|
||||
try {
|
||||
const notes = await NotificationLog.findAll({ where: { employee_id: req.user.id }, order: [['created_at', 'DESC']], limit: 20 });
|
||||
res.json(notes);
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@ -45,7 +45,7 @@ module.exports = class SearchService {
|
||||
|
||||
departments: ['name'],
|
||||
|
||||
employees: ['name', 'email', 'phone'],
|
||||
employees: ['name', 'email', 'phone', 'password_hash'],
|
||||
|
||||
visitors: ['full_name', 'email', 'phone', 'purpose', 'badge_id'],
|
||||
};
|
||||
|
||||
1
frontend/json/runtimeError.json
Normal file
1
frontend/json/runtimeError.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
@ -109,6 +109,26 @@ const CardEmployees = ({
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between gap-x-4 py-3'>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>
|
||||
Password hash
|
||||
</dt>
|
||||
<dd className='flex items-start gap-x-2'>
|
||||
<div className='font-medium line-clamp-4'>
|
||||
{item.password_hash}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between gap-x-4 py-3'>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>Role</dt>
|
||||
<dd className='flex items-start gap-x-2'>
|
||||
<div className='font-medium line-clamp-4'>
|
||||
{dataFormatter.rolesOneListFormatter(item.role)}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@ -78,6 +78,18 @@ const ListEmployees = ({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={'flex-1 px-3'}>
|
||||
<p className={'text-xs text-gray-500 '}>Password hash</p>
|
||||
<p className={'line-clamp-2'}>{item.password_hash}</p>
|
||||
</div>
|
||||
|
||||
<div className={'flex-1 px-3'}>
|
||||
<p className={'text-xs text-gray-500 '}>Role</p>
|
||||
<p className={'line-clamp-2'}>
|
||||
{dataFormatter.rolesOneListFormatter(item.role)}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<ListActionsPopover
|
||||
onDelete={onDelete}
|
||||
|
||||
@ -94,6 +94,38 @@ export const loadColumns = async (
|
||||
params?.value?.id ?? params?.value,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'password_hash',
|
||||
headerName: 'Password hash',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'role',
|
||||
headerName: 'Role',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
sortable: false,
|
||||
type: 'singleSelect',
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('roles'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
|
||||
@ -0,0 +1,112 @@
|
||||
import React from 'react';
|
||||
import ImageField from '../ImageField';
|
||||
import ListActionsPopover from '../ListActionsPopover';
|
||||
import { useAppSelector } from '../../stores/hooks';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import { Pagination } from '../Pagination';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import LoadingSpinner from '../LoadingSpinner';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { hasPermission } from '../../helpers/userPermissions';
|
||||
|
||||
type Props = {
|
||||
notification_logs: any[];
|
||||
loading: boolean;
|
||||
onDelete: (id: string) => void;
|
||||
currentPage: number;
|
||||
numPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
};
|
||||
|
||||
const CardNotification_logs = ({
|
||||
notification_logs,
|
||||
loading,
|
||||
onDelete,
|
||||
currentPage,
|
||||
numPages,
|
||||
onPageChange,
|
||||
}: Props) => {
|
||||
const asideScrollbarsStyle = useAppSelector(
|
||||
(state) => state.style.asideScrollbarsStyle,
|
||||
);
|
||||
const bgColor = useAppSelector((state) => state.style.cardsColor);
|
||||
const darkMode = useAppSelector((state) => state.style.darkMode);
|
||||
const corners = useAppSelector((state) => state.style.corners);
|
||||
const focusRing = useAppSelector((state) => state.style.focusRingColor);
|
||||
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
||||
const hasUpdatePermission = hasPermission(
|
||||
currentUser,
|
||||
'UPDATE_NOTIFICATION_LOGS',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'p-4'}>
|
||||
{loading && <LoadingSpinner />}
|
||||
<ul
|
||||
role='list'
|
||||
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
|
||||
>
|
||||
{!loading &&
|
||||
notification_logs.map((item, index) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={`overflow-hidden ${
|
||||
corners !== 'rounded-full' ? corners : 'rounded-3xl'
|
||||
} border ${focusRing} border-gray-200 dark:border-dark-700 ${
|
||||
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center ${bgColor} p-6 gap-x-4 border-b border-gray-900/5 bg-gray-50 dark:bg-dark-800 relative`}
|
||||
>
|
||||
<Link
|
||||
href={`/notification_logs/notification_logs-view/?id=${item.id}`}
|
||||
className='text-lg font-bold leading-6 line-clamp-1'
|
||||
>
|
||||
{item.id}
|
||||
</Link>
|
||||
|
||||
<div className='ml-auto '>
|
||||
<ListActionsPopover
|
||||
onDelete={onDelete}
|
||||
itemId={item.id}
|
||||
pathEdit={`/notification_logs/notification_logs-edit/?id=${item.id}`}
|
||||
pathView={`/notification_logs/notification_logs-view/?id=${item.id}`}
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<dl className='divide-y divide-stone-300 dark:divide-dark-700 px-6 py-4 text-sm leading-6 h-64 overflow-y-auto'>
|
||||
<div className='flex justify-between gap-x-4 py-3'>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>
|
||||
Employee
|
||||
</dt>
|
||||
<dd className='flex items-start gap-x-2'>
|
||||
<div className='font-medium line-clamp-4'>
|
||||
{dataFormatter.employeesOneListFormatter(item.employee)}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</li>
|
||||
))}
|
||||
{!loading && notification_logs.length === 0 && (
|
||||
<div className='col-span-full flex items-center justify-center h-40'>
|
||||
<p className=''>No data to display</p>
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
<div className={'flex items-center justify-center my-6'}>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
numPages={numPages}
|
||||
setCurrentPage={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardNotification_logs;
|
||||
@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import CardBox from '../CardBox';
|
||||
import ImageField from '../ImageField';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import ListActionsPopover from '../ListActionsPopover';
|
||||
import { useAppSelector } from '../../stores/hooks';
|
||||
import { Pagination } from '../Pagination';
|
||||
import LoadingSpinner from '../LoadingSpinner';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { hasPermission } from '../../helpers/userPermissions';
|
||||
|
||||
type Props = {
|
||||
notification_logs: any[];
|
||||
loading: boolean;
|
||||
onDelete: (id: string) => void;
|
||||
currentPage: number;
|
||||
numPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
};
|
||||
|
||||
const ListNotification_logs = ({
|
||||
notification_logs,
|
||||
loading,
|
||||
onDelete,
|
||||
currentPage,
|
||||
numPages,
|
||||
onPageChange,
|
||||
}: Props) => {
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
||||
const hasUpdatePermission = hasPermission(
|
||||
currentUser,
|
||||
'UPDATE_NOTIFICATION_LOGS',
|
||||
);
|
||||
|
||||
const corners = useAppSelector((state) => state.style.corners);
|
||||
const bgColor = useAppSelector((state) => state.style.cardsColor);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='relative overflow-x-auto p-4 space-y-4'>
|
||||
{loading && <LoadingSpinner />}
|
||||
{!loading &&
|
||||
notification_logs.map((item) => (
|
||||
<CardBox
|
||||
hasTable
|
||||
isList
|
||||
key={item.id}
|
||||
className={'rounded shadow-none'}
|
||||
>
|
||||
<div
|
||||
className={`flex rounded dark:bg-dark-900 border border-stone-300 items-center overflow-hidden`}
|
||||
>
|
||||
<Link
|
||||
href={`/notification_logs/notification_logs-view/?id=${item.id}`}
|
||||
className={
|
||||
'flex-1 px-4 py-6 h-24 flex divide-x-2 divide-stone-300 items-center overflow-hidden`}> dark:divide-dark-700 overflow-x-auto'
|
||||
}
|
||||
>
|
||||
<div className={'flex-1 px-3'}>
|
||||
<p className={'text-xs text-gray-500 '}>Employee</p>
|
||||
<p className={'line-clamp-2'}>
|
||||
{dataFormatter.employeesOneListFormatter(item.employee)}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<ListActionsPopover
|
||||
onDelete={onDelete}
|
||||
itemId={item.id}
|
||||
pathEdit={`/notification_logs/notification_logs-edit/?id=${item.id}`}
|
||||
pathView={`/notification_logs/notification_logs-view/?id=${item.id}`}
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
/>
|
||||
</div>
|
||||
</CardBox>
|
||||
))}
|
||||
{!loading && notification_logs.length === 0 && (
|
||||
<div className='col-span-full flex items-center justify-center h-40'>
|
||||
<p className=''>No data to display</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={'flex items-center justify-center my-6'}>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
numPages={numPages}
|
||||
setCurrentPage={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListNotification_logs;
|
||||
@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import BaseIcon from '../BaseIcon';
|
||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import DataGridMultiSelect from '../DataGridMultiSelect';
|
||||
import ListActionsPopover from '../ListActionsPopover';
|
||||
|
||||
import { hasPermission } from '../../helpers/userPermissions';
|
||||
|
||||
type Params = (id: string) => void;
|
||||
|
||||
export const loadColumns = async (
|
||||
onDelete: Params,
|
||||
entityName: string,
|
||||
|
||||
user,
|
||||
) => {
|
||||
async function callOptionsApi(entityName: string) {
|
||||
if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return [];
|
||||
|
||||
try {
|
||||
const data = await axios(`/${entityName}/autocomplete?limit=100`);
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_NOTIFICATION_LOGS');
|
||||
|
||||
return [
|
||||
{
|
||||
field: 'employee',
|
||||
headerName: 'Employee',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
sortable: false,
|
||||
type: 'singleSelect',
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('employees'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
minWidth: 30,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
getActions: (params: GridRowParams) => {
|
||||
return [
|
||||
<ListActionsPopover
|
||||
onDelete={onDelete}
|
||||
itemId={params?.row?.id}
|
||||
pathEdit={`/notification_logs/notification_logs-edit/?id=${params?.row?.id}`}
|
||||
pathView={`/notification_logs/notification_logs-view/?id=${params?.row?.id}`}
|
||||
key={1}
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
/>,
|
||||
];
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
@ -18,9 +18,9 @@ export default function WebSiteFooter({
|
||||
const borders = useAppSelector((state) => state.style.borders);
|
||||
const websiteHeder = useAppSelector((state) => state.style.websiteHeder);
|
||||
|
||||
const style = FooterStyle.WITH_PAGES;
|
||||
const style = FooterStyle.WITH_PROJECT_NAME;
|
||||
|
||||
const design = FooterDesigns.DESIGN_DIVERSITY;
|
||||
const design = FooterDesigns.DEFAULT_DESIGN;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -1,3 +1,15 @@
|
||||
/* Theme variables */
|
||||
:root {
|
||||
--color-primary: #007BFF;
|
||||
--color-secondary: #6C757D;
|
||||
--color-success: #28A745;
|
||||
--color-danger: #DC3545;
|
||||
--color-warning: #FFC107;
|
||||
--color-gray-light: #F8F9FA;
|
||||
--color-gray-lighter: #E9ECEF;
|
||||
}
|
||||
|
||||
|
||||
.theme-pink {
|
||||
.app-sidebar {
|
||||
@apply bg-pavitra-900 text-white;
|
||||
|
||||
@ -71,7 +71,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const title = 'VMS';
|
||||
const title = 'Aspect Visiter Management System Demo';
|
||||
|
||||
const description = 'VMS generated by Flatlogic';
|
||||
|
||||
|
||||
@ -74,6 +74,8 @@ const DepartmentsView = () => {
|
||||
<th>Email</th>
|
||||
|
||||
<th>Phone</th>
|
||||
|
||||
<th>Password hash</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -93,6 +95,10 @@ const DepartmentsView = () => {
|
||||
<td data-label='email'>{item.email}</td>
|
||||
|
||||
<td data-label='phone'>{item.phone}</td>
|
||||
|
||||
<td data-label='password_hash'>
|
||||
{item.password_hash}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@ -43,6 +43,10 @@ const EditEmployees = () => {
|
||||
phone: '',
|
||||
|
||||
department: null,
|
||||
|
||||
password_hash: '',
|
||||
|
||||
role: null,
|
||||
};
|
||||
const [initialValues, setInitialValues] = useState(initVals);
|
||||
|
||||
@ -120,6 +124,21 @@ const EditEmployees = () => {
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Password hash'>
|
||||
<Field name='password_hash' placeholder='Password hash' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Role' labelFor='role'>
|
||||
<Field
|
||||
name='role'
|
||||
id='role'
|
||||
component={SelectField}
|
||||
options={initialValues.role}
|
||||
itemRef={'roles'}
|
||||
showField={'name'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
|
||||
@ -43,6 +43,10 @@ const EditEmployeesPage = () => {
|
||||
phone: '',
|
||||
|
||||
department: null,
|
||||
|
||||
password_hash: '',
|
||||
|
||||
role: null,
|
||||
};
|
||||
const [initialValues, setInitialValues] = useState(initVals);
|
||||
|
||||
@ -118,6 +122,21 @@ const EditEmployeesPage = () => {
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Password hash'>
|
||||
<Field name='password_hash' placeholder='Password hash' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Role' labelFor='role'>
|
||||
<Field
|
||||
name='role'
|
||||
id='role'
|
||||
component={SelectField}
|
||||
options={initialValues.role}
|
||||
itemRef={'roles'}
|
||||
showField={'name'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
|
||||
@ -32,8 +32,11 @@ const EmployeesTablesPage = () => {
|
||||
{ label: 'Name', title: 'name' },
|
||||
{ label: 'Email', title: 'email' },
|
||||
{ label: 'Phone', title: 'phone' },
|
||||
{ label: 'Password hash', title: 'password_hash' },
|
||||
|
||||
{ label: 'Department', title: 'department' },
|
||||
|
||||
{ label: 'Role', title: 'role' },
|
||||
]);
|
||||
|
||||
const hasCreatePermission =
|
||||
|
||||
@ -40,6 +40,10 @@ const initialValues = {
|
||||
phone: '',
|
||||
|
||||
department: '',
|
||||
|
||||
password_hash: '',
|
||||
|
||||
role: '',
|
||||
};
|
||||
|
||||
const EmployeesNew = () => {
|
||||
@ -91,6 +95,20 @@ const EmployeesNew = () => {
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Password hash'>
|
||||
<Field name='password_hash' placeholder='Password hash' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Role' labelFor='role'>
|
||||
<Field
|
||||
name='role'
|
||||
id='role'
|
||||
component={SelectField}
|
||||
options={[]}
|
||||
itemRef={'roles'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
|
||||
@ -32,8 +32,11 @@ const EmployeesTablesPage = () => {
|
||||
{ label: 'Name', title: 'name' },
|
||||
{ label: 'Email', title: 'email' },
|
||||
{ label: 'Phone', title: 'phone' },
|
||||
{ label: 'Password hash', title: 'password_hash' },
|
||||
|
||||
{ label: 'Department', title: 'department' },
|
||||
|
||||
{ label: 'Role', title: 'role' },
|
||||
]);
|
||||
|
||||
const hasCreatePermission =
|
||||
|
||||
@ -75,6 +75,17 @@ const EmployeesView = () => {
|
||||
<p>{employees?.department?.name ?? 'No data'}</p>
|
||||
</div>
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Password hash</p>
|
||||
<p>{employees?.password_hash}</p>
|
||||
</div>
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Role</p>
|
||||
|
||||
<p>{employees?.role?.name ?? 'No data'}</p>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<p className={'block font-bold mb-2'}>Visitors Host</p>
|
||||
<CardBox
|
||||
@ -148,6 +159,39 @@ const EmployeesView = () => {
|
||||
</CardBox>
|
||||
</>
|
||||
|
||||
<>
|
||||
<p className={'block font-bold mb-2'}>Notification_logs Employee</p>
|
||||
<CardBox
|
||||
className='mb-6 border border-gray-300 rounded overflow-hidden'
|
||||
hasTable
|
||||
>
|
||||
<div className='overflow-x-auto'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{employees.notification_logs_employee &&
|
||||
Array.isArray(employees.notification_logs_employee) &&
|
||||
employees.notification_logs_employee.map((item: any) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/notification_logs/notification_logs-view/?id=${item.id}`,
|
||||
)
|
||||
}
|
||||
></tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{!employees?.notification_logs_employee?.length && (
|
||||
<div className={'text-center py-4'}>No data</div>
|
||||
)}
|
||||
</CardBox>
|
||||
</>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButton
|
||||
|
||||
@ -50,7 +50,7 @@ export default function Login() {
|
||||
remember: true,
|
||||
});
|
||||
|
||||
const title = 'VMS';
|
||||
const title = 'Aspect Visiter Management System Demo';
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
|
||||
140
frontend/src/pages/notification_logs/[notification_logsId].tsx
Normal file
140
frontend/src/pages/notification_logs/[notification_logsId].tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import CardBox from '../../components/CardBox';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import { getPageTitle } from '../../config';
|
||||
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import FormField from '../../components/FormField';
|
||||
import BaseDivider from '../../components/BaseDivider';
|
||||
import BaseButtons from '../../components/BaseButtons';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import FormCheckRadio from '../../components/FormCheckRadio';
|
||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup';
|
||||
import FormFilePicker from '../../components/FormFilePicker';
|
||||
import FormImagePicker from '../../components/FormImagePicker';
|
||||
import { SelectField } from '../../components/SelectField';
|
||||
import { SelectFieldMany } from '../../components/SelectFieldMany';
|
||||
import { SwitchField } from '../../components/SwitchField';
|
||||
import { RichTextField } from '../../components/RichTextField';
|
||||
|
||||
import {
|
||||
update,
|
||||
fetch,
|
||||
} from '../../stores/notification_logs/notification_logsSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import { useRouter } from 'next/router';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import ImageField from '../../components/ImageField';
|
||||
|
||||
const EditNotification_logs = () => {
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const initVals = {
|
||||
employee: null,
|
||||
};
|
||||
const [initialValues, setInitialValues] = useState(initVals);
|
||||
|
||||
const { notification_logs } = useAppSelector(
|
||||
(state) => state.notification_logs,
|
||||
);
|
||||
|
||||
const { notification_logsId } = router.query;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id: notification_logsId }));
|
||||
}, [notification_logsId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof notification_logs === 'object') {
|
||||
setInitialValues(notification_logs);
|
||||
}
|
||||
}, [notification_logs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof notification_logs === 'object') {
|
||||
const newInitialVal = { ...initVals };
|
||||
|
||||
Object.keys(initVals).forEach(
|
||||
(el) => (newInitialVal[el] = notification_logs[el]),
|
||||
);
|
||||
|
||||
setInitialValues(newInitialVal);
|
||||
}
|
||||
}, [notification_logs]);
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(update({ id: notification_logsId, data }));
|
||||
await router.push('/notification_logs/notification_logs-list');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Edit notification_logs')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title={'Edit notification_logs'}
|
||||
main
|
||||
>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField label='Employee' labelFor='employee'>
|
||||
<Field
|
||||
name='employee'
|
||||
id='employee'
|
||||
component={SelectField}
|
||||
options={initialValues.employee}
|
||||
itemRef={'employees'}
|
||||
showField={'name'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
<BaseButton type='reset' color='info' outline label='Reset' />
|
||||
<BaseButton
|
||||
type='reset'
|
||||
color='danger'
|
||||
outline
|
||||
label='Cancel'
|
||||
onClick={() =>
|
||||
router.push('/notification_logs/notification_logs-list')
|
||||
}
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
EditNotification_logs.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'UPDATE_NOTIFICATION_LOGS'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditNotification_logs;
|
||||
138
frontend/src/pages/notification_logs/notification_logs-edit.tsx
Normal file
138
frontend/src/pages/notification_logs/notification_logs-edit.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import CardBox from '../../components/CardBox';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import { getPageTitle } from '../../config';
|
||||
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import FormField from '../../components/FormField';
|
||||
import BaseDivider from '../../components/BaseDivider';
|
||||
import BaseButtons from '../../components/BaseButtons';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import FormCheckRadio from '../../components/FormCheckRadio';
|
||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup';
|
||||
import FormFilePicker from '../../components/FormFilePicker';
|
||||
import FormImagePicker from '../../components/FormImagePicker';
|
||||
import { SelectField } from '../../components/SelectField';
|
||||
import { SelectFieldMany } from '../../components/SelectFieldMany';
|
||||
import { SwitchField } from '../../components/SwitchField';
|
||||
import { RichTextField } from '../../components/RichTextField';
|
||||
|
||||
import {
|
||||
update,
|
||||
fetch,
|
||||
} from '../../stores/notification_logs/notification_logsSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import { useRouter } from 'next/router';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import ImageField from '../../components/ImageField';
|
||||
|
||||
const EditNotification_logsPage = () => {
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const initVals = {
|
||||
employee: null,
|
||||
};
|
||||
const [initialValues, setInitialValues] = useState(initVals);
|
||||
|
||||
const { notification_logs } = useAppSelector(
|
||||
(state) => state.notification_logs,
|
||||
);
|
||||
|
||||
const { id } = router.query;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id: id }));
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof notification_logs === 'object') {
|
||||
setInitialValues(notification_logs);
|
||||
}
|
||||
}, [notification_logs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof notification_logs === 'object') {
|
||||
const newInitialVal = { ...initVals };
|
||||
Object.keys(initVals).forEach(
|
||||
(el) => (newInitialVal[el] = notification_logs[el]),
|
||||
);
|
||||
setInitialValues(newInitialVal);
|
||||
}
|
||||
}, [notification_logs]);
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(update({ id: id, data }));
|
||||
await router.push('/notification_logs/notification_logs-list');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Edit notification_logs')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title={'Edit notification_logs'}
|
||||
main
|
||||
>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField label='Employee' labelFor='employee'>
|
||||
<Field
|
||||
name='employee'
|
||||
id='employee'
|
||||
component={SelectField}
|
||||
options={initialValues.employee}
|
||||
itemRef={'employees'}
|
||||
showField={'name'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
<BaseButton type='reset' color='info' outline label='Reset' />
|
||||
<BaseButton
|
||||
type='reset'
|
||||
color='danger'
|
||||
outline
|
||||
label='Cancel'
|
||||
onClick={() =>
|
||||
router.push('/notification_logs/notification_logs-list')
|
||||
}
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
EditNotification_logsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'UPDATE_NOTIFICATION_LOGS'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditNotification_logsPage;
|
||||
165
frontend/src/pages/notification_logs/notification_logs-list.tsx
Normal file
165
frontend/src/pages/notification_logs/notification_logs-list.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
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 TableNotification_logs from '../../components/Notification_logs/TableNotification_logs';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import axios from 'axios';
|
||||
import Link from 'next/link';
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import CardBoxModal from '../../components/CardBoxModal';
|
||||
import DragDropFilePicker from '../../components/DragDropFilePicker';
|
||||
import {
|
||||
setRefetch,
|
||||
uploadCsv,
|
||||
} from '../../stores/notification_logs/notification_logsSlice';
|
||||
|
||||
import { hasPermission } from '../../helpers/userPermissions';
|
||||
|
||||
const Notification_logsTablesPage = () => {
|
||||
const [filterItems, setFilterItems] = useState([]);
|
||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||
const [isModalActive, setIsModalActive] = useState(false);
|
||||
const [showTableView, setShowTableView] = useState(false);
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [filters] = useState([{ label: 'Employee', title: 'employee' }]);
|
||||
|
||||
const hasCreatePermission =
|
||||
currentUser && hasPermission(currentUser, 'CREATE_NOTIFICATION_LOGS');
|
||||
|
||||
const addFilter = () => {
|
||||
const newItem = {
|
||||
id: uniqueId(),
|
||||
fields: {
|
||||
filterValue: '',
|
||||
filterValueFrom: '',
|
||||
filterValueTo: '',
|
||||
selectedField: '',
|
||||
},
|
||||
};
|
||||
newItem.fields.selectedField = filters[0].title;
|
||||
setFilterItems([...filterItems, newItem]);
|
||||
};
|
||||
|
||||
const getNotification_logsCSV = async () => {
|
||||
const response = await axios({
|
||||
url: '/notification_logs?filetype=csv',
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
});
|
||||
const type = response.headers['content-type'];
|
||||
const blob = new Blob([response.data], { type: type });
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = 'notification_logsCSV.csv';
|
||||
link.click();
|
||||
};
|
||||
|
||||
const onModalConfirm = async () => {
|
||||
if (!csvFile) return;
|
||||
await dispatch(uploadCsv(csvFile));
|
||||
dispatch(setRefetch(true));
|
||||
setCsvFile(null);
|
||||
setIsModalActive(false);
|
||||
};
|
||||
|
||||
const onModalCancel = () => {
|
||||
setCsvFile(null);
|
||||
setIsModalActive(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Notification_logs')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title='Notification_logs'
|
||||
main
|
||||
>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
href={'/notification_logs/notification_logs-new'}
|
||||
color='info'
|
||||
label='New Item'
|
||||
/>
|
||||
)}
|
||||
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
color='info'
|
||||
label='Filter'
|
||||
onClick={addFilter}
|
||||
/>
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
color='info'
|
||||
label='Download CSV'
|
||||
onClick={getNotification_logsCSV}
|
||||
/>
|
||||
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Upload CSV'
|
||||
onClick={() => setIsModalActive(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='md:inline-flex items-center ms-auto'>
|
||||
<div id='delete-rows-button'></div>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
<CardBox className='mb-6' hasTable>
|
||||
<TableNotification_logs
|
||||
filterItems={filterItems}
|
||||
setFilterItems={setFilterItems}
|
||||
filters={filters}
|
||||
showGrid={false}
|
||||
/>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
<CardBoxModal
|
||||
title='Upload CSV'
|
||||
buttonColor='info'
|
||||
buttonLabel={'Confirm'}
|
||||
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
|
||||
isActive={isModalActive}
|
||||
onConfirm={onModalConfirm}
|
||||
onCancel={onModalCancel}
|
||||
>
|
||||
<DragDropFilePicker
|
||||
file={csvFile}
|
||||
setFile={setCsvFile}
|
||||
formats={'.csv'}
|
||||
/>
|
||||
</CardBoxModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Notification_logsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'READ_NOTIFICATION_LOGS'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification_logsTablesPage;
|
||||
106
frontend/src/pages/notification_logs/notification_logs-new.tsx
Normal file
106
frontend/src/pages/notification_logs/notification_logs-new.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiChartTimelineVariant,
|
||||
mdiMail,
|
||||
mdiUpload,
|
||||
} from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import React, { ReactElement } from 'react';
|
||||
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 { Field, Form, Formik } from 'formik';
|
||||
import FormField from '../../components/FormField';
|
||||
import BaseDivider from '../../components/BaseDivider';
|
||||
import BaseButtons from '../../components/BaseButtons';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import FormCheckRadio from '../../components/FormCheckRadio';
|
||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup';
|
||||
import FormFilePicker from '../../components/FormFilePicker';
|
||||
import FormImagePicker from '../../components/FormImagePicker';
|
||||
import { SwitchField } from '../../components/SwitchField';
|
||||
|
||||
import { SelectField } from '../../components/SelectField';
|
||||
import { SelectFieldMany } from '../../components/SelectFieldMany';
|
||||
import { RichTextField } from '../../components/RichTextField';
|
||||
|
||||
import { create } from '../../stores/notification_logs/notification_logsSlice';
|
||||
import { useAppDispatch } from '../../stores/hooks';
|
||||
import { useRouter } from 'next/router';
|
||||
import moment from 'moment';
|
||||
|
||||
const initialValues = {
|
||||
employee: '',
|
||||
};
|
||||
|
||||
const Notification_logsNew = () => {
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(create(data));
|
||||
await router.push('/notification_logs/notification_logs-list');
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('New Item')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title='New Item'
|
||||
main
|
||||
>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField label='Employee' labelFor='employee'>
|
||||
<Field
|
||||
name='employee'
|
||||
id='employee'
|
||||
component={SelectField}
|
||||
options={[]}
|
||||
itemRef={'employees'}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
<BaseButton type='reset' color='info' outline label='Reset' />
|
||||
<BaseButton
|
||||
type='reset'
|
||||
color='danger'
|
||||
outline
|
||||
label='Cancel'
|
||||
onClick={() =>
|
||||
router.push('/notification_logs/notification_logs-list')
|
||||
}
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Notification_logsNew.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'CREATE_NOTIFICATION_LOGS'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification_logsNew;
|
||||
164
frontend/src/pages/notification_logs/notification_logs-table.tsx
Normal file
164
frontend/src/pages/notification_logs/notification_logs-table.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
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 TableNotification_logs from '../../components/Notification_logs/TableNotification_logs';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import axios from 'axios';
|
||||
import Link from 'next/link';
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import CardBoxModal from '../../components/CardBoxModal';
|
||||
import DragDropFilePicker from '../../components/DragDropFilePicker';
|
||||
import {
|
||||
setRefetch,
|
||||
uploadCsv,
|
||||
} from '../../stores/notification_logs/notification_logsSlice';
|
||||
|
||||
import { hasPermission } from '../../helpers/userPermissions';
|
||||
|
||||
const Notification_logsTablesPage = () => {
|
||||
const [filterItems, setFilterItems] = useState([]);
|
||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
||||
const [isModalActive, setIsModalActive] = useState(false);
|
||||
const [showTableView, setShowTableView] = useState(false);
|
||||
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [filters] = useState([{ label: 'Employee', title: 'employee' }]);
|
||||
|
||||
const hasCreatePermission =
|
||||
currentUser && hasPermission(currentUser, 'CREATE_NOTIFICATION_LOGS');
|
||||
|
||||
const addFilter = () => {
|
||||
const newItem = {
|
||||
id: uniqueId(),
|
||||
fields: {
|
||||
filterValue: '',
|
||||
filterValueFrom: '',
|
||||
filterValueTo: '',
|
||||
selectedField: '',
|
||||
},
|
||||
};
|
||||
newItem.fields.selectedField = filters[0].title;
|
||||
setFilterItems([...filterItems, newItem]);
|
||||
};
|
||||
|
||||
const getNotification_logsCSV = async () => {
|
||||
const response = await axios({
|
||||
url: '/notification_logs?filetype=csv',
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
});
|
||||
const type = response.headers['content-type'];
|
||||
const blob = new Blob([response.data], { type: type });
|
||||
const link = document.createElement('a');
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = 'notification_logsCSV.csv';
|
||||
link.click();
|
||||
};
|
||||
|
||||
const onModalConfirm = async () => {
|
||||
if (!csvFile) return;
|
||||
await dispatch(uploadCsv(csvFile));
|
||||
dispatch(setRefetch(true));
|
||||
setCsvFile(null);
|
||||
setIsModalActive(false);
|
||||
};
|
||||
|
||||
const onModalCancel = () => {
|
||||
setCsvFile(null);
|
||||
setIsModalActive(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Notification_logs')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title='Notification_logs'
|
||||
main
|
||||
>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
href={'/notification_logs/notification_logs-new'}
|
||||
color='info'
|
||||
label='New Item'
|
||||
/>
|
||||
)}
|
||||
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
color='info'
|
||||
label='Filter'
|
||||
onClick={addFilter}
|
||||
/>
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
color='info'
|
||||
label='Download CSV'
|
||||
onClick={getNotification_logsCSV}
|
||||
/>
|
||||
|
||||
{hasCreatePermission && (
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Upload CSV'
|
||||
onClick={() => setIsModalActive(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='md:inline-flex items-center ms-auto'>
|
||||
<div id='delete-rows-button'></div>
|
||||
</div>
|
||||
</CardBox>
|
||||
<CardBox className='mb-6' hasTable>
|
||||
<TableNotification_logs
|
||||
filterItems={filterItems}
|
||||
setFilterItems={setFilterItems}
|
||||
filters={filters}
|
||||
showGrid={true}
|
||||
/>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
<CardBoxModal
|
||||
title='Upload CSV'
|
||||
buttonColor='info'
|
||||
buttonLabel={'Confirm'}
|
||||
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
|
||||
isActive={isModalActive}
|
||||
onConfirm={onModalConfirm}
|
||||
onCancel={onModalCancel}
|
||||
>
|
||||
<DragDropFilePicker
|
||||
file={csvFile}
|
||||
setFile={setCsvFile}
|
||||
formats={'.csv'}
|
||||
/>
|
||||
</CardBoxModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Notification_logsTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'READ_NOTIFICATION_LOGS'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification_logsTablesPage;
|
||||
@ -0,0 +1,88 @@
|
||||
import React, { ReactElement, useEffect } from 'react';
|
||||
import Head from 'next/head';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import dayjs from 'dayjs';
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import { useRouter } from 'next/router';
|
||||
import { fetch } from '../../stores/notification_logs/notification_logsSlice';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import ImageField from '../../components/ImageField';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import { getPageTitle } from '../../config';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import CardBox from '../../components/CardBox';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import BaseDivider from '../../components/BaseDivider';
|
||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||
import { SwitchField } from '../../components/SwitchField';
|
||||
import FormField from '../../components/FormField';
|
||||
|
||||
const Notification_logsView = () => {
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const { notification_logs } = useAppSelector(
|
||||
(state) => state.notification_logs,
|
||||
);
|
||||
|
||||
const { id } = router.query;
|
||||
|
||||
function removeLastCharacter(str) {
|
||||
console.log(str, `str`);
|
||||
return str.slice(0, -1);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('View notification_logs')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={mdiChartTimelineVariant}
|
||||
title={removeLastCharacter('View notification_logs')}
|
||||
main
|
||||
>
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Edit'
|
||||
href={`/notification_logs/notification_logs-edit/?id=${id}`}
|
||||
/>
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Employee</p>
|
||||
|
||||
<p>{notification_logs?.employee?.name ?? 'No data'}</p>
|
||||
</div>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButton
|
||||
color='info'
|
||||
label='Back'
|
||||
onClick={() =>
|
||||
router.push('/notification_logs/notification_logs-list')
|
||||
}
|
||||
/>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Notification_logsView.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission={'READ_NOTIFICATION_LOGS'}>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification_logsView;
|
||||
@ -149,6 +149,57 @@ const RolesView = () => {
|
||||
</CardBox>
|
||||
</>
|
||||
|
||||
<>
|
||||
<p className={'block font-bold mb-2'}>Employees Role</p>
|
||||
<CardBox
|
||||
className='mb-6 border border-gray-300 rounded overflow-hidden'
|
||||
hasTable
|
||||
>
|
||||
<div className='overflow-x-auto'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
||||
<th>Email</th>
|
||||
|
||||
<th>Phone</th>
|
||||
|
||||
<th>Password hash</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{roles.employees_role &&
|
||||
Array.isArray(roles.employees_role) &&
|
||||
roles.employees_role.map((item: any) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/employees/employees-view/?id=${item.id}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
<td data-label='name'>{item.name}</td>
|
||||
|
||||
<td data-label='email'>{item.email}</td>
|
||||
|
||||
<td data-label='phone'>{item.phone}</td>
|
||||
|
||||
<td data-label='password_hash'>
|
||||
{item.password_hash}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{!roles?.employees_role?.length && (
|
||||
<div className={'text-center py-4'}>No data</div>
|
||||
)}
|
||||
</CardBox>
|
||||
</>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButton
|
||||
|
||||
64
frontend/src/pages/staff-login.tsx
Normal file
64
frontend/src/pages/staff-login.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import axios from 'axios';
|
||||
|
||||
export default function StaffLogin() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const resp = await axios.post('/api/staff/login', { email, password });
|
||||
const { token } = resp.data;
|
||||
localStorage.setItem('staff-token', token);
|
||||
router.push('/staff/dashboard');
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.message || 'Login failed');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="bg-white p-8 rounded shadow-md w-full max-w-md">
|
||||
<h2 className="text-2xl font-semibold mb-6">Staff Login</h2>
|
||||
{error && <p className="text-red-600 mb-4">{error}</p>}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full border border-gray-300 p-2 rounded"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full border border-gray-300 p-2 rounded"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -144,7 +144,7 @@ export default function WebSite() {
|
||||
<FeaturesSection
|
||||
projectName={'VMS'}
|
||||
image={['Dashboard with visitor statistics']}
|
||||
withBg={0}
|
||||
withBg={1}
|
||||
features={features_points}
|
||||
mainText={`Discover Key Features of ${projectName}`}
|
||||
subTitle={`Enhance your visitor management with our innovative features designed to streamline operations and improve security.`}
|
||||
|
||||
@ -159,7 +159,7 @@ export default function WebSite() {
|
||||
|
||||
<PricingSection
|
||||
projectName={'VMS'}
|
||||
withBg={0}
|
||||
withBg={1}
|
||||
features={pricing_features}
|
||||
description={description}
|
||||
/>
|
||||
|
||||
@ -40,6 +40,16 @@ module.exports = {
|
||||
'fade-in': 'fade-in 250ms ease-in-out',
|
||||
},
|
||||
colors: {
|
||||
primary: 'var(--color-primary)',
|
||||
secondary: 'var(--color-secondary)',
|
||||
success: 'var(--color-success)',
|
||||
danger: 'var(--color-danger)',
|
||||
warning: 'var(--color-warning)',
|
||||
gray: {
|
||||
light: 'var(--color-gray-light)',
|
||||
lighter: 'var(--color-gray-lighter)',
|
||||
},
|
||||
|
||||
dark: {
|
||||
900: '#131618',
|
||||
800: '#21242A',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user