Autosave: 20260325-195455

This commit is contained in:
Flatlogic Bot 2026-03-25 19:54:56 +00:00
parent 3fa7195d98
commit 6b8c666c65
27 changed files with 717 additions and 160 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -279,7 +279,7 @@ module.exports = class Employee_pay_typesDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },
@ -296,7 +296,7 @@ module.exports = class Employee_pay_typesDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },

View File

@ -264,7 +264,7 @@ module.exports = class Job_chemical_usagesDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },
@ -281,7 +281,7 @@ module.exports = class Job_chemical_usagesDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },

View File

@ -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,
}, },

View File

@ -315,7 +315,7 @@ module.exports = class Payroll_line_itemsDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },
@ -332,7 +332,7 @@ module.exports = class Payroll_line_itemsDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },

View File

@ -484,7 +484,7 @@ module.exports = class UsersDBApi {
} }
}, },
] ]
} : {}, } : undefined,
}, },

View File

@ -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',

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View File

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

View File

@ -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>
&copy;{year},{` `} &copy;{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>

View File

@ -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>
) )
} }

View File

@ -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}`

View File

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

View File

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

View File

@ -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>
</> </>
) )
} }

View File

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

View File

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

View File

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

View File

@ -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(() => {

View File

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

View File

@ -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
View 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
View 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
View 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");