Commute Reimbursement Claims
blockhrMonthly 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.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/commute-reimbursement-claims.json$npx shadcn@latest add https://uipkge.dev/r/react/commute-reimbursement-claims.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/commute-reimbursement-claims.json$bunx shadcn@latest add https://uipkge.dev/r/react/commute-reimbursement-claims.jsonNamed registry:
npx shadcn@latest add @uipkge-react/commute-reimbursement-claimsInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
title | string | — | optional |
policyText | string | — | optional |
claimPeriod | string | — | optional |
className | string | — | optional |
Schema
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
}Files installed (1)
components/blocks/CommuteReimbursementClaims.tsx45.7 kB
'use client' import * as React from 'react' 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-react' 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 } export interface CommuteReimbursementClaimsProps { title?: string policyText?: string claimPeriod?: string className?: string } const categoryMeta: Record< CommuteCategory, { label: string shortLabel: string badgeClass: string icon: React.ComponentType<{ className?: string }> 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 initialClaimsData: 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.', }, ] export function CommuteReimbursementClaims({ title = 'Commute & Transit Expense Claims', policyText = 'Monthly Commuter Benefit Program · Up to $315.00/mo IRS pre-tax limit', claimPeriod = 'August 2026', className, }: CommuteReimbursementClaimsProps) { const [claims, setClaims] = React.useState<ExpenseClaim[]>(initialClaimsData) // Form State const [formCategory, setFormCategory] = React.useState<CommuteCategory>('transit') const [formDate, setFormDate] = React.useState('2026-08-20') const [formProvider, setFormProvider] = React.useState('BART Clipper Card') const [formDescription, setFormDescription] = React.useState('Monthly BART Clipper Card Pass') const [formAmount, setFormAmount] = React.useState('120.00') const [formMiles, setFormMiles] = React.useState('45') const [formAttachedReceipt, setFormAttachedReceipt] = React.useState<{ name: string; size: string } | null>({ name: 'clipper-reload-receipt.pdf', size: '240 KB', }) const [formSubmittedNotice, setFormSubmittedNotice] = React.useState(false) const [recentlySubmittedClaimId, setRecentlySubmittedClaimId] = React.useState('') // Filter State const [searchQuery, setSearchQuery] = React.useState('') const [categoryFilter, setCategoryFilter] = React.useState('all') const [statusFilter, setStatusFilter] = React.useState('all') // Receipt Modal State const [isReceiptModalOpen, setIsReceiptModalOpen] = React.useState(false) const [selectedClaim, setSelectedClaim] = React.useState<ExpenseClaim>(initialClaimsData[0]) const openReceipt = (claim: ExpenseClaim) => { setSelectedClaim(claim) setIsReceiptModalOpen(true) } const handleCategoryChange = (val: CommuteCategory) => { setFormCategory(val) if (val === 'transit') { setFormProvider('BART Clipper Card') setFormDescription('Monthly BART Clipper Card Pass') setFormAmount('120.00') } else if (val === 'parking') { setFormProvider('Downtown Station Garage') setFormDescription('Monthly Station Parking Permit') setFormAmount('65.00') } else if (val === 'mileage') { setFormProvider('Personal Vehicle (IRS Standard Rate)') setFormDescription('Commute detour travel to client site') const miles = parseFloat(formMiles) || 0 setFormAmount((miles * 0.67).toFixed(2)) } else if (val === 'ev_charging') { setFormProvider('EVgo Fast Charging') setFormDescription('Workplace EV public charging sessions') setFormAmount('42.00') } } const handleMilesChange = (val: string) => { setFormMiles(val) const miles = parseFloat(val) || 0 setFormAmount((miles * 0.67).toFixed(2)) } const handleAttachSampleReceipt = () => { setFormAttachedReceipt({ name: `receipt-${formCategory}-${Date.now().toString().slice(-4)}.pdf`, size: '318 KB', }) } const handleRemoveReceipt = () => { setFormAttachedReceipt(null) } const handleSubmitClaim = () => { const parsedAmt = parseFloat(formAmount) || 0 if (parsedAmt <= 0) return const meta = categoryMeta[formCategory] const newId = `CLM-${Math.floor(8200 + Math.random() * 700)}` const newClaim: ExpenseClaim = { id: newId, date: 'Aug 21, 2026', category: formCategory, categoryLabel: meta.shortLabel, description: formDescription || meta.label, provider: formProvider || 'Transit Provider', amount: parsedAmt, status: 'pending', statusLabel: 'Pending Approval', receiptFileName: formAttachedReceipt?.name || 'commute-claim-receipt.pdf', receiptFileSize: formAttachedReceipt?.size || '290 KB', receiptDate: 'Aug 21, 2026 · Just now', notes: `Submitted via Commuter Benefit Portal for ${claimPeriod}.`, } setClaims((prev) => [newClaim, ...prev]) setRecentlySubmittedClaimId(newId) setFormSubmittedNotice(true) setTimeout(() => { setFormSubmittedNotice(false) }, 6000) } const handleResetForm = () => { setFormCategory('transit') setFormDate('2026-08-20') setFormProvider('BART Clipper Card') setFormDescription('Monthly BART Clipper Card Pass') setFormAmount('120.00') setFormMiles('45') setFormAttachedReceipt({ name: 'clipper-reload-receipt.pdf', size: '240 KB', }) setFormSubmittedNotice(false) } const isFiltered = searchQuery.trim() !== '' || categoryFilter !== 'all' || statusFilter !== 'all' const filteredClaims = React.useMemo(() => { const q = searchQuery.trim().toLowerCase() return claims.filter((claim) => { const matchCat = categoryFilter === 'all' || claim.category === categoryFilter const matchStatus = statusFilter === 'all' || claim.status === statusFilter 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 }) }, [claims, searchQuery, categoryFilter, statusFilter]) const resetFilters = () => { setSearchQuery('') setCategoryFilter('all') setStatusFilter('all') } const scrollToSubmitForm = () => { const el = document.getElementById('commute-submit-form') if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }) } } const selectedCategoryMeta = categoryMeta[selectedClaim.category] const SelectedCategoryIcon = selectedCategoryMeta?.icon || Train return ( <div data-slot="commute-reimbursement-claims" className={cn('w-full space-y-6', className)}> {/* Header */} <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div className="space-y-1"> <div className="flex flex-wrap items-center gap-2.5"> <h2 className="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">{title}</h2> <Badge wrap variant="outline" className="border-info/30 bg-info/10 text-info text-xs font-medium"> <Calendar className="mr-1 size-3" aria-hidden="true" /> {claimPeriod} </Badge> </div> <p className="text-muted-foreground text-sm">{policyText}</p> </div> <div className="flex flex-wrap items-center gap-2.5"> <Button aria-label="Download attachment" variant="outline" size="sm" className="shadow-xs"> <Download className="size-4" aria-hidden="true" /> Export Statement </Button> <Button size="sm" className="shadow-xs" onClick={scrollToSubmitForm}> <Plus className="size-4" aria-hidden="true" /> Submit New Claim </Button> </div> </div> {/* 4 Monthly Benefit Metric Cards */} <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> {/* 1. Monthly Allowance Available */} <Card className="border-border shadow-xs"> <CardContent className="space-y-2 p-5"> <div className="flex items-center justify-between"> <p className="text-muted-foreground text-sm font-medium">Monthly Allowance</p> <div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <Wallet className="size-4" aria-hidden="true" /> </div> </div> <p className="text-foreground text-2xl font-bold tracking-tight tabular-nums">$315.00 / mo</p> <div className="text-muted-foreground flex items-center justify-between text-xs"> <span>IRS Pre-tax limit</span> <span className="text-foreground font-medium tabular-nums">$69.50 left</span> </div> </CardContent> </Card> {/* 2. Claimed This Month */} <Card className="border-border shadow-xs"> <CardContent className="space-y-2 p-5"> <div className="flex items-center justify-between"> <p className="text-muted-foreground text-sm font-medium">Claimed This Month</p> <div className="bg-info/10 text-info flex size-8 items-center justify-center rounded-lg"> <TrendingUp className="size-4" aria-hidden="true" /> </div> </div> <p className="text-foreground text-2xl font-bold tracking-tight tabular-nums">$245.50</p> <div className="space-y-1.5 pt-0.5"> <Progress value={78} className="h-1.5" /> <p className="text-muted-foreground text-xs tabular-nums">$245.50 / $315.00 · 78% utilized</p> </div> </CardContent> </Card> {/* 3. Reimbursed YTD */} <Card className="border-border shadow-xs"> <CardContent className="space-y-2 p-5"> <div className="flex items-center justify-between"> <p className="text-muted-foreground text-sm font-medium">Reimbursed YTD</p> <div className="bg-success/10 text-success flex size-8 items-center justify-center rounded-lg"> <CheckCircle2 className="size-4" aria-hidden="true" /> </div> </div> <p className="text-foreground text-2xl font-bold tracking-tight tabular-nums">$1,890.00</p> <div className="text-muted-foreground flex items-center gap-1.5 text-xs"> <span className="text-success inline-flex items-center font-medium"> <ShieldCheck className="mr-1 size-3.5" aria-hidden="true" /> 100% Settled </span> <span>· 6 claims in 2026</span> </div> </CardContent> </Card> {/* 4. Pending Review */} <Card className="border-border shadow-xs"> <CardContent className="space-y-2 p-5"> <div className="flex items-center justify-between"> <p className="text-muted-foreground text-sm font-medium">Pending Review</p> <div className="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-lg"> <Clock className="size-4" aria-hidden="true" /> </div> </div> <p className="text-foreground text-warning text-warning text-2xl font-bold tracking-tight tabular-nums"> 1 Claim · $65.00 </p> <p className="text-muted-foreground text-xs">Est. payout on Sep 1st payroll</p> </CardContent> </Card> </div> {/* Main 2-Column Portal Section */} <div className="grid grid-cols-1 gap-6 lg:grid-cols-12"> {/* Left Column: Submit Claim Interactive Card & Policy Card */} <div className="space-y-6 lg:col-span-5"> {/* Submit Claim Card */} <Card id="commute-submit-form" className="border-border shadow-xs"> <CardHeader className="pb-3"> <div className="flex items-center justify-between"> <div> <CardTitle className="text-base font-semibold">Submit Commute Claim</CardTitle> <CardDescription className="text-xs"> File transit pass, parking, mileage, or EV charging for monthly reimbursement. </CardDescription> </div> <Badge wrap variant="secondary" className="text-xs font-medium"> {claimPeriod} </Badge> </div> </CardHeader> <CardContent className="space-y-4"> {/* Submission Alert Notice */} {formSubmittedNotice && ( <div className="border-success/30 bg-success/10 text-success dark:text-foreground flex items-start gap-2.5 rounded-lg border p-3"> <CheckCircle2 className="text-success mt-0.5 size-4 shrink-0" /> <div className="text-xs"> <p className="font-semibold">Claim {recentlySubmittedClaimId} submitted successfully!</p> <p className="text-success/90 mt-0.5"> Itemized receipt received. Your claim has been added to pending payroll review. </p> </div> </div> )} {/* Category Selector */} <div className="space-y-1.5"> <label htmlFor="commute-category-select" className="text-foreground text-xs font-medium"> Expense Category <span className="text-destructive">*</span> </label> <Select value={formCategory} onValueChange={(v) => handleCategoryChange(v as CommuteCategory)}> <SelectTrigger id="commute-category-select" className="w-full text-xs"> <SelectValue placeholder="Select commute category" /> </SelectTrigger> <SelectContent> <SelectItem value="transit"> <span className="flex items-center gap-2"> <Train className="text-info size-3.5" /> <span>Public Transit Pass: BART/Metro</span> </span> </SelectItem> <SelectItem value="parking"> <span className="flex items-center gap-2"> <Building2 className="text-chart-1 size-3.5" /> <span>Parking (Station & Garage)</span> </span> </SelectItem> <SelectItem value="mileage"> <span className="flex items-center gap-2"> <Car className="text-warning size-3.5" /> <span>Mileage (IRS $0.67/mi)</span> </span> </SelectItem> <SelectItem value="ev_charging"> <span className="flex items-center gap-2"> <Zap className="text-success size-3.5" /> <span>EV Public Charging</span> </span> </SelectItem> </SelectContent> </Select> <p className="text-muted-foreground text-xs">{categoryMeta[formCategory].irsLimitInfo}</p> </div> {/* Date & Provider */} <div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div className="space-y-1.5"> <label htmlFor="commute-date-input" className="text-foreground text-xs font-medium"> Date of Commute <span className="text-destructive">*</span> </label> <Input id="commute-date-input" value={formDate} onChange={(e) => setFormDate(e.target.value)} type="date" className="h-9 text-xs" /> </div> <div className="space-y-1.5"> <label htmlFor="commute-provider-input" className="text-foreground text-xs font-medium"> Provider / Authority <span className="text-destructive">*</span> </label> <Input id="commute-provider-input" value={formProvider} onChange={(e) => setFormProvider(e.target.value)} placeholder="e.g. BART, Caltrain, EVgo" className="h-9 text-xs" /> </div> </div> {/* Description */} <div className="space-y-1.5"> <label htmlFor="commute-desc-input" className="text-foreground text-xs font-medium"> Claim Description & Route <span className="text-destructive">*</span> </label> <Input id="commute-desc-input" value={formDescription} onChange={(e) => setFormDescription(e.target.value)} placeholder="e.g. Monthly Clipper Pass · Embarcadero to Millbrae" className="h-9 text-xs" /> </div> {/* Mileage Calculation helper if mileage selected */} {formCategory === 'mileage' && ( <div className="border-border bg-muted/30 space-y-2 rounded-lg border p-3"> <div className="flex items-center justify-between text-xs"> <span className="text-muted-foreground font-medium">IRS Standard Mileage Rate</span> <Badge wrap variant="outline" className="text-xs font-normal"> $0.67 / mile </Badge> </div> <div className="flex items-center gap-3"> <div className="flex-1"> <label htmlFor="commute-miles-input" className="text-muted-foreground text-xs"> Total Miles Driven </label> <Input id="commute-miles-input" value={formMiles} onChange={(e) => handleMilesChange(e.target.value)} type="number" min="1" step="1" className="mt-1 h-8 text-xs tabular-nums" /> </div> <div className="flex-1 text-right"> <span className="text-muted-foreground text-xs">Calculated Total</span> <p className="text-foreground text-base font-bold tabular-nums">${formAmount}</p> </div> </div> </div> )} {/* Claim Amount Input */} <div className="space-y-1.5"> <label htmlFor="commute-amount-input" className="text-foreground text-xs font-medium"> Claim Amount (USD) <span className="text-destructive">*</span> </label> <div className="relative"> <DollarSign className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-3.5 -translate-y-1/2" /> <Input id="commute-amount-input" value={formAmount} onChange={(e) => setFormAmount(e.target.value)} type="text" placeholder="0.00" className="h-9 pl-8 text-xs font-semibold tabular-nums" /> </div> <div className="text-muted-foreground flex items-center justify-between text-xs"> <span>Maximum pre-tax monthly cap: $315.00</span> <span className="font-medium">Direct Payroll Payout</span> </div> </div> {/* Receipt Dropzone Placeholder */} <div className="space-y-1.5"> <label className="text-foreground text-xs font-medium"> Itemized Receipt Attachment <span className="text-destructive">*</span> </label> {/* Attached file view */} {formAttachedReceipt ? ( <div className="border-border bg-muted/40 flex items-center justify-between gap-3 rounded-lg border p-3 text-xs"> <div className="flex min-w-0 items-center gap-2.5"> <div className="bg-primary/10 text-primary flex size-8 shrink-0 items-center justify-center rounded-md"> <FileText className="size-4" /> </div> <div className="min-w-0"> <p className="text-foreground truncate font-medium">{formAttachedReceipt.name}</p> <p className="text-muted-foreground truncate"> {formAttachedReceipt.size} · Itemized proof attached </p> </div> </div> <div className="flex items-center gap-1.5"> <Badge wrap variant="outline" className="border-success/30 bg-success/10 text-success text-xs"> <Check className="mr-1 size-3" /> Valid </Badge> <Button type="button" variant="ghost" size="icon-sm" className="text-muted-foreground hover:text-foreground size-7" onClick={handleRemoveReceipt} > <X className="size-3.5" /> <span className="sr-only">Remove receipt</span> </Button> </div> </div> ) : ( /* Dropzone container */ <button type="button" className="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" onClick={handleAttachSampleReceipt} > <div className="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-full"> <Upload className="size-4" /> </div> <p className="text-foreground text-xs font-medium">Click to upload or drag receipt file</p> <p className="text-muted-foreground text-xs"> PDF, PNG, or JPG up to 10MB (Itemized proof required) </p> </button> )} </div> </CardContent> <CardFooter className="border-border flex items-center justify-between border-t pt-4"> <Button type="button" variant="ghost" size="sm" className="text-xs" onClick={handleResetForm}> <RotateCcw className="mr-1.5 size-3.5" /> Reset Form </Button> <Button type="button" size="sm" className="gap-1.5 text-xs shadow-xs" disabled={!formAmount || parseFloat(formAmount) <= 0} onClick={handleSubmitClaim} > <Send className="size-3.5" /> Submit Claim </Button> </CardFooter> </Card> {/* Compliance & IRS Rules Notice Card */} <Card className="border-border shadow-xs"> <CardHeader className="pb-2"> <div className="flex items-center gap-2"> <ShieldCheck className="text-primary size-4" /> <CardTitle className="text-sm font-semibold">Commuter Benefit Program Policy</CardTitle> </div> </CardHeader> <CardContent className="space-y-2.5 text-xs"> <p className="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 className="border-border/60 text-muted-foreground space-y-1.5 border-t pt-2"> <div className="flex items-center gap-2"> <Check className="text-success size-3" /> <span>Monthly maximum pre-tax election: $315.00</span> </div> <div className="flex items-center gap-2"> <Check className="text-success size-3" /> <span>Receipts must reflect date, transit authority, and paid amount</span> </div> <div className="flex items-center gap-2"> <Check className="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 className="space-y-4 lg:col-span-7"> <Card className="border-border shadow-xs"> <CardHeader className="pb-3"> <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <CardTitle className="text-base font-semibold">Expense Claims History</CardTitle> <CardDescription className="text-xs"> Itemized ledger of submitted commuter, transit, and EV expenses. </CardDescription> </div> <Badge wrap variant="outline" className="text-xs tabular-nums"> {claims.length} Total Claims </Badge> </div> </CardHeader> <CardContent className="space-y-4"> {/* Filter & Search Toolbar */} <div className="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 className="relative flex-1 sm:max-w-xs"> <Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> <Input value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} type="search" placeholder="Search claims, ID, route..." className="h-8 pl-8 text-xs" /> </div> <div className="flex flex-wrap items-center gap-2"> {/* Category Filter */} <Select value={categoryFilter} onValueChange={setCategoryFilter}> <SelectTrigger className="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 value={statusFilter} onValueChange={setStatusFilter}> <SelectTrigger className="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> {isFiltered && ( <Button variant="ghost" size="sm" className="h-8 px-2 text-xs" onClick={resetFilters}> <RotateCcw className="mr-1 size-3" /> Reset </Button> )} </div> </div> {/* Table */} <div className="border-border overflow-hidden rounded-lg border"> <div className="overflow-x-auto"> <Table> <TableHeader className="bg-muted/40"> <TableRow> <TableHead className="min-w-[95px] text-xs">Claim ID</TableHead> <TableHead className="min-w-[90px] text-xs">Date</TableHead> <TableHead className="min-w-[110px] text-xs">Category</TableHead> <TableHead className="min-w-[190px] text-xs">Description & Provider</TableHead> <TableHead className="min-w-[90px] text-right text-xs">Amount</TableHead> <TableHead className="min-w-[130px] text-xs">Status</TableHead> <TableHead className="min-w-[80px] text-center text-xs">Receipt</TableHead> <TableHead className="w-10 text-right text-xs"> <span className="sr-only">Actions</span> </TableHead> </TableRow> </TableHeader> <TableBody> {filteredClaims.map((claim) => { const catMeta = categoryMeta[claim.category] const CatIcon = catMeta?.icon || Train return ( <TableRow key={claim.id} className="hover:bg-muted/50 transition-colors"> {/* Claim ID */} <TableCell className="font-mono text-xs font-medium">{claim.id}</TableCell> {/* Date */} <TableCell className="text-muted-foreground text-xs whitespace-nowrap tabular-nums"> {claim.date} </TableCell> {/* Category Badge */} <TableCell> <Badge wrap variant="outline" className={cn('text-xs font-normal', catMeta?.badgeClass)}> <CatIcon className="mr-1 size-3" /> {claim.categoryLabel} </Badge> </TableCell> {/* Description & Provider */} <TableCell> <div className="min-w-0"> <p className="text-foreground truncate text-xs font-medium">{claim.description}</p> <p className="text-muted-foreground truncate text-xs">{claim.provider}</p> </div> </TableCell> {/* Amount */} <TableCell className="text-foreground text-right text-xs font-semibold whitespace-nowrap tabular-nums"> ${claim.amount.toFixed(2)} </TableCell> {/* Status Badge */} <TableCell> <Badge wrap variant="outline" className={cn('text-xs font-medium', statusStyles[claim.status])} > <span className={cn('mr-1.5 size-1.5 rounded-full', statusDots[claim.status])} aria-hidden="true" /> {claim.statusLabel} </Badge> </TableCell> {/* Receipt Button */} <TableCell className="text-center"> <Button aria-label="Attach file" variant="ghost" size="sm" className="h-7 px-2 text-xs" onClick={() => openReceipt(claim)} > <Paperclip className="text-muted-foreground mr-1 size-3" /> <span>View</span> </Button> </TableCell> {/* Actions Dropdown */} <TableCell className="text-right"> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon-sm" className="text-muted-foreground hover:text-foreground size-7" > <Eye className="size-3.5" /> <span className="sr-only">Actions for {claim.id}</span> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-44"> <DropdownMenuItem className="cursor-pointer text-xs" onClick={() => openReceipt(claim)} > <Receipt className="mr-2 size-3.5" /> Inspect Receipt </DropdownMenuItem> <DropdownMenuItem className="cursor-pointer text-xs"> <FileCheck className="mr-2 size-3.5" /> Policy Audit Log </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem className="cursor-pointer text-xs"> <Download className="mr-2 size-3.5" /> Download Voucher </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </TableCell> </TableRow> ) })} {/* Empty state */} {filteredClaims.length === 0 && ( <TableRow> <TableCell colSpan={8} className="h-32 text-center"> <div className="flex flex-col items-center justify-center gap-1.5"> <Search className="text-muted-foreground/50 size-5" /> <p className="text-foreground text-xs font-medium">No commute claims found</p> <p className="text-muted-foreground text-xs"> Try resetting your category or status filter </p> <Button variant="outline" size="sm" className="mt-1 h-7 text-xs" onClick={resetFilters}> Reset filters </Button> </div> </TableCell> </TableRow> )} </TableBody> </Table> </div> {/* Table Summary Footer */} <div className="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 className="tabular-nums"> Showing <span className="text-foreground font-medium">{filteredClaims.length}</span> of{' '} <span className="text-foreground font-medium">{claims.length}</span> claims </p> <div className="flex items-center gap-3"> <span className="tabular-nums"> Filtered Total:{' '} <span className="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 open={isReceiptModalOpen} onOpenChange={setIsReceiptModalOpen}> <DialogContent className="sm:max-w-md"> <DialogHeader> <div className="flex items-center justify-between pr-4"> <div className="space-y-0.5"> <DialogTitle className="text-base font-bold">Commute Receipt Voucher</DialogTitle> <DialogDescription className="text-xs"> IRS-compliant proof of transit and commuter benefit claim. </DialogDescription> </div> <Badge wrap variant="outline" className="font-mono text-xs"> {selectedClaim.id} </Badge> </div> </DialogHeader> <div className="space-y-4 py-2 text-xs"> {/* Itemized Receipt Box */} <div className="border-border bg-muted/30 space-y-3 rounded-lg border p-3.5"> <div className="flex items-center justify-between border-b pb-2"> <div className="flex items-center gap-2"> <SelectedCategoryIcon className="text-primary size-4" /> <span className="text-foreground font-semibold">{selectedClaim.provider}</span> </div> <Badge wrap className={cn('text-xs font-normal', statusStyles[selectedClaim.status])}> {selectedClaim.statusLabel} </Badge> </div> <div className="grid grid-cols-1 gap-2 text-xs sm:grid-cols-2"> <div> <span className="text-muted-foreground block">Transaction Date</span> <span className="text-foreground font-medium">{selectedClaim.receiptDate}</span> </div> <div> <span className="text-muted-foreground block">Benefit Category</span> <span className="text-foreground font-medium">{selectedClaim.categoryLabel}</span> </div> <div className="sm:col-span-2"> <span className="text-muted-foreground block">Line Item Description</span> <span className="text-foreground font-medium">{selectedClaim.description}</span> </div> <div className="sm:col-span-2"> <span className="text-muted-foreground block">Employee Commute Note</span> <span className="text-muted-foreground italic">"{selectedClaim.notes}"</span> </div> </div> <Separator /> {/* Amount Breakdown */} <div className="space-y-1.5 text-xs"> <div className="text-muted-foreground flex justify-between"> <span>Base Fare / Charge</span> <span className="font-mono tabular-nums">${selectedClaim.amount.toFixed(2)}</span> </div> <div className="text-muted-foreground flex justify-between"> <span>Pre-Tax IRS Exemption (IRC § 132f)</span> <span className="text-success text-success font-mono tabular-nums">-$0.00 (Tax Free)</span> </div> <div className="border-border flex justify-between border-t pt-1.5 text-sm font-bold"> <span className="text-foreground">Total Reimbursable Amount</span> <span className="text-foreground font-mono tabular-nums">${selectedClaim.amount.toFixed(2)}</span> </div> </div> {/* Attached File Strip */} <div className="border-border bg-background flex items-center justify-between rounded-md border p-2 text-xs"> <div className="flex items-center gap-2"> <FileText className="text-primary size-3.5" /> <span className="font-mono text-xs">{selectedClaim.receiptFileName}</span> <span className="text-muted-foreground">({selectedClaim.receiptFileSize})</span> </div> <Badge wrap variant="outline" className="border-success/30 bg-success/10 text-success text-xs"> <ShieldCheck className="mr-1 size-3" /> Verified </Badge> </div> </div> </div> <DialogFooter className="flex sm:justify-between"> <Button variant="outline" size="sm" className="text-xs" onClick={() => setIsReceiptModalOpen(false)}> Close </Button> <Button aria-label="Download attachment" size="sm" className="gap-1.5 text-xs" onClick={() => setIsReceiptModalOpen(false)} > <Download className="size-3.5" /> Download Receipt PDF </Button> </DialogFooter> </DialogContent> </Dialog> </div> ) }
Raw manifest:https://uipkge.dev/r/react/commute-reimbursement-claims.json