diff --git a/src/components/views/BillingView.tsx b/src/components/views/BillingView.tsx index 98e10cb..f88fc06 100644 --- a/src/components/views/BillingView.tsx +++ b/src/components/views/BillingView.tsx @@ -1,17 +1,21 @@ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useMemo } from 'react' import { Plus, Download, Receipt, - Envelope + Envelope, + ChartLine, + Warning } from '@phosphor-icons/react' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' -import { Card, CardContent } from '@/components/ui/card' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { PermanentPlacementInvoice } from '@/components/PermanentPlacementInvoice' import { CreditNoteGenerator } from '@/components/CreditNoteGenerator' import { InvoiceDetailDialog } from '@/components/InvoiceDetailDialog' import { AdvancedSearch, type FilterField } from '@/components/AdvancedSearch' +import { useInvoicing } from '@/hooks/use-invoicing' +import { toast } from 'sonner' import type { Invoice, RateCard } from '@/lib/types' interface BillingViewProps { @@ -35,6 +39,14 @@ export function BillingView({ }: BillingViewProps) { const [viewingInvoice, setViewingInvoice] = useState(null) const [filteredInvoices, setFilteredInvoices] = useState([]) + const [showAnalytics, setShowAnalytics] = useState(false) + + const { + calculateInvoiceAging, + getOverdueInvoices, + calculateTotalRevenue, + getInvoicesByStatus + } = useInvoicing() useEffect(() => { setFilteredInvoices(invoices) @@ -43,6 +55,11 @@ export function BillingView({ const handleResultsChange = useCallback((results: Invoice[]) => { setFilteredInvoices(results) }, []) + + const agingData = useMemo(() => calculateInvoiceAging(), [calculateInvoiceAging]) + const overdueInvoices = useMemo(() => getOverdueInvoices(), [getOverdueInvoices]) + const totalRevenue = useMemo(() => calculateTotalRevenue(), [calculateTotalRevenue]) + const draftInvoices = useMemo(() => getInvoicesByStatus('draft'), [getInvoicesByStatus]) const invoiceFields: FilterField[] = [ { name: 'invoiceNumber', label: 'Invoice Number', type: 'text' }, @@ -71,6 +88,13 @@ export function BillingView({

Manage invoices and track payments

+
+ {showAnalytics && ( +
+
+ + + Total Revenue + + +
+ £{totalRevenue.toLocaleString()} +
+
+
+ + + Draft Invoices + + +
{draftInvoices.length}
+
+
+ + + Overdue + + +
+
{overdueInvoices.length}
+ {overdueInvoices.length > 0 && ( + + + Action needed + + )} +
+
+
+ + + Outstanding + + +
+ £{(agingData.current + agingData.days30 + agingData.days60 + agingData.days90 + agingData.over90).toLocaleString()} +
+
+
+
+ + + + Invoice Aging Analysis + + +
+
+
Current
+
£{agingData.current.toLocaleString()}
+
+
+
1-30 Days
+
£{agingData.days30.toLocaleString()}
+
+
+
31-60 Days
+
£{agingData.days60.toLocaleString()}
+
+
+
61-90 Days
+
£{agingData.days90.toLocaleString()}
+
+
+
90+ Days
+
£{agingData.over90.toLocaleString()}
+
+
+
+
+
+ )} + (null) + const [showAnalytics, setShowAnalytics] = useState(false) + const [showCalculator, setShowCalculator] = useState(false) + const [calculatorGrossPay, setCalculatorGrossPay] = useState('1000') + const [calculatorResult, setCalculatorResult] = useState(null) + + const { + calculatePayroll, + calculateBatchPayroll, + payrollConfig + } = usePayrollCalculations() + + const approvedTimesheets = useMemo(() => + timesheets.filter(ts => ts.status === 'approved'), + [timesheets] + ) + + const pendingTimesheets = useMemo(() => + timesheets.filter(ts => ts.status === 'pending'), + [timesheets] + ) + + const totalPendingValue = useMemo(() => + pendingTimesheets.reduce((sum, ts) => sum + ts.amount, 0), + [pendingTimesheets] + ) + + const lastRun = useMemo(() => + payrollRuns.length > 0 ? payrollRuns[payrollRuns.length - 1] : null, + [payrollRuns] + ) + + const handleCalculate = () => { + const grossPay = parseFloat(calculatorGrossPay) + if (isNaN(grossPay) || grossPay <= 0) { + toast.error('Please enter a valid gross pay amount') + return + } + + const result = calculatePayroll('CALC-WORKER', grossPay, grossPay * 12, false) + setCalculatorResult(result) + } return (
@@ -27,10 +73,91 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr

Payroll Processing

Manage payroll runs and worker payments

- +
+ + + + + + + + Payroll Tax Calculator + +
+
+ + setCalculatorGrossPay(e.target.value)} + className="w-full mt-1 px-3 py-2 border border-input rounded-md" + placeholder="1000" + /> +
+ + + {calculatorResult && ( +
+
+ + +
Gross Pay
+
+ £{calculatorResult.grossPay.toFixed(2)} +
+
+
+ + +
Net Pay
+
+ £{calculatorResult.netPay.toFixed(2)} +
+
+
+
+ + + + Breakdown + + + {calculatorResult.breakdown.map((item: any, idx: number) => ( +
+ {item.description} + + £{Math.abs(item.amount).toFixed(2)} + +
+ ))} +
+
+ + + +
Tax Year: {payrollConfig.taxYear}
+
Personal Allowance: £{payrollConfig.personalAllowance.toLocaleString()}
+
+
+
+ )} +
+
+
+ +
+ {showAnalytics && ( +
+ + + Approved Timesheets + + +
{approvedTimesheets.length}
+

Ready for payroll

+
+
+ + + Pending Approval + + +
{pendingTimesheets.length}
+

+ £{totalPendingValue.toLocaleString()} value +

+
+
+ + + Total Payroll Runs + + +
{payrollRuns.length}
+
+
+ + + Last Run Total + + +
+ £{lastRun ? lastRun.totalAmount.toLocaleString() : '0'} +
+

+ {lastRun ? `${lastRun.workersCount} workers paid` : 'No runs yet'} +

+
+
+
+ )} +
@@ -54,7 +227,7 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr Pending Approval -
12 timesheets
+
{pendingTimesheets.length} timesheets

Must be approved for payroll

@@ -64,8 +237,12 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr Last Run Total -
£28,900
-

45 workers paid

+
+ £{lastRun ? lastRun.totalAmount.toLocaleString() : '0'} +
+

+ {lastRun ? `${lastRun.workersCount} workers paid` : 'No runs yet'} +

diff --git a/src/components/views/TimesheetsView.tsx b/src/components/views/TimesheetsView.tsx index 7ef13da..74ea4f8 100644 --- a/src/components/views/TimesheetsView.tsx +++ b/src/components/views/TimesheetsView.tsx @@ -1,12 +1,16 @@ import { useState, useMemo, useEffect, useCallback } from 'react' -import { Download, Funnel } from '@phosphor-icons/react' +import { Download, Funnel, ChartBar, Warning } from '@phosphor-icons/react' import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' import { TimesheetAdjustmentWizard } from '@/components/TimesheetAdjustmentWizard' import { TimesheetDetailDialog } from '@/components/TimesheetDetailDialog' import { AdvancedSearch, type FilterField } from '@/components/AdvancedSearch' import { TimesheetCreateDialogs } from '@/components/timesheets/TimesheetCreateDialogs' import { TimesheetTabs } from '@/components/timesheets/TimesheetTabs' +import { useTimeTracking } from '@/hooks/use-time-tracking' +import { toast } from 'sonner' import type { Timesheet, TimesheetStatus, ShiftEntry } from '@/lib/types' interface TimesheetsViewProps { @@ -53,6 +57,14 @@ export function TimesheetsView({ const [isBulkImportOpen, setIsBulkImportOpen] = useState(false) const [selectedTimesheet, setSelectedTimesheet] = useState(null) const [viewingTimesheet, setViewingTimesheet] = useState(null) + const [showAnalytics, setShowAnalytics] = useState(false) + + const { + validateTimesheet, + analyzeWorkingTime, + calculateShiftHours, + determineShiftType + } = useTimeTracking() const timesheetsToFilter = useMemo(() => { return timesheets.filter(t => { @@ -71,6 +83,26 @@ export function TimesheetsView({ setFilteredTimesheets(results) }, []) + const timesheetsWithValidation = useMemo(() => { + return filteredTimesheets.map(ts => { + const validation = validateTimesheet(ts) + return { + ...ts, + validationErrors: validation.errors, + validationWarnings: validation.warnings, + isValid: validation.isValid + } + }) + }, [filteredTimesheets, validateTimesheet]) + + const validationStats = useMemo(() => { + const invalid = timesheetsWithValidation.filter(ts => !ts.isValid).length + const withWarnings = timesheetsWithValidation.filter(ts => + ts.validationWarnings && ts.validationWarnings.length > 0 + ).length + return { invalid, withWarnings } + }, [timesheetsWithValidation]) + const [formData, setFormData] = useState({ workerName: '', clientName: '', @@ -101,6 +133,13 @@ export function TimesheetsView({

Manage and approve worker timesheets

+
+ {showAnalytics && ( +
+ + + Total Timesheets + + +
{filteredTimesheets.length}
+
+
+ + + Total Hours + + +
+ {filteredTimesheets.reduce((sum, ts) => sum + ts.hours, 0).toFixed(1)}h +
+
+
+ + + Validation Issues + + +
+
{validationStats.invalid}
+ {validationStats.invalid > 0 && ( + + + Errors + + )} +
+
+
+ + + Total Value + + +
+ £{filteredTimesheets.reduce((sum, ts) => sum + ts.amount, 0).toLocaleString()} +
+
+
+
+ )} +