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-vue@latest add https://uipkge.dev/r/vue/contractor-timesheet-approval.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/contractor-timesheet-approval.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/contractor-timesheet-approval.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/contractor-timesheet-approval.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/contractor-timesheet-approvalInstalls to:app/components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
title | string | 'Contractor Invoices & Timesheets' | optional |
subtitle | string | 'Review international contractor hours | optional |
payCycle | string | 'August 2026 · Bi-Weekly #2' | optional |
initialData | ContractorSubmission[] | () => initialSubmissions, | optional |
class | HTMLAttributes['class'] | — | 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)
app/components/blocks/ContractorTimesheetApproval.vue16.6 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, ref } from 'vue' import { Calendar, Check, CheckCheck, CheckCircle2, Clock, FileSpreadsheet, Globe, RotateCcw, Search, } from 'lucide-vue-next' 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.vue' import TimesheetKpiCards from './TimesheetKpiCards.vue' import TimesheetTableRow from './TimesheetTableRow.vue' import type { ApprovalStatus, ContractorSubmission, DailyTimesheetEntry, DeliverableType } from './timesheet-types' import { initialSubmissions } from './timesheet-data' export type { ApprovalStatus, ContractorSubmission, DailyTimesheetEntry, DeliverableType } interface Props { title?: string subtitle?: string payCycle?: string initialData?: ContractorSubmission[] class?: HTMLAttributes['class'] } const props = withDefaults(defineProps<Props>(), { 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, }) const submissions = ref<ContractorSubmission[]>(JSON.parse(JSON.stringify(props.initialData))) const searchQuery = ref('') const selectedStatus = ref<'all' | 'pending' | 'approved' | 'disputed'>('all') const currencyDisplay = ref<'both' | 'usd' | 'local'>('both') const selectedContractorId = ref<string | null>(null) const isDrawerOpen = ref(false) const isApprovingAll = ref(false) const bulkApprovalDone = ref(false) const pendingSubmissions = computed(() => submissions.value.filter((s) => s.status === 'pending')) const pendingCount = computed(() => pendingSubmissions.value.length) const pendingAmountUsd = computed(() => pendingSubmissions.value.reduce((acc, curr) => acc + curr.grossAmountUsd, 0)) const approvedSubmissions = computed(() => submissions.value.filter((s) => s.status === 'approved')) const approvedCount = computed(() => approvedSubmissions.value.length) const approvedAmountUsd = computed(() => approvedSubmissions.value.reduce((acc, curr) => acc + curr.grossAmountUsd, 0)) const disputedSubmissions = computed(() => submissions.value.filter((s) => s.status === 'disputed')) const disputedCount = computed(() => disputedSubmissions.value.length) const totalHoursLogged = computed(() => submissions.value.reduce((acc, curr) => acc + curr.hoursLogged, 0)) const scheduledPayoutsTotal = computed(() => 42800.0) const filteredSubmissions = computed(() => { return submissions.value.filter((item) => { const matchesStatus = selectedStatus.value === 'all' ? true : item.status === selectedStatus.value const query = searchQuery.value.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 }) }) const selectedContractor = computed(() => submissions.value.find((c) => c.id === selectedContractorId.value)) 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}` } function openDrawer(contractor: ContractorSubmission) { selectedContractorId.value = contractor.id isDrawerOpen.value = true } function approveInvoice(id: string) { const index = submissions.value.findIndex((s) => s.id === id) if (index !== -1) { submissions.value[index].status = 'approved' } } function disputeInvoice(id: string) { const index = submissions.value.findIndex((s) => s.id === id) if (index !== -1) { submissions.value[index].status = 'disputed' } } function handleApproveDrawer() { if (selectedContractorId.value) approveInvoice(selectedContractorId.value) isDrawerOpen.value = false } function handleDisputeDrawer() { if (selectedContractorId.value) disputeInvoice(selectedContractorId.value) isDrawerOpen.value = false } function approveAllPending() { if (pendingCount.value === 0) return isApprovingAll.value = true setTimeout(() => { submissions.value.forEach((s) => { if (s.status === 'pending') { s.status = 'approved' } }) isApprovingAll.value = false bulkApprovalDone.value = true setTimeout(() => { bulkApprovalDone.value = false }, 3000) }, 400) } function resetData() { submissions.value = JSON.parse(JSON.stringify(props.initialData)) selectedStatus.value = 'all' searchQuery.value = '' bulkApprovalDone.value = false } </script> <template> <div data-slot="contractor-timesheet-approval" :class="cn('text-foreground w-full space-y-6', props.class)"> <!-- Header Section --> <div class="border-border/80 flex flex-col gap-4 border-b pb-6 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2"> <Badge variant="outline" class="border-border text-muted-foreground text-xs font-medium"> <Globe class="text-primary mr-1 size-3" /> Global Workforce · HRMS </Badge> <div class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium"> <Calendar class="text-muted-foreground size-3.5" /> <span>Pay Cycle:</span> <span class="text-foreground font-semibold">{{ payCycle }}</span> </div> <Badge v-if="pendingCount > 0" variant="outline" class="border-warning/30 bg-warning/10 text-warning text-xs font-medium" > <span class="bg-warning mr-1.5 size-1.5 animate-pulse rounded-full" /> {{ pendingCount }} Invoices Pending Review </Badge> <Badge v-else variant="outline" class="border-success/30 bg-success/10 text-success text-xs font-medium"> <CheckCircle2 class="text-success mr-1 size-3" /> All Invoices Reviewed </Badge> </div> <h1 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl"> {{ title }} </h1> <p class="text-muted-foreground max-w-3xl text-sm leading-relaxed"> {{ subtitle }} </p> </div> <!-- Action Buttons --> <div class="flex flex-wrap items-center gap-2.5 pt-1 lg:pt-0"> <Button variant="outline" size="sm" class="border-border hover:bg-muted h-9 gap-1.5 text-xs font-medium shadow-xs" @click="resetData" > <RotateCcw class="text-muted-foreground size-3.5" /> Reset Demo </Button> <Button variant="default" size="sm" :disabled="pendingCount === 0 || isApprovingAll" class="h-9 gap-2 text-xs font-medium shadow-xs" @click="approveAllPending" > <template v-if="isApprovingAll"> <Clock class="size-3.5 animate-spin" /> Processing Batch... </template> <template v-else-if="bulkApprovalDone"> <Check class="text-success size-3.5" /> Batch Approved! </template> <template v-else> <CheckCheck class="size-3.5" /> <span>Approve All Pending ({{ formatUsd(pendingAmountUsd) }})</span> </template> </Button> </div> </div> <!-- 4 Billing Overview KPI Cards --> <TimesheetKpiCards :pending-amount-usd="pendingAmountUsd" :pending-count="pendingCount" :total-hours-logged="totalHoursLogged" :scheduled-payouts-total="scheduledPayoutsTotal" :format-usd="formatUsd" /> <!-- Filter Toolbar --> <Card class="border-border bg-card shadow-xs"> <CardContent class="p-4"> <div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <!-- Search input --> <div class="relative max-w-md flex-1"> <Search class="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" /> <input v-model="searchQuery" type="text" placeholder="Search contractor, role, country, or PR..." class="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 class="flex flex-wrap items-center gap-2"> <!-- Status filter tabs --> <div class="border-border bg-muted/40 inline-flex rounded-lg border p-0.5 text-xs"> <button type="button" :class=" 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', ) " @click="selectedStatus = 'all'" > All ({{ submissions.length }}) </button> <button type="button" :class=" 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', ) " @click="selectedStatus = 'pending'" > Pending ({{ pendingCount }}) </button> <button type="button" :class=" 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', ) " @click="selectedStatus = 'approved'" > Approved ({{ approvedCount }}) </button> <button type="button" :class=" 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', ) " @click="selectedStatus = 'disputed'" > Disputed ({{ disputedCount }}) </button> </div> <Separator orientation="vertical" class="hidden h-6 md:block" /> <!-- Currency toggle selector --> <div class="border-border bg-muted/40 inline-flex rounded-lg border p-0.5 text-xs"> <button type="button" :class=" 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', ) " @click="currencyDisplay = 'both'" > Dual FX </button> <button type="button" :class=" 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', ) " @click="currencyDisplay = 'usd'" > USD ($) </button> <button type="button" :class=" 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', ) " @click="currencyDisplay = 'local'" > Local </button> </div> </div> </div> </CardContent> </Card> <!-- Contractor Invoices & Timesheets Table --> <Card class="border-border bg-card overflow-hidden shadow-xs"> <div class="overflow-x-auto"> <Table> <TableHeader class="bg-muted/40"> <TableRow class="border-border hover:bg-transparent"> <TableHead class="text-muted-foreground min-w-[240px] text-xs font-semibold tracking-wider uppercase"> Contractor & Location </TableHead> <TableHead class="text-muted-foreground min-w-[180px] text-xs font-semibold tracking-wider uppercase"> Role & Rate </TableHead> <TableHead class="text-muted-foreground min-w-[140px] text-xs font-semibold tracking-wider uppercase"> Hours Logged </TableHead> <TableHead class="text-muted-foreground min-w-[200px] text-xs font-semibold tracking-wider uppercase"> Invoice Gross Amount </TableHead> <TableHead class="text-muted-foreground min-w-[220px] text-xs font-semibold tracking-wider uppercase"> Timesheet Verification </TableHead> <TableHead class="text-muted-foreground min-w-[120px] text-xs font-semibold tracking-wider uppercase"> Status </TableHead> <TableHead class="text-muted-foreground min-w-[180px] text-right text-xs font-semibold tracking-wider uppercase" > Actions </TableHead> </TableRow> </TableHeader> <TableBody> <template v-if="filteredSubmissions.length > 0"> <TimesheetTableRow v-for="contractor in filteredSubmissions" :key="contractor.id" :contractor="contractor" :currency-display="currencyDisplay" :format-usd="formatUsd" :format-local="formatLocal" @open-drawer="openDrawer" @approve-invoice="approveInvoice" @dispute-invoice="disputeInvoice" /> </template> <template v-else> <TableRow> <TableCell colspan="7" class="h-36 text-center"> <div class="flex flex-col items-center justify-center gap-1.5"> <FileSpreadsheet class="text-muted-foreground/50 size-8" /> <p class="text-foreground text-sm font-medium">No invoices match your filter</p> <p class="text-muted-foreground text-xs"> Try adjusting your search query or switching the status filter tab. </p> </div> </TableCell> </TableRow> </template> </TableBody> </Table> </div> </Card> <!-- Timesheet Detail Drawer / Slide-Over Component --> <TimesheetDetailSheet :open="isDrawerOpen" :selected-contractor="selectedContractor" :format-usd="formatUsd" :format-local="formatLocal" @update:open="isDrawerOpen = $event" @approve="handleApproveDrawer" @dispute="handleDisputeDrawer" /> </div> </template>app/components/blocks/TimesheetDetailSheet.vue12.1 kB
<script setup lang="ts"> import { Check, CheckCircle2, FileCheck, FileCode, GitPullRequest, ShieldCheck, XCircle } from 'lucide-vue-next' 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' interface Props { open: boolean selectedContractor: ContractorSubmission | undefined formatUsd: (val: number) => string formatLocal: (val: number, currencyCode: string, currencySymbol: string) => string } defineProps<Props>() defineEmits<{ 'update:open': [value: boolean] approve: [] dispute: [] }>() </script> <template> <Sheet :open="open" @update:open="$emit('update:open', $event)"> <SheetContent side="right" class="bg-background text-foreground flex w-full flex-col overflow-y-auto p-0 sm:max-w-xl md:max-w-2xl" > <template v-if="selectedContractor"> <!-- Drawer Header --> <SheetHeader class="border-border/80 space-y-3 border-b px-6 py-5 text-left"> <div class="flex items-start justify-between gap-4"> <div class="flex items-center gap-3"> <Avatar class="border-border size-12 border"> <AvatarFallback class="bg-primary/10 text-primary text-sm font-bold"> {{ selectedContractor.initials }} </AvatarFallback> </Avatar> <div> <div class="flex items-center gap-2"> <SheetTitle class="text-foreground text-lg font-bold"> {{ selectedContractor.name }} </SheetTitle> <span class="text-base" :title="selectedContractor.country"> {{ selectedContractor.flag }} </span> </div> <SheetDescription class="text-muted-foreground text-xs"> {{ selectedContractor.role }} · {{ selectedContractor.city }} </SheetDescription> </div> </div> <!-- Status Badge in Header --> <Badge v-if="selectedContractor.status === 'pending'" variant="outline" class="border-warning/30 bg-warning/10 text-warning shrink-0 text-xs font-medium" > <span class="bg-warning mr-1.5 size-1.5 rounded-full" /> Pending Review </Badge> <Badge v-else-if="selectedContractor.status === 'approved'" variant="outline" class="border-success/30 bg-success/10 text-success shrink-0 text-xs font-medium" > <Check class="text-success mr-1 size-3" /> Approved </Badge> <Badge v-else variant="outline" class="border-destructive/30 bg-destructive/10 text-destructive shrink-0 text-xs font-medium" > <XCircle class="text-destructive mr-1 size-3" /> Disputed </Badge> </div> <!-- Header Quick Stats Grid --> <div class="grid grid-cols-3 gap-2 pt-2 text-xs"> <div class="border-border bg-muted/40 rounded-lg border p-2.5"> <span class="text-muted-foreground block">Invoice Gross</span> <span class="text-foreground text-sm font-bold tabular-nums"> {{ formatUsd(selectedContractor.grossAmountUsd) }} </span> <span class="text-muted-foreground/70 block text-xs tabular-nums"> {{ formatLocal( selectedContractor.grossAmountLocal, selectedContractor.currencyCode, selectedContractor.currencySymbol, ) }} </span> </div> <div class="border-border bg-muted/40 rounded-lg border p-2.5"> <span class="text-muted-foreground block">Total Hours</span> <span class="text-foreground text-sm font-bold tabular-nums"> {{ selectedContractor.hoursLogged.toFixed(1) }} hrs </span> <span class="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 class="border-border bg-muted/40 rounded-lg border p-2.5"> <span class="text-muted-foreground block">Contract Rate</span> <span class="text-foreground text-sm font-bold tabular-nums"> {{ formatUsd(selectedContractor.hourlyRateUsd) }}/hr </span> <span class="text-muted-foreground/70 block truncate text-xs"> {{ selectedContractor.taxCompliance.jurisdiction }} </span> </div> </div> </SheetHeader> <!-- Drawer Body: Timesheet Breakdown --> <div class="flex-1 space-y-6 overflow-y-auto px-6 py-5"> <!-- Verification Callout Banner --> <div class="border-success/20 bg-success/10 text-success flex items-start gap-3 rounded-lg border p-3.5 text-xs" > <ShieldCheck class="text-success mt-0.5 size-4 shrink-0" /> <div class="space-y-0.5"> <p class="text-success font-semibold"> {{ selectedContractor.verificationBadge.label }} </p> <p class="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 class="space-y-3"> <div class="flex items-center justify-between gap-x-2"> <h2 class="text-muted-foreground text-xs font-semibold"> Daily Itemized Activity Log ({{ selectedContractor.dailyEntries.length }} Work Days) </h2> <span class="text-muted-foreground font-mono text-xs"> {{ selectedContractor.payPeriod }} </span> </div> <div class="space-y-2.5"> <div v-for="entry in selectedContractor.dailyEntries" :key="entry.id" class="border-border bg-card hover:border-border/80 space-y-2 rounded-lg border p-3.5 transition-colors" > <div class="flex items-center justify-between gap-x-2"> <div class="flex items-center gap-2"> <span class="text-foreground text-xs font-semibold">{{ entry.day }}</span> <Badge variant="outline" class="border-border text-muted-foreground h-4 py-0 text-xs"> {{ entry.date }} </Badge> </div> <div class="flex items-center gap-2"> <span class="text-foreground text-xs font-bold tabular-nums"> {{ entry.hours.toFixed(1) }} hrs </span> <span class="text-muted-foreground text-xs tabular-nums"> ({{ formatUsd(entry.hours * selectedContractor.hourlyRateUsd) }}) </span> </div> </div> <p class="text-muted-foreground text-xs leading-relaxed"> {{ entry.task }} </p> <!-- Linked Deliverables (PR / Commit / Figma / CI) --> <div class="flex flex-wrap items-center gap-2 pt-1"> <div class="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" > <template v-if="entry.deliverableType === 'github_pr'"> <GitPullRequest class="text-chart-1 size-3" /> <span>{{ entry.referenceId }}</span> <span class="text-muted-foreground">({{ entry.referenceLabel }})</span> <span v-if="entry.commitCount" class="text-muted-foreground" >· {{ entry.commitCount }} commits</span > </template> <template v-else-if="entry.deliverableType === 'figma'"> <FileCode class="text-chart-2 size-3" /> <span>{{ entry.referenceId }}</span> <span class="text-muted-foreground">({{ entry.referenceLabel }})</span> </template> <template v-else> <CheckCircle2 class="text-success size-3" /> <span>{{ entry.referenceId }}</span> <span class="text-muted-foreground">({{ entry.referenceLabel }})</span> </template> </div> <Badge variant="outline" class="border-success/20 bg-success/10 text-success h-5 gap-1 py-0 text-xs"> <Check class="size-2.5" /> Verified </Badge> </div> </div> </div> </div> <!-- Tax & Bank Payout Compliance Box --> <div class="border-border bg-muted/20 space-y-2.5 rounded-lg border p-4 text-xs"> <h3 class="text-foreground flex items-center gap-1.5 font-bold"> <FileCheck class="text-primary size-3.5" /> Compliance & Disbursement Routing </h3> <div class="grid grid-cols-2 gap-3 pt-1"> <div> <span class="text-muted-foreground block">Tax Form Status:</span> <span class="text-foreground font-medium"> Form W-8BEN ({{ selectedContractor.taxCompliance.w8benStatus.toUpperCase() }}) </span> <span class="text-muted-foreground block font-mono text-xs">{{ selectedContractor.taxCompliance.taxIdMasked }}</span> </div> <div> <span class="text-muted-foreground block">Disbursement Route:</span> <span class="text-foreground font-medium"> {{ selectedContractor.paymentMethod.type }} Payout </span> <span class="text-muted-foreground block text-xs">{{ selectedContractor.paymentMethod.bankName }}</span> </div> </div> </div> </div> <!-- Drawer Footer Actions --> <SheetFooter class="border-border/80 bg-muted/20 flex flex-row items-center justify-between gap-3 border-t px-6 py-4" > <SheetClose as-child> <Button variant="outline" size="sm" class="text-xs"> Close </Button> </SheetClose> <div class="flex items-center gap-2"> <Button v-if="selectedContractor.status !== 'disputed'" variant="outline" size="sm" class="border-destructive/30 text-destructive hover:bg-destructive/10 text-destructive text-xs" @click="$emit('dispute')" > <XCircle class="mr-1.5 size-3.5" /> Dispute Invoice </Button> <Button v-if="selectedContractor.status !== 'approved'" variant="default" size="sm" class="text-xs shadow-xs" @click="$emit('approve')" > <Check class="mr-1.5 size-3.5" /> Approve ({{ formatUsd(selectedContractor.grossAmountUsd) }}) </Button> <span v-else class="text-success flex items-center gap-1 text-xs font-semibold"> <CheckCircle2 class="size-4" /> Approved & Queued </span> </div> </SheetFooter> </template> </SheetContent> </Sheet> </template>app/components/blocks/TimesheetKpiCards.vue4.9 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { Clock, ShieldCheck, Timer, Users } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' interface Props { pendingAmountUsd: number pendingCount: number totalHoursLogged: number scheduledPayoutsTotal: number formatUsd: (val: number) => string class?: HTMLAttributes['class'] } defineProps<Props>() </script> <template> <div :class="cn('grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4', $props.class)"> <!-- Card 1: Pending Approval Amount --> <Card class="border-border bg-card shadow-xs transition-colors"> <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle class="text-muted-foreground text-xs font-medium"> Pending Approval Amount </CardTitle> <div class="border-warning/20 bg-warning/10 text-warning flex size-8 items-center justify-center rounded-lg border" > <Clock class="size-4" /> </div> </CardHeader> <CardContent class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums"> {{ formatUsd(pendingAmountUsd) }} </div> <p class="text-muted-foreground text-xs"> <span class="text-warning text-warning font-semibold tabular-nums"> {{ pendingCount }} invoices </span> awaiting your approval </p> <div class="pt-2"> <span class="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 class="border-border bg-card shadow-xs transition-colors"> <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle class="text-muted-foreground text-xs font-medium"> Total Contractor Hours </CardTitle> <div class="border-info/20 bg-info/10 text-info flex size-8 items-center justify-center rounded-lg border"> <Timer class="size-4" /> </div> </CardHeader> <CardContent class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums"> {{ totalHoursLogged.toFixed(1) }} hrs </div> <p class="text-muted-foreground text-xs"> <span class="text-foreground font-medium tabular-nums">160.0 regular</span> · 80.0 overtime / project </p> <div class="pt-2"> <span class="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 class="border-border bg-card shadow-xs transition-colors"> <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle class="text-muted-foreground text-xs font-medium"> Approved & Scheduled </CardTitle> <div class="border-success/20 bg-success/10 text-success flex size-8 items-center justify-center rounded-lg border" > <ShieldCheck class="size-4" /> </div> </CardHeader> <CardContent class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums"> {{ formatUsd(scheduledPayoutsTotal) }} </div> <p class="text-muted-foreground text-xs"> <span class="text-success text-success font-medium tabular-nums"> 5 invoices </span> batched for Aug 25 payout </p> <div class="pt-2"> <span class="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 class="border-border bg-card shadow-xs transition-colors"> <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle class="text-muted-foreground text-xs font-medium"> Active Contractors </CardTitle> <div class="border-chart-1/20 bg-chart-1/10 text-chart-1 flex size-8 items-center justify-center rounded-lg border" > <Users class="size-4" /> </div> </CardHeader> <CardContent class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">8 Contractors</div> <p class="text-muted-foreground text-xs">Across 5 countries (UK, JP, BR, FR, CA)</p> <div class="pt-2"> <span class="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> </template>app/components/blocks/TimesheetTableRow.vue9.2 kB
<script setup lang="ts"> import { AlertCircle, Check, CheckCircle2, Download, Eye, FileText, MoreHorizontal, ShieldCheck, XCircle, } from 'lucide-vue-next' 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' interface Props { contractor: ContractorSubmission currencyDisplay: 'both' | 'usd' | 'local' formatUsd: (val: number) => string formatLocal: (val: number, currencyCode: string, currencySymbol: string) => string } defineProps<Props>() const emit = defineEmits<{ openDrawer: [contractor: ContractorSubmission] approveInvoice: [id: string] disputeInvoice: [id: string] }>() </script> <template> <TableRow class="border-border hover:bg-muted/30 transition-colors"> <!-- Contractor & Location --> <TableCell class="py-4"> <div class="flex items-center gap-3"> <Avatar class="border-border size-10 shrink-0 border"> <AvatarFallback class="bg-primary/10 text-primary text-xs font-semibold"> {{ contractor.initials }} </AvatarFallback> </Avatar> <div class="space-y-0.5"> <div class="flex items-center gap-1.5"> <button type="button" class="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" @click="emit('openDrawer', contractor)" > {{ contractor.name }} </button> <span class="text-sm" :title="contractor.country">{{ contractor.flag }}</span> </div> <p class="text-muted-foreground flex items-center gap-1 text-xs"> <span>{{ contractor.city }}</span> <span>•</span> <span class="text-muted-foreground/80 font-mono">{{ contractor.invoiceNumber }}</span> </p> </div> </div> </TableCell> <!-- Role & Rate --> <TableCell class="py-4"> <div class="space-y-0.5"> <p class="text-foreground text-xs font-medium">{{ contractor.role }}</p> <p class="text-muted-foreground text-xs tabular-nums"> {{ formatUsd(contractor.hourlyRateUsd) }}/hr <span class="text-muted-foreground/70"> ({{ formatLocal(contractor.hourlyRateLocal, contractor.currencyCode, contractor.currencySymbol) }}/hr) </span> </p> </div> </TableCell> <!-- Hours Logged with daily breakdown expander button --> <TableCell class="py-4"> <div class="space-y-1"> <div class="flex items-center gap-1.5"> <span class="text-foreground text-sm font-bold tabular-nums"> {{ contractor.hoursLogged.toFixed(1) }} hrs </span> </div> <div class="text-muted-foreground flex items-center gap-1 text-xs tabular-nums"> <span class="text-success font-medium"> {{ contractor.regularHours.toFixed(0) }}h reg </span> <template v-if="contractor.overtimeHours > 0"> <span>+</span> <span class="text-info font-medium"> {{ contractor.overtimeHours.toFixed(0) }}h ot </span> </template> </div> </div> </TableCell> <!-- Invoice Gross (Local & USD) --> <TableCell class="py-4"> <div class="space-y-1"> <template v-if="currencyDisplay === 'both'"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-sm font-bold tabular-nums"> {{ formatUsd(contractor.grossAmountUsd) }} </span> <span class="text-muted-foreground text-xs font-normal">USD</span> </div> <p class="text-muted-foreground text-xs tabular-nums"> {{ formatLocal(contractor.grossAmountLocal, contractor.currencyCode, contractor.currencySymbol) }} <span class="text-muted-foreground/60 text-xs">(FX: {{ contractor.fxRate.toFixed(4) }})</span> </p> </template> <template v-else-if="currencyDisplay === 'usd'"> <div class="text-foreground text-sm font-bold tabular-nums"> {{ formatUsd(contractor.grossAmountUsd) }} USD </div> <p class="text-muted-foreground text-xs tabular-nums">Rate: {{ formatUsd(contractor.hourlyRateUsd) }}/hr</p> </template> <template v-else> <div class="text-foreground text-sm font-bold tabular-nums"> {{ formatLocal(contractor.grossAmountLocal, contractor.currencyCode, contractor.currencySymbol) }} </div> <p class="text-muted-foreground text-xs tabular-nums">Conv: {{ formatUsd(contractor.grossAmountUsd) }} USD</p> </template> </div> </TableCell> <!-- Timesheet Verification Status --> <TableCell class="py-4"> <div class="space-y-1"> <Badge variant="outline" class="border-success/25 bg-success/10 text-success gap-1 text-xs font-medium"> <CheckCircle2 class="text-success size-3" /> {{ contractor.verificationBadge.label }} </Badge> <p class="text-muted-foreground max-w-[200px] truncate text-xs" :title="contractor.verificationBadge.subtext"> {{ contractor.verificationBadge.subtext }} </p> </div> </TableCell> <!-- Approval Status --> <TableCell class="py-4"> <Badge v-if="contractor.status === 'pending'" variant="outline" class="border-warning/30 bg-warning/10 text-warning text-xs font-medium" > <span class="bg-warning mr-1.5 size-1.5 rounded-full" /> Pending Review </Badge> <Badge v-else-if="contractor.status === 'approved'" variant="outline" class="border-success/30 bg-success/10 text-success text-xs font-medium" > <Check class="text-success mr-1 size-3" /> Approved </Badge> <Badge v-else variant="outline" class="border-destructive/30 bg-destructive/10 text-destructive text-xs font-medium" > <XCircle class="text-destructive mr-1 size-3" /> Disputed </Badge> </TableCell> <!-- Action Buttons --> <TableCell class="py-4 text-right"> <div class="flex items-center justify-end gap-1.5"> <Button variant="outline" size="sm" class="border-border hover:bg-muted h-8 gap-1 px-2.5 text-xs font-medium" @click="emit('openDrawer', contractor)" > <Eye class="text-muted-foreground size-3.5" /> Timesheet </Button> <template v-if="contractor.status === 'pending'"> <Button variant="default" size="sm" class="h-8 px-2.5 text-xs font-medium shadow-xs" @click="emit('approveInvoice', contractor.id)" > <Check class="mr-1 size-3.5" /> Approve </Button> </template> <!-- Dropdown Menu for Secondary Actions --> <DropdownMenu> <DropdownMenuTrigger as-child> <Button variant="ghost" size="icon" class="text-muted-foreground hover:text-foreground size-8"> <MoreHorizontal class="size-4" /> <span class="sr-only">Open contractor menu</span> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" class="w-48"> <DropdownMenuLabel class="text-muted-foreground text-xs"> Invoice {{ contractor.invoiceNumber }} </DropdownMenuLabel> <DropdownMenuItem class="cursor-pointer text-xs" @click="emit('openDrawer', contractor)"> <FileText class="text-muted-foreground mr-2 size-3.5" /> Itemized Timesheet </DropdownMenuItem> <DropdownMenuItem v-if="contractor.status !== 'approved'" class="text-success cursor-pointer text-xs" @click="emit('approveInvoice', contractor.id)" > <Check class="mr-2 size-3.5" /> Approve Invoice </DropdownMenuItem> <DropdownMenuItem v-if="contractor.status !== 'disputed'" class="text-destructive cursor-pointer text-xs" @click="emit('disputeInvoice', contractor.id)" > <AlertCircle class="mr-2 size-3.5" /> Dispute / Reject </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem class="cursor-pointer text-xs"> <Download class="text-muted-foreground mr-2 size-3.5" /> Download Invoice PDF </DropdownMenuItem> <DropdownMenuItem class="cursor-pointer text-xs"> <ShieldCheck class="text-muted-foreground mr-2 size-3.5" /> View W-8BEN Form </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> </TableCell> </TableRow> </template>app/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[] }app/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/vue/contractor-timesheet-approval.json