Field Inspection Manager
blocklogisticsMobile/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.
Installation
$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.jsonNamed registry:
npx shadcn@latest add @uipkge-react/field-inspection-managerInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
initialFilter | 'all' | 'passed' | 'critical' | 'minor' | — | optional |
className | string | — | optional |
Schema
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[]
}npm dependencies
Files installed (8)
components/blocks/FieldInspectionManager.tsx26.9 kB
'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> ) }components/blocks/PhotoLightboxDialog.tsx9.6 kB
'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> ) }components/blocks/AuditReportDialog.tsx9.5 kB
'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> ) }components/blocks/NewAuditDialog.tsx6.6 kB
'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> ) }components/blocks/InspectionMetrics.tsx4.3 kB
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> ) }components/blocks/InspectionThumbnail.tsx7 kB
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> ) }components/blocks/field-inspection-types.ts1 kB
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[] }components/blocks/field-inspection-data.ts10.4 kB
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