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 && (
+
+ )}
+
+
+
+ );
+};
+
+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 && (
+
+ )}
+
+
+ >
+ )
+};
+
+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}
+ >
+
+
+ : 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 = () => {
{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
+
+
+
+
+ | Employee |
+ Hours |
+ Gross Pay |
+ Work Comp Amount |
+ Created At |
+
+
+
+ {reportData.lineItems.map((item: any) => (
+
+ | {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)}
+ >
+
+
+
+
+ >
+ )
+}
+
+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
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <>
+ Employee_workers_comp_classes WorkersCompClass
+
+
+
+
+
+
+
+
+
+
+
+ | Active |
+
+
+
+ EffectiveStart |
+
+
+
+ EffectiveEnd |
+
+
+
+
+
+ {workers_comp_classes.employee_workers_comp_classes_workers_comp_class && Array.isArray(workers_comp_classes.employee_workers_comp_classes_workers_comp_class) &&
+ workers_comp_classes.employee_workers_comp_classes_workers_comp_class.map((item: any) => (
+ router.push(`/employee_workers_comp_classes/employee_workers_comp_classes-view/?id=${item.id}`)}>
+
+
+
+
+
+
+ |
+ { dataFormatter.booleanFormatter(item.active) }
+ |
+
+
+
+
+ { dataFormatter.dateTimeFormatter(item.effective_start) }
+ |
+
+
+
+
+ { dataFormatter.dateTimeFormatter(item.effective_end) }
+ |
+
+
+
+ ))}
+
+
+
+ {!workers_comp_classes?.employee_workers_comp_classes_workers_comp_class?.length && No data
}
+
+ >
+
+
+
+ <>
+ Job_logs WorkersCompClass
+
+
+
+
+
+
+
+ | WorkDate |
+
+
+
+
+
+
+
+ HoursConducted |
+
+
+
+ ClientPaid |
+
+
+
+ WorkmansCompensationClass |
+
+
+
+
+
+
+
+ OdometerStart |
+
+
+
+ OdometerEnd |
+
+
+
+ JobAddress |
+
+
+
+ Status |
+
+
+
+ NotesToAdmin |
+
+
+
+
+
+ {workers_comp_classes.job_logs_workers_comp_class && Array.isArray(workers_comp_classes.job_logs_workers_comp_class) &&
+ workers_comp_classes.job_logs_workers_comp_class.map((item: any) => (
+ router.push(`/job_logs/job_logs-view/?id=${item.id}`)}>
+
+
+ |
+ { dataFormatter.dateTimeFormatter(item.work_date) }
+ |
+
+
+
+
+
+
+
+
+ { item.hours_conducted }
+ |
+
+
+
+
+ { item.client_paid }
+ |
+
+
+
+
+ { item.workers_comp_class }
+ |
+
+
+
+
+
+
+
+
+ { item.odometer_start }
+ |
+
+
+
+
+ { item.odometer_end }
+ |
+
+
+
+
+ { item.job_address }
+ |
+
+
+
+
+ { item.status }
+ |
+
+
+
+
+ { item.notes_to_admin }
+ |
+
+
+
+ ))}
+
+
+
+ {!workers_comp_classes?.job_logs_workers_comp_class?.length && No data
}
+
+ >
+
+
+
+
+
+
+
+
+ router.push('/workers_comp_classes/workers_comp_classes-list')}
+ />
+
+
+ >
+ );
+};
+
+Workers_comp_classesView.getLayout = function getLayout(page: ReactElement) {
+ return (
+
+ {page}
+
+ )
+}
+
+export default Workers_comp_classesView;
\ No newline at end of file
diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts
index 825744f..7ac49f9 100644
--- a/frontend/src/stores/store.ts
+++ b/frontend/src/stores/store.ts
@@ -16,6 +16,7 @@ import job_logsSlice from "./job_logs/job_logsSlice";
import job_chemical_usagesSlice from "./job_chemical_usages/job_chemical_usagesSlice";
import payroll_runsSlice from "./payroll_runs/payroll_runsSlice";
import payroll_line_itemsSlice from "./payroll_line_items/payroll_line_itemsSlice";
+import workers_comp_classesSlice from "./workers_comp_classes/workers_comp_classesSlice";
export const store = configureStore({
reducer: {
@@ -36,6 +37,7 @@ job_logs: job_logsSlice,
job_chemical_usages: job_chemical_usagesSlice,
payroll_runs: payroll_runsSlice,
payroll_line_items: payroll_line_itemsSlice,
+workers_comp_classes: workers_comp_classesSlice,
},
})
diff --git a/frontend/src/stores/workers_comp_classes/workers_comp_classesSlice.ts b/frontend/src/stores/workers_comp_classes/workers_comp_classesSlice.ts
new file mode 100644
index 0000000..40cb38b
--- /dev/null
+++ b/frontend/src/stores/workers_comp_classes/workers_comp_classesSlice.ts
@@ -0,0 +1,231 @@
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
+import axios from 'axios'
+import {fulfilledNotify, rejectNotify, resetNotify} from "../../helpers/notifyStateHandler";
+
+interface MainState {
+ workers_comp_classes: any
+ loading: boolean
+ count: number
+ refetch: boolean;
+ rolesWidgets: any[];
+ notify: {
+ showNotification: boolean
+ textNotification: string
+ typeNotification: string
+ }
+}
+
+const initialState: MainState = {
+ workers_comp_classes: [],
+ loading: false,
+ count: 0,
+ refetch: false,
+ rolesWidgets: [],
+ notify: {
+ showNotification: false,
+ textNotification: '',
+ typeNotification: 'warn',
+ },
+}
+
+export const fetch = createAsyncThunk('workers_comp_classes/fetch', async (data: any) => {
+ const { id, query } = data
+ const result = await axios.get(
+ `workers_comp_classes${
+ query || (id ? `/${id}` : '')
+ }`
+ )
+ return id ? result.data : {rows: result.data.rows, count: result.data.count};
+})
+
+export const deleteItemsByIds = createAsyncThunk(
+ 'workers_comp_classes/deleteByIds',
+ async (data: any, { rejectWithValue }) => {
+ try {
+ await axios.post('workers_comp_classes/deleteByIds', { data });
+ } catch (error) {
+ if (!error.response) {
+ throw error;
+ }
+
+ return rejectWithValue(error.response.data);
+ }
+ },
+);
+
+export const deleteItem = createAsyncThunk('workers_comp_classes/deleteWorkers_comp_classes', async (id: string, { rejectWithValue }) => {
+ try {
+ await axios.delete(`workers_comp_classes/${id}`)
+ } catch (error) {
+ if (!error.response) {
+ throw error;
+ }
+
+ return rejectWithValue(error.response.data);
+ }
+})
+
+export const create = createAsyncThunk('workers_comp_classes/createWorkers_comp_classes', async (data: any, { rejectWithValue }) => {
+ try {
+ const result = await axios.post(
+ 'workers_comp_classes',
+ { data }
+ )
+ return result.data
+ } catch (error) {
+ if (!error.response) {
+ throw error;
+ }
+
+ return rejectWithValue(error.response.data);
+ }
+})
+
+export const uploadCsv = createAsyncThunk(
+ 'workers_comp_classes/uploadCsv',
+ async (file: File, { rejectWithValue }) => {
+ try {
+ const data = new FormData();
+ data.append('file', file);
+ data.append('filename', file.name);
+
+ const result = await axios.post('workers_comp_classes/bulk-import', data, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ return result.data;
+ } catch (error) {
+ if (!error.response) {
+ throw error;
+ }
+
+ return rejectWithValue(error.response.data);
+ }
+ },
+);
+
+export const update = createAsyncThunk('workers_comp_classes/updateWorkers_comp_classes', async (payload: any, { rejectWithValue }) => {
+ try {
+ const result = await axios.put(
+ `workers_comp_classes/${payload.id}`,
+ { id: payload.id, data: payload.data }
+ )
+ return result.data
+ } catch (error) {
+ if (!error.response) {
+ throw error;
+ }
+
+ return rejectWithValue(error.response.data);
+ }
+})
+
+
+export const workers_comp_classesSlice = createSlice({
+ name: 'workers_comp_classes',
+ initialState,
+ reducers: {
+ setRefetch: (state, action: PayloadAction) => {
+ state.refetch = action.payload;
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(fetch.pending, (state) => {
+ state.loading = true
+ resetNotify(state);
+ })
+ builder.addCase(fetch.rejected, (state, action) => {
+ state.loading = false
+ rejectNotify(state, action);
+ })
+
+ builder.addCase(fetch.fulfilled, (state, action) => {
+ if (action.payload.rows && action.payload.count >= 0) {
+ state.workers_comp_classes = action.payload.rows;
+ state.count = action.payload.count;
+ } else {
+ state.workers_comp_classes = action.payload;
+ }
+ state.loading = false
+ })
+
+ builder.addCase(deleteItemsByIds.pending, (state) => {
+ state.loading = true;
+ resetNotify(state);
+ });
+
+ builder.addCase(deleteItemsByIds.fulfilled, (state) => {
+ state.loading = false;
+ fulfilledNotify(state, 'Workers_comp_classes has been deleted');
+ });
+
+ builder.addCase(deleteItemsByIds.rejected, (state, action) => {
+ state.loading = false;
+ rejectNotify(state, action);
+ });
+
+ builder.addCase(deleteItem.pending, (state) => {
+ state.loading = true
+ resetNotify(state);
+ })
+
+ builder.addCase(deleteItem.fulfilled, (state) => {
+ state.loading = false
+ fulfilledNotify(state, `${'Workers_comp_classes'.slice(0, -1)} has been deleted`);
+ })
+
+ builder.addCase(deleteItem.rejected, (state, action) => {
+ state.loading = false
+ rejectNotify(state, action);
+ })
+
+ builder.addCase(create.pending, (state) => {
+ state.loading = true
+ resetNotify(state);
+ })
+ builder.addCase(create.rejected, (state, action) => {
+ state.loading = false
+ rejectNotify(state, action);
+ })
+
+ builder.addCase(create.fulfilled, (state) => {
+ state.loading = false
+ fulfilledNotify(state, `${'Workers_comp_classes'.slice(0, -1)} has been created`);
+ })
+
+ builder.addCase(update.pending, (state) => {
+ state.loading = true
+ resetNotify(state);
+ })
+ builder.addCase(update.fulfilled, (state) => {
+ state.loading = false
+ fulfilledNotify(state, `${'Workers_comp_classes'.slice(0, -1)} has been updated`);
+ })
+ builder.addCase(update.rejected, (state, action) => {
+ state.loading = false
+ rejectNotify(state, action);
+ })
+
+ builder.addCase(uploadCsv.pending, (state) => {
+ state.loading = true;
+ resetNotify(state);
+ })
+ builder.addCase(uploadCsv.fulfilled, (state) => {
+ state.loading = false;
+ fulfilledNotify(state, 'Workers_comp_classes has been uploaded');
+ })
+ builder.addCase(uploadCsv.rejected, (state, action) => {
+ state.loading = false;
+ rejectNotify(state, action);
+ })
+
+
+ },
+})
+
+// Action creators are generated for each case reducer function
+ export const { setRefetch } = workers_comp_classesSlice.actions
+
+export default workers_comp_classesSlice.reducer
diff --git a/patch.js b/patch.js
new file mode 100644
index 0000000..c504bee
--- /dev/null
+++ b/patch.js
@@ -0,0 +1,21 @@
+const fs = require('fs');
+
+const file = 'backend/src/services/payroll_line_items.js';
+let content = fs.readFileSync(file, 'utf8');
+
+const createLogic = `
+ 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;
+ }
+ }
+ }
+`;
+
+content = content.replace(/try {\n await Payroll_line_itemsDBApi.create/g, 'try {' + createLogic + ' await Payroll_line_itemsDBApi.create');
+content = content.replace(/try {\n const updatedPayroll_line_items = await Payroll_line_itemsDBApi.update/g, 'try {' + createLogic + ' const updatedPayroll_line_items = await Payroll_line_itemsDBApi.update');
+
+fs.writeFileSync(file, content);
\ No newline at end of file