Generated by Spark: Update remaining views (Payroll, Compliance, Expenses) to use PageHeader, Grid, Stack, and MetricCard components

This commit is contained in:
2026-01-23 07:46:36 +00:00
committed by GitHub
parent 5380522401
commit d304733eca
3 changed files with 436 additions and 413 deletions

View File

@@ -3,16 +3,21 @@ import {
UploadSimple,
Warning,
XCircle,
CheckCircle
CheckCircle,
FileText
} from '@phosphor-icons/react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Card, CardContent } from '@/components/ui/card'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { PageHeader } from '@/components/ui/page-header'
import { Grid } from '@/components/ui/grid'
import { Stack } from '@/components/ui/stack'
import { MetricCard } from '@/components/ui/metric-card'
import { toast } from 'sonner'
import { ComplianceDetailDialog } from '@/components/ComplianceDetailDialog'
import { AdvancedSearch, type FilterField } from '@/components/AdvancedSearch'
@@ -44,6 +49,7 @@ export function ComplianceView({ complianceDocs, onUploadDocument }: ComplianceV
const expiringDocs = filteredDocs.filter(d => d.status === 'expiring')
const expiredDocs = filteredDocs.filter(d => d.status === 'expired')
const validDocs = filteredDocs.filter(d => d.status === 'valid')
const [uploadFormData, setUploadFormData] = useState({
workerId: '',
@@ -94,77 +100,77 @@ export function ComplianceView({ complianceDocs, onUploadDocument }: ComplianceV
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-semibold tracking-tight">Compliance Monitoring</h2>
<p className="text-muted-foreground mt-1">Track worker documentation and certifications</p>
</div>
<Dialog open={isUploadOpen} onOpenChange={setIsUploadOpen}>
<DialogTrigger asChild>
<Button>
<UploadSimple size={18} className="mr-2" />
Upload Document
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Upload Compliance Document</DialogTitle>
<DialogDescription>
Add a new document for a worker
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="workerName">Worker Name</Label>
<Input
id="workerName"
placeholder="Enter worker name"
value={uploadFormData.workerName}
onChange={(e) => setUploadFormData({ ...uploadFormData, workerName: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="documentType">Document Type</Label>
<Select
value={uploadFormData.documentType}
onValueChange={(value) => setUploadFormData({ ...uploadFormData, documentType: value })}
>
<SelectTrigger id="documentType">
<SelectValue placeholder="Select document type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="DBS Check">DBS Check</SelectItem>
<SelectItem value="Right to Work">Right to Work</SelectItem>
<SelectItem value="Professional License">Professional License</SelectItem>
<SelectItem value="First Aid Certificate">First Aid Certificate</SelectItem>
<SelectItem value="Driving License">Driving License</SelectItem>
<SelectItem value="Passport">Passport</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="expiryDate">Expiry Date</Label>
<Input
id="expiryDate"
type="date"
value={uploadFormData.expiryDate}
onChange={(e) => setUploadFormData({ ...uploadFormData, expiryDate: e.target.value })}
/>
</div>
<div className="border-2 border-dashed border-border rounded-lg p-6 text-center">
<UploadSimple size={32} className="mx-auto text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground mb-2">Click to upload or drag and drop</p>
<p className="text-xs text-muted-foreground">PDF, JPG, PNG up to 10MB</p>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsUploadOpen(false)}>Cancel</Button>
<Button onClick={handleSubmitUpload}>Upload Document</Button>
</div>
</DialogContent>
</Dialog>
</div>
<Stack spacing={6}>
<PageHeader
title="Compliance Monitoring"
description="Track worker documentation and certifications"
actions={
<Dialog open={isUploadOpen} onOpenChange={setIsUploadOpen}>
<DialogTrigger asChild>
<Button>
<UploadSimple size={18} className="mr-2" />
Upload Document
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Upload Compliance Document</DialogTitle>
<DialogDescription>
Add a new document for a worker
</DialogDescription>
</DialogHeader>
<Stack spacing={4} className="py-4">
<div className="space-y-2">
<Label htmlFor="workerName">Worker Name</Label>
<Input
id="workerName"
placeholder="Enter worker name"
value={uploadFormData.workerName}
onChange={(e) => setUploadFormData({ ...uploadFormData, workerName: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="documentType">Document Type</Label>
<Select
value={uploadFormData.documentType}
onValueChange={(value) => setUploadFormData({ ...uploadFormData, documentType: value })}
>
<SelectTrigger id="documentType">
<SelectValue placeholder="Select document type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="DBS Check">DBS Check</SelectItem>
<SelectItem value="Right to Work">Right to Work</SelectItem>
<SelectItem value="Professional License">Professional License</SelectItem>
<SelectItem value="First Aid Certificate">First Aid Certificate</SelectItem>
<SelectItem value="Driving License">Driving License</SelectItem>
<SelectItem value="Passport">Passport</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="expiryDate">Expiry Date</Label>
<Input
id="expiryDate"
type="date"
value={uploadFormData.expiryDate}
onChange={(e) => setUploadFormData({ ...uploadFormData, expiryDate: e.target.value })}
/>
</div>
<div className="border-2 border-dashed border-border rounded-lg p-6 text-center">
<UploadSimple size={32} className="mx-auto text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground mb-2">Click to upload or drag and drop</p>
<p className="text-xs text-muted-foreground">PDF, JPG, PNG up to 10MB</p>
</div>
</Stack>
<Stack direction="horizontal" spacing={2} justify="end">
<Button variant="outline" onClick={() => setIsUploadOpen(false)}>Cancel</Button>
<Button onClick={handleSubmitUpload}>Upload Document</Button>
</Stack>
</DialogContent>
</Dialog>
}
/>
<AdvancedSearch
items={complianceDocs}
@@ -173,33 +179,26 @@ export function ComplianceView({ complianceDocs, onUploadDocument }: ComplianceV
placeholder="Search documents or use query language (e.g., status = expiring daysUntilExpiry < 30)"
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="border-l-4 border-warning/20">
<CardHeader>
<CardTitle className="text-sm text-muted-foreground flex items-center gap-2">
<Warning size={18} className="text-warning" />
Expiring Soon
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-semibold">{expiringDocs.length}</div>
<p className="text-sm text-muted-foreground mt-1">Documents expiring within 30 days</p>
</CardContent>
</Card>
<Card className="border-l-4 border-destructive/20">
<CardHeader>
<CardTitle className="text-sm text-muted-foreground flex items-center gap-2">
<XCircle size={18} className="text-destructive" />
Expired
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-semibold">{expiredDocs.length}</div>
<p className="text-sm text-muted-foreground mt-1">Workers blocked from engagement</p>
</CardContent>
</Card>
</div>
<Grid cols={3} gap={4}>
<MetricCard
label="Valid Documents"
value={validDocs.length}
description="All compliance requirements met"
icon={<CheckCircle size={24} className="text-success" />}
/>
<MetricCard
label="Expiring Soon"
value={expiringDocs.length}
description="Documents expiring within 30 days"
icon={<Warning size={24} className="text-warning" />}
/>
<MetricCard
label="Expired"
value={expiredDocs.length}
description="Workers blocked from engagement"
icon={<XCircle size={24} className="text-destructive" />}
/>
</Grid>
<Tabs defaultValue="expiring" className="space-y-4">
<TabsList>
@@ -256,7 +255,7 @@ export function ComplianceView({ complianceDocs, onUploadDocument }: ComplianceV
if (!open) setViewingDocument(null)
}}
/>
</div>
</Stack>
)
}
@@ -278,19 +277,19 @@ function ComplianceCard({ document, onViewDetails }: ComplianceCardProps) {
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => onViewDetails?.(document)}>
<CardContent className="p-6">
<div className="flex items-start justify-between">
<div className="space-y-3 flex-1">
<div className="flex items-center gap-3">
<Stack spacing={3} className="flex-1">
<Stack direction="horizontal" spacing={3} align="start">
<div className={cn('p-2 rounded-lg', statusConfig[document.status].bgColor)}>
<StatusIcon size={20} weight="fill" className={statusConfig[document.status].color} />
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-1">
<Stack direction="horizontal" spacing={3} align="center" className="mb-1">
<h3 className="font-semibold">{document.workerName}</h3>
<Badge variant={document.status === 'valid' ? 'success' : document.status === 'expiring' ? 'warning' : 'destructive'}>
{document.status}
</Badge>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
</Stack>
<Grid cols={3} gap={4} className="text-sm">
<div>
<p className="text-muted-foreground">Document Type</p>
<p className="font-medium">{document.documentType}</p>
@@ -309,14 +308,14 @@ function ComplianceCard({ document, onViewDetails }: ComplianceCardProps) {
{document.daysUntilExpiry < 0 ? 'Expired' : `${document.daysUntilExpiry} days`}
</p>
</div>
</div>
</Grid>
</div>
</div>
</div>
<div className="flex gap-2 ml-4" onClick={(e) => e.stopPropagation()}>
</Stack>
</Stack>
<Stack direction="horizontal" spacing={2} className="ml-4" onClick={(e) => e.stopPropagation()}>
<Button size="sm" variant="outline">View</Button>
<Button size="sm">Upload New</Button>
</div>
</Stack>
</div>
</CardContent>
</Card>

View File

@@ -6,7 +6,8 @@ import {
CheckCircle,
ClockCounterClockwise,
XCircle,
Camera
Camera,
CurrencyDollar
} from '@phosphor-icons/react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -17,6 +18,10 @@ import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { PageHeader } from '@/components/ui/page-header'
import { Grid } from '@/components/ui/grid'
import { Stack } from '@/components/ui/stack'
import { MetricCard } from '@/components/ui/metric-card'
import { toast } from 'sonner'
import { ExpenseDetailDialog } from '@/components/ExpenseDetailDialog'
import { AdvancedSearch, type FilterField } from '@/components/AdvancedSearch'
@@ -68,6 +73,26 @@ export function ExpensesView({
setFilteredExpenses(results)
}, [])
const pendingExpenses = useMemo(() =>
expenses.filter(e => e.status === 'pending'),
[expenses]
)
const approvedExpenses = useMemo(() =>
expenses.filter(e => e.status === 'approved'),
[expenses]
)
const totalPendingAmount = useMemo(() =>
pendingExpenses.reduce((sum, e) => sum + e.amount, 0),
[pendingExpenses]
)
const totalApprovedAmount = useMemo(() =>
approvedExpenses.reduce((sum, e) => sum + e.amount, 0),
[approvedExpenses]
)
const [formData, setFormData] = useState({
workerName: '',
clientName: '',
@@ -132,113 +157,113 @@ export function ExpensesView({
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-semibold tracking-tight">Expense Management</h2>
<p className="text-muted-foreground mt-1">Manage worker expenses and reimbursements</p>
</div>
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus size={18} className="mr-2" />
Create Expense
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Create New Expense</DialogTitle>
<DialogDescription>
Enter expense details for worker reimbursement or client billing
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="exp-worker">Worker Name</Label>
<Input
id="exp-worker"
placeholder="Enter worker name"
value={formData.workerName}
onChange={(e) => setFormData({ ...formData, workerName: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="exp-client">Client Name</Label>
<Input
id="exp-client"
placeholder="Enter client name"
value={formData.clientName}
onChange={(e) => setFormData({ ...formData, clientName: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="exp-date">Expense Date</Label>
<Input
id="exp-date"
type="date"
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="exp-category">Category</Label>
<Select
value={formData.category}
onValueChange={(value) => setFormData({ ...formData, category: value })}
>
<SelectTrigger id="exp-category">
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Travel">Travel</SelectItem>
<SelectItem value="Accommodation">Accommodation</SelectItem>
<SelectItem value="Meals">Meals</SelectItem>
<SelectItem value="Equipment">Equipment</SelectItem>
<SelectItem value="Training">Training</SelectItem>
<SelectItem value="Other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 col-span-2">
<Label htmlFor="exp-description">Description</Label>
<Textarea
id="exp-description"
placeholder="Describe the expense"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="exp-amount">Amount (£)</Label>
<Input
id="exp-amount"
type="number"
step="0.01"
placeholder="0.00"
value={formData.amount}
onChange={(e) => setFormData({ ...formData, amount: e.target.value })}
/>
</div>
<div className="space-y-2 flex items-end">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.billable}
onChange={(e) => setFormData({ ...formData, billable: e.target.checked })}
className="w-4 h-4"
<Stack spacing={6}>
<PageHeader
title="Expense Management"
description="Manage worker expenses and reimbursements"
actions={
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus size={18} className="mr-2" />
Create Expense
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Create New Expense</DialogTitle>
<DialogDescription>
Enter expense details for worker reimbursement or client billing
</DialogDescription>
</DialogHeader>
<Grid cols={2} gap={4} className="py-4">
<div className="space-y-2">
<Label htmlFor="exp-worker">Worker Name</Label>
<Input
id="exp-worker"
placeholder="Enter worker name"
value={formData.workerName}
onChange={(e) => setFormData({ ...formData, workerName: e.target.value })}
/>
<span className="text-sm">Billable to client</span>
</label>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)}>Cancel</Button>
<Button onClick={handleSubmitCreate}>Create Expense</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="exp-client">Client Name</Label>
<Input
id="exp-client"
placeholder="Enter client name"
value={formData.clientName}
onChange={(e) => setFormData({ ...formData, clientName: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="exp-date">Expense Date</Label>
<Input
id="exp-date"
type="date"
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="exp-category">Category</Label>
<Select
value={formData.category}
onValueChange={(value) => setFormData({ ...formData, category: value })}
>
<SelectTrigger id="exp-category">
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Travel">Travel</SelectItem>
<SelectItem value="Accommodation">Accommodation</SelectItem>
<SelectItem value="Meals">Meals</SelectItem>
<SelectItem value="Equipment">Equipment</SelectItem>
<SelectItem value="Training">Training</SelectItem>
<SelectItem value="Other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 col-span-2">
<Label htmlFor="exp-description">Description</Label>
<Textarea
id="exp-description"
placeholder="Describe the expense"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="exp-amount">Amount (£)</Label>
<Input
id="exp-amount"
type="number"
step="0.01"
placeholder="0.00"
value={formData.amount}
onChange={(e) => setFormData({ ...formData, amount: e.target.value })}
/>
</div>
<div className="space-y-2 flex items-end">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.billable}
onChange={(e) => setFormData({ ...formData, billable: e.target.checked })}
className="w-4 h-4"
/>
<span className="text-sm">Billable to client</span>
</label>
</div>
</Grid>
<Stack direction="horizontal" spacing={2} justify="end">
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)}>Cancel</Button>
<Button onClick={handleSubmitCreate}>Create Expense</Button>
</Stack>
</DialogContent>
</Dialog>
}
/>
<AdvancedSearch
items={expensesToFilter}
@@ -247,7 +272,32 @@ export function ExpensesView({
placeholder="Search expenses or use query language (e.g., category = Travel billable = true)"
/>
<div className="flex items-center gap-4">
<Grid cols={4} gap={4}>
<MetricCard
label="Pending Approval"
value={pendingExpenses.length}
description={`£${totalPendingAmount.toLocaleString()} total`}
icon={<ClockCounterClockwise size={24} />}
/>
<MetricCard
label="Approved"
value={approvedExpenses.length}
description={`£${totalApprovedAmount.toLocaleString()} total`}
icon={<CheckCircle size={24} className="text-success" />}
/>
<MetricCard
label="Rejected"
value={expenses.filter(e => e.status === 'rejected').length}
icon={<XCircle size={24} className="text-destructive" />}
/>
<MetricCard
label="Paid"
value={expenses.filter(e => e.status === 'paid').length}
icon={<CurrencyDollar size={24} />}
/>
</Grid>
<Stack direction="horizontal" spacing={4} align="center">
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as any)}>
<SelectTrigger className="w-40">
<div className="flex items-center gap-2">
@@ -267,7 +317,7 @@ export function ExpensesView({
<Download size={18} className="mr-2" />
Export
</Button>
</div>
</Stack>
<Tabs defaultValue="pending" className="space-y-4">
<TabsList>
@@ -340,7 +390,7 @@ export function ExpensesView({
onApprove={onApprove}
onReject={onReject}
/>
</div>
</Stack>
)
}
@@ -365,15 +415,15 @@ function ExpenseCard({ expense, onApprove, onReject, onViewDetails }: ExpenseCar
<Card className="hover:shadow-md transition-shadow cursor-pointer" onClick={() => onViewDetails?.(expense)}>
<CardContent className="p-6">
<div className="flex items-start justify-between">
<div className="space-y-3 flex-1">
<div className="flex items-start gap-4">
<Stack spacing={3} className="flex-1">
<Stack direction="horizontal" spacing={4} align="start">
<StatusIcon
size={24}
weight="fill"
className={statusConfig[expense.status].color}
/>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<Stack direction="horizontal" spacing={3} align="center" className="mb-2">
<h3 className="font-semibold text-lg">{expense.workerName}</h3>
<Badge variant={expense.status === 'approved' || expense.status === 'paid' ? 'success' : expense.status === 'rejected' ? 'destructive' : 'warning'}>
{expense.status}
@@ -381,8 +431,8 @@ function ExpenseCard({ expense, onApprove, onReject, onViewDetails }: ExpenseCar
{expense.billable && (
<Badge variant="outline">Billable</Badge>
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 text-sm">
</Stack>
<Grid cols={5} gap={4} className="text-sm">
<div>
<p className="text-muted-foreground">Client</p>
<p className="font-medium">{expense.clientName}</p>
@@ -403,7 +453,7 @@ function ExpenseCard({ expense, onApprove, onReject, onViewDetails }: ExpenseCar
<p className="text-muted-foreground">Currency</p>
<p className="font-medium font-mono">{expense.currency}</p>
</div>
</div>
</Grid>
{expense.description && (
<div className="mt-2 text-sm text-muted-foreground">
{expense.description}
@@ -413,10 +463,10 @@ function ExpenseCard({ expense, onApprove, onReject, onViewDetails }: ExpenseCar
Submitted {new Date(expense.submittedDate).toLocaleDateString()}
</div>
</div>
</div>
</div>
</Stack>
</Stack>
<div className="flex gap-2 ml-4" onClick={(e) => e.stopPropagation()}>
<Stack direction="horizontal" spacing={2} className="ml-4" onClick={(e) => e.stopPropagation()}>
{expense.status === 'pending' && onApprove && onReject && (
<>
<Button
@@ -443,7 +493,7 @@ function ExpenseCard({ expense, onApprove, onReject, onViewDetails }: ExpenseCar
View Receipt
</Button>
)}
</div>
</Stack>
</div>
</CardContent>
</Card>

View File

@@ -4,12 +4,19 @@ import {
CurrencyDollar,
Download,
ChartBar,
Calculator
Calculator,
Users,
CalendarBlank,
ClockCounterClockwise
} 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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { PageHeader } from '@/components/ui/page-header'
import { Grid } from '@/components/ui/grid'
import { Stack } from '@/components/ui/stack'
import { MetricCard } from '@/components/ui/metric-card'
import { PayrollDetailDialog } from '@/components/PayrollDetailDialog'
import { OneClickPayroll } from '@/components/OneClickPayroll'
import { usePayrollCalculations } from '@/hooks/use-payroll-calculations'
@@ -67,98 +74,98 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<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>
<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>
<Stack spacing={6}>
<PageHeader
title="Payroll Processing"
description="Manage payroll runs and worker payments"
actions={
<Stack direction="horizontal" spacing={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>
<Stack spacing={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>
)}
</div>
</DialogContent>
</Dialog>
<Button>
<Plus size={18} className="mr-2" />
Run Payroll
</Button>
</div>
</div>
<Button onClick={handleCalculate}>Calculate</Button>
{calculatorResult && (
<Stack spacing={3} className="border-t pt-4">
<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>
</Grid>
<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>
</Stack>
)}
</Stack>
</DialogContent>
</Dialog>
<Button>
<Plus size={18} className="mr-2" />
Run Payroll
</Button>
</Stack>
}
/>
<OneClickPayroll
timesheets={timesheets}
@@ -166,88 +173,55 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr
/>
{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>
<Grid cols={4} gap={4}>
<MetricCard
label="Approved Timesheets"
value={approvedTimesheets.length}
description="Ready for payroll"
icon={<Users size={24} />}
/>
<MetricCard
label="Pending Approval"
value={pendingTimesheets.length}
description={`£${totalPendingValue.toLocaleString()} value`}
icon={<ClockCounterClockwise size={24} />}
/>
<MetricCard
label="Total Payroll Runs"
value={payrollRuns.length}
icon={<CurrencyDollar size={24} />}
/>
<MetricCard
label="Last Run Total"
value={lastRun ? `£${lastRun.totalAmount.toLocaleString()}` : '£0'}
description={lastRun ? `${lastRun.workersCount} workers paid` : 'No runs yet'}
icon={<Download size={24} />}
/>
</Grid>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader>
<CardTitle className="text-sm text-muted-foreground">Next Pay Date</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold">22 Jan 2025</div>
<p className="text-sm text-muted-foreground mt-1">Weekly run in 3 days</p>
</CardContent>
</Card>
<Grid cols={3} gap={4}>
<MetricCard
label="Next Pay Date"
value="22 Jan 2025"
description="Weekly run in 3 days"
icon={<CalendarBlank size={24} />}
/>
<MetricCard
label="Pending Approval"
value={`${pendingTimesheets.length} timesheets`}
description="Must be approved for payroll"
icon={<ClockCounterClockwise size={24} />}
/>
<MetricCard
label="Last Run Total"
value={lastRun ? `£${lastRun.totalAmount.toLocaleString()}` : '£0'}
description={lastRun ? `${lastRun.workersCount} workers paid` : 'No runs yet'}
icon={<CurrencyDollar size={24} />}
/>
</Grid>
<Card>
<CardHeader>
<CardTitle className="text-sm text-muted-foreground">Pending Approval</CardTitle>
</CardHeader>
<CardContent>
<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>
<Card>
<CardHeader>
<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="space-y-3">
<Stack spacing={3}>
{payrollRuns.map(run => (
<Card
key={run.id}
@@ -256,15 +230,15 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr
>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="space-y-2 flex-1">
<div className="flex items-center gap-3">
<Stack spacing={2} className="flex-1">
<Stack direction="horizontal" spacing={3} align="center">
<CurrencyDollar size={20} weight="fill" className="text-primary" />
<h3 className="font-semibold text-lg">Payroll Run</h3>
<Badge variant={run.status === 'completed' ? 'success' : run.status === 'failed' ? 'destructive' : 'warning'}>
{run.status}
</Badge>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
</Stack>
<Grid cols={4} gap={4} className="text-sm">
<div>
<p className="text-muted-foreground">Period Ending</p>
<p className="font-medium">{new Date(run.periodEnding).toLocaleDateString()}</p>
@@ -283,9 +257,9 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr
{run.processedDate ? new Date(run.processedDate).toLocaleDateString() : 'Not yet'}
</p>
</div>
</div>
</div>
<div className="flex gap-2 ml-4" onClick={(e) => e.stopPropagation()}>
</Grid>
</Stack>
<Stack direction="horizontal" spacing={2} className="ml-4" onClick={(e) => e.stopPropagation()}>
<Button size="sm" variant="outline" onClick={() => setViewingPayroll(run)}>
View Details
</Button>
@@ -295,7 +269,7 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr
Export
</Button>
)}
</div>
</Stack>
</div>
</CardContent>
</Card>
@@ -308,7 +282,7 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr
<p className="text-muted-foreground">Create your first payroll run to get started</p>
</Card>
)}
</div>
</Stack>
<PayrollDetailDialog
payrollRun={viewingPayroll}
@@ -317,6 +291,6 @@ export function PayrollView({ payrollRuns, timesheets, onPayrollComplete }: Payr
if (!open) setViewingPayroll(null)
}}
/>
</div>
</Stack>
)
}