mirror of
https://github.com/johndoe6345789/workforce-pay-bill-p.git
synced 2026-04-24 13:24:57 +00:00
Generated by Spark: Integrate the new business logic hooks into existing views (Timesheets, Billing, Payroll)
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user