Menu

Change language

GitHub repo

Driver Inspection Checklist

blocklogistics

Commercial vehicle Driver Vehicle Inspection Report (DVIR) pre-trip and post-trip safety audit. Features vehicle odometer & ELD telemetry header, 5 grouped FMCSA safety inspection categories (Brakes & Air Pressure, Tires & Wheels, Lights & Electrical, Engine & Fluids, Emergency Safety Gear) with Pass/Fail/N/A segmented controls, dynamic defect report card with Out-of-Service severity classification, defect notes, photo dropzone, driver CDL certification signature pad, and post-submission compliance receipt.

Installation

$npx uipkge-ng@latest add driver-inspection-checklist
Manifest:https://uipkge.dev/r/angular/driver-inspection-checklist.jsonInstalls to:src/app/components/blocks/driver-inspection-checklist/

Variants

Loading interactive previews…

Schema

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

InspectionItem
interface InspectionItem {
  id: string
  label: string
  spec: string
  status: CheckStatus
  notes?: string
}
InspectionCategory
interface InspectionCategory {
  id: string
  title: string
  description: string
  icon: string
  items: InspectionItem[]
}
DefectPhoto
interface DefectPhoto {
  id: string
  name: string
  size: string
  url?: string
}
DVIRReportData
interface DVIRReportData {
  reportId: string
  vehicleId: string
  trailerId: string
  odometer: string
  inspectionType: InspectionType
  driverName: string
  driverCdl: string
  carrierName: string
  dotNumber: string
  inspectionDate: string
  inspectionTime: string
  location: string
  categories: InspectionCategory[]
  defectSeverity: DefectSeverity
  defectNotes: string
  defectPhotos: DefectPhoto[]
  certified: boolean
  signatureTimestamp: string
}

Files installed (2)

  • src/app/components/blocks/driver-inspection-checklist/driver-inspection-checklist.component.ts50.8 kB
    // Commercial vehicle Driver Vehicle Inspection Report (DVIR) pre-trip and post-trip safety
    // audit. Features vehicle odometer & ELD telemetry header, 5 grouped FMCSA safety inspection
    // categories (Brakes & Air Pressure, Tires & Wheels, Lights & Electrical, Engine & Fluids,
    // Emergency Safety Gear) with Pass/Fail/N/A segmented controls, dynamic defect report card with
    // Out-of-Service severity classification, defect notes, photo dropzone, driver CDL certification
    // signature pad, and post-submission compliance receipt. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed, signal } from '@angular/core'
    import {
      AlertCircle,
      AlertTriangle,
      Camera,
      Check,
      CheckCircle2,
      Clock,
      Disc,
      Download,
      FileCheck,
      FileText,
      Flame,
      Gauge,
      Layers,
      Lightbulb,
      LucideAngularModule,
      Minus,
      Printer,
      RotateCcw,
      ShieldAlert,
      ShieldCheck,
      Trash2,
      Truck,
      UploadCloud,
      User,
      Wrench,
      X,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardFooterComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiLabelComponent } from '@/ui/label/label.component'
    import {
      UiSelectComponent,
      UiSelectContentComponent,
      UiSelectItemComponent,
      UiSelectTriggerComponent,
      UiSelectValueComponent,
    } from '@/ui/select/select.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import { UiTextareaComponent } from '@/ui/textarea/textarea.component'
    
    export type CheckStatus = 'pass' | 'fail' | 'na'
    export type DefectSeverity = 'minor' | 'critical'
    export type InspectionType = 'pre-trip' | 'post-trip' | 'intermodal'
    
    export interface InspectionItem {
      id: string
      label: string
      spec: string
      status: CheckStatus
      notes?: string
    }
    
    export interface InspectionCategory {
      id: string
      title: string
      description: string
      icon: string
      items: InspectionItem[]
    }
    
    export interface DefectPhoto {
      id: string
      name: string
      size: string
      url?: string
    }
    
    export interface DVIRReportData {
      reportId: string
      vehicleId: string
      trailerId: string
      odometer: string
      inspectionType: InspectionType
      driverName: string
      driverCdl: string
      carrierName: string
      dotNumber: string
      inspectionDate: string
      inspectionTime: string
      location: string
      categories: InspectionCategory[]
      defectSeverity: DefectSeverity
      defectNotes: string
      defectPhotos: DefectPhoto[]
      certified: boolean
      signatureTimestamp: string
    }
    
    const DEFAULT_CATEGORIES: InspectionCategory[] = [
      {
        id: 'brakes',
        title: '1. Brakes & Air Pressure',
        description: 'FMCSA § 393.40 / 393.51 Air Brake System & Lines',
        icon: 'disc',
        items: [
          {
            id: 'brakes-service',
            label: 'Service Brakes & Stopping Response',
            spec: 'Smooth pedal feel, no pulling, stopping within 35 ft at 20 mph',
            status: 'pass',
          },
          {
            id: 'brakes-parking',
            label: 'Parking Brake Mechanism',
            spec: 'Holds fully loaded vehicle against low gear engine torque',
            status: 'pass',
          },
          {
            id: 'brakes-airlines',
            label: 'Air Lines & System Pressure (120 PSI)',
            spec: 'Maintains 120 PSI operating pressure; gladhand seals leak-free',
            status: 'pass',
          },
          {
            id: 'brakes-drums',
            label: 'Brake Drums, Linings & Slack Adjusters',
            spec: 'Lining thickness > 1/4", no oil/grease contamination on drums',
            status: 'pass',
          },
        ],
      },
      {
        id: 'tires',
        title: '2. Tires, Wheels & Rims',
        description: 'FMCSA § 393.75 Wheel Assemblies & Tread Integrity',
        icon: 'layers',
        items: [
          {
            id: 'tires-tread',
            label: 'Steer & Drive Tire Tread Depth',
            spec: 'Steer tires > 4/32" tread; drive/trailer tires > 2/32" tread',
            status: 'pass',
          },
          {
            id: 'tires-pressure',
            label: 'Tire Pressure & Cold Inflation (100 PSI)',
            spec: 'All 10 dual/steer tires at 100 PSI cold; valve caps sealed',
            status: 'pass',
          },
          {
            id: 'tires-lugnuts',
            label: 'Lug Nuts & Wheel Rim Fasteners',
            spec: 'All wheel studs present & torqued; no rust trails or rim cracks',
            status: 'pass',
          },
          {
            id: 'tires-hubseals',
            label: 'Wheel Hub Oil Seals & Bearings',
            spec: 'Oil level visible in sight glass; hub seals dry with no leaks',
            status: 'pass',
          },
        ],
      },
      {
        id: 'lights',
        title: '3. Lights & Electrical Systems',
        description: 'FMCSA § 393.9 Lamps, Reflective Devices & Electrical Wiring',
        icon: 'lightbulb',
        items: [
          {
            id: 'lights-headlights',
            label: 'Headlights (Low & High Beams)',
            spec: 'Both sealed beams operable; lenses clean and properly aimed',
            status: 'pass',
          },
          {
            id: 'lights-turnsignals',
            label: 'Turn Signals & 4-Way Hazard Flashers',
            spec: 'Front, cab-side, and rear flashers functional on tractor & trailer',
            status: 'pass',
          },
          {
            id: 'lights-brakelights',
            label: 'Brake Lights & Tail Lights',
            spec: 'Instant illumination upon pedal depression; lenses intact',
            status: 'pass',
          },
          {
            id: 'lights-clearance',
            label: 'Clearance Lights & DOT Reflectors',
            spec: 'Amber front/sides, red rear; DOT-C2 reflective sheeting clean',
            status: 'pass',
          },
        ],
      },
      {
        id: 'engine',
        title: '4. Engine Compartment & Fluids',
        description: 'Powertrain fluids, radiator integrity, belts & steering gear',
        icon: 'wrench',
        items: [
          {
            id: 'engine-oil',
            label: 'Engine Oil Level & Quality',
            spec: 'Dipstick level in safe crosshatch; oil clean without burnt odor',
            status: 'pass',
          },
          {
            id: 'engine-coolant',
            label: 'Engine Coolant & Radiator Core',
            spec: 'Surge tank level at MAX line; radiator cap tight, no hose weeping',
            status: 'pass',
          },
          {
            id: 'engine-powersteering',
            label: 'Power Steering Fluid & Reservoir',
            spec: 'Fluid level within cold fill mark; pump operates without whine',
            status: 'pass',
          },
          {
            id: 'engine-belts',
            label: 'Serpentine Belts & Cooling Hoses',
            spec: 'Belt deflection < 1/2"; no rib fraying, cracks, or soft hoses',
            status: 'pass',
          },
        ],
      },
      {
        id: 'safety',
        title: '5. Emergency & Safety Equipment',
        description: 'FMCSA § 393.95 Emergency Equipment in Commercial Motor Vehicles',
        icon: 'flame',
        items: [
          {
            id: 'safety-extinguisher',
            label: 'Fire Extinguisher (Charged & Tagged)',
            spec: 'Minimum 5 B:C rating; pressure needle in green, annual tag valid',
            status: 'pass',
          },
          {
            id: 'safety-triangles',
            label: 'Reflective Warning Triangles (3 Pack)',
            spec: 'Three bidirectional red reflective triangles present & secured',
            status: 'pass',
          },
          {
            id: 'safety-firstaid',
            label: 'First Aid & Spill Response Kit',
            spec: 'Sealed commercial first aid pack, eye wash, HazMat pads',
            status: 'pass',
          },
          {
            id: 'safety-wipers',
            label: 'Windshield Wipers, Washers & Horn',
            spec: 'Clean sweep without streaks; air horn and electric city horn loud',
            status: 'pass',
          },
        ],
      },
    ]
    
    const DEFECT_SEED_NOTE =
      'Air supply line to secondary reservoir exhibits slight pressure drop (down to 105 PSI under load). Audible hiss near tractor-trailer gladhand seal.'
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-driver-inspection-checklist, [ui-driver-inspection-checklist]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardFooterComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiCheckboxComponent,
        UiInputComponent,
        UiLabelComponent,
        UiSelectComponent,
        UiSelectContentComponent,
        UiSelectItemComponent,
        UiSelectTriggerComponent,
        UiSelectValueComponent,
        UiSeparatorComponent,
        UiTextareaComponent,
      ],
      template: `
        <div data-slot="driver-inspection-checklist" [class]="rootClass">
          @if (submitted()) {
            <div class="space-y-6">
              <div ui-card class="border-border overflow-hidden shadow-xs">
                <div [class]="receiptBannerClass">
                  <div class="relative mx-auto mb-4 size-16">
                    <span [class]="receiptGlowClass" aria-hidden="true"></span>
                    <span [class]="receiptIconWrapClass">
                      @if (hasDefects()) {
                        <lucide-icon [img]="ShieldAlert" class="size-8" aria-hidden="true" />
                      } @else {
                        <lucide-icon [img]="CheckCircle2" class="size-8" aria-hidden="true" />
                      }
                    </span>
                  </div>
    
                  <span ui-badge [variant]="hasDefects() ? 'destructive' : 'outline'" [class]="receiptBadgeClass">
                    FMCSA § 396.11 Certified Electronic DVIR
                  </span>
    
                  <h2 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">
                    {{ hasDefects() ? 'DVIR Transmitted · Defect Action Required' : 'DVIR Certified · Ready for Dispatch' }}
                  </h2>
                  <p class="text-muted-foreground mx-auto mt-1.5 max-w-xl text-xs sm:text-sm">
                    {{
                      hasDefects()
                        ? 'Inspection report logged with safety defects. Maintenance dispatch notification has been dispatched to the terminal garage desk.'
                        : 'Commercial motor vehicle safety inspection verified with zero safety defects. Safe to operate under FMCSA regulations.'
                    }}
                  </p>
    
                  <div
                    class="border-border bg-background/80 mt-4 inline-flex flex-wrap items-center gap-2 rounded-full border px-3.5 py-1 text-xs backdrop-blur-xs"
                  >
                    <span class="text-muted-foreground font-medium">Compliance Document Ref:</span>
                    <span class="text-foreground font-mono font-bold">{{ reportId }}</span>
                  </div>
                </div>
    
                <div ui-card-content class="space-y-6 p-6">
                  <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
                    <div class="border-border bg-muted/20 rounded-lg border p-3">
                      <span class="text-muted-foreground block text-xs font-medium">Vehicle / Tractor</span>
                      <span class="text-foreground mt-0.5 block text-sm font-semibold">{{ vehicleId }}</span>
                      <span class="text-muted-foreground text-xs">{{ odometer }}</span>
                    </div>
                    <div class="border-border bg-muted/20 rounded-lg border p-3">
                      <span class="text-muted-foreground block text-xs font-medium">Associated Trailer</span>
                      <span class="text-foreground mt-0.5 block text-sm font-semibold">{{ trailerId }}</span>
                      <span class="text-muted-foreground text-xs">Type: 53-ft Dry Van</span>
                    </div>
                    <div class="border-border bg-muted/20 rounded-lg border p-3">
                      <span class="text-muted-foreground block text-xs font-medium">Certified Driver</span>
                      <span class="text-foreground mt-0.5 block text-sm font-semibold">{{ driverName }}</span>
                      <span class="text-muted-foreground font-mono text-xs">CDL #{{ driverCdl }}</span>
                    </div>
                    <div class="border-border bg-muted/20 rounded-lg border p-3">
                      <span class="text-muted-foreground block text-xs font-medium">Timestamp & Hub</span>
                      <span class="text-foreground mt-0.5 block text-sm font-semibold">
                        {{ inspectionDate }} · {{ inspectionTime }}
                      </span>
                      <span class="text-muted-foreground block truncate text-xs">{{ location }}</span>
                    </div>
                  </div>
    
                  <div
                    class="border-border bg-card flex flex-wrap items-center justify-between gap-3 rounded-lg border p-4"
                  >
                    <div class="flex flex-wrap items-center gap-x-6 gap-y-3">
                      <div>
                        <span class="text-muted-foreground block text-xs">Items Inspected</span>
                        <span class="text-foreground text-lg font-bold tabular-nums">
                          {{ totalCount() }} / {{ totalCount() }}
                        </span>
                      </div>
                      <ui-separator orientation="vertical" class="h-8" />
                      <div>
                        <span class="text-muted-foreground block text-xs">Passed</span>
                        <span class="text-success text-lg font-bold tabular-nums">{{ passCount() }}</span>
                      </div>
                      <ui-separator orientation="vertical" class="h-8" />
                      <div>
                        <span class="text-muted-foreground block text-xs">Failed / Defects</span>
                        <span [class]="receiptFailClass">{{ failCount() }}</span>
                      </div>
                      <ui-separator orientation="vertical" class="h-8" />
                      <div>
                        <span class="text-muted-foreground block text-xs">N/A</span>
                        <span class="text-muted-foreground text-lg font-bold tabular-nums">{{ naCount() }}</span>
                      </div>
                    </div>
    
                    <span ui-badge [variant]="hasDefects() ? 'destructive' : 'outline'" class="gap-1.5 px-3 py-1 text-xs">
                      @if (!hasDefects()) {
                        <lucide-icon [img]="CheckCircle2" class="size-3.5" />
                      } @else {
                        <lucide-icon [img]="AlertTriangle" class="size-3.5" />
                      }
                      {{ hasDefects() ? 'Defects Found (' + failCount() + ')' : 'All Systems Verified' }}
                    </span>
                  </div>
    
                  <div class="border-border bg-muted/15 space-y-2 rounded-xl border p-4">
                    <div class="flex items-center justify-between">
                      <span class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
                        <lucide-icon [img]="ShieldCheck" class="text-primary size-3.5" />
                        Electronic Cryptographic Driver Signature
                      </span>
                      <span class="text-muted-foreground font-mono text-xs">DOT § 396.11 Certified</span>
                    </div>
                    <div class="border-border/60 border-b pt-1 pb-3">
                      <p class="text-primary text-2xl font-medium tracking-wide italic">
                        {{ typedSignature() || driverName }}
                      </p>
                    </div>
                    <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-2 font-mono text-xs">
                      <span>Signer: {{ driverName }} · CDL #{{ driverCdl }}</span>
                      <span>SHA-256: 8f9b7c2d-9482-41e9-b631-{{ reportId }}</span>
                    </div>
                  </div>
    
                  <div class="flex flex-wrap items-center justify-between gap-3 pt-2">
                    <button
                      ui-button
                      variant="outline"
                      size="sm"
                      class="gap-1.5 text-xs font-medium"
                      (click)="handleReset()"
                    >
                      <lucide-icon [img]="RotateCcw" class="size-3.5" />
                      Start New Inspection
                    </button>
    
                    <div class="flex flex-wrap items-center gap-2">
                      <button ui-button variant="outline" size="sm" class="gap-1.5 text-xs font-medium">
                        <lucide-icon [img]="Printer" class="size-3.5" />
                        Print Driver Copy
                      </button>
                      <button ui-button aria-label="Download attachment" size="sm" class="gap-1.5 text-xs font-medium">
                        <lucide-icon [img]="Download" class="size-3.5" />
                        Download Certified DVIR PDF
                      </button>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          } @else {
            <div class="space-y-6">
              <!-- Top Inspection Header Card -->
              <div ui-card class="border-border shadow-xs">
                <div ui-card-header class="pb-4">
                  <div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
                    <div class="space-y-1.5">
                      <div class="flex flex-wrap items-center gap-2">
                        <span
                          ui-badge
                          variant="outline"
                          class="border-primary/30 bg-primary/10 text-primary gap-1.5 py-0.5 text-xs font-medium"
                        >
                          <lucide-icon [img]="Truck" class="size-3.5" aria-hidden="true" />
                          FMCSA § 396.11 Audit
                        </span>
    
                        @if (hasDefects()) {
                          <span ui-badge variant="destructive" class="animate-pulse gap-1.5 py-0.5 text-xs font-medium">
                            <lucide-icon [img]="AlertTriangle" class="size-3.5" aria-hidden="true" />
                            Defects Found · {{ failCount() }} {{ failCount() === 1 ? 'Item' : 'Items' }} Failed
                          </span>
                        } @else {
                          <span
                            ui-badge
                            variant="outline"
                            class="border-warning/40 bg-warning/10 text-warning gap-1.5 py-0.5 text-xs font-medium"
                          >
                            <lucide-icon [img]="Clock" class="size-3.5" aria-hidden="true" />
                            Inspection In Progress
                          </span>
                        }
                      </div>
    
                      <h3 ui-card-title class="text-xl font-bold tracking-tight sm:text-2xl">
                        Driver Vehicle Inspection Report (DVIR)
                      </h3>
                      <p ui-card-description class="text-xs sm:text-sm">
                        Mandatory pre-trip and post-trip commercial motor vehicle safety certification and defect tracking
                        portal.
                      </p>
                    </div>
    
                    <div class="flex flex-wrap items-center gap-2">
                      <button
                        type="button"
                        ui-button
                        variant="outline"
                        size="sm"
                        class="gap-1.5 text-xs font-medium"
                        (click)="markAllPass()"
                      >
                        <lucide-icon [img]="CheckCircle2" class="text-success size-3.5" aria-hidden="true" />
                        Quick Pass All
                      </button>
                      <button
                        type="button"
                        ui-button
                        size="sm"
                        class="gap-1.5 text-xs font-medium"
                        [disabled]="!certified() || !typedSignature().trim()"
                        (click)="handleSubmit()"
                      >
                        <lucide-icon [img]="FileCheck" class="size-3.5" aria-hidden="true" />
                        Submit Completed DVIR
                      </button>
                    </div>
                  </div>
                </div>
    
                <div ui-card-content class="pt-0 pb-5">
                  <div class="border-border bg-muted/20 grid gap-3 rounded-lg border p-4 sm:grid-cols-2 lg:grid-cols-4">
                    <div class="space-y-1">
                      <div class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
                        <lucide-icon [img]="Truck" class="text-primary size-3.5" />
                        <span>Vehicle / Unit Number</span>
                      </div>
                      <div class="text-foreground text-xs font-semibold">{{ vehicleId }}</div>
                      <div class="text-muted-foreground text-xs">Trailer: {{ trailerId }}</div>
                    </div>
    
                    <div class="space-y-1">
                      <div class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
                        <lucide-icon [img]="Gauge" class="text-primary size-3.5" />
                        <span>Odometer Mileage</span>
                      </div>
                      <div class="text-foreground text-xs font-semibold tabular-nums">{{ odometer }}</div>
                      <div class="text-muted-foreground text-xs">ECM Telemetry Synced</div>
                    </div>
    
                    <div class="space-y-1">
                      <div class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
                        <lucide-icon [img]="FileText" class="text-primary size-3.5" />
                        <span>Inspection Type</span>
                      </div>
                      <ui-select [value]="inspectionType()" (valueChange)="setInspectionType($event)">
                        <button ui-select-trigger class="h-7 text-xs font-medium">
                          <ui-select-value />
                        </button>
                        <ui-select-content>
                          <ui-select-item value="pre-trip">Pre-Trip Safety Inspection</ui-select-item>
                          <ui-select-item value="post-trip">Post-Trip Safety Audit</ui-select-item>
                          <ui-select-item value="intermodal">Intermodal Chassis Inspection</ui-select-item>
                        </ui-select-content>
                      </ui-select>
                    </div>
    
                    <div class="space-y-1">
                      <div class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
                        <lucide-icon [img]="User" class="text-primary size-3.5" />
                        <span>Driver & Terminal</span>
                      </div>
                      <div class="text-foreground text-xs font-semibold">{{ driverName }} · CDL #{{ driverCdl }}</div>
                      <div class="text-muted-foreground truncate text-xs">{{ location }}</div>
                    </div>
                  </div>
    
                  <div class="mt-4 space-y-1.5">
                    <div class="text-muted-foreground flex items-center justify-between text-xs">
                      <span>Audit Completion Progress</span>
                      <span class="text-foreground font-mono font-medium tabular-nums">
                        {{ passCount() }} Pass · {{ failCount() }} Fail · {{ naCount() }} N/A ({{ totalCount() }} Total)
                      </span>
                    </div>
                    <div class="bg-muted h-1.5 w-full overflow-hidden rounded-full">
                      <div [class]="progressBarClass" [style.width]="completionPercentage() + '%'"></div>
                    </div>
                  </div>
                </div>
              </div>
    
              <!-- 5 FMCSA INSPECTION CATEGORIES -->
              <div class="space-y-4">
                @for (category of categories(); track category.id) {
                  <div ui-card class="border-border overflow-hidden shadow-xs">
                    <div ui-card-header class="bg-muted/15 border-border border-b px-5 py-3.5">
                      <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                        <div class="flex flex-wrap items-center gap-2.5">
                          <div
                            class="border-primary/20 bg-primary/10 text-primary flex size-8 shrink-0 items-center justify-center rounded-md border"
                          >
                            @if (category.icon === 'disc') {
                              <lucide-icon [img]="Disc" class="size-4" />
                            } @else if (category.icon === 'layers') {
                              <lucide-icon [img]="Layers" class="size-4" />
                            } @else if (category.icon === 'lightbulb') {
                              <lucide-icon [img]="Lightbulb" class="size-4" />
                            } @else if (category.icon === 'wrench') {
                              <lucide-icon [img]="Wrench" class="size-4" />
                            } @else if (category.icon === 'flame') {
                              <lucide-icon [img]="Flame" class="size-4" />
                            }
                          </div>
                          <div>
                            <h3 ui-card-title class="text-sm font-bold tracking-tight sm:text-base">
                              {{ category.title }}
                            </h3>
                            <p ui-card-description class="text-xs">{{ category.description }}</p>
                          </div>
                        </div>
    
                        <div class="flex flex-wrap items-center gap-2 self-end sm:self-auto">
                          <button
                            type="button"
                            ui-button
                            variant="ghost"
                            size="sm"
                            class="text-muted-foreground hover:text-foreground h-7 text-xs"
                            (click)="setCategoryAll(category.id, 'pass')"
                          >
                            Pass All
                          </button>
                          <button
                            type="button"
                            ui-button
                            variant="ghost"
                            size="sm"
                            class="text-muted-foreground hover:text-foreground h-7 text-xs"
                            (click)="setCategoryAll(category.id, 'na')"
                          >
                            N/A All
                          </button>
                        </div>
                      </div>
                    </div>
    
                    <div ui-card-content class="divide-border divide-y p-0">
                      @for (item of category.items; track item.id) {
                        <div [class]="itemRowClass(item.status)">
                          <div class="min-w-0 space-y-1 pr-2">
                            <div class="flex flex-wrap items-center gap-2">
                              <span class="text-foreground text-xs font-semibold sm:text-sm">{{ item.label }}</span>
                              @if (item.status === 'fail') {
                                <span ui-badge variant="destructive" class="h-4 py-0 font-mono text-xs uppercase">
                                  Defect
                                </span>
                              }
                            </div>
                            <p class="text-muted-foreground text-xs leading-relaxed">{{ item.spec }}</p>
                          </div>
    
                          <div class="flex shrink-0 items-center gap-1 self-start sm:self-center">
                            <button
                              type="button"
                              [attr.aria-label]="'Mark ' + item.label + ' as Pass'"
                              [class]="segmentClass(item.status, 'pass')"
                              (click)="setItemStatus(category.id, item.id, 'pass')"
                            >
                              <lucide-icon [img]="Check" class="size-3.5" aria-hidden="true" />
                              Pass
                            </button>
                            <button
                              type="button"
                              [attr.aria-label]="'Mark ' + item.label + ' as Fail'"
                              [class]="segmentClass(item.status, 'fail')"
                              (click)="setItemStatus(category.id, item.id, 'fail')"
                            >
                              <lucide-icon [img]="X" class="size-3.5" aria-hidden="true" />
                              Fail
                            </button>
                            <button
                              type="button"
                              [attr.aria-label]="'Mark ' + item.label + ' as Not Applicable'"
                              [class]="segmentClass(item.status, 'na')"
                              (click)="setItemStatus(category.id, item.id, 'na')"
                            >
                              <lucide-icon [img]="Minus" class="size-3.5" aria-hidden="true" />
                              N/A
                            </button>
                          </div>
                        </div>
                      }
                    </div>
                  </div>
                }
              </div>
    
              <!-- DYNAMIC DEFECT REPORT CARD -->
              @if (hasDefects()) {
                <div
                  ui-card
                  class="border-destructive/40 bg-destructive/5 dark:bg-destructive/10 overflow-hidden shadow-xs transition-colors duration-200"
                >
                  <div ui-card-header class="border-destructive/20 bg-destructive/10 border-b pb-4">
                    <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                      <div class="flex flex-wrap items-center gap-2.5">
                        <div
                          class="bg-destructive/20 text-destructive flex size-8 shrink-0 items-center justify-center rounded-md"
                        >
                          <lucide-icon [img]="AlertTriangle" class="size-4.5" aria-hidden="true" />
                        </div>
                        <div>
                          <h3 ui-card-title class="text-destructive text-base font-bold">
                            Critical Defect Log & Repair Dispatch Form
                          </h3>
                          <p ui-card-description class="text-destructive/80 text-xs">
                            {{ failCount() }} item(s) flagged with safety defects requiring mechanic sign-off under FMCSA §
                            396.13.
                          </p>
                        </div>
                      </div>
    
                      <span ui-badge variant="destructive" class="font-mono text-xs uppercase">Action Required</span>
                    </div>
                  </div>
    
                  <div ui-card-content class="space-y-4 p-5">
                    <div class="space-y-1.5">
                      <label ui-label class="text-foreground text-xs font-semibold">Defective Components Identified</label>
                      <div class="flex flex-wrap gap-2">
                        @for (item of failedItems(); track item.id) {
                          <span
                            class="border-destructive/30 bg-destructive/10 text-destructive inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium"
                          >
                            <lucide-icon [img]="AlertCircle" class="size-3.5 shrink-0" />
                            {{ item.label }}
                          </span>
                        }
                      </div>
                    </div>
    
                    <div class="grid gap-4 sm:grid-cols-2">
                      <div class="min-w-0 space-y-1.5">
                        <label ui-label for="defect-severity-ng" class="text-xs font-semibold">
                          Defect Severity Classification *
                        </label>
                        <ui-select [value]="defectSeverity()" (valueChange)="setDefectSeverity($event)">
                          <button
                            ui-select-trigger
                            id="defect-severity-ng"
                            class="h-9 w-full min-w-0 text-xs [&>span]:truncate"
                          >
                            <ui-select-value />
                          </button>
                          <ui-select-content>
                            <ui-select-item value="minor">
                              Minor Defect — Safe to operate; scheduled for terminal shop
                            </ui-select-item>
                            <ui-select-item value="critical">
                              Out-of-Service (OOS) Critical — Dispatch prohibited until repaired
                            </ui-select-item>
                          </ui-select-content>
                        </ui-select>
                      </div>
    
                      <div class="space-y-1.5">
                        <label ui-label class="text-xs font-semibold">Fleet Maintenance Desk Dispatch</label>
                        <div class="border-border bg-card flex flex-wrap items-center gap-2 rounded-md border p-2 text-xs">
                          <ui-checkbox
                            id="desk-notify-ng"
                            [checked]="defectReportedToDesk()"
                            (checkedChange)="defectReportedToDesk.set(!!$event)"
                          />
                          <label ui-label for="desk-notify-ng" class="cursor-pointer text-xs font-normal">
                            Auto-create Priority Work Order in Garage Portal (Ticket #WO-8910)
                          </label>
                        </div>
                      </div>
                    </div>
    
                    <div class="space-y-1.5">
                      <label ui-label for="defect-notes-ng" class="text-xs font-semibold">
                        Detailed Defect Description & Driver Notes *
                      </label>
                      <ui-textarea
                        id="defect-notes-ng"
                        [value]="defectNotes()"
                        (valueChange)="defectNotes.set($event)"
                        [rows]="3"
                        placeholder="Describe exact defect location, leak sounds, tire damage, PSI measurements, or electrical malfunction..."
                        class="text-xs"
                      />
                    </div>
    
                    <div class="space-y-2">
                      <div class="flex items-center justify-between">
                        <label ui-label class="flex items-center gap-1.5 text-xs font-semibold">
                          <lucide-icon [img]="Camera" class="text-primary size-3.5" />
                          Defect Photographic Evidence
                        </label>
                        <span class="text-muted-foreground text-xs">JPG, PNG up to 15MB</span>
                      </div>
    
                      <div
                        role="button"
                        tabindex="0"
                        aria-label="Upload photo"
                        class="border-border/80 hover:border-primary/50 focus-visible:ring-ring bg-card cursor-pointer rounded-xl border-2 border-dashed p-4 text-center transition-colors outline-none focus-visible:ring-2"
                        (click)="addMockPhoto()"
                        (keydown)="onDropzoneKeydown($event)"
                      >
                        <div class="flex flex-col items-center justify-center gap-1.5 py-2">
                          <div class="bg-primary/10 text-primary flex size-9 items-center justify-center rounded-full">
                            <lucide-icon [img]="UploadCloud" class="size-4.5" />
                          </div>
                          <p class="text-foreground text-xs font-medium">
                            Click to attach photo or drag and drop image here
                          </p>
                          <p class="text-muted-foreground text-xs">
                            Capture close-ups of damaged tires, fluid puddles, broken lights, or air fittings
                          </p>
                        </div>
                      </div>
    
                      @if (defectPhotos().length > 0) {
                        <div class="grid gap-2 sm:grid-cols-2">
                          @for (photo of defectPhotos(); track photo.id) {
                            <div
                              class="border-border bg-card flex items-center justify-between rounded-lg border px-3 py-2 text-xs"
                            >
                              <div class="flex flex-wrap items-center gap-2 truncate">
                                <lucide-icon [img]="Camera" class="text-primary size-3.5 shrink-0" />
                                <span class="text-foreground truncate font-medium">{{ photo.name }}</span>
                                <span class="text-muted-foreground text-xs">({{ photo.size }})</span>
                              </div>
                              <button
                                type="button"
                                aria-label="Remove photo"
                                class="text-muted-foreground hover:text-destructive focus-visible:ring-ring cursor-pointer rounded-sm p-1 transition-colors outline-none focus-visible:ring-2"
                                (click)="removePhoto(photo.id)"
                              >
                                <lucide-icon [img]="Trash2" class="size-3.5" />
                              </button>
                            </div>
                          }
                        </div>
                      }
                    </div>
                  </div>
                </div>
              }
    
              <!-- DRIVER CERTIFICATION & SIGNATURE -->
              <div ui-card class="border-border shadow-xs">
                <div ui-card-header class="pb-3">
                  <div class="flex items-center justify-between">
                    <div class="flex flex-wrap items-center gap-2">
                      <lucide-icon [img]="ShieldCheck" class="text-primary size-4" />
                      <h3 ui-card-title class="text-base font-bold">Driver Safety Certification & Electronic Signature</h3>
                    </div>
                    <span ui-badge variant="outline" class="text-primary border-primary/30 font-mono text-xs">
                      FMCSA § 396.11(a)
                    </span>
                  </div>
                  <p ui-card-description class="text-xs">
                    Commercial motor vehicle driver must certify that all required items have been inspected and all known
                    defects disclosed.
                  </p>
                </div>
    
                <div ui-card-content class="space-y-4">
                  <div class="border-border bg-muted/20 rounded-lg border p-3.5">
                    <div class="flex items-start gap-3">
                      <ui-checkbox
                        id="cert-check-ng"
                        [checked]="certified()"
                        (checkedChange)="certified.set(!!$event)"
                        class="mt-0.5"
                      />
                      <div class="space-y-1">
                        <label
                          ui-label
                          for="cert-check-ng"
                          class="text-foreground cursor-pointer text-xs leading-relaxed font-semibold"
                        >
                          I certify that I have conducted a thorough safety inspection of this vehicle and trailer in
                          compliance with Federal Motor Carrier Safety Regulations.
                        </label>
                        <p class="text-muted-foreground text-xs leading-relaxed">
                          All components listed above have been checked. Any conditions likely to affect the safe operation
                          of this vehicle or result in mechanical breakdown have been truthfully recorded.
                        </p>
                      </div>
                    </div>
                  </div>
    
                  <div class="grid gap-3 sm:grid-cols-3">
                    <div class="space-y-1.5 sm:col-span-2">
                      <label ui-label for="driver-sig-ng" class="text-xs font-semibold">
                        Type Full Legal Name as Electronic Signature *
                      </label>
                      <ui-input
                        id="driver-sig-ng"
                        [value]="typedSignature()"
                        (valueChange)="typedSignature.set($event)"
                        placeholder="Marcus Vance"
                        class="h-8 text-xs"
                      />
                    </div>
                    <div class="space-y-1.5">
                      <label ui-label class="text-xs font-semibold">Inspection Timestamp</label>
                      <div
                        class="border-border bg-muted/30 text-muted-foreground flex h-8 items-center rounded-md border px-3 font-mono text-xs"
                      >
                        {{ inspectionDate }} · {{ inspectionTime }}
                      </div>
                    </div>
                  </div>
    
                  <div class="border-primary/30 bg-primary/5 space-y-2 rounded-xl border p-4">
                    <div class="flex items-center justify-between">
                      <span class="text-muted-foreground flex items-center gap-1.5 text-xs font-medium">
                        <lucide-icon [img]="ShieldCheck" class="text-primary size-3.5" />
                        Live Digital Signature Stamp
                      </span>
                      <span class="text-primary font-mono text-xs font-medium">Verified CDL Class-A Holder</span>
                    </div>
    
                    <div class="border-primary/20 border-b pt-1 pb-2">
                      <p class="text-primary text-2xl font-medium tracking-wide italic">
                        {{ typedSignature() || 'Marcus Vance' }}
                      </p>
                    </div>
    
                    <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-2 font-mono text-xs">
                      <span>Driver: {{ driverName }} · CDL #{{ driverCdl }}</span>
                      <span>Carrier: {{ carrierName }} ({{ dotNumber }})</span>
                    </div>
                  </div>
                </div>
    
                <div
                  ui-card-footer
                  class="border-border bg-muted/15 flex flex-wrap items-center justify-between gap-3 border-t px-6 py-4"
                >
                  <div class="text-muted-foreground text-xs">
                    Audit Status:
                    <strong class="text-foreground"
                      >{{ passCount() }} Pass, {{ failCount() }} Fail, {{ naCount() }} N/A</strong
                    >
                  </div>
    
                  <div class="flex flex-wrap items-center gap-2">
                    <button
                      type="button"
                      ui-button
                      variant="outline"
                      size="sm"
                      class="text-xs font-medium"
                      (click)="handleReset()"
                    >
                      Reset Audit
                    </button>
                    <button
                      type="button"
                      ui-button
                      size="sm"
                      class="gap-1.5 text-xs font-medium"
                      [disabled]="!certified() || !typedSignature().trim()"
                      (click)="handleSubmit()"
                    >
                      <lucide-icon [img]="FileCheck" class="size-4" aria-hidden="true" />
                      Submit Completed DVIR
                    </button>
                  </div>
                </div>
              </div>
            </div>
          }
        </div>
      `,
    })
    export class UiDriverInspectionChecklistComponent {
      protected readonly AlertCircle = AlertCircle
      protected readonly AlertTriangle = AlertTriangle
      protected readonly Camera = Camera
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Disc = Disc
      protected readonly Download = Download
      protected readonly FileCheck = FileCheck
      protected readonly FileText = FileText
      protected readonly Flame = Flame
      protected readonly Gauge = Gauge
      protected readonly Layers = Layers
      protected readonly Lightbulb = Lightbulb
      protected readonly Minus = Minus
      protected readonly Printer = Printer
      protected readonly RotateCcw = RotateCcw
      protected readonly ShieldAlert = ShieldAlert
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Trash2 = Trash2
      protected readonly Truck = Truck
      protected readonly UploadCloud = UploadCloud
      protected readonly User = User
      protected readonly Wrench = Wrench
      protected readonly X = X
    
      protected readonly reportId = 'DVIR-2026-90412'
      protected readonly carrierName = 'Apex Freight Logistics Inc.'
      protected readonly dotNumber = 'USDOT #2940182'
      protected readonly inspectionDate = '2026-08-21'
      protected readonly inspectionTime = '06:45 AM EDT'
      protected readonly location = 'Terminal #12 · Ontario Logistics Hub, CA'
    
      @Input() vehicleId = 'Truck #104 · Freightliner Cascadia'
      @Input() trailerId = "Trailer #TR-8821 (53' Dry Van)"
      @Input() odometer = '142,850 miles'
      @Input() driverName = 'Marcus Vance'
      @Input() driverCdl = 'CA-948201'
      @Input() set initialInspectionType(v: InspectionType) {
        this.inspectionType.set(v)
      }
      @Input() set initialDefectState(v: boolean) {
        this.applyDefectSeed(!!v)
      }
      @Input() set initialSubmitted(v: boolean) {
        this.submitted.set(!!v)
      }
      @Input('class') className?: string
      @Output() submit = new EventEmitter<DVIRReportData>()
      @Output() defectChange = new EventEmitter<InspectionItem[]>()
    
      readonly inspectionType = signal<InspectionType>('pre-trip')
      readonly categories = signal<InspectionCategory[]>(
        DEFAULT_CATEGORIES.map((cat) => ({ ...cat, items: cat.items.map((item) => ({ ...item })) })),
      )
      readonly defectSeverity = signal<DefectSeverity>('minor')
      readonly defectNotes = signal('')
      readonly defectReportedToDesk = signal(true)
      readonly defectPhotos = signal<DefectPhoto[]>([])
      readonly certified = signal(true)
      readonly typedSignature = signal('Marcus Vance')
      readonly submitted = signal(false)
    
      private photoCounter = 0
    
      constructor() {
        this.typedSignature.set(this.driverName)
      }
    
      readonly allItems = computed(() => this.categories().flatMap((c) => c.items))
      readonly totalCount = computed(() => this.allItems().length)
      readonly passCount = computed(() => this.allItems().filter((i) => i.status === 'pass').length)
      readonly failCount = computed(() => this.allItems().filter((i) => i.status === 'fail').length)
      readonly naCount = computed(() => this.allItems().filter((i) => i.status === 'na').length)
      readonly failedItems = computed(() => this.allItems().filter((i) => i.status === 'fail'))
      readonly hasDefects = computed(() => this.failedItems().length > 0)
      readonly completionPercentage = computed(() => {
        const total = this.totalCount()
        return total > 0 ? Math.round((this.allItems().filter((i) => i.status !== undefined).length / total) * 100) : 0
      })
    
      get rootClass(): string {
        return cn('text-foreground mx-auto w-full max-w-5xl space-y-6', this.className)
      }
    
      get progressBarClass(): string {
        return cn('h-full transition-[width] duration-300', this.hasDefects() ? 'bg-warning' : 'bg-primary')
      }
    
      get receiptBannerClass(): string {
        return cn(
          'border-b p-6 text-center sm:p-8',
          this.hasDefects() ? 'border-warning/30 bg-warning/10' : 'border-success/30 bg-success/10',
        )
      }
    
      get receiptGlowClass(): string {
        return cn('absolute inset-0 rounded-full blur-xl', this.hasDefects() ? 'bg-warning/30' : 'bg-success/30')
      }
    
      get receiptIconWrapClass(): string {
        return cn(
          'relative flex size-16 items-center justify-center rounded-full border shadow-xs',
          this.hasDefects()
            ? 'border-warning/40 bg-warning/20 text-warning'
            : 'border-success/40 bg-success/20 text-success',
        )
      }
    
      get receiptBadgeClass(): string {
        return cn(
          'mb-2 font-mono text-xs tracking-wider uppercase',
          !this.hasDefects() && 'border-success/30 bg-success/10 text-success',
        )
      }
    
      get receiptFailClass(): string {
        return cn('text-lg font-bold tabular-nums', this.failCount() > 0 ? 'text-destructive' : 'text-muted-foreground')
      }
    
      itemRowClass(status: CheckStatus): string {
        return cn(
          'flex flex-col gap-3 p-4 transition-colors sm:flex-row sm:items-center sm:justify-between',
          status === 'fail' ? 'bg-destructive/5' : 'hover:bg-muted/30',
        )
      }
    
      segmentClass(current: CheckStatus, kind: CheckStatus): string {
        const base =
          'focus-visible:ring-ring inline-flex cursor-pointer items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium shadow-xs transition-colors outline-none focus-visible:ring-2'
        if (current === kind) {
          if (kind === 'pass') return cn(base, 'border-success/40 bg-success/15 text-success font-semibold')
          if (kind === 'fail') return cn(base, 'border-destructive/40 bg-destructive/15 text-destructive font-semibold')
          return cn(base, 'border-border bg-muted text-foreground font-semibold')
        }
        return cn(base, 'border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground')
      }
    
      setInspectionType(value: string): void {
        this.inspectionType.set(value as InspectionType)
      }
    
      setDefectSeverity(value: string): void {
        this.defectSeverity.set(value as DefectSeverity)
      }
    
      private applyDefectSeed(on: boolean): void {
        this.categories.set(
          DEFAULT_CATEGORIES.map((cat) => ({
            ...cat,
            items: cat.items.map((item) => {
              if (on && (item.id === 'brakes-airlines' || item.id === 'tires-pressure')) {
                return { ...item, status: 'fail' as CheckStatus }
              }
              return { ...item }
            }),
          })),
        )
        this.defectSeverity.set(on ? 'critical' : 'minor')
        this.defectNotes.set(on ? DEFECT_SEED_NOTE : '')
        this.defectPhotos.set(
          on
            ? [
                { id: 'p1', name: 'gladhand_seal_wear.jpg', size: '2.4 MB' },
                { id: 'p2', name: 'air_pressure_gauge_105psi.png', size: '1.8 MB' },
              ]
            : [],
        )
      }
    
      setItemStatus(categoryId: string, itemId: string, status: CheckStatus): void {
        this.categories.update((prev) =>
          prev.map((cat) => {
            if (cat.id !== categoryId) return cat
            return { ...cat, items: cat.items.map((item) => (item.id === itemId ? { ...item, status } : item)) }
          }),
        )
        this.emitDefectChange()
      }
    
      setCategoryAll(categoryId: string, status: CheckStatus): void {
        this.categories.update((prev) =>
          prev.map((cat) => {
            if (cat.id !== categoryId) return cat
            return { ...cat, items: cat.items.map((item) => ({ ...item, status })) }
          }),
        )
        this.emitDefectChange()
      }
    
      markAllPass(): void {
        this.categories.update((prev) =>
          prev.map((cat) => ({ ...cat, items: cat.items.map((item) => ({ ...item, status: 'pass' as CheckStatus })) })),
        )
        this.emitDefectChange()
      }
    
      addMockPhoto(): void {
        this.photoCounter += 1
        const n = this.photoCounter + this.defectPhotos().length
        this.defectPhotos.update((prev) => [
          ...prev,
          { id: `photo-${Date.now()}-${this.photoCounter}`, name: `defect_evidence_0${n}.jpg`, size: '1.9 MB' },
        ])
      }
    
      removePhoto(id: string): void {
        this.defectPhotos.update((prev) => prev.filter((p) => p.id !== id))
      }
    
      onDropzoneKeydown(event: KeyboardEvent): void {
        if (event.key === 'Enter' || event.key === ' ') {
          event.preventDefault()
          this.addMockPhoto()
        }
      }
    
      private emitDefectChange(): void {
        this.defectChange.emit(this.failedItems())
      }
    
      handleSubmit(): void {
        if (!this.certified() || this.typedSignature().trim() === '') return
        this.submitted.set(true)
        this.submit.emit({
          reportId: this.reportId,
          vehicleId: this.vehicleId,
          trailerId: this.trailerId,
          odometer: this.odometer,
          inspectionType: this.inspectionType(),
          driverName: this.driverName,
          driverCdl: this.driverCdl,
          carrierName: this.carrierName,
          dotNumber: this.dotNumber,
          inspectionDate: this.inspectionDate,
          inspectionTime: this.inspectionTime,
          location: this.location,
          categories: this.categories(),
          defectSeverity: this.defectSeverity(),
          defectNotes: this.defectNotes(),
          defectPhotos: this.defectPhotos(),
          certified: this.certified(),
          signatureTimestamp: new Date().toISOString(),
        })
      }
    
      handleReset(): void {
        this.submitted.set(false)
        this.markAllPass()
        this.defectNotes.set('')
        this.defectPhotos.set([])
      }
    }
    
  • src/app/components/blocks/driver-inspection-checklist/index.ts0.3 kB

Raw manifest:https://uipkge.dev/r/angular/driver-inspection-checklist.json