Contractor Timesheet Approval
blockfinanceDeel/Remote style international contractor invoice approval, hourly timesheet verification against GitHub PRs & deliverables, multi-currency payout calculation, overview KPI metrics, and itemized daily timesheet slide-over drawer.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/contractor-timesheet-approval.json$npx shadcn@latest add https://uipkge.dev/r/react/contractor-timesheet-approval.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/contractor-timesheet-approval.json$bunx shadcn@latest add https://uipkge.dev/r/react/contractor-timesheet-approval.jsonNamed registry:
npx shadcn@latest add @uipkge-react/contractor-timesheet-approvalInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
title | string | — | optional |
subtitle | string | — | optional |
payCycle | string | — | optional |
initialData | ContractorSubmission[] | — | optional |
className | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
DailyTimesheetEntryinterface DailyTimesheetEntry {
id: string
date: string
day: string
hours: number
regularHours: number
overtimeHours: number
task: string
deliverableType: DeliverableType
referenceId: string
referenceLabel: string
commitCount?: number
verified: boolean
}ContractorSubmissioninterface ContractorSubmission {
id: string
name: string
initials: string
avatarUrl?: string
role: string
country: string
countryCode: string
flag: string
city: string
contractType: string
hourlyRateUsd: number
hourlyRateLocal: number
currencyCode: string
currencySymbol: string
fxRate: number
hoursLogged: number
regularHours: number
overtimeHours: number
grossAmountLocal: number
grossAmountUsd: number
verificationBadge: {
label: string
subtext: string
status: 'verified' | 'manual'
}
status: ApprovalStatus
invoiceNumber: string
submittedAt: string
payPeriod: string
taxCompliance: {
w8benStatus: 'valid' | 'pending' | 'exempt'
taxIdMasked: string
jurisdiction: string
}
paymentMethod: {
type: 'SWIFT' | 'SEPA' | 'Local Wire' | 'ACH'
accountMasked: string
bankName: string
}
dailyEntries: DailyTimesheetEntry[]
}npm dependencies
Files installed (6)
components/blocks/ContractorTimesheetApproval.tsx17.1 kB
'use client' import * as React from 'react' import { Calendar, Check, CheckCheck, CheckCircle2, Clock, FileSpreadsheet, Globe, RotateCcw, Search, } from 'lucide-react' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { Separator } from '@/components/ui/separator' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { TimesheetDetailSheet } from './TimesheetDetailSheet' import { TimesheetKpiCards } from './TimesheetKpiCards' import { TimesheetTableRow } from './TimesheetTableRow' import type { ApprovalStatus, ContractorSubmission, DailyTimesheetEntry, DeliverableType } from './timesheet-types' import { initialSubmissions } from './timesheet-data' export type { ApprovalStatus, ContractorSubmission, DailyTimesheetEntry, DeliverableType } export interface ContractorTimesheetApprovalProps { title?: string subtitle?: string payCycle?: string initialData?: ContractorSubmission[] className?: string } function formatUsd(val: number): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(val) } function formatLocal(val: number, currencyCode: string, currencySymbol: string): string { if (currencyCode === 'JPY') { return `${currencySymbol}${new Intl.NumberFormat('ja-JP').format(val)}` } return `${currencySymbol}${new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(val)} ${currencyCode}` } export function ContractorTimesheetApproval({ title = 'Contractor Invoices & Timesheets', subtitle = 'Review international contractor hours, verify commit logs & deliverables, and execute multi-currency invoice approvals.', payCycle = 'August 2026 · Bi-Weekly #2', initialData = initialSubmissions, className, }: ContractorTimesheetApprovalProps) { const [submissions, setSubmissions] = React.useState<ContractorSubmission[]>(initialData) const [searchQuery, setSearchQuery] = React.useState('') const [selectedStatus, setSelectedStatus] = React.useState<'all' | 'pending' | 'approved' | 'disputed'>('all') const [currencyDisplay, setCurrencyDisplay] = React.useState<'both' | 'usd' | 'local'>('both') const [selectedContractorId, setSelectedContractorId] = React.useState<string | null>(null) const [isDrawerOpen, setIsDrawerOpen] = React.useState(false) const [isApprovingAll, setIsApprovingAll] = React.useState(false) const [bulkApprovalDone, setBulkApprovalDone] = React.useState(false) const pendingSubmissions = React.useMemo(() => submissions.filter((s) => s.status === 'pending'), [submissions]) const pendingCount = pendingSubmissions.length const pendingAmountUsd = React.useMemo( () => pendingSubmissions.reduce((acc, curr) => acc + curr.grossAmountUsd, 0), [pendingSubmissions], ) const approvedSubmissions = React.useMemo(() => submissions.filter((s) => s.status === 'approved'), [submissions]) const approvedCount = approvedSubmissions.length const disputedSubmissions = React.useMemo(() => submissions.filter((s) => s.status === 'disputed'), [submissions]) const disputedCount = disputedSubmissions.length const totalHoursLogged = React.useMemo( () => submissions.reduce((acc, curr) => acc + curr.hoursLogged, 0), [submissions], ) const scheduledPayoutsTotal = 42800.0 const filteredSubmissions = React.useMemo(() => { return submissions.filter((item) => { const matchesStatus = selectedStatus === 'all' ? true : item.status === selectedStatus const query = searchQuery.trim().toLowerCase() if (!query) return matchesStatus const matchesSearch = item.name.toLowerCase().includes(query) || item.role.toLowerCase().includes(query) || item.country.toLowerCase().includes(query) || item.city.toLowerCase().includes(query) || item.invoiceNumber.toLowerCase().includes(query) || item.verificationBadge.label.toLowerCase().includes(query) return matchesStatus && matchesSearch }) }, [submissions, selectedStatus, searchQuery]) const selectedContractor = React.useMemo( () => submissions.find((c) => c.id === selectedContractorId), [submissions, selectedContractorId], ) const openDrawer = (contractor: ContractorSubmission) => { setSelectedContractorId(contractor.id) setIsDrawerOpen(true) } const approveInvoice = (id: string) => { setSubmissions((prev) => prev.map((item) => (item.id === id ? { ...item, status: 'approved' } : item))) } const disputeInvoice = (id: string) => { setSubmissions((prev) => prev.map((item) => (item.id === id ? { ...item, status: 'disputed' } : item))) } const handleApproveDrawer = () => { if (selectedContractorId) approveInvoice(selectedContractorId) setIsDrawerOpen(false) } const handleDisputeDrawer = () => { if (selectedContractorId) disputeInvoice(selectedContractorId) setIsDrawerOpen(false) } const approveAllPending = () => { if (pendingCount === 0) return setIsApprovingAll(true) setTimeout(() => { setSubmissions((prev) => prev.map((item) => (item.status === 'pending' ? { ...item, status: 'approved' } : item))) setIsApprovingAll(false) setBulkApprovalDone(true) setTimeout(() => { setBulkApprovalDone(false) }, 3000) }, 400) } const resetData = () => { setSubmissions(JSON.parse(JSON.stringify(initialData))) setSelectedStatus('all') setSearchQuery('') setBulkApprovalDone(false) } return ( <div data-slot="contractor-timesheet-approval" className={cn('text-foreground w-full space-y-6', className)}> {/* Header Section */} <div className="border-border/80 flex flex-col gap-4 border-b pb-6 lg:flex-row lg:items-center lg:justify-between"> <div className="space-y-1.5"> <div className="flex flex-wrap items-center gap-2"> <Badge variant="outline" className="border-border text-muted-foreground text-xs font-medium"> <Globe className="text-primary mr-1 size-3" /> Global Workforce · HRMS </Badge> <div className="text-muted-foreground flex items-center gap-1.5 text-xs font-medium"> <Calendar className="text-muted-foreground size-3.5" /> <span>Pay Cycle:</span> <span className="text-foreground font-semibold">{payCycle}</span> </div> {pendingCount > 0 ? ( <Badge variant="outline" className="border-warning/30 bg-warning/10 text-warning text-xs font-medium"> <span className="bg-warning mr-1.5 size-1.5 animate-pulse rounded-full" /> {pendingCount} Invoices Pending Review </Badge> ) : ( <Badge variant="outline" className="border-success/30 bg-success/10 text-success text-xs font-medium"> <CheckCircle2 className="text-success mr-1 size-3" /> All Invoices Reviewed </Badge> )} </div> <h1 className="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">{title}</h1> <p className="text-muted-foreground max-w-3xl text-sm leading-relaxed">{subtitle}</p> </div> {/* Action Buttons */} <div className="flex flex-wrap items-center gap-2.5 pt-1 lg:pt-0"> <Button variant="outline" size="sm" className="border-border hover:bg-muted h-9 gap-1.5 text-xs font-medium shadow-xs" onClick={resetData} > <RotateCcw className="text-muted-foreground size-3.5" /> Reset Demo </Button> <Button variant="default" size="sm" disabled={pendingCount === 0 || isApprovingAll} className="h-9 gap-2 text-xs font-medium shadow-xs" onClick={approveAllPending} > {isApprovingAll ? ( <> <Clock className="size-3.5 animate-spin" /> Processing Batch... </> ) : bulkApprovalDone ? ( <> <Check className="text-success size-3.5" /> Batch Approved! </> ) : ( <> <CheckCheck className="size-3.5" /> <span>Approve All Pending ({formatUsd(pendingAmountUsd)})</span> </> )} </Button> </div> </div> {/* 4 Billing Overview KPI Cards */} <TimesheetKpiCards pendingAmountUsd={pendingAmountUsd} pendingCount={pendingCount} totalHoursLogged={totalHoursLogged} scheduledPayoutsTotal={scheduledPayoutsTotal} formatUsd={formatUsd} /> {/* Filter Toolbar */} <Card className="border-border bg-card shadow-xs"> <CardContent className="p-4"> <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> {/* Search input */} <div className="relative max-w-md flex-1"> <Search className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" /> <input value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} type="text" placeholder="Search contractor, role, country, or PR..." className="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border py-1.5 pr-3 pl-9 text-xs focus-visible:ring-2 focus-visible:outline-hidden" /> </div> {/* Filter buttons & Currency Toggle */} <div className="flex flex-wrap items-center gap-2"> {/* Status filter tabs */} <div className="border-border bg-muted/40 inline-flex rounded-lg border p-0.5 text-xs"> <button type="button" className={cn( 'rounded-md px-2.5 py-1 font-medium transition-colors', selectedStatus === 'all' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setSelectedStatus('all')} > All ({submissions.length}) </button> <button type="button" className={cn( 'rounded-md px-2.5 py-1 font-medium transition-colors', selectedStatus === 'pending' ? 'bg-background text-warning text-warning shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setSelectedStatus('pending')} > Pending ({pendingCount}) </button> <button type="button" className={cn( 'rounded-md px-2.5 py-1 font-medium transition-colors', selectedStatus === 'approved' ? 'bg-background text-success text-success shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setSelectedStatus('approved')} > Approved ({approvedCount}) </button> <button type="button" className={cn( 'rounded-md px-2.5 py-1 font-medium transition-colors', selectedStatus === 'disputed' ? 'bg-background text-destructive text-destructive shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setSelectedStatus('disputed')} > Disputed ({disputedCount}) </button> </div> <Separator orientation="vertical" className="hidden h-6 md:block" /> {/* Currency toggle selector */} <div className="border-border bg-muted/40 inline-flex rounded-lg border p-0.5 text-xs"> <button type="button" className={cn( 'rounded-md px-2 py-1 font-medium transition-colors', currencyDisplay === 'both' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setCurrencyDisplay('both')} > Dual FX </button> <button type="button" className={cn( 'rounded-md px-2 py-1 font-medium transition-colors', currencyDisplay === 'usd' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setCurrencyDisplay('usd')} > USD ($) </button> <button type="button" className={cn( 'rounded-md px-2 py-1 font-medium transition-colors', currencyDisplay === 'local' ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', )} onClick={() => setCurrencyDisplay('local')} > Local </button> </div> </div> </div> </CardContent> </Card> {/* Contractor Invoices & Timesheets Table */} <Card className="border-border bg-card overflow-hidden shadow-xs"> <div className="overflow-x-auto"> <Table> <TableHeader className="bg-muted/40"> <TableRow className="border-border hover:bg-transparent"> <TableHead className="text-muted-foreground min-w-[240px] text-xs font-semibold tracking-wider uppercase"> Contractor & Location </TableHead> <TableHead className="text-muted-foreground min-w-[180px] text-xs font-semibold tracking-wider uppercase"> Role & Rate </TableHead> <TableHead className="text-muted-foreground min-w-[140px] text-xs font-semibold tracking-wider uppercase"> Hours Logged </TableHead> <TableHead className="text-muted-foreground min-w-[200px] text-xs font-semibold tracking-wider uppercase"> Invoice Gross Amount </TableHead> <TableHead className="text-muted-foreground min-w-[220px] text-xs font-semibold tracking-wider uppercase"> Timesheet Verification </TableHead> <TableHead className="text-muted-foreground min-w-[120px] text-xs font-semibold tracking-wider uppercase"> Status </TableHead> <TableHead className="text-muted-foreground min-w-[180px] text-right text-xs font-semibold tracking-wider uppercase"> Actions </TableHead> </TableRow> </TableHeader> <TableBody> {filteredSubmissions.length > 0 ? ( filteredSubmissions.map((contractor) => ( <TimesheetTableRow key={contractor.id} contractor={contractor} currencyDisplay={currencyDisplay} formatUsd={formatUsd} formatLocal={formatLocal} onOpenDrawer={openDrawer} onApproveInvoice={approveInvoice} onDisputeInvoice={disputeInvoice} /> )) ) : ( <TableRow> <TableCell colSpan={7} className="h-36 text-center"> <div className="flex flex-col items-center justify-center gap-1.5"> <FileSpreadsheet className="text-muted-foreground/50 size-8" /> <p className="text-foreground text-sm font-medium">No invoices match your filter</p> <p className="text-muted-foreground text-xs"> Try adjusting your search query or switching the status filter tab. </p> </div> </TableCell> </TableRow> )} </TableBody> </Table> </div> </Card> {/* Timesheet Detail Drawer / Slide-Over Component */} <TimesheetDetailSheet open={isDrawerOpen} onOpenChange={setIsDrawerOpen} selectedContractor={selectedContractor} formatUsd={formatUsd} formatLocal={formatLocal} onApprove={handleApproveDrawer} onDispute={handleDisputeDrawer} /> </div> ) }components/blocks/TimesheetDetailSheet.tsx14 kB
'use client' import { Check, CheckCircle2, FileCheck, FileCode, GitPullRequest, ShieldCheck, XCircle } from 'lucide-react' import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet' import type { ContractorSubmission } from './timesheet-types' export interface TimesheetDetailSheetProps { open: boolean onOpenChange: (open: boolean) => void selectedContractor: ContractorSubmission | undefined formatUsd: (val: number) => string formatLocal: (val: number, currencyCode: string, currencySymbol: string) => string onApprove?: () => void onDispute?: () => void onApproveInvoice?: (id: string) => void onDisputeInvoice?: (id: string) => void } export function TimesheetDetailSheet({ open, onOpenChange, selectedContractor, formatUsd, formatLocal, onApprove, onDispute, onApproveInvoice, onDisputeInvoice, }: TimesheetDetailSheetProps) { return ( <Sheet open={open} onOpenChange={onOpenChange}> <SheetContent side="right" className="bg-background text-foreground flex w-full flex-col overflow-y-auto p-0 sm:max-w-xl md:max-w-2xl" > {selectedContractor && ( <> {/* Drawer Header */} <SheetHeader className="border-border/80 space-y-3 border-b px-6 py-5 text-left"> <div className="flex items-start justify-between gap-4"> <div className="flex items-center gap-3"> <Avatar className="border-border size-12 border"> <AvatarFallback className="bg-primary/10 text-primary text-sm font-bold"> {selectedContractor.initials} </AvatarFallback> </Avatar> <div> <div className="flex items-center gap-2"> <SheetTitle className="text-foreground text-lg font-bold">{selectedContractor.name}</SheetTitle> <span className="text-base" title={selectedContractor.country}> {selectedContractor.flag} </span> </div> <SheetDescription className="text-muted-foreground text-xs"> {selectedContractor.role} · {selectedContractor.city} </SheetDescription> </div> </div> {/* Status Badge in Header */} {selectedContractor.status === 'pending' ? ( <Badge variant="outline" className="border-warning/30 bg-warning/10 text-warning shrink-0 text-xs font-medium" > <span className="bg-warning mr-1.5 size-1.5 rounded-full" /> Pending Review </Badge> ) : selectedContractor.status === 'approved' ? ( <Badge variant="outline" className="border-success/30 bg-success/10 text-success shrink-0 text-xs font-medium" > <Check className="text-success mr-1 size-3" /> Approved </Badge> ) : ( <Badge variant="outline" className="border-destructive/30 bg-destructive/10 text-destructive shrink-0 text-xs font-medium" > <XCircle className="text-destructive mr-1 size-3" /> Disputed </Badge> )} </div> {/* Header Quick Stats Grid */} <div className="grid grid-cols-3 gap-2 pt-2 text-xs"> <div className="border-border bg-muted/40 rounded-lg border p-2.5"> <span className="text-muted-foreground block">Invoice Gross</span> <span className="text-foreground text-sm font-bold tabular-nums"> {formatUsd(selectedContractor.grossAmountUsd)} </span> <span className="text-muted-foreground/70 block text-xs tabular-nums"> {formatLocal( selectedContractor.grossAmountLocal, selectedContractor.currencyCode, selectedContractor.currencySymbol, )} </span> </div> <div className="border-border bg-muted/40 rounded-lg border p-2.5"> <span className="text-muted-foreground block">Total Hours</span> <span className="text-foreground text-sm font-bold tabular-nums"> {selectedContractor.hoursLogged.toFixed(1)} hrs </span> <span className="text-success text-success block text-xs font-medium tabular-nums"> {selectedContractor.regularHours.toFixed(0)}h reg · {selectedContractor.overtimeHours.toFixed(0)}h ot </span> </div> <div className="border-border bg-muted/40 rounded-lg border p-2.5"> <span className="text-muted-foreground block">Contract Rate</span> <span className="text-foreground text-sm font-bold tabular-nums"> {formatUsd(selectedContractor.hourlyRateUsd)}/hr </span> <span className="text-muted-foreground/70 block truncate text-xs"> {selectedContractor.taxCompliance.jurisdiction} </span> </div> </div> </SheetHeader> {/* Drawer Body: Timesheet Breakdown */} <div className="flex-1 space-y-6 overflow-y-auto px-6 py-5"> {/* Verification Callout Banner */} <div className="border-success/20 bg-success/10 text-success flex items-start gap-3 rounded-lg border p-3.5 text-xs"> <ShieldCheck className="text-success mt-0.5 size-4 shrink-0" /> <div className="space-y-0.5"> <p className="text-success font-semibold">{selectedContractor.verificationBadge.label}</p> <p className="text-success leading-normal"> {selectedContractor.verificationBadge.subtext}. Automated hash verification confirmed timestamps match developer activity during the billing window. </p> </div> </div> {/* Daily Task Itemization */} <div className="space-y-3"> <div className="flex items-center justify-between gap-x-2"> <h2 className="text-muted-foreground text-xs font-semibold"> Daily Itemized Activity Log ({selectedContractor.dailyEntries.length} Work Days) </h2> <span className="text-muted-foreground font-mono text-xs">{selectedContractor.payPeriod}</span> </div> <div className="space-y-2.5"> {selectedContractor.dailyEntries.map((entry) => ( <div key={entry.id} className="border-border bg-card hover:border-border/80 space-y-2 rounded-lg border p-3.5 transition-colors" > <div className="flex items-center justify-between gap-x-2"> <div className="flex items-center gap-2"> <span className="text-foreground text-xs font-semibold">{entry.day}</span> <Badge variant="outline" className="border-border text-muted-foreground h-4 py-0 text-xs"> {entry.date} </Badge> </div> <div className="flex items-center gap-2"> <span className="text-foreground text-xs font-bold tabular-nums"> {entry.hours.toFixed(1)} hrs </span> <span className="text-muted-foreground text-xs tabular-nums"> ({formatUsd(entry.hours * selectedContractor.hourlyRateUsd)}) </span> </div> </div> <p className="text-muted-foreground text-xs leading-relaxed">{entry.task}</p> {/* Linked Deliverables (PR / Commit / Figma / CI) */} <div className="flex flex-wrap items-center gap-2 pt-1"> <div className="border-border bg-muted/50 text-foreground inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 font-mono text-xs"> {entry.deliverableType === 'github_pr' ? ( <> <GitPullRequest className="text-chart-1 size-3" /> <span>{entry.referenceId}</span> <span className="text-muted-foreground">({entry.referenceLabel})</span> {entry.commitCount && ( <span className="text-muted-foreground">· {entry.commitCount} commits</span> )} </> ) : entry.deliverableType === 'figma' ? ( <> <FileCode className="text-chart-2 size-3" /> <span>{entry.referenceId}</span> <span className="text-muted-foreground">({entry.referenceLabel})</span> </> ) : ( <> <CheckCircle2 className="text-success size-3" /> <span>{entry.referenceId}</span> <span className="text-muted-foreground">({entry.referenceLabel})</span> </> )} </div> <Badge variant="outline" className="border-success/20 bg-success/10 text-success h-5 gap-1 py-0 text-xs" > <Check className="size-2.5" /> Verified </Badge> </div> </div> ))} </div> </div> {/* Tax & Bank Payout Compliance Box */} <div className="border-border bg-muted/20 space-y-2.5 rounded-lg border p-4 text-xs"> <h3 className="text-foreground flex items-center gap-1.5 font-bold"> <FileCheck className="text-primary size-3.5" /> Compliance & Disbursement Routing </h3> <div className="grid grid-cols-2 gap-3 pt-1"> <div> <span className="text-muted-foreground block">Tax Form Status:</span> <span className="text-foreground font-medium"> Form W-8BEN ({selectedContractor.taxCompliance.w8benStatus.toUpperCase()}) </span> <span className="text-muted-foreground block font-mono text-xs"> {selectedContractor.taxCompliance.taxIdMasked} </span> </div> <div> <span className="text-muted-foreground block">Disbursement Route:</span> <span className="text-foreground font-medium">{selectedContractor.paymentMethod.type} Payout</span> <span className="text-muted-foreground block text-xs"> {selectedContractor.paymentMethod.bankName} </span> </div> </div> </div> </div> {/* Drawer Footer Actions */} <SheetFooter className="border-border/80 bg-muted/20 flex flex-row items-center justify-between gap-3 border-t px-6 py-4"> <SheetClose asChild> <Button variant="outline" size="sm" className="text-xs"> Close </Button> </SheetClose> <div className="flex items-center gap-2"> {selectedContractor.status !== 'disputed' && ( <Button variant="outline" size="sm" className="border-destructive/30 text-destructive hover:bg-destructive/10 text-destructive text-xs" onClick={() => { if (onDispute) onDispute() else if (onDisputeInvoice) onDisputeInvoice(selectedContractor.id) onOpenChange(false) }} > <XCircle className="mr-1.5 size-3.5" /> Dispute Invoice </Button> )} {selectedContractor.status !== 'approved' ? ( <Button variant="default" size="sm" className="text-xs shadow-xs" onClick={() => { if (onApprove) onApprove() else if (onApproveInvoice) onApproveInvoice(selectedContractor.id) onOpenChange(false) }} > <Check className="mr-1.5 size-3.5" /> Approve ({formatUsd(selectedContractor.grossAmountUsd)}) </Button> ) : ( <span className="text-success flex items-center gap-1 text-xs font-semibold"> <CheckCircle2 className="size-4" /> Approved & Queued </span> )} </div> </SheetFooter> </> )} </SheetContent> </Sheet> ) }components/blocks/TimesheetKpiCards.tsx5.3 kB
'use client' import * as React from 'react' import { Clock, ShieldCheck, Timer, Users } from 'lucide-react' import { cn } from '@/lib/utils' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' export interface TimesheetKpiCardsProps { pendingAmountUsd: number pendingCount: number totalHoursLogged: number scheduledPayoutsTotal: number formatUsd: (val: number) => string className?: string } export function TimesheetKpiCards({ pendingAmountUsd, pendingCount, totalHoursLogged, scheduledPayoutsTotal, formatUsd, className, }: TimesheetKpiCardsProps) { return ( <div className={cn('grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4', className)}> {/* Card 1: Pending Approval Amount */} <Card className="border-border bg-card shadow-xs transition-colors"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-muted-foreground text-xs font-medium"> Pending Approval Amount </CardTitle> <div className="border-warning/20 bg-warning/10 text-warning flex size-8 items-center justify-center rounded-lg border"> <Clock className="size-4" /> </div> </CardHeader> <CardContent className="space-y-1"> <div className="text-foreground text-2xl font-bold tracking-tight tabular-nums"> {formatUsd(pendingAmountUsd)} </div> <p className="text-muted-foreground text-xs"> <span className="text-warning text-warning font-semibold tabular-nums">{pendingCount} invoices</span>{' '} awaiting your approval </p> <div className="pt-2"> <span className="bg-muted text-muted-foreground inline-flex items-center rounded-md px-2 py-0.5 text-xs tabular-nums"> £3,800 · ¥810k · R$41k </span> </div> </CardContent> </Card> {/* Card 2: Total Contractor Hours */} <Card className="border-border bg-card shadow-xs transition-colors"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-muted-foreground text-xs font-medium"> Total Contractor Hours </CardTitle> <div className="border-info/20 bg-info/10 text-info flex size-8 items-center justify-center rounded-lg border"> <Timer className="size-4" /> </div> </CardHeader> <CardContent className="space-y-1"> <div className="text-foreground text-2xl font-bold tracking-tight tabular-nums"> {totalHoursLogged.toFixed(1)} hrs </div> <p className="text-muted-foreground text-xs"> <span className="text-foreground font-medium tabular-nums">160.0 regular</span> · 80.0 overtime / project </p> <div className="pt-2"> <span className="bg-info/10 text-info inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium"> 100% Timesheets submitted </span> </div> </CardContent> </Card> {/* Card 3: Approved & Scheduled Payouts */} <Card className="border-border bg-card shadow-xs transition-colors"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-muted-foreground text-xs font-medium"> Approved & Scheduled </CardTitle> <div className="border-success/20 bg-success/10 text-success flex size-8 items-center justify-center rounded-lg border"> <ShieldCheck className="size-4" /> </div> </CardHeader> <CardContent className="space-y-1"> <div className="text-foreground text-2xl font-bold tracking-tight tabular-nums"> {formatUsd(scheduledPayoutsTotal)} </div> <p className="text-muted-foreground text-xs"> <span className="text-success text-success font-medium tabular-nums"> 5 invoices </span> batched for Aug 25 payout </p> <div className="pt-2"> <span className="bg-muted text-muted-foreground inline-flex items-center rounded-md px-2 py-0.5 text-xs"> Direct SEPA · ACH · SWIFT </span> </div> </CardContent> </Card> {/* Card 4: Active International Contractors */} <Card className="border-border bg-card shadow-xs transition-colors"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-muted-foreground text-xs font-medium"> Active Contractors </CardTitle> <div className="border-chart-1/20 bg-chart-1/10 text-chart-1 flex size-8 items-center justify-center rounded-lg border"> <Users className="size-4" /> </div> </CardHeader> <CardContent className="space-y-1"> <div className="text-foreground text-2xl font-bold tracking-tight tabular-nums">8 Contractors</div> <p className="text-muted-foreground text-xs">Across 5 countries (UK, JP, BR, FR, CA)</p> <div className="pt-2"> <span className="bg-chart-1/10 text-chart-1 inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium"> 100% W-8BEN Tax Valid </span> </div> </CardContent> </Card> </div> ) }components/blocks/TimesheetTableRow.tsx10.1 kB
'use client' import * as React from 'react' import { AlertCircle, Check, CheckCircle2, Download, Eye, FileText, MoreHorizontal, ShieldCheck, XCircle, } from 'lucide-react' import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { TableCell, TableRow } from '@/components/ui/table' import type { ContractorSubmission } from './timesheet-types' export interface TimesheetTableRowProps { contractor: ContractorSubmission currencyDisplay: 'both' | 'usd' | 'local' formatUsd: (val: number) => string formatLocal: (val: number, currencyCode: string, currencySymbol: string) => string onOpenDrawer: (contractor: ContractorSubmission) => void onApproveInvoice: (id: string) => void onDisputeInvoice: (id: string) => void } export function TimesheetTableRow({ contractor, currencyDisplay, formatUsd, formatLocal, onOpenDrawer, onApproveInvoice, onDisputeInvoice, }: TimesheetTableRowProps) { return ( <TableRow className="border-border hover:bg-muted/30 transition-colors"> {/* Contractor & Location */} <TableCell className="py-4"> <div className="flex items-center gap-3"> <Avatar className="border-border size-10 shrink-0 border"> <AvatarFallback className="bg-primary/10 text-primary text-xs font-semibold"> {{ contractor: contractor.initials }.contractor} </AvatarFallback> </Avatar> <div className="space-y-0.5"> <div className="flex items-center gap-1.5"> <button type="button" className="text-foreground focus-visible:ring-ring cursor-pointer rounded-xs text-left text-sm font-semibold hover:underline focus-visible:ring-2 focus-visible:outline-none" onClick={() => onOpenDrawer(contractor)} > {contractor.name} </button> <span className="text-sm" title={contractor.country}> {contractor.flag} </span> </div> <p className="text-muted-foreground flex items-center gap-1 text-xs"> <span>{contractor.city}</span> <span>•</span> <span className="text-muted-foreground/80 font-mono">{contractor.invoiceNumber}</span> </p> </div> </div> </TableCell> {/* Role & Rate */} <TableCell className="py-4"> <div className="space-y-0.5"> <p className="text-foreground text-xs font-medium">{contractor.role}</p> <p className="text-muted-foreground text-xs tabular-nums"> {formatUsd(contractor.hourlyRateUsd)}/hr{' '} <span className="text-muted-foreground/70"> ({formatLocal(contractor.hourlyRateLocal, contractor.currencyCode, contractor.currencySymbol)}/hr) </span> </p> </div> </TableCell> {/* Hours Logged */} <TableCell className="py-4"> <div className="space-y-1"> <div className="flex items-center gap-1.5"> <span className="text-foreground text-sm font-bold tabular-nums"> {contractor.hoursLogged.toFixed(1)} hrs </span> </div> <div className="text-muted-foreground flex items-center gap-1 text-xs tabular-nums"> <span className="text-success font-medium">{contractor.regularHours.toFixed(0)}h reg</span> {contractor.overtimeHours > 0 && ( <> <span>+</span> <span className="text-info font-medium">{contractor.overtimeHours.toFixed(0)}h ot</span> </> )} </div> </div> </TableCell> {/* Invoice Gross (Local & USD) */} <TableCell className="py-4"> <div className="space-y-1"> {currencyDisplay === 'both' ? ( <> <div className="flex items-baseline gap-1.5"> <span className="text-foreground text-sm font-bold tabular-nums"> {formatUsd(contractor.grossAmountUsd)} </span> <span className="text-muted-foreground text-xs font-normal">USD</span> </div> <p className="text-muted-foreground text-xs tabular-nums"> {formatLocal(contractor.grossAmountLocal, contractor.currencyCode, contractor.currencySymbol)}{' '} <span className="text-muted-foreground/60 text-xs">(FX: {contractor.fxRate.toFixed(4)})</span> </p> </> ) : currencyDisplay === 'usd' ? ( <> <div className="text-foreground text-sm font-bold tabular-nums"> {formatUsd(contractor.grossAmountUsd)} USD </div> <p className="text-muted-foreground text-xs tabular-nums"> Rate: {formatUsd(contractor.hourlyRateUsd)}/hr </p> </> ) : ( <> <div className="text-foreground text-sm font-bold tabular-nums"> {formatLocal(contractor.grossAmountLocal, contractor.currencyCode, contractor.currencySymbol)} </div> <p className="text-muted-foreground text-xs tabular-nums"> Conv: {formatUsd(contractor.grossAmountUsd)} USD </p> </> )} </div> </TableCell> {/* Timesheet Verification Status */} <TableCell className="py-4"> <div className="space-y-1"> <Badge variant="outline" className="border-success/25 bg-success/10 text-success gap-1 text-xs font-medium"> <CheckCircle2 className="text-success size-3" /> {contractor.verificationBadge.label} </Badge> <p className="text-muted-foreground max-w-[200px] truncate text-xs" title={contractor.verificationBadge.subtext} > {contractor.verificationBadge.subtext} </p> </div> </TableCell> {/* Approval Status */} <TableCell className="py-4"> {contractor.status === 'pending' ? ( <Badge variant="outline" className="border-warning/30 bg-warning/10 text-warning text-xs font-medium"> <span className="bg-warning mr-1.5 size-1.5 rounded-full" /> Pending Review </Badge> ) : contractor.status === 'approved' ? ( <Badge variant="outline" className="border-success/30 bg-success/10 text-success text-xs font-medium"> <Check className="text-success mr-1 size-3" /> Approved </Badge> ) : ( <Badge variant="outline" className="border-destructive/30 bg-destructive/10 text-destructive text-xs font-medium" > <XCircle className="text-destructive mr-1 size-3" /> Disputed </Badge> )} </TableCell> {/* Action Buttons */} <TableCell className="py-4 text-right"> <div className="flex items-center justify-end gap-1.5"> <Button variant="outline" size="sm" className="border-border hover:bg-muted h-8 gap-1 px-2.5 text-xs font-medium" onClick={() => onOpenDrawer(contractor)} > <Eye className="text-muted-foreground size-3.5" /> Timesheet </Button> {contractor.status === 'pending' && ( <Button variant="default" size="sm" className="h-8 px-2.5 text-xs font-medium shadow-xs" onClick={() => onApproveInvoice(contractor.id)} > <Check className="mr-1 size-3.5" /> Approve </Button> )} {/* Dropdown Menu for Secondary Actions */} <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground size-8"> <MoreHorizontal className="size-4" /> <span className="sr-only">Open contractor menu</span> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-48"> <DropdownMenuLabel className="text-muted-foreground text-xs"> Invoice {contractor.invoiceNumber} </DropdownMenuLabel> <DropdownMenuItem className="cursor-pointer text-xs" onClick={() => onOpenDrawer(contractor)}> <FileText className="text-muted-foreground mr-2 size-3.5" /> Itemized Timesheet </DropdownMenuItem> {contractor.status !== 'approved' && ( <DropdownMenuItem className="text-success cursor-pointer text-xs" onClick={() => onApproveInvoice(contractor.id)} > <Check className="mr-2 size-3.5" /> Approve Invoice </DropdownMenuItem> )} {contractor.status !== 'disputed' && ( <DropdownMenuItem className="text-destructive cursor-pointer text-xs" onClick={() => onDisputeInvoice(contractor.id)} > <AlertCircle className="mr-2 size-3.5" /> Dispute / Reject </DropdownMenuItem> )} <DropdownMenuSeparator /> <DropdownMenuItem className="cursor-pointer text-xs"> <Download className="text-muted-foreground mr-2 size-3.5" /> Download Invoice PDF </DropdownMenuItem> <DropdownMenuItem className="cursor-pointer text-xs"> <ShieldCheck className="text-muted-foreground mr-2 size-3.5" /> View W-8BEN Form </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> </TableCell> </TableRow> ) }components/blocks/timesheet-types.ts1.3 kB
export type ApprovalStatus = 'pending' | 'approved' | 'disputed' export type DeliverableType = 'github_pr' | 'figma' | 'ci_pipeline' | 'code_review' export interface DailyTimesheetEntry { id: string date: string day: string hours: number regularHours: number overtimeHours: number task: string deliverableType: DeliverableType referenceId: string referenceLabel: string commitCount?: number verified: boolean } export interface ContractorSubmission { id: string name: string initials: string avatarUrl?: string role: string country: string countryCode: string flag: string city: string contractType: string hourlyRateUsd: number hourlyRateLocal: number currencyCode: string currencySymbol: string fxRate: number hoursLogged: number regularHours: number overtimeHours: number grossAmountLocal: number grossAmountUsd: number verificationBadge: { label: string subtext: string status: 'verified' | 'manual' } status: ApprovalStatus invoiceNumber: string submittedAt: string payPeriod: string taxCompliance: { w8benStatus: 'valid' | 'pending' | 'exempt' taxIdMasked: string jurisdiction: string } paymentMethod: { type: 'SWIFT' | 'SEPA' | 'Local Wire' | 'ACH' accountMasked: string bankName: string } dailyEntries: DailyTimesheetEntry[] }components/blocks/timesheet-data.ts13.1 kB
import type { ContractorSubmission } from './timesheet-types' export const initialSubmissions: ContractorSubmission[] = [ { id: 'ctr-1', name: "Liam O'Connor", initials: 'LO', role: 'Senior React Engineer', country: 'United Kingdom', countryCode: 'GB', flag: '🇬🇧', city: 'London, UK', contractType: 'Hourly Contract · $97.00/hr', hourlyRateUsd: 97.0, hourlyRateLocal: 76.0, currencyCode: 'GBP', currencySymbol: '£', fxRate: 1.2763, hoursLogged: 50.0, regularHours: 40.0, overtimeHours: 10.0, grossAmountLocal: 3800.0, grossAmountUsd: 4850.0, verificationBadge: { label: 'Verified with GitHub PRs', subtext: '14 PRs merged · 46 verified commits', status: 'verified', }, status: 'pending', invoiceNumber: 'INV-2026-UK-0841', submittedAt: 'Aug 21, 2026 · 17:45 UTC', payPeriod: 'Aug 08 – Aug 21, 2026', taxCompliance: { w8benStatus: 'valid', taxIdMasked: 'GB-UTR ••••••8912', jurisdiction: 'HMRC UK (Non-US Source)', }, paymentMethod: { type: 'SWIFT', accountMasked: 'Barclays •••• 4421', bankName: 'Barclays Bank UK PLC', }, dailyEntries: [ { id: 'entry-1-1', date: '2026-08-17', day: 'Mon, Aug 17', hours: 10.0, regularHours: 8.0, overtimeHours: 2.0, task: 'Refactoring OKLCH Color Engine & AST Parser for sub-theme resolution', deliverableType: 'github_pr', referenceId: 'PR #412', referenceLabel: 'feature/v4-ast-parser', commitCount: 12, verified: true, }, { id: 'entry-1-2', date: '2026-08-18', day: 'Tue, Aug 18', hours: 10.0, regularHours: 8.0, overtimeHours: 2.0, task: 'SSR Hydration Mismatch Resolver for cross-framework Island Architectures', deliverableType: 'github_pr', referenceId: 'PR #415', referenceLabel: 'fix/island-hydration', commitCount: 8, verified: true, }, { id: 'entry-1-3', date: '2026-08-19', day: 'Wed, Aug 19', hours: 10.0, regularHours: 8.0, overtimeHours: 2.0, task: 'Design token runtime compilation and memoized theme class cache', deliverableType: 'github_pr', referenceId: 'PR #418', referenceLabel: 'perf/token-runtime', commitCount: 7, verified: true, }, { id: 'entry-1-4', date: '2026-08-20', day: 'Thu, Aug 20', hours: 10.0, regularHours: 8.0, overtimeHours: 2.0, task: 'Figma token sync GitHub Action workflow optimization and schema tests', deliverableType: 'github_pr', referenceId: 'PR #419', referenceLabel: 'ci/figma-token-sync', commitCount: 9, verified: true, }, { id: 'entry-1-5', date: '2026-08-21', day: 'Fri, Aug 21', hours: 10.0, regularHours: 8.0, overtimeHours: 2.0, task: 'Reka UI combobox accessible keyboard navigation bridge and focus ring parity', deliverableType: 'github_pr', referenceId: 'PR #423', referenceLabel: 'feat/reka-combobox-a11y', commitCount: 10, verified: true, }, ], }, { id: 'ctr-2', name: 'Yuki Tanaka', initials: 'YT', role: 'UI/UX Designer', country: 'Japan', countryCode: 'JP', flag: '🇯🇵', city: 'Tokyo, JP', contractType: 'Hourly Contract · $90.00/hr', hourlyRateUsd: 90.0, hourlyRateLocal: 13500, currencyCode: 'JPY', currencySymbol: '¥', fxRate: 0.00667, hoursLogged: 60.0, regularHours: 40.0, overtimeHours: 20.0, grossAmountLocal: 810000, grossAmountUsd: 5400.0, verificationBadge: { label: 'Figma Files & Handoff Verified', subtext: '8 wireframes & design spec approved', status: 'verified', }, status: 'pending', invoiceNumber: 'INV-2026-JP-0219', submittedAt: 'Aug 21, 2026 · 14:10 UTC', payPeriod: 'Aug 08 – Aug 21, 2026', taxCompliance: { w8benStatus: 'valid', taxIdMasked: 'JP-NTA ••••••4401', jurisdiction: 'National Tax Agency Japan', }, paymentMethod: { type: 'Local Wire', accountMasked: 'MUFG •••• 9102', bankName: 'Mitsubishi UFJ Financial Group', }, dailyEntries: [ { id: 'entry-2-1', date: '2026-08-17', day: 'Mon, Aug 17', hours: 12.0, regularHours: 8.0, overtimeHours: 4.0, task: 'Enterprise Multi-currency Invoicing Flow User Journey Map & Edge Cases', deliverableType: 'figma', referenceId: 'Node #492:102', referenceLabel: 'figma.com/@uipkge/multi-currency-flow', commitCount: 6, verified: true, }, { id: 'entry-2-2', date: '2026-08-18', day: 'Tue, Aug 18', hours: 12.0, regularHours: 8.0, overtimeHours: 4.0, task: 'Contractor Compliance Document Verification UX Specs & Empty States', deliverableType: 'figma', referenceId: 'Node #495:88', referenceLabel: 'figma.com/@uipkge/compliance-docs', commitCount: 5, verified: true, }, { id: 'entry-2-3', date: '2026-08-19', day: 'Wed, Aug 19', hours: 12.0, regularHours: 8.0, overtimeHours: 4.0, task: 'Mobile responsive Timesheet approval breakpoints, drawers, and gesture specs', deliverableType: 'figma', referenceId: 'Node #501:14', referenceLabel: 'figma.com/@uipkge/mobile-timesheet', commitCount: 8, verified: true, }, { id: 'entry-2-4', date: '2026-08-20', day: 'Thu, Aug 20', hours: 12.0, regularHours: 8.0, overtimeHours: 4.0, task: 'Interactive Design System Token Library for Dark/Light Themes & High-Contrast', deliverableType: 'figma', referenceId: 'Node #510:33', referenceLabel: 'figma.com/@uipkge/tokens-library', commitCount: 11, verified: true, }, { id: 'entry-2-5', date: '2026-08-21', day: 'Fri, Aug 21', hours: 12.0, regularHours: 8.0, overtimeHours: 4.0, task: 'Usability testing session synthesis, prototype refinements, and engineering handoff', deliverableType: 'figma', referenceId: 'Node #514:02', referenceLabel: 'figma.com/@uipkge/handoff-v2', commitCount: 4, verified: true, }, ], }, { id: 'ctr-3', name: 'Mateo Silva', initials: 'MS', role: 'Go Backend Specialist', country: 'Brazil', countryCode: 'BR', flag: '🇧🇷', city: 'São Paulo, BR', contractType: 'Hourly Contract · $91.11/hr', hourlyRateUsd: 91.11, hourlyRateLocal: 455.55, currencyCode: 'BRL', currencySymbol: 'R$', fxRate: 0.2, hoursLogged: 90.0, regularHours: 40.0, overtimeHours: 50.0, grossAmountLocal: 41000.0, grossAmountUsd: 8200.0, verificationBadge: { label: 'Verified with GitHub PRs', subtext: 'Kafka partitioning & Redis idempotency engine', status: 'verified', }, status: 'pending', invoiceNumber: 'INV-2026-BR-0572', submittedAt: 'Aug 21, 2026 · 19:20 UTC', payPeriod: 'Aug 08 – Aug 21, 2026', taxCompliance: { w8benStatus: 'valid', taxIdMasked: 'BR-CNPJ ••••••7100', jurisdiction: 'Receita Federal do Brasil', }, paymentMethod: { type: 'SWIFT', accountMasked: 'Itaú Unibanco •••• 8831', bankName: 'Banco Itaú S.A.', }, dailyEntries: [ { id: 'entry-3-1', date: '2026-08-17', day: 'Mon, Aug 17', hours: 18.0, regularHours: 8.0, overtimeHours: 10.0, task: 'Distributed transaction saga orchestrator for multi-currency payout reconciliation', deliverableType: 'github_pr', referenceId: 'PR #289', referenceLabel: 'feat/saga-orchestrator', commitCount: 15, verified: true, }, { id: 'entry-3-2', date: '2026-08-18', day: 'Tue, Aug 18', hours: 18.0, regularHours: 8.0, overtimeHours: 10.0, task: 'Zero-downtime PostgreSQL table partitioning for timesheet audit logs & retention', deliverableType: 'github_pr', referenceId: 'PR #294', referenceLabel: 'db/pg-partitioning', commitCount: 7, verified: true, }, { id: 'entry-3-3', date: '2026-08-19', day: 'Wed, Aug 19', hours: 18.0, regularHours: 8.0, overtimeHours: 10.0, task: 'Webhook delivery retry backoff with Kafka dead-letter queue & idempotency keys', deliverableType: 'github_pr', referenceId: 'PR #298', referenceLabel: 'feat/kafka-dlq-retries', commitCount: 19, verified: true, }, { id: 'entry-3-4', date: '2026-08-20', day: 'Thu, Aug 20', hours: 18.0, regularHours: 8.0, overtimeHours: 10.0, task: 'Prometheus latency metric instrumentation & Grafana alert rules for payment workers', deliverableType: 'github_pr', referenceId: 'PR #302', referenceLabel: 'obs/prometheus-metrics', commitCount: 5, verified: true, }, { id: 'entry-3-5', date: '2026-08-21', day: 'Fri, Aug 21', hours: 18.0, regularHours: 8.0, overtimeHours: 10.0, task: 'Payment gateway failover simulation & multi-region load testing at 25k req/s', deliverableType: 'github_pr', referenceId: 'PR #307', referenceLabel: 'test/gateway-failover', commitCount: 12, verified: true, }, ], }, { id: 'ctr-4', name: 'Chloe Dubois', initials: 'CD', role: 'QA Automation Lead', country: 'France', countryCode: 'FR', flag: '🇫🇷', city: 'Paris, FR', contractType: 'Hourly Contract · $92.00/hr', hourlyRateUsd: 92.0, hourlyRateLocal: 85.0, currencyCode: 'EUR', currencySymbol: '€', fxRate: 1.0824, hoursLogged: 40.0, regularHours: 40.0, overtimeHours: 0.0, grossAmountLocal: 3400.0, grossAmountUsd: 3680.0, verificationBadge: { label: 'Playwright E2E Suites 100% Pass', subtext: 'CI Run #8821 verified · 342 browser tests green', status: 'verified', }, status: 'approved', invoiceNumber: 'INV-2026-FR-1108', submittedAt: 'Aug 20, 2026 · 16:30 UTC', payPeriod: 'Aug 08 – Aug 21, 2026', taxCompliance: { w8benStatus: 'valid', taxIdMasked: 'FR-SIRET ••••••3921', jurisdiction: 'DGFiP France (EU VAT Reg)', }, paymentMethod: { type: 'SEPA', accountMasked: 'BNP Paribas •••• 5590', bankName: 'BNP Paribas S.A.', }, dailyEntries: [ { id: 'entry-4-1', date: '2026-08-17', day: 'Mon, Aug 17', hours: 8.0, regularHours: 8.0, overtimeHours: 0.0, task: 'Playwright end-to-end multi-currency payment checkout regression suite', deliverableType: 'ci_pipeline', referenceId: 'CI Run #8812', referenceLabel: 'e2e-checkout-matrix', commitCount: 8, verified: true, }, { id: 'entry-4-2', date: '2026-08-18', day: 'Tue, Aug 18', hours: 8.0, regularHours: 8.0, overtimeHours: 0.0, task: 'Visual regression tests for dark mode and high-contrast accessibility across viewports', deliverableType: 'ci_pipeline', referenceId: 'CI Run #8815', referenceLabel: 'visual-regression-a11y', commitCount: 6, verified: true, }, { id: 'entry-4-3', date: '2026-08-19', day: 'Wed, Aug 19', hours: 8.0, regularHours: 8.0, overtimeHours: 0.0, task: 'Automated timesheet submission stress testing & concurrent approval race conditions', deliverableType: 'ci_pipeline', referenceId: 'CI Run #8818', referenceLabel: 'load-timesheet-concurrency', commitCount: 14, verified: true, }, { id: 'entry-4-4', date: '2026-08-20', day: 'Thu, Aug 20', hours: 8.0, regularHours: 8.0, overtimeHours: 0.0, task: 'Cross-browser matrix testing (Chromium, Firefox, WebKit) on macOS/Linux runners', deliverableType: 'ci_pipeline', referenceId: 'CI Run #8820', referenceLabel: 'cross-browser-ci', commitCount: 4, verified: true, }, { id: 'entry-4-5', date: '2026-08-21', day: 'Fri, Aug 21', hours: 8.0, regularHours: 8.0, overtimeHours: 0.0, task: 'CI test sharding optimization reducing full regression duration from 18m to 3m 12s', deliverableType: 'ci_pipeline', referenceId: 'CI Run #8821', referenceLabel: 'ci-sharding-speedup', commitCount: 5, verified: true, }, ], }, ]
Raw manifest:https://uipkge.dev/r/react/contractor-timesheet-approval.json