UIPackage
Menu

Framework

Change language

Boilerplate repo

Field Inspection Manager

blocklogistics

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 ->

Installation

$npx shadcn@latest add https://uipkge.dev/r/react/field-inspection-manager.json
Named registry:npx shadcn@latest add @uipkge-react/field-inspection-managerInstalls to:components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
initialFilter'all' | 'passed' | 'critical' | 'minor'optional
classNamestringoptional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

PhotoEvidence
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'
}
ChecklistItem
interface ChecklistItem {
  id: string
  label: string
  status: 'pass' | 'fail' | 'minor'
  value: string
}
InspectionRecord
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[]
}

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
  • components/blocks/AuditReportDialog.tsx9.5 kB
  • components/blocks/NewAuditDialog.tsx6.6 kB
  • components/blocks/InspectionMetrics.tsx4.3 kB
  • components/blocks/InspectionThumbnail.tsx7 kB
  • components/blocks/field-inspection-types.ts1 kB
  • components/blocks/field-inspection-data.ts10.4 kB

Raw manifest:https://uipkge.dev/r/react/field-inspection-manager.json