Forced merge: merge ai-dev into master
0
.perm_test_apache
Normal file
0
.perm_test_exec
Normal file
BIN
assets/pasted-20260314-132921-6765892c.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/pasted-20260413-130727-e67a0305.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
assets/pasted-20260413-131715-2c4e92ec.png
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
assets/pasted-20260413-145044-bf0d1263.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
@ -3,6 +3,7 @@ const db = require('../models');
|
|||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
const { isRestrictedPayrollUser } = require('../../security/payrollAccess');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -205,12 +206,17 @@ module.exports = class Employee_pay_typesDBApi {
|
|||||||
|
|
||||||
static async findBy(where, options) {
|
static async findBy(where, options) {
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
|
|
||||||
const employee_pay_types = await db.employee_pay_types.findOne(
|
const employee_pay_types = await db.employee_pay_types.findOne(
|
||||||
{ where },
|
{ where },
|
||||||
{ transaction },
|
{ transaction },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (employee_pay_types && isRestrictedPayrollUser(currentUser) && employee_pay_types.employeeId !== currentUser.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!employee_pay_types) {
|
if (!employee_pay_types) {
|
||||||
return employee_pay_types;
|
return employee_pay_types;
|
||||||
}
|
}
|
||||||
@ -249,6 +255,8 @@ module.exports = class Employee_pay_typesDBApi {
|
|||||||
filter,
|
filter,
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
|
const effectiveEmployeeFilter = isRestrictedPayrollUser(currentUser) ? currentUser.id : filter.employee;
|
||||||
const limit = filter.limit || 0;
|
const limit = filter.limit || 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let where = {};
|
let where = {};
|
||||||
@ -270,16 +278,16 @@ module.exports = class Employee_pay_typesDBApi {
|
|||||||
model: db.users,
|
model: db.users,
|
||||||
as: 'employee',
|
as: 'employee',
|
||||||
|
|
||||||
where: filter.employee ? {
|
where: effectiveEmployeeFilter ? {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ id: { [Op.in]: filter.employee.split('|').map(term => Utils.uuid(term)) } },
|
{ id: { [Op.in]: effectiveEmployeeFilter.split('|').map(term => Utils.uuid(term)) } },
|
||||||
{
|
{
|
||||||
firstName: {
|
firstName: {
|
||||||
[Op.or]: filter.employee.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
|
[Op.or]: effectiveEmployeeFilter.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -296,7 +304,7 @@ module.exports = class Employee_pay_typesDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -447,7 +455,8 @@ module.exports = class Employee_pay_typesDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAllAutocomplete(query, limit, offset, ) {
|
static async findAllAutocomplete(query, limit, offset, options) {
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
let where = {};
|
let where = {};
|
||||||
|
|
||||||
|
|
||||||
@ -465,6 +474,13 @@ module.exports = class Employee_pay_typesDBApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser)) {
|
||||||
|
where = {
|
||||||
|
...where,
|
||||||
|
employeeId: currentUser.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const records = await db.employee_pay_types.findAll({
|
const records = await db.employee_pay_types.findAll({
|
||||||
attributes: [ 'id', 'active' ],
|
attributes: [ 'id', 'active' ],
|
||||||
where,
|
where,
|
||||||
|
|||||||
@ -264,7 +264,7 @@ module.exports = class Job_chemical_usagesDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -281,7 +281,7 @@ module.exports = class Job_chemical_usagesDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ const db = require('../models');
|
|||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
const { isRestrictedPayrollUser } = require('../../security/payrollAccess');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -90,6 +91,10 @@ module.exports = class Job_logsDBApi {
|
|||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await job_logs.setWorkersCompClass( data.workersCompClass || null, {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -248,6 +253,15 @@ module.exports = class Job_logsDBApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.workersCompClass !== undefined) {
|
||||||
|
await job_logs.setWorkersCompClass(
|
||||||
|
|
||||||
|
data.workersCompClass,
|
||||||
|
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -307,12 +321,17 @@ module.exports = class Job_logsDBApi {
|
|||||||
|
|
||||||
static async findBy(where, options) {
|
static async findBy(where, options) {
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
|
|
||||||
const job_logs = await db.job_logs.findOne(
|
const job_logs = await db.job_logs.findOne(
|
||||||
{ where },
|
{ where },
|
||||||
{ transaction },
|
{ transaction },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (job_logs && isRestrictedPayrollUser(currentUser) && job_logs.employeeId !== currentUser.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!job_logs) {
|
if (!job_logs) {
|
||||||
return job_logs;
|
return job_logs;
|
||||||
}
|
}
|
||||||
@ -356,6 +375,10 @@ module.exports = class Job_logsDBApi {
|
|||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
|
|
||||||
|
output.workersCompClass = await job_logs.getWorkersCompClass({
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
@ -365,6 +388,7 @@ module.exports = class Job_logsDBApi {
|
|||||||
filter,
|
filter,
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
const limit = filter.limit || 0;
|
const limit = filter.limit || 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let where = {};
|
let where = {};
|
||||||
@ -395,7 +419,7 @@ module.exports = class Job_logsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -412,7 +436,7 @@ module.exports = class Job_logsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -429,7 +453,7 @@ module.exports = class Job_logsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -446,14 +470,36 @@ module.exports = class Job_logsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
model: db.workers_comp_classes,
|
||||||
|
as: 'workersCompClass',
|
||||||
|
where: filter.workersCompClass ? {
|
||||||
|
[Op.or]: [
|
||||||
|
{ id: { [Op.in]: filter.workersCompClass.split('|').map(term => Utils.uuid(term)) } },
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
[Op.or]: filter.workersCompClass.split('|').map(term => ({ [Op.iLike]: `%${term}%` }))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} : undefined,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser)) {
|
||||||
|
where = {
|
||||||
|
...where,
|
||||||
|
employeeId: currentUser.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
if (filter.id) {
|
if (filter.id) {
|
||||||
where = {
|
where = {
|
||||||
@ -720,7 +766,8 @@ module.exports = class Job_logsDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAllAutocomplete(query, limit, offset, ) {
|
static async findAllAutocomplete(query, limit, offset, options) {
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
let where = {};
|
let where = {};
|
||||||
|
|
||||||
|
|
||||||
@ -738,6 +785,13 @@ module.exports = class Job_logsDBApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser)) {
|
||||||
|
where = {
|
||||||
|
...where,
|
||||||
|
employeeId: currentUser.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const records = await db.job_logs.findAll({
|
const records = await db.job_logs.findAll({
|
||||||
attributes: [ 'id', 'job_address' ],
|
attributes: [ 'id', 'job_address' ],
|
||||||
where,
|
where,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ const db = require('../models');
|
|||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
const { isRestrictedPayrollUser } = require('../../security/payrollAccess');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -141,10 +142,10 @@ module.exports = class Pay_typesDBApi {
|
|||||||
if (data.pay_method !== undefined) updatePayload.pay_method = data.pay_method;
|
if (data.pay_method !== undefined) updatePayload.pay_method = data.pay_method;
|
||||||
|
|
||||||
|
|
||||||
if (data.hourly_rate !== undefined) updatePayload.hourly_rate = data.hourly_rate;
|
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;
|
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.active !== undefined) updatePayload.active = data.active;
|
||||||
@ -218,9 +219,20 @@ module.exports = class Pay_typesDBApi {
|
|||||||
|
|
||||||
static async findBy(where, options) {
|
static async findBy(where, options) {
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
|
const accessInclude = isRestrictedPayrollUser(currentUser) ? [{
|
||||||
|
model: db.employee_pay_types,
|
||||||
|
as: 'employee_pay_types_pay_type',
|
||||||
|
required: true,
|
||||||
|
attributes: [],
|
||||||
|
where: {
|
||||||
|
employeeId: currentUser.id,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
}] : undefined;
|
||||||
|
|
||||||
const pay_types = await db.pay_types.findOne(
|
const pay_types = await db.pay_types.findOne(
|
||||||
{ where },
|
{ where, include: accessInclude },
|
||||||
{ transaction },
|
{ transaction },
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -238,13 +250,15 @@ module.exports = class Pay_typesDBApi {
|
|||||||
|
|
||||||
|
|
||||||
output.employee_pay_types_pay_type = await pay_types.getEmployee_pay_types_pay_type({
|
output.employee_pay_types_pay_type = await pay_types.getEmployee_pay_types_pay_type({
|
||||||
transaction
|
transaction,
|
||||||
|
where: isRestrictedPayrollUser(currentUser) ? { employeeId: currentUser.id, active: true } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.job_logs_pay_type = await pay_types.getJob_logs_pay_type({
|
output.job_logs_pay_type = await pay_types.getJob_logs_pay_type({
|
||||||
transaction
|
transaction,
|
||||||
|
where: isRestrictedPayrollUser(currentUser) ? { employeeId: currentUser.id } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -260,6 +274,7 @@ module.exports = class Pay_typesDBApi {
|
|||||||
filter,
|
filter,
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
const limit = filter.limit || 0;
|
const limit = filter.limit || 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let where = {};
|
let where = {};
|
||||||
@ -281,6 +296,22 @@ module.exports = class Pay_typesDBApi {
|
|||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser)) {
|
||||||
|
include = [
|
||||||
|
{
|
||||||
|
model: db.employee_pay_types,
|
||||||
|
as: 'employee_pay_types_pay_type',
|
||||||
|
required: true,
|
||||||
|
attributes: [],
|
||||||
|
where: {
|
||||||
|
employeeId: currentUser.id,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...include,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
if (filter.id) {
|
if (filter.id) {
|
||||||
where = {
|
where = {
|
||||||
@ -449,7 +480,8 @@ module.exports = class Pay_typesDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAllAutocomplete(query, limit, offset, ) {
|
static async findAllAutocomplete(query, limit, offset, options) {
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
let where = {};
|
let where = {};
|
||||||
|
|
||||||
|
|
||||||
@ -470,6 +502,16 @@ module.exports = class Pay_typesDBApi {
|
|||||||
const records = await db.pay_types.findAll({
|
const records = await db.pay_types.findAll({
|
||||||
attributes: [ 'id', 'name' ],
|
attributes: [ 'id', 'name' ],
|
||||||
where,
|
where,
|
||||||
|
include: isRestrictedPayrollUser(currentUser) ? [{
|
||||||
|
model: db.employee_pay_types,
|
||||||
|
as: 'employee_pay_types_pay_type',
|
||||||
|
required: true,
|
||||||
|
attributes: [],
|
||||||
|
where: {
|
||||||
|
employeeId: currentUser.id,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
}] : undefined,
|
||||||
limit: limit ? Number(limit) : undefined,
|
limit: limit ? Number(limit) : undefined,
|
||||||
offset: offset ? Number(offset) : undefined,
|
offset: offset ? Number(offset) : undefined,
|
||||||
orderBy: [['name', 'ASC']],
|
orderBy: [['name', 'ASC']],
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -36,6 +35,11 @@ module.exports = class Payroll_line_itemsDBApi {
|
|||||||
null
|
null
|
||||||
,
|
,
|
||||||
|
|
||||||
|
workers_comp_amount: data.workers_comp_amount
|
||||||
|
||
|
||||||
|
null
|
||||||
|
,
|
||||||
|
|
||||||
total_client_paid: data.total_client_paid
|
total_client_paid: data.total_client_paid
|
||||||
||
|
||
|
||||||
null
|
null
|
||||||
@ -92,6 +96,11 @@ module.exports = class Payroll_line_itemsDBApi {
|
|||||||
total_commission_base: item.total_commission_base
|
total_commission_base: item.total_commission_base
|
||||||
||
|
||
|
||||||
null
|
null
|
||||||
|
,
|
||||||
|
|
||||||
|
workers_comp_amount: item.workers_comp_amount
|
||||||
|
||
|
||||||
|
null
|
||||||
,
|
,
|
||||||
|
|
||||||
total_client_paid: item.total_client_paid
|
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.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;
|
if (data.total_client_paid !== undefined) updatePayload.total_client_paid = data.total_client_paid;
|
||||||
|
|
||||||
|
|
||||||
@ -303,7 +315,7 @@ module.exports = class Payroll_line_itemsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -320,7 +332,7 @@ module.exports = class Payroll_line_itemsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -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) {
|
if (filter.total_client_paidRange) {
|
||||||
const [start, end] = filter.total_client_paidRange;
|
const [start, end] = filter.total_client_paidRange;
|
||||||
|
|
||||||
@ -556,5 +592,4 @@ module.exports = class Payroll_line_itemsDBApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -240,9 +240,19 @@ module.exports = class Payroll_runsDBApi {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
output.payroll_line_items_payroll_run = await payroll_runs.getPayroll_line_items_payroll_run({
|
output.payroll_line_items_payroll_run = await Promise.all(
|
||||||
transaction
|
(await payroll_runs.getPayroll_line_items_payroll_run({
|
||||||
});
|
transaction
|
||||||
|
})).map(async (payrollLineItem) => {
|
||||||
|
const payrollLineItemOutput = payrollLineItem.get({ plain: true });
|
||||||
|
|
||||||
|
payrollLineItemOutput.employee = await payrollLineItem.getEmployee({
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
return payrollLineItemOutput;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ const db = require('../models');
|
|||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
const { isRestrictedPayrollUser } = require('../../security/payrollAccess');
|
||||||
|
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const config = require('../../config');
|
const config = require('../../config');
|
||||||
@ -387,9 +388,19 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
static async findBy(where, options) {
|
static async findBy(where, options) {
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
|
const effectiveWhere = { ...(where || {}) };
|
||||||
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser)) {
|
||||||
|
if (effectiveWhere.id && Utils.uuid(effectiveWhere.id) !== currentUser.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveWhere.id = currentUser.id;
|
||||||
|
}
|
||||||
|
|
||||||
const users = await db.users.findOne(
|
const users = await db.users.findOne(
|
||||||
{ where },
|
{ where: effectiveWhere },
|
||||||
{ transaction },
|
{ transaction },
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -407,7 +418,14 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
|
|
||||||
output.employee_pay_types_employee = await users.getEmployee_pay_types_employee({
|
output.employee_pay_types_employee = await users.getEmployee_pay_types_employee({
|
||||||
transaction
|
transaction,
|
||||||
|
where: isRestrictedPayrollUser(currentUser) ? { employeeId: currentUser.id, active: true } : undefined,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: db.pay_types,
|
||||||
|
as: 'pay_type',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -454,6 +472,7 @@ module.exports = class UsersDBApi {
|
|||||||
filter,
|
filter,
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
const limit = filter.limit || 0;
|
const limit = filter.limit || 0;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let where = {};
|
let where = {};
|
||||||
@ -484,7 +503,7 @@ module.exports = class UsersDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -501,8 +520,30 @@ module.exports = class UsersDBApi {
|
|||||||
as: 'avatar',
|
as: 'avatar',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
model: db.employee_pay_types,
|
||||||
|
as: 'employee_pay_types_employee',
|
||||||
|
required: false,
|
||||||
|
where: {
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: db.pay_types,
|
||||||
|
as: 'pay_type',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser)) {
|
||||||
|
where = {
|
||||||
|
...where,
|
||||||
|
id: currentUser.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
if (filter.id) {
|
if (filter.id) {
|
||||||
where = {
|
where = {
|
||||||
@ -762,7 +803,8 @@ module.exports = class UsersDBApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async findAllAutocomplete(query, limit, offset, ) {
|
static async findAllAutocomplete(query, limit, offset, options) {
|
||||||
|
const currentUser = options?.currentUser;
|
||||||
let where = {};
|
let where = {};
|
||||||
|
|
||||||
|
|
||||||
@ -780,6 +822,13 @@ module.exports = class UsersDBApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser)) {
|
||||||
|
where = {
|
||||||
|
...where,
|
||||||
|
id: currentUser.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const records = await db.users.findAll({
|
const records = await db.users.findAll({
|
||||||
attributes: [ 'id', 'firstName' ],
|
attributes: [ 'id', 'firstName' ],
|
||||||
where,
|
where,
|
||||||
|
|||||||
188
backend/src/db/api/workers_comp_classes.js
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
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,
|
||||||
|
percentage: data.percentage !== undefined ? data.percentage : null,
|
||||||
|
importHash: data.importHash || null,
|
||||||
|
createdById: currentUser.id,
|
||||||
|
updatedById: currentUser.id,
|
||||||
|
},
|
||||||
|
{ transaction },
|
||||||
|
);
|
||||||
|
|
||||||
|
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.percentage !== undefined) updatePayload.percentage = data.percentage === "" ? null : data.percentage;
|
||||||
|
|
||||||
|
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});
|
||||||
|
|
||||||
|
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 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryOptions = {
|
||||||
|
where,
|
||||||
|
include,
|
||||||
|
distinct: true,
|
||||||
|
order: filter && filter.field && filter.sort
|
||||||
|
? [[filter.field, filter.sort]]
|
||||||
|
: [['createdAt', 'desc']],
|
||||||
|
transaction: options && options.transaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!(options && 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 && 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,
|
||||||
|
order: [['name', 'ASC']],
|
||||||
|
});
|
||||||
|
|
||||||
|
return records.map((record) => ({
|
||||||
|
id: record.id,
|
||||||
|
label: record.name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
32
backend/src/db/migrations/1773330455690.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.createTable("usersCustom_permissionsPermissions", {
|
||||||
|
id: {
|
||||||
|
type: Sequelize.DataTypes.UUID,
|
||||||
|
defaultValue: Sequelize.DataTypes.UUIDV4,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
users_custom_permissionsId: {
|
||||||
|
type: Sequelize.DataTypes.UUID,
|
||||||
|
references: {
|
||||||
|
model: "users",
|
||||||
|
key: "id",
|
||||||
|
},
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
},
|
||||||
|
permissionId: {
|
||||||
|
type: Sequelize.DataTypes.UUID,
|
||||||
|
references: {
|
||||||
|
model: "permissions",
|
||||||
|
key: "id",
|
||||||
|
},
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
},
|
||||||
|
createdAt: { type: Sequelize.DataTypes.DATE },
|
||||||
|
updatedAt: { type: Sequelize.DataTypes.DATE },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.dropTable("usersCustom_permissionsPermissions");
|
||||||
|
}
|
||||||
|
};
|
||||||
12
backend/src/db/migrations/1773330455692.js
Normal file
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
12
backend/src/db/migrations/1773330455693.js
Normal file
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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');
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
'use strict';
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
const createdAt = new Date();
|
||||||
|
const updatedAt = new Date();
|
||||||
|
// Find all roles
|
||||||
|
const roles = await queryInterface.sequelize.query(
|
||||||
|
`SELECT id, name FROM roles WHERE name != 'Public';`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
// Find the read permission
|
||||||
|
const permissions = await queryInterface.sequelize.query(
|
||||||
|
`SELECT id, name FROM permissions WHERE name = 'READ_WORKERS_COMP_CLASSES';`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
if (permissions.length === 0) return;
|
||||||
|
const readPerm = permissions[0];
|
||||||
|
const rolePermissions = [];
|
||||||
|
const existing = await queryInterface.sequelize.query(
|
||||||
|
`SELECT "roles_permissionsId", "permissionId" FROM "rolesPermissionsPermissions" WHERE "permissionId" = '${readPerm.id}';`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
const existingSet = new Set(existing.map(e => e.roles_permissionsId + '-' + e.permissionId));
|
||||||
|
for (const role of roles) {
|
||||||
|
if (!existingSet.has(role.id + '-' + readPerm.id)) {
|
||||||
|
rolePermissions.push({
|
||||||
|
roles_permissionsId: role.id,
|
||||||
|
permissionId: readPerm.id,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rolePermissions.length > 0) {
|
||||||
|
await queryInterface.bulkInsert('rolesPermissionsPermissions', rolePermissions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down: async (queryInterface, Sequelize) => {}
|
||||||
|
};
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
'use strict';
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
const createdAt = new Date();
|
||||||
|
const updatedAt = new Date();
|
||||||
|
|
||||||
|
const roles = await queryInterface.sequelize.query(
|
||||||
|
`SELECT id, name FROM roles WHERE name != 'Public';`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
|
||||||
|
const permissions = await queryInterface.sequelize.query(
|
||||||
|
`SELECT id, name FROM permissions WHERE name = 'READ_EMPLOYEE_PAY_TYPES';`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (permissions.length === 0) return;
|
||||||
|
const readPerm = permissions[0];
|
||||||
|
const rolePermissions = [];
|
||||||
|
|
||||||
|
const existing = await queryInterface.sequelize.query(
|
||||||
|
`SELECT "roles_permissionsId", "permissionId" FROM "rolesPermissionsPermissions" WHERE "permissionId" = '${readPerm.id}';`,
|
||||||
|
{ type: Sequelize.QueryTypes.SELECT }
|
||||||
|
);
|
||||||
|
const existingSet = new Set(existing.map(e => e.roles_permissionsId + '-' + e.permissionId));
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
if (!existingSet.has(role.id + '-' + readPerm.id)) {
|
||||||
|
rolePermissions.push({
|
||||||
|
roles_permissionsId: role.id,
|
||||||
|
permissionId: readPerm.id,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rolePermissions.length > 0) {
|
||||||
|
await queryInterface.bulkInsert('rolesPermissionsPermissions', rolePermissions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down: async (queryInterface, Sequelize) => {}
|
||||||
|
};
|
||||||
@ -35,25 +35,6 @@ client_paid: {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
workers_comp_class: {
|
|
||||||
type: DataTypes.ENUM,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
values: [
|
|
||||||
|
|
||||||
"roof",
|
|
||||||
|
|
||||||
|
|
||||||
"ladder",
|
|
||||||
|
|
||||||
|
|
||||||
"ground"
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
odometer_start: {
|
odometer_start: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
|
|
||||||
@ -180,6 +161,14 @@ notes_to_admin: {
|
|||||||
constraints: false,
|
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;
|
return job_logs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -49,6 +49,13 @@ commission_rate: {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
workers_comp_percentage: {
|
||||||
|
type: DataTypes.DECIMAL,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
active: {
|
active: {
|
||||||
@ -135,6 +142,4 @@ description: {
|
|||||||
|
|
||||||
|
|
||||||
return pay_types;
|
return pay_types;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -33,6 +33,13 @@ total_commission_base: {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
workers_comp_amount: {
|
||||||
|
type: DataTypes.DECIMAL,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
total_client_paid: {
|
total_client_paid: {
|
||||||
@ -116,6 +123,4 @@ summary: {
|
|||||||
|
|
||||||
|
|
||||||
return payroll_line_items;
|
return payroll_line_items;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
54
backend/src/db/models/workers_comp_classes.js
Normal file
@ -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;
|
||||||
|
};
|
||||||
@ -32,6 +32,8 @@ const customersRoutes = require('./routes/customers');
|
|||||||
const vehiclesRoutes = require('./routes/vehicles');
|
const vehiclesRoutes = require('./routes/vehicles');
|
||||||
|
|
||||||
const pay_typesRoutes = require('./routes/pay_types');
|
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');
|
const employee_pay_typesRoutes = require('./routes/employee_pay_types');
|
||||||
|
|
||||||
@ -44,6 +46,7 @@ const job_chemical_usagesRoutes = require('./routes/job_chemical_usages');
|
|||||||
const payroll_runsRoutes = require('./routes/payroll_runs');
|
const payroll_runsRoutes = require('./routes/payroll_runs');
|
||||||
|
|
||||||
const payroll_line_itemsRoutes = require('./routes/payroll_line_items');
|
const payroll_line_itemsRoutes = require('./routes/payroll_line_items');
|
||||||
|
const reportsRoutes = require("./routes/reports");
|
||||||
|
|
||||||
|
|
||||||
const getBaseUrl = (url) => {
|
const getBaseUrl = (url) => {
|
||||||
@ -114,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/vehicles', passport.authenticate('jwt', {session: false}), vehiclesRoutes);
|
||||||
|
|
||||||
app.use('/api/pay_types', passport.authenticate('jwt', {session: false}), pay_typesRoutes);
|
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);
|
app.use('/api/employee_pay_types', passport.authenticate('jwt', {session: false}), employee_pay_typesRoutes);
|
||||||
|
|
||||||
@ -126,6 +131,8 @@ 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_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/payroll_line_items', passport.authenticate('jwt', {session: false}), payroll_line_itemsRoutes);
|
||||||
|
app.use('/api/reports', passport.authenticate('jwt', {session: false}), reportsRoutes);
|
||||||
|
app.use('/api/payroll_generator', require('./routes/payroll_generator'));
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/api/openai',
|
'/api/openai',
|
||||||
@ -166,7 +173,7 @@ if (fs.existsSync(publicDir)) {
|
|||||||
const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080;
|
const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080;
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Listening on port ${PORT}`);
|
console.log(`Listening on port ${PORT}`); console.log('Watcher triggered');
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
@ -335,7 +335,6 @@ router.get('/count', wrapAsync(async (req, res) => {
|
|||||||
const currentUser = req.currentUser;
|
const currentUser = req.currentUser;
|
||||||
const payload = await Employee_pay_typesDBApi.findAll(
|
const payload = await Employee_pay_typesDBApi.findAll(
|
||||||
req.query,
|
req.query,
|
||||||
null,
|
|
||||||
{ countOnly: true, currentUser }
|
{ countOnly: true, currentUser }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -373,7 +372,7 @@ router.get('/autocomplete', async (req, res) => {
|
|||||||
req.query.query,
|
req.query.query,
|
||||||
req.query.limit,
|
req.query.limit,
|
||||||
req.query.offset,
|
req.query.offset,
|
||||||
|
{ currentUser: req.currentUser },
|
||||||
);
|
);
|
||||||
|
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
@ -414,6 +413,7 @@ router.get('/autocomplete', async (req, res) => {
|
|||||||
router.get('/:id', wrapAsync(async (req, res) => {
|
router.get('/:id', wrapAsync(async (req, res) => {
|
||||||
const payload = await Employee_pay_typesDBApi.findBy(
|
const payload = await Employee_pay_typesDBApi.findBy(
|
||||||
{ id: req.params.id },
|
{ id: req.params.id },
|
||||||
|
{ currentUser: req.currentUser },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -355,7 +355,6 @@ router.get('/count', wrapAsync(async (req, res) => {
|
|||||||
const currentUser = req.currentUser;
|
const currentUser = req.currentUser;
|
||||||
const payload = await Job_logsDBApi.findAll(
|
const payload = await Job_logsDBApi.findAll(
|
||||||
req.query,
|
req.query,
|
||||||
null,
|
|
||||||
{ countOnly: true, currentUser }
|
{ countOnly: true, currentUser }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -393,7 +392,7 @@ router.get('/autocomplete', async (req, res) => {
|
|||||||
req.query.query,
|
req.query.query,
|
||||||
req.query.limit,
|
req.query.limit,
|
||||||
req.query.offset,
|
req.query.offset,
|
||||||
|
{ currentUser: req.currentUser },
|
||||||
);
|
);
|
||||||
|
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
@ -434,6 +433,7 @@ router.get('/autocomplete', async (req, res) => {
|
|||||||
router.get('/:id', wrapAsync(async (req, res) => {
|
router.get('/:id', wrapAsync(async (req, res) => {
|
||||||
const payload = await Job_logsDBApi.findBy(
|
const payload = await Job_logsDBApi.findBy(
|
||||||
{ id: req.params.id },
|
{ id: req.params.id },
|
||||||
|
{ currentUser: req.currentUser },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -348,7 +348,6 @@ router.get('/count', wrapAsync(async (req, res) => {
|
|||||||
const currentUser = req.currentUser;
|
const currentUser = req.currentUser;
|
||||||
const payload = await Pay_typesDBApi.findAll(
|
const payload = await Pay_typesDBApi.findAll(
|
||||||
req.query,
|
req.query,
|
||||||
null,
|
|
||||||
{ countOnly: true, currentUser }
|
{ countOnly: true, currentUser }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -386,7 +385,7 @@ router.get('/autocomplete', async (req, res) => {
|
|||||||
req.query.query,
|
req.query.query,
|
||||||
req.query.limit,
|
req.query.limit,
|
||||||
req.query.offset,
|
req.query.offset,
|
||||||
|
{ currentUser: req.currentUser },
|
||||||
);
|
);
|
||||||
|
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
@ -427,6 +426,7 @@ router.get('/autocomplete', async (req, res) => {
|
|||||||
router.get('/:id', wrapAsync(async (req, res) => {
|
router.get('/:id', wrapAsync(async (req, res) => {
|
||||||
const payload = await Pay_typesDBApi.findBy(
|
const payload = await Pay_typesDBApi.findBy(
|
||||||
{ id: req.params.id },
|
{ id: req.params.id },
|
||||||
|
{ currentUser: req.currentUser },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
208
backend/src/routes/payroll_generator.js
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
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');
|
||||||
|
|
||||||
|
const getInclusiveDateRange = (startDate, endDate) => ({
|
||||||
|
start: new Date(`${startDate}T00:00:00.000Z`),
|
||||||
|
end: new Date(`${endDate}T23:59:59.999Z`),
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/preview', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => {
|
||||||
|
const { startDate, endDate } = req.body;
|
||||||
|
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
return res.status(400).send('startDate and endDate are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { start, end } = getInclusiveDateRange(startDate, endDate);
|
||||||
|
|
||||||
|
// Find job logs in range that are not paid
|
||||||
|
const jobLogs = await db.job_logs.findAll({
|
||||||
|
where: {
|
||||||
|
work_date: {
|
||||||
|
[Op.between]: [start, end]
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
[Op.ne]: 'paid'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{ model: db.users, as: 'employee' },
|
||||||
|
{ model: db.pay_types, as: 'pay_type' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const employeeTotals = {};
|
||||||
|
|
||||||
|
jobLogs.forEach(log => {
|
||||||
|
const empId = log.employeeId;
|
||||||
|
if (!empId) return;
|
||||||
|
|
||||||
|
if (!employeeTotals[empId]) {
|
||||||
|
employeeTotals[empId] = {
|
||||||
|
employee: log.employee,
|
||||||
|
total_hours: 0,
|
||||||
|
total_commission_base: 0,
|
||||||
|
gross_pay: 0,
|
||||||
|
job_log_ids: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const totals = employeeTotals[empId];
|
||||||
|
totals.job_log_ids.push(log.id);
|
||||||
|
|
||||||
|
const hours = parseFloat(log.hours_conducted || 0);
|
||||||
|
totals.total_hours += hours;
|
||||||
|
|
||||||
|
const clientPaid = parseFloat(log.client_paid || 0);
|
||||||
|
totals.total_commission_base += clientPaid;
|
||||||
|
|
||||||
|
let pay = 0;
|
||||||
|
if (log.pay_type) {
|
||||||
|
if (log.pay_type.pay_method === 'hourly') {
|
||||||
|
pay = hours * parseFloat(log.pay_type.hourly_rate || 0);
|
||||||
|
} else if (log.pay_type.pay_method === 'commission') {
|
||||||
|
pay = clientPaid * (parseFloat(log.pay_type.commission_rate || 0) / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totals.gross_pay += pay;
|
||||||
|
});
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
totalGrossPay: 0,
|
||||||
|
totalHours: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineItems = Object.values(employeeTotals).map(item => {
|
||||||
|
summary.totalGrossPay += item.gross_pay;
|
||||||
|
summary.totalHours += item.total_hours;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ lineItems, summary });
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/generate', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => {
|
||||||
|
const { startDate, endDate, name, notes } = req.body;
|
||||||
|
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
return res.status(400).send('startDate and endDate are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { start, end } = getInclusiveDateRange(startDate, endDate);
|
||||||
|
|
||||||
|
// Find job logs
|
||||||
|
const jobLogs = await db.job_logs.findAll({
|
||||||
|
where: {
|
||||||
|
work_date: {
|
||||||
|
[Op.between]: [start, end]
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
[Op.ne]: 'paid'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{ model: db.users, as: 'employee' },
|
||||||
|
{ model: db.pay_types, as: 'pay_type' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (jobLogs.length === 0) {
|
||||||
|
return res.status(400).send('No unpaid job logs found in this date range.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const employeeTotals = {};
|
||||||
|
|
||||||
|
jobLogs.forEach(log => {
|
||||||
|
const empId = log.employeeId;
|
||||||
|
if (!empId) return;
|
||||||
|
|
||||||
|
if (!employeeTotals[empId]) {
|
||||||
|
employeeTotals[empId] = {
|
||||||
|
total_hours: 0,
|
||||||
|
total_commission_base: 0,
|
||||||
|
gross_pay: 0,
|
||||||
|
workers_comp_amount: 0,
|
||||||
|
employeeId: empId,
|
||||||
|
job_log_ids: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const totals = employeeTotals[empId];
|
||||||
|
totals.job_log_ids.push(log.id);
|
||||||
|
|
||||||
|
const hours = parseFloat(log.hours_conducted || 0);
|
||||||
|
totals.total_hours += hours;
|
||||||
|
|
||||||
|
const clientPaid = parseFloat(log.client_paid || 0);
|
||||||
|
totals.total_commission_base += clientPaid;
|
||||||
|
|
||||||
|
let pay = 0;
|
||||||
|
let wcAmount = 0;
|
||||||
|
if (log.pay_type) {
|
||||||
|
if (log.pay_type.pay_method === 'hourly') {
|
||||||
|
pay = hours * parseFloat(log.pay_type.hourly_rate || 0);
|
||||||
|
} else if (log.pay_type.pay_method === 'commission') {
|
||||||
|
pay = clientPaid * (parseFloat(log.pay_type.commission_rate || 0) / 100);
|
||||||
|
}
|
||||||
|
if (log.pay_type.workers_comp_percentage) {
|
||||||
|
wcAmount = pay * (parseFloat(log.pay_type.workers_comp_percentage) / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totals.gross_pay += pay;
|
||||||
|
totals.workers_comp_amount += wcAmount;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a transaction
|
||||||
|
const transaction = await db.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
// Create payroll run
|
||||||
|
const payrollRun = await db.payroll_runs.create({
|
||||||
|
name: name || `Payroll ${startDate} to ${endDate}`,
|
||||||
|
period_start: startDate,
|
||||||
|
period_end: endDate,
|
||||||
|
run_date: new Date(),
|
||||||
|
status: 'finalized',
|
||||||
|
notes: notes || '',
|
||||||
|
createdById: req.currentUser.id
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
// Create line items
|
||||||
|
for (const empId of Object.keys(employeeTotals)) {
|
||||||
|
const totals = employeeTotals[empId];
|
||||||
|
await db.payroll_line_items.create({
|
||||||
|
payroll_runId: payrollRun.id,
|
||||||
|
employeeId: totals.employeeId,
|
||||||
|
total_hours: totals.total_hours,
|
||||||
|
gross_pay: totals.gross_pay,
|
||||||
|
total_commission_base: totals.total_commission_base,
|
||||||
|
workers_comp_amount: totals.workers_comp_amount,
|
||||||
|
createdById: req.currentUser.id
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
// Mark job logs as paid
|
||||||
|
await db.job_logs.update({
|
||||||
|
status: 'paid'
|
||||||
|
}, {
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
[Op.in]: totals.job_log_ids
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
res.json({ success: true, payrollRunId: payrollRun.id });
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).send('Error generating payroll');
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
39
backend/src/routes/reports.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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');
|
||||||
|
|
||||||
|
const getDateBoundary = (date, boundary) =>
|
||||||
|
new Date(`${date}T${boundary === 'start' ? '00:00:00.000' : '23:59:59.999'}Z`);
|
||||||
|
|
||||||
|
router.post('/', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => {
|
||||||
|
const { startDate, endDate, employeeId } = req.body;
|
||||||
|
|
||||||
|
const where = {};
|
||||||
|
if (startDate || endDate) {
|
||||||
|
where.createdAt = {};
|
||||||
|
if (startDate) where.createdAt[Op.gte] = getDateBoundary(startDate, 'start');
|
||||||
|
if (endDate) where.createdAt[Op.lte] = getDateBoundary(endDate, 'end');
|
||||||
|
}
|
||||||
|
if (employeeId) {
|
||||||
|
where.employeeId = employeeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineItems = await db.payroll_line_items.findAll({
|
||||||
|
where,
|
||||||
|
include: [{ model: db.users, as: 'employee' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
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, totalWorkersComp: 0 });
|
||||||
|
|
||||||
|
res.json({ lineItems, summary });
|
||||||
|
}));
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@ -347,7 +347,6 @@ router.get('/count', wrapAsync(async (req, res) => {
|
|||||||
const currentUser = req.currentUser;
|
const currentUser = req.currentUser;
|
||||||
const payload = await UsersDBApi.findAll(
|
const payload = await UsersDBApi.findAll(
|
||||||
req.query,
|
req.query,
|
||||||
null,
|
|
||||||
{ countOnly: true, currentUser }
|
{ countOnly: true, currentUser }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -385,7 +384,7 @@ router.get('/autocomplete', async (req, res) => {
|
|||||||
req.query.query,
|
req.query.query,
|
||||||
req.query.limit,
|
req.query.limit,
|
||||||
req.query.offset,
|
req.query.offset,
|
||||||
|
{ currentUser: req.currentUser },
|
||||||
);
|
);
|
||||||
|
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
@ -426,11 +425,12 @@ router.get('/autocomplete', async (req, res) => {
|
|||||||
router.get('/:id', wrapAsync(async (req, res) => {
|
router.get('/:id', wrapAsync(async (req, res) => {
|
||||||
const payload = await UsersDBApi.findBy(
|
const payload = await UsersDBApi.findBy(
|
||||||
{ id: req.params.id },
|
{ id: req.params.id },
|
||||||
|
{ currentUser: req.currentUser },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (payload) {
|
||||||
delete payload.password;
|
delete payload.password;
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}));
|
}));
|
||||||
|
|||||||
439
backend/src/routes/workers_comp_classes.js
Normal file
@ -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;
|
||||||
79
backend/src/routes/workers_comp_report.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
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');
|
||||||
|
|
||||||
|
const buildDateBoundary = (dateValue, boundary) => {
|
||||||
|
if (!dateValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeSuffix =
|
||||||
|
boundary === 'start' ? 'T00:00:00.000Z' : 'T23:59:59.999Z';
|
||||||
|
|
||||||
|
return new Date(`${dateValue}${timeSuffix}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
router.get('/report', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => {
|
||||||
|
const { startDate, endDate, classId } = req.query;
|
||||||
|
|
||||||
|
const where = {};
|
||||||
|
const start = buildDateBoundary(startDate, 'start');
|
||||||
|
const end = buildDateBoundary(endDate, 'end');
|
||||||
|
|
||||||
|
if (start && end && start > end) {
|
||||||
|
return res.status(400).json({ message: 'startDate must be on or before endDate' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start || end) {
|
||||||
|
where.work_date = {};
|
||||||
|
|
||||||
|
if (start) {
|
||||||
|
where.work_date[Op.gte] = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end) {
|
||||||
|
where.work_date[Op.lte] = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
19
backend/src/security/payrollAccess.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const PRIVILEGED_PAYROLL_ROLE_NAMES = new Set([
|
||||||
|
'Administrator',
|
||||||
|
'System Owner',
|
||||||
|
'Payroll Manager',
|
||||||
|
'Operations Manager',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isRestrictedPayrollUser(currentUser) {
|
||||||
|
if (!currentUser?.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !PRIVILEGED_PAYROLL_ROLE_NAMES.has(currentUser?.app_role?.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
PRIVILEGED_PAYROLL_ROLE_NAMES,
|
||||||
|
isRestrictedPayrollUser,
|
||||||
|
};
|
||||||
@ -1,28 +1,79 @@
|
|||||||
const db = require('../db/models');
|
const db = require('../db/models');
|
||||||
const Job_logsDBApi = require('../db/api/job_logs');
|
const Job_logsDBApi = require('../db/api/job_logs');
|
||||||
|
const CustomersDBApi = require('../db/api/customers');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require("../middlewares/upload");
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
|
const { isRestrictedPayrollUser } = require('../security/payrollAccess');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Job_logsService {
|
module.exports = class Job_logsService {
|
||||||
|
static async ensureRestrictedPayTypeAccess(payTypeId, currentUser, transaction) {
|
||||||
|
if (!isRestrictedPayrollUser(currentUser) || !payTypeId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignment = await db.employee_pay_types.findOne({
|
||||||
|
where: {
|
||||||
|
employeeId: currentUser.id,
|
||||||
|
pay_typeId: payTypeId,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!assignment) {
|
||||||
|
throw new ValidationError('errors.forbidden.message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await Job_logsDBApi.create(
|
let customerId = data.customer;
|
||||||
data,
|
|
||||||
|
// If customer is a string and not a UUID, try to find or create
|
||||||
|
if (typeof customerId === 'string' && customerId.length > 0 && !customerId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) {
|
||||||
|
let customer = await db.customers.findOne({ where: { name: customerId }, transaction });
|
||||||
|
if (!customer) {
|
||||||
|
customer = await CustomersDBApi.create({ name: customerId }, { currentUser, transaction });
|
||||||
|
}
|
||||||
|
customerId = customer.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobPayload = { ...data, customer: customerId };
|
||||||
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser)) {
|
||||||
|
jobPayload.employee = currentUser.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Job_logsService.ensureRestrictedPayTypeAccess(jobPayload.pay_type, currentUser, transaction);
|
||||||
|
|
||||||
|
const createdJob = await Job_logsDBApi.create(
|
||||||
|
jobPayload,
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
transaction,
|
transaction,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (data.chemical_usages && Array.isArray(data.chemical_usages)) {
|
||||||
|
for (const usage of data.chemical_usages) {
|
||||||
|
if (usage.chemical_product && usage.quantity_used) {
|
||||||
|
await db.job_chemical_usages.create({
|
||||||
|
job_logId: createdJob.id,
|
||||||
|
chemical_productId: usage.chemical_product,
|
||||||
|
quantity_used: usage.quantity_used,
|
||||||
|
notes: usage.notes || '',
|
||||||
|
createdById: currentUser.id,
|
||||||
|
updatedById: currentUser.id
|
||||||
|
}, { transaction });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@ -70,7 +121,10 @@ module.exports = class Job_logsService {
|
|||||||
try {
|
try {
|
||||||
let job_logs = await Job_logsDBApi.findBy(
|
let job_logs = await Job_logsDBApi.findBy(
|
||||||
{id},
|
{id},
|
||||||
{transaction},
|
{
|
||||||
|
transaction,
|
||||||
|
currentUser,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!job_logs) {
|
if (!job_logs) {
|
||||||
@ -79,9 +133,36 @@ module.exports = class Job_logsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser) && job_logs.employeeId !== currentUser.id) {
|
||||||
|
throw new ValidationError('errors.forbidden.message');
|
||||||
|
}
|
||||||
|
|
||||||
|
let customerId = data.customer;
|
||||||
|
|
||||||
|
// If customer is a string and not a UUID, try to find or create
|
||||||
|
if (typeof customerId === 'string' && customerId.length > 0 && !customerId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)) {
|
||||||
|
let customer = await db.customers.findOne({ where: { name: customerId }, transaction });
|
||||||
|
if (!customer) {
|
||||||
|
customer = await CustomersDBApi.create({ name: customerId }, { currentUser, transaction });
|
||||||
|
}
|
||||||
|
customerId = customer.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobPayload = { ...data, customer: customerId };
|
||||||
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser)) {
|
||||||
|
jobPayload.employee = currentUser.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Job_logsService.ensureRestrictedPayTypeAccess(
|
||||||
|
jobPayload.pay_type !== undefined ? jobPayload.pay_type : job_logs.pay_typeId,
|
||||||
|
currentUser,
|
||||||
|
transaction,
|
||||||
|
);
|
||||||
|
|
||||||
const updatedJob_logs = await Job_logsDBApi.update(
|
const updatedJob_logs = await Job_logsDBApi.update(
|
||||||
id,
|
id,
|
||||||
data,
|
jobPayload,
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
transaction,
|
transaction,
|
||||||
@ -131,8 +212,4 @@ module.exports = class Job_logsService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@ -15,6 +15,15 @@ module.exports = class Payroll_line_itemsService {
|
|||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
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(
|
await Payroll_line_itemsDBApi.create(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -13,6 +13,63 @@ const EmailSender = require('./email');
|
|||||||
const AuthService = require('./auth');
|
const AuthService = require('./auth');
|
||||||
|
|
||||||
module.exports = class UsersService {
|
module.exports = class UsersService {
|
||||||
|
static normalizePayTypeIds(payTypes = []) {
|
||||||
|
return [...new Set((payTypes || [])
|
||||||
|
.map((payType) => {
|
||||||
|
if (typeof payType === 'string') {
|
||||||
|
return payType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payType?.id || payType?.value || null;
|
||||||
|
})
|
||||||
|
.filter(Boolean))];
|
||||||
|
}
|
||||||
|
|
||||||
|
static async syncEmployeePayTypes(employeeId, payTypes, currentUser, transaction) {
|
||||||
|
const payTypeIds = UsersService.normalizePayTypeIds(payTypes);
|
||||||
|
const selectedPayTypeIds = new Set(payTypeIds);
|
||||||
|
|
||||||
|
const existingAssignments = await db.employee_pay_types.findAll({
|
||||||
|
where: { employeeId },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingPayTypeIds = new Set();
|
||||||
|
|
||||||
|
for (const assignment of existingAssignments) {
|
||||||
|
existingPayTypeIds.add(assignment.pay_typeId);
|
||||||
|
|
||||||
|
const shouldBeActive = selectedPayTypeIds.has(assignment.pay_typeId);
|
||||||
|
|
||||||
|
if (assignment.active !== shouldBeActive) {
|
||||||
|
await assignment.update(
|
||||||
|
{
|
||||||
|
active: shouldBeActive,
|
||||||
|
updatedById: currentUser.id,
|
||||||
|
},
|
||||||
|
{ transaction },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const payTypeId of payTypeIds) {
|
||||||
|
if (existingPayTypeIds.has(payTypeId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.employee_pay_types.create(
|
||||||
|
{
|
||||||
|
employeeId,
|
||||||
|
pay_typeId: payTypeId,
|
||||||
|
active: true,
|
||||||
|
createdById: currentUser.id,
|
||||||
|
updatedById: currentUser.id,
|
||||||
|
},
|
||||||
|
{ transaction },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async create(data, currentUser, sendInvitationEmails = true, host) {
|
static async create(data, currentUser, sendInvitationEmails = true, host) {
|
||||||
let transaction = await db.sequelize.transaction();
|
let transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
@ -26,7 +83,7 @@ module.exports = class UsersService {
|
|||||||
'iam.errors.userAlreadyExists',
|
'iam.errors.userAlreadyExists',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await UsersDBApi.create(
|
const createdUser = await UsersDBApi.create(
|
||||||
{data},
|
{data},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -34,6 +91,15 @@ module.exports = class UsersService {
|
|||||||
transaction,
|
transaction,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (data.pay_types !== undefined) {
|
||||||
|
await UsersService.syncEmployeePayTypes(
|
||||||
|
createdUser.id,
|
||||||
|
data.pay_types,
|
||||||
|
currentUser,
|
||||||
|
transaction,
|
||||||
|
);
|
||||||
|
}
|
||||||
emailsToInvite.push(email);
|
emailsToInvite.push(email);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -127,6 +193,15 @@ module.exports = class UsersService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (data.pay_types !== undefined) {
|
||||||
|
await UsersService.syncEmployeePayTypes(
|
||||||
|
id,
|
||||||
|
data.pay_types,
|
||||||
|
currentUser,
|
||||||
|
transaction,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
return updatedUser;
|
return updatedUser;
|
||||||
|
|
||||||
|
|||||||
138
backend/src/services/workers_comp_classes.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
1
fix.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "usersCustom_permissionsPermissions" ADD COLUMN "permissionId" UUID REFERENCES "permissions"(id);
|
||||||
BIN
frontend/public/assets/logo.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/public/assets/vm-shot-2026-03-14T13-28-33-023Z.jpg
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
frontend/public/assets/vm-shot-2026-04-13T13-17-17-604Z.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
@ -19,7 +19,7 @@ export default function AsideMenu({
|
|||||||
<>
|
<>
|
||||||
<AsideMenuLayer
|
<AsideMenuLayer
|
||||||
menu={props.menu}
|
menu={props.menu}
|
||||||
className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${
|
className={`${isAsideMobileExpanded ? 'left-0' : '-left-72 lg:left-0'} ${
|
||||||
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
|
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
|
||||||
}`}
|
}`}
|
||||||
onAsideLgCloseClick={props.onAsideLgClose}
|
onAsideLgCloseClick={props.onAsideLgClose}
|
||||||
|
|||||||
@ -46,9 +46,10 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
|
|||||||
<BaseIcon path={item.icon} className={`flex-none mx-3 ${activeClassAddon}`} size="18" />
|
<BaseIcon path={item.icon} className={`flex-none mx-3 ${activeClassAddon}`} size="18" />
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={`grow text-ellipsis line-clamp-1 ${
|
className={`min-w-0 grow break-words whitespace-normal leading-5 line-clamp-2 ${
|
||||||
item.menu ? '' : 'pr-12'
|
item.menu ? '' : 'pr-12'
|
||||||
} ${activeClassAddon}`}
|
} ${activeClassAddon}`}
|
||||||
|
title={item.label}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
@ -63,7 +64,7 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const componentClass = [
|
const componentClass = [
|
||||||
'flex cursor-pointer py-1.5 ',
|
'flex items-start cursor-pointer py-1.5 min-h-[3rem]',
|
||||||
isDropdownList ? 'px-6 text-sm' : '',
|
isDropdownList ? 'px-6 text-sm' : '',
|
||||||
item.color
|
item.color
|
||||||
? getButtonColor(item.color, false, true)
|
? getButtonColor(item.color, false, true)
|
||||||
@ -77,12 +78,12 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
|
|||||||
<li className={'px-3 py-1.5'}>
|
<li className={'px-3 py-1.5'}>
|
||||||
{item.withDevider && <hr className={`${borders} mb-3`} />}
|
{item.withDevider && <hr className={`${borders} mb-3`} />}
|
||||||
{item.href && (
|
{item.href && (
|
||||||
<Link href={item.href} target={item.target} className={componentClass}>
|
<Link href={item.href} target={item.target} className={componentClass} title={item.label}>
|
||||||
{asideMenuItemInnerContents}
|
{asideMenuItemInnerContents}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{!item.href && (
|
{!item.href && (
|
||||||
<div className={componentClass} onClick={() => setIsDropdownActive(!isDropdownActive)}>
|
<div className={componentClass} onClick={() => setIsDropdownActive(!isDropdownActive)} title={item.label}>
|
||||||
{asideMenuItemInnerContents}
|
{asideMenuItemInnerContents}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
id='asideMenu'
|
id='asideMenu'
|
||||||
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
|
className={`${className} zzz lg:py-2 lg:pl-2 w-72 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
|
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
|
||||||
@ -39,7 +39,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
>
|
>
|
||||||
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
||||||
|
|
||||||
<b className="font-black">Pressure Wash Payroll</b>
|
<div className="flex items-center justify-center lg:justify-start gap-2"><img src="/assets/logo.webp" alt="Logo" className="h-8" /><b className="font-black text-xs">Major League Pressure Washing</b></div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { MenuAsideItem } from '../interfaces'
|
|||||||
import AsideMenuItem from './AsideMenuItem'
|
import AsideMenuItem from './AsideMenuItem'
|
||||||
import {useAppSelector} from "../stores/hooks";
|
import {useAppSelector} from "../stores/hooks";
|
||||||
import {hasPermission} from "../helpers/userPermissions";
|
import {hasPermission} from "../helpers/userPermissions";
|
||||||
|
import { isBlockedWorkerRoute, isRestrictedPayrollUser } from '../helpers/accessControl';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
menu: MenuAsideItem[]
|
menu: MenuAsideItem[]
|
||||||
@ -18,9 +19,10 @@ export default function AsideMenuList({ menu, isDropdownList = false, className
|
|||||||
return (
|
return (
|
||||||
<ul className={className}>
|
<ul className={className}>
|
||||||
{menu.map((item, index) => {
|
{menu.map((item, index) => {
|
||||||
|
if (!hasPermission(currentUser, item.permissions)) return null;
|
||||||
if (!hasPermission(currentUser, item.permissions)) return null;
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser) && isBlockedWorkerRoute(item.href)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<AsideMenuItem
|
<AsideMenuItem
|
||||||
|
|||||||
@ -109,34 +109,7 @@ const CardEmployee_pay_types = ({
|
|||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className='flex justify-between gap-x-4 py-3'>
|
|
||||||
<dt className=' text-gray-500 dark:text-dark-600'>EffectiveStart</dt>
|
|
||||||
<dd className='flex items-start gap-x-2'>
|
|
||||||
<div className='font-medium line-clamp-4'>
|
|
||||||
{ dataFormatter.dateTimeFormatter(item.effective_start) }
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className='flex justify-between gap-x-4 py-3'>
|
|
||||||
<dt className=' text-gray-500 dark:text-dark-600'>EffectiveEnd</dt>
|
|
||||||
<dd className='flex items-start gap-x-2'>
|
|
||||||
<div className='font-medium line-clamp-4'>
|
|
||||||
{ dataFormatter.dateTimeFormatter(item.effective_end) }
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</dl>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
{!loading && employee_pay_types.length === 0 && (
|
{!loading && employee_pay_types.length === 0 && (
|
||||||
|
|||||||
@ -67,26 +67,7 @@ const ListEmployee_pay_types = ({ employee_pay_types, loading, onDelete, current
|
|||||||
<p className={'text-xs text-gray-500 '}>Active</p>
|
<p className={'text-xs text-gray-500 '}>Active</p>
|
||||||
<p className={'line-clamp-2'}>{ dataFormatter.booleanFormatter(item.active) }</p>
|
<p className={'line-clamp-2'}>{ dataFormatter.booleanFormatter(item.active) }</p>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'flex-1 px-3'}>
|
|
||||||
<p className={'text-xs text-gray-500 '}>EffectiveStart</p>
|
|
||||||
<p className={'line-clamp-2'}>{ dataFormatter.dateTimeFormatter(item.effective_start) }</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'flex-1 px-3'}>
|
|
||||||
<p className={'text-xs text-gray-500 '}>EffectiveEnd</p>
|
|
||||||
<p className={'line-clamp-2'}>{ dataFormatter.dateTimeFormatter(item.effective_end) }</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Link>
|
|
||||||
<ListActionsPopover
|
<ListActionsPopover
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
itemId={item.id}
|
itemId={item.id}
|
||||||
|
|||||||
@ -100,44 +100,7 @@ export const loadColumns = async (
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
{
|
|
||||||
field: 'effective_start',
|
|
||||||
headerName: 'EffectiveStart',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
type: 'dateTime',
|
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
|
||||||
new Date(params.row.effective_start),
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'effective_end',
|
|
||||||
headerName: 'EffectiveEnd',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
type: 'dateTime',
|
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
|
||||||
new Date(params.row.effective_end),
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
type: 'actions',
|
type: 'actions',
|
||||||
minWidth: 30,
|
minWidth: 30,
|
||||||
|
|||||||
@ -15,8 +15,8 @@ export default function FooterBar({ children }: Props) {
|
|||||||
<div className="text-center md:text-left mb-6 md:mb-0">
|
<div className="text-center md:text-left mb-6 md:mb-0">
|
||||||
<b>
|
<b>
|
||||||
©{year},{` `}
|
©{year},{` `}
|
||||||
<a href="https://flatlogic.com/" rel="noreferrer" target="_blank">
|
<a href="#" rel="noreferrer" target="_blank">
|
||||||
Flatlogic
|
Major League Pressure Washing
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</b>
|
</b>
|
||||||
@ -25,7 +25,7 @@ export default function FooterBar({ children }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex item-center md:py-2 gap-4">
|
<div className="flex item-center md:py-2 gap-4">
|
||||||
<a href="https://flatlogic.com/" rel="noreferrer" target="_blank">
|
<a href="#" rel="noreferrer" target="_blank">
|
||||||
<Logo className="w-auto h-8 md:h-6 mx-auto" />
|
<Logo className="w-auto h-8 md:h-6 mx-auto" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
196
frontend/src/components/JobLogPayPreview.tsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import CardBox from './CardBox';
|
||||||
|
import {
|
||||||
|
calculateJobLogPayPreview,
|
||||||
|
formatCurrency,
|
||||||
|
formatDecimal,
|
||||||
|
hasPayTypeDetails,
|
||||||
|
hasWorkersCompDetails,
|
||||||
|
resolveEntityId,
|
||||||
|
} from '../helpers/jobLogPayPreview';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
payTypeOptions?: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const findById = (items: any[] = [], id: string | null) => items.find((item) => item?.id === id) || null;
|
||||||
|
|
||||||
|
export default function JobLogPayPreview({ payTypeOptions = [] }: Props) {
|
||||||
|
const { values } = useFormikContext<any>();
|
||||||
|
const [payTypeDetails, setPayTypeDetails] = useState<any>(null);
|
||||||
|
const [workersCompClassDetails, setWorkersCompClassDetails] = useState<any>(null);
|
||||||
|
|
||||||
|
const selectedPayTypeId = resolveEntityId(values?.pay_type);
|
||||||
|
const selectedWorkersCompClassId = resolveEntityId(values?.workersCompClass);
|
||||||
|
|
||||||
|
const selectedPayTypeFromOptions = useMemo(
|
||||||
|
() => findById(payTypeOptions, selectedPayTypeId),
|
||||||
|
[payTypeOptions, selectedPayTypeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
if (hasPayTypeDetails(values?.pay_type)) {
|
||||||
|
setPayTypeDetails(values.pay_type);
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedPayTypeFromOptions) {
|
||||||
|
setPayTypeDetails(selectedPayTypeFromOptions);
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedPayTypeId) {
|
||||||
|
setPayTypeDetails(null);
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(`/pay_types/${selectedPayTypeId}`)
|
||||||
|
.then(({ data }) => {
|
||||||
|
if (active) {
|
||||||
|
setPayTypeDetails(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to fetch pay type for job log preview:', error);
|
||||||
|
if (active) {
|
||||||
|
setPayTypeDetails(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [selectedPayTypeFromOptions, selectedPayTypeId, values?.pay_type]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
if (hasWorkersCompDetails(values?.workersCompClass)) {
|
||||||
|
setWorkersCompClassDetails(values.workersCompClass);
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedWorkersCompClassId) {
|
||||||
|
setWorkersCompClassDetails(null);
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(`/workers_comp_classes/${selectedWorkersCompClassId}`)
|
||||||
|
.then(({ data }) => {
|
||||||
|
if (active) {
|
||||||
|
setWorkersCompClassDetails(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to fetch workers comp class for job log preview:', error);
|
||||||
|
if (active) {
|
||||||
|
setWorkersCompClassDetails(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [selectedWorkersCompClassId, values?.workersCompClass]);
|
||||||
|
|
||||||
|
const preview = useMemo(
|
||||||
|
() =>
|
||||||
|
calculateJobLogPayPreview({
|
||||||
|
payType: payTypeDetails,
|
||||||
|
workersCompClass: workersCompClassDetails,
|
||||||
|
hoursConducted: values?.hours_conducted,
|
||||||
|
clientPaid: values?.client_paid,
|
||||||
|
}),
|
||||||
|
[payTypeDetails, workersCompClassDetails, values?.client_paid, values?.hours_conducted],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBox className="mb-6 border border-blue-100 bg-blue-50/70 dark:border-dark-700 dark:bg-dark-800">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-1 md:flex-row md:items-end md:justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
|
||||||
|
Earnings Preview
|
||||||
|
</p>
|
||||||
|
<h3 className="text-lg font-bold">Live estimate for this job</h3>
|
||||||
|
</div>
|
||||||
|
<div className="text-left md:text-right">
|
||||||
|
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Estimated Job Pay</p>
|
||||||
|
<p className="text-2xl font-bold text-green-700 dark:text-green-400">
|
||||||
|
{formatCurrency(preview.estimatedPay)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<div className="rounded-lg border border-white/80 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
|
||||||
|
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Pay Type</p>
|
||||||
|
<p className="font-semibold capitalize">{preview.payTypeName}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-white/80 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
|
||||||
|
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Pay Method</p>
|
||||||
|
<p className="font-semibold capitalize">{preview.payMethod || 'Not selected'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-white/80 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
|
||||||
|
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">
|
||||||
|
{preview.payMethod === 'commission' ? 'Client Paid' : 'Hours'}
|
||||||
|
</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{preview.payMethod === 'commission'
|
||||||
|
? formatCurrency(preview.clientAmount)
|
||||||
|
: formatDecimal(preview.hours)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-white/80 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
|
||||||
|
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Rate</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{preview.payMethod === 'commission'
|
||||||
|
? `${formatDecimal(preview.commissionRate)}%`
|
||||||
|
: formatCurrency(preview.hourlyRate)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-dashed border-blue-200 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
|
||||||
|
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Calculation</p>
|
||||||
|
<p className="font-medium">
|
||||||
|
{preview.formulaLabel}
|
||||||
|
{preview.hasEstimate ? ` = ${formatCurrency(preview.estimatedPay)}` : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{preview.workersCompName && (
|
||||||
|
<div className="rounded-lg border border-white/80 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
|
||||||
|
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Workers Comp Estimate</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{preview.workersCompName} · {formatDecimal(preview.workersCompPercentage)}% ·{' '}
|
||||||
|
{formatCurrency(preview.workersCompAmount)}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
This is shown for visibility only and does not reduce the employee pay preview.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import LoadingSpinner from "../LoadingSpinner";
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
import { calculateJobLogPayPreview, formatCurrency } from '../../helpers/jobLogPayPreview';
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -47,7 +48,15 @@ const CardJob_logs = ({
|
|||||||
role='list'
|
role='list'
|
||||||
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
|
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
|
||||||
>
|
>
|
||||||
{!loading && job_logs.map((item, index) => (
|
{!loading && job_logs.map((item, index) => {
|
||||||
|
const payPreview = calculateJobLogPayPreview({
|
||||||
|
payType: item.pay_type,
|
||||||
|
workersCompClass: item.workersCompClass,
|
||||||
|
hoursConducted: item.hours_conducted,
|
||||||
|
clientPaid: item.client_paid,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
<li
|
<li
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={`overflow-hidden ${corners !== 'rounded-full'? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${
|
className={`overflow-hidden ${corners !== 'rounded-full'? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${
|
||||||
@ -141,7 +150,7 @@ const CardJob_logs = ({
|
|||||||
<dt className=' text-gray-500 dark:text-dark-600'>WorkmansCompensationClass</dt>
|
<dt className=' text-gray-500 dark:text-dark-600'>WorkmansCompensationClass</dt>
|
||||||
<dd className='flex items-start gap-x-2'>
|
<dd className='flex items-start gap-x-2'>
|
||||||
<div className='font-medium line-clamp-4'>
|
<div className='font-medium line-clamp-4'>
|
||||||
{ item.workers_comp_class }
|
{ item.workersCompClass?.name }
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@ -161,6 +170,18 @@ const CardJob_logs = ({
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
|
<dt className=' text-gray-500 dark:text-dark-600'>EmployeePay</dt>
|
||||||
|
<dd className='flex items-start gap-x-2'>
|
||||||
|
<div className='font-medium line-clamp-4'>
|
||||||
|
{ formatCurrency(payPreview.estimatedPay) }
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className='flex justify-between gap-x-4 py-3'>
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
<dt className=' text-gray-500 dark:text-dark-600'>Vehicle</dt>
|
<dt className=' text-gray-500 dark:text-dark-600'>Vehicle</dt>
|
||||||
<dd className='flex items-start gap-x-2'>
|
<dd className='flex items-start gap-x-2'>
|
||||||
@ -234,7 +255,8 @@ const CardJob_logs = ({
|
|||||||
|
|
||||||
</dl>
|
</dl>
|
||||||
</li>
|
</li>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{!loading && job_logs.length === 0 && (
|
{!loading && job_logs.length === 0 && (
|
||||||
<div className='col-span-full flex items-center justify-center h-40'>
|
<div className='col-span-full flex items-center justify-center h-40'>
|
||||||
<p className=''>No data to display</p>
|
<p className=''>No data to display</p>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import LoadingSpinner from "../LoadingSpinner";
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
import { calculateJobLogPayPreview, formatCurrency } from '../../helpers/jobLogPayPreview';
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -34,7 +35,15 @@ const ListJob_logs = ({ job_logs, loading, onDelete, currentPage, numPages, onPa
|
|||||||
<>
|
<>
|
||||||
<div className='relative overflow-x-auto p-4 space-y-4'>
|
<div className='relative overflow-x-auto p-4 space-y-4'>
|
||||||
{loading && <LoadingSpinner />}
|
{loading && <LoadingSpinner />}
|
||||||
{!loading && job_logs.map((item) => (
|
{!loading && job_logs.map((item) => {
|
||||||
|
const payPreview = calculateJobLogPayPreview({
|
||||||
|
payType: item.pay_type,
|
||||||
|
workersCompClass: item.workersCompClass,
|
||||||
|
hoursConducted: item.hours_conducted,
|
||||||
|
clientPaid: item.client_paid,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
<CardBox hasTable isList className={'rounded shadow-none'}>
|
<CardBox hasTable isList className={'rounded shadow-none'}>
|
||||||
<div className={`flex rounded dark:bg-dark-900 border border-stone-300 items-center overflow-hidden`}>
|
<div className={`flex rounded dark:bg-dark-900 border border-stone-300 items-center overflow-hidden`}>
|
||||||
@ -89,7 +98,7 @@ const ListJob_logs = ({ job_logs, loading, onDelete, currentPage, numPages, onPa
|
|||||||
|
|
||||||
<div className={'flex-1 px-3'}>
|
<div className={'flex-1 px-3'}>
|
||||||
<p className={'text-xs text-gray-500 '}>WorkmansCompensationClass</p>
|
<p className={'text-xs text-gray-500 '}>WorkmansCompensationClass</p>
|
||||||
<p className={'line-clamp-2'}>{ item.workers_comp_class }</p>
|
<p className={'line-clamp-2'}>{ item.workersCompClass?.name }</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -103,6 +112,14 @@ const ListJob_logs = ({ job_logs, loading, onDelete, currentPage, numPages, onPa
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className={'flex-1 px-3'}>
|
||||||
|
<p className={'text-xs text-gray-500 '}>EmployeePay</p>
|
||||||
|
<p className={'line-clamp-2 font-semibold'}>{ formatCurrency(payPreview.estimatedPay) }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'flex-1 px-3'}>
|
<div className={'flex-1 px-3'}>
|
||||||
<p className={'text-xs text-gray-500 '}>Vehicle</p>
|
<p className={'text-xs text-gray-500 '}>Vehicle</p>
|
||||||
<p className={'line-clamp-2'}>{ dataFormatter.vehiclesOneListFormatter(item.vehicle) }</p>
|
<p className={'line-clamp-2'}>{ dataFormatter.vehiclesOneListFormatter(item.vehicle) }</p>
|
||||||
@ -163,7 +180,8 @@ const ListJob_logs = ({ job_logs, loading, onDelete, currentPage, numPages, onPa
|
|||||||
</div>
|
</div>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{!loading && job_logs.length === 0 && (
|
{!loading && job_logs.length === 0 && (
|
||||||
<div className='col-span-full flex items-center justify-center h-40'>
|
<div className='col-span-full flex items-center justify-center h-40'>
|
||||||
<p className=''>No data to display</p>
|
<p className=''>No data to display</p>
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import DataGridMultiSelect from "../DataGridMultiSelect";
|
|||||||
import ListActionsPopover from '../ListActionsPopover';
|
import ListActionsPopover from '../ListActionsPopover';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
import { calculateJobLogPayPreview, formatCurrency } from '../../helpers/jobLogPayPreview';
|
||||||
|
|
||||||
type Params = (id: string) => void;
|
type Params = (id: string) => void;
|
||||||
|
|
||||||
@ -136,7 +137,7 @@ export const loadColumns = async (
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'workers_comp_class',
|
field: 'workersCompClass', valueGetter: (params) => params.row?.workersCompClass?.name,
|
||||||
headerName: 'WorkmansCompensationClass',
|
headerName: 'WorkmansCompensationClass',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
@ -172,6 +173,29 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
field: 'employeePay',
|
||||||
|
headerName: 'EmployeePay',
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 140,
|
||||||
|
filterable: false,
|
||||||
|
headerClassName: 'datagrid--header',
|
||||||
|
cellClassName: 'datagrid--cell',
|
||||||
|
|
||||||
|
editable: false,
|
||||||
|
sortable: false,
|
||||||
|
type: 'number',
|
||||||
|
valueGetter: (params: GridValueGetterParams) =>
|
||||||
|
calculateJobLogPayPreview({
|
||||||
|
payType: params.row?.pay_type,
|
||||||
|
workersCompClass: params.row?.workersCompClass,
|
||||||
|
hoursConducted: params.row?.hours_conducted,
|
||||||
|
clientPaid: params.row?.client_paid,
|
||||||
|
}).estimatedPay,
|
||||||
|
valueFormatter: ({ value }) => formatCurrency(value),
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'vehicle',
|
field: 'vehicle',
|
||||||
headerName: 'Vehicle',
|
headerName: 'Vehicle',
|
||||||
@ -194,52 +218,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
field: 'odometer_start',
|
|
||||||
headerName: 'OdometerStart',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
type: 'number',
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'odometer_end',
|
|
||||||
headerName: 'OdometerEnd',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
type: 'number',
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'job_address',
|
|
||||||
headerName: 'JobAddress',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'status',
|
field: 'status',
|
||||||
|
|||||||
@ -7,9 +7,9 @@ type Props = {
|
|||||||
export default function Logo({ className = '' }: Props) {
|
export default function Logo({ className = '' }: Props) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={"https://flatlogic.com/logo.svg"}
|
src={"/assets/logo.webp"}
|
||||||
className={className}
|
className={className}
|
||||||
alt={'Flatlogic logo'}>
|
alt={'Major League Pressure Washing logo'}>
|
||||||
</img>
|
</img>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useRef} from 'react'
|
import React, {useEffect, useRef, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
|
|||||||
@ -17,6 +17,15 @@ import {hasPermission} from "../../helpers/userPermissions";
|
|||||||
|
|
||||||
type Params = (id: string) => void;
|
type Params = (id: string) => void;
|
||||||
|
|
||||||
|
const formatAssignedPayTypes = (assignments = []) => {
|
||||||
|
const uniquePayTypeNames = [...new Set((assignments || [])
|
||||||
|
.filter((assignment) => assignment?.active !== false)
|
||||||
|
.map((assignment) => assignment?.pay_type?.name)
|
||||||
|
.filter(Boolean))];
|
||||||
|
|
||||||
|
return uniquePayTypeNames.length ? uniquePayTypeNames.join(', ') : '—';
|
||||||
|
};
|
||||||
|
|
||||||
export const loadColumns = async (
|
export const loadColumns = async (
|
||||||
onDelete: Params,
|
onDelete: Params,
|
||||||
entityName: string,
|
entityName: string,
|
||||||
@ -180,6 +189,27 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
field: 'employee_pay_types_employee',
|
||||||
|
headerName: 'Pay Types',
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 180,
|
||||||
|
filterable: false,
|
||||||
|
headerClassName: 'datagrid--header',
|
||||||
|
cellClassName: 'datagrid--cell',
|
||||||
|
|
||||||
|
editable: false,
|
||||||
|
sortable: false,
|
||||||
|
valueGetter: (params: GridValueGetterParams) =>
|
||||||
|
formatAssignedPayTypes(params?.row?.employee_pay_types_employee),
|
||||||
|
renderCell: (params: GridValueGetterParams) => (
|
||||||
|
<span className='whitespace-normal leading-5 py-2'>
|
||||||
|
{formatAssignedPayTypes(params?.row?.employee_pay_types_employee)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
type: 'actions',
|
type: 'actions',
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<div className={'p-4'}>
|
||||||
|
{loading && <LoadingSpinner />}
|
||||||
|
<ul
|
||||||
|
role='list'
|
||||||
|
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
|
||||||
|
>
|
||||||
|
{!loading && workers_comp_classes.map((item, index) => (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
className={`overflow-hidden ${corners !== 'rounded-full'? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${
|
||||||
|
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
|
||||||
|
<div className={`flex items-center ${bgColor} p-6 gap-x-4 border-b border-gray-900/5 bg-gray-50 dark:bg-dark-800 relative`}>
|
||||||
|
|
||||||
|
<Link href={`/workers_comp_classes/workers_comp_classes-view/?id=${item.id}`} className='text-lg font-bold leading-6 line-clamp-1'>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
|
||||||
|
<div className='ml-auto '>
|
||||||
|
<ListActionsPopover
|
||||||
|
onDelete={onDelete}
|
||||||
|
itemId={item.id}
|
||||||
|
pathEdit={`/workers_comp_classes/workers_comp_classes-edit/?id=${item.id}`}
|
||||||
|
pathView={`/workers_comp_classes/workers_comp_classes-view/?id=${item.id}`}
|
||||||
|
|
||||||
|
hasUpdatePermission={hasUpdatePermission}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dl className='divide-y divide-stone-300 dark:divide-dark-700 px-6 py-4 text-sm leading-6 h-64 overflow-y-auto'>
|
||||||
|
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
|
<dt className=' text-gray-500 dark:text-dark-600'>WorkersCompClassName</dt>
|
||||||
|
<dd className='flex items-start gap-x-2'>
|
||||||
|
<div className='font-medium line-clamp-4'>
|
||||||
|
{ item.name }
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
|
<dt className=' text-gray-500 dark:text-dark-600'>PayMethod</dt>
|
||||||
|
<dd className='flex items-start gap-x-2'>
|
||||||
|
<div className='font-medium line-clamp-4'>
|
||||||
|
{ item.pay_method }
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
|
<dt className=' text-gray-500 dark:text-dark-600'>HourlyRate</dt>
|
||||||
|
<dd className='flex items-start gap-x-2'>
|
||||||
|
<div className='font-medium line-clamp-4'>
|
||||||
|
{ item.hourly_rate }
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
|
<dt className=' text-gray-500 dark:text-dark-600'>CommissionRate</dt>
|
||||||
|
<dd className='flex items-start gap-x-2'>
|
||||||
|
<div className='font-medium line-clamp-4'>
|
||||||
|
{ item.commission_rate }
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
|
<dt className=' text-gray-500 dark:text-dark-600'>Active</dt>
|
||||||
|
<dd className='flex items-start gap-x-2'>
|
||||||
|
<div className='font-medium line-clamp-4'>
|
||||||
|
{ dataFormatter.booleanFormatter(item.active) }
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
|
<dt className=' text-gray-500 dark:text-dark-600'>Description</dt>
|
||||||
|
<dd className='flex items-start gap-x-2'>
|
||||||
|
<div className='font-medium line-clamp-4'>
|
||||||
|
{ item.description }
|
||||||
|
</div>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{!loading && workers_comp_classes.length === 0 && (
|
||||||
|
<div className='col-span-full flex items-center justify-center h-40'>
|
||||||
|
<p className=''>No data to display</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
<div className={'flex items-center justify-center my-6'}>
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
numPages={numPages}
|
||||||
|
setCurrentPage={onPageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CardWorkers_comp_classes;
|
||||||
@ -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 (
|
||||||
|
<>
|
||||||
|
<div className='relative overflow-x-auto p-4 space-y-4'>
|
||||||
|
{loading && <LoadingSpinner />}
|
||||||
|
{!loading && workers_comp_classes.map((item) => (
|
||||||
|
<div key={item.id}>
|
||||||
|
<CardBox hasTable isList className={'rounded shadow-none'}>
|
||||||
|
<div className={`flex rounded dark:bg-dark-900 border border-stone-300 items-center overflow-hidden`}>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/workers_comp_classes/workers_comp_classes-view/?id=${item.id}`}
|
||||||
|
className={
|
||||||
|
'flex-1 px-4 py-6 h-24 flex divide-x-2 divide-stone-300 items-center overflow-hidden`}> dark:divide-dark-700 overflow-x-auto'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<div className={'flex-1 px-3'}>
|
||||||
|
<p className={'text-xs text-gray-500 '}>WorkersCompClassName</p>
|
||||||
|
<p className={'line-clamp-2'}>{ item.name }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className={'flex-1 px-3'}>
|
||||||
|
<p className={'text-xs text-gray-500 '}>PayMethod</p>
|
||||||
|
<p className={'line-clamp-2'}>{ item.pay_method }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className={'flex-1 px-3'}>
|
||||||
|
<p className={'text-xs text-gray-500 '}>HourlyRate</p>
|
||||||
|
<p className={'line-clamp-2'}>{ item.hourly_rate }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className={'flex-1 px-3'}>
|
||||||
|
<p className={'text-xs text-gray-500 '}>CommissionRate</p>
|
||||||
|
<p className={'line-clamp-2'}>{ item.commission_rate }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className={'flex-1 px-3'}>
|
||||||
|
<p className={'text-xs text-gray-500 '}>Active</p>
|
||||||
|
<p className={'line-clamp-2'}>{ dataFormatter.booleanFormatter(item.active) }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className={'flex-1 px-3'}>
|
||||||
|
<p className={'text-xs text-gray-500 '}>Description</p>
|
||||||
|
<p className={'line-clamp-2'}>{ item.description }</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Link>
|
||||||
|
<ListActionsPopover
|
||||||
|
onDelete={onDelete}
|
||||||
|
itemId={item.id}
|
||||||
|
pathEdit={`/workers_comp_classes/workers_comp_classes-edit/?id=${item.id}`}
|
||||||
|
pathView={`/workers_comp_classes/workers_comp_classes-view/?id=${item.id}`}
|
||||||
|
|
||||||
|
hasUpdatePermission={hasUpdatePermission}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!loading && workers_comp_classes.length === 0 && (
|
||||||
|
<div className='col-span-full flex items-center justify-center h-40'>
|
||||||
|
<p className=''>No data to display</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={'flex items-center justify-center my-6'}>
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
numPages={numPages}
|
||||||
|
setCurrentPage={onPageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListWorkers_comp_classes
|
||||||
@ -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<GridColDef[]>([]);
|
||||||
|
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 = (
|
||||||
|
<div className='relative overflow-x-auto'>
|
||||||
|
<DataGrid
|
||||||
|
autoHeight
|
||||||
|
rowHeight={64}
|
||||||
|
sx={dataGridStyles}
|
||||||
|
className={'datagrid--table'}
|
||||||
|
getRowClassName={() => `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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{filterItems && Array.isArray( filterItems ) && filterItems.length ?
|
||||||
|
<CardBox>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
checkboxes: ['lorem'],
|
||||||
|
switches: ['lorem'],
|
||||||
|
radio: 'lorem',
|
||||||
|
}}
|
||||||
|
onSubmit={() => null}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<>
|
||||||
|
{filterItems && filterItems.map((filterItem) => {
|
||||||
|
return (
|
||||||
|
<div key={filterItem.id} className="flex mb-4">
|
||||||
|
<div className="flex flex-col w-full mr-3">
|
||||||
|
<div className=" text-gray-500 font-bold">Filter</div>
|
||||||
|
<Field
|
||||||
|
className={controlClasses}
|
||||||
|
name='selectedField'
|
||||||
|
id='selectedField'
|
||||||
|
component='select'
|
||||||
|
value={filterItem?.fields?.selectedField || ''}
|
||||||
|
onChange={handleChange(filterItem.id)}
|
||||||
|
>
|
||||||
|
{filters.map((selectOption) => (
|
||||||
|
<option
|
||||||
|
key={selectOption.title}
|
||||||
|
value={`${selectOption.title}`}
|
||||||
|
>
|
||||||
|
{selectOption.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
{filters.find((filter) =>
|
||||||
|
filter.title === filterItem?.fields?.selectedField
|
||||||
|
)?.type === 'enum' ? (
|
||||||
|
<div className="flex flex-col w-full mr-3">
|
||||||
|
<div className="text-gray-500 font-bold">
|
||||||
|
Value
|
||||||
|
</div>
|
||||||
|
<Field
|
||||||
|
className={controlClasses}
|
||||||
|
name="filterValue"
|
||||||
|
id='filterValue'
|
||||||
|
component="select"
|
||||||
|
value={filterItem?.fields?.filterValue || ''}
|
||||||
|
onChange={handleChange(filterItem.id)}
|
||||||
|
>
|
||||||
|
<option value="">Select Value</option>
|
||||||
|
{filters.find((filter) =>
|
||||||
|
filter.title === filterItem?.fields?.selectedField
|
||||||
|
)?.options?.map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
) : filters.find((filter) =>
|
||||||
|
filter.title === filterItem?.fields?.selectedField
|
||||||
|
)?.number ? (
|
||||||
|
<div className="flex flex-row w-full mr-3">
|
||||||
|
<div className="flex flex-col w-full mr-3">
|
||||||
|
<div className=" text-gray-500 font-bold">From</div>
|
||||||
|
<Field
|
||||||
|
className={controlClasses}
|
||||||
|
name='filterValueFrom'
|
||||||
|
placeholder='From'
|
||||||
|
id='filterValueFrom'
|
||||||
|
value={filterItem?.fields?.filterValueFrom || ''}
|
||||||
|
onChange={handleChange(filterItem.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
|
<div className=" text-gray-500 font-bold">To</div>
|
||||||
|
<Field
|
||||||
|
className={controlClasses}
|
||||||
|
name='filterValueTo'
|
||||||
|
placeholder='to'
|
||||||
|
id='filterValueTo'
|
||||||
|
value={filterItem?.fields?.filterValueTo || ''}
|
||||||
|
onChange={handleChange(filterItem.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : filters.find(
|
||||||
|
(filter) =>
|
||||||
|
filter.title ===
|
||||||
|
filterItem?.fields?.selectedField
|
||||||
|
)?.date ? (
|
||||||
|
<div className='flex flex-row w-full mr-3'>
|
||||||
|
<div className='flex flex-col w-full mr-3'>
|
||||||
|
<div className=' text-gray-500 font-bold'>
|
||||||
|
From
|
||||||
|
</div>
|
||||||
|
<Field
|
||||||
|
className={controlClasses}
|
||||||
|
name='filterValueFrom'
|
||||||
|
placeholder='From'
|
||||||
|
id='filterValueFrom'
|
||||||
|
type='datetime-local'
|
||||||
|
value={filterItem?.fields?.filterValueFrom || ''}
|
||||||
|
onChange={handleChange(filterItem.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col w-full'>
|
||||||
|
<div className=' text-gray-500 font-bold'>To</div>
|
||||||
|
<Field
|
||||||
|
className={controlClasses}
|
||||||
|
name='filterValueTo'
|
||||||
|
placeholder='to'
|
||||||
|
id='filterValueTo'
|
||||||
|
type='datetime-local'
|
||||||
|
value={filterItem?.fields?.filterValueTo || ''}
|
||||||
|
onChange={handleChange(filterItem.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col w-full mr-3">
|
||||||
|
<div className=" text-gray-500 font-bold">Contains</div>
|
||||||
|
<Field
|
||||||
|
className={controlClasses}
|
||||||
|
name='filterValue'
|
||||||
|
placeholder='Contained'
|
||||||
|
id='filterValue'
|
||||||
|
value={filterItem?.fields?.filterValue || ''}
|
||||||
|
onChange={handleChange(filterItem.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className=" text-gray-500 font-bold">Action</div>
|
||||||
|
<BaseButton
|
||||||
|
className="my-2"
|
||||||
|
type='reset'
|
||||||
|
color='danger'
|
||||||
|
label='Delete'
|
||||||
|
onClick={() => {
|
||||||
|
deleteFilter(filterItem.id)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<div className="flex">
|
||||||
|
<BaseButton
|
||||||
|
className="my-2 mr-3"
|
||||||
|
color="success"
|
||||||
|
label='Apply'
|
||||||
|
onClick={handleSubmit}
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
className="my-2"
|
||||||
|
color='info'
|
||||||
|
label='Cancel'
|
||||||
|
onClick={handleReset}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</CardBox> : null
|
||||||
|
}
|
||||||
|
<CardBoxModal
|
||||||
|
title="Please confirm"
|
||||||
|
buttonColor="info"
|
||||||
|
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
|
||||||
|
isActive={isModalTrashActive}
|
||||||
|
onConfirm={handleDeleteAction}
|
||||||
|
onCancel={handleModalAction}
|
||||||
|
>
|
||||||
|
<p>Are you sure you want to delete this item?</p>
|
||||||
|
</CardBoxModal>
|
||||||
|
|
||||||
|
|
||||||
|
{dataGrid}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{selectedRows.length > 0 &&
|
||||||
|
createPortal(
|
||||||
|
<BaseButton
|
||||||
|
className='me-4'
|
||||||
|
color='danger'
|
||||||
|
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
|
||||||
|
onClick={() => onDeleteRows(selectedRows)}
|
||||||
|
/>,
|
||||||
|
document.getElementById('delete-rows-button'),
|
||||||
|
)}
|
||||||
|
<ToastContainer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableSampleWorkers_comp_classes
|
||||||
@ -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 [
|
||||||
|
<div key={params?.row?.id}>
|
||||||
|
<ListActionsPopover
|
||||||
|
onDelete={onDelete}
|
||||||
|
itemId={params?.row?.id}
|
||||||
|
pathEdit={`/workers_comp_classes/workers_comp_classes-edit/?id=${params?.row?.id}`}
|
||||||
|
pathView={`/workers_comp_classes/workers_comp_classes-view/?id=${params?.row?.id}`}
|
||||||
|
|
||||||
|
hasUpdatePermission={hasUpdatePermission}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
@ -8,7 +8,7 @@ export const localStorageStyleKey = 'style'
|
|||||||
|
|
||||||
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
||||||
|
|
||||||
export const appTitle = 'created by Flatlogic generator!'
|
export const appTitle = 'Major League Pressure Washing'
|
||||||
|
|
||||||
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
||||||
|
|
||||||
|
|||||||
24
frontend/src/helpers/accessControl.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const PRIVILEGED_PAYROLL_ROLE_NAMES = new Set([
|
||||||
|
'Administrator',
|
||||||
|
'System Owner',
|
||||||
|
'Payroll Manager',
|
||||||
|
'Operations Manager',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const BLOCKED_WORKER_ROUTE_PREFIXES = ['/users', '/pay_types', '/employee_pay_types'];
|
||||||
|
|
||||||
|
export function isRestrictedPayrollUser(user: any) {
|
||||||
|
if (!user?.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !PRIVILEGED_PAYROLL_ROLE_NAMES.has(user?.app_role?.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBlockedWorkerRoute(pathname?: string) {
|
||||||
|
if (!pathname) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BLOCKED_WORKER_ROUTE_PREFIXES.some((prefix) => pathname.startsWith(prefix));
|
||||||
|
}
|
||||||
104
frontend/src/helpers/jobLogPayPreview.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
const currencyFormatter = new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD',
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const numberFormatter = new Intl.NumberFormat('en-US', {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const coerceNumber = (value: unknown) => {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(value);
|
||||||
|
|
||||||
|
return Number.isFinite(parsed) ? parsed : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatCurrency = (value: unknown) => currencyFormatter.format(coerceNumber(value));
|
||||||
|
|
||||||
|
export const formatDecimal = (value: unknown) => numberFormatter.format(coerceNumber(value));
|
||||||
|
|
||||||
|
export const resolveEntityId = (value: any) => {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && value.id) {
|
||||||
|
return value.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPayTypeDetails = (payType: any) => {
|
||||||
|
if (!payType || typeof payType !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(payType.pay_method || payType.hourly_rate || payType.commission_rate);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasWorkersCompDetails = (workersCompClass: any) => {
|
||||||
|
if (!workersCompClass || typeof workersCompClass !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return workersCompClass.percentage !== undefined && workersCompClass.percentage !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calculateJobLogPayPreview = ({
|
||||||
|
payType,
|
||||||
|
workersCompClass,
|
||||||
|
hoursConducted,
|
||||||
|
clientPaid,
|
||||||
|
}: {
|
||||||
|
payType?: any;
|
||||||
|
workersCompClass?: any;
|
||||||
|
hoursConducted?: unknown;
|
||||||
|
clientPaid?: unknown;
|
||||||
|
}) => {
|
||||||
|
const payMethod = payType?.pay_method || null;
|
||||||
|
const hours = coerceNumber(hoursConducted);
|
||||||
|
const clientAmount = coerceNumber(clientPaid);
|
||||||
|
const hourlyRate = coerceNumber(payType?.hourly_rate);
|
||||||
|
const commissionRate = coerceNumber(payType?.commission_rate);
|
||||||
|
const workersCompPercentage = coerceNumber(workersCompClass?.percentage);
|
||||||
|
|
||||||
|
let estimatedPay = 0;
|
||||||
|
let formulaLabel = 'Select a pay type to preview earnings.';
|
||||||
|
|
||||||
|
if (payMethod === 'hourly') {
|
||||||
|
estimatedPay = hours * hourlyRate;
|
||||||
|
formulaLabel = `${formatDecimal(hours)} hours × ${formatCurrency(hourlyRate)}/hr`;
|
||||||
|
} else if (payMethod === 'commission') {
|
||||||
|
estimatedPay = clientAmount * (commissionRate / 100);
|
||||||
|
formulaLabel = `${formatCurrency(clientAmount)} client paid × ${formatDecimal(commissionRate)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workersCompAmount = estimatedPay * (workersCompPercentage / 100);
|
||||||
|
|
||||||
|
return {
|
||||||
|
payMethod,
|
||||||
|
payTypeName: payType?.name || 'Not selected',
|
||||||
|
hours,
|
||||||
|
clientAmount,
|
||||||
|
hourlyRate,
|
||||||
|
commissionRate,
|
||||||
|
estimatedPay,
|
||||||
|
formulaLabel,
|
||||||
|
workersCompName: workersCompClass?.name || null,
|
||||||
|
workersCompPercentage,
|
||||||
|
workersCompAmount,
|
||||||
|
hasEstimate: payMethod === 'hourly' || payMethod === 'commission',
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react'
|
import React, { ReactNode, useEffect , useState } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
@ -15,6 +14,7 @@ import { useRouter } from 'next/router'
|
|||||||
import {findMe, logoutUser} from "../stores/authSlice";
|
import {findMe, logoutUser} from "../stores/authSlice";
|
||||||
|
|
||||||
import {hasPermission} from "../helpers/userPermissions";
|
import {hasPermission} from "../helpers/userPermissions";
|
||||||
|
import { isBlockedWorkerRoute, isRestrictedPayrollUser } from '../helpers/accessControl';
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -62,7 +62,15 @@ export default function LayoutAuthenticated({
|
|||||||
if (!permission || !currentUser) return;
|
if (!permission || !currentUser) return;
|
||||||
|
|
||||||
if (!hasPermission(currentUser, permission)) router.push('/error');
|
if (!hasPermission(currentUser, permission)) router.push('/error');
|
||||||
}, [currentUser, permission]);
|
}, [currentUser, permission, router]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUser) return;
|
||||||
|
|
||||||
|
if (isRestrictedPayrollUser(currentUser) && isBlockedWorkerRoute(router.pathname)) {
|
||||||
|
router.replace('/my-logs');
|
||||||
|
}
|
||||||
|
}, [currentUser, router, router.pathname]);
|
||||||
|
|
||||||
|
|
||||||
const darkMode = useAppSelector((state) => state.style.darkMode)
|
const darkMode = useAppSelector((state) => state.style.darkMode)
|
||||||
@ -86,18 +94,18 @@ export default function LayoutAuthenticated({
|
|||||||
}, [router.events, dispatch])
|
}, [router.events, dispatch])
|
||||||
|
|
||||||
|
|
||||||
const layoutAsidePadding = 'xl:pl-60'
|
const layoutAsidePadding = 'xl:pl-72'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
||||||
<div
|
<div
|
||||||
className={`${layoutAsidePadding} ${
|
className={`${layoutAsidePadding} ${
|
||||||
isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
|
isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''
|
||||||
} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
|
} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
|
||||||
>
|
>
|
||||||
<NavBar
|
<NavBar
|
||||||
menu={menuNavBar}
|
menu={menuNavBar}
|
||||||
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`}
|
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''}`}
|
||||||
>
|
>
|
||||||
<NavBarItemPlain
|
<NavBarItemPlain
|
||||||
display="flex lg:hidden"
|
display="flex lg:hidden"
|
||||||
|
|||||||
@ -6,6 +6,20 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
href: '/dashboard',
|
href: '/dashboard',
|
||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
|
permissions: 'UPDATE_USERS',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
href: '/log-work',
|
||||||
|
label: 'Log Work',
|
||||||
|
icon: icon.mdiPencil,
|
||||||
|
permissions: 'CREATE_JOB_LOGS'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/my-logs',
|
||||||
|
label: 'My Logs',
|
||||||
|
icon: icon.mdiViewList,
|
||||||
|
permissions: 'READ_JOB_LOGS'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -14,7 +28,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
||||||
permissions: 'READ_USERS'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/roles/roles-list',
|
href: '/roles/roles-list',
|
||||||
@ -22,7 +36,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
||||||
permissions: 'READ_ROLES'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/permissions/permissions-list',
|
href: '/permissions/permissions-list',
|
||||||
@ -30,7 +44,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
||||||
permissions: 'READ_PERMISSIONS'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/customers/customers-list',
|
href: '/customers/customers-list',
|
||||||
@ -38,7 +52,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiAccountMultiple' in icon ? icon['mdiAccountMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiAccountMultiple' in icon ? icon['mdiAccountMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_CUSTOMERS'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/vehicles/vehicles-list',
|
href: '/vehicles/vehicles-list',
|
||||||
@ -46,7 +60,13 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiTruck' in icon ? icon['mdiTruck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiTruck' in icon ? icon['mdiTruck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_VEHICLES'
|
permissions: 'UPDATE_USERS'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/workers_comp_classes/workers_comp_classes-list',
|
||||||
|
label: 'Workmans Comp',
|
||||||
|
icon: icon.mdiTable,
|
||||||
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/pay_types/pay_types-list',
|
href: '/pay_types/pay_types-list',
|
||||||
@ -54,7 +74,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiCashMultiple' in icon ? icon['mdiCashMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiCashMultiple' in icon ? icon['mdiCashMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_PAY_TYPES'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/employee_pay_types/employee_pay_types-list',
|
href: '/employee_pay_types/employee_pay_types-list',
|
||||||
@ -62,31 +82,13 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_EMPLOYEE_PAY_TYPES'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/chemical_products/chemical_products-list',
|
|
||||||
label: 'Chemical products',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiFlask' in icon ? icon['mdiFlask' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_CHEMICAL_PRODUCTS'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/job_logs/job_logs-list',
|
href: '/job_logs/job_logs-list',
|
||||||
label: 'Job logs',
|
label: 'All Job Logs',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiClipboardTextClock' in icon ? icon['mdiClipboardTextClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiClipboardTextClock' in icon ? icon['mdiClipboardTextClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_JOB_LOGS'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/job_chemical_usages/job_chemical_usages-list',
|
|
||||||
label: 'Job chemical usages',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiFlaskOutline' in icon ? icon['mdiFlaskOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_JOB_CHEMICAL_USAGES'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/payroll_runs/payroll_runs-list',
|
href: '/payroll_runs/payroll_runs-list',
|
||||||
@ -94,7 +96,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiCalendarClock' in icon ? icon['mdiCalendarClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiCalendarClock' in icon ? icon['mdiCalendarClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_PAYROLL_RUNS'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/payroll_line_items/payroll_line_items-list',
|
href: '/payroll_line_items/payroll_line_items-list',
|
||||||
@ -102,7 +104,19 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiFileDocumentOutline' in icon ? icon['mdiFileDocumentOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiFileDocumentOutline' in icon ? icon['mdiFileDocumentOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_PAYROLL_LINE_ITEMS'
|
permissions: 'UPDATE_USERS'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/reports',
|
||||||
|
label: 'Payroll Dashboard',
|
||||||
|
icon: icon.mdiChartBar,
|
||||||
|
permissions: 'UPDATE_USERS'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/admin-help',
|
||||||
|
label: 'Admin Help',
|
||||||
|
icon: icon.mdiHelpCircleOutline,
|
||||||
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/profile',
|
href: '/profile',
|
||||||
@ -116,8 +130,8 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
target: '_blank',
|
target: '_blank',
|
||||||
label: 'Swagger API',
|
label: 'Swagger API',
|
||||||
icon: icon.mdiFileCode,
|
icon: icon.mdiFileCode,
|
||||||
permissions: 'READ_API_DOCS'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default menuAside
|
export default menuAside
|
||||||
@ -149,7 +149,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
setStepsEnabled(false);
|
setStepsEnabled(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const title = 'Pressure Wash Payroll'
|
const title = 'Major League Pressure Washing'
|
||||||
const description = "Internal payroll app for pressure washing techs to log jobs, track mileage and chemicals, and run payroll reports."
|
const description = "Internal payroll app for pressure washing techs to log jobs, track mileage and chemicals, and run payroll reports."
|
||||||
const url = "https://flatlogic.com/"
|
const url = "https://flatlogic.com/"
|
||||||
const image = "https://project-screens.s3.amazonaws.com/screenshots/39157/app-hero-20260312-154649.png"
|
const image = "https://project-screens.s3.amazonaws.com/screenshots/39157/app-hero-20260312-154649.png"
|
||||||
|
|||||||
304
frontend/src/pages/admin-help.tsx
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
import {
|
||||||
|
mdiAccountCashOutline,
|
||||||
|
mdiCashRegister,
|
||||||
|
mdiClipboardCheckOutline,
|
||||||
|
mdiClipboardListOutline,
|
||||||
|
mdiCogOutline,
|
||||||
|
mdiHelpCircleOutline,
|
||||||
|
mdiOpenInNew,
|
||||||
|
mdiPlayCircleOutline,
|
||||||
|
mdiShieldCheckOutline,
|
||||||
|
} from '@mdi/js';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
|
||||||
|
import BaseButton from '../components/BaseButton';
|
||||||
|
import BaseDivider from '../components/BaseDivider';
|
||||||
|
import BaseIcon from '../components/BaseIcon';
|
||||||
|
import CardBox from '../components/CardBox';
|
||||||
|
import SectionMain from '../components/SectionMain';
|
||||||
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||||
|
import { getPageTitle } from '../config';
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
|
||||||
|
type HelpSection = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
summary: string;
|
||||||
|
steps: string[];
|
||||||
|
checks?: string[];
|
||||||
|
warning?: string;
|
||||||
|
href?: string;
|
||||||
|
hrefLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const helpSections: HelpSection[] = [
|
||||||
|
{
|
||||||
|
id: 'getting-started',
|
||||||
|
title: 'Getting started',
|
||||||
|
icon: mdiPlayCircleOutline,
|
||||||
|
summary:
|
||||||
|
'Use this as the admin’s quick-start routine before you begin assigning pay or creating payroll runs.',
|
||||||
|
steps: [
|
||||||
|
'Confirm your users, customers, vehicles, pay types, and workers comp classes are up to date.',
|
||||||
|
'Make sure employees know whether they should use Log Work or whether an admin is entering logs on their behalf.',
|
||||||
|
'Decide how often payroll should be run and which date range each payroll run should cover.',
|
||||||
|
'Review this help page the first few times so your workflow stays consistent.',
|
||||||
|
],
|
||||||
|
checks: [
|
||||||
|
'Users have the correct role and active account status.',
|
||||||
|
'Employees have at least one pay setup assigned before they start logging jobs.',
|
||||||
|
'Admins know which menu pages they will use most often: Users, Pay types, Employee pay types, All Job Logs, and Payroll runs.',
|
||||||
|
],
|
||||||
|
href: '/dashboard',
|
||||||
|
hrefLabel: 'Open Dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'assign-employee-pay',
|
||||||
|
title: 'Assign employee pay',
|
||||||
|
icon: mdiAccountCashOutline,
|
||||||
|
summary:
|
||||||
|
'This links an employee to the pay option they are allowed to use on jobs.',
|
||||||
|
steps: [
|
||||||
|
'Open Employee pay types.',
|
||||||
|
'Create a new record and select the employee.',
|
||||||
|
'Choose the pay type you want that employee to use, such as hourly or commission.',
|
||||||
|
'Save the record and verify the employee can now select that pay option while logging work.',
|
||||||
|
'If an employee should have multiple valid pay options, add each allowed pay type as its own assignment.',
|
||||||
|
],
|
||||||
|
checks: [
|
||||||
|
'Use clear pay type names so employees can easily choose the right option while logging work.',
|
||||||
|
'Remove outdated assignments if an employee should no longer use an old rate or method.',
|
||||||
|
],
|
||||||
|
href: '/employee_pay_types/employee_pay_types-list',
|
||||||
|
hrefLabel: 'Open Employee Pay Types',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'set-up-pay-types',
|
||||||
|
title: 'Set up pay types',
|
||||||
|
icon: mdiCogOutline,
|
||||||
|
summary:
|
||||||
|
'Pay types control how job pay is calculated. Keep these clean and intentional so payroll stays predictable.',
|
||||||
|
steps: [
|
||||||
|
'Open Pay types and review the existing list before adding a new one.',
|
||||||
|
'For hourly work, confirm the hourly rate is correct.',
|
||||||
|
'For commission-based work, confirm the commission percentage is correct.',
|
||||||
|
'Use consistent naming so admins and employees can tell similar pay types apart.',
|
||||||
|
'Save changes, then test a sample job log if the pay logic is new or changed.',
|
||||||
|
],
|
||||||
|
checks: [
|
||||||
|
'Avoid creating duplicate pay types with only slightly different names.',
|
||||||
|
'When changing a rate, double-check whether you should edit an existing pay type or create a new one for future use.',
|
||||||
|
],
|
||||||
|
href: '/pay_types/pay_types-list',
|
||||||
|
hrefLabel: 'Open Pay Types',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'review-job-logs',
|
||||||
|
title: 'Review job logs before payroll',
|
||||||
|
icon: mdiClipboardCheckOutline,
|
||||||
|
summary:
|
||||||
|
'A quick review before payroll helps catch missing hours, wrong pay types, or incomplete client-paid amounts.',
|
||||||
|
steps: [
|
||||||
|
'Open All Job Logs and filter by the payroll period you are about to run.',
|
||||||
|
'Scan Employee Pay, hours, client paid, and pay type selections for anything that looks incorrect.',
|
||||||
|
'Open any questionable record and verify the employee, customer, vehicle, and work details.',
|
||||||
|
'Fix missing or incorrect values before creating the payroll run.',
|
||||||
|
],
|
||||||
|
checks: [
|
||||||
|
'Hourly jobs should have hours entered.',
|
||||||
|
'Commission jobs should have the client paid amount entered.',
|
||||||
|
'Employees should not have duplicate or accidental logs for the same work.',
|
||||||
|
],
|
||||||
|
href: '/job_logs/job_logs-list',
|
||||||
|
hrefLabel: 'Open All Job Logs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'run-payroll',
|
||||||
|
title: 'Run payroll',
|
||||||
|
icon: mdiCashRegister,
|
||||||
|
summary:
|
||||||
|
'Create a payroll run only after the logs in that date range look clean.',
|
||||||
|
steps: [
|
||||||
|
'Open Payroll runs and create a new run.',
|
||||||
|
'Set the payroll dates carefully so the run only includes the jobs you intend to pay.',
|
||||||
|
'Save the payroll run and review the generated results.',
|
||||||
|
'Open the payroll line items if you need job-by-job detail for what was included.',
|
||||||
|
'If totals look wrong, go back to the source job logs, correct them, and then rerun or recreate payroll as needed.',
|
||||||
|
],
|
||||||
|
checks: [
|
||||||
|
'Watch for an incorrect date range first; that is one of the most common causes of missing or extra pay.',
|
||||||
|
'If one employee total looks off, inspect that employee’s individual job logs before changing anything else.',
|
||||||
|
],
|
||||||
|
href: '/payroll_runs/payroll_runs-list',
|
||||||
|
hrefLabel: 'Open Payroll Runs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'check-results',
|
||||||
|
title: 'Check payroll results',
|
||||||
|
icon: mdiClipboardListOutline,
|
||||||
|
summary:
|
||||||
|
'After a run is created, verify the output before treating it as final.',
|
||||||
|
steps: [
|
||||||
|
'Review the payroll run totals and compare them with the expected workload for that period.',
|
||||||
|
'Open Payroll line items to confirm each employee has the right combination of job entries.',
|
||||||
|
'Spot-check a few records manually using the job log Employee Pay values and the live preview logic on the log form.',
|
||||||
|
'Use the Payroll Dashboard if you want a broader visual review of payroll activity.',
|
||||||
|
],
|
||||||
|
checks: [
|
||||||
|
'Investigate unusual spikes or drops before approving the numbers internally.',
|
||||||
|
'Keep a repeatable review routine so payroll checks stay consistent every pay period.',
|
||||||
|
],
|
||||||
|
href: '/reports',
|
||||||
|
hrefLabel: 'Open Payroll Dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'troubleshooting',
|
||||||
|
title: 'Troubleshooting and common issues',
|
||||||
|
icon: mdiShieldCheckOutline,
|
||||||
|
summary:
|
||||||
|
'Use this checklist whenever the numbers do not look right or an employee cannot complete a job log correctly.',
|
||||||
|
steps: [
|
||||||
|
'If an employee cannot choose the right pay option, confirm the Employee pay types assignment exists.',
|
||||||
|
'If live pay looks wrong on a job log, verify the pay type values and the job fields used for that method.',
|
||||||
|
'If payroll totals look off, compare the payroll date range with the dates on the job logs included in the run.',
|
||||||
|
'If a commission total looks too low or too high, check the client paid amount on the job log first.',
|
||||||
|
'If an hourly total looks wrong, check hours entered and the selected hourly pay type.',
|
||||||
|
],
|
||||||
|
warning:
|
||||||
|
'Best practice: fix the source job log or pay setup first, then regenerate or recreate the payroll output instead of trying to explain away a mismatch later.',
|
||||||
|
href: '/payroll_line_items/payroll_line_items-list',
|
||||||
|
hrefLabel: 'Open Payroll Line Items',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const AdminHelpPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Admin Help')}</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton
|
||||||
|
icon={mdiHelpCircleOutline}
|
||||||
|
title='Admin Help'
|
||||||
|
main
|
||||||
|
>
|
||||||
|
{''}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<CardBox className='mb-6'>
|
||||||
|
<div className='flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between'>
|
||||||
|
<div className='max-w-3xl'>
|
||||||
|
<h2 className='text-xl font-semibold mb-2'>Admin task checklist</h2>
|
||||||
|
<p className='text-gray-600 dark:text-gray-300'>
|
||||||
|
This page is a simple internal knowledge base for the most common admin workflows in the app.
|
||||||
|
Use the quick links below to jump to a process, then open the related page directly when you are ready to do the task.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
|
href='/payroll_runs/payroll_runs-list'
|
||||||
|
color='info'
|
||||||
|
icon={mdiOpenInNew}
|
||||||
|
label='Go to Payroll Runs'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
|
||||||
|
<div className='flex flex-wrap gap-3'>
|
||||||
|
{helpSections.map((section) => (
|
||||||
|
<BaseButton
|
||||||
|
key={section.id}
|
||||||
|
asAnchor
|
||||||
|
href={`#${section.id}`}
|
||||||
|
color='whiteDark'
|
||||||
|
small
|
||||||
|
label={section.title}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||||
|
{helpSections.map((section) => (
|
||||||
|
<CardBox key={section.id} className='h-full'>
|
||||||
|
<div id={section.id} className='scroll-mt-20'>
|
||||||
|
<div className='flex items-start justify-between gap-4 mb-4'>
|
||||||
|
<div className='flex items-start gap-3'>
|
||||||
|
<BaseIcon path={section.icon} size={28} className='text-blue-600 dark:text-blue-400 mt-1' />
|
||||||
|
<div>
|
||||||
|
<h2 className='text-xl font-semibold'>{section.title}</h2>
|
||||||
|
<p className='text-sm text-gray-600 dark:text-gray-300 mt-1'>
|
||||||
|
{section.summary}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{section.href && section.hrefLabel ? (
|
||||||
|
<BaseButton
|
||||||
|
href={section.href}
|
||||||
|
color='info'
|
||||||
|
small
|
||||||
|
icon={mdiOpenInNew}
|
||||||
|
label={section.hrefLabel}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-4'>
|
||||||
|
<div className='text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-2'>
|
||||||
|
Checklist
|
||||||
|
</div>
|
||||||
|
<ol className='list-decimal pl-5 space-y-2 text-sm leading-6'>
|
||||||
|
{section.steps.map((step) => (
|
||||||
|
<li key={step}>{step}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{section.checks?.length ? (
|
||||||
|
<div className='mb-4'>
|
||||||
|
<div className='text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-2'>
|
||||||
|
Verify before moving on
|
||||||
|
</div>
|
||||||
|
<ul className='list-disc pl-5 space-y-2 text-sm leading-6'>
|
||||||
|
{section.checks.map((check) => (
|
||||||
|
<li key={check}>{check}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{section.warning ? (
|
||||||
|
<div className='rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm leading-6 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-100'>
|
||||||
|
<span className='font-semibold'>Admin note:</span> {section.warning}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardBox className='mt-6'>
|
||||||
|
<div className='flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between'>
|
||||||
|
<div>
|
||||||
|
<h2 className='text-lg font-semibold'>Suggested next improvement</h2>
|
||||||
|
<p className='text-sm text-gray-600 dark:text-gray-300 mt-1'>
|
||||||
|
If you want, this help page can later become searchable or editable so admins can maintain their own internal SOPs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseButton href='/dashboard' color='whiteDark' label='Back to Dashboard' />
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AdminHelpPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated permission='UPDATE_USERS'>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminHelpPage;
|
||||||
@ -11,6 +11,7 @@ import { getPageTitle } from '../config'
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { hasPermission } from "../helpers/userPermissions";
|
import { hasPermission } from "../helpers/userPermissions";
|
||||||
|
import { isRestrictedPayrollUser } from '../helpers/accessControl';
|
||||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||||
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
||||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
||||||
@ -32,9 +33,7 @@ const Dashboard = () => {
|
|||||||
const [vehicles, setVehicles] = React.useState(loadingMessage);
|
const [vehicles, setVehicles] = React.useState(loadingMessage);
|
||||||
const [pay_types, setPay_types] = React.useState(loadingMessage);
|
const [pay_types, setPay_types] = React.useState(loadingMessage);
|
||||||
const [employee_pay_types, setEmployee_pay_types] = React.useState(loadingMessage);
|
const [employee_pay_types, setEmployee_pay_types] = React.useState(loadingMessage);
|
||||||
const [chemical_products, setChemical_products] = React.useState(loadingMessage);
|
|
||||||
const [job_logs, setJob_logs] = React.useState(loadingMessage);
|
const [job_logs, setJob_logs] = React.useState(loadingMessage);
|
||||||
const [job_chemical_usages, setJob_chemical_usages] = React.useState(loadingMessage);
|
|
||||||
const [payroll_runs, setPayroll_runs] = React.useState(loadingMessage);
|
const [payroll_runs, setPayroll_runs] = React.useState(loadingMessage);
|
||||||
const [payroll_line_items, setPayroll_line_items] = React.useState(loadingMessage);
|
const [payroll_line_items, setPayroll_line_items] = React.useState(loadingMessage);
|
||||||
|
|
||||||
@ -46,11 +45,20 @@ const Dashboard = () => {
|
|||||||
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
||||||
|
|
||||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
||||||
|
const isAdmin = hasPermission(currentUser, "UPDATE_USERS");
|
||||||
|
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const entities = ['users','roles','permissions','customers','vehicles','pay_types','employee_pay_types','chemical_products','job_logs','job_chemical_usages','payroll_runs','payroll_line_items',];
|
if (!currentUser) return;
|
||||||
const fns = [setUsers,setRoles,setPermissions,setCustomers,setVehicles,setPay_types,setEmployee_pay_types,setChemical_products,setJob_logs,setJob_chemical_usages,setPayroll_runs,setPayroll_line_items,];
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
// For regular employees, we just want to load their own logs
|
||||||
|
axios.get('/job_logs/count?employee=' + currentUser.id).then((res) => setJob_logs(res.data.count)).catch(() => setJob_logs(0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entities = ['users','roles','permissions','customers','vehicles','pay_types','employee_pay_types','job_logs','payroll_runs','payroll_line_items',];
|
||||||
|
const fns = [setUsers,setRoles,setPermissions,setCustomers,setVehicles,setPay_types,setEmployee_pay_types,setJob_logs,setPayroll_runs,setPayroll_line_items,];
|
||||||
|
|
||||||
const requests = entities.map((entity, index) => {
|
const requests = entities.map((entity, index) => {
|
||||||
|
|
||||||
@ -142,12 +150,15 @@ const Dashboard = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
||||||
|
|
||||||
|
{isAdmin ? (
|
||||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
{!isRestrictedPayrollUser(currentUser) && hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
||||||
<div
|
<div
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||||
>
|
>
|
||||||
@ -287,7 +298,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>}
|
</Link>}
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_PAY_TYPES') && <Link href={'/pay_types/pay_types-list'}>
|
{!isRestrictedPayrollUser(currentUser) && hasPermission(currentUser, 'READ_PAY_TYPES') && <Link href={'/pay_types/pay_types-list'}>
|
||||||
<div
|
<div
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||||
>
|
>
|
||||||
@ -315,7 +326,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>}
|
</Link>}
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_EMPLOYEE_PAY_TYPES') && <Link href={'/employee_pay_types/employee_pay_types-list'}>
|
{!isRestrictedPayrollUser(currentUser) && hasPermission(currentUser, 'READ_EMPLOYEE_PAY_TYPES') && <Link href={'/employee_pay_types/employee_pay_types-list'}>
|
||||||
<div
|
<div
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||||
>
|
>
|
||||||
@ -343,34 +354,6 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>}
|
</Link>}
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_CHEMICAL_PRODUCTS') && <Link href={'/chemical_products/chemical_products-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Chemical products
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{chemical_products}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiFlask' in icon ? icon['mdiFlask' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_JOB_LOGS') && <Link href={'/job_logs/job_logs-list'}>
|
{hasPermission(currentUser, 'READ_JOB_LOGS') && <Link href={'/job_logs/job_logs-list'}>
|
||||||
<div
|
<div
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||||
@ -399,34 +382,6 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>}
|
</Link>}
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_JOB_CHEMICAL_USAGES') && <Link href={'/job_chemical_usages/job_chemical_usages-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Job chemical usages
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{job_chemical_usages}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiFlaskOutline' in icon ? icon['mdiFlaskOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_PAYROLL_RUNS') && <Link href={'/payroll_runs/payroll_runs-list'}>
|
{hasPermission(currentUser, 'READ_PAYROLL_RUNS') && <Link href={'/payroll_runs/payroll_runs-list'}>
|
||||||
<div
|
<div
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
||||||
@ -481,11 +436,34 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Link>}
|
</Link>}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div id="dashboard" className="grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6">
|
||||||
|
<Link href={'/my-logs'}>
|
||||||
|
<div className={"dark:bg-dark-900 dark:border-dark-700 p-6 " + (corners !== 'rounded-full' ? corners : 'rounded-3xl') + " " + cardsStyle}>
|
||||||
|
<div className="flex justify-between align-center">
|
||||||
|
<div>
|
||||||
|
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
||||||
|
My Logs
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl leading-tight font-semibold">
|
||||||
|
{job_logs}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<BaseIcon className={iconsColor} w="w-16" h="h-16" size={48} path={icon.mdiClipboardTextClock} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'
|
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import DatePicker from "react-datepicker";
|
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
@ -109,78 +106,7 @@ const EditEmployee_pay_types = () => {
|
|||||||
|
|
||||||
|
|
||||||
active: false,
|
active: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
effective_start: new Date(),
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
effective_end: new Date(),
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
const [initialValues, setInitialValues] = useState(initVals)
|
const [initialValues, setInitialValues] = useState(initVals)
|
||||||
|
|
||||||
const { employee_pay_types } = useAppSelector((state) => state.employee_pay_types)
|
const { employee_pay_types } = useAppSelector((state) => state.employee_pay_types)
|
||||||
@ -392,106 +318,7 @@ const EditEmployee_pay_types = () => {
|
|||||||
component={SwitchField}
|
component={SwitchField}
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
<BaseDivider />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="EffectiveStart"
|
|
||||||
>
|
|
||||||
<DatePicker
|
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
|
||||||
showTimeSelect
|
|
||||||
selected={initialValues.effective_start ?
|
|
||||||
new Date(
|
|
||||||
dayjs(initialValues.effective_start).format('YYYY-MM-DD hh:mm'),
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onChange={(date) => setInitialValues({...initialValues, 'effective_start': date})}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="EffectiveEnd"
|
|
||||||
>
|
|
||||||
<DatePicker
|
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
|
||||||
showTimeSelect
|
|
||||||
selected={initialValues.effective_end ?
|
|
||||||
new Date(
|
|
||||||
dayjs(initialValues.effective_end).format('YYYY-MM-DD hh:mm'),
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onChange={(date) => setInitialValues({...initialValues, 'effective_end': date})}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'
|
import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
import DatePicker from "react-datepicker";
|
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
@ -109,78 +106,7 @@ const EditEmployee_pay_typesPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
active: false,
|
active: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
effective_start: new Date(),
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
effective_end: new Date(),
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
const [initialValues, setInitialValues] = useState(initVals)
|
const [initialValues, setInitialValues] = useState(initVals)
|
||||||
|
|
||||||
const { employee_pay_types } = useAppSelector((state) => state.employee_pay_types)
|
const { employee_pay_types } = useAppSelector((state) => state.employee_pay_types)
|
||||||
@ -389,106 +315,7 @@ const EditEmployee_pay_typesPage = () => {
|
|||||||
component={SwitchField}
|
component={SwitchField}
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
<BaseDivider />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="EffectiveStart"
|
|
||||||
>
|
|
||||||
<DatePicker
|
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
|
||||||
showTimeSelect
|
|
||||||
selected={initialValues.effective_start ?
|
|
||||||
new Date(
|
|
||||||
dayjs(initialValues.effective_start).format('YYYY-MM-DD hh:mm'),
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onChange={(date) => setInitialValues({...initialValues, 'effective_start': date})}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="EffectiveEnd"
|
|
||||||
>
|
|
||||||
<DatePicker
|
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
|
||||||
showTimeSelect
|
|
||||||
selected={initialValues.effective_end ?
|
|
||||||
new Date(
|
|
||||||
dayjs(initialValues.effective_end).format('YYYY-MM-DD hh:mm'),
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onChange={(date) => setInitialValues({...initialValues, 'effective_end': date})}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||||
|
|||||||
@ -37,7 +37,7 @@ const Employee_pay_typesTablesPage = () => {
|
|||||||
const [filters] = useState([
|
const [filters] = useState([
|
||||||
|
|
||||||
|
|
||||||
{label: 'EffectiveStart', title: 'effective_start', date: 'true'},{label: 'EffectiveEnd', title: 'effective_end', date: 'true'},
|
|
||||||
|
|
||||||
|
|
||||||
{label: 'Employee', title: 'employee'},
|
{label: 'Employee', title: 'employee'},
|
||||||
|
|||||||
@ -69,46 +69,6 @@ const initialValues = {
|
|||||||
|
|
||||||
|
|
||||||
active: false,
|
active: false,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
effective_start: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
effective_end: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -228,88 +188,7 @@ const Employee_pay_typesNew = () => {
|
|||||||
component={SwitchField}
|
component={SwitchField}
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
<BaseDivider />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="EffectiveStart"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="datetime-local"
|
|
||||||
name="effective_start"
|
|
||||||
placeholder="EffectiveStart"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="EffectiveEnd"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="datetime-local"
|
|
||||||
name="effective_end"
|
|
||||||
placeholder="EffectiveEnd"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||||
|
|||||||
@ -37,7 +37,7 @@ const Employee_pay_typesTablesPage = () => {
|
|||||||
const [filters] = useState([
|
const [filters] = useState([
|
||||||
|
|
||||||
|
|
||||||
{label: 'EffectiveStart', title: 'effective_start', date: 'true'},{label: 'EffectiveEnd', title: 'effective_end', date: 'true'},
|
|
||||||
|
|
||||||
|
|
||||||
{label: 'Employee', title: 'employee'},
|
{label: 'Employee', title: 'employee'},
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
import React, { ReactElement, useEffect } from 'react';
|
import React, { ReactElement, useEffect } from 'react';
|
||||||
import Head from 'next/head'
|
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 {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
||||||
import {useRouter} from "next/router";
|
import {useRouter} from "next/router";
|
||||||
import { fetch } from '../../stores/employee_pay_types/employee_pay_typesSlice'
|
import { fetch } from '../../stores/employee_pay_types/employee_pay_typesSlice'
|
||||||
@ -199,112 +196,7 @@ const Employee_pay_typesView = () => {
|
|||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
<BaseDivider />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='EffectiveStart'>
|
|
||||||
{employee_pay_types.effective_start ? <DatePicker
|
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
|
||||||
showTimeSelect
|
|
||||||
selected={employee_pay_types.effective_start ?
|
|
||||||
new Date(
|
|
||||||
dayjs(employee_pay_types.effective_start).format('YYYY-MM-DD hh:mm'),
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
disabled
|
|
||||||
/> : <p>No EffectiveStart</p>}
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='EffectiveEnd'>
|
|
||||||
{employee_pay_types.effective_end ? <DatePicker
|
|
||||||
dateFormat="yyyy-MM-dd hh:mm"
|
|
||||||
showTimeSelect
|
|
||||||
selected={employee_pay_types.effective_end ?
|
|
||||||
new Date(
|
|
||||||
dayjs(employee_pay_types.effective_end).format('YYYY-MM-DD hh:mm'),
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
disabled
|
|
||||||
/> : <p>No EffectiveEnd</p>}
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
color='info'
|
color='info'
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export default function Starter() {
|
|||||||
const [contentPosition, setContentPosition] = useState('left');
|
const [contentPosition, setContentPosition] = useState('left');
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||||
|
|
||||||
const title = 'Pressure Wash Payroll'
|
const title = 'Major League Pressure Washing'
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
// Fetch Pexels image/video
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -128,7 +128,7 @@ export default function Starter() {
|
|||||||
: null}
|
: null}
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||||
<CardBoxComponentTitle title="Welcome to your Pressure Wash Payroll app!"/>
|
<CardBoxComponentTitle title="Welcome to your Major League Pressure Washing app!"/>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||||
|
|||||||
@ -270,7 +270,6 @@ const EditJob_logs = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
odometer_start: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -298,7 +297,6 @@ const EditJob_logs = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
odometer_end: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -320,7 +318,6 @@ const EditJob_logs = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
'job_address': '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -875,146 +872,6 @@ const EditJob_logs = () => {
|
|||||||
|
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="OdometerStart"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="number"
|
|
||||||
name="odometer_start"
|
|
||||||
placeholder="OdometerStart"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="OdometerEnd"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="number"
|
|
||||||
name="odometer_end"
|
|
||||||
placeholder="OdometerEnd"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="JobAddress"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="job_address"
|
|
||||||
placeholder="JobAddress"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Status" labelFor="status">
|
<FormField label="Status" labelFor="status">
|
||||||
<Field name="status" id="status" component="select">
|
<Field name="status" id="status" component="select">
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import CardBox from '../../components/CardBox'
|
|||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
||||||
|
import JobLogPayPreview from '../../components/JobLogPayPreview'
|
||||||
import { getPageTitle } from '../../config'
|
import { getPageTitle } from '../../config'
|
||||||
|
|
||||||
import { Field, Form, Formik } from 'formik'
|
import { Field, Form, Formik } from 'formik'
|
||||||
@ -196,7 +197,7 @@ const EditJob_logsPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
workers_comp_class: '',
|
workersCompClass: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -270,7 +271,6 @@ const EditJob_logsPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
odometer_start: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -298,7 +298,6 @@ const EditJob_logsPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
odometer_end: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -320,7 +319,6 @@ const EditJob_logsPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
'job_address': '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -723,17 +721,9 @@ const EditJob_logsPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="WorkmansCompensationClass" labelFor="workers_comp_class">
|
<FormField label="Workmans Comp Class" labelFor="workersCompClassId">
|
||||||
<Field name="workers_comp_class" id="workers_comp_class" component="select">
|
<Field name="workersCompClass" id="workersCompClass" component={SelectField} options={initialValues.workersCompClass} itemRef={'workers_comp_classes'} showField={'name'}></Field>
|
||||||
|
</FormField>
|
||||||
<option value="roof">roof</option>
|
|
||||||
|
|
||||||
<option value="ladder">ladder</option>
|
|
||||||
|
|
||||||
<option value="ground">ground</option>
|
|
||||||
|
|
||||||
</Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -804,6 +794,8 @@ const EditJob_logsPage = () => {
|
|||||||
|
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<JobLogPayPreview />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -872,146 +864,6 @@ const EditJob_logsPage = () => {
|
|||||||
|
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="OdometerStart"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="number"
|
|
||||||
name="odometer_start"
|
|
||||||
placeholder="OdometerStart"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="OdometerEnd"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="number"
|
|
||||||
name="odometer_end"
|
|
||||||
placeholder="OdometerEnd"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="JobAddress"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="job_address"
|
|
||||||
placeholder="JobAddress"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Status" labelFor="status">
|
<FormField label="Status" labelFor="status">
|
||||||
<Field name="status" id="status" component="select">
|
<Field name="status" id="status" component="select">
|
||||||
|
|
||||||
|
|||||||
@ -34,8 +34,7 @@ const Job_logsTablesPage = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
|
||||||
const [filters] = useState([{label: 'JobAddress', title: 'job_address'},{label: 'NotesToAdmin', title: 'notes_to_admin'},
|
const [filters] = useState([{label: 'NotesToAdmin', title: 'notes_to_admin'},
|
||||||
{label: 'OdometerStart', title: 'odometer_start', number: 'true'},{label: 'OdometerEnd', title: 'odometer_end', number: 'true'},
|
|
||||||
{label: 'HoursConducted', title: 'hours_conducted', number: 'true'},{label: 'ClientPaid', title: 'client_paid', number: 'true'},
|
{label: 'HoursConducted', title: 'hours_conducted', number: 'true'},{label: 'ClientPaid', title: 'client_paid', number: 'true'},
|
||||||
{label: 'WorkDate', title: 'work_date', date: 'true'},
|
{label: 'WorkDate', title: 'work_date', date: 'true'},
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import CardBox from '../../components/CardBox'
|
|||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
||||||
|
import JobLogPayPreview from '../../components/JobLogPayPreview'
|
||||||
import { getPageTitle } from '../../config'
|
import { getPageTitle } from '../../config'
|
||||||
|
|
||||||
import { Field, Form, Formik } from 'formik'
|
import { Field, Form, Formik } from 'formik'
|
||||||
@ -119,7 +120,7 @@ const initialValues = {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
workers_comp_class: 'roof',
|
workersCompClass: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -162,7 +163,6 @@ const initialValues = {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
odometer_start: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -178,7 +178,6 @@ const initialValues = {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
odometer_end: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -191,7 +190,6 @@ const initialValues = {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
job_address: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -457,16 +455,8 @@ const Job_logsNew = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="WorkmansCompensationClass" labelFor="workers_comp_class">
|
<FormField label="Workmans Comp Class" labelFor="workersCompClassId">
|
||||||
<Field name="workers_comp_class" id="workers_comp_class" component="select">
|
<Field name="workersCompClass" id="workersCompClass" component={SelectField} options={[]} itemRef={'workers_comp_classes'} showField={'name'}></Field>
|
||||||
|
|
||||||
<option value="roof">roof</option>
|
|
||||||
|
|
||||||
<option value="ladder">ladder</option>
|
|
||||||
|
|
||||||
<option value="ground">ground</option>
|
|
||||||
|
|
||||||
</Field>
|
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
@ -503,6 +493,8 @@ const Job_logsNew = () => {
|
|||||||
<Field name="pay_type" id="pay_type" component={SelectField} options={[]} itemRef={'pay_types'}></Field>
|
<Field name="pay_type" id="pay_type" component={SelectField} options={[]} itemRef={'pay_types'}></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<JobLogPayPreview />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -532,136 +524,6 @@ const Job_logsNew = () => {
|
|||||||
<FormField label="Vehicle" labelFor="vehicle">
|
<FormField label="Vehicle" labelFor="vehicle">
|
||||||
<Field name="vehicle" id="vehicle" component={SelectField} options={[]} itemRef={'vehicles'}></Field>
|
<Field name="vehicle" id="vehicle" component={SelectField} options={[]} itemRef={'vehicles'}></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="OdometerStart"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="number"
|
|
||||||
name="odometer_start"
|
|
||||||
placeholder="OdometerStart"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="OdometerEnd"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="number"
|
|
||||||
name="odometer_end"
|
|
||||||
placeholder="OdometerEnd"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="JobAddress"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="job_address"
|
|
||||||
placeholder="JobAddress"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Status" labelFor="status">
|
<FormField label="Status" labelFor="status">
|
||||||
<Field name="status" id="status" component="select">
|
<Field name="status" id="status" component="select">
|
||||||
|
|
||||||
|
|||||||
@ -34,8 +34,7 @@ const Job_logsTablesPage = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
|
||||||
const [filters] = useState([{label: 'JobAddress', title: 'job_address'},{label: 'NotesToAdmin', title: 'notes_to_admin'},
|
const [filters] = useState([{label: 'NotesToAdmin', title: 'notes_to_admin'},
|
||||||
{label: 'OdometerStart', title: 'odometer_start', number: 'true'},{label: 'OdometerEnd', title: 'odometer_end', number: 'true'},
|
|
||||||
{label: 'HoursConducted', title: 'hours_conducted', number: 'true'},{label: 'ClientPaid', title: 'client_paid', number: 'true'},
|
{label: 'HoursConducted', title: 'hours_conducted', number: 'true'},{label: 'ClientPaid', title: 'client_paid', number: 'true'},
|
||||||
{label: 'WorkDate', title: 'work_date', date: 'true'},
|
{label: 'WorkDate', title: 'work_date', date: 'true'},
|
||||||
|
|
||||||
|
|||||||
@ -421,127 +421,6 @@ const Job_logsView = () => {
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>OdometerStart</p>
|
|
||||||
<p>{job_logs?.odometer_start || 'No data'}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>OdometerEnd</p>
|
|
||||||
<p>{job_logs?.odometer_end || 'No data'}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>JobAddress</p>
|
|
||||||
<p>{job_logs?.job_address}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
<div className={'mb-4'}>
|
||||||
<p className={'block font-bold mb-2'}>Status</p>
|
<p className={'block font-bold mb-2'}>Status</p>
|
||||||
<p>{job_logs?.status ?? 'No data'}</p>
|
<p>{job_logs?.status ?? 'No data'}</p>
|
||||||
@ -600,65 +479,6 @@ const Job_logsView = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<>
|
|
||||||
<p className={'block font-bold mb-2'}>Job_chemical_usages JobLog</p>
|
|
||||||
<CardBox
|
|
||||||
className='mb-6 border border-gray-300 rounded overflow-hidden'
|
|
||||||
hasTable
|
|
||||||
>
|
|
||||||
<div className='overflow-x-auto'>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>QuantityUsed</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>Notes</th>
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{job_logs.job_chemical_usages_job_log && Array.isArray(job_logs.job_chemical_usages_job_log) &&
|
|
||||||
job_logs.job_chemical_usages_job_log.map((item: any) => (
|
|
||||||
<tr key={item.id} onClick={() => router.push(`/job_chemical_usages/job_chemical_usages-view/?id=${item.id}`)}>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="quantity_used">
|
|
||||||
{ item.quantity_used }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="notes">
|
|
||||||
{ item.notes }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{!job_logs?.job_chemical_usages_job_log?.length && <div className={'text-center py-4'}>No data</div>}
|
|
||||||
</CardBox>
|
|
||||||
</>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
|
|||||||
138
frontend/src/pages/log-work.tsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { mdiPencil } 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 JobLogPayPreview from '../components/JobLogPayPreview';
|
||||||
|
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 { SelectField } from '../components/SelectField';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { create } from '../stores/job_logs/job_logsSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const LogWorkPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
||||||
|
const [assignedPayTypes, setAssignedPayTypes] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.id) {
|
||||||
|
axios
|
||||||
|
.get(`/employee_pay_types?employee=${currentUser.id}&active=true`)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data && res.data.rows) {
|
||||||
|
const payTypes = res.data.rows
|
||||||
|
.map((row: any) => row.pay_type)
|
||||||
|
.filter(Boolean);
|
||||||
|
setAssignedPayTypes(payTypes);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => console.error('Failed to fetch assigned pay types:', err));
|
||||||
|
}
|
||||||
|
}, [currentUser]);
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
work_date: new Date().toISOString().slice(0, 10),
|
||||||
|
employee: currentUser?.id || '',
|
||||||
|
customer: '',
|
||||||
|
hours_conducted: '',
|
||||||
|
client_paid: '',
|
||||||
|
workersCompClass: '',
|
||||||
|
pay_type: '',
|
||||||
|
vehicle: '',
|
||||||
|
status: 'submitted',
|
||||||
|
notes_to_admin: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (data: any) => {
|
||||||
|
await dispatch(create(data));
|
||||||
|
await router.push('/my-logs');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Log Work')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiPencil} title="Log Work" main>
|
||||||
|
{''}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
<CardBox>
|
||||||
|
<Formik initialValues={initialValues} onSubmit={(values) => handleSubmit(values)}>
|
||||||
|
{() => (
|
||||||
|
<Form>
|
||||||
|
<FormField label="Work Date">
|
||||||
|
<Field type="date" name="work_date" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Customer" labelFor="customer">
|
||||||
|
<Field name="customer" id="customer" placeholder="Enter customer name" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Hours Conducted">
|
||||||
|
<Field type="number" name="hours_conducted" placeholder="Hours" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Client Paid">
|
||||||
|
<Field type="number" name="client_paid" placeholder="Amount" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Worker's Comp Class" labelFor="workersCompClass">
|
||||||
|
<Field
|
||||||
|
name="workersCompClass"
|
||||||
|
id="workersCompClass"
|
||||||
|
component={SelectField}
|
||||||
|
options={[]}
|
||||||
|
itemRef={'workers_comp_classes'}
|
||||||
|
showField={'name'}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Pay Type" labelFor="pay_type">
|
||||||
|
<Field name="pay_type" id="pay_type" as="select">
|
||||||
|
<option value="">Select an assigned pay type</option>
|
||||||
|
{assignedPayTypes.map((pt) => (
|
||||||
|
<option key={pt.id} value={pt.id}>
|
||||||
|
{pt.name || pt.pay_method}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Field>
|
||||||
|
</FormField>
|
||||||
|
<JobLogPayPreview payTypeOptions={assignedPayTypes} />
|
||||||
|
<FormField label="Vehicle" labelFor="vehicle">
|
||||||
|
<Field
|
||||||
|
name="vehicle"
|
||||||
|
id="vehicle"
|
||||||
|
component={SelectField}
|
||||||
|
options={[]}
|
||||||
|
itemRef={'vehicles'}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Notes to Admin" hasTextareaHeight>
|
||||||
|
<Field name="notes_to_admin" as="textarea" placeholder="Notes..." />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
<BaseButtons>
|
||||||
|
<BaseButton type="submit" color="info" label="Submit Work Log" />
|
||||||
|
</BaseButtons>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</CardBox>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LogWorkPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogWorkPage;
|
||||||
@ -44,7 +44,7 @@ export default function Login() {
|
|||||||
password: '2fa72adf',
|
password: '2fa72adf',
|
||||||
remember: true })
|
remember: true })
|
||||||
|
|
||||||
const title = 'Pressure Wash Payroll'
|
const title = 'Major League Pressure Washing'
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
// Fetch Pexels image/video
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
@ -165,7 +165,7 @@ export default function Login() {
|
|||||||
|
|
||||||
<CardBox id="loginRoles" className='w-full md:w-3/5 lg:w-2/3'>
|
<CardBox id="loginRoles" className='w-full md:w-3/5 lg:w-2/3'>
|
||||||
|
|
||||||
<h2 className="text-4xl font-semibold my-4">{title}</h2>
|
<div className="flex flex-col items-center"><img src="/assets/logo.webp" alt="Logo" className="h-24 mb-4" /><h2 className="text-4xl font-semibold my-4 text-center">{title}</h2></div>
|
||||||
|
|
||||||
<div className='flex flex-row text-gray-500 justify-between'>
|
<div className='flex flex-row text-gray-500 justify-between'>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
51
frontend/src/pages/my-logs.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { mdiViewList, mdiChartTimelineVariant } from '@mdi/js';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import type { ReactElement } from 'react';
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
import SectionMain from '../components/SectionMain';
|
||||||
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||||
|
import { getPageTitle } from '../config';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
|
import { fetch } from '../stores/job_logs/job_logsSlice';
|
||||||
|
import ListJob_logs from '../components/Job_logs/ListJob_logs';
|
||||||
|
|
||||||
|
const MyLogsPage = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
||||||
|
const { job_logs, loading } = useAppSelector((state) => state.job_logs);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser?.id) {
|
||||||
|
dispatch(fetch({ filter: { employee: currentUser.id }, page: currentPage }));
|
||||||
|
}
|
||||||
|
}, [dispatch, currentUser, currentPage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('My Logs')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiViewList} title="My Logs" main>
|
||||||
|
{''}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
<ListJob_logs
|
||||||
|
job_logs={job_logs}
|
||||||
|
loading={loading}
|
||||||
|
onDelete={() => console.log('Delete')}
|
||||||
|
currentPage={currentPage}
|
||||||
|
numPages={1}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
/>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MyLogsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MyLogsPage;
|
||||||
@ -128,6 +128,11 @@ const EditPay_typesPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
'workers_comp_percentage': '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -406,7 +411,15 @@ const EditPay_typesPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="WorkersCompPercentage"
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="number"
|
||||||
|
name="workers_comp_percentage"
|
||||||
|
placeholder="WorkersCompPercentage"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -503,4 +516,4 @@ EditPay_typesPage.getLayout = function getLayout(page: ReactElement) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditPay_typesPage
|
export default EditPay_typesPage
|
||||||
@ -80,6 +80,8 @@ const initialValues = {
|
|||||||
|
|
||||||
|
|
||||||
commission_rate: '',
|
commission_rate: '',
|
||||||
|
|
||||||
|
workers_comp_percentage: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -285,6 +287,15 @@ const Pay_typesNew = () => {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="WorkersCompPercentage"
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="number"
|
||||||
|
name="workers_comp_percentage"
|
||||||
|
placeholder="WorkersCompPercentage"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -393,4 +404,4 @@ Pay_typesNew.getLayout = function getLayout(page: ReactElement) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Pay_typesNew
|
export default Pay_typesNew
|
||||||
@ -276,11 +276,11 @@ const Pay_typesView = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>EffectiveStart</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>EffectiveEnd</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
@ -298,21 +298,7 @@ const Pay_typesView = () => {
|
|||||||
<td data-label="active">
|
<td data-label="active">
|
||||||
{ dataFormatter.booleanFormatter(item.active) }
|
{ dataFormatter.booleanFormatter(item.active) }
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
<td data-label="effective_start">
|
|
||||||
{ dataFormatter.dateTimeFormatter(item.effective_start) }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="effective_end">
|
|
||||||
{ dataFormatter.dateTimeFormatter(item.effective_end) }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@ -30,10 +30,20 @@ const Payroll_runsView = () => {
|
|||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
||||||
function removeLastCharacter(str) {
|
function removeLastCharacter(str) {
|
||||||
console.log(str,`str`)
|
|
||||||
return str.slice(0, -1);
|
return str.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatEmployeeName(employee) {
|
||||||
|
if (!employee) return 'No data';
|
||||||
|
|
||||||
|
const fullName = [employee.firstName, employee.lastName]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return fullName || employee.email || 'No data';
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetch({ id }));
|
dispatch(fetch({ id }));
|
||||||
}, [dispatch, id]);
|
}, [dispatch, id]);
|
||||||
@ -292,72 +302,28 @@ const Payroll_runsView = () => {
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Employee</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>TotalHours</th>
|
<th>TotalHours</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>GrossPay</th>
|
<th>GrossPay</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>TotalCommissionBase</th>
|
<th>TotalCommissionBase</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>TotalClientPaid</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>Summary</th>
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{payroll_runs.payroll_line_items_payroll_run && Array.isArray(payroll_runs.payroll_line_items_payroll_run) &&
|
{payroll_runs.payroll_line_items_payroll_run && Array.isArray(payroll_runs.payroll_line_items_payroll_run) &&
|
||||||
payroll_runs.payroll_line_items_payroll_run.map((item: any) => (
|
payroll_runs.payroll_line_items_payroll_run.map((item: any) => (
|
||||||
<tr key={item.id} onClick={() => router.push(`/payroll_line_items/payroll_line_items-view/?id=${item.id}`)}>
|
<tr key={item.id} onClick={() => router.push(`/payroll_line_items/payroll_line_items-view/?id=${item.id}`)}>
|
||||||
|
<td data-label="employee">
|
||||||
|
{formatEmployeeName(item.employee)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="total_hours">
|
<td data-label="total_hours">
|
||||||
{ item.total_hours }
|
{ item.total_hours }
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="gross_pay">
|
<td data-label="gross_pay">
|
||||||
{ item.gross_pay }
|
{ item.gross_pay }
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="total_commission_base">
|
<td data-label="total_commission_base">
|
||||||
{ item.total_commission_base }
|
{ item.total_commission_base }
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="total_client_paid">
|
|
||||||
{ item.total_client_paid }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="summary">
|
|
||||||
{ item.summary }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
|
|||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
const title = 'Pressure Wash Payroll'
|
const title = 'Major League Pressure Washing'
|
||||||
const [projectUrl, setProjectUrl] = useState('');
|
const [projectUrl, setProjectUrl] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
267
frontend/src/pages/reports.tsx
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import { mdiChartBar, mdiCashCheck, mdiHistory } from '@mdi/js';
|
||||||
|
import Head from 'next/head';
|
||||||
|
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 FormField from '../components/FormField';
|
||||||
|
import BaseButton from '../components/BaseButton';
|
||||||
|
import BaseButtons from '../components/BaseButtons';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const ReportsPage = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState('current'); // 'current' or 'historical'
|
||||||
|
|
||||||
|
// Historical state
|
||||||
|
const [reportData, setReportData] = useState(null);
|
||||||
|
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||||
|
const [filtersHistory, setFiltersHistory] = useState({
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
employeeId: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Current (Generator) state
|
||||||
|
const [previewData, setPreviewData] = useState(null);
|
||||||
|
const [loadingPreview, setLoadingPreview] = useState(false);
|
||||||
|
const [generating, setGenerating] = useState(false);
|
||||||
|
const [filtersCurrent, setFiltersCurrent] = useState({
|
||||||
|
startDate: '',
|
||||||
|
endDate: ''
|
||||||
|
});
|
||||||
|
const [runSuccess, setRunSuccess] = useState('');
|
||||||
|
const [runError, setRunError] = useState('');
|
||||||
|
|
||||||
|
const fetchHistoricalReport = async () => {
|
||||||
|
setLoadingHistory(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/reports', filtersHistory);
|
||||||
|
setReportData(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch historical report:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingHistory(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchCurrentPreview = async () => {
|
||||||
|
setLoadingPreview(true);
|
||||||
|
setRunSuccess('');
|
||||||
|
setRunError('');
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/payroll_generator/preview', filtersCurrent);
|
||||||
|
setPreviewData(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch current preview:', error);
|
||||||
|
setRunError(error.response?.data || 'Failed to fetch unpaid logs.');
|
||||||
|
} finally {
|
||||||
|
setLoadingPreview(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePayroll = async () => {
|
||||||
|
if (!confirm('Are you sure you want to run payroll? This will finalize these amounts and mark the associated logs as paid.')) return;
|
||||||
|
|
||||||
|
setGenerating(true);
|
||||||
|
setRunError('');
|
||||||
|
setRunSuccess('');
|
||||||
|
try {
|
||||||
|
await axios.post('/payroll_generator/generate', {
|
||||||
|
startDate: filtersCurrent.startDate,
|
||||||
|
endDate: filtersCurrent.endDate,
|
||||||
|
name: `Payroll ${filtersCurrent.startDate} to ${filtersCurrent.endDate}`
|
||||||
|
});
|
||||||
|
setRunSuccess('Payroll Run successfully generated!');
|
||||||
|
setPreviewData(null); // Clear preview since they are now paid
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate payroll:', error);
|
||||||
|
setRunError(error.response?.data || 'An error occurred while generating payroll.');
|
||||||
|
} finally {
|
||||||
|
setGenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Payroll Reports')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiChartBar} title='Payroll Dashboard' main>
|
||||||
|
{''}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<div className="flex mb-6 space-x-4 border-b pb-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('current')}
|
||||||
|
className={`px-4 py-2 font-semibold rounded-t-lg transition-colors flex items-center ${
|
||||||
|
activeTab === 'current' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Current Unpaid Logs
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('historical')}
|
||||||
|
className={`px-4 py-2 font-semibold rounded-t-lg transition-colors flex items-center ${
|
||||||
|
activeTab === 'historical' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Historical Reports
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === 'current' && (
|
||||||
|
<div>
|
||||||
|
<CardBox className="mb-6">
|
||||||
|
<div className="mb-4 text-gray-600">
|
||||||
|
Select a date range to view all <strong>unpaid</strong> Job Logs. From here, you can preview what your employees have currently earned and generate a finalized Payroll Run.
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<FormField label="Start Date">
|
||||||
|
<input type="date" value={filtersCurrent.startDate} onChange={e => setFiltersCurrent({...filtersCurrent, startDate: e.target.value})} className="px-3 py-2 border border-gray-300 rounded w-full" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="End Date">
|
||||||
|
<input type="date" value={filtersCurrent.endDate} onChange={e => setFiltersCurrent({...filtersCurrent, endDate: e.target.value})} className="px-3 py-2 border border-gray-300 rounded w-full" />
|
||||||
|
</FormField>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<BaseButton label="Preview Current Payroll" color="info" onClick={fetchCurrentPreview} disabled={loadingPreview} className="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{runError && <div className="mt-4 p-3 bg-red-100 text-red-700 rounded">{runError}</div>}
|
||||||
|
{runSuccess && <div className="mt-4 p-3 bg-green-100 text-green-700 rounded">{runSuccess}</div>}
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
{previewData && (
|
||||||
|
<>
|
||||||
|
<CardBox className="mb-6">
|
||||||
|
<h2 className="text-xl font-bold mb-4">Live Preview Summary</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="p-4 bg-gray-100 rounded">
|
||||||
|
<p className="text-sm text-gray-500">Total Unpaid Gross Pay</p>
|
||||||
|
<p className="text-2xl font-semibold">${previewData.summary.totalGrossPay.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-gray-100 rounded">
|
||||||
|
<p className="text-sm text-gray-500">Total Unpaid Hours</p>
|
||||||
|
<p className="text-2xl font-semibold">{previewData.summary.totalHours.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<BaseButton label="Run & Finalize Payroll" color="success" onClick={generatePayroll} disabled={generating} />
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox>
|
||||||
|
<h2 className="text-xl font-bold mb-4">Employee Breakdown</h2>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="p-2 border-b">Employee</th>
|
||||||
|
<th className="p-2 border-b">Total Hours</th>
|
||||||
|
<th className="p-2 border-b">Commission Base</th>
|
||||||
|
<th className="p-2 border-b">Calculated Gross Pay</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{previewData.lineItems.map((item: any, idx: number) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td className="p-2 border-b">{item.employee?.firstName} {item.employee?.lastName || ''}</td>
|
||||||
|
<td className="p-2 border-b">{item.total_hours.toFixed(2)}</td>
|
||||||
|
<td className="p-2 border-b">${item.total_commission_base.toFixed(2)}</td>
|
||||||
|
<td className="p-2 border-b font-semibold text-green-600">${item.gross_pay.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'historical' && (
|
||||||
|
<div>
|
||||||
|
<CardBox className="mb-6">
|
||||||
|
<div className="mb-4 text-gray-600">
|
||||||
|
View previously generated payroll line items by date range or employee ID.
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
|
<FormField label="Start Date">
|
||||||
|
<input type="date" value={filtersHistory.startDate} onChange={e => setFiltersHistory({...filtersHistory, startDate: e.target.value})} className="px-3 py-2 border border-gray-300 rounded w-full" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="End Date">
|
||||||
|
<input type="date" value={filtersHistory.endDate} onChange={e => setFiltersHistory({...filtersHistory, endDate: e.target.value})} className="px-3 py-2 border border-gray-300 rounded w-full" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Employee ID">
|
||||||
|
<input type="text" value={filtersHistory.employeeId} onChange={e => setFiltersHistory({...filtersHistory, employeeId: e.target.value})} className="px-3 py-2 border border-gray-300 rounded w-full" />
|
||||||
|
</FormField>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<BaseButton label="View Historical Report" color="info" onClick={fetchHistoricalReport} disabled={loadingHistory} className="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
{reportData && (
|
||||||
|
<>
|
||||||
|
<CardBox className="mb-6">
|
||||||
|
<h2 className="text-xl font-bold mb-4">Historical Summary</h2>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 bg-gray-100 rounded">
|
||||||
|
<p className="text-sm text-gray-500">Total Gross Pay</p>
|
||||||
|
<p className="text-2xl font-semibold">${reportData.summary.totalGrossPay.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-gray-100 rounded">
|
||||||
|
<p className="text-sm text-gray-500">Total Hours</p>
|
||||||
|
<p className="text-2xl font-semibold">{reportData.summary.totalHours.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-gray-100 rounded">
|
||||||
|
<p className="text-sm text-gray-500">Total Work Comp</p>
|
||||||
|
<p className="text-2xl font-semibold">${reportData.summary.totalWorkersComp.toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
<CardBox>
|
||||||
|
<h2 className="text-xl font-bold mb-4">Line Items</h2>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="p-2 border-b">Employee</th>
|
||||||
|
<th className="p-2 border-b">Hours</th>
|
||||||
|
<th className="p-2 border-b">Gross Pay</th>
|
||||||
|
<th className="p-2 border-b">Work Comp Amount</th>
|
||||||
|
<th className="p-2 border-b">Created At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{reportData.lineItems.map((item: any) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td className="p-2 border-b">{item.employee?.firstName} {item.employee?.lastName || ''}</td>
|
||||||
|
<td className="p-2 border-b">{item.total_hours}</td>
|
||||||
|
<td className="p-2 border-b">${item.gross_pay}</td>
|
||||||
|
<td className="p-2 border-b">${item.workers_comp_amount}</td>
|
||||||
|
<td className="p-2 border-b">{new Date(item.createdAt).toLocaleDateString()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ReportsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReportsPage;
|
||||||
@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
|
|||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
const title = 'Pressure Wash Payroll';
|
const title = 'Major League Pressure Washing';
|
||||||
const [projectUrl, setProjectUrl] = useState('');
|
const [projectUrl, setProjectUrl] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -261,6 +261,7 @@ const EditUsersPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
custom_permissions: [],
|
custom_permissions: [],
|
||||||
|
pay_types: [],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -278,16 +279,17 @@ const EditUsersPage = () => {
|
|||||||
dispatch(fetch({ id: id }))
|
dispatch(fetch({ id: id }))
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof users === 'object') {
|
|
||||||
setInitialValues(users)
|
|
||||||
}
|
|
||||||
}, [users])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof users === 'object') {
|
if (typeof users === 'object') {
|
||||||
const newInitialVal = {...initVals};
|
const newInitialVal = {...initVals};
|
||||||
Object.keys(initVals).forEach(el => newInitialVal[el] = (users)[el])
|
Object.keys(initVals).forEach((el) => {
|
||||||
|
newInitialVal[el] = users[el]
|
||||||
|
})
|
||||||
|
newInitialVal.pay_types = Array.isArray(users.employee_pay_types_employee)
|
||||||
|
? users.employee_pay_types_employee
|
||||||
|
.filter((item) => item?.active && item?.pay_type)
|
||||||
|
.map((item) => item.pay_type)
|
||||||
|
: []
|
||||||
setInitialValues(newInitialVal);
|
setInitialValues(newInitialVal);
|
||||||
}
|
}
|
||||||
}, [users])
|
}, [users])
|
||||||
@ -676,6 +678,18 @@ const EditUsersPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FormField label='Pay Types' labelFor='pay_types'>
|
||||||
|
<Field
|
||||||
|
name='pay_types'
|
||||||
|
id='pay_types'
|
||||||
|
component={SelectFieldMany}
|
||||||
|
options={initialValues.pay_types}
|
||||||
|
itemRef={'pay_types'}
|
||||||
|
showField={'name'}
|
||||||
|
></Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Password"
|
label="Password"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -155,6 +155,7 @@ const initialValues = {
|
|||||||
|
|
||||||
|
|
||||||
custom_permissions: [],
|
custom_permissions: [],
|
||||||
|
pay_types: [],
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -469,7 +470,16 @@ const UsersNew = () => {
|
|||||||
</Field>
|
</Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Pay Types' labelFor='pay_types'>
|
||||||
|
<Field
|
||||||
|
name='pay_types'
|
||||||
|
id='pay_types'
|
||||||
|
itemRef={'pay_types'}
|
||||||
|
options={[]}
|
||||||
|
component={SelectFieldMany}
|
||||||
|
showField={'name'}>
|
||||||
|
</Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -433,15 +433,16 @@ const UsersView = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<th>Pay Type</th>
|
||||||
<th>Active</th>
|
<th>Active</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>EffectiveStart</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th>EffectiveEnd</th>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
@ -456,24 +457,13 @@ const UsersView = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<td data-label="pay_type">
|
||||||
|
{item?.pay_type?.name ?? 'No data'}
|
||||||
|
</td>
|
||||||
<td data-label="active">
|
<td data-label="active">
|
||||||
{ dataFormatter.booleanFormatter(item.active) }
|
{ dataFormatter.booleanFormatter(item.active) }
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
<td data-label="effective_start">
|
|
||||||
{ dataFormatter.dateTimeFormatter(item.effective_start) }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<td data-label="effective_end">
|
|
||||||
{ dataFormatter.dateTimeFormatter(item.effective_end) }
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Edit workers_comp_classes')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit workers_comp_classes'} main>
|
||||||
|
{''}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
<CardBox>
|
||||||
|
<Formik
|
||||||
|
enableReinitialize
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="WorkersCompClassName"
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
name="name"
|
||||||
|
placeholder="WorkersCompClassName"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FormField label="PayMethod" labelFor="pay_method">
|
||||||
|
<Field name="pay_method" id="pay_method" component="select">
|
||||||
|
|
||||||
|
<option value="hourly">hourly</option>
|
||||||
|
|
||||||
|
<option value="commission">commission</option>
|
||||||
|
|
||||||
|
</Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="HourlyRate"
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="number"
|
||||||
|
name="hourly_rate"
|
||||||
|
placeholder="HourlyRate"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="CommissionRate"
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
type="number"
|
||||||
|
name="commission_rate"
|
||||||
|
placeholder="CommissionRate"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FormField label='Active' labelFor='active'>
|
||||||
|
<Field
|
||||||
|
name='active'
|
||||||
|
id='active'
|
||||||
|
component={SwitchField}
|
||||||
|
></Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FormField label="Description" hasTextareaHeight>
|
||||||
|
<Field name="description" as="textarea" placeholder="Description" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
<BaseButtons>
|
||||||
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
|
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||||
|
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/workers_comp_classes/workers_comp_classes-list')}/>
|
||||||
|
</BaseButtons>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</CardBox>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
EditWorkers_comp_classes.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return (
|
||||||
|
<LayoutAuthenticated
|
||||||
|
|
||||||
|
permission={'UPDATE_PAY_TYPES'}
|
||||||
|
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</LayoutAuthenticated>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditWorkers_comp_classes
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
import { mdiChartTimelineVariant } 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 { 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 { update, fetch } from '../../stores/workers_comp_classes/workers_comp_classesSlice'
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
const EditWorkers_comp_classesPage = () => {
|
||||||
|
const router = useRouter()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const initVals = {
|
||||||
|
name: '',
|
||||||
|
percentage: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const [initialValues, setInitialValues] = useState(initVals)
|
||||||
|
|
||||||
|
const { workers_comp_classes } = useAppSelector((state) => state.workers_comp_classes)
|
||||||
|
const { id } = router.query
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
dispatch(fetch({ id: id as string }))
|
||||||
|
}
|
||||||
|
}, [id, dispatch])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof workers_comp_classes === 'object' && workers_comp_classes !== null) {
|
||||||
|
const newInitialVal = { ...initVals };
|
||||||
|
Object.keys(initVals).forEach(el => {
|
||||||
|
if (workers_comp_classes[el] !== undefined) {
|
||||||
|
newInitialVal[el] = workers_comp_classes[el];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setInitialValues(newInitialVal);
|
||||||
|
}
|
||||||
|
}, [workers_comp_classes])
|
||||||
|
|
||||||
|
const handleSubmit = async (data: any) => {
|
||||||
|
await dispatch(update({ id: id, data }))
|
||||||
|
await router.push('/workers_comp_classes/workers_comp_classes-list')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Edit Workers Comp Class')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={'Edit Workers Comp Class'} main>
|
||||||
|
{''}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
<CardBox>
|
||||||
|
<Formik
|
||||||
|
enableReinitialize
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<FormField label="Name" labelFor="name">
|
||||||
|
<Field name="name" id="name" type="text" />
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Percentage" labelFor="percentage">
|
||||||
|
<Field name="percentage" id="percentage" type="number" step="0.01" />
|
||||||
|
</FormField>
|
||||||
|
<BaseDivider />
|
||||||
|
<BaseButtons>
|
||||||
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
|
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||||
|
</BaseButtons>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</CardBox>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
EditWorkers_comp_classesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return (
|
||||||
|
<LayoutAuthenticated permission={'UPDATE_WORKERS_COMP_CLASSES'}>
|
||||||
|
{page}
|
||||||
|
</LayoutAuthenticated>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditWorkers_comp_classesPage
|
||||||
@ -0,0 +1,260 @@
|
|||||||
|
import { mdiChartTimelineVariant, mdiPlus } from '@mdi/js'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import { uniqueId } from 'lodash'
|
||||||
|
import React, { ReactElement, useCallback, 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 { useAppSelector } from '../../stores/hooks'
|
||||||
|
import { hasPermission } from '../../helpers/userPermissions'
|
||||||
|
import FormField from '../../components/FormField'
|
||||||
|
|
||||||
|
const formatDateLabel = (value: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(`${value}T00:00:00`).toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDateInputValue = (value: Date) => {
|
||||||
|
const year = value.getFullYear()
|
||||||
|
const month = `${value.getMonth() + 1}`.padStart(2, '0')
|
||||||
|
const day = `${value.getDate()}`.padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getThisWeekFilters = () => {
|
||||||
|
const today = new Date()
|
||||||
|
const startOfWeek = new Date(today)
|
||||||
|
|
||||||
|
startOfWeek.setDate(today.getDate() - today.getDay())
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDate: formatDateInputValue(startOfWeek),
|
||||||
|
endDate: formatDateInputValue(today),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Workers_comp_classesList = () => {
|
||||||
|
const [filterItems, setFilterItems] = useState<any[]>([])
|
||||||
|
const [filters] = useState([
|
||||||
|
{ label: 'Name', title: 'name' },
|
||||||
|
{ label: 'Percentage', title: 'percentage', number: 'true' },
|
||||||
|
])
|
||||||
|
const [reportFilters, setReportFilters] = useState({
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
})
|
||||||
|
const [appliedReportFilters, setAppliedReportFilters] = useState({
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
})
|
||||||
|
const [reportData, setReportData] = useState<any>(null)
|
||||||
|
const [reportLoading, setReportLoading] = useState(false)
|
||||||
|
const [reportError, setReportError] = useState('')
|
||||||
|
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth)
|
||||||
|
|
||||||
|
const addFilter = () => {
|
||||||
|
const newItem = {
|
||||||
|
id: uniqueId(),
|
||||||
|
fields: {
|
||||||
|
filterValue: '',
|
||||||
|
filterValueFrom: '',
|
||||||
|
filterValueTo: '',
|
||||||
|
selectedField: filters[0].title,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
setFilterItems([...filterItems, newItem])
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchReport = useCallback(async (nextFilters: { startDate: string; endDate: string }) => {
|
||||||
|
setReportLoading(true)
|
||||||
|
setReportError('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params: { startDate?: string; endDate?: string } = {}
|
||||||
|
|
||||||
|
if (nextFilters.startDate) {
|
||||||
|
params.startDate = nextFilters.startDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextFilters.endDate) {
|
||||||
|
params.endDate = nextFilters.endDate
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await axios.get('/workers_comp_report/report', { params })
|
||||||
|
setReportData(res.data)
|
||||||
|
setAppliedReportFilters(nextFilters)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to fetch workers comp report', error)
|
||||||
|
setReportData(null)
|
||||||
|
setReportError(
|
||||||
|
error?.response?.data?.message || 'Failed to fetch workers comp report.',
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setReportLoading(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchReport({ startDate: '', endDate: '' })
|
||||||
|
}, [fetchReport])
|
||||||
|
|
||||||
|
const handleApplyDateFilter = () => {
|
||||||
|
fetchReport(reportFilters)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleThisWeekFilter = () => {
|
||||||
|
const thisWeekFilters = getThisWeekFilters()
|
||||||
|
setReportFilters(thisWeekFilters)
|
||||||
|
fetchReport(thisWeekFilters)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClearDateFilter = () => {
|
||||||
|
const clearedFilters = { startDate: '', endDate: '' }
|
||||||
|
setReportFilters(clearedFilters)
|
||||||
|
fetchReport(clearedFilters)
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportHeading = appliedReportFilters.startDate || appliedReportFilters.endDate
|
||||||
|
? `Workman's Comp Totals (${formatDateLabel(appliedReportFilters.startDate) || 'Beginning'} - ${formatDateLabel(appliedReportFilters.endDate) || 'Today'})`
|
||||||
|
: "Workman's Comp Totals (All Time)"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Workers Comp Classes')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton
|
||||||
|
icon={mdiChartTimelineVariant}
|
||||||
|
title={'Workers Comp Classes'}
|
||||||
|
main
|
||||||
|
>
|
||||||
|
{hasPermission(currentUser, 'CREATE_WORKERS_COMP_CLASSES') && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BaseButton color='info' label='Filter' onClick={addFilter} />
|
||||||
|
<Link href={'/workers_comp_classes/workers_comp_classes-new'}>
|
||||||
|
<BaseButton
|
||||||
|
color="info"
|
||||||
|
icon={mdiPlus}
|
||||||
|
label="New Workers Comp Class"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<CardBox className="mb-6">
|
||||||
|
<div className="mb-4 flex flex-wrap gap-3">
|
||||||
|
<BaseButton
|
||||||
|
color="info"
|
||||||
|
outline
|
||||||
|
label="This Week"
|
||||||
|
onClick={handleThisWeekFilter}
|
||||||
|
disabled={reportLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<FormField label="Start Date" labelFor="workers-comp-start-date">
|
||||||
|
<input
|
||||||
|
id="workers-comp-start-date"
|
||||||
|
type="date"
|
||||||
|
value={reportFilters.startDate}
|
||||||
|
onChange={(event) =>
|
||||||
|
setReportFilters({
|
||||||
|
...reportFilters,
|
||||||
|
startDate: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="End Date" labelFor="workers-comp-end-date">
|
||||||
|
<input
|
||||||
|
id="workers-comp-end-date"
|
||||||
|
type="date"
|
||||||
|
value={reportFilters.endDate}
|
||||||
|
onChange={(event) =>
|
||||||
|
setReportFilters({
|
||||||
|
...reportFilters,
|
||||||
|
endDate: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<div className="flex items-end gap-3 pb-6">
|
||||||
|
<BaseButton
|
||||||
|
color="info"
|
||||||
|
label={reportLoading ? 'Applying...' : 'Apply Date Filter'}
|
||||||
|
onClick={handleApplyDateFilter}
|
||||||
|
disabled={reportLoading}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
color="info"
|
||||||
|
outline
|
||||||
|
label="Clear"
|
||||||
|
onClick={handleClearDateFilter}
|
||||||
|
disabled={reportLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{reportError && (
|
||||||
|
<div className="mt-2 rounded bg-red-100 p-3 text-red-700">
|
||||||
|
{reportError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
{reportData && (
|
||||||
|
<CardBox className="mb-6">
|
||||||
|
<h3 className="text-xl font-semibold mb-4">{reportHeading}</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="p-4 bg-blue-50 rounded shadow">
|
||||||
|
<p className="text-sm text-blue-600 font-bold">Total Work Comp</p>
|
||||||
|
<p className="text-2xl font-semibold">
|
||||||
|
${Number(reportData.totalComp || 0).toFixed(2)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{Object.entries(reportData.totalsByClass || {}).map(([className, total]: any) => (
|
||||||
|
<div key={className} className="p-4 bg-gray-50 rounded shadow">
|
||||||
|
<p className="text-sm text-gray-500 font-bold">{className}</p>
|
||||||
|
<p className="text-xl font-semibold">${Number(total).toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{!reportLoading && !Object.keys(reportData.totalsByClass || {}).length && (
|
||||||
|
<p className="mt-4 text-sm text-gray-500">
|
||||||
|
No workers comp totals found for the selected date range.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CardBox className="mb-6" hasTable>
|
||||||
|
<TableWorkers_comp_classes
|
||||||
|
filterItems={filterItems}
|
||||||
|
setFilterItems={setFilterItems}
|
||||||
|
filters={filters}
|
||||||
|
showGrid={false}
|
||||||
|
/>
|
||||||
|
</CardBox>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Workers_comp_classesList.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Workers_comp_classesList
|
||||||