From 74905956effcadf50a50f335ab2ca847206ac2e1 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 13 Mar 2026 19:43:56 +0000 Subject: [PATCH] Autosave: 20260313-194355 --- backend/src/db/api/payroll_line_items.js | 41 +- backend/src/db/api/workers_comp_classes.js | 486 +++++++++++++++++ backend/src/db/migrations/1773330455692.js | 12 + backend/src/db/migrations/1773330455693.js | 12 + ...73330455694-create-workers-comp-classes.js | 51 ++ ...73330455695-alter-job-logs-workers-comp.js | 24 + ...3330455696-add-workers-comp-permissions.js | 62 +++ backend/src/db/models/job_logs.js | 31 +- backend/src/db/models/pay_types.js | 11 +- backend/src/db/models/payroll_line_items.js | 11 +- backend/src/db/models/workers_comp_classes.js | 54 ++ backend/src/index.js | 7 +- backend/src/routes/reports.js | 7 +- backend/src/routes/workers_comp_classes.js | 439 +++++++++++++++ backend/src/routes/workers_comp_report.js | 54 ++ backend/src/services/payroll_line_items.js | 9 + backend/src/services/workers_comp_classes.js | 138 +++++ .../src/components/Job_logs/CardJob_logs.tsx | 2 +- .../src/components/Job_logs/ListJob_logs.tsx | 2 +- .../Job_logs/configureJob_logsCols.tsx | 2 +- .../CardWorkers_comp_classes.tsx | 171 ++++++ .../ListWorkers_comp_classes.tsx | 128 +++++ .../TableWorkers_comp_classes.tsx | 463 ++++++++++++++++ .../configureWorkers_comp_classesCols.tsx | 88 +++ frontend/src/menuAside.ts | 36 +- frontend/src/pages/job_logs/job_logs-edit.tsx | 16 +- frontend/src/pages/job_logs/job_logs-new.tsx | 14 +- .../src/pages/pay_types/pay_types-edit.tsx | 17 +- .../src/pages/pay_types/pay_types-new.tsx | 13 +- frontend/src/pages/reports.tsx | 63 ++- .../[workers_comp_classesId].tsx | 509 ++++++++++++++++++ .../workers_comp_classes-edit.tsx | 294 ++++++++++ .../workers_comp_classes-list.tsx | 90 ++++ .../workers_comp_classes-new.tsx | 195 +++++++ .../workers_comp_classes-table.tsx | 164 ++++++ .../workers_comp_classes-view.tsx | 489 +++++++++++++++++ frontend/src/stores/store.ts | 2 + .../workers_comp_classesSlice.ts | 231 ++++++++ patch.js | 21 + 39 files changed, 4355 insertions(+), 104 deletions(-) create mode 100644 backend/src/db/api/workers_comp_classes.js create mode 100644 backend/src/db/migrations/1773330455692.js create mode 100644 backend/src/db/migrations/1773330455693.js create mode 100644 backend/src/db/migrations/1773330455694-create-workers-comp-classes.js create mode 100644 backend/src/db/migrations/1773330455695-alter-job-logs-workers-comp.js create mode 100644 backend/src/db/migrations/1773330455696-add-workers-comp-permissions.js create mode 100644 backend/src/db/models/workers_comp_classes.js create mode 100644 backend/src/routes/workers_comp_classes.js create mode 100644 backend/src/routes/workers_comp_report.js create mode 100644 backend/src/services/workers_comp_classes.js create mode 100644 frontend/src/components/Workers_comp_classes/CardWorkers_comp_classes.tsx create mode 100644 frontend/src/components/Workers_comp_classes/ListWorkers_comp_classes.tsx create mode 100644 frontend/src/components/Workers_comp_classes/TableWorkers_comp_classes.tsx create mode 100644 frontend/src/components/Workers_comp_classes/configureWorkers_comp_classesCols.tsx create mode 100644 frontend/src/pages/workers_comp_classes/[workers_comp_classesId].tsx create mode 100644 frontend/src/pages/workers_comp_classes/workers_comp_classes-edit.tsx create mode 100644 frontend/src/pages/workers_comp_classes/workers_comp_classes-list.tsx create mode 100644 frontend/src/pages/workers_comp_classes/workers_comp_classes-new.tsx create mode 100644 frontend/src/pages/workers_comp_classes/workers_comp_classes-table.tsx create mode 100644 frontend/src/pages/workers_comp_classes/workers_comp_classes-view.tsx create mode 100644 frontend/src/stores/workers_comp_classes/workers_comp_classesSlice.ts create mode 100644 patch.js diff --git a/backend/src/db/api/payroll_line_items.js b/backend/src/db/api/payroll_line_items.js index a0b5b58..826e0a2 100644 --- a/backend/src/db/api/payroll_line_items.js +++ b/backend/src/db/api/payroll_line_items.js @@ -1,4 +1,3 @@ - const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); @@ -36,6 +35,11 @@ module.exports = class Payroll_line_itemsDBApi { null , + workers_comp_amount: data.workers_comp_amount + || + null + , + total_client_paid: data.total_client_paid || null @@ -92,6 +96,11 @@ module.exports = class Payroll_line_itemsDBApi { total_commission_base: item.total_commission_base || null + , + + workers_comp_amount: item.workers_comp_amount + || + null , total_client_paid: item.total_client_paid @@ -140,6 +149,9 @@ module.exports = class Payroll_line_itemsDBApi { if (data.total_commission_base !== undefined) updatePayload.total_commission_base = data.total_commission_base; + if (data.workers_comp_amount !== undefined) updatePayload.workers_comp_amount = data.workers_comp_amount; + + if (data.total_client_paid !== undefined) updatePayload.total_client_paid = data.total_client_paid; @@ -425,6 +437,30 @@ module.exports = class Payroll_line_itemsDBApi { } } + if (filter.workers_comp_amountRange) { + const [start, end] = filter.workers_comp_amountRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + workers_comp_amount: { + ...where.workers_comp_amount, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + workers_comp_amount: { + ...where.workers_comp_amount, + [Op.lte]: end, + }, + }; + } + } + if (filter.total_client_paidRange) { const [start, end] = filter.total_client_paidRange; @@ -556,5 +592,4 @@ module.exports = class Payroll_line_itemsDBApi { } -}; - +}; \ No newline at end of file diff --git a/backend/src/db/api/workers_comp_classes.js b/backend/src/db/api/workers_comp_classes.js new file mode 100644 index 0000000..fcbf77c --- /dev/null +++ b/backend/src/db/api/workers_comp_classes.js @@ -0,0 +1,486 @@ + +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 Workers_comp_classesDBApi { + + + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const workers_comp_classes = await db.workers_comp_classes.create( + { + id: data.id || undefined, + + name: data.name + || + null + , + + pay_method: data.pay_method + || + null + , + + hourly_rate: data.hourly_rate + || + null + , + + commission_rate: data.commission_rate + || + null + , + + active: data.active + || + false + + , + + description: data.description + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + + + + + + + return workers_comp_classes; + } + + + 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 workers_comp_classesData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name + || + null + , + + pay_method: item.pay_method + || + null + , + + hourly_rate: item.hourly_rate + || + null + , + + commission_rate: item.commission_rate + || + null + , + + active: item.active + || + false + + , + + description: item.description + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const workers_comp_classes = await db.workers_comp_classes.bulkCreate(workers_comp_classesData, { transaction }); + + // For each item created, replace relation files + + + return workers_comp_classes; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + + const workers_comp_classes = await db.workers_comp_classes.findByPk(id, {}, {transaction}); + + + + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + + if (data.pay_method !== undefined) updatePayload.pay_method = data.pay_method; + + + if (data.hourly_rate !== undefined) updatePayload.hourly_rate = data.hourly_rate === "" ? null : data.hourly_rate; + + + if (data.commission_rate !== undefined) updatePayload.commission_rate = data.commission_rate === "" ? null : data.commission_rate; + + + if (data.active !== undefined) updatePayload.active = data.active; + + + if (data.description !== undefined) updatePayload.description = data.description; + + + updatePayload.updatedById = currentUser.id; + + await workers_comp_classes.update(updatePayload, {transaction}); + + + + + + + + + + return workers_comp_classes; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const workers_comp_classes = await db.workers_comp_classes.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of workers_comp_classes) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of workers_comp_classes) { + await record.destroy({transaction}); + } + }); + + + return workers_comp_classes; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const workers_comp_classes = await db.workers_comp_classes.findByPk(id, options); + + await workers_comp_classes.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await workers_comp_classes.destroy({ + transaction + }); + + return workers_comp_classes; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const workers_comp_classes = await db.workers_comp_classes.findOne( + { where }, + { transaction }, + ); + + if (!workers_comp_classes) { + return workers_comp_classes; + } + + const output = workers_comp_classes.get({plain: true}); + + + + + + + + + output.employee_workers_comp_classes_workers_comp_class = await workers_comp_classes.getEmployee_workers_comp_classes_workers_comp_class({ + transaction + }); + + + + output.job_logs_workers_comp_class = await workers_comp_classes.getJob_logs_workers_comp_class({ + 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 = [ + + + + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'workers_comp_classes', + 'name', + filter.name, + ), + }; + } + + if (filter.description) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'workers_comp_classes', + 'description', + filter.description, + ), + }; + } + + + + + + + if (filter.hourly_rateRange) { + const [start, end] = filter.hourly_rateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + hourly_rate: { + ...where.hourly_rate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + hourly_rate: { + ...where.hourly_rate, + [Op.lte]: end, + }, + }; + } + } + + if (filter.commission_rateRange) { + const [start, end] = filter.commission_rateRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + commission_rate: { + ...where.commission_rate, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + commission_rate: { + ...where.commission_rate, + [Op.lte]: end, + }, + }; + } + } + + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + + if (filter.pay_method) { + where = { + ...where, + pay_method: filter.pay_method, + }; + } + + if (filter.active) { + where = { + ...where, + active: filter.active, + }; + } + + + + + + 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.workers_comp_classes.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( + 'workers_comp_classes', + 'name', + query, + ), + ], + }; + } + + const records = await db.workers_comp_classes.findAll({ + attributes: [ 'id', 'name' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } + + +}; + diff --git a/backend/src/db/migrations/1773330455692.js b/backend/src/db/migrations/1773330455692.js new file mode 100644 index 0000000..7ef167b --- /dev/null +++ b/backend/src/db/migrations/1773330455692.js @@ -0,0 +1,12 @@ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn('pay_types', 'workers_comp_percentage', { + type: Sequelize.DataTypes.DECIMAL, + allowNull: true, + defaultValue: 0, + }); + }, + async down(queryInterface, Sequelize) { + await queryInterface.removeColumn('pay_types', 'workers_comp_percentage'); + } +}; \ No newline at end of file diff --git a/backend/src/db/migrations/1773330455693.js b/backend/src/db/migrations/1773330455693.js new file mode 100644 index 0000000..2cf012f --- /dev/null +++ b/backend/src/db/migrations/1773330455693.js @@ -0,0 +1,12 @@ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn('payroll_line_items', 'workers_comp_amount', { + type: Sequelize.DataTypes.DECIMAL, + allowNull: true, + defaultValue: 0, + }); + }, + async down(queryInterface, Sequelize) { + await queryInterface.removeColumn('payroll_line_items', 'workers_comp_amount'); + } +}; diff --git a/backend/src/db/migrations/1773330455694-create-workers-comp-classes.js b/backend/src/db/migrations/1773330455694-create-workers-comp-classes.js new file mode 100644 index 0000000..7874ed7 --- /dev/null +++ b/backend/src/db/migrations/1773330455694-create-workers-comp-classes.js @@ -0,0 +1,51 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('workers_comp_classes', { + id: { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4, + primaryKey: true, + }, + name: { + type: Sequelize.TEXT, + }, + percentage: { + type: Sequelize.DECIMAL, + }, + importHash: { + type: Sequelize.STRING(255), + allowNull: true, + unique: true, + }, + createdAt: { + type: Sequelize.DATE, + }, + updatedAt: { + type: Sequelize.DATE, + }, + deletedAt: { + type: Sequelize.DATE, + }, + createdById: { + type: Sequelize.UUID, + references: { + model: 'users', + key: 'id', + }, + }, + updatedById: { + type: Sequelize.UUID, + references: { + model: 'users', + key: 'id', + }, + }, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('workers_comp_classes'); + }, +}; diff --git a/backend/src/db/migrations/1773330455695-alter-job-logs-workers-comp.js b/backend/src/db/migrations/1773330455695-alter-job-logs-workers-comp.js new file mode 100644 index 0000000..77b891e --- /dev/null +++ b/backend/src/db/migrations/1773330455695-alter-job-logs-workers-comp.js @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('job_logs', 'workersCompClassId', { + type: Sequelize.UUID, + references: { + model: 'workers_comp_classes', + key: 'id', + }, + }); + // Remove the old enum column + // The enum type might cause issues if we try to drop it directly without dropping dependent views, + // but just dropping the column is usually fine. + await queryInterface.removeColumn('job_logs', 'workers_comp_class'); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('job_logs', 'workersCompClassId'); + await queryInterface.addColumn('job_logs', 'workers_comp_class', { + type: Sequelize.ENUM('roof', 'ladder', 'ground'), + }); + }, +}; diff --git a/backend/src/db/migrations/1773330455696-add-workers-comp-permissions.js b/backend/src/db/migrations/1773330455696-add-workers-comp-permissions.js new file mode 100644 index 0000000..affc565 --- /dev/null +++ b/backend/src/db/migrations/1773330455696-add-workers-comp-permissions.js @@ -0,0 +1,62 @@ +'use strict'; +const { v4: uuid } = require('uuid'); + +module.exports = { + up: async (queryInterface, Sequelize) => { + const createdAt = new Date(); + const updatedAt = new Date(); + const entity = 'workers_comp_classes'; + + const permissions = [ + { id: uuid(), name: `CREATE_${entity.toUpperCase()}`, createdAt, updatedAt }, + { id: uuid(), name: `READ_${entity.toUpperCase()}`, createdAt, updatedAt }, + { id: uuid(), name: `UPDATE_${entity.toUpperCase()}`, createdAt, updatedAt }, + { id: uuid(), name: `DELETE_${entity.toUpperCase()}`, createdAt, updatedAt }, + ]; + + await queryInterface.bulkInsert('permissions', permissions); + + // Get Admin and SystemOwner roles + const roles = await queryInterface.sequelize.query( + `SELECT id, name FROM roles WHERE name IN ('Administrator', 'SystemOwner');`, + { type: Sequelize.QueryTypes.SELECT } + ); + + const rolePermissions = []; + for (const role of roles) { + for (const perm of permissions) { + rolePermissions.push({ + roles_permissionsId: role.id, + permissionId: perm.id, + createdAt, + updatedAt, + }); + } + } + + if (rolePermissions.length > 0) { + await queryInterface.bulkInsert('rolesPermissionsPermissions', rolePermissions); + } + }, + + down: async (queryInterface, Sequelize) => { + const entity = 'workers_comp_classes'; + const permissionNames = [ + `CREATE_${entity.toUpperCase()}`, + `READ_${entity.toUpperCase()}`, + `UPDATE_${entity.toUpperCase()}`, + `DELETE_${entity.toUpperCase()}`, + ]; + + await queryInterface.sequelize.query( + `DELETE FROM "rolesPermissionsPermissions" WHERE "permissionId" IN (SELECT id FROM permissions WHERE name IN (:names));`, + { replacements: { names: permissionNames } } + ); + + await queryInterface.bulkDelete('permissions', { + name: { + [Sequelize.Op.in]: permissionNames, + }, + }); + }, +}; diff --git a/backend/src/db/models/job_logs.js b/backend/src/db/models/job_logs.js index 9ebb791..37dc8b9 100644 --- a/backend/src/db/models/job_logs.js +++ b/backend/src/db/models/job_logs.js @@ -35,25 +35,6 @@ client_paid: { }, -workers_comp_class: { - type: DataTypes.ENUM, - - - - values: [ - -"roof", - - -"ladder", - - -"ground" - - ], - - }, - odometer_start: { type: DataTypes.INTEGER, @@ -180,6 +161,14 @@ notes_to_admin: { constraints: false, }); + db.job_logs.belongsTo(db.workers_comp_classes, { + as: 'workersCompClass', + foreignKey: { + name: 'workersCompClassId', + }, + constraints: false, + }); + @@ -195,6 +184,4 @@ notes_to_admin: { return job_logs; -}; - - +}; \ No newline at end of file diff --git a/backend/src/db/models/pay_types.js b/backend/src/db/models/pay_types.js index a6f504a..a789dce 100644 --- a/backend/src/db/models/pay_types.js +++ b/backend/src/db/models/pay_types.js @@ -49,6 +49,13 @@ commission_rate: { + }, + +workers_comp_percentage: { + type: DataTypes.DECIMAL, + + + }, active: { @@ -135,6 +142,4 @@ description: { return pay_types; -}; - - +}; \ No newline at end of file diff --git a/backend/src/db/models/payroll_line_items.js b/backend/src/db/models/payroll_line_items.js index 8d92dda..ec6c5fb 100644 --- a/backend/src/db/models/payroll_line_items.js +++ b/backend/src/db/models/payroll_line_items.js @@ -33,6 +33,13 @@ total_commission_base: { + }, + +workers_comp_amount: { + type: DataTypes.DECIMAL, + + + }, total_client_paid: { @@ -116,6 +123,4 @@ summary: { return payroll_line_items; -}; - - +}; \ No newline at end of file diff --git a/backend/src/db/models/workers_comp_classes.js b/backend/src/db/models/workers_comp_classes.js new file mode 100644 index 0000000..8289ac5 --- /dev/null +++ b/backend/src/db/models/workers_comp_classes.js @@ -0,0 +1,54 @@ +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 workers_comp_classes = sequelize.define( + 'workers_comp_classes', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + }, + percentage: { + type: DataTypes.DECIMAL, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + workers_comp_classes.associate = (db) => { + db.workers_comp_classes.hasMany(db.job_logs, { + as: 'job_logs_workers_comp_class', + foreignKey: { + name: 'workersCompClassId', + }, + constraints: false, + }); + + db.workers_comp_classes.belongsTo(db.users, { + as: 'createdBy', + }); + + db.workers_comp_classes.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return workers_comp_classes; +}; diff --git a/backend/src/index.js b/backend/src/index.js index d748c03..72694a9 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -32,6 +32,8 @@ const customersRoutes = require('./routes/customers'); const vehiclesRoutes = require('./routes/vehicles'); const pay_typesRoutes = require('./routes/pay_types'); +const workers_comp_classesRoutes = require('./routes/workers_comp_classes'); +const workers_comp_reportRoutes = require('./routes/workers_comp_report'); const employee_pay_typesRoutes = require('./routes/employee_pay_types'); @@ -115,6 +117,8 @@ app.use('/api/customers', passport.authenticate('jwt', {session: false}), custom app.use('/api/vehicles', passport.authenticate('jwt', {session: false}), vehiclesRoutes); app.use('/api/pay_types', passport.authenticate('jwt', {session: false}), pay_typesRoutes); +app.use('/api/workers_comp_classes', passport.authenticate('jwt', {session: false}), workers_comp_classesRoutes); +app.use('/api/workers_comp_report', passport.authenticate('jwt', {session: false}), workers_comp_reportRoutes); app.use('/api/employee_pay_types', passport.authenticate('jwt', {session: false}), employee_pay_typesRoutes); @@ -127,6 +131,7 @@ app.use('/api/job_chemical_usages', passport.authenticate('jwt', {session: false app.use('/api/payroll_runs', passport.authenticate('jwt', {session: false}), payroll_runsRoutes); app.use('/api/payroll_line_items', passport.authenticate('jwt', {session: false}), payroll_line_itemsRoutes); +app.use('/api/reports', passport.authenticate('jwt', {session: false}), reportsRoutes); app.use( '/api/openai', @@ -167,7 +172,7 @@ if (fs.existsSync(publicDir)) { const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080; app.listen(PORT, () => { - console.log(`Listening on port ${PORT}`); + console.log(`Listening on port ${PORT}`); console.log('Watcher triggered'); }); module.exports = app; diff --git a/backend/src/routes/reports.js b/backend/src/routes/reports.js index b13c292..3224401 100644 --- a/backend/src/routes/reports.js +++ b/backend/src/routes/reports.js @@ -5,7 +5,7 @@ const db = require('../db/models'); const { wrapAsync } = require('../helpers'); const { Op } = require('sequelize'); -router.post('/payroll', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => { +router.post('/', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => { const { startDate, endDate, employeeId } = req.body; const where = {}; @@ -26,10 +26,11 @@ router.post('/payroll', passport.authenticate('jwt', { session: false }), wrapAs const summary = lineItems.reduce((acc, item) => { acc.totalGrossPay += parseFloat(item.gross_pay || 0); acc.totalHours += parseFloat(item.total_hours || 0); + acc.totalWorkersComp += parseFloat(item.workers_comp_amount || 0); return acc; - }, { totalGrossPay: 0, totalHours: 0 }); + }, { totalGrossPay: 0, totalHours: 0, totalWorkersComp: 0 }); res.json({ lineItems, summary }); })); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/workers_comp_classes.js b/backend/src/routes/workers_comp_classes.js new file mode 100644 index 0000000..3f70f04 --- /dev/null +++ b/backend/src/routes/workers_comp_classes.js @@ -0,0 +1,439 @@ + +const express = require('express'); + +const Workers_comp_classesService = require('../services/workers_comp_classes'); +const Workers_comp_classesDBApi = require('../db/api/workers_comp_classes'); +const wrapAsync = require('../helpers').wrapAsync; + + +const router = express.Router(); + +const { parse } = require('json2csv'); + + +const { + checkCrudPermissions, +} = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('workers_comp_classes')); + + +/** + * @swagger + * components: + * schemas: + * Workers_comp_classes: + * type: object + * properties: + + * name: + * type: string + * default: name + * description: + * type: string + * default: description + + + * hourly_rate: + * type: integer + * format: int64 + * commission_rate: + * type: integer + * format: int64 + + * + */ + +/** + * @swagger + * tags: + * name: Workers_comp_classes + * description: The Workers_comp_classes managing API + */ + +/** +* @swagger +* /api/workers_comp_classes: +* post: +* security: +* - bearerAuth: [] +* tags: [Workers_comp_classes] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Workers_comp_classes" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Workers_comp_classes" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Workers_comp_classesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Workers_comp_classes] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Workers_comp_classes" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Workers_comp_classes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Workers_comp_classesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/workers_comp_classes/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Workers_comp_classes] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Workers_comp_classes" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Workers_comp_classes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Workers_comp_classesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/workers_comp_classes/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Workers_comp_classes] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Workers_comp_classes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Workers_comp_classesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/workers_comp_classes/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Workers_comp_classes] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Workers_comp_classes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Workers_comp_classesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/workers_comp_classes: + * get: + * security: + * - bearerAuth: [] + * tags: [Workers_comp_classes] + * summary: Get all workers_comp_classes + * description: Get all workers_comp_classes + * responses: + * 200: + * description: Workers_comp_classes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Workers_comp_classes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + + const currentUser = req.currentUser; + const payload = await Workers_comp_classesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','name','description', + + 'hourly_rate','commission_rate', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/workers_comp_classes/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Workers_comp_classes] + * summary: Count all workers_comp_classes + * description: Count all workers_comp_classes + * responses: + * 200: + * description: Workers_comp_classes count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Workers_comp_classes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + + const currentUser = req.currentUser; + const payload = await Workers_comp_classesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/workers_comp_classes/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Workers_comp_classes] + * summary: Find all workers_comp_classes that match search criteria + * description: Find all workers_comp_classes that match search criteria + * responses: + * 200: + * description: Workers_comp_classes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Workers_comp_classes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + + const payload = await Workers_comp_classesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/workers_comp_classes/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Workers_comp_classes] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Workers_comp_classes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Workers_comp_classesDBApi.findBy( + { id: req.params.id }, + ); + + + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/workers_comp_report.js b/backend/src/routes/workers_comp_report.js new file mode 100644 index 0000000..9b28756 --- /dev/null +++ b/backend/src/routes/workers_comp_report.js @@ -0,0 +1,54 @@ +const express = require('express'); +const router = express.Router(); +const passport = require('passport'); +const db = require('../db/models'); +const { wrapAsync } = require('../helpers'); +const { Op } = require('sequelize'); + +router.get('/report', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => { + const { startDate, endDate, classId } = req.query; + + const where = {}; + if (startDate || endDate) { + where.work_date = {}; + if (startDate) where.work_date[Op.gte] = new Date(startDate); + if (endDate) where.work_date[Op.lte] = new Date(endDate); + } + if (classId) { + where.workersCompClassId = classId; + } + + const jobLogs = await db.job_logs.findAll({ + where, + include: [ + { model: db.workers_comp_classes, as: 'workersCompClass' }, + { model: db.pay_types, as: 'pay_type' }, + { model: db.users, as: 'employee' } + ] + }); + + const totalsByClass = {}; + let totalComp = 0; + + jobLogs.forEach(log => { + if (!log.workersCompClass || !log.pay_type) return; + + let employeePay = 0; + if (log.pay_type.pay_method === 'hourly') { + employeePay = Number(log.hours_conducted || 0) * Number(log.pay_type.hourly_rate || 0); + } else if (log.pay_type.pay_method === 'commission') { + employeePay = Number(log.client_paid || 0) * (Number(log.pay_type.commission_rate || 0) / 100); + } + + const compAmount = employeePay * (Number(log.workersCompClass.percentage || 0) / 100); + const className = log.workersCompClass.name; + + if (!totalsByClass[className]) totalsByClass[className] = 0; + totalsByClass[className] += compAmount; + totalComp += compAmount; + }); + + res.json({ totalsByClass, totalComp }); +})); + +module.exports = router; diff --git a/backend/src/services/payroll_line_items.js b/backend/src/services/payroll_line_items.js index f3d5234..3583010 100644 --- a/backend/src/services/payroll_line_items.js +++ b/backend/src/services/payroll_line_items.js @@ -15,6 +15,15 @@ module.exports = class Payroll_line_itemsService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { + if (data.job_logId && data.gross_pay) { + const jobLog = await db.job_logs.findByPk(data.job_logId, { transaction }); + if (jobLog && jobLog.workersCompClassId) { + const compClass = await db.workers_comp_classes.findByPk(jobLog.workersCompClassId, { transaction }); + if (compClass && compClass.percentage) { + data.workers_comp_amount = (Number(data.gross_pay || 0) * Number(compClass.percentage || 0)) / 100; + } + } + } await Payroll_line_itemsDBApi.create( data, { diff --git a/backend/src/services/workers_comp_classes.js b/backend/src/services/workers_comp_classes.js new file mode 100644 index 0000000..70d1f08 --- /dev/null +++ b/backend/src/services/workers_comp_classes.js @@ -0,0 +1,138 @@ +const db = require('../db/models'); +const Workers_comp_classesDBApi = require('../db/api/workers_comp_classes'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + + + + + +module.exports = class Workers_comp_classesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Workers_comp_classesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Workers_comp_classesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let workers_comp_classes = await Workers_comp_classesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!workers_comp_classes) { + throw new ValidationError( + 'workers_comp_classesNotFound', + ); + } + + const updatedWorkers_comp_classes = await Workers_comp_classesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedWorkers_comp_classes; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Workers_comp_classesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Workers_comp_classesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + +}; + + diff --git a/frontend/src/components/Job_logs/CardJob_logs.tsx b/frontend/src/components/Job_logs/CardJob_logs.tsx index e04b1bb..88941c2 100644 --- a/frontend/src/components/Job_logs/CardJob_logs.tsx +++ b/frontend/src/components/Job_logs/CardJob_logs.tsx @@ -141,7 +141,7 @@ const CardJob_logs = ({
WorkmansCompensationClass
- { item.workers_comp_class } + { item.workersCompClass?.name }
diff --git a/frontend/src/components/Job_logs/ListJob_logs.tsx b/frontend/src/components/Job_logs/ListJob_logs.tsx index a3763e3..2152628 100644 --- a/frontend/src/components/Job_logs/ListJob_logs.tsx +++ b/frontend/src/components/Job_logs/ListJob_logs.tsx @@ -89,7 +89,7 @@ const ListJob_logs = ({ job_logs, loading, onDelete, currentPage, numPages, onPa

WorkmansCompensationClass

-

{ item.workers_comp_class }

+

{ item.workersCompClass?.name }

diff --git a/frontend/src/components/Job_logs/configureJob_logsCols.tsx b/frontend/src/components/Job_logs/configureJob_logsCols.tsx index ea9da9e..442789b 100644 --- a/frontend/src/components/Job_logs/configureJob_logsCols.tsx +++ b/frontend/src/components/Job_logs/configureJob_logsCols.tsx @@ -136,7 +136,7 @@ export const loadColumns = async ( }, { - field: 'workers_comp_class', + field: 'workersCompClass', valueGetter: (params) => params.row?.workersCompClass?.name, headerName: 'WorkmansCompensationClass', flex: 1, minWidth: 120, diff --git a/frontend/src/components/Workers_comp_classes/CardWorkers_comp_classes.tsx b/frontend/src/components/Workers_comp_classes/CardWorkers_comp_classes.tsx new file mode 100644 index 0000000..0d2bff7 --- /dev/null +++ b/frontend/src/components/Workers_comp_classes/CardWorkers_comp_classes.tsx @@ -0,0 +1,171 @@ +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 = { + workers_comp_classes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardWorkers_comp_classes = ({ + workers_comp_classes, + 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_PAY_TYPES') + + + return ( +
+ {loading && } +
    + {!loading && workers_comp_classes.map((item, index) => ( +
  • + +
    + + + {item.name} + + + +
    + +
    +
    +
    + + +
    +
    WorkersCompClassName
    +
    +
    + { item.name } +
    +
    +
    + + + + +
    +
    PayMethod
    +
    +
    + { item.pay_method } +
    +
    +
    + + + + +
    +
    HourlyRate
    +
    +
    + { item.hourly_rate } +
    +
    +
    + + + + +
    +
    CommissionRate
    +
    +
    + { item.commission_rate } +
    +
    +
    + + + + +
    +
    Active
    +
    +
    + { dataFormatter.booleanFormatter(item.active) } +
    +
    +
    + + + + +
    +
    Description
    +
    +
    + { item.description } +
    +
    +
    + + + +
    +
  • + ))} + {!loading && workers_comp_classes.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardWorkers_comp_classes; diff --git a/frontend/src/components/Workers_comp_classes/ListWorkers_comp_classes.tsx b/frontend/src/components/Workers_comp_classes/ListWorkers_comp_classes.tsx new file mode 100644 index 0000000..49ba480 --- /dev/null +++ b/frontend/src/components/Workers_comp_classes/ListWorkers_comp_classes.tsx @@ -0,0 +1,128 @@ +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 = { + workers_comp_classes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListWorkers_comp_classes = ({ workers_comp_classes, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_PAY_TYPES') + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + + return ( + <> +
+ {loading && } + {!loading && workers_comp_classes.map((item) => ( +
+ +
+ + dark:divide-dark-700 overflow-x-auto' + } + > + + +
+

WorkersCompClassName

+

{ item.name }

+
+ + + + +
+

PayMethod

+

{ item.pay_method }

+
+ + + + +
+

HourlyRate

+

{ item.hourly_rate }

+
+ + + + +
+

CommissionRate

+

{ item.commission_rate }

+
+ + + + +
+

Active

+

{ dataFormatter.booleanFormatter(item.active) }

+
+ + + + +
+

Description

+

{ item.description }

+
+ + + + + +
+
+
+ ))} + {!loading && workers_comp_classes.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ) +}; + +export default ListWorkers_comp_classes \ No newline at end of file diff --git a/frontend/src/components/Workers_comp_classes/TableWorkers_comp_classes.tsx b/frontend/src/components/Workers_comp_classes/TableWorkers_comp_classes.tsx new file mode 100644 index 0000000..52a5d26 --- /dev/null +++ b/frontend/src/components/Workers_comp_classes/TableWorkers_comp_classes.tsx @@ -0,0 +1,463 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/workers_comp_classes/workers_comp_classesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureWorkers_comp_classesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; + + + +const perPage = 10 + +const TableSampleWorkers_comp_classes = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { workers_comp_classes, loading, count, notify: workers_comp_classesNotify, refetch } = useAppSelector((state) => state.workers_comp_classes) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (workers_comp_classesNotify.showNotification) { + notify(workers_comp_classesNotify.typeNotification, workers_comp_classesNotify.textNotification); + } + }, [workers_comp_classesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + + + + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + + useEffect(() => { + if (!currentUser) return; + + loadColumns( + handleDeleteModalAction, + `workers_comp_classes`, + currentUser, + ).then((newCols) => setColumns(newCols)); + }, [currentUser]); + + + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + + const dataGrid = ( +
+ `datagrid--row`} + rows={workers_comp_classes ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
+ <> + {filterItems && filterItems.map((filterItem) => { + return ( +
+
+
Filter
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
+
+ Value +
+ + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
+ ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
+
+
From
+ +
+
+
To
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
+
+
+ From +
+ +
+
+
To
+ +
+
+ ) : ( +
+
Contains
+ +
+ )} +
+
Action
+ { + deleteFilter(filterItem.id) + }} + /> +
+
+ ) + })} +
+ + +
+ +
+
+
: null + } + +

Are you sure you want to delete this item?

+
+ + + {dataGrid} + + + + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleWorkers_comp_classes diff --git a/frontend/src/components/Workers_comp_classes/configureWorkers_comp_classesCols.tsx b/frontend/src/components/Workers_comp_classes/configureWorkers_comp_classesCols.tsx new file mode 100644 index 0000000..82b0955 --- /dev/null +++ b/frontend/src/components/Workers_comp_classes/configureWorkers_comp_classesCols.tsx @@ -0,0 +1,88 @@ +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_WORKERS_COMP_CLASSES') + + return [ + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + }, + { + field: 'percentage', + headerName: 'Percentage (%)', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + editable: hasUpdatePermission, + type: 'number', + }, + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
+ +
, + ] + }, + }, + ]; +}; diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 9ce49e6..cb342e4 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -61,6 +61,12 @@ const menuAside: MenuAsideItem[] = [ icon: 'mdiTruck' in icon ? icon['mdiTruck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, permissions: 'READ_VEHICLES' }, + { + href: '/workers_comp_classes/workers_comp_classes-list', + label: 'Workmans Comp', + icon: icon.mdiTable, + permissions: 'READ_WORKERS_COMP_CLASSES' + }, { href: '/pay_types/pay_types-list', label: 'Pay types', @@ -110,40 +116,10 @@ const menuAside: MenuAsideItem[] = [ { href: '/payroll_line_items/payroll_line_items-list', label: 'Payroll line items', - { - href: '/reports', - label: 'Payroll Reports', - icon: icon.mdiChartBar, - permissions: 'READ_PAYROLL_LINE_ITEMS' - }, // eslint-disable-next-line @typescript-eslint/ban-ts-comment - { - href: '/reports', - label: 'Payroll Reports', - icon: icon.mdiChartBar, - permissions: 'READ_PAYROLL_LINE_ITEMS' - }, // @ts-ignore - { - href: '/reports', - label: 'Payroll Reports', - icon: icon.mdiChartBar, - permissions: 'READ_PAYROLL_LINE_ITEMS' - }, icon: 'mdiFileDocumentOutline' in icon ? icon['mdiFileDocumentOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - { - href: '/reports', - label: 'Payroll Reports', - icon: icon.mdiChartBar, permissions: 'READ_PAYROLL_LINE_ITEMS' - }, - permissions: 'READ_PAYROLL_LINE_ITEMS' - { - href: '/reports', - label: 'Payroll Reports', - icon: icon.mdiChartBar, - permissions: 'READ_PAYROLL_LINE_ITEMS' - }, }, { href: '/reports', diff --git a/frontend/src/pages/job_logs/job_logs-edit.tsx b/frontend/src/pages/job_logs/job_logs-edit.tsx index b1ed0bb..06fd348 100644 --- a/frontend/src/pages/job_logs/job_logs-edit.tsx +++ b/frontend/src/pages/job_logs/job_logs-edit.tsx @@ -196,7 +196,7 @@ const EditJob_logsPage = () => { - workers_comp_class: '', + workersCompClass: '', @@ -723,17 +723,9 @@ const EditJob_logsPage = () => { - - - - - - - - - - - + + + diff --git a/frontend/src/pages/job_logs/job_logs-new.tsx b/frontend/src/pages/job_logs/job_logs-new.tsx index 9bd4ee3..1c3d8f6 100644 --- a/frontend/src/pages/job_logs/job_logs-new.tsx +++ b/frontend/src/pages/job_logs/job_logs-new.tsx @@ -119,7 +119,7 @@ const initialValues = { - workers_comp_class: 'roof', + workersCompClass: '', @@ -457,16 +457,8 @@ const Job_logsNew = () => { - - - - - - - - - - + + diff --git a/frontend/src/pages/pay_types/pay_types-edit.tsx b/frontend/src/pages/pay_types/pay_types-edit.tsx index baca3cd..126b323 100644 --- a/frontend/src/pages/pay_types/pay_types-edit.tsx +++ b/frontend/src/pages/pay_types/pay_types-edit.tsx @@ -128,6 +128,11 @@ const EditPay_typesPage = () => { + + 'workers_comp_percentage': '', + + + @@ -406,7 +411,15 @@ const EditPay_typesPage = () => { - + + + @@ -503,4 +516,4 @@ EditPay_typesPage.getLayout = function getLayout(page: ReactElement) { ) } -export default EditPay_typesPage +export default EditPay_typesPage \ No newline at end of file diff --git a/frontend/src/pages/pay_types/pay_types-new.tsx b/frontend/src/pages/pay_types/pay_types-new.tsx index b011761..1a5c04e 100644 --- a/frontend/src/pages/pay_types/pay_types-new.tsx +++ b/frontend/src/pages/pay_types/pay_types-new.tsx @@ -80,6 +80,8 @@ const initialValues = { commission_rate: '', + + workers_comp_percentage: '', @@ -285,6 +287,15 @@ const Pay_typesNew = () => { /> + + + @@ -393,4 +404,4 @@ Pay_typesNew.getLayout = function getLayout(page: ReactElement) { ) } -export default Pay_typesNew +export default Pay_typesNew \ No newline at end of file diff --git a/frontend/src/pages/reports.tsx b/frontend/src/pages/reports.tsx index 531fba6..1810cc9 100644 --- a/frontend/src/pages/reports.tsx +++ b/frontend/src/pages/reports.tsx @@ -1,6 +1,6 @@ import { mdiChartBar } from '@mdi/js'; import Head from 'next/head'; -import React, { ReactElement, useState, useEffect } from 'react'; +import React, { ReactElement, useState } from 'react'; import CardBox from '../components/CardBox'; import LayoutAuthenticated from '../layouts/Authenticated'; import SectionMain from '../components/SectionMain'; @@ -22,7 +22,7 @@ const ReportsPage = () => { const fetchReport = async () => { setLoading(true); try { - const response = await axios.post('/reports/payroll', filters); + const response = await axios.post('/reports', filters); setReportData(response.data); } catch (error) { console.error('Failed to fetch report:', error); @@ -43,23 +43,66 @@ const ReportsPage = () => {
- setFilters({...filters, startDate: e.target.value})} className="px-3 py-2 border border-gray-300 rounded" /> + setFilters({...filters, startDate: e.target.value})} className="px-3 py-2 border border-gray-300 rounded w-full" /> - setFilters({...filters, endDate: e.target.value})} className="px-3 py-2 border border-gray-300 rounded" /> + setFilters({...filters, endDate: e.target.value})} className="px-3 py-2 border border-gray-300 rounded w-full" /> - - setFilters({...filters, employeeId: e.target.value})} className="px-3 py-2 border border-gray-300 rounded" /> + + setFilters({...filters, employeeId: e.target.value})} className="px-3 py-2 border border-gray-300 rounded w-full" />
- +
{reportData && ( - -
{JSON.stringify(reportData, null, 2)}
-
+ <> + +

Summary

+
+
+

Total Gross Pay

+

${reportData.summary.totalGrossPay.toFixed(2)}

+
+
+

Total Hours

+

{reportData.summary.totalHours.toFixed(2)}

+
+
+

Total Work Comp

+

${reportData.summary.totalWorkersComp.toFixed(2)}

+
+
+
+ +

Line Items

+
+ + + + + + + + + + + + {reportData.lineItems.map((item: any) => ( + + + + + + + + ))} + +
EmployeeHoursGross PayWork Comp AmountCreated At
{item.employee?.firstName} {item.employee?.lastName || ''}{item.total_hours}${item.gross_pay}${item.workers_comp_amount}{new Date(item.createdAt).toLocaleDateString()}
+
+
+ )} diff --git a/frontend/src/pages/workers_comp_classes/[workers_comp_classesId].tsx b/frontend/src/pages/workers_comp_classes/[workers_comp_classesId].tsx new file mode 100644 index 0000000..af7828c --- /dev/null +++ b/frontend/src/pages/workers_comp_classes/[workers_comp_classesId].tsx @@ -0,0 +1,509 @@ +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/workers_comp_classes/workers_comp_classesSlice' +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 EditWorkers_comp_classes = () => { + const router = useRouter() + const dispatch = useAppDispatch() + const initVals = { + + + 'name': '', + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pay_method: '', + + + + + + + + + + + + 'hourly_rate': '', + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'commission_rate': '', + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + active: false, + + + + + + + + + + + + + + + + + + description: '', + + + + + + + + + + + + + + + + + + + + + + + + + + } + const [initialValues, setInitialValues] = useState(initVals) + + const { workers_comp_classes } = useAppSelector((state) => state.workers_comp_classes) + + + const { workers_comp_classesId } = router.query + + useEffect(() => { + dispatch(fetch({ id: workers_comp_classesId })) + }, [workers_comp_classesId]) + + useEffect(() => { + if (typeof workers_comp_classes === 'object') { + setInitialValues(workers_comp_classes) + } + }, [workers_comp_classes]) + + useEffect(() => { + if (typeof workers_comp_classes === 'object') { + + const newInitialVal = {...initVals}; + + Object.keys(initVals).forEach(el => newInitialVal[el] = (workers_comp_classes)[el]) + + setInitialValues(newInitialVal); + } + }, [workers_comp_classes]) + + const handleSubmit = async (data) => { + await dispatch(update({ id: workers_comp_classesId, data })) + await router.push('/workers_comp_classes/workers_comp_classes-list') + } + + return ( + <> + + {getPageTitle('Edit workers_comp_classes')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/workers_comp_classes/workers_comp_classes-list')}/> + + +
+
+
+ + ) +} + +EditWorkers_comp_classes.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default EditWorkers_comp_classes diff --git a/frontend/src/pages/workers_comp_classes/workers_comp_classes-edit.tsx b/frontend/src/pages/workers_comp_classes/workers_comp_classes-edit.tsx new file mode 100644 index 0000000..3ee3169 --- /dev/null +++ b/frontend/src/pages/workers_comp_classes/workers_comp_classes-edit.tsx @@ -0,0 +1,294 @@ +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/workers_comp_classes/workers_comp_classesSlice' +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 EditWorkers_comp_classesPage = () => { + const router = useRouter() + const dispatch = useAppDispatch() + const initVals = { + + + 'name': '', + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pay_method: '', + + + + + + + + + + + + 'hourly_rate': '', + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'commission_rate': '', + + + + + 'workers_comp_percentage': '', + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + active: false, + + + + + + + + + + + + + + + + + + description: '', + + + + + + + + + + + + + + + + + + + + + + + + + + } + const [initialValues, setInitialValues] = useState(initVals) + + const { workers_comp_classes } = useAppSelector((state) => state.workers_comp_classes) + + + const { id } = router.query + + useEffect(() => { + dispatch(fetch({ id: id })) + }, [id]) + + useEffect(() => { + if (typeof workers_comp_classes === 'object') { + setInitialValues(workers_comp_classes) + } + }, [workers_comp_classes]) + + useEffect(() => { + if (typeof workers_comp_classes === 'object') { + const newInitialVal = {...initVals}; + Object.keys(initVals).forEach(el => newInitialVal[el] = (workers_comp_classes)[el]) + setInitialValues(newInitialVal); + } + }, [workers_comp_classes]) + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })) + await router.push('/workers_comp_classes/workers_comp_classes-list') + } + + return ( + <> + + {getPageTitle('Edit workers_comp_classes')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + +
+
+
+ + ) +} + +EditWorkers_comp_classesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default EditWorkers_comp_classesPage \ No newline at end of file diff --git a/frontend/src/pages/workers_comp_classes/workers_comp_classes-list.tsx b/frontend/src/pages/workers_comp_classes/workers_comp_classes-list.tsx new file mode 100644 index 0000000..85c4f3c --- /dev/null +++ b/frontend/src/pages/workers_comp_classes/workers_comp_classes-list.tsx @@ -0,0 +1,90 @@ +import { mdiChartTimelineVariant, mdiPlus } from '@mdi/js' +import Head from 'next/head' +import React, { ReactElement, useEffect, 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 TableWorkers_comp_classes from '../../components/Workers_comp_classes/TableWorkers_comp_classes' +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/workers_comp_classes/workers_comp_classesSlice'; +import { hasPermission } from '../../helpers/userPermissions'; + +const Workers_comp_classesList = () => { + const dispatch = useAppDispatch(); + const { currentUser } = useAppSelector((state) => state.auth); + const [reportData, setReportData] = useState(null); + + useEffect(() => { + const fetchReport = async () => { + try { + const res = await axios.get('/workers_comp_report/report'); + setReportData(res.data); + } catch (e) { + console.error("Failed to fetch report", e); + } + }; + fetchReport(); + }, []); + + return ( + <> + + {getPageTitle('Workers Comp Classes')} + + + + {hasPermission(currentUser, 'CREATE_WORKERS_COMP_CLASSES') && ( +
+ + + +
+ )} +
+ + {reportData && ( + +

Workman's Comp Totals (All Time)

+
+
+

Total Work Comp

+

${reportData.totalComp.toFixed(2)}

+
+ {Object.entries(reportData.totalsByClass).map(([className, total]: any) => ( +
+

{className}

+

${total.toFixed(2)}

+
+ ))} +
+
+ )} + + + + +
+ + ) +} + +Workers_comp_classesList.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default Workers_comp_classesList \ No newline at end of file diff --git a/frontend/src/pages/workers_comp_classes/workers_comp_classes-new.tsx b/frontend/src/pages/workers_comp_classes/workers_comp_classes-new.tsx new file mode 100644 index 0000000..57ff73a --- /dev/null +++ b/frontend/src/pages/workers_comp_classes/workers_comp_classes-new.tsx @@ -0,0 +1,195 @@ +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/workers_comp_classes/workers_comp_classesSlice' +import { useAppDispatch } from '../../stores/hooks' +import { useRouter } from 'next/router' +import moment from 'moment'; + +const initialValues = { + + + name: '', + + + + + + + + + + + + + + + + + + + + + + + + + pay_method: 'hourly', + + + + + + + + hourly_rate: '', + + + + + + + + + + + + + + + + commission_rate: '', + + workers_comp_percentage: '', + + + + + + + + + + + + + + + + + + + + + + active: false, + + + + + + + + + + + description: '', + + + + + + + + + + + + + +} + + +const Workers_comp_classesNew = () => { + const router = useRouter() + const dispatch = useAppDispatch() + + + + + const handleSubmit = async (data) => { + await dispatch(create(data)) + await router.push('/workers_comp_classes/workers_comp_classes-list') + } + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + +
+
+
+ + ) +} + +Workers_comp_classesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default Workers_comp_classesNew \ No newline at end of file diff --git a/frontend/src/pages/workers_comp_classes/workers_comp_classes-table.tsx b/frontend/src/pages/workers_comp_classes/workers_comp_classes-table.tsx new file mode 100644 index 0000000..0356dd6 --- /dev/null +++ b/frontend/src/pages/workers_comp_classes/workers_comp_classes-table.tsx @@ -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 TableWorkers_comp_classes from '../../components/Workers_comp_classes/TableWorkers_comp_classes' +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/workers_comp_classes/workers_comp_classesSlice'; + + +import {hasPermission} from "../../helpers/userPermissions"; + + + +const Workers_comp_classesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + + const { currentUser } = useAppSelector((state) => state.auth); + + + const dispatch = useAppDispatch(); + + + const [filters] = useState([{label: 'WorkersCompClassName', title: 'name'},{label: 'Description', title: 'description'}, + + {label: 'HourlyRate', title: 'hourly_rate', number: 'true'},{label: 'CommissionRate', title: 'commission_rate', number: 'true'}, + + + + {label: 'PayMethod', title: 'pay_method', type: 'enum', options: ['hourly','commission']}, + ]); + + const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_PAY_TYPES'); + + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getWorkers_comp_classesCSV = async () => { + const response = await axios({url: '/workers_comp_classes?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 = 'workers_comp_classesCSV.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 ( + <> + + {getPageTitle('Workers_comp_classes')} + + + + {''} + + + + {hasCreatePermission && } + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+ + + Back to table + + +
+
+ + + +
+ + + + + ) +} + +Workers_comp_classesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ) +} + +export default Workers_comp_classesTablesPage diff --git a/frontend/src/pages/workers_comp_classes/workers_comp_classes-view.tsx b/frontend/src/pages/workers_comp_classes/workers_comp_classes-view.tsx new file mode 100644 index 0000000..560f936 --- /dev/null +++ b/frontend/src/pages/workers_comp_classes/workers_comp_classes-view.tsx @@ -0,0 +1,489 @@ +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/workers_comp_classes/workers_comp_classesSlice' +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 Workers_comp_classesView = () => { + const router = useRouter() + const dispatch = useAppDispatch() + const { workers_comp_classes } = useAppSelector((state) => state.workers_comp_classes) + + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str,`str`) + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + + return ( + <> + + {getPageTitle('View workers_comp_classes')} + + + + + + + + + +
+

WorkersCompClassName

+

{workers_comp_classes?.name}

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

PayMethod

+

{workers_comp_classes?.pay_method ?? 'No data'}

+
+ + + + + + + + + + + + + + + + + + + + +
+

HourlyRate

+

{workers_comp_classes?.hourly_rate || 'No data'}

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

CommissionRate

+

{workers_comp_classes?.commission_rate || 'No data'}

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + null}} + disabled + /> + + + + + + + + + + + + + + + + +