mirror of
https://github.com/johndoe6345789/workforce-pay-bill-p.git
synced 2026-04-24 13:24:57 +00:00
Generated by Spark: Update remaining views (Payroll, Compliance, Expenses) to use PageHeader, Grid, Stack, and MetricCard components
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user