This commit is contained in:
Flatlogic Bot 2025-05-05 12:12:09 +00:00
parent d130092f78
commit 25962a2c05
45 changed files with 2377 additions and 15 deletions

5
.gitignore vendored
View File

@ -1,3 +1,8 @@
node_modules/ node_modules/
*/node_modules/ */node_modules/
*/build/ */build/
**/node_modules/
**/build/
.DS_Store
.env

File diff suppressed because one or more lines are too long

View File

@ -18,6 +18,7 @@ module.exports = class EmployeesDBApi {
name: data.name || null, name: data.name || null,
email: data.email || null, email: data.email || null,
phone: data.phone || null, phone: data.phone || null,
password_hash: data.password_hash || null,
importHash: data.importHash || null, importHash: data.importHash || null,
createdById: currentUser.id, createdById: currentUser.id,
updatedById: currentUser.id, updatedById: currentUser.id,
@ -29,6 +30,10 @@ module.exports = class EmployeesDBApi {
transaction, transaction,
}); });
await employees.setRole(data.role || null, {
transaction,
});
return employees; return employees;
} }
@ -43,6 +48,7 @@ module.exports = class EmployeesDBApi {
name: item.name || null, name: item.name || null,
email: item.email || null, email: item.email || null,
phone: item.phone || null, phone: item.phone || null,
password_hash: item.password_hash || null,
importHash: item.importHash || null, importHash: item.importHash || null,
createdById: currentUser.id, createdById: currentUser.id,
updatedById: currentUser.id, updatedById: currentUser.id,
@ -73,6 +79,9 @@ module.exports = class EmployeesDBApi {
if (data.phone !== undefined) updatePayload.phone = data.phone; if (data.phone !== undefined) updatePayload.phone = data.phone;
if (data.password_hash !== undefined)
updatePayload.password_hash = data.password_hash;
updatePayload.updatedById = currentUser.id; updatePayload.updatedById = currentUser.id;
await employees.update(updatePayload, { transaction }); 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; return employees;
} }
@ -150,10 +167,19 @@ module.exports = class EmployeesDBApi {
transaction, transaction,
}); });
output.notification_logs_employee =
await employees.getNotification_logs_employee({
transaction,
});
output.department = await employees.getDepartment({ output.department = await employees.getDepartment({
transaction, transaction,
}); });
output.role = await employees.getRole({
transaction,
});
return output; 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) { 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) { if (filter.active !== undefined) {
where = { where = {
...where, ...where,

View 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,
}));
}
};

View File

@ -141,6 +141,10 @@ module.exports = class RolesDBApi {
transaction, transaction,
}); });
output.employees_role = await roles.getEmployees_role({
transaction,
});
output.permissions = await roles.getPermissions({ output.permissions = await roles.getPermissions({
transaction, transaction,
}); });

View 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;
}
},
};

View 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;
}
},
};

View 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;
}
},
};

View File

@ -26,6 +26,10 @@ module.exports = function (sequelize, DataTypes) {
type: DataTypes.TEXT, type: DataTypes.TEXT,
}, },
password_hash: {
type: DataTypes.TEXT,
},
importHash: { importHash: {
type: DataTypes.STRING(255), type: DataTypes.STRING(255),
allowNull: true, allowNull: true,
@ -50,6 +54,14 @@ module.exports = function (sequelize, DataTypes) {
constraints: false, constraints: false,
}); });
db.employees.hasMany(db.notification_logs, {
as: 'notification_logs_employee',
foreignKey: {
name: 'employeeId',
},
constraints: false,
});
//end loop //end loop
db.employees.belongsTo(db.departments, { db.employees.belongsTo(db.departments, {
@ -60,6 +72,14 @@ module.exports = function (sequelize, DataTypes) {
constraints: false, constraints: false,
}); });
db.employees.belongsTo(db.roles, {
as: 'role',
foreignKey: {
name: 'roleId',
},
constraints: false,
});
db.employees.belongsTo(db.users, { db.employees.belongsTo(db.users, {
as: 'createdBy', as: 'createdBy',
}); });

View 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;
};

View File

@ -64,6 +64,14 @@ module.exports = function (sequelize, DataTypes) {
constraints: false, constraints: false,
}); });
db.roles.hasMany(db.employees, {
as: 'employees_role',
foreignKey: {
name: 'roleId',
},
constraints: false,
});
//end loop //end loop
db.roles.belongsTo(db.users, { db.roles.belongsTo(db.users, {

View File

@ -9,6 +9,8 @@ const PreRegistrations = db.pre_registrations;
const Visitors = db.visitors; const Visitors = db.visitors;
const NotificationLogs = db.notification_logs;
const DepartmentsData = [ const DepartmentsData = [
{ {
name: 'HR', name: 'HR',
@ -21,6 +23,14 @@ const DepartmentsData = [
{ {
name: 'Admin', name: 'Admin',
}, },
{
name: 'Security',
},
{
name: 'James Clerk Maxwell',
},
]; ];
const EmployeesData = [ const EmployeesData = [
@ -32,6 +42,10 @@ const EmployeesData = [
phone: '5551234567', phone: '5551234567',
// type code here for "relation_one" field // 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', phone: '5552345678',
// type code here for "relation_one" field // 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', phone: '5553456789',
// type code here for "relation_one" field // 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'), 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'), 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'), 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', status: 'cancelled',
}, },
]; ];
@ -141,12 +207,78 @@ const VisitorsData = [
check_out_time: new Date('2023-10-03T15:30:00Z'), check_out_time: new Date('2023-10-03T15:30:00Z'),
status: 'checked-in', status: 'checked-out',
// type code here for "images" field // type code here for "images" field
badge_id: 'V12347', 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" // Similar logic for "relation_many"
@ -184,6 +316,28 @@ async function associateEmployeeWithDepartment() {
if (Employee2?.setDepartment) { if (Employee2?.setDepartment) {
await Employee2.setDepartment(relatedDepartment2); 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() { async function associatePreRegistrationWithVisitor() {
@ -219,6 +373,28 @@ async function associatePreRegistrationWithVisitor() {
if (PreRegistration2?.setVisitor) { if (PreRegistration2?.setVisitor) {
await PreRegistration2.setVisitor(relatedVisitor2); 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() { async function associateVisitorWithHost() {
@ -254,6 +430,85 @@ async function associateVisitorWithHost() {
if (Visitor2?.setHost) { if (Visitor2?.setHost) {
await Visitor2.setHost(relatedHost2); 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 = { module.exports = {
@ -266,6 +521,8 @@ module.exports = {
await Visitors.bulkCreate(VisitorsData); await Visitors.bulkCreate(VisitorsData);
await NotificationLogs.bulkCreate(NotificationLogsData);
await Promise.all([ await Promise.all([
// Similar logic for "relation_many" // Similar logic for "relation_many"
@ -274,6 +531,8 @@ module.exports = {
await associatePreRegistrationWithVisitor(), await associatePreRegistrationWithVisitor(),
await associateVisitorWithHost(), await associateVisitorWithHost(),
await associateNotificationLogWithEmployee(),
]); ]);
}, },
@ -285,5 +544,7 @@ module.exports = {
await queryInterface.bulkDelete('pre_registrations', null, {}); await queryInterface.bulkDelete('pre_registrations', null, {});
await queryInterface.bulkDelete('visitors', null, {}); await queryInterface.bulkDelete('visitors', null, {});
await queryInterface.bulkDelete('notification_logs', null, {});
}, },
}; };

View File

@ -18,6 +18,7 @@ const pexelsRoutes = require('./routes/pexels');
const openaiRoutes = require('./routes/openai'); const openaiRoutes = require('./routes/openai');
const contactFormRoutes = require('./routes/contactForm'); const contactFormRoutes = require('./routes/contactForm');
const staffRoutes = require('./routes/staff');
const usersRoutes = require('./routes/users'); const usersRoutes = require('./routes/users');
@ -147,6 +148,7 @@ app.use(
); );
app.use('/api/contact-form', contactFormRoutes); app.use('/api/contact-form', contactFormRoutes);
app.use('/api/staff', staffRoutes);
app.use( app.use(
'/api/search', '/api/search',

View File

@ -29,6 +29,9 @@ router.use(checkCrudPermissions('employees'));
* phone: * phone:
* type: string * type: string
* default: phone * default: phone
* password_hash:
* type: string
* default: password_hash
*/ */
@ -310,7 +313,7 @@ router.get(
const currentUser = req.currentUser; const currentUser = req.currentUser;
const payload = await EmployeesDBApi.findAll(req.query, { currentUser }); const payload = await EmployeesDBApi.findAll(req.query, { currentUser });
if (filetype && filetype === 'csv') { if (filetype && filetype === 'csv') {
const fields = ['id', 'name', 'email', 'phone']; const fields = ['id', 'name', 'email', 'phone', 'password_hash'];
const opts = { fields }; const opts = { fields };
try { try {
const csv = parse(payload.rows, opts); const csv = parse(payload.rows, opts);

View 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;

View File

@ -45,7 +45,7 @@ module.exports = class SearchService {
departments: ['name'], departments: ['name'],
employees: ['name', 'email', 'phone'], employees: ['name', 'email', 'phone', 'password_hash'],
visitors: ['full_name', 'email', 'phone', 'purpose', 'badge_id'], visitors: ['full_name', 'email', 'phone', 'purpose', 'badge_id'],
}; };

View File

@ -0,0 +1 @@
{}

View File

@ -109,6 +109,26 @@ const CardEmployees = ({
</div> </div>
</dd> </dd>
</div> </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> </dl>
</li> </li>
))} ))}

View File

@ -78,6 +78,18 @@ const ListEmployees = ({
)} )}
</p> </p>
</div> </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> </Link>
<ListActionsPopover <ListActionsPopover
onDelete={onDelete} onDelete={onDelete}

View File

@ -94,6 +94,38 @@ export const loadColumns = async (
params?.value?.id ?? params?.value, 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', field: 'actions',
type: 'actions', type: 'actions',

View File

@ -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;

View File

@ -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;

View File

@ -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}
/>,
];
},
},
];
};

View File

@ -18,9 +18,9 @@ export default function WebSiteFooter({
const borders = useAppSelector((state) => state.style.borders); const borders = useAppSelector((state) => state.style.borders);
const websiteHeder = useAppSelector((state) => state.style.websiteHeder); 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 ( return (
<div <div

View File

@ -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 { .theme-pink {
.app-sidebar { .app-sidebar {
@apply bg-pavitra-900 text-white; @apply bg-pavitra-900 text-white;

View File

@ -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'; const description = 'VMS generated by Flatlogic';

View File

@ -74,6 +74,8 @@ const DepartmentsView = () => {
<th>Email</th> <th>Email</th>
<th>Phone</th> <th>Phone</th>
<th>Password hash</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -93,6 +95,10 @@ const DepartmentsView = () => {
<td data-label='email'>{item.email}</td> <td data-label='email'>{item.email}</td>
<td data-label='phone'>{item.phone}</td> <td data-label='phone'>{item.phone}</td>
<td data-label='password_hash'>
{item.password_hash}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@ -43,6 +43,10 @@ const EditEmployees = () => {
phone: '', phone: '',
department: null, department: null,
password_hash: '',
role: null,
}; };
const [initialValues, setInitialValues] = useState(initVals); const [initialValues, setInitialValues] = useState(initVals);
@ -120,6 +124,21 @@ const EditEmployees = () => {
></Field> ></Field>
</FormField> </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 /> <BaseDivider />
<BaseButtons> <BaseButtons>
<BaseButton type='submit' color='info' label='Submit' /> <BaseButton type='submit' color='info' label='Submit' />

View File

@ -43,6 +43,10 @@ const EditEmployeesPage = () => {
phone: '', phone: '',
department: null, department: null,
password_hash: '',
role: null,
}; };
const [initialValues, setInitialValues] = useState(initVals); const [initialValues, setInitialValues] = useState(initVals);
@ -118,6 +122,21 @@ const EditEmployeesPage = () => {
></Field> ></Field>
</FormField> </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 /> <BaseDivider />
<BaseButtons> <BaseButtons>
<BaseButton type='submit' color='info' label='Submit' /> <BaseButton type='submit' color='info' label='Submit' />

View File

@ -32,8 +32,11 @@ const EmployeesTablesPage = () => {
{ label: 'Name', title: 'name' }, { label: 'Name', title: 'name' },
{ label: 'Email', title: 'email' }, { label: 'Email', title: 'email' },
{ label: 'Phone', title: 'phone' }, { label: 'Phone', title: 'phone' },
{ label: 'Password hash', title: 'password_hash' },
{ label: 'Department', title: 'department' }, { label: 'Department', title: 'department' },
{ label: 'Role', title: 'role' },
]); ]);
const hasCreatePermission = const hasCreatePermission =

View File

@ -40,6 +40,10 @@ const initialValues = {
phone: '', phone: '',
department: '', department: '',
password_hash: '',
role: '',
}; };
const EmployeesNew = () => { const EmployeesNew = () => {
@ -91,6 +95,20 @@ const EmployeesNew = () => {
></Field> ></Field>
</FormField> </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 /> <BaseDivider />
<BaseButtons> <BaseButtons>
<BaseButton type='submit' color='info' label='Submit' /> <BaseButton type='submit' color='info' label='Submit' />

View File

@ -32,8 +32,11 @@ const EmployeesTablesPage = () => {
{ label: 'Name', title: 'name' }, { label: 'Name', title: 'name' },
{ label: 'Email', title: 'email' }, { label: 'Email', title: 'email' },
{ label: 'Phone', title: 'phone' }, { label: 'Phone', title: 'phone' },
{ label: 'Password hash', title: 'password_hash' },
{ label: 'Department', title: 'department' }, { label: 'Department', title: 'department' },
{ label: 'Role', title: 'role' },
]); ]);
const hasCreatePermission = const hasCreatePermission =

View File

@ -75,6 +75,17 @@ const EmployeesView = () => {
<p>{employees?.department?.name ?? 'No data'}</p> <p>{employees?.department?.name ?? 'No data'}</p>
</div> </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> <p className={'block font-bold mb-2'}>Visitors Host</p>
<CardBox <CardBox
@ -148,6 +159,39 @@ const EmployeesView = () => {
</CardBox> </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 /> <BaseDivider />
<BaseButton <BaseButton

View File

@ -50,7 +50,7 @@ export default function Login() {
remember: true, remember: true,
}); });
const title = 'VMS'; const title = 'Aspect Visiter Management System Demo';
// Fetch Pexels image/video // Fetch Pexels image/video
useEffect(() => { useEffect(() => {

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -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;

View File

@ -149,6 +149,57 @@ const RolesView = () => {
</CardBox> </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 /> <BaseDivider />
<BaseButton <BaseButton

View 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>
);
}

View File

@ -144,7 +144,7 @@ export default function WebSite() {
<FeaturesSection <FeaturesSection
projectName={'VMS'} projectName={'VMS'}
image={['Dashboard with visitor statistics']} image={['Dashboard with visitor statistics']}
withBg={0} withBg={1}
features={features_points} features={features_points}
mainText={`Discover Key Features of ${projectName}`} mainText={`Discover Key Features of ${projectName}`}
subTitle={`Enhance your visitor management with our innovative features designed to streamline operations and improve security.`} subTitle={`Enhance your visitor management with our innovative features designed to streamline operations and improve security.`}

View File

@ -159,7 +159,7 @@ export default function WebSite() {
<PricingSection <PricingSection
projectName={'VMS'} projectName={'VMS'}
withBg={0} withBg={1}
features={pricing_features} features={pricing_features}
description={description} description={description}
/> />

View File

@ -40,6 +40,16 @@ module.exports = {
'fade-in': 'fade-in 250ms ease-in-out', 'fade-in': 'fade-in 250ms ease-in-out',
}, },
colors: { 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: { dark: {
900: '#131618', 900: '#131618',
800: '#21242A', 800: '#21242A',