
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Mobile/Desktop commercial property & equipment site inspection audit manager featuring GPS geostamp telemetry, 4 KPI metric cards, searchable audits table with pass/fail checklist scores, photo evidence thumbnail gallery with interactive lightbox modal, cryptographic sign-off badges, and work order dispatch workflows.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/field-inspection-manager.json$npx shadcn@latest add https://uipkge.dev/r/react/field-inspection-manager.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/field-inspection-manager.json$bunx shadcn@latest add https://uipkge.dev/r/react/field-inspection-manager.jsonnpx shadcn@latest add @uipkge-react/field-inspection-managerInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
initialFilter | 'all' | 'passed' | 'critical' | 'minor' | — | optional |
className | string | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
PhotoEvidenceinterface PhotoEvidence {
id: string
title: string
caption: string
time: string
previewType:
| 'gauge'
| 'coil'
| 'mount'
| 'thermal'
| 'box'
| 'conduit'
| 'riser'
| 'pump'
| 'switch'
| 'tank'
| 'battery'
| 'ats'
| 'spall'
| 'rebar'
| 'crack'
}ChecklistIteminterface ChecklistItem {
id: string
label: string
status: 'pass' | 'fail' | 'minor'
value: string
}InspectionRecordinterface InspectionRecord {
id: string
assetId: string
assetName: string
category: string
facility: string
buildingBadge: string
date: string
time: string
gps: string
gpsAccuracy: string
scorePassed: number
scoreTotal: number
status: InspectionStatus
statusLabel: string
duration: string
defectSummary?: string
workOrder?: string
photos: PhotoEvidence[]
inspector: {
name: string
role: string
initials: string
badge: string
signoffStatus: string
signoffHash: string
}
checklist: ChecklistItem[]
}'use client'
import * as React from 'react'
import {
AlertCircle,
AlertTriangle,
Building2,
Calendar,
Camera,
Check,
CheckCircle2,
ClipboardCheck,
Clock,
Copy,
Download,
Eye,
MapPin,
MoreHorizontal,
Plus,
Search,
ShieldAlert,
ShieldCheck,
Wrench,
} from 'lucide-react'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { cn } from '@/lib/utils'
import { PhotoLightboxDialog } from './PhotoLightboxDialog'
import { AuditReportDialog } from './AuditReportDialog'
import { NewAuditDialog } from './NewAuditDialog'
import { InspectionMetrics } from './InspectionMetrics'
import { InspectionThumbnail } from './InspectionThumbnail'
import { INITIAL_INSPECTION_RECORDS } from './field-inspection-data'
import type { InspectionStatus, PhotoEvidence, ChecklistItem, InspectionRecord } from './field-inspection-types'
export type { InspectionStatus, PhotoEvidence, ChecklistItem, InspectionRecord }
export interface FieldInspectionManagerProps {
initialFilter?: 'all' | 'passed' | 'critical' | 'minor'
className?: string
}
export function FieldInspectionManager({ initialFilter = 'all', className }: FieldInspectionManagerProps) {
// Active UI states
const [activeFilter, setActiveFilter] = React.useState<'all' | 'passed' | 'critical' | 'minor'>(initialFilter)
const [searchQuery, setSearchQuery] = React.useState('')
const [selectedFacility, setSelectedFacility] = React.useState('all')
const [copiedGpsId, setCopiedGpsId] = React.useState<string | null>(null)
const [toastMessage, setToastMessage] = React.useState<string | null>(null)
// Dialog Modals
const [previewPhoto, setPreviewPhoto] = React.useState<{ photo: PhotoEvidence; record: InspectionRecord } | null>(
null,
)
const [selectedAudit, setSelectedAudit] = React.useState<InspectionRecord | null>(null)
const [isNewAuditOpen, setIsNewAuditOpen] = React.useState(false)
// Inspection Records Data
const [inspectionRecords] = React.useState<InspectionRecord[]>(INITIAL_INSPECTION_RECORDS)
// Filtered Records
const filteredRecords = React.useMemo(() => {
return inspectionRecords.filter((record) => {
// Status Filter
if (activeFilter === 'passed' && record.status !== 'passed') return false
if (activeFilter === 'critical' && record.status !== 'critical') return false
if (activeFilter === 'minor' && record.status !== 'minor') return false
// Facility Filter
if (selectedFacility !== 'all' && !record.facility.includes(selectedFacility)) return false
// Search Query
if (searchQuery.trim() !== '') {
const q = searchQuery.toLowerCase()
const matchesText =
record.assetName.toLowerCase().includes(q) ||
record.assetId.toLowerCase().includes(q) ||
record.facility.toLowerCase().includes(q) ||
record.category.toLowerCase().includes(q)
if (!matchesText) return false
}
return true
})
}, [inspectionRecords, activeFilter, selectedFacility, searchQuery])
const copyGps = (gps: string, recordId: string) => {
navigator.clipboard?.writeText(gps)
setCopiedGpsId(recordId)
showToast(`GPS Coordinates ${gps} copied to clipboard`)
setTimeout(() => {
setCopiedGpsId((curr) => (curr === recordId ? null : curr))
}, 2200)
}
const showToast = (msg: string) => {
setToastMessage(msg)
setTimeout(() => {
setToastMessage((curr) => (curr === msg ? null : curr))
}, 3000)
}
const handleOpenPhoto = (photo: PhotoEvidence, record: InspectionRecord) => {
setPreviewPhoto({ photo, record })
}
const handleViewReport = (record: InspectionRecord) => {
setSelectedAudit(record)
}
const handleDownloadPdf = (record: InspectionRecord) => {
showToast(`Downloading certified PDF audit report for ${record.assetId}...`)
}
const handleCreateWorkOrder = (record: InspectionRecord) => {
showToast(`Work Order generated for ${record.assetName} (Assigned to Facilities Ops)`)
}
return (
<div data-slot="field-inspection-manager" className={cn('text-foreground w-full space-y-6', className)}>
{/* Toast Banner */}
{toastMessage && (
<div className="border-primary/30 bg-primary/10 text-foreground fixed top-4 right-4 z-50 flex items-center gap-2 rounded-lg border px-4 py-2.5 text-xs font-medium shadow-lg backdrop-blur-md">
<CheckCircle2 className="text-primary size-4 shrink-0" />
<span>{toastMessage}</span>
</div>
)}
{/* Main Header */}
<Card className="border-border shadow-xs">
<CardHeader className="flex flex-col gap-4 pb-6 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline" className="gap-1.5 font-mono text-xs">
<ClipboardCheck className="text-primary size-3.5" aria-hidden="true" />
ISO-55001 & OSHA Audit Matrix
</Badge>
<div className="border-success/20 bg-success/10 text-success inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium">
<span className="relative flex size-1.5">
<span className="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
<span className="bg-success relative inline-flex size-1.5 rounded-full"></span>
</span>
8 Audits Completed Today · 100% On-Time
</div>
</div>
<div>
<h1 className="text-foreground text-xl font-bold tracking-tight sm:text-2xl">
Site & Asset Inspection Audits
</h1>
<div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-xs sm:text-sm">
<div className="text-foreground flex items-center gap-1.5 font-medium">
<Avatar className="border-border size-5 border">
<AvatarFallback className="bg-primary/10 text-primary text-xs font-bold">MV</AvatarFallback>
</Avatar>
<span>Marcus Vance</span>
</div>
<span className="text-muted-foreground">Senior Field Engineer (PE #84920-CA)</span>
<span className="text-muted-foreground font-mono">Terminal: Apex Facility West</span>
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-2.5 pt-1">
<Button
aria-label="Download attachment"
variant="outline"
size="sm"
className="gap-1.5 text-xs font-medium shadow-xs"
onClick={() => showToast('Exporting complete site inspection audit summary CSV...')}
>
<Download className="size-3.5" aria-hidden="true" />
Export Audits CSV
</Button>
<Button size="sm" className="gap-1.5 text-xs font-medium shadow-xs" onClick={() => setIsNewAuditOpen(true)}>
<Plus className="size-3.5" aria-hidden="true" />
Start New Inspection
</Button>
</div>
</CardHeader>
</Card>
{/* 4 KPI Cards */}
<InspectionMetrics />
{/* Search & Filter Toolbar */}
<Card className="border-border shadow-xs">
<CardContent className="p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
{/* Filter Pills */}
<div className="flex flex-wrap items-center gap-1.5">
<button
type="button"
className={cn(
'focus-visible:ring-ring inline-flex cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
activeFilter === 'all'
? 'bg-primary text-primary-foreground font-semibold shadow-xs'
: 'bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground',
)}
onClick={() => setActiveFilter('all')}
>
All Audits (5)
</button>
<button
type="button"
className={cn(
'focus-visible:ring-ring inline-flex cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
activeFilter === 'passed'
? 'bg-success font-semibold text-white shadow-xs'
: 'bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground',
)}
onClick={() => setActiveFilter('passed')}
>
<CheckCircle2 className="size-3.5" />
Passed (2)
</button>
<button
type="button"
className={cn(
'focus-visible:ring-ring inline-flex cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
activeFilter === 'critical'
? 'bg-destructive font-semibold text-white shadow-xs'
: 'bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground',
)}
onClick={() => setActiveFilter('critical')}
>
<AlertTriangle className="size-3.5" />
Critical Defects (2)
</button>
<button
type="button"
className={cn(
'focus-visible:ring-ring inline-flex cursor-pointer items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
activeFilter === 'minor'
? 'bg-warning font-semibold text-white shadow-xs'
: 'bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground',
)}
onClick={() => setActiveFilter('minor')}
>
<AlertCircle className="size-3.5" />
Minor Issues (1)
</button>
</div>
{/* Search Input & Facility Selector */}
<div className="flex flex-wrap items-center gap-2">
<div className="w-full sm:w-48">
<Select value={selectedFacility} onValueChange={(val) => val && setSelectedFacility(val)}>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="All Facilities" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Facilities (5)</SelectItem>
<SelectItem value="Building A">Building A (2)</SelectItem>
<SelectItem value="Building B">Building B (1)</SelectItem>
<SelectItem value="Building C">Building C (1)</SelectItem>
<SelectItem value="Parking Structure">Parking Structure (1)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="relative w-full sm:w-64">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" />
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
type="text"
placeholder="Search asset, ID, category..."
className="h-8 pl-8 text-xs"
/>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Audit Records Table */}
<Card className="border-border overflow-hidden shadow-xs">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-muted/30">
<TableHead className="text-foreground w-[220px] text-xs font-semibold">Asset & Facility</TableHead>
<TableHead className="text-foreground text-xs font-semibold">Date & GPS Stamping</TableHead>
<TableHead className="text-foreground text-xs font-semibold">Inspection Checklist</TableHead>
<TableHead className="text-foreground text-xs font-semibold">Status & Work Order</TableHead>
<TableHead className="text-foreground text-xs font-semibold">Photographic Evidence</TableHead>
<TableHead className="text-foreground text-xs font-semibold">Inspector & Sign-off</TableHead>
<TableHead className="text-foreground text-right text-xs font-semibold">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRecords.map((record) => (
<TableRow key={record.id} className="border-border hover:bg-muted/20 transition-colors">
{/* 1. Asset & Facility */}
<TableCell className="py-3.5 align-top">
<div className="space-y-1">
<div className="flex items-center gap-1.5">
<span className="text-foreground text-xs font-bold">{record.assetName}</span>
<Badge variant="outline" className="font-mono text-xs">
{record.buildingBadge}
</Badge>
</div>
<div className="text-muted-foreground font-mono text-xs">{record.assetId}</div>
<div className="text-muted-foreground/90 flex items-center gap-1 text-xs">
<Building2 className="size-3 shrink-0" />
<span className="truncate">{record.facility}</span>
</div>
</div>
</TableCell>
{/* 2. Date & GPS Stamping */}
<TableCell className="py-3.5 align-top">
<div className="space-y-1">
<div className="text-foreground flex items-center gap-1 text-xs font-medium">
<Calendar className="text-muted-foreground size-3" />
<span>{record.date}</span>
</div>
<div className="text-muted-foreground flex items-center gap-1 font-mono text-xs">
<Clock className="size-3" />
<span>
{record.time} ({record.duration})
</span>
</div>
<button
type="button"
aria-label={`Copy GPS coordinates for ${record.assetId}: ${record.gps}`}
className="border-border hover:bg-muted focus-visible:ring-ring bg-muted/40 text-muted-foreground inline-flex cursor-pointer items-center gap-1 rounded border px-2 py-0.5 font-mono text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none"
onClick={() => copyGps(record.gps, record.id)}
>
<MapPin className="text-primary size-2.5" />
<span>{record.gps}</span>
{copiedGpsId === record.id ? (
<Check className="text-success size-2.5" />
) : (
<Copy className="text-muted-foreground/60 size-2.5" />
)}
</button>
</div>
</TableCell>
{/* 3. Inspection Checklist */}
<TableCell className="py-3.5 align-top">
<div className="w-48 space-y-1.5">
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground font-medium">Score</span>
<span
className={cn(
'font-mono font-semibold',
record.status === 'passed'
? 'text-success'
: record.status === 'critical'
? 'text-destructive'
: 'text-warning',
)}
>
{{ record }.record.statusLabel}
</span>
</div>
<div className="bg-muted h-1.5 w-full overflow-hidden rounded-full">
<div
className={cn(
'h-full rounded-full transition-[width] duration-300',
record.status === 'passed'
? 'bg-success'
: record.status === 'critical'
? 'bg-destructive'
: 'bg-warning',
)}
style={{ width: `${(record.scorePassed / record.scoreTotal) * 100}%` }}
/>
</div>
<div className="text-muted-foreground space-y-0.5 text-xs">
{record.checklist.slice(0, 2).map((item) => (
<div key={item.id} className="flex items-center gap-1 truncate font-mono text-xs">
{item.status === 'pass' && <CheckCircle2 className="text-success size-2.5 shrink-0" />}
{item.status === 'fail' && <AlertTriangle className="text-destructive size-2.5 shrink-0" />}
{item.status === 'minor' && <AlertCircle className="text-warning size-2.5 shrink-0" />}
<span className="truncate">
{item.label}: {item.value}
</span>
</div>
))}
</div>
</div>
</TableCell>
{/* 4. Status & Work Order */}
<TableCell className="py-3.5 align-top">
<div className="space-y-1.5">
<Badge
variant={record.status === 'passed' ? 'default' : 'destructive'}
className={cn(
'text-xs font-semibold capitalize',
record.status === 'passed' && 'bg-success hover:bg-success/90 text-white',
record.status === 'minor' && 'bg-warning hover:bg-warning/90 text-white',
record.status === 'critical' && 'bg-destructive hover:bg-destructive/90 text-white',
)}
>
{record.status === 'passed' && <CheckCircle2 className="mr-1 size-3" />}
{record.status === 'critical' && <AlertTriangle className="mr-1 size-3" />}
{record.status === 'minor' && <AlertCircle className="mr-1 size-3" />}
{record.status}
</Badge>
{record.defectSummary ? (
<p className="text-destructive max-w-[200px] text-xs leading-tight font-medium">
{record.defectSummary}
</p>
) : (
<p className="text-muted-foreground text-xs leading-tight">
All calibration points verified within tolerance.
</p>
)}
{record.workOrder && (
<div className="pt-0.5">
<span className="border-border bg-muted/30 text-muted-foreground inline-flex items-center gap-1 rounded border px-1.5 py-0.5 font-mono text-xs">
<Wrench className="text-warning size-2.5" />
<span className="max-w-[140px] truncate">{record.workOrder}</span>
</span>
</div>
)}
</div>
</TableCell>
{/* 5. Photographic Evidence */}
<TableCell className="py-3.5 align-top">
<div className="space-y-1.5">
<div className="flex items-center gap-1.5">
{record.photos.map((photo, pIdx) => (
<button
key={photo.id}
type="button"
aria-label={`View inspection photo ${pIdx + 1}: ${photo.title}`}
className="border-border hover:border-primary focus-visible:ring-ring group/thumb relative size-11 shrink-0 cursor-pointer overflow-hidden rounded-md border bg-zinc-900 shadow-xs transition-colors focus-visible:ring-2 focus-visible:outline-none"
onClick={() => handleOpenPhoto(photo, record)}
>
<InspectionThumbnail type={photo.previewType} />
<div className="absolute inset-0 flex items-center justify-center bg-black/60 opacity-0 transition-opacity group-hover/thumb:opacity-100">
<Eye className="size-3.5 text-white" />
</div>
</button>
))}
</div>
<div className="text-muted-foreground flex items-center gap-1 text-xs">
<Camera className="size-3" />
<span>{record.photos.length} Attachments</span>
</div>
</div>
</TableCell>
{/* 6. Inspector & Sign-off */}
<TableCell className="py-3.5 align-top">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Avatar className="border-border size-6 border">
<AvatarFallback className="bg-primary/10 text-primary text-xs font-bold">
{record.inspector.initials}
</AvatarFallback>
</Avatar>
<span className="text-foreground text-xs font-semibold">{record.inspector.name}</span>
</div>
<div>
<span
className={cn(
'inline-flex items-center gap-1 font-mono text-xs font-medium',
record.status === 'critical' ? 'text-destructive' : 'text-success',
)}
>
{record.status !== 'critical' ? (
<ShieldCheck className="size-3 shrink-0" />
) : (
<ShieldAlert className="size-3 shrink-0" />
)}
{record.inspector.signoffStatus}
</span>
</div>
<div className="text-muted-foreground/80 font-mono text-xs">{record.inspector.signoffHash}</div>
</div>
</TableCell>
{/* 7. Actions Dropdown */}
<TableCell className="py-3.5 text-right align-top" onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="size-8 p-0">
<span className="sr-only">Open audit menu</span>
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 text-xs">
<DropdownMenuLabel className="font-mono text-xs">{record.assetId}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer gap-2" onClick={() => handleViewReport(record)}>
<Eye className="text-primary size-3.5" />
<span>View Audit Report</span>
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer gap-2" onClick={() => handleDownloadPdf(record)}>
<Download className="size-3.5" />
<span>Download PDF</span>
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer gap-2"
onClick={() => copyGps(record.gps, record.id)}
>
<Copy className="size-3.5" />
<span>Copy GPS Coordinates</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className={cn(
'cursor-pointer gap-2',
record.status === 'critical' ? 'text-destructive font-semibold' : '',
)}
onClick={() => handleCreateWorkOrder(record)}
>
<Wrench className="size-3.5" />
<span>{record.status === 'critical' ? 'Expedite Work Order' : 'Create Work Order'}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</Card>
{/* Photo Lightbox Dialog */}
<PhotoLightboxDialog
previewPhoto={previewPhoto}
onOpenChange={(open) => !open && setPreviewPhoto(null)}
onDownloadPhoto={() => showToast('Exporting high-resolution raw image file...')}
/>
{/* Comprehensive Audit Report Dialog */}
<AuditReportDialog
selectedAudit={selectedAudit}
onOpenChange={(open) => !open && setSelectedAudit(null)}
onPreviewPhoto={handleOpenPhoto}
onDownloadReport={handleDownloadPdf}
/>
{/* Start New Inspection Dialog */}
<NewAuditDialog open={isNewAuditOpen} onOpenChange={setIsNewAuditOpen} onToast={showToast} />
</div>
)
}
'use client'
import { Camera, Download } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import type { InspectionRecord, PhotoEvidence } from './field-inspection-types'
export interface PhotoLightboxDialogProps {
previewPhoto: { photo: PhotoEvidence; record: InspectionRecord } | null
onOpenChange: (open: boolean) => void
onDownloadPhoto: () => void
}
export function PhotoLightboxDialog({ previewPhoto, onOpenChange, onDownloadPhoto }: PhotoLightboxDialogProps) {
return (
<Dialog open={!!previewPhoto} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl">
{previewPhoto && (
<>
<DialogHeader>
<div className="flex items-center justify-between pr-4">
<div className="flex items-center gap-2">
<Camera className="text-primary size-4" />
<DialogTitle className="text-base font-bold">{previewPhoto.photo.title}</DialogTitle>
</div>
<Badge variant="outline" className="font-mono text-xs">
{previewPhoto.record.assetId}
</Badge>
</div>
<DialogDescription className="text-xs">
Captured during {previewPhoto.record.assetName} site inspection audit.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="border-border relative aspect-[16/10] w-full overflow-hidden rounded-lg border bg-gradient-to-br from-zinc-800 via-zinc-900 to-black p-4 text-white shadow-inner">
<div className="absolute inset-0 flex items-center justify-center p-6">
{previewPhoto.photo.previewType === 'gauge' ? (
<svg className="h-full w-full" viewBox="0 0 300 200" fill="none">
<rect width="300" height="200" fill="#18181b" />
<circle cx="150" cy="100" r="70" fill="#27272a" stroke="#71717a" strokeWidth="4" />
<path
d="M 100,125 A 55,55 0 1,1 200,125"
fill="none"
stroke="#22c55e"
strokeWidth="6"
strokeDasharray="4 2"
/>
<line
x1="150"
y1="100"
x2="185"
y2="65"
stroke="#ef4444"
strokeWidth="3.5"
strokeLinecap="round"
/>
<circle cx="150" cy="100" r="8" fill="#f4f4f5" />
<text x="150" y="140" fill="#a1a1aa" fontSize="12" textAnchor="middle" fontFamily="monospace">
68.4 PSI · NOMINAL
</text>
</svg>
) : previewPhoto.photo.previewType === 'thermal' ? (
<svg className="h-full w-full" viewBox="0 0 300 200" fill="none">
<rect width="300" height="200" fill="#3b0764" />
<circle cx="150" cy="100" r="65" fill="#db2777" fillOpacity="0.6" />
<circle cx="150" cy="100" r="30" fill="#facc15" />
<line x1="80" y1="100" x2="220" y2="100" stroke="#ffffff" strokeWidth="1.5" />
<line x1="150" y1="30" x2="150" y2="170" stroke="#ffffff" strokeWidth="1.5" />
<circle cx="150" cy="100" r="6" stroke="#ffffff" strokeWidth="1.5" fill="none" />
<text x="160" y="90" fill="#ffffff" fontSize="14" fontWeight="bold" fontFamily="monospace">
84.2°C CRITICAL
</text>
</svg>
) : previewPhoto.photo.previewType === 'spall' ? (
<svg className="h-full w-full" viewBox="0 0 300 200" fill="none">
<rect width="300" height="200" fill="#27272a" />
<polygon
points="40,30 260,30 260,170 170,170 130,110 40,100"
fill="#52525b"
stroke="#71717a"
strokeWidth="2"
/>
<polygon
points="130,110 180,90 170,170"
fill="#ef4444"
fillOpacity="0.4"
stroke="#ef4444"
strokeWidth="2"
/>
<line x1="130" y1="110" x2="180" y2="90" stroke="#facc15" strokeWidth="2" strokeDasharray="3 3" />
<text x="155" y="80" fill="#facc15" fontSize="12" fontFamily="monospace">
18cm SHEAR SPALL
</text>
</svg>
) : (
<svg className="h-full w-full" viewBox="0 0 300 200" fill="none">
<rect width="300" height="200" fill="#18181b" />
<rect
x="40"
y="30"
width="220"
height="140"
rx="4"
fill="#27272a"
stroke="#3f3f46"
strokeWidth="2"
/>
<circle cx="150" cy="100" r="35" fill="#3f3f46" stroke="#38bdf8" strokeWidth="2" />
<line x1="70" y1="60" x2="230" y2="60" stroke="#38bdf8" strokeWidth="1.5" />
<line x1="70" y1="140" x2="230" y2="140" stroke="#38bdf8" strokeWidth="1.5" />
</svg>
)}
</div>
<div className="pointer-events-none absolute inset-4 border border-white/20">
<div className="border-success absolute -top-1 -left-1 size-3 border-t-2 border-l-2" />
<div className="border-success absolute -top-1 -right-1 size-3 border-t-2 border-r-2" />
<div className="border-success absolute -bottom-1 -left-1 size-3 border-b-2 border-l-2" />
<div className="border-success absolute -right-1 -bottom-1 size-3 border-r-2 border-b-2" />
</div>
<div className="absolute top-3 left-3 flex items-center gap-1.5 rounded bg-black/70 px-2 py-1 font-mono text-xs backdrop-blur-sm">
<span className="bg-success size-2 animate-pulse rounded-full" />
<span>GEO-AUTHENTICATED FIELD PHOTO</span>
</div>
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/90 via-black/60 to-transparent p-3 pt-6 font-mono text-xs">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="text-success font-bold tabular-nums">
📍 {previewPhoto.record.gps} ({previewPhoto.record.gpsAccuracy})
</span>
<span className="text-zinc-300">
{previewPhoto.record.date} · {previewPhoto.photo.time}
</span>
</div>
</div>
</div>
<div className="border-border bg-muted/20 space-y-2 rounded-lg border p-3 text-xs">
<div className="text-foreground font-semibold">Inspector Field Observation:</div>
<p className="text-muted-foreground leading-relaxed">{previewPhoto.photo.caption}</p>
</div>
<div className="grid grid-cols-2 gap-2 text-xs sm:grid-cols-4">
<div className="border-border bg-muted/30 rounded border p-2">
<span className="text-muted-foreground block text-xs">Asset ID</span>
<span className="text-foreground font-mono font-semibold">{previewPhoto.record.assetId}</span>
</div>
<div className="border-border bg-muted/30 rounded border p-2">
<span className="text-muted-foreground block text-xs">Facility</span>
<span className="text-foreground truncate font-medium">{previewPhoto.record.buildingBadge}</span>
</div>
<div className="border-border bg-muted/30 rounded border p-2">
<span className="text-muted-foreground block text-xs">Capture Timestamp</span>
<span className="text-foreground font-mono font-medium tabular-nums">{previewPhoto.photo.time}</span>
</div>
<div className="border-border bg-muted/30 rounded border p-2">
<span className="text-muted-foreground block text-xs">Cryptographic Seal</span>
<span className="text-success font-mono font-medium">Valid SHA-256</span>
</div>
</div>
</div>
<DialogFooter className="flex flex-wrap items-center justify-between gap-2">
<Button
aria-label="Download attachment"
variant="outline"
size="sm"
className="text-xs font-medium"
onClick={() => onDownloadPhoto()}
>
<Download className="mr-1.5 size-3.5" />
Download Original Photo
</Button>
<DialogClose asChild>
<Button size="sm" className="text-xs">
Close
</Button>
</DialogClose>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
)
}
'use client'
import { AlertTriangle, Camera, Download, ShieldCheck } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import type { InspectionRecord, PhotoEvidence } from './field-inspection-types'
export interface AuditReportDialogProps {
selectedAudit: InspectionRecord | null
onOpenChange: (open: boolean) => void
onPreviewPhoto: (photo: PhotoEvidence, record: InspectionRecord) => void
onDownloadReport: (audit: InspectionRecord) => void
}
export function AuditReportDialog({
selectedAudit,
onOpenChange,
onPreviewPhoto,
onDownloadReport,
}: AuditReportDialogProps) {
return (
<Dialog open={!!selectedAudit} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-3xl">
{selectedAudit && (
<>
<DialogHeader>
<div className="flex flex-wrap items-center justify-between gap-2 pr-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Badge variant="outline" className="font-mono text-xs">
{selectedAudit.assetId}
</Badge>
<Badge
variant={
selectedAudit.status === 'critical'
? 'destructive'
: selectedAudit.status === 'minor'
? 'warning'
: 'outline'
}
className={cn(
'font-mono text-xs font-semibold',
selectedAudit.status === 'passed' && 'border-success/30 bg-success/10 text-success',
)}
>
{selectedAudit.statusLabel}
</Badge>
</div>
<DialogTitle className="text-lg font-bold sm:text-xl">{selectedAudit.assetName}</DialogTitle>
</div>
</div>
<DialogDescription className="text-xs">
Commercial facility asset engineering inspection audit record · {selectedAudit.category}
</DialogDescription>
</DialogHeader>
<div className="space-y-5 py-2">
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<div className="border-border bg-muted/20 rounded-lg border p-3">
<span className="text-muted-foreground block text-xs font-medium">Facility Location</span>
<span className="text-foreground mt-0.5 block text-xs font-semibold">{selectedAudit.facility}</span>
</div>
<div className="border-border bg-muted/20 rounded-lg border p-3">
<span className="text-muted-foreground block text-xs font-medium">Inspection Time</span>
<span className="text-foreground mt-0.5 block text-xs font-semibold tabular-nums">
{selectedAudit.date} · {selectedAudit.time}
</span>
</div>
<div className="border-border bg-muted/20 rounded-lg border p-3">
<span className="text-muted-foreground block text-xs font-medium">GPS Geostamp</span>
<span className="text-foreground mt-0.5 block font-mono text-xs font-semibold tabular-nums">
{selectedAudit.gps}
</span>
<span className="text-muted-foreground text-xs">Accuracy: {selectedAudit.gpsAccuracy}</span>
</div>
<div className="border-border bg-muted/20 rounded-lg border p-3">
<span className="text-muted-foreground block text-xs font-medium">Audit Duration</span>
<span className="text-foreground mt-0.5 block text-xs font-semibold tabular-nums">
{selectedAudit.duration}
</span>
<span className="text-muted-foreground text-xs">SLA compliant</span>
</div>
</div>
{selectedAudit.status === 'critical' && (
<div className="border-destructive/30 bg-destructive/10 text-destructive space-y-1.5 rounded-lg border p-3.5 text-xs">
<div className="flex items-center gap-1.5 font-bold">
<AlertTriangle className="text-destructive size-4" />
<span>Critical Deficiencies Flagged · Action Dispatched</span>
</div>
<p className="leading-relaxed">{selectedAudit.defectSummary}</p>
<div className="pt-1 font-mono font-semibold">Active Work Order: {selectedAudit.workOrder}</div>
</div>
)}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-foreground text-xs font-semibold">Pass / Fail Checklist Audit Items</span>
<span className="text-muted-foreground font-mono text-xs tabular-nums">
{selectedAudit.scorePassed} / {selectedAudit.scoreTotal} Passed
</span>
</div>
<div className="border-border divide-border divide-y rounded-lg border">
{selectedAudit.checklist.map((item) => (
<div key={item.id} className="flex items-center justify-between p-2.5 text-xs">
<div className="flex items-center gap-2">
<Badge
variant={
item.status === 'fail' ? 'destructive' : item.status === 'minor' ? 'warning' : 'outline'
}
className="h-5 px-1.5 font-mono text-xs uppercase"
>
{item.status}
</Badge>
<span className="text-foreground font-medium">{item.label}</span>
</div>
<span className="text-muted-foreground font-mono text-xs tabular-nums">{item.value}</span>
</div>
))}
</div>
</div>
<div className="space-y-2">
<span className="text-foreground text-xs font-semibold">Photographic Evidence Attachments</span>
<div className="grid grid-cols-3 gap-3">
{selectedAudit.photos.map((photo) => (
<button
key={photo.id}
type="button"
className="border-border hover:border-primary/60 group/p cursor-pointer overflow-hidden rounded-lg border bg-zinc-900 p-2 text-left transition-colors"
onClick={() => onPreviewPhoto(photo, selectedAudit)}
>
<div className="relative aspect-[16/10] w-full overflow-hidden rounded bg-black">
<div className="absolute inset-0 flex items-center justify-center">
<Camera className="text-muted-foreground size-6" />
</div>
</div>
<div className="text-foreground mt-1.5 truncate text-xs font-semibold">{photo.title}</div>
<div className="text-muted-foreground font-mono text-xs tabular-nums">{photo.time}</div>
</button>
))}
</div>
</div>
<div className="border-border bg-muted/15 space-y-2 rounded-xl border p-4">
<div className="flex items-center justify-between">
<span className="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
<ShieldCheck className="text-primary size-3.5" />
Electronic Cryptographic Engineer Sign-off
</span>
<span className="text-muted-foreground font-mono text-xs">{selectedAudit.inspector.badge}</span>
</div>
<div className="border-border/60 border-b pt-1 pb-3">
<p className="text-primary text-2xl font-medium tracking-wide italic">
{selectedAudit.inspector.name}
</p>
</div>
<div className="text-muted-foreground flex flex-wrap items-center justify-between gap-2 font-mono text-xs">
<span>
Signer: {selectedAudit.inspector.name} · {selectedAudit.inspector.role}
</span>
<span>{selectedAudit.inspector.signoffHash}</span>
</div>
</div>
</div>
<DialogFooter className="flex flex-wrap items-center justify-between gap-2">
<Button
aria-label="Download attachment"
variant="outline"
size="sm"
className="text-xs font-medium"
onClick={() => onDownloadReport(selectedAudit)}
>
<Download className="mr-1.5 size-3.5" />
Download PDF Report
</Button>
<DialogClose asChild>
<Button size="sm" className="text-xs">
Close
</Button>
</DialogClose>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
)
}
'use client'
import * as React from 'react'
import { CheckCircle2, ClipboardCheck, MapPin, UploadCloud } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
export interface NewAuditDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onToast: (msg: string) => void
}
export function NewAuditDialog({ open, onOpenChange, onToast }: NewAuditDialogProps) {
const [newAuditAsset, setNewAuditAsset] = React.useState('AST-HVAC-4091')
const [newAuditFacility, setNewAuditFacility] = React.useState('Building A · Rooftop Mechanical Room')
const [newAuditNotes, setNewAuditNotes] = React.useState('')
const [newAuditSuccess, setNewAuditSuccess] = React.useState(false)
const submitNewAudit = () => {
setNewAuditSuccess(true)
onToast('New audit inspection successfully submitted and recorded!')
setTimeout(() => {
onOpenChange(false)
setNewAuditSuccess(false)
setNewAuditNotes('')
}, 1400)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<div className="flex items-center gap-2">
<ClipboardCheck className="text-primary size-5" />
<DialogTitle className="text-base font-bold"> Start New Site & Asset Inspection Audit </DialogTitle>
</div>
<DialogDescription className="text-xs">
Initiate a field engineering inspection with automatic GPS geostamping and checklist validation.
</DialogDescription>
</DialogHeader>
{newAuditSuccess ? (
<div className="space-y-3 py-8 text-center">
<div className="bg-success/20 text-success mx-auto flex size-12 items-center justify-center rounded-full">
<CheckCircle2 className="size-6" />
</div>
<h3 className="text-foreground text-base font-bold">Inspection Audit Logged Successfully</h3>
<p className="text-muted-foreground mx-auto max-w-sm text-xs">
Audit has been recorded with GPS geostamp 34.0522°N, 118.2437°W and synced to cloud repository.
</p>
</div>
) : (
<div className="space-y-4 py-2">
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<label className="text-foreground text-xs font-semibold">Select Target Asset *</label>
<Select value={newAuditAsset} onValueChange={(val) => val && setNewAuditAsset(val)}>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AST-HVAC-4091">AST-HVAC-4091 · Chiller Unit #4</SelectItem>
<SelectItem value="AST-SOLAR-1082">AST-SOLAR-1082 · Solar Array & Inverters</SelectItem>
<SelectItem value="AST-FIRE-8820">AST-FIRE-8820 · Fire Sprinkler System</SelectItem>
<SelectItem value="AST-GEN-3304">AST-GEN-3304 · Backup Generator #2</SelectItem>
<SelectItem value="AST-STR-0019">AST-STR-0019 · Foundation & Beams</SelectItem>
<SelectItem value="AST-ELEV-9012">AST-ELEV-9012 · Passenger Elevator Bank A</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<label className="text-foreground text-xs font-semibold">Facility Location</label>
<Input
value={newAuditFacility}
onChange={(e) => setNewAuditFacility(e.target.value)}
className="h-8 text-xs"
/>
</div>
</div>
<div className="border-border bg-muted/20 flex items-center justify-between rounded-lg border p-3 text-xs">
<div className="flex items-center gap-2">
<MapPin className="text-success size-4 shrink-0" />
<div>
<span className="text-foreground font-semibold">Live GPS Telemetry Lock</span>
<span className="text-muted-foreground block font-mono text-xs">
34.0522°N, 118.2437°W · Accuracy ±1.5m
</span>
</div>
</div>
<Badge variant="outline" className="border-success/30 bg-success/10 text-success font-mono text-xs">
GPS Locked
</Badge>
</div>
<div className="space-y-1.5">
<label className="text-foreground text-xs font-semibold">Photographic Evidence</label>
<div
className="border-border/80 hover:border-primary/50 bg-card cursor-pointer rounded-lg border-2 border-dashed p-4 text-center transition-colors"
onClick={() => onToast('Mock photo uploaded: asset_evidence_live.jpg')}
>
<div className="flex flex-col items-center justify-center gap-1">
<UploadCloud className="text-primary size-5" />
<p className="text-foreground text-xs font-medium">Click to upload photo evidence or drag & drop</p>
<p className="text-muted-foreground text-xs">Supports JPG, PNG up to 15MB with GPS EXIF retention</p>
</div>
</div>
</div>
<div className="space-y-1.5">
<label className="text-foreground text-xs font-semibold">Inspector Observations & Checklist Notes</label>
<Input
value={newAuditNotes}
onChange={(e) => setNewAuditNotes(e.target.value)}
placeholder="Enter notes, gauge pressures, or deficiency remarks..."
className="h-8 text-xs"
/>
</div>
</div>
)}
{!newAuditSuccess && (
<DialogFooter className="flex items-center justify-end gap-2">
<DialogClose asChild>
<Button variant="outline" size="sm" className="text-xs">
Cancel
</Button>
</DialogClose>
<Button size="sm" className="gap-1.5 text-xs font-medium" onClick={submitNewAudit}>
<ClipboardCheck className="size-3.5" />
Submit Completed Audit
</Button>
</DialogFooter>
)}
</DialogContent>
</Dialog>
)
}
import * as React from 'react'
import { CheckCircle2, Clock, Percent, ShieldAlert } from 'lucide-react'
import { Card, CardContent } from '@/components/ui/card'
export function InspectionMetrics() {
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* Card 1: Completed Inspections */}
<Card className="border-border shadow-xs">
<CardContent className="p-4 sm:p-5">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-medium">Completed Inspections</span>
<div className="border-success/20 bg-success/10 text-success flex size-8 items-center justify-center rounded-md border">
<CheckCircle2 className="size-4" aria-hidden="true" />
</div>
</div>
<div className="mt-3">
<div className="text-foreground text-2xl font-bold tracking-tight tabular-nums">24</div>
<div className="text-muted-foreground mt-1 flex items-center gap-1.5 text-xs">
<span className="text-success font-medium">24 Audits This Week</span>
<span>·</span>
<span className="text-muted-foreground">+14.2% vs target</span>
</div>
</div>
</CardContent>
</Card>
{/* Card 2: Critical Defects Flagged */}
<Card className="border-border shadow-xs">
<CardContent className="p-4 sm:p-5">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-medium">Critical Defects Flagged</span>
<div className="border-destructive/20 bg-destructive/10 text-destructive flex size-8 items-center justify-center rounded-md border">
<ShieldAlert className="size-4" aria-hidden="true" />
</div>
</div>
<div className="mt-3">
<div className="text-foreground text-2xl font-bold tracking-tight tabular-nums">2</div>
<div className="text-muted-foreground mt-1 flex items-center gap-1.5 text-xs">
<span className="text-destructive font-semibold">2 Critical Deficiencies</span>
<span>·</span>
<span className="text-muted-foreground">Work Orders Dispatched</span>
</div>
</div>
</CardContent>
</Card>
{/* Card 3: First-Time Pass Rate */}
<Card className="border-border shadow-xs">
<CardContent className="p-4 sm:p-5">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-medium">First-Time Pass Rate</span>
<div className="border-primary/20 bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md border">
<Percent className="size-4" aria-hidden="true" />
</div>
</div>
<div className="mt-3">
<div className="text-foreground text-2xl font-bold tracking-tight tabular-nums">91.6%</div>
<div className="text-muted-foreground mt-1 flex items-center gap-1.5 text-xs">
<span className="text-success font-medium">91.6% Pass Rate</span>
<span>·</span>
<span className="text-muted-foreground">Above 90.0% SLA</span>
</div>
</div>
</CardContent>
</Card>
{/* Card 4: Avg Inspection Duration */}
<Card className="border-border shadow-xs">
<CardContent className="p-4 sm:p-5">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-medium">Avg Inspection Duration</span>
<div className="border-warning/20 bg-warning/10 text-warning flex size-8 items-center justify-center rounded-md border">
<Clock className="size-4" aria-hidden="true" />
</div>
</div>
<div className="mt-3">
<div className="text-foreground text-2xl font-bold tracking-tight tabular-nums">28m</div>
<div className="text-muted-foreground mt-1 flex items-center gap-1.5 text-xs">
<span className="text-foreground font-medium">28 mins / audit</span>
<span>·</span>
<span className="text-success">-4m vs benchmark</span>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
import * as React from 'react'
import { cn } from '@/lib/utils'
import type { PhotoEvidence } from './field-inspection-types'
export interface InspectionThumbnailProps {
type: PhotoEvidence['previewType']
className?: string
}
export function InspectionThumbnail({ type, className }: InspectionThumbnailProps) {
return (
<div className={cn('h-full w-full', className)}>
{type === 'gauge' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<circle cx="22" cy="22" r="19" fill="#18181b" stroke="#3f3f46" strokeWidth="1.5" />
<path d="M 12,28 A 14,14 0 1,1 32,28" fill="none" stroke="#22c55e" strokeWidth="2" strokeDasharray="2 2" />
<line x1="22" y1="22" x2="28" y2="15" stroke="#ef4444" strokeWidth="1.5" strokeLinecap="round" />
<circle cx="22" cy="22" r="2.5" fill="#f4f4f5" />
</svg>
)}
{type === 'coil' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#18181b" />
<line x1="6" y1="10" x2="38" y2="10" stroke="#38bdf8" strokeWidth="1.5" />
<line x1="6" y1="18" x2="38" y2="18" stroke="#38bdf8" strokeWidth="1.5" />
<line x1="6" y1="26" x2="38" y2="26" stroke="#38bdf8" strokeWidth="1.5" />
<line x1="6" y1="34" x2="38" y2="34" stroke="#38bdf8" strokeWidth="1.5" />
<circle cx="22" cy="22" r="7" fill="#0284c7" fillOpacity="0.3" stroke="#38bdf8" strokeWidth="1.5" />
</svg>
)}
{type === 'mount' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#18181b" />
<rect x="8" y="8" width="28" height="4" rx="1" fill="#71717a" />
<path d="M 14,12 C 14,18 30,18 30,24 C 30,30 14,30 14,36" fill="none" stroke="#f59e0b" strokeWidth="2" />
<rect x="8" y="36" width="28" height="4" rx="1" fill="#71717a" />
</svg>
)}
{type === 'thermal' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#3b0764" />
<circle cx="22" cy="22" r="14" fill="#db2777" fillOpacity="0.6" />
<circle cx="22" cy="22" r="7" fill="#facc15" />
<line x1="12" y1="22" x2="32" y2="22" stroke="#ffffff" strokeWidth="0.75" />
<line x1="22" y1="12" x2="22" y2="32" stroke="#ffffff" strokeWidth="0.75" />
</svg>
)}
{type === 'box' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#18181b" />
<rect x="6" y="6" width="32" height="32" rx="2" stroke="#eab308" strokeWidth="1.5" />
<line x1="14" y1="12" x2="14" y2="32" stroke="#f97316" strokeWidth="2" />
<line x1="22" y1="12" x2="22" y2="32" stroke="#f97316" strokeWidth="2" />
<circle cx="22" cy="22" r="5" fill="#ef4444" />
</svg>
)}
{type === 'conduit' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#18181b" />
<rect x="16" y="4" width="12" height="36" fill="#52525b" />
<circle cx="22" cy="22" r="10" fill="#a1a1aa" fillOpacity="0.3" stroke="#e4e4e7" strokeWidth="1.5" />
<line x1="16" y1="22" x2="28" y2="22" stroke="#ef4444" strokeWidth="1.5" />
</svg>
)}
{type === 'riser' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#18181b" />
<rect x="14" y="4" width="16" height="36" fill="#dc2626" />
<circle cx="22" cy="18" r="8" fill="#e4e4e7" stroke="#dc2626" strokeWidth="1.5" />
<line x1="22" y1="18" x2="26" y2="14" stroke="#000000" strokeWidth="1" />
</svg>
)}
{type === 'pump' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#18181b" />
<circle cx="22" cy="22" r="13" fill="#b91c1c" stroke="#f87171" strokeWidth="1.5" />
<circle cx="22" cy="22" r="5" fill="#52525b" />
<line x1="22" y1="4" x2="22" y2="10" stroke="#f87171" strokeWidth="2" />
</svg>
)}
{type === 'switch' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#18181b" />
<circle cx="22" cy="16" r="9" stroke="#dc2626" strokeWidth="2" fill="none" />
<rect x="16" y="24" width="12" height="14" rx="2" fill="#eab308" />
</svg>
)}
{type === 'tank' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#18181b" />
<rect x="10" y="8" width="24" height="28" rx="6" fill="#3f3f46" stroke="#71717a" strokeWidth="1.5" />
<rect x="14" y="14" width="4" height="16" rx="1" fill="#22c55e" />
</svg>
)}
{type === 'battery' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#18181b" />
<rect x="6" y="12" width="32" height="24" rx="2" fill="#27272a" stroke="#52525b" strokeWidth="1.5" />
<rect x="11" y="8" width="6" height="4" fill="#ef4444" />
<rect x="27" y="8" width="6" height="4" fill="#3b82f6" />
</svg>
)}
{type === 'ats' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#18181b" />
<rect x="6" y="6" width="32" height="32" rx="2" fill="#27272a" stroke="#059669" strokeWidth="1.5" />
<circle cx="14" cy="14" r="2.5" fill="#22c55e" />
<circle cx="14" cy="22" r="2.5" fill="#eab308" />
<line x1="22" y1="12" x2="32" y2="12" stroke="#a1a1aa" strokeWidth="2" />
</svg>
)}
{type === 'spall' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#27272a" />
<polygon points="6,6 38,6 38,38 24,38 18,26 6,24" fill="#71717a" />
<polygon points="18,26 26,24 24,38" fill="#ef4444" fillOpacity="0.4" stroke="#ef4444" strokeWidth="1" />
</svg>
)}
{type === 'rebar' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#27272a" />
<line x1="6" y1="38" x2="38" y2="6" stroke="#b45309" strokeWidth="4" strokeDasharray="2 1" />
<circle cx="22" cy="22" r="6" stroke="#ef4444" strokeWidth="1.5" fill="none" />
</svg>
)}
{type === 'crack' && (
<svg className="h-full w-full" viewBox="0 0 44 44" fill="none">
<rect width="44" height="44" fill="#18181b" />
<path d="M 12,38 L 18,26 L 24,20 L 32,6" stroke="#ef4444" strokeWidth="2" fill="none" />
<rect
x="14"
y="14"
width="16"
height="12"
rx="1"
fill="#ffffff"
fillOpacity="0.2"
stroke="#ffffff"
strokeWidth="0.75"
/>
</svg>
)}
</div>
)
}
export type InspectionStatus = 'passed' | 'critical' | 'minor'
export interface PhotoEvidence {
id: string
title: string
caption: string
time: string
previewType:
| 'gauge'
| 'coil'
| 'mount'
| 'thermal'
| 'box'
| 'conduit'
| 'riser'
| 'pump'
| 'switch'
| 'tank'
| 'battery'
| 'ats'
| 'spall'
| 'rebar'
| 'crack'
}
export interface ChecklistItem {
id: string
label: string
status: 'pass' | 'fail' | 'minor'
value: string
}
export interface InspectionRecord {
id: string
assetId: string
assetName: string
category: string
facility: string
buildingBadge: string
date: string
time: string
gps: string
gpsAccuracy: string
scorePassed: number
scoreTotal: number
status: InspectionStatus
statusLabel: string
duration: string
defectSummary?: string
workOrder?: string
photos: PhotoEvidence[]
inspector: {
name: string
role: string
initials: string
badge: string
signoffStatus: string
signoffHash: string
}
checklist: ChecklistItem[]
}
import type { InspectionRecord } from './field-inspection-types'
export const INITIAL_INSPECTION_RECORDS: InspectionRecord[] = [
{
id: 'insp-1',
assetId: 'AST-HVAC-4091',
assetName: 'Commercial HVAC Chiller Unit #4',
category: 'HVAC & Thermal Systems',
facility: 'Building A · Rooftop Mechanical Room',
buildingBadge: 'Bldg A',
date: 'Aug 21, 2026',
time: '09:15 AM EDT',
gps: '34.0522°N, 118.2437°W',
gpsAccuracy: '±2.4m',
scorePassed: 18,
scoreTotal: 18,
status: 'passed',
statusLabel: '18 / 18 Passed',
duration: '26 mins',
photos: [
{
id: 'p1-1',
title: 'Compressor Suction Manifold',
caption: 'Suction pressure holding steady at 68.4 PSI; temperature sensor verified within calibration spec.',
time: '09:18 AM',
previewType: 'gauge',
},
{
id: 'p1-2',
title: 'Condenser Fan Coil Assembly',
caption: 'Clean aluminum fin surfaces with zero debris obstruction; axial fan bearings lubricated.',
time: '09:24 AM',
previewType: 'coil',
},
{
id: 'p1-3',
title: 'Vibration Dampener Isolators',
caption: 'Neoprene spring isolator pads intact with zero hairline settling fractures or mounting deflection.',
time: '09:32 AM',
previewType: 'mount',
},
],
inspector: {
name: 'Marcus Vance',
role: 'Senior Field Engineer',
initials: 'MV',
badge: 'PE #84920-CA',
signoffStatus: 'Verified Cryptographic Sign-off',
signoffHash: 'SHA-256: 9f8e21...41a2',
},
checklist: [
{
id: 'c1-1',
label: 'Refrigerant Charge & Pressures',
status: 'pass',
value: '68.4 PSI Suction / 225 PSI Discharge',
},
{ id: 'c1-2', label: 'Compressor Motor Amp Draw', status: 'pass', value: '42.1A (rated 46A max)' },
{ id: 'c1-3', label: 'Chilled Water Loop Delta-T', status: 'pass', value: '10.2°F Temperature Drop' },
{ id: 'c1-4', label: 'Emergency High-Pressure Cutout', status: 'pass', value: 'Trips accurately at 380 PSI' },
],
},
{
id: 'insp-2',
assetId: 'AST-SOLAR-1082',
assetName: 'Rooftop Solar Array & Inverters',
category: 'Renewable Energy & Power',
facility: 'Building C · South Wing Solar Deck',
buildingBadge: 'Bldg C',
date: 'Aug 21, 2026',
time: '10:45 AM EDT',
gps: '34.0528°N, 118.2445°W',
gpsAccuracy: '±1.8m',
scorePassed: 14,
scoreTotal: 18,
status: 'critical',
statusLabel: '14 / 18 - 2 Critical Fails',
duration: '34 mins',
defectSummary: 'DC string combiner box busbar overheating (84.2°C); micro-inverter #6 circuit failure.',
workOrder: 'WO-SOLAR-4921 (High Priority Dispatched)',
photos: [
{
id: 'p2-1',
title: 'Inverter #6 Thermal Hotspot',
caption: 'FLIR radiometric thermography shows 84.2°C localized hotspot at DC-AC bridge capacitor.',
time: '10:52 AM',
previewType: 'thermal',
},
{
id: 'p2-2',
title: 'Combiner Box Arc Degradation',
caption: 'Busbar contact surface oxidation with heat discoloration and slight terminal pitting.',
time: '11:04 AM',
previewType: 'box',
},
{
id: 'p2-3',
title: 'PV String #4 Conduit Ingress',
caption: 'Degraded rubber gasket on string junction conduit fitting allowing moisture penetration.',
time: '11:12 AM',
previewType: 'conduit',
},
],
inspector: {
name: 'Marcus Vance',
role: 'Senior Field Engineer',
initials: 'MV',
badge: 'PE #84920-CA',
signoffStatus: 'Critical Defect Action Required',
signoffHash: 'SHA-256: 4c3d88...71ef',
},
checklist: [
{
id: 'c2-1',
label: 'Combiner Box Busbar Temp',
status: 'fail',
value: '84.2°C (exceeds 65°C safe operating limit)',
},
{ id: 'c2-2', label: 'Micro-inverter #6 Communication', status: 'fail', value: 'Modbus timeout / Offline state' },
{ id: 'c2-3', label: 'PV Panel Surface Clarity', status: 'pass', value: 'Soiling index < 1.8%' },
{ id: 'c2-4', label: 'Rapid Shutdown System', status: 'pass', value: 'Voltages drop < 30V in 12s' },
],
},
{
id: 'insp-3',
assetId: 'AST-FIRE-8820',
assetName: 'Fire Suppression & Sprinkler System',
category: 'Life Safety & Protection',
facility: 'Main Tower · Sub-Basement Pump Room B2',
buildingBadge: 'Tower B2',
date: 'Aug 21, 2026',
time: '11:30 AM EDT',
gps: '34.0515°N, 118.2420°W',
gpsAccuracy: '±3.1m',
scorePassed: 17,
scoreTotal: 18,
status: 'minor',
statusLabel: '17 / 18 - 1 Minor Defect',
duration: '22 mins',
defectSummary: 'Main riser pressure gauge annual NIST calibration tag expired by 21 days; pressure intact.',
workOrder: 'WO-FIRE-8820 (Standard Calibration Scheduled)',
photos: [
{
id: 'p3-1',
title: 'Main Riser Pressure Gauge',
caption: 'Static pressure 145 PSI (nominal). Calibration tag stamped 07/2025 requiring routine re-tagging.',
time: '11:34 AM',
previewType: 'riser',
},
{
id: 'p3-2',
title: 'Jockey Pump Shaft Packing',
caption: 'Packing gland seal dry with zero leakage; auto start/stop pressure cutoff cycle verified.',
time: '11:41 AM',
previewType: 'pump',
},
{
id: 'p3-3',
title: 'OS&Y Tamper Supervisory Switch',
caption: 'Zone 1 control valve open; tamper switch sends instant supervisory signal to FACP.',
time: '11:48 AM',
previewType: 'switch',
},
],
inspector: {
name: 'Marcus Vance',
role: 'Senior Field Engineer',
initials: 'MV',
badge: 'PE #84920-CA',
signoffStatus: 'Verified Cryptographic Sign-off',
signoffHash: 'SHA-256: 7d1a99...33bc',
},
checklist: [
{ id: 'c3-1', label: 'System Static Water Pressure', status: 'pass', value: '145 PSI static / 125 PSI residual' },
{
id: 'c3-2',
label: 'Riser Gauge Calibration Tag',
status: 'minor',
value: 'Overdue by 21 days (recalibration due)',
},
{ id: 'c3-3', label: 'Diesel Fire Pump Auto-Start', status: 'pass', value: 'Cranked to 1750 RPM in 4.2s' },
{ id: 'c3-4', label: 'Flow Alarm Switch Delay', status: 'pass', value: 'Gong chime triggered at 32s' },
],
},
{
id: 'insp-4',
assetId: 'AST-GEN-3304',
assetName: 'Emergency Backup Generator #2',
category: 'Emergency Standby Power',
facility: 'East Campus · Utility Pad North',
buildingBadge: 'East Pad',
date: 'Aug 21, 2026',
time: '01:15 PM EDT',
gps: '34.0535°N, 118.2450°W',
gpsAccuracy: '±2.1m',
scorePassed: 18,
scoreTotal: 18,
status: 'passed',
statusLabel: '18 / 18 Passed',
duration: '29 mins',
photos: [
{
id: 'p4-1',
title: 'Diesel Sub-Base Day Tank',
caption:
'Ultra-low sulfur diesel fuel level at 98.4%; optical fuel water separator clean with zero particulate.',
time: '01:21 PM',
previewType: 'tank',
},
{
id: 'p4-2',
title: '24V Dual Starter Battery Bank',
caption: 'Terminal posts clean with dielectric grease coating; float charging verified at 27.6V.',
time: '01:28 PM',
previewType: 'battery',
},
{
id: 'p4-3',
title: '800A ATS Transfer Switch',
caption: 'Phase alignment and mechanical interlocking verified; utility power sync nominal.',
time: '01:38 PM',
previewType: 'ats',
},
],
inspector: {
name: 'Marcus Vance',
role: 'Senior Field Engineer',
initials: 'MV',
badge: 'PE #84920-CA',
signoffStatus: 'Verified Cryptographic Sign-off',
signoffHash: 'SHA-256: 3a7b54...82e9',
},
checklist: [
{ id: 'c4-1', label: 'Engine Crank & Run Test', status: 'pass', value: '60 Hz output reached in 6.8s' },
{ id: 'c4-2', label: 'Coolant Jacket Heater Temp', status: 'pass', value: '118°F Block temperature' },
{ id: 'c4-3', label: 'Oil Pressure & Level', status: 'pass', value: '55 PSI running oil pressure' },
{ id: 'c4-4', label: 'Exhaust Silencer & Insulation', status: 'pass', value: 'Zero exhaust gas or soot leakage' },
],
},
{
id: 'insp-5',
assetId: 'AST-STR-0019',
assetName: 'Structural Foundation & Load Beams',
category: 'Structural Integrity & Civil',
facility: 'Parking Structure · Level P3 Pillar Grid 4D',
buildingBadge: 'P3 Grid 4D',
date: 'Aug 21, 2026',
time: '02:40 PM EDT',
gps: '34.0510°N, 118.2412°W',
gpsAccuracy: '±1.9m',
scorePassed: 16,
scoreTotal: 18,
status: 'critical',
statusLabel: '16 / 18 - 2 Critical Fails',
duration: '31 mins',
defectSummary:
'Concrete shear spall at beam joint 4D corbel; exposed tension rebar #6 exhibiting active oxidation.',
workOrder: 'WO-STR-4922 (Structural Engineer On-Site)',
photos: [
{
id: 'p5-1',
title: 'Pillar 4D Beam Shear Spall',
caption: 'Concrete shear delamination (18cm wide x 4cm depth) near primary corbel support bracket.',
time: '02:46 PM',
previewType: 'spall',
},
{
id: 'p5-2',
title: 'Exposed Steel Rebar Oxidation',
caption: 'Deformed rebar #6 exposed to ambient moisture with active ferric surface oxidation.',
time: '02:54 PM',
previewType: 'rebar',
},
{
id: 'p5-3',
title: 'Optical Crack Comparator Test',
caption: 'Structural hairline fissure width measured at 2.2mm across tension face (spec max 0.3mm).',
time: '03:02 PM',
previewType: 'crack',
},
],
inspector: {
name: 'Marcus Vance',
role: 'Senior Field Engineer',
initials: 'MV',
badge: 'PE #84920-CA',
signoffStatus: 'Critical Defect Action Required',
signoffHash: 'SHA-256: 8e5f12...04db',
},
checklist: [
{ id: 'c5-1', label: 'Concrete Cover Spalling', status: 'fail', value: '18cm spall at corbel connection' },
{
id: 'c5-2',
label: 'Structural Crack Aperture',
status: 'fail',
value: '2.2mm fissure (exceeds 0.3mm tolerance)',
},
{ id: 'c5-3', label: 'Expansion Joint Elastomer', status: 'pass', value: 'Sealant elasticity within spec' },
{ id: 'c5-4', label: 'Drainage Channel Clearance', status: 'pass', value: 'Deck scuppers free of sediment' },
],
},
]
Raw manifest:https://uipkge.dev/r/react/field-inspection-manager.json