
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Monthly commuter transit pass, mileage, and EV charging expense claim reimbursement portal with IRS pre-tax allowance metrics, claim submission form with receipt dropzone, and claims history ledger.
Also available for React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/commute-reimbursement-claims.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/commute-reimbursement-claims.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/commute-reimbursement-claims.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/commute-reimbursement-claims.jsonnpx shadcn-vue@latest add @uipkge/commute-reimbursement-claimsInstalls to:app/components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
title | string | 'Commute & Transit Expense Claims' | optional |
policyText | string | 'Monthly Commuter Benefit Program · Up to $315.00/mo IRS … | optional |
claimPeriod | string | 'August 2026' | optional |
class | HTMLAttributes['class'] | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
ExpenseClaiminterface ExpenseClaim {
id: string
date: string
category: CommuteCategory
categoryLabel: string
description: string
provider: string
amount: number
status: ClaimStatus
statusLabel: string
receiptFileName: string
receiptFileSize: string
receiptDate: string
notes: string
}<script setup lang="ts">
import type { Component, HTMLAttributes } from 'vue'
import { computed, ref } from 'vue'
import {
Building2,
Calendar,
Car,
Check,
CheckCircle2,
Clock,
DollarSign,
Download,
Eye,
FileCheck,
FileText,
Paperclip,
Plus,
Receipt,
RotateCcw,
Search,
Send,
ShieldCheck,
Train,
TrendingUp,
Upload,
Wallet,
X,
Zap,
} from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Input } from '@/components/ui/input'
import { Progress } from '@/components/ui/progress'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
export type CommuteCategory = 'transit' | 'parking' | 'mileage' | 'ev_charging'
export type ClaimStatus = 'reimbursed' | 'pending'
export interface ExpenseClaim {
id: string
date: string
category: CommuteCategory
categoryLabel: string
description: string
provider: string
amount: number
status: ClaimStatus
statusLabel: string
receiptFileName: string
receiptFileSize: string
receiptDate: string
notes: string
}
interface Props {
title?: string
policyText?: string
claimPeriod?: string
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
title: 'Commute & Transit Expense Claims',
policyText: 'Monthly Commuter Benefit Program · Up to $315.00/mo IRS pre-tax limit',
claimPeriod: 'August 2026',
})
const categoryMeta: Record<
CommuteCategory,
{ label: string; shortLabel: string; badgeClass: string; icon: Component; irsLimitInfo: string }
> = {
transit: {
label: 'Public Transit Pass: BART/Metro',
shortLabel: 'Public Transit',
badgeClass: 'bg-info/10 text-info border-info/20',
icon: Train,
irsLimitInfo: 'BART, Caltrain, Metro, Subway & Commuter Bus passes (IRC § 132(f))',
},
parking: {
label: 'Parking (Station & Garage)',
shortLabel: 'Parking',
badgeClass: 'bg-chart-1/10 text-chart-1 border-chart-1/20',
icon: Building2,
irsLimitInfo: 'Park-and-ride commuter lots and near-office parking facilities',
},
mileage: {
label: 'Mileage (IRS $0.67/mi)',
shortLabel: 'Mileage',
badgeClass: 'bg-warning/10 text-warning border-warning/20',
icon: Car,
irsLimitInfo: '2026 IRS standard rate: $0.67 per business/commuter detour mile',
},
ev_charging: {
label: 'EV Public Charging',
shortLabel: 'EV Charging',
badgeClass: 'bg-chart-2/10 text-chart-2 border-chart-2/20',
icon: Zap,
irsLimitInfo: 'Level 2 & DC fast charging at public networks & workplace chargers',
},
}
const statusStyles: Record<ClaimStatus, string> = {
reimbursed: 'bg-success/10 text-success border-success/20',
pending: 'bg-warning/10 text-warning border-warning/20',
}
const statusDots: Record<ClaimStatus, string> = {
reimbursed: 'bg-success',
pending: 'bg-warning',
}
const claims = ref<ExpenseClaim[]>([
{
id: 'CLM-8041',
date: 'Aug 18, 2026',
category: 'transit',
categoryLabel: 'Public Transit',
description: 'Monthly BART Clipper Card Pass',
provider: 'Bay Area Rapid Transit (BART)',
amount: 120.0,
status: 'reimbursed',
statusLabel: 'Reimbursed on Payroll',
receiptFileName: 'bart-clipper-pass-aug2026.pdf',
receiptFileSize: '340 KB',
receiptDate: 'Aug 18, 2026 · 08:14 AM',
notes: 'Monthly pass reload for Embarcadero ↔ Millbrae commuter route.',
},
{
id: 'CLM-7982',
date: 'Aug 12, 2026',
category: 'transit',
categoryLabel: 'Public Transit',
description: 'San Francisco Caltrain Monthly Pass',
provider: 'Caltrain Peninsula Corridor',
amount: 60.5,
status: 'reimbursed',
statusLabel: 'Reimbursed on Payroll',
receiptFileName: 'caltrain-zone1-3-pass.pdf',
receiptFileSize: '412 KB',
receiptDate: 'Aug 12, 2026 · 07:45 AM',
notes: 'Zone 1 to Zone 3 Caltrain commuter pass for headquarters travel.',
},
{
id: 'CLM-8104',
date: 'Aug 08, 2026',
category: 'parking',
categoryLabel: 'Parking',
description: 'Downtown Parking Garage',
provider: 'SOMA Park Management Corp.',
amount: 65.0,
status: 'pending',
statusLabel: 'Pending Approval',
receiptFileName: 'soma-garage-monthly-receipt.pdf',
receiptFileSize: '185 KB',
receiptDate: 'Aug 08, 2026 · 09:30 AM',
notes: 'Commuter parking space #204 during quarterly planning week.',
},
{
id: 'CLM-7729',
date: 'Aug 03, 2026',
category: 'ev_charging',
categoryLabel: 'EV Charging',
description: 'EV Supercharger Network',
provider: 'EVgo / ChargePoint Public Hub',
amount: 42.0,
status: 'reimbursed',
statusLabel: 'Reimbursed on Payroll',
receiptFileName: 'evgo-charge-sessions-aug.pdf',
receiptFileSize: '520 KB',
receiptDate: 'Aug 03, 2026 · 06:20 PM',
notes: '4x workplace Level-2 charge sessions during sprint week.',
},
])
// Form Reactive State
const formCategory = ref<CommuteCategory>('transit')
const formDate = ref('2026-08-20')
const formProvider = ref('BART Clipper Card')
const formDescription = ref('Monthly BART Clipper Card Pass')
const formAmount = ref('120.00')
const formMiles = ref('45')
const formAttachedReceipt = ref<{ name: string; size: string } | null>({
name: 'clipper-reload-receipt.pdf',
size: '240 KB',
})
const formSubmittedNotice = ref(false)
const recentlySubmittedClaimId = ref('')
// Filter State
const searchQuery = ref('')
const categoryFilter = ref('all')
const statusFilter = ref('all')
// Receipt Modal State
const isReceiptModalOpen = ref(false)
const selectedClaim = ref<ExpenseClaim>(claims.value[0])
function openReceipt(claim: ExpenseClaim) {
selectedClaim.value = claim
isReceiptModalOpen.value = true
}
function handleCategoryChange(val: CommuteCategory) {
formCategory.value = val
if (val === 'transit') {
formProvider.value = 'BART Clipper Card'
formDescription.value = 'Monthly BART Clipper Card Pass'
formAmount.value = '120.00'
} else if (val === 'parking') {
formProvider.value = 'Downtown Station Garage'
formDescription.value = 'Monthly Station Parking Permit'
formAmount.value = '65.00'
} else if (val === 'mileage') {
formProvider.value = 'Personal Vehicle (IRS Standard Rate)'
formDescription.value = 'Commute detour travel to client site'
updateMileageAmount()
} else if (val === 'ev_charging') {
formProvider.value = 'EVgo Fast Charging'
formDescription.value = 'Workplace EV public charging sessions'
formAmount.value = '42.00'
}
}
function updateMileageAmount() {
const miles = parseFloat(formMiles.value) || 0
const calculated = (miles * 0.67).toFixed(2)
formAmount.value = calculated
}
function handleAttachSampleReceipt() {
formAttachedReceipt.value = {
name: `receipt-${formCategory.value}-${Date.now().toString().slice(-4)}.pdf`,
size: '318 KB',
}
}
function handleRemoveReceipt() {
formAttachedReceipt.value = null
}
function handleSubmitClaim() {
const parsedAmt = parseFloat(formAmount.value) || 0
if (parsedAmt <= 0) return
const meta = categoryMeta[formCategory.value]
const newId = `CLM-${Math.floor(8200 + Math.random() * 700)}`
const newClaim: ExpenseClaim = {
id: newId,
date: 'Aug 21, 2026',
category: formCategory.value,
categoryLabel: meta.shortLabel,
description: formDescription.value || meta.label,
provider: formProvider.value || 'Transit Provider',
amount: parsedAmt,
status: 'pending',
statusLabel: 'Pending Approval',
receiptFileName: formAttachedReceipt.value?.name || 'commute-claim-receipt.pdf',
receiptFileSize: formAttachedReceipt.value?.size || '290 KB',
receiptDate: 'Aug 21, 2026 · Just now',
notes: `Submitted via Commuter Benefit Portal for ${props.claimPeriod}.`,
}
claims.value.unshift(newClaim)
recentlySubmittedClaimId.value = newId
formSubmittedNotice.value = true
// Scroll or highlight
setTimeout(() => {
formSubmittedNotice.value = false
}, 6000)
}
function handleResetForm() {
formCategory.value = 'transit'
formDate.value = '2026-08-20'
formProvider.value = 'BART Clipper Card'
formDescription.value = 'Monthly BART Clipper Card Pass'
formAmount.value = '120.00'
formMiles.value = '45'
formAttachedReceipt.value = {
name: 'clipper-reload-receipt.pdf',
size: '240 KB',
}
formSubmittedNotice.value = false
}
const isFiltered = computed(() => {
return searchQuery.value.trim() !== '' || categoryFilter.value !== 'all' || statusFilter.value !== 'all'
})
const filteredClaims = computed(() => {
const q = searchQuery.value.trim().toLowerCase()
return claims.value.filter((claim) => {
const matchCat = categoryFilter.value === 'all' || claim.category === categoryFilter.value
const matchStatus = statusFilter.value === 'all' || claim.status === statusFilter.value
const matchQuery =
!q ||
claim.id.toLowerCase().includes(q) ||
claim.description.toLowerCase().includes(q) ||
claim.provider.toLowerCase().includes(q) ||
claim.categoryLabel.toLowerCase().includes(q)
return matchCat && matchStatus && matchQuery
})
})
function resetFilters() {
searchQuery.value = ''
categoryFilter.value = 'all'
statusFilter.value = 'all'
}
function scrollToSubmitForm() {
const el = document.getElementById('commute-submit-form')
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
</script>
<template>
<div data-slot="commute-reimbursement-claims" :class="cn('w-full space-y-6', props.class)">
<!-- Header -->
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-1">
<div class="flex flex-wrap items-center gap-2.5">
<h2 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">{{ props.title }}</h2>
<Badge wrap variant="outline" class="border-info/30 bg-info/10 text-info text-xs font-medium">
<Calendar class="mr-1 size-3" aria-hidden="true" />
{{ props.claimPeriod }}
</Badge>
</div>
<p class="text-muted-foreground text-sm">
{{ props.policyText }}
</p>
</div>
<div class="flex flex-wrap items-center gap-2.5">
<Button aria-label="Download attachment" variant="outline" size="sm" class="shadow-xs">
<Download class="size-4" aria-hidden="true" />
Export Statement
</Button>
<Button size="sm" class="shadow-xs" @click="scrollToSubmitForm">
<Plus class="size-4" aria-hidden="true" />
Submit New Claim
</Button>
</div>
</div>
<!-- 4 Monthly Benefit Metric Cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<!-- 1. Monthly Allowance Available -->
<Card class="border-border shadow-xs">
<CardContent class="space-y-2 p-5">
<div class="flex items-center justify-between">
<p class="text-muted-foreground text-sm font-medium">Monthly Allowance</p>
<div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg">
<Wallet class="size-4" aria-hidden="true" />
</div>
</div>
<p class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$315.00 / mo</p>
<div class="text-muted-foreground flex items-center justify-between text-xs">
<span>IRS Pre-tax limit</span>
<span class="text-foreground font-medium tabular-nums">$69.50 left</span>
</div>
</CardContent>
</Card>
<!-- 2. Claimed This Month -->
<Card class="border-border shadow-xs">
<CardContent class="space-y-2 p-5">
<div class="flex items-center justify-between">
<p class="text-muted-foreground text-sm font-medium">Claimed This Month</p>
<div class="bg-info/10 text-info flex size-8 items-center justify-center rounded-lg">
<TrendingUp class="size-4" aria-hidden="true" />
</div>
</div>
<p class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$245.50</p>
<div class="space-y-1.5 pt-0.5">
<Progress :model-value="78" class="h-1.5" />
<p class="text-muted-foreground text-xs tabular-nums">$245.50 / $315.00 · 78% utilized</p>
</div>
</CardContent>
</Card>
<!-- 3. Reimbursed YTD -->
<Card class="border-border shadow-xs">
<CardContent class="space-y-2 p-5">
<div class="flex items-center justify-between">
<p class="text-muted-foreground text-sm font-medium">Reimbursed YTD</p>
<div class="bg-success/10 text-success flex size-8 items-center justify-center rounded-lg">
<CheckCircle2 class="size-4" aria-hidden="true" />
</div>
</div>
<p class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$1,890.00</p>
<div class="text-muted-foreground flex items-center gap-1.5 text-xs">
<span class="text-success inline-flex items-center font-medium">
<ShieldCheck class="mr-1 size-3.5" aria-hidden="true" />
100% Settled
</span>
<span>· 6 claims in 2026</span>
</div>
</CardContent>
</Card>
<!-- 4. Pending Review -->
<Card class="border-border shadow-xs">
<CardContent class="space-y-2 p-5">
<div class="flex items-center justify-between">
<p class="text-muted-foreground text-sm font-medium">Pending Review</p>
<div class="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-lg">
<Clock class="size-4" aria-hidden="true" />
</div>
</div>
<p class="text-foreground text-warning text-warning text-2xl font-bold tracking-tight tabular-nums">
1 Claim · $65.00
</p>
<p class="text-muted-foreground text-xs">Est. payout on Sep 1st payroll</p>
</CardContent>
</Card>
</div>
<!-- Main 2-Column Portal Section -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-12">
<!-- Left Column: Submit Claim Interactive Card & Policy Card -->
<div class="space-y-6 lg:col-span-5">
<!-- Submit Claim Card -->
<Card id="commute-submit-form" class="border-border shadow-xs">
<CardHeader class="pb-3">
<div class="flex items-center justify-between">
<div>
<CardTitle class="text-base font-semibold">Submit Commute Claim</CardTitle>
<CardDescription class="text-xs">
File transit pass, parking, mileage, or EV charging for monthly reimbursement.
</CardDescription>
</div>
<Badge wrap variant="secondary" class="text-xs font-medium">
{{ props.claimPeriod }}
</Badge>
</div>
</CardHeader>
<CardContent class="space-y-4">
<!-- Submission Alert Notice -->
<div
v-if="formSubmittedNotice"
class="border-success/30 bg-success/10 text-success dark:text-foreground flex items-start gap-2.5 rounded-lg border p-3"
>
<CheckCircle2 class="text-success mt-0.5 size-4 shrink-0" />
<div class="text-xs">
<p class="font-semibold">Claim {{ recentlySubmittedClaimId }} submitted successfully!</p>
<p class="text-success/90 mt-0.5">
Itemized receipt received. Your claim has been added to pending payroll review.
</p>
</div>
</div>
<!-- Category Selector -->
<div class="space-y-1.5">
<label for="commute-category-select" class="text-foreground text-xs font-medium">
Expense Category <span class="text-destructive">*</span>
</label>
<Select
:model-value="formCategory"
@update:model-value="(v) => handleCategoryChange(v as CommuteCategory)"
>
<SelectTrigger id="commute-category-select" class="w-full text-xs">
<SelectValue placeholder="Select commute category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="transit">
<span class="flex items-center gap-2">
<Train class="text-info size-3.5" />
<span>Public Transit Pass: BART/Metro</span>
</span>
</SelectItem>
<SelectItem value="parking">
<span class="flex items-center gap-2">
<Building2 class="text-chart-1 size-3.5" />
<span>Parking (Station & Garage)</span>
</span>
</SelectItem>
<SelectItem value="mileage">
<span class="flex items-center gap-2">
<Car class="text-warning size-3.5" />
<span>Mileage (IRS $0.67/mi)</span>
</span>
</SelectItem>
<SelectItem value="ev_charging">
<span class="flex items-center gap-2">
<Zap class="text-success size-3.5" />
<span>EV Public Charging</span>
</span>
</SelectItem>
</SelectContent>
</Select>
<p class="text-muted-foreground text-xs">
{{ categoryMeta[formCategory].irsLimitInfo }}
</p>
</div>
<!-- Date & Provider -->
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div class="space-y-1.5">
<label for="commute-date-input" class="text-foreground text-xs font-medium">
Date of Commute <span class="text-destructive">*</span>
</label>
<Input id="commute-date-input" v-model="formDate" type="date" class="h-9 text-xs" />
</div>
<div class="space-y-1.5">
<label for="commute-provider-input" class="text-foreground text-xs font-medium">
Provider / Authority <span class="text-destructive">*</span>
</label>
<Input
id="commute-provider-input"
v-model="formProvider"
placeholder="e.g. BART, Caltrain, EVgo"
class="h-9 text-xs"
/>
</div>
</div>
<!-- Description -->
<div class="space-y-1.5">
<label for="commute-desc-input" class="text-foreground text-xs font-medium">
Claim Description & Route <span class="text-destructive">*</span>
</label>
<Input
id="commute-desc-input"
v-model="formDescription"
placeholder="e.g. Monthly Clipper Pass · Embarcadero to Millbrae"
class="h-9 text-xs"
/>
</div>
<!-- Mileage Calculation helper if mileage selected -->
<div v-if="formCategory === 'mileage'" class="border-border bg-muted/30 space-y-2 rounded-lg border p-3">
<div class="flex items-center justify-between text-xs">
<span class="text-muted-foreground font-medium">IRS Standard Mileage Rate</span>
<Badge wrap variant="outline" class="text-xs font-normal">$0.67 / mile</Badge>
</div>
<div class="flex items-center gap-3">
<div class="flex-1">
<label for="commute-miles-input" class="text-muted-foreground text-xs">Total Miles Driven</label>
<Input
id="commute-miles-input"
v-model="formMiles"
type="number"
min="1"
step="1"
class="mt-1 h-8 text-xs tabular-nums"
@input="updateMileageAmount"
/>
</div>
<div class="flex-1 text-right">
<span class="text-muted-foreground text-xs">Calculated Total</span>
<p class="text-foreground text-base font-bold tabular-nums">${{ formAmount }}</p>
</div>
</div>
</div>
<!-- Claim Amount Input -->
<div class="space-y-1.5">
<label for="commute-amount-input" class="text-foreground text-xs font-medium">
Claim Amount (USD) <span class="text-destructive">*</span>
</label>
<div class="relative">
<DollarSign
class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-3.5 -translate-y-1/2"
/>
<Input
id="commute-amount-input"
v-model="formAmount"
type="text"
placeholder="0.00"
class="h-9 pl-8 text-xs font-semibold tabular-nums"
/>
</div>
<div class="text-muted-foreground flex items-center justify-between text-xs">
<span>Maximum pre-tax monthly cap: $315.00</span>
<span class="font-medium">Direct Payroll Payout</span>
</div>
</div>
<!-- Receipt Dropzone Placeholder -->
<div class="space-y-1.5">
<label class="text-foreground text-xs font-medium">
Itemized Receipt Attachment <span class="text-destructive">*</span>
</label>
<!-- Attached file view -->
<div
v-if="formAttachedReceipt"
class="border-border bg-muted/40 flex items-center justify-between gap-3 rounded-lg border p-3 text-xs"
>
<div class="flex min-w-0 items-center gap-2.5">
<div class="bg-primary/10 text-primary flex size-8 shrink-0 items-center justify-center rounded-md">
<FileText class="size-4" />
</div>
<div class="min-w-0">
<p class="text-foreground truncate font-medium">{{ formAttachedReceipt.name }}</p>
<p class="text-muted-foreground truncate">
{{ formAttachedReceipt.size }} · Itemized proof attached
</p>
</div>
</div>
<div class="flex items-center gap-1.5">
<Badge wrap variant="outline" class="border-success/30 bg-success/10 text-success text-xs">
<Check class="mr-1 size-3" />
Valid
</Badge>
<Button
type="button"
variant="ghost"
size="icon-sm"
class="text-muted-foreground hover:text-foreground size-7"
@click="handleRemoveReceipt"
>
<X class="size-3.5" />
<span class="sr-only">Remove receipt</span>
</Button>
</div>
</div>
<!-- Dropzone container -->
<button
v-else
type="button"
class="border-border hover:bg-muted/40 hover:border-primary/50 focus-visible:ring-ring flex w-full flex-col items-center justify-center gap-1.5 rounded-lg border border-dashed p-4 text-center transition-colors focus-visible:ring-2 focus-visible:outline-none"
@click="handleAttachSampleReceipt"
>
<div class="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-full">
<Upload class="size-4" />
</div>
<p class="text-foreground text-xs font-medium">Click to upload or drag receipt file</p>
<p class="text-muted-foreground text-xs">PDF, PNG, or JPG up to 10MB (Itemized proof required)</p>
</button>
</div>
</CardContent>
<CardFooter class="border-border flex items-center justify-between border-t pt-4">
<Button type="button" variant="ghost" size="sm" class="text-xs" @click="handleResetForm">
<RotateCcw class="mr-1.5 size-3.5" />
Reset Form
</Button>
<Button
type="button"
size="sm"
class="gap-1.5 text-xs shadow-xs"
:disabled="!formAmount || parseFloat(formAmount) <= 0"
@click="handleSubmitClaim"
>
<Send class="size-3.5" />
Submit Claim
</Button>
</CardFooter>
</Card>
<!-- Compliance & IRS Rules Notice Card -->
<Card class="border-border shadow-xs">
<CardHeader class="pb-2">
<div class="flex items-center gap-2">
<ShieldCheck class="text-primary size-4" />
<CardTitle class="text-sm font-semibold">Commuter Benefit Program Policy</CardTitle>
</div>
</CardHeader>
<CardContent class="space-y-2.5 text-xs">
<p class="text-muted-foreground leading-relaxed">
Expenses are reimbursed under Section 132(f) Qualified Transportation Fringe Benefits. Reimbursements are
disbursed directly into employee payroll on the 1st of every month.
</p>
<div class="border-border/60 text-muted-foreground space-y-1.5 border-t pt-2">
<div class="flex items-center gap-2">
<Check class="text-success size-3" />
<span>Monthly maximum pre-tax election: $315.00</span>
</div>
<div class="flex items-center gap-2">
<Check class="text-success size-3" />
<span>Receipts must reflect date, transit authority, and paid amount</span>
</div>
<div class="flex items-center gap-2">
<Check class="text-success size-3" />
<span>Deadline for current month submissions: 25th of month</span>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- Right Column: Expense Claims History Table -->
<div class="space-y-4 lg:col-span-7">
<Card class="border-border shadow-xs">
<CardHeader class="pb-3">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardTitle class="text-base font-semibold">Expense Claims History</CardTitle>
<CardDescription class="text-xs">
Itemized ledger of submitted commuter, transit, and EV expenses.
</CardDescription>
</div>
<Badge wrap variant="outline" class="text-xs tabular-nums"> {{ claims.length }} Total Claims </Badge>
</div>
</CardHeader>
<CardContent class="space-y-4">
<!-- Filter & Search Toolbar -->
<div
class="border-border bg-muted/20 flex flex-col gap-2.5 rounded-lg border p-2.5 sm:flex-row sm:items-center sm:justify-between"
>
<div class="relative flex-1 sm:max-w-xs">
<Search
class="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2"
/>
<Input
v-model="searchQuery"
type="search"
placeholder="Search claims, ID, route..."
class="h-8 pl-8 text-xs"
/>
</div>
<div class="flex flex-wrap items-center gap-2">
<!-- Category Filter -->
<Select v-model="categoryFilter">
<SelectTrigger class="h-8 w-36 text-xs">
<SelectValue placeholder="All Categories" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
<SelectItem value="transit">Public Transit</SelectItem>
<SelectItem value="parking">Parking</SelectItem>
<SelectItem value="mileage">Mileage</SelectItem>
<SelectItem value="ev_charging">EV Charging</SelectItem>
</SelectContent>
</Select>
<!-- Status Filter -->
<Select v-model="statusFilter">
<SelectTrigger class="h-8 w-36 text-xs">
<SelectValue placeholder="All Statuses" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="reimbursed">Reimbursed</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
</SelectContent>
</Select>
<Button v-if="isFiltered" variant="ghost" size="sm" class="h-8 px-2 text-xs" @click="resetFilters">
<RotateCcw class="mr-1 size-3" />
Reset
</Button>
</div>
</div>
<!-- Table -->
<div class="border-border overflow-hidden rounded-lg border">
<div class="overflow-x-auto">
<Table>
<TableHeader class="bg-muted/40">
<TableRow>
<TableHead class="min-w-[95px] text-xs">Claim ID</TableHead>
<TableHead class="min-w-[90px] text-xs">Date</TableHead>
<TableHead class="min-w-[110px] text-xs">Category</TableHead>
<TableHead class="min-w-[190px] text-xs">Description & Provider</TableHead>
<TableHead class="min-w-[90px] text-right text-xs">Amount</TableHead>
<TableHead class="min-w-[130px] text-xs">Status</TableHead>
<TableHead class="min-w-[80px] text-center text-xs">Receipt</TableHead>
<TableHead class="w-10 text-right text-xs">
<span class="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="claim in filteredClaims"
:key="claim.id"
class="hover:bg-muted/50 transition-colors"
>
<!-- Claim ID -->
<TableCell class="font-mono text-xs font-medium">
{{ claim.id }}
</TableCell>
<!-- Date -->
<TableCell class="text-muted-foreground text-xs whitespace-nowrap tabular-nums">
{{ claim.date }}
</TableCell>
<!-- Category Badge -->
<TableCell>
<Badge
wrap
variant="outline"
:class="cn('text-xs font-normal', categoryMeta[claim.category]?.badgeClass)"
>
<component :is="categoryMeta[claim.category]?.icon" class="mr-1 size-3" />
{{ claim.categoryLabel }}
</Badge>
</TableCell>
<!-- Description & Provider -->
<TableCell>
<div class="min-w-0">
<p class="text-foreground truncate text-xs font-medium">{{ claim.description }}</p>
<p class="text-muted-foreground truncate text-xs">{{ claim.provider }}</p>
</div>
</TableCell>
<!-- Amount -->
<TableCell
class="text-foreground text-right text-xs font-semibold whitespace-nowrap tabular-nums"
>
${{ claim.amount.toFixed(2) }}
</TableCell>
<!-- Status Badge -->
<TableCell>
<Badge wrap variant="outline" :class="cn('text-xs font-medium', statusStyles[claim.status])">
<span
class="mr-1.5 size-1.5 rounded-full"
:class="statusDots[claim.status]"
aria-hidden="true"
/>
{{ claim.statusLabel }}
</Badge>
</TableCell>
<!-- Receipt Button -->
<TableCell class="text-center">
<Button
aria-label="Attach file"
variant="ghost"
size="sm"
class="h-7 px-2 text-xs"
@click="openReceipt(claim)"
>
<Paperclip class="text-muted-foreground mr-1 size-3" />
<span>View</span>
</Button>
</TableCell>
<!-- Actions Dropdown -->
<TableCell class="text-right">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
size="icon-sm"
class="text-muted-foreground hover:text-foreground size-7"
>
<Eye class="size-3.5" />
<span class="sr-only">Actions for {{ claim.id }}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-44">
<DropdownMenuItem class="cursor-pointer text-xs" @click="openReceipt(claim)">
<Receipt class="mr-2 size-3.5" />
Inspect Receipt
</DropdownMenuItem>
<DropdownMenuItem class="cursor-pointer text-xs">
<FileCheck class="mr-2 size-3.5" />
Policy Audit Log
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem class="cursor-pointer text-xs">
<Download class="mr-2 size-3.5" />
Download Voucher
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<!-- Empty state -->
<TableRow v-if="filteredClaims.length === 0">
<TableCell :colspan="8" class="h-32 text-center">
<div class="flex flex-col items-center justify-center gap-1.5">
<Search class="text-muted-foreground/50 size-5" />
<p class="text-foreground text-xs font-medium">No commute claims found</p>
<p class="text-muted-foreground text-xs">Try resetting your category or status filter</p>
<Button variant="outline" size="sm" class="mt-1 h-7 text-xs" @click="resetFilters">
Reset filters
</Button>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<!-- Table Summary Footer -->
<div
class="border-border bg-muted/20 text-muted-foreground flex flex-col items-center justify-between gap-2 border-t px-3 py-2 text-xs sm:flex-row"
>
<p class="tabular-nums">
Showing <span class="text-foreground font-medium">{{ filteredClaims.length }}</span> of
<span class="text-foreground font-medium">{{ claims.length }}</span> claims
</p>
<div class="flex items-center gap-3">
<span class="tabular-nums">
Filtered Total:
<span class="text-foreground font-semibold">
${{ filteredClaims.reduce((acc, c) => acc + c.amount, 0).toFixed(2) }}
</span>
</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
<!-- Receipt Details Modal Dialog -->
<Dialog v-model:open="isReceiptModalOpen">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<div class="flex items-center justify-between pr-4">
<div class="space-y-0.5">
<DialogTitle class="text-base font-bold">Commute Receipt Voucher</DialogTitle>
<DialogDescription class="text-xs">
IRS-compliant proof of transit and commuter benefit claim.
</DialogDescription>
</div>
<Badge wrap variant="outline" class="font-mono text-xs">
{{ selectedClaim.id }}
</Badge>
</div>
</DialogHeader>
<div class="space-y-4 py-2 text-xs">
<!-- Itemized Receipt Box -->
<div class="border-border bg-muted/30 space-y-3 rounded-lg border p-3.5">
<div class="flex items-center justify-between border-b pb-2">
<div class="flex items-center gap-2">
<component :is="categoryMeta[selectedClaim.category]?.icon" class="text-primary size-4" />
<span class="text-foreground font-semibold">{{ selectedClaim.provider }}</span>
</div>
<Badge wrap :class="cn('text-xs font-normal', statusStyles[selectedClaim.status])">
{{ selectedClaim.statusLabel }}
</Badge>
</div>
<div class="grid grid-cols-1 gap-2 text-xs sm:grid-cols-2">
<div>
<span class="text-muted-foreground block">Transaction Date</span>
<span class="text-foreground font-medium">{{ selectedClaim.receiptDate }}</span>
</div>
<div>
<span class="text-muted-foreground block">Benefit Category</span>
<span class="text-foreground font-medium">{{ selectedClaim.categoryLabel }}</span>
</div>
<div class="sm:col-span-2">
<span class="text-muted-foreground block">Line Item Description</span>
<span class="text-foreground font-medium">{{ selectedClaim.description }}</span>
</div>
<div class="sm:col-span-2">
<span class="text-muted-foreground block">Employee Commute Note</span>
<span class="text-muted-foreground italic">"{{ selectedClaim.notes }}"</span>
</div>
</div>
<Separator />
<!-- Amount Breakdown -->
<div class="space-y-1.5 text-xs">
<div class="text-muted-foreground flex justify-between">
<span>Base Fare / Charge</span>
<span class="font-mono tabular-nums">${{ selectedClaim.amount.toFixed(2) }}</span>
</div>
<div class="text-muted-foreground flex justify-between">
<span>Pre-Tax IRS Exemption (IRC § 132f)</span>
<span class="text-success text-success font-mono tabular-nums">-$0.00 (Tax Free)</span>
</div>
<div class="border-border flex justify-between border-t pt-1.5 text-sm font-bold">
<span class="text-foreground">Total Reimbursable Amount</span>
<span class="text-foreground font-mono tabular-nums">${{ selectedClaim.amount.toFixed(2) }}</span>
</div>
</div>
<!-- Attached File Strip -->
<div class="border-border bg-background flex items-center justify-between rounded-md border p-2 text-xs">
<div class="flex items-center gap-2">
<FileText class="text-primary size-3.5" />
<span class="font-mono text-xs">{{ selectedClaim.receiptFileName }}</span>
<span class="text-muted-foreground">({{ selectedClaim.receiptFileSize }})</span>
</div>
<Badge wrap variant="outline" class="border-success/30 bg-success/10 text-success text-xs">
<ShieldCheck class="mr-1 size-3" />
Verified
</Badge>
</div>
</div>
</div>
<DialogFooter class="flex sm:justify-between">
<Button variant="outline" size="sm" class="text-xs" @click="isReceiptModalOpen = false"> Close </Button>
<Button
aria-label="Download attachment"
size="sm"
class="gap-1.5 text-xs"
@click="isReceiptModalOpen = false"
>
<Download class="size-3.5" />
Download Receipt PDF
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>
Raw manifest:https://uipkge.dev/r/vue/commute-reimbursement-claims.json