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

Installation

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

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
initialFilter
'all''passed''critical''minor'
'all'optional
classHTMLAttributes['class']optional

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)

  • app/components/blocks/FieldInspectionManager.vue25.6 kB
    <script setup lang="ts">
    import { computed, ref } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import {
      AlertCircle,
      AlertTriangle,
      Building2,
      Calendar,
      Camera,
      Check,
      CheckCircle2,
      ClipboardCheck,
      Clock,
      Copy,
      Download,
      Eye,
      MapPin,
      MoreHorizontal,
      Percent,
      Plus,
      Search,
      ShieldAlert,
      ShieldCheck,
      UploadCloud,
      Wrench,
    } from 'lucide-vue-next'
    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.vue'
    import AuditReportDialog from './AuditReportDialog.vue'
    import NewAuditDialog from './NewAuditDialog.vue'
    import InspectionMetrics from './InspectionMetrics.vue'
    import InspectionThumbnail from './InspectionThumbnail.vue'
    import { INITIAL_INSPECTION_RECORDS } from './field-inspection-data'
    import type { InspectionStatus, PhotoEvidence, ChecklistItem, InspectionRecord } from './field-inspection-types'
    
    export type { InspectionStatus, PhotoEvidence, ChecklistItem, InspectionRecord }
    
    interface Props {
      initialFilter?: 'all' | 'passed' | 'critical' | 'minor'
      class?: HTMLAttributes['class']
    }
    
    const props = withDefaults(defineProps<Props>(), {
      initialFilter: 'all',
    })
    
    // Active UI states
    const activeFilter = ref<'all' | 'passed' | 'critical' | 'minor'>(props.initialFilter)
    const searchQuery = ref('')
    const selectedFacility = ref('all')
    const copiedGpsId = ref<string | null>(null)
    const toastMessage = ref<string | null>(null)
    
    // Dialog Modals
    const previewPhoto = ref<{ photo: PhotoEvidence; record: InspectionRecord } | null>(null)
    const selectedAudit = ref<InspectionRecord | null>(null)
    const isNewAuditOpen = ref(false)
    
    // Inspection Records Data
    const inspectionRecords = ref<InspectionRecord[]>(INITIAL_INSPECTION_RECORDS)
    
    // Filtered Records
    const filteredRecords = computed(() => {
      return inspectionRecords.value.filter((record) => {
        // Status Filter
        if (activeFilter.value === 'passed' && record.status !== 'passed') return false
        if (activeFilter.value === 'critical' && record.status !== 'critical') return false
        if (activeFilter.value === 'minor' && record.status !== 'minor') return false
    
        // Facility Filter
        if (selectedFacility.value !== 'all' && !record.facility.includes(selectedFacility.value)) return false
    
        // Search Query
        if (searchQuery.value.trim() !== '') {
          const q = searchQuery.value.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
      })
    })
    
    function copyGps(gps: string, recordId: string) {
      navigator.clipboard?.writeText(gps)
      copiedGpsId.value = recordId
      showToast(`GPS Coordinates ${gps} copied to clipboard`)
      setTimeout(() => {
        if (copiedGpsId.value === recordId) {
          copiedGpsId.value = null
        }
      }, 2200)
    }
    
    function showToast(msg: string) {
      toastMessage.value = msg
      setTimeout(() => {
        if (toastMessage.value === msg) {
          toastMessage.value = null
        }
      }, 3000)
    }
    
    function handleOpenPhoto(photo: PhotoEvidence, record: InspectionRecord) {
      previewPhoto.value = { photo, record }
    }
    
    function handleViewReport(record: InspectionRecord) {
      selectedAudit.value = record
    }
    
    function handleDownloadPdf(record: InspectionRecord) {
      showToast(`Downloading certified PDF audit report for ${record.assetId}...`)
    }
    
    function handleCreateWorkOrder(record: InspectionRecord) {
      showToast(`Work Order generated for ${record.assetName} (Assigned to Facilities Ops)`)
    }
    
    function handleStartNewInspection() {
      isNewAuditOpen.value = true
    }
    </script>
    
    <template>
      <div data-slot="field-inspection-manager" :class="cn('text-foreground w-full space-y-6', props.class)">
        <!-- TOAST NOTIFICATION BANNER -->
        <transition
          enter-active-class="transition duration-200 ease-out"
          enter-from-class="opacity-0 -translate-y-2"
          enter-to-class="opacity-100 translate-y-0"
          leave-active-class="transition duration-150 ease-in"
          leave-from-class="opacity-100 translate-y-0"
          leave-to-class="opacity-0 -translate-y-2"
        >
          <div
            v-if="toastMessage"
            class="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 class="text-primary size-4 shrink-0" />
            <span>{{ toastMessage }}</span>
          </div>
        </transition>
    
        <!-- MAIN HEADER: TITLE, INSPECTOR BADGE, ON-TIME STATUS, ACTION CTA -->
        <Card class="border-border shadow-xs">
          <CardHeader class="flex flex-col gap-4 pb-6 lg:flex-row lg:items-start lg:justify-between">
            <div class="space-y-2">
              <!-- Compliance Pills & Live Status -->
              <div class="flex flex-wrap items-center gap-2">
                <Badge variant="outline" class="gap-1.5 font-mono text-xs">
                  <ClipboardCheck class="text-primary size-3.5" aria-hidden="true" />
                  ISO-55001 & OSHA Audit Matrix
                </Badge>
    
                <!-- Emerald Status Badge -->
                <div
                  class="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 class="relative flex size-1.5">
                    <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                    <span class="bg-success relative inline-flex size-1.5 rounded-full"></span>
                  </span>
                  8 Audits Completed Today · 100% On-Time
                </div>
              </div>
    
              <!-- Title & Inspector Subtext -->
              <div>
                <h1 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">Site & Asset Inspection Audits</h1>
                <div class="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-xs sm:text-sm">
                  <div class="text-foreground flex items-center gap-1.5 font-medium">
                    <Avatar class="border-border size-5 border">
                      <AvatarFallback class="bg-primary/10 text-primary text-xs font-bold">MV</AvatarFallback>
                    </Avatar>
                    <span>Marcus Vance</span>
                  </div>
                  <span class="text-muted-foreground">Senior Field Engineer (PE #84920-CA)</span>
                  <span class="text-muted-foreground font-mono">Terminal: Apex Facility West</span>
                </div>
              </div>
            </div>
    
            <!-- Action CTAs -->
            <div class="flex flex-wrap items-center gap-2.5 pt-1">
              <Button
                aria-label="Download attachment"
                variant="outline"
                size="sm"
                class="gap-1.5 text-xs font-medium shadow-xs"
                @click="showToast('Exporting complete site inspection audit summary CSV...')"
              >
                <Download class="size-3.5" aria-hidden="true" />
                Export Audits CSV
              </Button>
    
              <Button size="sm" class="gap-1.5 text-xs font-medium shadow-xs" @click="handleStartNewInspection">
                <Plus class="size-3.5" aria-hidden="true" />
                Start New Inspection
              </Button>
            </div>
          </CardHeader>
        </Card>
    
        <!-- 4 FIELD INSPECTION METRIC KPI CARDS -->
        <InspectionMetrics />
    
        <!-- SEARCH & FILTER TOOLBAR -->
        <Card class="border-border shadow-xs">
          <CardContent class="p-4">
            <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              <!-- Filter Tabs / Pills -->
              <div class="flex flex-wrap items-center gap-1.5">
                <button
                  type="button"
                  :class="[
                    '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',
                  ]"
                  @click="activeFilter = 'all'"
                >
                  All Audits (5)
                </button>
    
                <button
                  type="button"
                  :class="[
                    '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',
                  ]"
                  @click="activeFilter = 'passed'"
                >
                  <CheckCircle2 class="size-3.5" />
                  Passed (2)
                </button>
    
                <button
                  type="button"
                  :class="[
                    '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',
                  ]"
                  @click="activeFilter = 'critical'"
                >
                  <AlertTriangle class="size-3.5" />
                  Critical Defects (2)
                </button>
    
                <button
                  type="button"
                  :class="[
                    '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',
                  ]"
                  @click="activeFilter = 'minor'"
                >
                  <AlertCircle class="size-3.5" />
                  Minor Issues (1)
                </button>
              </div>
    
              <!-- Search Input & Facility Selector -->
              <div class="flex flex-wrap items-center gap-2">
                <!-- Facility Dropdown Filter -->
                <div class="w-full sm:w-48">
                  <Select v-model="selectedFacility">
                    <SelectTrigger class="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>
    
                <!-- Search Field -->
                <div class="relative w-full sm:w-64">
                  <Search class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" />
                  <Input
                    v-model="searchQuery"
                    type="text"
                    placeholder="Search asset, ID, category..."
                    class="h-8 pl-8 text-xs"
                  />
                </div>
              </div>
            </div>
          </CardContent>
        </Card>
    
        <!-- AUDIT RECORDS DATA TABLE -->
        <Card class="border-border overflow-hidden shadow-xs">
          <div class="overflow-x-auto">
            <Table>
              <TableHeader>
                <TableRow class="bg-muted/30">
                  <TableHead class="text-foreground w-[220px] text-xs font-semibold"> Asset & Facility </TableHead>
                  <TableHead class="text-foreground text-xs font-semibold"> Date & GPS Stamping </TableHead>
                  <TableHead class="text-foreground text-xs font-semibold"> Inspection Checklist </TableHead>
                  <TableHead class="text-foreground text-xs font-semibold"> Status & Work Order </TableHead>
                  <TableHead class="text-foreground text-xs font-semibold"> Photographic Evidence </TableHead>
                  <TableHead class="text-foreground text-xs font-semibold"> Inspector & Sign-off </TableHead>
                  <TableHead class="text-foreground text-right text-xs font-semibold"> Actions </TableHead>
                </TableRow>
              </TableHeader>
    
              <TableBody>
                <TableRow
                  v-for="record in filteredRecords"
                  :key="record.id"
                  class="border-border hover:bg-muted/20 transition-colors"
                >
                  <!-- 1. Asset & Facility -->
                  <TableCell class="py-3.5 align-top">
                    <div class="space-y-1">
                      <div class="flex items-center gap-1.5">
                        <span class="text-foreground text-xs font-bold">{{ record.assetName }}</span>
                        <Badge variant="outline" class="font-mono text-xs">{{ record.buildingBadge }}</Badge>
                      </div>
                      <div class="text-muted-foreground font-mono text-xs">
                        {{ record.assetId }}
                      </div>
                      <div class="text-muted-foreground/90 flex items-center gap-1 text-xs">
                        <Building2 class="size-3 shrink-0" />
                        <span class="truncate">{{ record.facility }}</span>
                      </div>
                    </div>
                  </TableCell>
    
                  <!-- 2. Date & GPS Stamping -->
                  <TableCell class="py-3.5 align-top">
                    <div class="space-y-1">
                      <div class="text-foreground flex items-center gap-1 text-xs font-medium">
                        <Calendar class="text-muted-foreground size-3" />
                        <span>{{ record.date }}</span>
                      </div>
                      <div class="text-muted-foreground flex items-center gap-1 font-mono text-xs">
                        <Clock class="size-3" />
                        <span>{{ record.time }} ({{ record.duration }})</span>
                      </div>
                      <!-- GPS Pill -->
                      <button
                        type="button"
                        :aria-label="`Copy GPS coordinates for ${record.assetId}: ${record.gps}`"
                        class="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"
                        @click="copyGps(record.gps, record.id)"
                      >
                        <MapPin class="text-primary size-2.5" />
                        <span>{{ record.gps }}</span>
                        <Check v-if="copiedGpsId === record.id" class="text-success size-2.5" />
                        <Copy v-else class="text-muted-foreground/60 size-2.5" />
                      </button>
                    </div>
                  </TableCell>
    
                  <!-- 3. Inspection Checklist -->
                  <TableCell class="py-3.5 align-top">
                    <div class="w-48 space-y-1.5">
                      <div class="flex items-center justify-between text-xs">
                        <span class="text-muted-foreground font-medium">Score</span>
                        <span
                          :class="[
                            'font-mono font-semibold',
                            record.status === 'passed'
                              ? 'text-success'
                              : record.status === 'critical'
                                ? 'text-destructive'
                                : 'text-warning',
                          ]"
                        >
                          {{ record.statusLabel }}
                        </span>
                      </div>
                      <!-- Progress Bar -->
                      <div class="bg-muted h-1.5 w-full overflow-hidden rounded-full">
                        <div
                          :class="[
                            '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>
                      <!-- Mini Checklist Summary -->
                      <div class="text-muted-foreground space-y-0.5 text-xs">
                        <div
                          v-for="item in record.checklist.slice(0, 2)"
                          :key="item.id"
                          class="flex items-center gap-1 truncate font-mono text-xs"
                        >
                          <CheckCircle2 v-if="item.status === 'pass'" class="text-success size-2.5 shrink-0" />
                          <AlertTriangle v-else-if="item.status === 'fail'" class="text-destructive size-2.5 shrink-0" />
                          <AlertCircle v-else class="text-warning size-2.5 shrink-0" />
                          <span class="truncate">{{ item.label }}: {{ item.value }}</span>
                        </div>
                      </div>
                    </div>
                  </TableCell>
    
                  <!-- 4. Status & Work Order -->
                  <TableCell class="py-3.5 align-top">
                    <div class="space-y-1.5">
                      <Badge
                        :variant="record.status === 'passed' ? 'default' : 'destructive'"
                        :class="[
                          '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',
                        ]"
                      >
                        <CheckCircle2 v-if="record.status === 'passed'" class="mr-1 size-3" />
                        <AlertTriangle v-else-if="record.status === 'critical'" class="mr-1 size-3" />
                        <AlertCircle v-else class="mr-1 size-3" />
                        {{ record.status }}
                      </Badge>
    
                      <!-- Defect or Pass Note -->
                      <p
                        v-if="record.defectSummary"
                        class="text-destructive max-w-[200px] text-xs leading-tight font-medium"
                      >
                        {{ record.defectSummary }}
                      </p>
                      <p v-else class="text-muted-foreground text-xs leading-tight">
                        All calibration points verified within tolerance.
                      </p>
    
                      <!-- Work Order Tag -->
                      <div v-if="record.workOrder" class="pt-0.5">
                        <span
                          class="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 class="text-warning size-2.5" />
                          <span class="max-w-[140px] truncate">{{ record.workOrder }}</span>
                        </span>
                      </div>
                    </div>
                  </TableCell>
    
                  <!-- 5. Photographic Evidence -->
                  <TableCell class="py-3.5 align-top">
                    <div class="space-y-1.5">
                      <div class="flex items-center gap-1.5">
                        <button
                          v-for="(photo, pIdx) in record.photos"
                          :key="photo.id"
                          type="button"
                          :aria-label="`View inspection photo ${pIdx + 1}: ${photo.title}`"
                          class="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"
                          @click="handleOpenPhoto(photo, record)"
                        >
                          <!-- Stylized Vector SVG Illustration per Preview Type -->
                          <InspectionThumbnail :type="photo.previewType" />
    
                          <!-- Hover magnifying glass overlay -->
                          <div
                            class="absolute inset-0 flex items-center justify-center bg-black/60 opacity-0 transition-opacity group-hover/thumb:opacity-100"
                          >
                            <Eye class="size-3.5 text-white" />
                          </div>
                        </button>
                      </div>
                      <div class="text-muted-foreground flex items-center gap-1 text-xs">
                        <Camera class="size-3" />
                        <span>{{ record.photos.length }} Attachments</span>
                      </div>
                    </div>
                  </TableCell>
    
                  <!-- 6. Inspector & Sign-off -->
                  <TableCell class="py-3.5 align-top">
                    <div class="space-y-1">
                      <div class="flex items-center gap-2">
                        <Avatar class="border-border size-6 border">
                          <AvatarFallback class="bg-primary/10 text-primary text-xs font-bold">
                            {{ record.inspector.initials }}
                          </AvatarFallback>
                        </Avatar>
                        <span class="text-foreground text-xs font-semibold">
                          {{ record.inspector.name }}
                        </span>
                      </div>
                      <div>
                        <span
                          :class="[
                            'inline-flex items-center gap-1 font-mono text-xs font-medium',
                            record.status === 'critical' ? 'text-destructive' : 'text-success',
                          ]"
                        >
                          <ShieldCheck v-if="record.status !== 'critical'" class="size-3 shrink-0" />
                          <ShieldAlert v-else class="size-3 shrink-0" />
                          {{ record.inspector.signoffStatus }}
                        </span>
                      </div>
                      <div class="text-muted-foreground/80 font-mono text-xs">
                        {{ record.inspector.signoffHash }}
                      </div>
                    </div>
                  </TableCell>
    
                  <!-- 7. Actions Dropdown -->
                  <TableCell class="py-3.5 text-right align-top">
                    <DropdownMenu>
                      <DropdownMenuTrigger as-child>
                        <Button variant="ghost" size="sm" class="size-8 p-0">
                          <span class="sr-only">Open audit menu</span>
                          <MoreHorizontal class="size-4" />
                        </Button>
                      </DropdownMenuTrigger>
                      <DropdownMenuContent align="end" class="w-48 text-xs">
                        <DropdownMenuLabel class="font-mono text-xs">{{ record.assetId }}</DropdownMenuLabel>
                        <DropdownMenuSeparator />
                        <DropdownMenuItem class="cursor-pointer gap-2" @click="handleViewReport(record)">
                          <Eye class="text-primary size-3.5" />
                          <span>View Audit Report</span>
                        </DropdownMenuItem>
                        <DropdownMenuItem class="cursor-pointer gap-2" @click="handleDownloadPdf(record)">
                          <Download class="size-3.5" />
                          <span>Download PDF</span>
                        </DropdownMenuItem>
                        <DropdownMenuItem class="cursor-pointer gap-2" @click="copyGps(record.gps, record.id)">
                          <Copy class="size-3.5" />
                          <span>Copy GPS Coordinates</span>
                        </DropdownMenuItem>
                        <DropdownMenuSeparator />
                        <DropdownMenuItem
                          :class="[
                            'cursor-pointer gap-2',
                            record.status === 'critical' ? 'text-destructive font-semibold' : '',
                          ]"
                          @click="handleCreateWorkOrder(record)"
                        >
                          <Wrench class="size-3.5" />
                          <span>{{ record.status === 'critical' ? 'Expedite Work Order' : 'Create Work Order' }}</span>
                        </DropdownMenuItem>
                      </DropdownMenuContent>
                    </DropdownMenu>
                  </TableCell>
                </TableRow>
              </TableBody>
            </Table>
          </div>
        </Card>
    
        <!-- PHOTO EVIDENCE LIGHTBOX MODAL DIALOG -->
        <PhotoLightboxDialog
          :preview-photo="previewPhoto"
          @close="previewPhoto = null"
          @download-photo="showToast('Exporting high-resolution raw image file...')"
        />
    
        <!-- COMPREHENSIVE AUDIT REPORT MODAL DIALOG -->
        <AuditReportDialog
          :selected-audit="selectedAudit"
          @close="selectedAudit = null"
          @preview-photo="handleOpenPhoto"
          @download-report="handleDownloadPdf"
        />
    
        <!-- START NEW INSPECTION MODAL DIALOG -->
        <NewAuditDialog v-model:open="isNewAuditOpen" @toast="showToast" />
      </div>
    </template>
    
  • app/components/blocks/PhotoLightboxDialog.vue8.7 kB
  • app/components/blocks/AuditReportDialog.vue8.4 kB
  • app/components/blocks/NewAuditDialog.vue5.8 kB
  • app/components/blocks/InspectionMetrics.vue4.1 kB
  • app/components/blocks/InspectionThumbnail.vue6.8 kB
  • app/components/blocks/field-inspection-types.ts1 kB
  • app/components/blocks/field-inspection-data.ts10.4 kB

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