Autosave: 20260325-195455
This commit is contained in:
parent
3fa7195d98
commit
6b8c666c65
BIN
assets/pasted-20260314-132921-6765892c.webp
Normal file
BIN
assets/pasted-20260314-132921-6765892c.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@ -279,7 +279,7 @@ module.exports = class Employee_pay_typesDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -296,7 +296,7 @@ module.exports = class Employee_pay_typesDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -264,7 +264,7 @@ module.exports = class Job_chemical_usagesDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -281,7 +281,7 @@ module.exports = class Job_chemical_usagesDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -412,7 +412,7 @@ module.exports = class Job_logsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -429,7 +429,7 @@ module.exports = class Job_logsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -446,7 +446,7 @@ module.exports = class Job_logsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -463,7 +463,7 @@ module.exports = class Job_logsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -479,7 +479,7 @@ module.exports = class Job_logsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -315,7 +315,7 @@ module.exports = class Payroll_line_itemsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -332,7 +332,7 @@ module.exports = class Payroll_line_itemsDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -484,7 +484,7 @@ module.exports = class UsersDBApi {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
} : {},
|
} : undefined,
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -132,6 +132,7 @@ app.use('/api/payroll_runs', passport.authenticate('jwt', {session: false}), pay
|
|||||||
|
|
||||||
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/reports', passport.authenticate('jwt', {session: false}), reportsRoutes);
|
||||||
|
app.use('/api/payroll_generator', require('./routes/payroll_generator'));
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/api/openai',
|
'/api/openai',
|
||||||
|
|||||||
199
backend/src/routes/payroll_generator.js
Normal file
199
backend/src/routes/payroll_generator.js
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const passport = require('passport');
|
||||||
|
const db = require('../db/models');
|
||||||
|
const { wrapAsync } = require('../helpers');
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
|
router.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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find job logs in range that are not paid
|
||||||
|
const jobLogs = await db.job_logs.findAll({
|
||||||
|
where: {
|
||||||
|
work_date: {
|
||||||
|
[Op.between]: [new Date(startDate), new Date(endDate)]
|
||||||
|
},
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find job logs
|
||||||
|
const jobLogs = await db.job_logs.findAll({
|
||||||
|
where: {
|
||||||
|
work_date: {
|
||||||
|
[Op.between]: [new Date(startDate), new Date(endDate)]
|
||||||
|
},
|
||||||
|
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;
|
||||||
@ -23,7 +23,7 @@ module.exports = class Job_logsService {
|
|||||||
customerId = customer.id;
|
customerId = customer.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Job_logsDBApi.create(
|
const createdJob = await Job_logsDBApi.create(
|
||||||
{ ...data, customer: customerId },
|
{ ...data, customer: customerId },
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -31,6 +31,21 @@ module.exports = class Job_logsService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|||||||
BIN
frontend/public/assets/logo.webp
Normal file
BIN
frontend/public/assets/logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/public/assets/vm-shot-2026-03-14T13-28-33-023Z.jpg
Normal file
BIN
frontend/public/assets/vm-shot-2026-03-14T13-28-33-023Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}`
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
href: '/dashboard',
|
href: '/dashboard',
|
||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
|
permissions: 'UPDATE_USERS',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -27,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',
|
||||||
@ -35,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',
|
||||||
@ -43,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',
|
||||||
@ -51,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',
|
||||||
@ -59,13 +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',
|
href: '/workers_comp_classes/workers_comp_classes-list',
|
||||||
label: 'Workmans Comp',
|
label: 'Workmans Comp',
|
||||||
icon: icon.mdiTable,
|
icon: icon.mdiTable,
|
||||||
permissions: 'READ_WORKERS_COMP_CLASSES'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/pay_types/pay_types-list',
|
href: '/pay_types/pay_types-list',
|
||||||
@ -73,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',
|
||||||
@ -81,7 +82,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: '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',
|
href: '/chemical_products/chemical_products-list',
|
||||||
@ -89,13 +90,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: 'mdiFlask' in icon ? icon['mdiFlask' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiFlask' in icon ? icon['mdiFlask' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_CHEMICAL_PRODUCTS'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/job_logs/job_logs-list',
|
href: '/job_logs/job_logs-list',
|
||||||
label: 'All Job Logs',
|
label: 'All Job Logs',
|
||||||
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',
|
href: '/job_chemical_usages/job_chemical_usages-list',
|
||||||
@ -103,7 +104,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: 'mdiFlaskOutline' in icon ? icon['mdiFlaskOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiFlaskOutline' in icon ? icon['mdiFlaskOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_JOB_CHEMICAL_USAGES'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/payroll_runs/payroll_runs-list',
|
href: '/payroll_runs/payroll_runs-list',
|
||||||
@ -111,7 +112,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',
|
||||||
@ -119,13 +120,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: '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',
|
href: '/reports',
|
||||||
label: 'Payroll Reports',
|
label: 'Payroll Dashboard',
|
||||||
icon: icon.mdiChartBar,
|
icon: icon.mdiChartBar,
|
||||||
permissions: 'READ_PAYROLL_LINE_ITEMS'
|
permissions: 'UPDATE_USERS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/profile',
|
href: '/profile',
|
||||||
@ -139,7 +140,7 @@ 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'
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -46,9 +46,18 @@ 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() {
|
||||||
|
if (!currentUser) return;
|
||||||
|
|
||||||
|
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','chemical_products','job_logs','job_chemical_usages','payroll_runs','payroll_line_items',];
|
const entities = ['users','roles','permissions','customers','vehicles','pay_types','employee_pay_types','chemical_products','job_logs','job_chemical_usages','payroll_runs','payroll_line_items',];
|
||||||
const fns = [setUsers,setRoles,setPermissions,setCustomers,setVehicles,setPay_types,setEmployee_pay_types,setChemical_products,setJob_logs,setJob_chemical_usages,setPayroll_runs,setPayroll_line_items,];
|
const fns = [setUsers,setRoles,setPermissions,setCustomers,setVehicles,setPay_types,setEmployee_pay_types,setChemical_products,setJob_logs,setJob_chemical_usages,setPayroll_runs,setPayroll_line_items,];
|
||||||
|
|
||||||
@ -142,9 +151,12 @@ 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'}>
|
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
||||||
@ -481,11 +493,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>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { mdiPencil, mdiChartTimelineVariant } from '@mdi/js';
|
import { mdiPencil, mdiPlus, mdiTrashCan } 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 CardBox from '../components/CardBox';
|
import CardBox from '../components/CardBox';
|
||||||
@ -7,7 +7,7 @@ import SectionMain from '../components/SectionMain';
|
|||||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
|
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik, FieldArray } from 'formik';
|
||||||
import FormField from '../components/FormField';
|
import FormField from '../components/FormField';
|
||||||
import BaseDivider from '../components/BaseDivider';
|
import BaseDivider from '../components/BaseDivider';
|
||||||
import BaseButtons from '../components/BaseButtons';
|
import BaseButtons from '../components/BaseButtons';
|
||||||
@ -52,6 +52,7 @@ const LogWorkPage = () => {
|
|||||||
job_address: '',
|
job_address: '',
|
||||||
status: 'submitted',
|
status: 'submitted',
|
||||||
notes_to_admin: '',
|
notes_to_admin: '',
|
||||||
|
chemical_usages: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (data: any) => {
|
const handleSubmit = async (data: any) => {
|
||||||
@ -70,52 +71,102 @@ const LogWorkPage = () => {
|
|||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
<Formik initialValues={initialValues} onSubmit={(values) => handleSubmit(values)}>
|
<Formik initialValues={initialValues} onSubmit={(values) => handleSubmit(values)}>
|
||||||
<Form>
|
{({ values }) => (
|
||||||
<FormField label="Work Date">
|
<Form>
|
||||||
<Field type="datetime-local" name="work_date" />
|
<FormField label="Work Date">
|
||||||
</FormField>
|
<Field type="datetime-local" name="work_date" />
|
||||||
<FormField label="Customer" labelFor="customer">
|
</FormField>
|
||||||
<Field name="customer" id="customer" placeholder="Enter customer name" />
|
<FormField label="Customer" labelFor="customer">
|
||||||
</FormField>
|
<Field name="customer" id="customer" placeholder="Enter customer name" />
|
||||||
<FormField label="Hours Conducted">
|
</FormField>
|
||||||
<Field type="number" name="hours_conducted" placeholder="Hours" />
|
<FormField label="Hours Conducted">
|
||||||
</FormField>
|
<Field type="number" name="hours_conducted" placeholder="Hours" />
|
||||||
<FormField label="Client Paid">
|
</FormField>
|
||||||
<Field type="number" name="client_paid" placeholder="Amount" />
|
<FormField label="Client Paid">
|
||||||
</FormField>
|
<Field type="number" name="client_paid" placeholder="Amount" />
|
||||||
<FormField label="Worker's Comp Class" labelFor="workersCompClass">
|
</FormField>
|
||||||
<Field name="workersCompClass" id="workersCompClass" component={SelectField} options={[]} itemRef={"workers_comp_classes"} showField={"name"} />
|
<FormField label="Worker's Comp Class" labelFor="workersCompClass">
|
||||||
</FormField>
|
<Field name="workersCompClass" id="workersCompClass" component={SelectField} options={[]} itemRef={"workers_comp_classes"} showField={"name"} />
|
||||||
<FormField label="Pay Type" labelFor="pay_type">
|
</FormField>
|
||||||
<Field name="pay_type" id="pay_type" as="select">
|
<FormField label="Pay Type" labelFor="pay_type">
|
||||||
<option value="">Select an assigned pay type</option>
|
<Field name="pay_type" id="pay_type" as="select">
|
||||||
{assignedPayTypes.map((pt) => (
|
<option value="">Select an assigned pay type</option>
|
||||||
<option key={pt.id} value={pt.id}>
|
{assignedPayTypes.map((pt) => (
|
||||||
{pt.name || pt.pay_method}
|
<option key={pt.id} value={pt.id}>
|
||||||
</option>
|
{pt.name || pt.pay_method}
|
||||||
))}
|
</option>
|
||||||
</Field>
|
))}
|
||||||
</FormField>
|
</Field>
|
||||||
<FormField label="Vehicle" labelFor="vehicle">
|
</FormField>
|
||||||
<Field name="vehicle" id="vehicle" component={SelectField} options={[]} itemRef={'vehicles'} />
|
<FormField label="Vehicle" labelFor="vehicle">
|
||||||
</FormField>
|
<Field name="vehicle" id="vehicle" component={SelectField} options={[]} itemRef={'vehicles'} />
|
||||||
<FormField label="Odometer Start">
|
</FormField>
|
||||||
<Field type="number" name="odometer_start" placeholder="Start" />
|
<FormField label="Odometer Start">
|
||||||
</FormField>
|
<Field type="number" name="odometer_start" placeholder="Start" />
|
||||||
<FormField label="Odometer End">
|
</FormField>
|
||||||
<Field type="number" name="odometer_end" placeholder="End" />
|
<FormField label="Odometer End">
|
||||||
</FormField>
|
<Field type="number" name="odometer_end" placeholder="End" />
|
||||||
<FormField label="Job Address">
|
</FormField>
|
||||||
<Field name="job_address" placeholder="Address" />
|
<FormField label="Job Address">
|
||||||
</FormField>
|
<Field name="job_address" placeholder="Address" />
|
||||||
<FormField label="Notes to Admin" hasTextareaHeight>
|
</FormField>
|
||||||
<Field name="notes_to_admin" as="textarea" placeholder="Notes..." />
|
<FormField label="Notes to Admin" hasTextareaHeight>
|
||||||
</FormField>
|
<Field name="notes_to_admin" as="textarea" placeholder="Notes..." />
|
||||||
<BaseDivider />
|
</FormField>
|
||||||
<BaseButtons>
|
|
||||||
<BaseButton type="submit" color="info" label="Submit Work Log" />
|
<BaseDivider />
|
||||||
</BaseButtons>
|
<h3 className="text-lg font-bold mb-4">Chemical Usage (Optional)</h3>
|
||||||
</Form>
|
<FieldArray name="chemical_usages">
|
||||||
|
{({ push, remove }) => (
|
||||||
|
<div>
|
||||||
|
{values.chemical_usages.map((usage, index) => (
|
||||||
|
<div key={index} className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4 items-end border p-4 rounded-lg bg-gray-50 dark:bg-slate-800">
|
||||||
|
<FormField label="Chemical Product" labelFor={`chemical_usages.${index}.chemical_product`}>
|
||||||
|
<Field
|
||||||
|
name={`chemical_usages.${index}.chemical_product`}
|
||||||
|
id={`chemical_usages.${index}.chemical_product`}
|
||||||
|
component={SelectField}
|
||||||
|
itemRef="chemical_products"
|
||||||
|
showField="name"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Quantity Used" labelFor={`chemical_usages.${index}.quantity_used`}>
|
||||||
|
<Field
|
||||||
|
type="number"
|
||||||
|
name={`chemical_usages.${index}.quantity_used`}
|
||||||
|
id={`chemical_usages.${index}.quantity_used`}
|
||||||
|
placeholder="e.g. 5.5"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<div className="mb-4">
|
||||||
|
<BaseButton
|
||||||
|
type="button"
|
||||||
|
color="danger"
|
||||||
|
label="Remove"
|
||||||
|
icon={mdiTrashCan}
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<BaseButton
|
||||||
|
type="button"
|
||||||
|
color="success"
|
||||||
|
label="Add Chemical"
|
||||||
|
icon={mdiPlus}
|
||||||
|
onClick={() => push({ chemical_product: '', quantity_used: '' })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FieldArray>
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
<BaseButtons>
|
||||||
|
<BaseButton type="submit" color="info" label="Submit Work Log" />
|
||||||
|
</BaseButtons>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { mdiChartBar } from '@mdi/js';
|
import { mdiChartBar, mdiCashCheck, mdiHistory } from '@mdi/js';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import React, { ReactElement, useState } from 'react';
|
import React, { ReactElement, useState } from 'react';
|
||||||
import CardBox from '../components/CardBox';
|
import CardBox from '../components/CardBox';
|
||||||
@ -8,26 +8,78 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
|
|||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import FormField from '../components/FormField';
|
import FormField from '../components/FormField';
|
||||||
import BaseButton from '../components/BaseButton';
|
import BaseButton from '../components/BaseButton';
|
||||||
|
import BaseButtons from '../components/BaseButtons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const ReportsPage = () => {
|
const ReportsPage = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState('current'); // 'current' or 'historical'
|
||||||
|
|
||||||
|
// Historical state
|
||||||
const [reportData, setReportData] = useState(null);
|
const [reportData, setReportData] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||||
const [filters, setFilters] = useState({
|
const [filtersHistory, setFiltersHistory] = useState({
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
employeeId: ''
|
employeeId: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchReport = async () => {
|
// Current (Generator) state
|
||||||
setLoading(true);
|
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 {
|
try {
|
||||||
const response = await axios.post('/reports', filters);
|
const response = await axios.post('/reports', filtersHistory);
|
||||||
setReportData(response.data);
|
setReportData(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch report:', error);
|
console.error('Failed to fetch historical report:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,73 +89,172 @@ const ReportsPage = () => {
|
|||||||
<title>{getPageTitle('Payroll Reports')}</title>
|
<title>{getPageTitle('Payroll Reports')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartBar} title='Payroll Reports' main>
|
<SectionTitleLineWithButton icon={mdiChartBar} title='Payroll Dashboard' main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox className="mb-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
<div className="flex mb-6 space-x-4 border-b pb-2">
|
||||||
<FormField label="Start Date">
|
<button
|
||||||
<input type="date" value={filters.startDate} onChange={e => setFilters({...filters, startDate: e.target.value})} className="px-3 py-2 border border-gray-300 rounded w-full" />
|
onClick={() => setActiveTab('current')}
|
||||||
</FormField>
|
className={`px-4 py-2 font-semibold rounded-t-lg transition-colors flex items-center ${
|
||||||
<FormField label="End Date">
|
activeTab === 'current' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
<input type="date" value={filters.endDate} onChange={e => setFilters({...filters, endDate: e.target.value})} className="px-3 py-2 border border-gray-300 rounded w-full" />
|
}`}
|
||||||
</FormField>
|
>
|
||||||
<FormField label="Employee ID">
|
Current Unpaid Logs
|
||||||
<input type="text" value={filters.employeeId} onChange={e => setFilters({...filters, employeeId: e.target.value})} className="px-3 py-2 border border-gray-300 rounded w-full" />
|
</button>
|
||||||
</FormField>
|
<button
|
||||||
<div className="flex items-end">
|
onClick={() => setActiveTab('historical')}
|
||||||
<BaseButton label="Generate Report" color="info" onClick={fetchReport} disabled={loading} className="w-full" />
|
className={`px-4 py-2 font-semibold rounded-t-lg transition-colors flex items-center ${
|
||||||
</div>
|
activeTab === 'historical' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
</div>
|
}`}
|
||||||
</CardBox>
|
>
|
||||||
{reportData && (
|
Historical Reports
|
||||||
<>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === 'current' && (
|
||||||
|
<div>
|
||||||
<CardBox className="mb-6">
|
<CardBox className="mb-6">
|
||||||
<h2 className="text-xl font-bold mb-4">Summary</h2>
|
<div className="mb-4 text-gray-600">
|
||||||
<div className="grid grid-cols-3 gap-4">
|
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 className="p-4 bg-gray-100 rounded">
|
</div>
|
||||||
<p className="text-sm text-gray-500">Total Gross Pay</p>
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<p className="text-2xl font-semibold">${reportData.summary.totalGrossPay.toFixed(2)}</p>
|
<FormField label="Start Date">
|
||||||
</div>
|
<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" />
|
||||||
<div className="p-4 bg-gray-100 rounded">
|
</FormField>
|
||||||
<p className="text-sm text-gray-500">Total Hours</p>
|
<FormField label="End Date">
|
||||||
<p className="text-2xl font-semibold">{reportData.summary.totalHours.toFixed(2)}</p>
|
<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" />
|
||||||
</div>
|
</FormField>
|
||||||
<div className="p-4 bg-gray-100 rounded">
|
<div className="flex items-end">
|
||||||
<p className="text-sm text-gray-500">Total Work Comp</p>
|
<BaseButton label="Preview Current Payroll" color="info" onClick={fetchCurrentPreview} disabled={loadingPreview} className="w-full" />
|
||||||
<p className="text-2xl font-semibold">${reportData.summary.totalWorkersComp.toFixed(2)}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</CardBox>
|
||||||
<CardBox>
|
|
||||||
<h2 className="text-xl font-bold mb-4">Line Items</h2>
|
{previewData && (
|
||||||
<div className="overflow-x-auto">
|
<>
|
||||||
<table className="w-full text-left">
|
<CardBox className="mb-6">
|
||||||
<thead>
|
<h2 className="text-xl font-bold mb-4">Live Preview Summary</h2>
|
||||||
<tr>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<th className="p-2 border-b">Employee</th>
|
<div className="p-4 bg-gray-100 rounded">
|
||||||
<th className="p-2 border-b">Hours</th>
|
<p className="text-sm text-gray-500">Total Unpaid Gross Pay</p>
|
||||||
<th className="p-2 border-b">Gross Pay</th>
|
<p className="text-2xl font-semibold">${previewData.summary.totalGrossPay.toFixed(2)}</p>
|
||||||
<th className="p-2 border-b">Work Comp Amount</th>
|
</div>
|
||||||
<th className="p-2 border-b">Created At</th>
|
<div className="p-4 bg-gray-100 rounded">
|
||||||
</tr>
|
<p className="text-sm text-gray-500">Total Unpaid Hours</p>
|
||||||
</thead>
|
<p className="text-2xl font-semibold">{previewData.summary.totalHours.toFixed(2)}</p>
|
||||||
<tbody>
|
</div>
|
||||||
{reportData.lineItems.map((item: any) => (
|
</div>
|
||||||
<tr key={item.id}>
|
<div className="mt-6">
|
||||||
<td className="p-2 border-b">{item.employee?.firstName} {item.employee?.lastName || ''}</td>
|
<BaseButton label="Run & Finalize Payroll" color="success" onClick={generatePayroll} disabled={generating} />
|
||||||
<td className="p-2 border-b">{item.total_hours}</td>
|
</div>
|
||||||
<td className="p-2 border-b">${item.gross_pay}</td>
|
</CardBox>
|
||||||
<td className="p-2 border-b">${item.workers_comp_amount}</td>
|
|
||||||
<td className="p-2 border-b">{new Date(item.createdAt).toLocaleDateString()}</td>
|
<CardBox>
|
||||||
</tr>
|
<h2 className="text-xl font-bold mb-4">Employee Breakdown</h2>
|
||||||
))}
|
<div className="overflow-x-auto">
|
||||||
</tbody>
|
<table className="w-full text-left">
|
||||||
</table>
|
<thead>
|
||||||
</div>
|
<tr>
|
||||||
</CardBox>
|
<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>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -113,4 +264,4 @@ ReportsPage.getLayout = function getLayout(page: ReactElement) {
|
|||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ReportsPage;
|
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(() => {
|
||||||
|
|||||||
63
patch_dashboard.js
Normal file
63
patch_dashboard.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
let content = fs.readFileSync('frontend/src/pages/dashboard.tsx', 'utf8');
|
||||||
|
|
||||||
|
const replacement = `
|
||||||
|
const isAdmin = hasPermission(currentUser, 'UPDATE_USERS');
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
if (!currentUser) return;
|
||||||
|
|
||||||
|
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','chemical_products','job_logs','job_chemical_usages','payroll_runs','payroll_line_items',];
|
||||||
|
`;
|
||||||
|
|
||||||
|
content = content.replace(/async function loadData\(\)\{\n\s*const entities = \['users','roles',/g, replacement);
|
||||||
|
|
||||||
|
const replacement2 = `
|
||||||
|
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
||||||
|
|
||||||
|
{isAdmin ? (
|
||||||
|
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
||||||
|
`;
|
||||||
|
|
||||||
|
content = content.replace(/\{\!\!rolesWidgets\.length && <hr className='my-6 ' \/>\}\n\s*<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>/g, replacement2);
|
||||||
|
|
||||||
|
const replacement3 = `
|
||||||
|
</Link>}
|
||||||
|
|
||||||
|
|
||||||
|
</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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
content = content.replace(/<\/Link>\}\n\s*<\/div>\n\s*<\/SectionMain>/g, replacement3);
|
||||||
|
|
||||||
|
fs.writeFileSync('frontend/src/pages/dashboard.tsx', content);
|
||||||
|
console.log("Patched dashboard.tsx");
|
||||||
27
patch_menuAside.js
Normal file
27
patch_menuAside.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
let content = fs.readFileSync('frontend/src/menuAside.ts', 'utf8');
|
||||||
|
|
||||||
|
// Replace READ_ with UPDATE_ for specific items
|
||||||
|
const itemsToUpdate = [
|
||||||
|
'READ_CUSTOMERS',
|
||||||
|
'READ_VEHICLES',
|
||||||
|
'READ_WORKERS_COMP_CLASSES',
|
||||||
|
'READ_PAY_TYPES',
|
||||||
|
'READ_EMPLOYEE_PAY_TYPES',
|
||||||
|
'READ_CHEMICAL_PRODUCTS',
|
||||||
|
'READ_JOB_CHEMICAL_USAGES',
|
||||||
|
];
|
||||||
|
|
||||||
|
itemsToUpdate.forEach(perm => {
|
||||||
|
content = content.replace(new RegExp(`permissions: '${perm}'`, 'g'), `permissions: '${perm.replace('READ_', 'UPDATE_')}'`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For All Job Logs, we need to be careful not to replace My Logs which is also READ_JOB_LOGS
|
||||||
|
content = content.replace(/label: 'All Job Logs',\n\s*icon: [^,]+,\n\s*permissions: 'READ_JOB_LOGS'/g, (match) => {
|
||||||
|
return match.replace("permissions: 'READ_JOB_LOGS'", "permissions: 'UPDATE_JOB_LOGS'");
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync('frontend/src/menuAside.ts', content);
|
||||||
|
console.log("Patched menuAside.ts");
|
||||||
|
|
||||||
14
patch_menuAside2.js
Normal file
14
patch_menuAside2.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
let content = fs.readFileSync('frontend/src/menuAside.ts', 'utf8');
|
||||||
|
|
||||||
|
const itemsToUpdate = [
|
||||||
|
'READ_PAYROLL_RUNS',
|
||||||
|
'READ_PAYROLL_LINE_ITEMS',
|
||||||
|
];
|
||||||
|
|
||||||
|
itemsToUpdate.forEach(perm => {
|
||||||
|
content = content.replace(new RegExp(`permissions: '${perm}'`, 'g'), `permissions: '${perm.replace('READ_', 'UPDATE_')}'`);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync('frontend/src/menuAside.ts', content);
|
||||||
|
console.log("Patched payroll in menuAside.ts");
|
||||||
Loading…
x
Reference in New Issue
Block a user