diff --git a/frontend/src/components/AsideMenu.tsx b/frontend/src/components/AsideMenu.tsx index 442dfac..11e58a3 100644 --- a/frontend/src/components/AsideMenu.tsx +++ b/frontend/src/components/AsideMenu.tsx @@ -19,7 +19,7 @@ export default function AsideMenu({ <> { )} {item.label} @@ -63,7 +64,7 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { ) const componentClass = [ - 'flex cursor-pointer py-1.5 ', + 'flex items-start cursor-pointer py-1.5 min-h-[3rem]', isDropdownList ? 'px-6 text-sm' : '', item.color ? getButtonColor(item.color, false, true) @@ -77,12 +78,12 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
  • {item.withDevider &&
    } {item.href && ( - + {asideMenuItemInnerContents} )} {!item.href && ( -
    setIsDropdownActive(!isDropdownActive)}> +
    setIsDropdownActive(!isDropdownActive)} title={item.label}> {asideMenuItemInnerContents}
    )} diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx index 9356368..a11aebe 100644 --- a/frontend/src/components/AsideMenuLayer.tsx +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -29,7 +29,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props return (
  • - ))} + ); + })} {!loading && job_logs.length === 0 && (

    No data to display

    diff --git a/frontend/src/components/Job_logs/ListJob_logs.tsx b/frontend/src/components/Job_logs/ListJob_logs.tsx index 2152628..184185d 100644 --- a/frontend/src/components/Job_logs/ListJob_logs.tsx +++ b/frontend/src/components/Job_logs/ListJob_logs.tsx @@ -10,6 +10,7 @@ import LoadingSpinner from "../LoadingSpinner"; import Link from 'next/link'; import {hasPermission} from "../../helpers/userPermissions"; +import { calculateJobLogPayPreview, formatCurrency } from '../../helpers/jobLogPayPreview'; type Props = { @@ -34,7 +35,15 @@ const ListJob_logs = ({ job_logs, loading, onDelete, currentPage, numPages, onPa <>
    {loading && } - {!loading && job_logs.map((item) => ( + {!loading && job_logs.map((item) => { + const payPreview = calculateJobLogPayPreview({ + payType: item.pay_type, + workersCompClass: item.workersCompClass, + hoursConducted: item.hours_conducted, + clientPaid: item.client_paid, + }); + + return (
    @@ -103,6 +112,14 @@ const ListJob_logs = ({ job_logs, loading, onDelete, currentPage, numPages, onPa +
    +

    EmployeePay

    +

    { formatCurrency(payPreview.estimatedPay) }

    +
    + + + +

    Vehicle

    { dataFormatter.vehiclesOneListFormatter(item.vehicle) }

    @@ -163,7 +180,8 @@ const ListJob_logs = ({ job_logs, loading, onDelete, currentPage, numPages, onPa
    - ))} + ); + })} {!loading && job_logs.length === 0 && (

    No data to display

    diff --git a/frontend/src/components/Job_logs/configureJob_logsCols.tsx b/frontend/src/components/Job_logs/configureJob_logsCols.tsx index 442789b..3585223 100644 --- a/frontend/src/components/Job_logs/configureJob_logsCols.tsx +++ b/frontend/src/components/Job_logs/configureJob_logsCols.tsx @@ -14,6 +14,7 @@ import DataGridMultiSelect from "../DataGridMultiSelect"; import ListActionsPopover from '../ListActionsPopover'; import {hasPermission} from "../../helpers/userPermissions"; +import { calculateJobLogPayPreview, formatCurrency } from '../../helpers/jobLogPayPreview'; type Params = (id: string) => void; @@ -172,6 +173,29 @@ export const loadColumns = async ( }, + { + field: 'employeePay', + headerName: 'EmployeePay', + flex: 1, + minWidth: 140, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: false, + sortable: false, + type: 'number', + valueGetter: (params: GridValueGetterParams) => + calculateJobLogPayPreview({ + payType: params.row?.pay_type, + workersCompClass: params.row?.workersCompClass, + hoursConducted: params.row?.hours_conducted, + clientPaid: params.row?.client_paid, + }).estimatedPay, + valueFormatter: ({ value }) => formatCurrency(value), + + }, + { field: 'vehicle', headerName: 'Vehicle', diff --git a/frontend/src/helpers/jobLogPayPreview.ts b/frontend/src/helpers/jobLogPayPreview.ts new file mode 100644 index 0000000..8e78207 --- /dev/null +++ b/frontend/src/helpers/jobLogPayPreview.ts @@ -0,0 +1,104 @@ +const currencyFormatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, +}); + +const numberFormatter = new Intl.NumberFormat('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 2, +}); + +export const coerceNumber = (value: unknown) => { + if (value === null || value === undefined || value === '') { + return 0; + } + + const parsed = Number(value); + + return Number.isFinite(parsed) ? parsed : 0; +}; + +export const formatCurrency = (value: unknown) => currencyFormatter.format(coerceNumber(value)); + +export const formatDecimal = (value: unknown) => numberFormatter.format(coerceNumber(value)); + +export const resolveEntityId = (value: any) => { + if (!value) { + return null; + } + + if (typeof value === 'string') { + return value; + } + + if (typeof value === 'object' && value.id) { + return value.id; + } + + return null; +}; + +export const hasPayTypeDetails = (payType: any) => { + if (!payType || typeof payType !== 'object') { + return false; + } + + return Boolean(payType.pay_method || payType.hourly_rate || payType.commission_rate); +}; + +export const hasWorkersCompDetails = (workersCompClass: any) => { + if (!workersCompClass || typeof workersCompClass !== 'object') { + return false; + } + + return workersCompClass.percentage !== undefined && workersCompClass.percentage !== null; +}; + +export const calculateJobLogPayPreview = ({ + payType, + workersCompClass, + hoursConducted, + clientPaid, +}: { + payType?: any; + workersCompClass?: any; + hoursConducted?: unknown; + clientPaid?: unknown; +}) => { + const payMethod = payType?.pay_method || null; + const hours = coerceNumber(hoursConducted); + const clientAmount = coerceNumber(clientPaid); + const hourlyRate = coerceNumber(payType?.hourly_rate); + const commissionRate = coerceNumber(payType?.commission_rate); + const workersCompPercentage = coerceNumber(workersCompClass?.percentage); + + let estimatedPay = 0; + let formulaLabel = 'Select a pay type to preview earnings.'; + + if (payMethod === 'hourly') { + estimatedPay = hours * hourlyRate; + formulaLabel = `${formatDecimal(hours)} hours × ${formatCurrency(hourlyRate)}/hr`; + } else if (payMethod === 'commission') { + estimatedPay = clientAmount * (commissionRate / 100); + formulaLabel = `${formatCurrency(clientAmount)} client paid × ${formatDecimal(commissionRate)}%`; + } + + const workersCompAmount = estimatedPay * (workersCompPercentage / 100); + + return { + payMethod, + payTypeName: payType?.name || 'Not selected', + hours, + clientAmount, + hourlyRate, + commissionRate, + estimatedPay, + formulaLabel, + workersCompName: workersCompClass?.name || null, + workersCompPercentage, + workersCompAmount, + hasEstimate: payMethod === 'hourly' || payMethod === 'commission', + }; +}; diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index ecc1243..0db2c3a 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -85,18 +85,18 @@ export default function LayoutAuthenticated({ }, [router.events, dispatch]) - const layoutAsidePadding = 'xl:pl-60' + const layoutAsidePadding = 'xl:pl-72' return (
    { + return ( + <> + + {getPageTitle('Admin Help')} + + + + + {''} + + + +
    +
    +

    Admin task checklist

    +

    + This page is a simple internal knowledge base for the most common admin workflows in the app. + Use the quick links below to jump to a process, then open the related page directly when you are ready to do the task. +

    +
    + +
    + + + +
    + {helpSections.map((section) => ( + + ))} +
    +
    + +
    + {helpSections.map((section) => ( + +
    +
    +
    + +
    +

    {section.title}

    +

    + {section.summary} +

    +
    +
    + {section.href && section.hrefLabel ? ( + + ) : null} +
    + +
    +
    + Checklist +
    +
      + {section.steps.map((step) => ( +
    1. {step}
    2. + ))} +
    +
    + + {section.checks?.length ? ( +
    +
    + Verify before moving on +
    +
      + {section.checks.map((check) => ( +
    • {check}
    • + ))} +
    +
    + ) : null} + + {section.warning ? ( +
    + Admin note: {section.warning} +
    + ) : null} +
    +
    + ))} +
    + + +
    +
    +

    Suggested next improvement

    +

    + If you want, this help page can later become searchable or editable so admins can maintain their own internal SOPs. +

    +
    + +
    +
    +
    + + ); +}; + +AdminHelpPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default AdminHelpPage; diff --git a/frontend/src/pages/job_logs/job_logs-edit.tsx b/frontend/src/pages/job_logs/job_logs-edit.tsx index b358247..44c7dd1 100644 --- a/frontend/src/pages/job_logs/job_logs-edit.tsx +++ b/frontend/src/pages/job_logs/job_logs-edit.tsx @@ -9,6 +9,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import JobLogPayPreview from '../../components/JobLogPayPreview' import { getPageTitle } from '../../config' import { Field, Form, Formik } from 'formik' @@ -796,6 +797,8 @@ const EditJob_logsPage = () => { > + + diff --git a/frontend/src/pages/job_logs/job_logs-new.tsx b/frontend/src/pages/job_logs/job_logs-new.tsx index a19111b..a60713c 100644 --- a/frontend/src/pages/job_logs/job_logs-new.tsx +++ b/frontend/src/pages/job_logs/job_logs-new.tsx @@ -5,6 +5,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' +import JobLogPayPreview from '../../components/JobLogPayPreview' import { getPageTitle } from '../../config' import { Field, Form, Formik } from 'formik' @@ -495,6 +496,8 @@ const Job_logsNew = () => { + + diff --git a/frontend/src/pages/log-work.tsx b/frontend/src/pages/log-work.tsx index 071d0a2..1c43ad8 100644 --- a/frontend/src/pages/log-work.tsx +++ b/frontend/src/pages/log-work.tsx @@ -5,6 +5,7 @@ import CardBox from '../components/CardBox'; import LayoutAuthenticated from '../layouts/Authenticated'; import SectionMain from '../components/SectionMain'; import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import JobLogPayPreview from '../components/JobLogPayPreview'; import { getPageTitle } from '../config'; import { Field, Form, Formik, FieldArray } from 'formik'; @@ -98,6 +99,7 @@ const LogWorkPage = () => { ))} +