VBAF.Center.ReferenceCard.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    VBAF-Center — Reference Card Generator
.DESCRIPTION
    Generates a printable A4 landscape PDF reference card
    for a customer showing all signal thresholds in Green/Yellow/Red.

    Reads directly from the customer's signal JSON files —
    always matches what VBAF is actually configured to use.

    Functions:
      Export-VBAFCenterReferenceCard — generate PDF wall card for a customer
#>


function Export-VBAFCenterReferenceCard {
    <#
    .SYNOPSIS
        Generate a printable signal reference card for a customer.
    .EXAMPLE
        Export-VBAFCenterReferenceCard -CustomerID "NordLogistik"
        Export-VBAFCenterReferenceCard -CustomerID "NordLogistik" -OpenBrowser
    #>

    param(
        [Parameter(Mandatory)] [string] $CustomerID,
        [switch] $OpenBrowser
    )

    # ── Check Python available ────────────────────────────────
    $python = $null
    foreach ($cmd in @('python','python3')) {
        if (Get-Command $cmd -ErrorAction SilentlyContinue) {
            $python = $cmd; break
        }
    }
    if (-not $python) {
        Write-Host "Python not found — cannot generate PDF." -ForegroundColor Red
        Write-Host "Install Python from https://python.org" -ForegroundColor Yellow
        return
    }

    # ── Check reportlab ───────────────────────────────────────
    $rlCheck = & $python -c "import reportlab" 2>&1
    if ($rlCheck -like "*No module*" -or $rlCheck -like "*ModuleNotFoundError*") {
        Write-Host "Installing reportlab..." -ForegroundColor Yellow
        & $python -m pip install reportlab --quiet 2>$null
    }

    # ── Load customer data ────────────────────────────────────
    $base         = Join-Path $env:USERPROFILE "VBAFCenter"
    $profilePath  = Join-Path $base "customers\$CustomerID.json"
    $signalPath   = Join-Path $base "signals"
    $schedPath    = Join-Path $base "schedules\$CustomerID-schedule.json"

    if (-not (Test-Path $profilePath)) {
        Write-Host "Customer not found: $CustomerID" -ForegroundColor Red
        return
    }

    $profile     = Get-Content $profilePath -Raw | ConvertFrom-Json
    $companyName = $profile.CompanyName
    $signals     = @()

    Get-ChildItem $signalPath -Filter "$CustomerID-*.json" |
        Sort-Object Name | ForEach-Object {
            $s = Get-Content $_.FullName -Raw | ConvertFrom-Json
            $signals += $s
        }

    if ($signals.Count -eq 0) {
        Write-Host "No signals found for: $CustomerID" -ForegroundColor Red
        return
    }

    # Load thresholds
    $t1 = 0.25; $t2 = 0.50; $t3 = 0.72
    if (Test-Path $schedPath) {
        $sched = Get-Content $schedPath -Raw | ConvertFrom-Json
        if ($sched.Action1Threshold) { $t1 = [double]$sched.Action1Threshold }
        if ($sched.Action2Threshold) { $t2 = [double]$sched.Action2Threshold }
        if ($sched.Action3Threshold) { $t3 = [double]$sched.Action3Threshold }
    }

    # ── Output path ───────────────────────────────────────────
    $outPath = Join-Path $env:USERPROFILE "VBAFCenter\briefings\$CustomerID-referencekort.pdf"
    $outDir  = Split-Path $outPath
    if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null }

    Write-Host ""
    Write-Host (" Generating reference card: {0}" -f $companyName) -ForegroundColor Cyan
    Write-Host (" Signals : {0}"                  -f $signals.Count) -ForegroundColor White

    # ── Build Python signal data ──────────────────────────────
    $sigLines = @()
    foreach ($s in $signals) {
        $name      = $s.SignalName -replace "'","\\'"
        $rawMin    = if ($null -ne $s.RawMin)    { $s.RawMin }    else { 0 }
        $rawMax    = if ($null -ne $s.RawMax)    { $s.RawMax }    else { 100 }
        $goodBelow = if ($null -ne $s.GoodBelow -and $s.GoodBelow -ge 0) { $s.GoodBelow } else { -1 }
        $badAbove  = if ($null -ne $s.BadAbove  -and $s.BadAbove  -ge 0) { $s.BadAbove  } else { -1 }
        $weight    = if ($null -ne $s.Weight    -and $s.Weight    -gt 0) { $s.Weight    } else { 3  }

        # Determine unit from signal name
        $unit = if ($name -like "*%*")   { "%" }
                elseif ($name -like "*DKK*") { "DKK" }
                elseif ($name -like "*kg*")  { "kg" }
                elseif ($name -like "*km*")  { "km" }
                else { "" }

        # Determine if inverted signal (high raw = bad)
        $inverted = ($goodBelow -ge 0 -and $badAbove -ge 0 -and $goodBelow -lt $badAbove) -eq $false
        if ($goodBelow -ge 0 -and $badAbove -ge 0) {
            $inverted = $goodBelow -gt $badAbove
        }

        # Build threshold labels
        if ($goodBelow -ge 0 -and $badAbove -ge 0) {
            if ($inverted) {
                # High is bad (Route Efficiency, ETA Accuracy etc)
                $greenVal  = "Under $goodBelow $unit".Trim()
                $yellowVal = "$goodBelow – $badAbove $unit".Trim()
                $redVal    = "Over $badAbove $unit".Trim()
                $note      = "Lav $unit = god præstation · Høj $unit = problem *"
            } else {
                # Low is bad (On-Time, Driver Performance etc)
                $greenVal  = "Over $goodBelow $unit".Trim()
                $yellowVal = "$badAbove – $goodBelow $unit".Trim()
                $redVal    = "Under $badAbove $unit".Trim()
                $note      = "Høj $unit = god præstation"
            }
        } else {
            # No thresholds — use normalised
            $greenVal  = "Under 0.40"
            $yellowVal = "0.40 – 0.75"
            $redVal    = "Over 0.75"
            $note      = "Baseret på normaliseret værdi (0-1)"
        }

        $greenVal  = $greenVal  -replace "'","\\'"
        $yellowVal = $yellowVal -replace "'","\\'"
        $redVal    = $redVal    -replace "'","\\'"
        $note      = $note      -replace "'","\\'"

        $sigLines += " ('$name', '$unit', '$greenVal', '$yellowVal', '$redVal', '$note', $weight),"
    }
    $sigData = $sigLines -join "`n"

    $actionT1 = $t1.ToString("F2")
    $actionT2 = $t2.ToString("F2")
    $actionT3 = $t3.ToString("F2")
    $outEsc   = $outPath -replace '\\','\\\\'
    $company  = $companyName -replace "'","\\'"
    $custID   = $CustomerID  -replace "'","\\'"

    # ── Python script ─────────────────────────────────────────
    $py = @"
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
import datetime

W, H = landscape(A4)

DARK = colors.HexColor('#2C2C2A')
CYAN = colors.HexColor('#0B7EA3')
GREEN = colors.HexColor('#1D9E75')
YELLOW = colors.HexColor('#EF9F27')
RED = colors.HexColor('#E24B4A')
LIGHT = colors.HexColor('#F4F4F0')
MID = colors.HexColor('#D3D1C7')
WHITE = colors.white
GL = colors.HexColor('#E1F5EE')
YL = colors.HexColor('#FEF3E2')
RL = colors.HexColor('#FDECEA')

signals = [
$sigData
]

company = '$company'
custID = '$custID'
t1 = $actionT1
t2 = $actionT2
t3 = $actionT3
out_path = '$outEsc'
today = datetime.date.today().strftime('%d. %B %Y')

c = canvas.Canvas(out_path, pagesize=landscape(A4))

# Background
c.setFillColor(LIGHT)
c.rect(0, 0, W, H, fill=1, stroke=0)

# Top bar
c.setFillColor(DARK)
c.rect(0, H-12*mm, W, 12*mm, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont('Helvetica-Bold', 12)
c.drawString(10*mm, H-8.5*mm, 'VBAF-Center — Signal Referencekort')
c.setFont('Helvetica-Bold', 11)
c.drawCentredString(W/2, H-8.5*mm, company)
c.setFont('Helvetica', 8)
c.drawRightString(W-10*mm, H-8.5*mm, today)

# Column setup
col_x = [10*mm, 58*mm, 102*mm, 153*mm, 204*mm]
col_w = [46*mm, 42*mm, 49*mm, 49*mm, 85*mm]

# Column headers
header_y = H - 21*mm
hdrs = ['Signal + forklaring', 'Enhed', 'GROEN OK', 'GUL Hold oeje', 'ROED Handle nu']
hcols = [DARK, DARK, GREEN, YELLOW, RED]
hfg = [WHITE, WHITE, WHITE, DARK, WHITE]

for i,(hdr,hc,fg) in enumerate(zip(hdrs,hcols,hfg)):
    c.setFillColor(hc)
    c.rect(col_x[i], header_y, col_w[i]-1*mm, 8*mm, fill=1, stroke=0)
    c.setFillColor(fg)
    c.setFont('Helvetica-Bold', 9)
    c.drawCentredString(col_x[i]+col_w[i]/2-0.5*mm, header_y+2.5*mm, hdr)

# Rows
n = len(signals)
avail_h = header_y - 20*mm
row_h = avail_h / n

for idx,(name,unit,gv,yv,rv,note,wt) in enumerate(signals):
    y = header_y - (idx+1)*row_h
    bg = WHITE if idx%2==0 else colors.HexColor('#EDEDEB')

    c.setFillColor(bg)
    c.rect(10*mm, y, W-20*mm, row_h, fill=1, stroke=0)
    c.setStrokeColor(MID)
    c.setLineWidth(0.3)
    c.line(10*mm, y, W-10*mm, y)

    # Signal name
    c.setFillColor(DARK)
    c.setFont('Helvetica-Bold', 8)
    c.drawString(col_x[0]+2*mm, y+row_h-4.5*mm, name)
    # Weight badge
    c.setFillColor(CYAN)
    c.roundRect(col_x[0]+38*mm, y+row_h-5.5*mm, 7*mm, 4.5*mm, 1.5, fill=1, stroke=0)
    c.setFillColor(WHITE)
    c.setFont('Helvetica-Bold', 6)
    c.drawCentredString(col_x[0]+41.5*mm, y+row_h-3*mm, f'W{wt}')
    # Note
    c.setFillColor(colors.HexColor('#666664'))
    c.setFont('Helvetica', 6.5)
    words = note.split()
    line1,line2='',''
    for w in words:
        test = (line1+' '+w).strip()
        if len(test) < 30: line1=test
        else: line2=(line2+' '+w).strip()
    c.drawString(col_x[0]+2*mm, y+row_h-9*mm, line1)
    if line2:
        c.drawString(col_x[0]+2*mm, y+row_h-13*mm, line2)

    # Unit
    c.setFillColor(DARK)
    c.setFont('Helvetica-Bold', 9)
    c.drawCentredString(col_x[1]+col_w[1]/2-0.5*mm, y+row_h/2-2*mm, unit)

    # Green
    pad=2*mm
    c.setFillColor(GL)
    c.roundRect(col_x[2]+pad, y+pad, col_w[2]-2*pad-1*mm, row_h-2*pad, 3, fill=1, stroke=0)
    c.setFillColor(colors.HexColor('#0A5C3A'))
    c.setFont('Helvetica-Bold', 9)
    c.drawCentredString(col_x[2]+col_w[2]/2-0.5*mm, y+row_h/2-2*mm, gv)

    # Yellow
    c.setFillColor(YL)
    c.roundRect(col_x[3]+pad, y+pad, col_w[3]-2*pad-1*mm, row_h-2*pad, 3, fill=1, stroke=0)
    c.setFillColor(colors.HexColor('#7A4F00'))
    c.setFont('Helvetica-Bold', 9)
    c.drawCentredString(col_x[3]+col_w[3]/2-0.5*mm, y+row_h/2-2*mm, yv)

    # Red
    c.setFillColor(RL)
    c.roundRect(col_x[4]+pad, y+pad, col_w[4]-2*pad-1*mm, row_h-2*pad, 3, fill=1, stroke=0)
    c.setFillColor(colors.HexColor('#7A0A0A'))
    c.setFont('Helvetica-Bold', 9)
    c.drawCentredString(col_x[4]+col_w[4]/2-0.5*mm, y+row_h/2-2*mm, rv)

# Action thresholds bar
act_y = 9*mm
act_labels = [
    (GREEN, WHITE, f'0 — Monitor (avg under {t1})'),
    (YELLOW, DARK, f'1 — Omfordel ({t1} – {t2})'),
    (colors.HexColor('#EF6B27'), WHITE, f'2 — Omdiriger ({t2} – {t3})'),
    (RED, WHITE, f'3 — Eskalee r (avg over {t3})'),
]
aw = (W-20*mm)/4
for i,(ac,fg,lbl) in enumerate(act_labels):
    ax = 10*mm + i*aw
    c.setFillColor(ac)
    c.rect(ax, act_y, aw-1*mm, 8*mm, fill=1, stroke=0)
    c.setFillColor(fg)
    c.setFont('Helvetica-Bold', 8)
    c.drawCentredString(ax+aw/2-0.5*mm, act_y+2.5*mm, lbl)

# Bottom bar
c.setFillColor(DARK)
c.rect(0, 0, W, 8.5*mm, fill=1, stroke=0)
c.setFillColor(colors.HexColor('#AAAAAA'))
c.setFont('Helvetica', 6.5)
c.drawString(10*mm, 2*mm, 'VBAF-Center v1.0.38 Roskilde, Danmark vbaf.dk')
c.drawCentredString(W/2, 2*mm, '* Omvendt signal: lav vaerdi = god praestaTion')
c.drawRightString(W-10*mm, 2*mm, 'Print Laminer Haeng paa vaeggen')

c.save()
print('OK')
"@


    # ── Run Python ────────────────────────────────────────────
    $tmpScript = [System.IO.Path]::GetTempFileName() + ".py"
    $py | Set-Content $tmpScript -Encoding UTF8
    $result = & $python $tmpScript 2>&1
    Remove-Item $tmpScript -Force -ErrorAction SilentlyContinue

    if ($result -eq 'OK') {
        Write-Host (" Saved : {0}" -f $outPath) -ForegroundColor Green
        Write-Host ""
        Write-Host " Print · Laminér · Hæng på væggen" -ForegroundColor Cyan
        Write-Host ""
        if ($OpenBrowser) { Start-Process $outPath }
    } else {
        Write-Host " Error generating PDF:" -ForegroundColor Red
        Write-Host $result -ForegroundColor Red
    }
}

# ── Load message ──────────────────────────────────────────────
Write-Host ""
Write-Host " +--------------------------------------------------+" -ForegroundColor Cyan
Write-Host " | VBAF-Center — Reference Card Generator |" -ForegroundColor Cyan
Write-Host " | Reads live signal config — always accurate |" -ForegroundColor Cyan
Write-Host " +--------------------------------------------------+" -ForegroundColor Cyan
Write-Host ""
Write-Host " Export-VBAFCenterReferenceCard -CustomerID 'X'" -ForegroundColor White
Write-Host " Export-VBAFCenterReferenceCard -CustomerID 'X' -OpenBrowser" -ForegroundColor White
Write-Host ""