diff --git a/assets/pasted-20260314-132921-6765892c.webp b/assets/pasted-20260314-132921-6765892c.webp
new file mode 100644
index 0000000..bf459c8
Binary files /dev/null and b/assets/pasted-20260314-132921-6765892c.webp differ
diff --git a/backend/src/db/api/employee_pay_types.js b/backend/src/db/api/employee_pay_types.js
index ad0e660..9a63562 100644
--- a/backend/src/db/api/employee_pay_types.js
+++ b/backend/src/db/api/employee_pay_types.js
@@ -279,7 +279,7 @@ module.exports = class Employee_pay_typesDBApi {
}
},
]
- } : {},
+ } : undefined,
},
@@ -296,7 +296,7 @@ module.exports = class Employee_pay_typesDBApi {
}
},
]
- } : {},
+ } : undefined,
},
diff --git a/backend/src/db/api/job_chemical_usages.js b/backend/src/db/api/job_chemical_usages.js
index 15ab2b6..fbf1229 100644
--- a/backend/src/db/api/job_chemical_usages.js
+++ b/backend/src/db/api/job_chemical_usages.js
@@ -264,7 +264,7 @@ module.exports = class Job_chemical_usagesDBApi {
}
},
]
- } : {},
+ } : undefined,
},
@@ -281,7 +281,7 @@ module.exports = class Job_chemical_usagesDBApi {
}
},
]
- } : {},
+ } : undefined,
},
diff --git a/backend/src/db/api/job_logs.js b/backend/src/db/api/job_logs.js
index c781703..fbc095f 100644
--- a/backend/src/db/api/job_logs.js
+++ b/backend/src/db/api/job_logs.js
@@ -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,
},
diff --git a/backend/src/db/api/payroll_line_items.js b/backend/src/db/api/payroll_line_items.js
index 826e0a2..e580f29 100644
--- a/backend/src/db/api/payroll_line_items.js
+++ b/backend/src/db/api/payroll_line_items.js
@@ -315,7 +315,7 @@ module.exports = class Payroll_line_itemsDBApi {
}
},
]
- } : {},
+ } : undefined,
},
@@ -332,7 +332,7 @@ module.exports = class Payroll_line_itemsDBApi {
}
},
]
- } : {},
+ } : undefined,
},
diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js
index 78ffae8..251dcd9 100644
--- a/backend/src/db/api/users.js
+++ b/backend/src/db/api/users.js
@@ -484,7 +484,7 @@ module.exports = class UsersDBApi {
}
},
]
- } : {},
+ } : undefined,
},
diff --git a/backend/src/index.js b/backend/src/index.js
index 72694a9..3613690 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -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/reports', passport.authenticate('jwt', {session: false}), reportsRoutes);
+app.use('/api/payroll_generator', require('./routes/payroll_generator'));
app.use(
'/api/openai',
diff --git a/backend/src/routes/payroll_generator.js b/backend/src/routes/payroll_generator.js
new file mode 100644
index 0000000..4adea9d
--- /dev/null
+++ b/backend/src/routes/payroll_generator.js
@@ -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;
diff --git a/backend/src/services/job_logs.js b/backend/src/services/job_logs.js
index 96d6e8d..f21fc42 100644
--- a/backend/src/services/job_logs.js
+++ b/backend/src/services/job_logs.js
@@ -23,7 +23,7 @@ module.exports = class Job_logsService {
customerId = customer.id;
}
- await Job_logsDBApi.create(
+ const createdJob = await Job_logsDBApi.create(
{ ...data, customer: customerId },
{
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();
} catch (error) {
await transaction.rollback();
diff --git a/frontend/public/assets/logo.webp b/frontend/public/assets/logo.webp
new file mode 100644
index 0000000..bf459c8
Binary files /dev/null and b/frontend/public/assets/logo.webp differ
diff --git a/frontend/public/assets/vm-shot-2026-03-14T13-28-33-023Z.jpg b/frontend/public/assets/vm-shot-2026-03-14T13-28-33-023Z.jpg
new file mode 100644
index 0000000..deb8ea0
Binary files /dev/null and b/frontend/public/assets/vm-shot-2026-03-14T13-28-33-023Z.jpg differ
diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx
index 4da4312..9356368 100644
--- a/frontend/src/components/AsideMenuLayer.tsx
+++ b/frontend/src/components/AsideMenuLayer.tsx
@@ -39,7 +39,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
>
-
Pressure Wash Payroll
+
Major League Pressure Washing
diff --git a/frontend/src/components/FooterBar.tsx b/frontend/src/components/FooterBar.tsx
index 0acc9c5..4b6b84f 100644
--- a/frontend/src/components/FooterBar.tsx
+++ b/frontend/src/components/FooterBar.tsx
@@ -15,8 +15,8 @@ export default function FooterBar({ children }: Props) {
diff --git a/frontend/src/components/Logo/index.tsx b/frontend/src/components/Logo/index.tsx
index a582e29..5ce524c 100644
--- a/frontend/src/components/Logo/index.tsx
+++ b/frontend/src/components/Logo/index.tsx
@@ -7,9 +7,9 @@ type Props = {
export default function Logo({ className = '' }: Props) {
return (
+ alt={'Major League Pressure Washing logo'}>
)
}
diff --git a/frontend/src/config.ts b/frontend/src/config.ts
index a9783c8..1d12419 100644
--- a/frontend/src/config.ts
+++ b/frontend/src/config.ts
@@ -8,7 +8,7 @@ export const localStorageStyleKey = 'style'
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}`
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index cb342e4..70fd70c 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -6,6 +6,7 @@ const menuAside: MenuAsideItem[] = [
href: '/dashboard',
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
+ permissions: 'UPDATE_USERS',
},
{
@@ -27,7 +28,7 @@ const menuAside: MenuAsideItem[] = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiAccountGroup ?? icon.mdiTable,
- permissions: 'READ_USERS'
+ permissions: 'UPDATE_USERS'
},
{
href: '/roles/roles-list',
@@ -35,7 +36,7 @@ const menuAside: MenuAsideItem[] = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
- permissions: 'READ_ROLES'
+ permissions: 'UPDATE_USERS'
},
{
href: '/permissions/permissions-list',
@@ -43,7 +44,7 @@ const menuAside: MenuAsideItem[] = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
- permissions: 'READ_PERMISSIONS'
+ permissions: 'UPDATE_USERS'
},
{
href: '/customers/customers-list',
@@ -51,7 +52,7 @@ const menuAside: MenuAsideItem[] = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiAccountMultiple' in icon ? icon['mdiAccountMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_CUSTOMERS'
+ permissions: 'UPDATE_USERS'
},
{
href: '/vehicles/vehicles-list',
@@ -59,13 +60,13 @@ const menuAside: MenuAsideItem[] = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiTruck' in icon ? icon['mdiTruck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_VEHICLES'
+ permissions: 'UPDATE_USERS'
},
{
href: '/workers_comp_classes/workers_comp_classes-list',
label: 'Workmans Comp',
icon: icon.mdiTable,
- permissions: 'READ_WORKERS_COMP_CLASSES'
+ permissions: 'UPDATE_USERS'
},
{
href: '/pay_types/pay_types-list',
@@ -73,7 +74,7 @@ const menuAside: MenuAsideItem[] = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
@@ -81,7 +82,7 @@ const menuAside: MenuAsideItem[] = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
@@ -89,13 +90,13 @@ const menuAside: MenuAsideItem[] = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFlask' in icon ? icon['mdiFlask' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_CHEMICAL_PRODUCTS'
+ permissions: 'UPDATE_USERS'
},
{
href: '/job_logs/job_logs-list',
label: 'All Job Logs',
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',
@@ -103,7 +104,7 @@ const menuAside: MenuAsideItem[] = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFlaskOutline' in icon ? icon['mdiFlaskOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_JOB_CHEMICAL_USAGES'
+ permissions: 'UPDATE_USERS'
},
{
href: '/payroll_runs/payroll_runs-list',
@@ -111,7 +112,7 @@ const menuAside: MenuAsideItem[] = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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',
@@ -119,13 +120,13 @@ const menuAside: MenuAsideItem[] = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: 'mdiFileDocumentOutline' in icon ? icon['mdiFileDocumentOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_PAYROLL_LINE_ITEMS'
+ permissions: 'UPDATE_USERS'
},
{
href: '/reports',
- label: 'Payroll Reports',
+ label: 'Payroll Dashboard',
icon: icon.mdiChartBar,
- permissions: 'READ_PAYROLL_LINE_ITEMS'
+ permissions: 'UPDATE_USERS'
},
{
href: '/profile',
@@ -139,7 +140,7 @@ const menuAside: MenuAsideItem[] = [
target: '_blank',
label: 'Swagger API',
icon: icon.mdiFileCode,
- permissions: 'READ_API_DOCS'
+ permissions: 'UPDATE_USERS'
},
]
diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx
index 82e11aa..f46d508 100644
--- a/frontend/src/pages/_app.tsx
+++ b/frontend/src/pages/_app.tsx
@@ -149,7 +149,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
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 url = "https://flatlogic.com/"
const image = "https://project-screens.s3.amazonaws.com/screenshots/39157/app-hero-20260312-154649.png"
diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx
index 263c7b7..dc96fdb 100644
--- a/frontend/src/pages/dashboard.tsx
+++ b/frontend/src/pages/dashboard.tsx
@@ -46,9 +46,18 @@ const Dashboard = () => {
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
+ 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',];
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 = () => {
))}
+
{!!rolesWidgets.length && }
+ {isAdmin ? (
+
{hasPermission(currentUser, 'READ_USERS') &&
@@ -481,11 +493,34 @@ const Dashboard = () => {
+
}
+ ) : (
+
+
+
+
+
+
+ My Logs
+
+
+ {job_logs}
+
+
+
+
+
+
+
+
+
+ )}
+
>
)
}
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index b48f0aa..aeaa8d5 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -26,7 +26,7 @@ export default function Starter() {
const [contentPosition, setContentPosition] = useState('left');
const textColor = useAppSelector((state) => state.style.linkColor);
- const title = 'Pressure Wash Payroll'
+ const title = 'Major League Pressure Washing'
// Fetch Pexels image/video
useEffect(() => {
@@ -128,7 +128,7 @@ export default function Starter() {
: null}
-
+
This is a React.js/Node.js app generated by the Flatlogic Web App Generator
diff --git a/frontend/src/pages/log-work.tsx b/frontend/src/pages/log-work.tsx
index 7f26eb9..071d0a2 100644
--- a/frontend/src/pages/log-work.tsx
+++ b/frontend/src/pages/log-work.tsx
@@ -1,4 +1,4 @@
-import { mdiPencil, mdiChartTimelineVariant } from '@mdi/js';
+import { mdiPencil, mdiPlus, mdiTrashCan } from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
import CardBox from '../components/CardBox';
@@ -7,7 +7,7 @@ import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
-import { Field, Form, Formik } from 'formik';
+import { Field, Form, Formik, FieldArray } from 'formik';
import FormField from '../components/FormField';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
@@ -52,6 +52,7 @@ const LogWorkPage = () => {
job_address: '',
status: 'submitted',
notes_to_admin: '',
+ chemical_usages: [],
};
const handleSubmit = async (data: any) => {
@@ -70,52 +71,102 @@ const LogWorkPage = () => {
handleSubmit(values)}>
-
+ {({ values }) => (
+
+ )}
diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx
index 2680bf2..edd7b1e 100644
--- a/frontend/src/pages/login.tsx
+++ b/frontend/src/pages/login.tsx
@@ -44,7 +44,7 @@ export default function Login() {
password: '2fa72adf',
remember: true })
- const title = 'Pressure Wash Payroll'
+ const title = 'Major League Pressure Washing'
// Fetch Pexels image/video
useEffect( () => {
@@ -165,7 +165,7 @@ export default function Login() {
- {title}
+ {title}
diff --git a/frontend/src/pages/privacy-policy.tsx b/frontend/src/pages/privacy-policy.tsx
index 772ad82..7c3a76a 100644
--- a/frontend/src/pages/privacy-policy.tsx
+++ b/frontend/src/pages/privacy-policy.tsx
@@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
export default function PrivacyPolicy() {
- const title = 'Pressure Wash Payroll'
+ const title = 'Major League Pressure Washing'
const [projectUrl, setProjectUrl] = useState('');
useEffect(() => {
diff --git a/frontend/src/pages/reports.tsx b/frontend/src/pages/reports.tsx
index 1810cc9..7034009 100644
--- a/frontend/src/pages/reports.tsx
+++ b/frontend/src/pages/reports.tsx
@@ -1,4 +1,4 @@
-import { mdiChartBar } from '@mdi/js';
+import { mdiChartBar, mdiCashCheck, mdiHistory } from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useState } from 'react';
import CardBox from '../components/CardBox';
@@ -8,26 +8,78 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
import { getPageTitle } from '../config';
import FormField from '../components/FormField';
import BaseButton from '../components/BaseButton';
+import BaseButtons from '../components/BaseButtons';
import axios from 'axios';
const ReportsPage = () => {
+ const [activeTab, setActiveTab] = useState('current'); // 'current' or 'historical'
+
+ // Historical state
const [reportData, setReportData] = useState(null);
- const [loading, setLoading] = useState(false);
- const [filters, setFilters] = useState({
+ const [loadingHistory, setLoadingHistory] = useState(false);
+ const [filtersHistory, setFiltersHistory] = useState({
startDate: '',
endDate: '',
employeeId: ''
});
- const fetchReport = async () => {
- setLoading(true);
+ // Current (Generator) state
+ const [previewData, setPreviewData] = useState(null);
+ const [loadingPreview, setLoadingPreview] = useState(false);
+ const [generating, setGenerating] = useState(false);
+ const [filtersCurrent, setFiltersCurrent] = useState({
+ startDate: '',
+ endDate: ''
+ });
+ const [runSuccess, setRunSuccess] = useState('');
+ const [runError, setRunError] = useState('');
+
+ const fetchHistoricalReport = async () => {
+ setLoadingHistory(true);
try {
- const response = await axios.post('/reports', filters);
+ const response = await axios.post('/reports', filtersHistory);
setReportData(response.data);
} catch (error) {
- console.error('Failed to fetch report:', error);
+ console.error('Failed to fetch historical report:', error);
} 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 = () => {
{getPageTitle('Payroll Reports')}
-
+
{''}
-
-
-
- {reportData && (
- <>
+
+
+ setActiveTab('current')}
+ className={`px-4 py-2 font-semibold rounded-t-lg transition-colors flex items-center ${
+ activeTab === 'current' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
+ }`}
+ >
+ Current Unpaid Logs
+
+ setActiveTab('historical')}
+ className={`px-4 py-2 font-semibold rounded-t-lg transition-colors flex items-center ${
+ activeTab === 'historical' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
+ }`}
+ >
+ Historical Reports
+
+
+
+ {activeTab === 'current' && (
+
- Summary
-
-
-
Total Gross Pay
-
${reportData.summary.totalGrossPay.toFixed(2)}
-
-
-
Total Hours
-
{reportData.summary.totalHours.toFixed(2)}
-
-
-
Total Work Comp
-
${reportData.summary.totalWorkersComp.toFixed(2)}
+
+ Select a date range to view all unpaid Job Logs. From here, you can preview what your employees have currently earned and generate a finalized Payroll Run.
+
+
+ {runError &&
{runError}
}
+ {runSuccess &&
{runSuccess}
}
-
- Line Items
-
-
-
-
- Employee
- Hours
- Gross Pay
- Work Comp Amount
- Created At
-
-
-
- {reportData.lineItems.map((item: any) => (
-
- {item.employee?.firstName} {item.employee?.lastName || ''}
- {item.total_hours}
- ${item.gross_pay}
- ${item.workers_comp_amount}
- {new Date(item.createdAt).toLocaleDateString()}
-
- ))}
-
-
-
-
- >
+
+ {previewData && (
+ <>
+
+ Live Preview Summary
+
+
+
Total Unpaid Gross Pay
+
${previewData.summary.totalGrossPay.toFixed(2)}
+
+
+
Total Unpaid Hours
+
{previewData.summary.totalHours.toFixed(2)}
+
+
+
+
+
+
+
+
+ Employee Breakdown
+
+
+
+
+ Employee
+ Total Hours
+ Commission Base
+ Calculated Gross Pay
+
+
+
+ {previewData.lineItems.map((item: any, idx: number) => (
+
+ {item.employee?.firstName} {item.employee?.lastName || ''}
+ {item.total_hours.toFixed(2)}
+ ${item.total_commission_base.toFixed(2)}
+ ${item.gross_pay.toFixed(2)}
+
+ ))}
+
+
+
+
+ >
+ )}
+
)}
+
+ {activeTab === 'historical' && (
+
+
+
+ View previously generated payroll line items by date range or employee ID.
+
+
+
+
+ {reportData && (
+ <>
+
+ Historical Summary
+
+
+
Total Gross Pay
+
${reportData.summary.totalGrossPay.toFixed(2)}
+
+
+
Total Hours
+
{reportData.summary.totalHours.toFixed(2)}
+
+
+
Total Work Comp
+
${reportData.summary.totalWorkersComp.toFixed(2)}
+
+
+
+
+ Line Items
+
+
+
+
+ Employee
+ Hours
+ Gross Pay
+ Work Comp Amount
+ Created At
+
+
+
+ {reportData.lineItems.map((item: any) => (
+
+ {item.employee?.firstName} {item.employee?.lastName || ''}
+ {item.total_hours}
+ ${item.gross_pay}
+ ${item.workers_comp_amount}
+ {new Date(item.createdAt).toLocaleDateString()}
+
+ ))}
+
+
+
+
+ >
+ )}
+
+ )}
+
>
);
@@ -113,4 +264,4 @@ ReportsPage.getLayout = function getLayout(page: ReactElement) {
return
{page} ;
};
-export default ReportsPage;
+export default ReportsPage;
\ No newline at end of file
diff --git a/frontend/src/pages/terms-of-use.tsx b/frontend/src/pages/terms-of-use.tsx
index f40ca44..c28b4da 100644
--- a/frontend/src/pages/terms-of-use.tsx
+++ b/frontend/src/pages/terms-of-use.tsx
@@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
export default function PrivacyPolicy() {
- const title = 'Pressure Wash Payroll';
+ const title = 'Major League Pressure Washing';
const [projectUrl, setProjectUrl] = useState('');
useEffect(() => {
diff --git a/patch_dashboard.js b/patch_dashboard.js
new file mode 100644
index 0000000..3b06dcc
--- /dev/null
+++ b/patch_dashboard.js
@@ -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 &&
}
+
+ {isAdmin ? (
+
+`;
+
+content = content.replace(/\{\!\!rolesWidgets\.length &&
\}\n\s*
/g, replacement2);
+
+const replacement3 = `
+ }
+
+
+
+ ) : (
+
+
+
+
+
+
+ My Logs
+
+
+ {job_logs}
+
+
+
+
+
+
+
+
+
+ )}
+
+`;
+
+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");
\ No newline at end of file
diff --git a/patch_menuAside.js b/patch_menuAside.js
new file mode 100644
index 0000000..1b00082
--- /dev/null
+++ b/patch_menuAside.js
@@ -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");
+
diff --git a/patch_menuAside2.js b/patch_menuAside2.js
new file mode 100644
index 0000000..dc63f93
--- /dev/null
+++ b/patch_menuAside2.js
@@ -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");