Autosave: 20260413-025244

This commit is contained in:
Flatlogic Bot 2026-04-13 02:52:45 +00:00
parent 6b8c666c65
commit c9732c5db8
14 changed files with 696 additions and 13 deletions

View File

@ -19,7 +19,7 @@ export default function AsideMenu({
<>
<AsideMenuLayer
menu={props.menu}
className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${
className={`${isAsideMobileExpanded ? 'left-0' : '-left-72 lg:left-0'} ${
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
}`}
onAsideLgCloseClick={props.onAsideLgClose}

View File

@ -46,9 +46,10 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
<BaseIcon path={item.icon} className={`flex-none mx-3 ${activeClassAddon}`} size="18" />
)}
<span
className={`grow text-ellipsis line-clamp-1 ${
className={`min-w-0 grow break-words whitespace-normal leading-5 line-clamp-2 ${
item.menu ? '' : 'pr-12'
} ${activeClassAddon}`}
title={item.label}
>
{item.label}
</span>
@ -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) => {
<li className={'px-3 py-1.5'}>
{item.withDevider && <hr className={`${borders} mb-3`} />}
{item.href && (
<Link href={item.href} target={item.target} className={componentClass}>
<Link href={item.href} target={item.target} className={componentClass} title={item.label}>
{asideMenuItemInnerContents}
</Link>
)}
{!item.href && (
<div className={componentClass} onClick={() => setIsDropdownActive(!isDropdownActive)}>
<div className={componentClass} onClick={() => setIsDropdownActive(!isDropdownActive)} title={item.label}>
{asideMenuItemInnerContents}
</div>
)}

View File

@ -29,7 +29,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
return (
<aside
id='asideMenu'
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
className={`${className} zzz lg:py-2 lg:pl-2 w-72 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
>
<div
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}

View File

@ -0,0 +1,196 @@
import React, { useEffect, useMemo, useState } from 'react';
import axios from 'axios';
import { useFormikContext } from 'formik';
import CardBox from './CardBox';
import {
calculateJobLogPayPreview,
formatCurrency,
formatDecimal,
hasPayTypeDetails,
hasWorkersCompDetails,
resolveEntityId,
} from '../helpers/jobLogPayPreview';
type Props = {
payTypeOptions?: any[];
};
const findById = (items: any[] = [], id: string | null) => items.find((item) => item?.id === id) || null;
export default function JobLogPayPreview({ payTypeOptions = [] }: Props) {
const { values } = useFormikContext<any>();
const [payTypeDetails, setPayTypeDetails] = useState<any>(null);
const [workersCompClassDetails, setWorkersCompClassDetails] = useState<any>(null);
const selectedPayTypeId = resolveEntityId(values?.pay_type);
const selectedWorkersCompClassId = resolveEntityId(values?.workersCompClass);
const selectedPayTypeFromOptions = useMemo(
() => findById(payTypeOptions, selectedPayTypeId),
[payTypeOptions, selectedPayTypeId],
);
useEffect(() => {
let active = true;
if (hasPayTypeDetails(values?.pay_type)) {
setPayTypeDetails(values.pay_type);
return () => {
active = false;
};
}
if (selectedPayTypeFromOptions) {
setPayTypeDetails(selectedPayTypeFromOptions);
return () => {
active = false;
};
}
if (!selectedPayTypeId) {
setPayTypeDetails(null);
return () => {
active = false;
};
}
axios
.get(`/pay_types/${selectedPayTypeId}`)
.then(({ data }) => {
if (active) {
setPayTypeDetails(data);
}
})
.catch((error) => {
console.error('Failed to fetch pay type for job log preview:', error);
if (active) {
setPayTypeDetails(null);
}
});
return () => {
active = false;
};
}, [selectedPayTypeFromOptions, selectedPayTypeId, values?.pay_type]);
useEffect(() => {
let active = true;
if (hasWorkersCompDetails(values?.workersCompClass)) {
setWorkersCompClassDetails(values.workersCompClass);
return () => {
active = false;
};
}
if (!selectedWorkersCompClassId) {
setWorkersCompClassDetails(null);
return () => {
active = false;
};
}
axios
.get(`/workers_comp_classes/${selectedWorkersCompClassId}`)
.then(({ data }) => {
if (active) {
setWorkersCompClassDetails(data);
}
})
.catch((error) => {
console.error('Failed to fetch workers comp class for job log preview:', error);
if (active) {
setWorkersCompClassDetails(null);
}
});
return () => {
active = false;
};
}, [selectedWorkersCompClassId, values?.workersCompClass]);
const preview = useMemo(
() =>
calculateJobLogPayPreview({
payType: payTypeDetails,
workersCompClass: workersCompClassDetails,
hoursConducted: values?.hours_conducted,
clientPaid: values?.client_paid,
}),
[payTypeDetails, workersCompClassDetails, values?.client_paid, values?.hours_conducted],
);
return (
<CardBox className="mb-6 border border-blue-100 bg-blue-50/70 dark:border-dark-700 dark:bg-dark-800">
<div className="space-y-4">
<div className="flex flex-col gap-1 md:flex-row md:items-end md:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
Earnings Preview
</p>
<h3 className="text-lg font-bold">Live estimate for this job</h3>
</div>
<div className="text-left md:text-right">
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Estimated Job Pay</p>
<p className="text-2xl font-bold text-green-700 dark:text-green-400">
{formatCurrency(preview.estimatedPay)}
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="rounded-lg border border-white/80 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Pay Type</p>
<p className="font-semibold capitalize">{preview.payTypeName}</p>
</div>
<div className="rounded-lg border border-white/80 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Pay Method</p>
<p className="font-semibold capitalize">{preview.payMethod || 'Not selected'}</p>
</div>
<div className="rounded-lg border border-white/80 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">
{preview.payMethod === 'commission' ? 'Client Paid' : 'Hours'}
</p>
<p className="font-semibold">
{preview.payMethod === 'commission'
? formatCurrency(preview.clientAmount)
: formatDecimal(preview.hours)}
</p>
</div>
<div className="rounded-lg border border-white/80 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Rate</p>
<p className="font-semibold">
{preview.payMethod === 'commission'
? `${formatDecimal(preview.commissionRate)}%`
: formatCurrency(preview.hourlyRate)}
</p>
</div>
</div>
<div className="rounded-lg border border-dashed border-blue-200 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Calculation</p>
<p className="font-medium">
{preview.formulaLabel}
{preview.hasEstimate ? ` = ${formatCurrency(preview.estimatedPay)}` : ''}
</p>
</div>
{preview.workersCompName && (
<div className="rounded-lg border border-white/80 bg-white/80 p-3 dark:border-dark-700 dark:bg-dark-900">
<p className="text-xs uppercase text-gray-500 dark:text-gray-400">Workers Comp Estimate</p>
<p className="font-semibold">
{preview.workersCompName} · {formatDecimal(preview.workersCompPercentage)}% ·{' '}
{formatCurrency(preview.workersCompAmount)}
</p>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
This is shown for visibility only and does not reduce the employee pay preview.
</p>
</div>
)}
</div>
</CardBox>
);
}

View File

@ -9,6 +9,7 @@ import LoadingSpinner from "../LoadingSpinner";
import Link from 'next/link';
import {hasPermission} from "../../helpers/userPermissions";
import { calculateJobLogPayPreview, formatCurrency } from '../../helpers/jobLogPayPreview';
type Props = {
@ -47,7 +48,15 @@ const CardJob_logs = ({
role='list'
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
>
{!loading && job_logs.map((item, index) => (
{!loading && job_logs.map((item, index) => {
const payPreview = calculateJobLogPayPreview({
payType: item.pay_type,
workersCompClass: item.workersCompClass,
hoursConducted: item.hours_conducted,
clientPaid: item.client_paid,
});
return (
<li
key={item.id}
className={`overflow-hidden ${corners !== 'rounded-full'? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${
@ -161,6 +170,18 @@ const CardJob_logs = ({
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>EmployeePay</dt>
<dd className='flex items-start gap-x-2'>
<div className='font-medium line-clamp-4'>
{ formatCurrency(payPreview.estimatedPay) }
</div>
</dd>
</div>
<div className='flex justify-between gap-x-4 py-3'>
<dt className=' text-gray-500 dark:text-dark-600'>Vehicle</dt>
<dd className='flex items-start gap-x-2'>
@ -234,7 +255,8 @@ const CardJob_logs = ({
</dl>
</li>
))}
);
})}
{!loading && job_logs.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>

View File

@ -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
<>
<div className='relative overflow-x-auto p-4 space-y-4'>
{loading && <LoadingSpinner />}
{!loading && job_logs.map((item) => (
{!loading && job_logs.map((item) => {
const payPreview = calculateJobLogPayPreview({
payType: item.pay_type,
workersCompClass: item.workersCompClass,
hoursConducted: item.hours_conducted,
clientPaid: item.client_paid,
});
return (
<div key={item.id}>
<CardBox hasTable isList className={'rounded shadow-none'}>
<div className={`flex rounded dark:bg-dark-900 border border-stone-300 items-center overflow-hidden`}>
@ -103,6 +112,14 @@ const ListJob_logs = ({ job_logs, loading, onDelete, currentPage, numPages, onPa
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>EmployeePay</p>
<p className={'line-clamp-2 font-semibold'}>{ formatCurrency(payPreview.estimatedPay) }</p>
</div>
<div className={'flex-1 px-3'}>
<p className={'text-xs text-gray-500 '}>Vehicle</p>
<p className={'line-clamp-2'}>{ dataFormatter.vehiclesOneListFormatter(item.vehicle) }</p>
@ -163,7 +180,8 @@ const ListJob_logs = ({ job_logs, loading, onDelete, currentPage, numPages, onPa
</div>
</CardBox>
</div>
))}
);
})}
{!loading && job_logs.length === 0 && (
<div className='col-span-full flex items-center justify-center h-40'>
<p className=''>No data to display</p>

View File

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

View File

@ -0,0 +1,104 @@
const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const numberFormatter = new Intl.NumberFormat('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
export const coerceNumber = (value: unknown) => {
if (value === null || value === undefined || value === '') {
return 0;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
};
export const formatCurrency = (value: unknown) => currencyFormatter.format(coerceNumber(value));
export const formatDecimal = (value: unknown) => numberFormatter.format(coerceNumber(value));
export const resolveEntityId = (value: any) => {
if (!value) {
return null;
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'object' && value.id) {
return value.id;
}
return null;
};
export const hasPayTypeDetails = (payType: any) => {
if (!payType || typeof payType !== 'object') {
return false;
}
return Boolean(payType.pay_method || payType.hourly_rate || payType.commission_rate);
};
export const hasWorkersCompDetails = (workersCompClass: any) => {
if (!workersCompClass || typeof workersCompClass !== 'object') {
return false;
}
return workersCompClass.percentage !== undefined && workersCompClass.percentage !== null;
};
export const calculateJobLogPayPreview = ({
payType,
workersCompClass,
hoursConducted,
clientPaid,
}: {
payType?: any;
workersCompClass?: any;
hoursConducted?: unknown;
clientPaid?: unknown;
}) => {
const payMethod = payType?.pay_method || null;
const hours = coerceNumber(hoursConducted);
const clientAmount = coerceNumber(clientPaid);
const hourlyRate = coerceNumber(payType?.hourly_rate);
const commissionRate = coerceNumber(payType?.commission_rate);
const workersCompPercentage = coerceNumber(workersCompClass?.percentage);
let estimatedPay = 0;
let formulaLabel = 'Select a pay type to preview earnings.';
if (payMethod === 'hourly') {
estimatedPay = hours * hourlyRate;
formulaLabel = `${formatDecimal(hours)} hours × ${formatCurrency(hourlyRate)}/hr`;
} else if (payMethod === 'commission') {
estimatedPay = clientAmount * (commissionRate / 100);
formulaLabel = `${formatCurrency(clientAmount)} client paid × ${formatDecimal(commissionRate)}%`;
}
const workersCompAmount = estimatedPay * (workersCompPercentage / 100);
return {
payMethod,
payTypeName: payType?.name || 'Not selected',
hours,
clientAmount,
hourlyRate,
commissionRate,
estimatedPay,
formulaLabel,
workersCompName: workersCompClass?.name || null,
workersCompPercentage,
workersCompAmount,
hasEstimate: payMethod === 'hourly' || payMethod === 'commission',
};
};

View File

@ -85,18 +85,18 @@ export default function LayoutAuthenticated({
}, [router.events, dispatch])
const layoutAsidePadding = 'xl:pl-60'
const layoutAsidePadding = 'xl:pl-72'
return (
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
<div
className={`${layoutAsidePadding} ${
isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''
} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
>
<NavBar
menu={menuNavBar}
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`}
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''}`}
>
<NavBarItemPlain
display="flex lg:hidden"

View File

@ -128,6 +128,12 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiChartBar,
permissions: 'UPDATE_USERS'
},
{
href: '/admin-help',
label: 'Admin Help',
icon: icon.mdiHelpCircleOutline,
permissions: 'UPDATE_USERS'
},
{
href: '/profile',
label: 'Profile',

View File

@ -0,0 +1,304 @@
import {
mdiAccountCashOutline,
mdiCashRegister,
mdiClipboardCheckOutline,
mdiClipboardListOutline,
mdiCogOutline,
mdiHelpCircleOutline,
mdiOpenInNew,
mdiPlayCircleOutline,
mdiShieldCheckOutline,
} from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement } from 'react';
import BaseButton from '../components/BaseButton';
import BaseDivider from '../components/BaseDivider';
import BaseIcon from '../components/BaseIcon';
import CardBox from '../components/CardBox';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import LayoutAuthenticated from '../layouts/Authenticated';
type HelpSection = {
id: string;
title: string;
icon: string;
summary: string;
steps: string[];
checks?: string[];
warning?: string;
href?: string;
hrefLabel?: string;
};
const helpSections: HelpSection[] = [
{
id: 'getting-started',
title: 'Getting started',
icon: mdiPlayCircleOutline,
summary:
'Use this as the admins quick-start routine before you begin assigning pay or creating payroll runs.',
steps: [
'Confirm your users, customers, vehicles, pay types, and workers comp classes are up to date.',
'Make sure employees know whether they should use Log Work or whether an admin is entering logs on their behalf.',
'Decide how often payroll should be run and which date range each payroll run should cover.',
'Review this help page the first few times so your workflow stays consistent.',
],
checks: [
'Users have the correct role and active account status.',
'Employees have at least one pay setup assigned before they start logging jobs.',
'Admins know which menu pages they will use most often: Users, Pay types, Employee pay types, All Job Logs, and Payroll runs.',
],
href: '/dashboard',
hrefLabel: 'Open Dashboard',
},
{
id: 'assign-employee-pay',
title: 'Assign employee pay',
icon: mdiAccountCashOutline,
summary:
'This links an employee to the pay option they are allowed to use on jobs.',
steps: [
'Open Employee pay types.',
'Create a new record and select the employee.',
'Choose the pay type you want that employee to use, such as hourly or commission.',
'Save the record and verify the employee can now select that pay option while logging work.',
'If an employee should have multiple valid pay options, add each allowed pay type as its own assignment.',
],
checks: [
'Use clear pay type names so employees can easily choose the right option while logging work.',
'Remove outdated assignments if an employee should no longer use an old rate or method.',
],
href: '/employee_pay_types/employee_pay_types-list',
hrefLabel: 'Open Employee Pay Types',
},
{
id: 'set-up-pay-types',
title: 'Set up pay types',
icon: mdiCogOutline,
summary:
'Pay types control how job pay is calculated. Keep these clean and intentional so payroll stays predictable.',
steps: [
'Open Pay types and review the existing list before adding a new one.',
'For hourly work, confirm the hourly rate is correct.',
'For commission-based work, confirm the commission percentage is correct.',
'Use consistent naming so admins and employees can tell similar pay types apart.',
'Save changes, then test a sample job log if the pay logic is new or changed.',
],
checks: [
'Avoid creating duplicate pay types with only slightly different names.',
'When changing a rate, double-check whether you should edit an existing pay type or create a new one for future use.',
],
href: '/pay_types/pay_types-list',
hrefLabel: 'Open Pay Types',
},
{
id: 'review-job-logs',
title: 'Review job logs before payroll',
icon: mdiClipboardCheckOutline,
summary:
'A quick review before payroll helps catch missing hours, wrong pay types, or incomplete client-paid amounts.',
steps: [
'Open All Job Logs and filter by the payroll period you are about to run.',
'Scan Employee Pay, hours, client paid, and pay type selections for anything that looks incorrect.',
'Open any questionable record and verify the employee, customer, vehicle, and work details.',
'Fix missing or incorrect values before creating the payroll run.',
],
checks: [
'Hourly jobs should have hours entered.',
'Commission jobs should have the client paid amount entered.',
'Employees should not have duplicate or accidental logs for the same work.',
],
href: '/job_logs/job_logs-list',
hrefLabel: 'Open All Job Logs',
},
{
id: 'run-payroll',
title: 'Run payroll',
icon: mdiCashRegister,
summary:
'Create a payroll run only after the logs in that date range look clean.',
steps: [
'Open Payroll runs and create a new run.',
'Set the payroll dates carefully so the run only includes the jobs you intend to pay.',
'Save the payroll run and review the generated results.',
'Open the payroll line items if you need job-by-job detail for what was included.',
'If totals look wrong, go back to the source job logs, correct them, and then rerun or recreate payroll as needed.',
],
checks: [
'Watch for an incorrect date range first; that is one of the most common causes of missing or extra pay.',
'If one employee total looks off, inspect that employees individual job logs before changing anything else.',
],
href: '/payroll_runs/payroll_runs-list',
hrefLabel: 'Open Payroll Runs',
},
{
id: 'check-results',
title: 'Check payroll results',
icon: mdiClipboardListOutline,
summary:
'After a run is created, verify the output before treating it as final.',
steps: [
'Review the payroll run totals and compare them with the expected workload for that period.',
'Open Payroll line items to confirm each employee has the right combination of job entries.',
'Spot-check a few records manually using the job log Employee Pay values and the live preview logic on the log form.',
'Use the Payroll Dashboard if you want a broader visual review of payroll activity.',
],
checks: [
'Investigate unusual spikes or drops before approving the numbers internally.',
'Keep a repeatable review routine so payroll checks stay consistent every pay period.',
],
href: '/reports',
hrefLabel: 'Open Payroll Dashboard',
},
{
id: 'troubleshooting',
title: 'Troubleshooting and common issues',
icon: mdiShieldCheckOutline,
summary:
'Use this checklist whenever the numbers do not look right or an employee cannot complete a job log correctly.',
steps: [
'If an employee cannot choose the right pay option, confirm the Employee pay types assignment exists.',
'If live pay looks wrong on a job log, verify the pay type values and the job fields used for that method.',
'If payroll totals look off, compare the payroll date range with the dates on the job logs included in the run.',
'If a commission total looks too low or too high, check the client paid amount on the job log first.',
'If an hourly total looks wrong, check hours entered and the selected hourly pay type.',
],
warning:
'Best practice: fix the source job log or pay setup first, then regenerate or recreate the payroll output instead of trying to explain away a mismatch later.',
href: '/payroll_line_items/payroll_line_items-list',
hrefLabel: 'Open Payroll Line Items',
},
];
const AdminHelpPage = () => {
return (
<>
<Head>
<title>{getPageTitle('Admin Help')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiHelpCircleOutline}
title='Admin Help'
main
>
{''}
</SectionTitleLineWithButton>
<CardBox className='mb-6'>
<div className='flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between'>
<div className='max-w-3xl'>
<h2 className='text-xl font-semibold mb-2'>Admin task checklist</h2>
<p className='text-gray-600 dark:text-gray-300'>
This page is a simple internal knowledge base for the most common admin workflows in the app.
Use the quick links below to jump to a process, then open the related page directly when you are ready to do the task.
</p>
</div>
<BaseButton
href='/payroll_runs/payroll_runs-list'
color='info'
icon={mdiOpenInNew}
label='Go to Payroll Runs'
/>
</div>
<BaseDivider />
<div className='flex flex-wrap gap-3'>
{helpSections.map((section) => (
<BaseButton
key={section.id}
asAnchor
href={`#${section.id}`}
color='whiteDark'
small
label={section.title}
/>
))}
</div>
</CardBox>
<div className='grid grid-cols-1 gap-6 xl:grid-cols-2'>
{helpSections.map((section) => (
<CardBox key={section.id} className='h-full'>
<div id={section.id} className='scroll-mt-20'>
<div className='flex items-start justify-between gap-4 mb-4'>
<div className='flex items-start gap-3'>
<BaseIcon path={section.icon} size={28} className='text-blue-600 dark:text-blue-400 mt-1' />
<div>
<h2 className='text-xl font-semibold'>{section.title}</h2>
<p className='text-sm text-gray-600 dark:text-gray-300 mt-1'>
{section.summary}
</p>
</div>
</div>
{section.href && section.hrefLabel ? (
<BaseButton
href={section.href}
color='info'
small
icon={mdiOpenInNew}
label={section.hrefLabel}
/>
) : null}
</div>
<div className='mb-4'>
<div className='text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-2'>
Checklist
</div>
<ol className='list-decimal pl-5 space-y-2 text-sm leading-6'>
{section.steps.map((step) => (
<li key={step}>{step}</li>
))}
</ol>
</div>
{section.checks?.length ? (
<div className='mb-4'>
<div className='text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 mb-2'>
Verify before moving on
</div>
<ul className='list-disc pl-5 space-y-2 text-sm leading-6'>
{section.checks.map((check) => (
<li key={check}>{check}</li>
))}
</ul>
</div>
) : null}
{section.warning ? (
<div className='rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm leading-6 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-100'>
<span className='font-semibold'>Admin note:</span> {section.warning}
</div>
) : null}
</div>
</CardBox>
))}
</div>
<CardBox className='mt-6'>
<div className='flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between'>
<div>
<h2 className='text-lg font-semibold'>Suggested next improvement</h2>
<p className='text-sm text-gray-600 dark:text-gray-300 mt-1'>
If you want, this help page can later become searchable or editable so admins can maintain their own internal SOPs.
</p>
</div>
<BaseButton href='/dashboard' color='whiteDark' label='Back to Dashboard' />
</div>
</CardBox>
</SectionMain>
</>
);
};
AdminHelpPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission='UPDATE_USERS'>{page}</LayoutAuthenticated>;
};
export default AdminHelpPage;

View File

@ -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 = () => {
></Field>
</FormField>
<JobLogPayPreview />

View File

@ -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 = () => {
<Field name="pay_type" id="pay_type" component={SelectField} options={[]} itemRef={'pay_types'}></Field>
</FormField>
<JobLogPayPreview />

View File

@ -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 = () => {
))}
</Field>
</FormField>
<JobLogPayPreview payTypeOptions={assignedPayTypes} />
<FormField label="Vehicle" labelFor="vehicle">
<Field name="vehicle" id="vehicle" component={SelectField} options={[]} itemRef={'vehicles'} />
</FormField>