Generated by Spark: Integrate the new business logic hooks into existing views (Timesheets, Billing, Payroll)

This commit is contained in:
2026-01-23 07:25:18 +00:00
committed by GitHub
parent b072ac4689
commit 9d1b71714b
3 changed files with 384 additions and 14 deletions

View File

@@ -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<Invoice | null>(null)
const [filteredInvoices, setFilteredInvoices] = useState<Invoice[]>([])
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({
<p className="text-muted-foreground mt-1">Manage invoices and track payments</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setShowAnalytics(!showAnalytics)}
>
<ChartLine size={18} className="mr-2" />
{showAnalytics ? 'Hide' : 'Show'} Analytics
</Button>
<PermanentPlacementInvoice onCreateInvoice={onCreatePlacementInvoice} />
<CreditNoteGenerator invoices={invoices} onCreateCreditNote={onCreateCreditNote} />
<Button>
@@ -80,6 +104,87 @@ export function BillingView({
</div>
</div>
{showAnalytics && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Total Revenue</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold font-mono">
£{totalRevenue.toLocaleString()}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Draft Invoices</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold">{draftInvoices.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Overdue</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
<div className="text-2xl font-semibold">{overdueInvoices.length}</div>
{overdueInvoices.length > 0 && (
<Badge variant="destructive" className="text-xs">
<Warning size={12} className="mr-1" />
Action needed
</Badge>
)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Outstanding</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold font-mono">
£{(agingData.current + agingData.days30 + agingData.days60 + agingData.days90 + agingData.over90).toLocaleString()}
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle className="text-sm">Invoice Aging Analysis</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-5 gap-4">
<div>
<div className="text-xs text-muted-foreground mb-1">Current</div>
<div className="font-semibold font-mono">£{agingData.current.toLocaleString()}</div>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">1-30 Days</div>
<div className="font-semibold font-mono">£{agingData.days30.toLocaleString()}</div>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">31-60 Days</div>
<div className="font-semibold font-mono text-warning">£{agingData.days60.toLocaleString()}</div>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">61-90 Days</div>
<div className="font-semibold font-mono text-warning">£{agingData.days90.toLocaleString()}</div>
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">90+ Days</div>
<div className="font-semibold font-mono text-destructive">£{agingData.over90.toLocaleString()}</div>
</div>
</div>
</CardContent>
</Card>
</div>
)}
<AdvancedSearch
items={invoices}
fields={invoiceFields}

View File

@@ -1,14 +1,19 @@
import { useState } from 'react'
import { useState, useMemo } from 'react'
import {
Plus,
CurrencyDollar,
Download
Download,
ChartBar,
Calculator
} from '@phosphor-icons/react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { PayrollDetailDialog } from '@/components/PayrollDetailDialog'
import { OneClickPayroll } from '@/components/OneClickPayroll'
import { usePayrollCalculations } from '@/hooks/use-payroll-calculations'
import { toast } from 'sonner'
import type { PayrollRun, Timesheet } from '@/lib/types'
interface PayrollViewProps {
@@ -19,6 +24,47 @@ interface PayrollViewProps {
export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: PayrollViewProps) {
const [viewingPayroll, setViewingPayroll] = useState<PayrollRun | null>(null)
const [showAnalytics, setShowAnalytics] = useState(false)
const [showCalculator, setShowCalculator] = useState(false)
const [calculatorGrossPay, setCalculatorGrossPay] = useState('1000')
const [calculatorResult, setCalculatorResult] = useState<any>(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 (
<div className="space-y-6">
@@ -27,10 +73,91 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr
<h2 className="text-3xl font-semibold tracking-tight">Payroll Processing</h2>
<p className="text-muted-foreground mt-1">Manage payroll runs and worker payments</p>
</div>
<Button>
<Plus size={18} className="mr-2" />
Run Payroll
</Button>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setShowAnalytics(!showAnalytics)}
>
<ChartBar size={18} className="mr-2" />
{showAnalytics ? 'Hide' : 'Show'} Analytics
</Button>
<Dialog open={showCalculator} onOpenChange={setShowCalculator}>
<DialogTrigger asChild>
<Button variant="outline">
<Calculator size={18} className="mr-2" />
Tax Calculator
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Payroll Tax Calculator</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Gross Pay (Monthly)</label>
<input
type="number"
value={calculatorGrossPay}
onChange={(e) => setCalculatorGrossPay(e.target.value)}
className="w-full mt-1 px-3 py-2 border border-input rounded-md"
placeholder="1000"
/>
</div>
<Button onClick={handleCalculate}>Calculate</Button>
{calculatorResult && (
<div className="space-y-3 border-t pt-4">
<div className="grid grid-cols-2 gap-3">
<Card>
<CardContent className="pt-4">
<div className="text-sm text-muted-foreground">Gross Pay</div>
<div className="text-xl font-semibold font-mono">
£{calculatorResult.grossPay.toFixed(2)}
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm text-muted-foreground">Net Pay</div>
<div className="text-xl font-semibold font-mono text-success">
£{calculatorResult.netPay.toFixed(2)}
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle className="text-sm">Breakdown</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{calculatorResult.breakdown.map((item: any, idx: number) => (
<div key={idx} className="flex justify-between text-sm">
<span>{item.description}</span>
<span className={`font-mono ${item.amount < 0 ? 'text-destructive' : ''}`}>
£{Math.abs(item.amount).toFixed(2)}
</span>
</div>
))}
</CardContent>
</Card>
<Card className="bg-muted/50">
<CardContent className="pt-4">
<div className="text-xs text-muted-foreground mb-1">Tax Year: {payrollConfig.taxYear}</div>
<div className="text-xs text-muted-foreground">Personal Allowance: £{payrollConfig.personalAllowance.toLocaleString()}</div>
</CardContent>
</Card>
</div>
)}
</div>
</DialogContent>
</Dialog>
<Button>
<Plus size={18} className="mr-2" />
Run Payroll
</Button>
</div>
</div>
<OneClickPayroll
@@ -38,6 +165,52 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr
onPayrollComplete={onPayrollComplete}
/>
{showAnalytics && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Approved Timesheets</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold">{approvedTimesheets.length}</div>
<p className="text-sm text-muted-foreground mt-1">Ready for payroll</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Pending Approval</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold">{pendingTimesheets.length}</div>
<p className="text-sm text-muted-foreground mt-1">
£{totalPendingValue.toLocaleString()} value
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Total Payroll Runs</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold">{payrollRuns.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Last Run Total</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold font-mono">
£{lastRun ? lastRun.totalAmount.toLocaleString() : '0'}
</div>
<p className="text-sm text-muted-foreground mt-1">
{lastRun ? `${lastRun.workersCount} workers paid` : 'No runs yet'}
</p>
</CardContent>
</Card>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader>
@@ -54,7 +227,7 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr
<CardTitle className="text-sm text-muted-foreground">Pending Approval</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold">12 timesheets</div>
<div className="text-2xl font-semibold">{pendingTimesheets.length} timesheets</div>
<p className="text-sm text-muted-foreground mt-1">Must be approved for payroll</p>
</CardContent>
</Card>
@@ -64,8 +237,12 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr
<CardTitle className="text-sm text-muted-foreground">Last Run Total</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold font-mono">£28,900</div>
<p className="text-sm text-muted-foreground mt-1">45 workers paid</p>
<div className="text-2xl font-semibold font-mono">
£{lastRun ? lastRun.totalAmount.toLocaleString() : '0'}
</div>
<p className="text-sm text-muted-foreground mt-1">
{lastRun ? `${lastRun.workersCount} workers paid` : 'No runs yet'}
</p>
</CardContent>
</Card>
</div>

View File

@@ -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<Timesheet | null>(null)
const [viewingTimesheet, setViewingTimesheet] = useState<Timesheet | null>(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({
<p className="text-muted-foreground mt-1">Manage and approve worker timesheets</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setShowAnalytics(!showAnalytics)}
>
<ChartBar size={18} className="mr-2" />
{showAnalytics ? 'Hide' : 'Show'} Analytics
</Button>
<TimesheetCreateDialogs
isCreateDialogOpen={isCreateDialogOpen}
setIsCreateDialogOpen={setIsCreateDialogOpen}
@@ -117,6 +156,55 @@ export function TimesheetsView({
</div>
</div>
{showAnalytics && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Total Timesheets</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold">{filteredTimesheets.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Total Hours</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold">
{filteredTimesheets.reduce((sum, ts) => sum + ts.hours, 0).toFixed(1)}h
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Validation Issues</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
<div className="text-2xl font-semibold">{validationStats.invalid}</div>
{validationStats.invalid > 0 && (
<Badge variant="destructive" className="text-xs">
<Warning size={12} className="mr-1" />
Errors
</Badge>
)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm text-muted-foreground">Total Value</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold font-mono">
£{filteredTimesheets.reduce((sum, ts) => sum + ts.amount, 0).toLocaleString()}
</div>
</CardContent>
</Card>
</div>
)}
<AdvancedSearch
items={timesheetsToFilter}
fields={timesheetFields}
@@ -146,7 +234,7 @@ export function TimesheetsView({
</div>
<TimesheetTabs
filteredTimesheets={filteredTimesheets}
filteredTimesheets={timesheetsWithValidation}
onApprove={onApprove}
onReject={onReject}
onCreateInvoice={onCreateInvoice}