tool/Automation/functions/Export-HtmlReport.ps1

function Export-HtmlReport {
    <#
    .SYNOPSIS
        Generate a self-contained interactive HTML report from scored results.

    .DESCRIPTION
        Produces a single HTML file with embedded CSS/JS (no external dependencies).
        Features: score bar chart, band distribution, sortable table, detail expansion,
        band/urgency filter, and search.

    .PARAMETER Results
        Array of flat result hashtables (same shape as opportunity-report.json .results).

    .PARAMETER Metadata
        Hashtable with profile, exported_at, source info.

    .PARAMETER OutputPath
        Directory to write opportunity-report.html into.

    .OUTPUTS
        String — path to the generated HTML file.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [array]$Results,

        [Parameter(Mandatory)]
        [hashtable]$Metadata,

        [Parameter(Mandatory)]
        [string]$OutputPath
    )

    $htmlPath = Join-Path $OutputPath 'opportunity-report.html'

    # Convert results to JSON for embedding
    $jsonData = $Results | ConvertTo-Json -Depth 5 -Compress

    # Band stats
    $bandCounts = @(0, 0, 0, 0, 0)
    foreach ($r in $Results) {
        $band = [int]$r.Band
        if ($band -ge 1 -and $band -le 5) { $bandCounts[$band - 1]++ }
    }

    $totalContacts = $Results.Count
    $avgScore = if ($totalContacts -gt 0) { [math]::Round(($Results | ForEach-Object { $_.'Final Score' } | Measure-Object -Average).Average, 1) } else { 0 }
    $maxScore = if ($totalContacts -gt 0) { ($Results | ForEach-Object { $_.'Final Score' } | Measure-Object -Maximum).Maximum } else { 0 }

    $html = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LeadForge Opportunity Report</title>
<style>
:root {
    --band1: #2ecc71; --band2: #3498db; --band3: #f39c12; --band4: #e74c3c; --band5: #95a5a6;
    --bg: #f8f9fa; --card: #ffffff; --text: #2c3e50; --text-muted: #6c757d;
    --border: #dee2e6; --hover: #e9ecef;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; padding: 1.5rem; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 1.8rem; margin-bottom: 0.25rem; }
.subtitle { color: var(--text-muted); font-size: 0.9rem; margin-bottom: 1.5rem; }

/* Summary Cards */
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
.card { background: var(--card); border-radius: 8px; padding: 1.25rem; box-shadow: 0 1px 3px rgba(0,0,0,0.08); text-align: center; }
.card .value { font-size: 2rem; font-weight: 700; }
.card .label { font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }

/* Band pills */
.bands { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
.band-pill { padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.85rem; font-weight: 600; color: #fff; cursor: pointer; opacity: 0.85; transition: opacity 0.2s, transform 0.2s; }
.band-pill:hover, .band-pill.active { opacity: 1; transform: scale(1.05); }
.band-pill.b1 { background: var(--band1); } .band-pill.b2 { background: var(--band2); }
.band-pill.b3 { background: var(--band3); } .band-pill.b4 { background: var(--band4); }
.band-pill.b5 { background: var(--band5); }

/* Charts */
.charts-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem; }
.chart-section { background: var(--card); border-radius: 8px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.chart-section h2 { font-size: 1rem; margin-bottom: 1rem; }

/* Histogram (left) */
.histogram { display: flex; align-items: flex-end; gap: 2px; height: 140px; }
.hist-bar-wrap { flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; justify-content: flex-end; }
.hist-bar { width: 100%; border-radius: 3px 3px 0 0; min-height: 0; transition: height 0.3s; position: relative; }
.hist-bar:hover { opacity: 0.85; }
.hist-count { font-size: 0.7rem; font-weight: 600; margin-bottom: 2px; color: var(--text); }
.hist-label { font-size: 0.7rem; color: var(--text-muted); margin-top: 4px; text-align: center; }

/* Top-N horizontal bars (right) */
.hbar-chart { display: flex; flex-direction: column; gap: 6px; }
.hbar-row { display: flex; align-items: center; gap: 8px; }
.hbar-name { width: 130px; font-size: 0.8rem; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 0; }
.hbar-track { flex: 1; height: 22px; background: #ecf0f1; border-radius: 4px; overflow: hidden; position: relative; }
.hbar-fill { height: 100%; border-radius: 4px; transition: width 0.4s; display: flex; align-items: center; padding-left: 6px; }
.hbar-score { font-size: 0.75rem; font-weight: 600; color: #fff; text-shadow: 0 1px 1px rgba(0,0,0,0.3); }
.hbar-org { font-size: 0.7rem; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 120px; flex-shrink: 0; }
.hbar-more { font-size: 0.8rem; color: var(--text-muted); margin-top: 4px; text-align: center; }

@media (max-width: 768px) {
    .charts-row { grid-template-columns: 1fr; }
    .hbar-name { width: 90px; }
    .hbar-org { display: none; }
}

/* Controls */
.controls { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; align-items: center; }
.search-input { padding: 0.5rem 1rem; border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem; width: 250px; }
.search-input:focus { outline: none; border-color: var(--band2); box-shadow: 0 0 0 2px rgba(52,152,219,0.2); }

/* Table */
.table-wrap { background: var(--card); border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
thead { background: #2c3e50; color: #fff; position: sticky; top: 0; }
th { padding: 0.75rem 0.5rem; text-align: left; cursor: pointer; user-select: none; white-space: nowrap; }
th:hover { background: #34495e; }
th .sort-arrow { margin-left: 4px; opacity: 0.5; }
th.sorted .sort-arrow { opacity: 1; }
td { padding: 0.6rem 0.5rem; border-bottom: 1px solid var(--border); vertical-align: top; }
tr:hover { background: var(--hover); }
tr.band-1 td:first-child { border-left: 3px solid var(--band1); }
tr.band-2 td:first-child { border-left: 3px solid var(--band2); }
tr.band-3 td:first-child { border-left: 3px solid var(--band3); }
tr.band-4 td:first-child { border-left: 3px solid var(--band4); }
tr.band-5 td:first-child { border-left: 3px solid var(--band5); }

/* Detail row */
.detail-row td { padding: 0; border: none; background: #f0f4f8; }
.detail-content { padding: 1rem 1.5rem; display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; font-size: 0.85rem; }
.detail-content h4 { grid-column: 1 / -1; margin-bottom: 0.25rem; font-size: 0.9rem; }
.detail-content .field { display: flex; flex-direction: column; }
.detail-content .field-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; }
.detail-content .field-value { font-weight: 500; }
.score-breakdown { display: flex; gap: 0.5rem; flex-wrap: wrap; grid-column: 1 / -1; }
.score-chip { padding: 0.25rem 0.6rem; border-radius: 4px; font-size: 0.75rem; background: #e8edf2; }

.hidden { display: none; }
@media (max-width: 768px) {
    .summary { grid-template-columns: repeat(2, 1fr); }
    .controls { flex-direction: column; align-items: stretch; }
    .search-input { width: 100%; }
    .detail-content { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="container">
    <h1>LeadForge Opportunity Report</h1>
    <div class="subtitle">Generated $(Get-Date -Format 'yyyy-MM-dd HH:mm') &bull; Profile: $($Metadata.profile ?? 'Default') &bull; $totalContacts contacts scored</div>

    <!-- Summary Cards -->
    <div class="summary">
        <div class="card"><div class="value">$totalContacts</div><div class="label">Contacts</div></div>
        <div class="card"><div class="value">$maxScore</div><div class="label">Top Score</div></div>
        <div class="card"><div class="value">$avgScore</div><div class="label">Avg Score</div></div>
        <div class="card"><div class="value">$($bandCounts[0] + $bandCounts[1] + $bandCounts[2])</div><div class="label">Actionable (B1-3)</div></div>
    </div>

    <!-- Band Filter -->
    <div class="bands">
        <span class="band-pill b1 active" data-band="1" onclick="toggleBand(1)">Band 1: Immediate ($($bandCounts[0]))</span>
        <span class="band-pill b2 active" data-band="2" onclick="toggleBand(2)">Band 2: High ($($bandCounts[1]))</span>
        <span class="band-pill b3 active" data-band="3" onclick="toggleBand(3)">Band 3: Medium ($($bandCounts[2]))</span>
        <span class="band-pill b4 active" data-band="4" onclick="toggleBand(4)">Band 4: Low ($($bandCounts[3]))</span>
        <span class="band-pill b5 active" data-band="5" onclick="toggleBand(5)">Band 5: Archive ($($bandCounts[4]))</span>
    </div>

    <!-- Charts -->
    <div class="charts-row">
        <div class="chart-section">
            <h2>Score Distribution</h2>
            <div class="histogram" id="histogram"></div>
        </div>
        <div class="chart-section">
            <h2>Top Opportunities</h2>
            <div class="hbar-chart" id="topChart"></div>
        </div>
    </div>

    <!-- Controls -->
    <div class="controls">
        <input type="text" class="search-input" id="searchInput" placeholder="Search name, company, email..." oninput="filterTable()">
    </div>

    <!-- Table -->
    <div class="table-wrap">
        <table id="reportTable">
            <thead>
                <tr>
                    <th onclick="sortTable(0)">Score <span class="sort-arrow">&#9650;</span></th>
                    <th onclick="sortTable(1)">Band <span class="sort-arrow">&#9650;</span></th>
                    <th onclick="sortTable(2)">Name <span class="sort-arrow">&#9650;</span></th>
                    <th onclick="sortTable(3)">Organisation <span class="sort-arrow">&#9650;</span></th>
                    <th onclick="sortTable(4)">Category <span class="sort-arrow">&#9650;</span></th>
                    <th onclick="sortTable(5)">Confidence <span class="sort-arrow">&#9650;</span></th>
                    <th onclick="sortTable(6)">Action <span class="sort-arrow">&#9650;</span></th>
                </tr>
            </thead>
            <tbody id="tableBody"></tbody>
        </table>
    </div>
</div>

<script>
const DATA = $jsonData;

const bandColors = {1:'#2ecc71',2:'#3498db',3:'#f39c12',4:'#e74c3c',5:'#95a5a6'};
const bandNames = {1:'Immediate',2:'High',3:'Medium',4:'Low',5:'Archive'};
let activeBands = new Set([1,2,3,4,5]);
let sortCol = -1, sortAsc = false;
let expandedRow = null;

function toggleBand(b) {
    const pill = document.querySelector('.band-pill[data-band="'+b+'"]');
    if (activeBands.has(b)) { activeBands.delete(b); pill.classList.remove('active'); pill.style.opacity='0.4'; }
    else { activeBands.add(b); pill.classList.add('active'); pill.style.opacity=''; }
    renderTable();
}

function getFiltered() {
    const q = (document.getElementById('searchInput').value||'').toLowerCase();
    return DATA.filter(r => {
        if (!activeBands.has(r.Band)) return false;
        if (!q) return true;
        return [r['Contact Name'],r.Organisation,r['Contact Email'],r.Category,r['Recommended Action']]
            .some(v => v && v.toLowerCase().includes(q));
    });
}

function renderChart() {
    // --- Left: Score histogram (10 buckets) ---
    const hist = document.getElementById('histogram');
    const bucketSize = 10;
    const buckets = Array.from({length:10}, ()=>({count:0, bands:{}}));
    DATA.forEach(r => {
        const idx = Math.min(9, Math.floor(r['Final Score'] / bucketSize));
        buckets[idx].count++;
        buckets[idx].bands[r.Band] = (buckets[idx].bands[r.Band]||0) + 1;
    });
    const maxCount = Math.max(...buckets.map(b=>b.count), 1);
    hist.innerHTML = buckets.map((b,i) => {
        const h = b.count > 0 ? Math.max(8, (b.count/maxCount)*100) : 0;
        // Determine dominant band colour for this bucket
        let dominantBand = 5;
        let maxBandCount = 0;
        for (const [band, cnt] of Object.entries(b.bands)) {
            if (cnt > maxBandCount) { maxBandCount = cnt; dominantBand = parseInt(band); }
        }
        const color = bandColors[dominantBand]||'#ccc';
        const label = (i*bucketSize)+'-'+(i*bucketSize+bucketSize-1);
        return '<div class="hist-bar-wrap">'
            +(b.count>0?'<div class="hist-count">'+b.count+'</div>':'')
            +'<div class="hist-bar" style="height:'+h+'%;background:'+color+'" title="'+label+': '+b.count+' contacts"></div>'
            +'<div class="hist-label">'+label+'</div></div>';
    }).join('');

    // --- Right: Top 10 horizontal bars ---
    const topChart = document.getElementById('topChart');
    const top10 = [...DATA].sort((a,b) => b['Final Score'] - a['Final Score']).slice(0, 10);
    const topMax = Math.max(...top10.map(r=>r['Final Score']), 1);
    const remaining = DATA.length - top10.length;
    topChart.innerHTML = top10.map(r => {
        const pct = Math.max(5, (r['Final Score']/topMax)*100);
        const color = bandColors[r.Band]||'#ccc';
        return '<div class="hbar-row">'
            +'<div class="hbar-name" title="'+esc(r['Contact Name'])+'">'+esc(r['Contact Name'])+'</div>'
            +'<div class="hbar-track"><div class="hbar-fill" style="width:'+pct+'%;background:'+color+'"><span class="hbar-score">'+r['Final Score']+'</span></div></div>'
            +'<div class="hbar-org" title="'+esc(r.Organisation)+'">'+esc(r.Organisation)+'</div>'
            +'</div>';
    }).join('') + (remaining > 0 ? '<div class="hbar-more">+ '+remaining+' more in table below</div>' : '');
}

function renderTable() {
    const tbody = document.getElementById('tableBody');
    let rows = getFiltered();
    rows.sort((a,b) => {
        const keys = ['Final Score','Band','Contact Name','Organisation','Category','Research Confidence','Recommended Action'];
        let av = a[keys[sortCol]], bv = b[keys[sortCol]];
        if (typeof av === 'number') return sortAsc ? av-bv : bv-av;
        av = (av||'').toString().toLowerCase(); bv = (bv||'').toString().toLowerCase();
        return sortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
    });
    tbody.innerHTML = rows.map((r,i) => {
        const action = (r['Recommended Action']||'').substring(0,80) + ((r['Recommended Action']||'').length>80?'...':'');
        return '<tr class="data-row band-'+r.Band+'" onclick="toggleDetail(this,'+i+')" data-idx="'+i+'">'
            +'<td><strong>'+r['Final Score']+'</strong></td>'
            +'<td><span style="color:'+bandColors[r.Band]+'">&#9679;</span> '+bandNames[r.Band]+'</td>'
            +'<td>'+esc(r['Contact Name'])+'</td>'
            +'<td>'+esc(r.Organisation)+'</td>'
            +'<td>'+esc(r.Category)+'</td>'
            +'<td>'+r['Research Confidence']+'/5</td>'
            +'<td>'+esc(action)+'</td></tr>';
    }).join('');
    expandedRow = null;
}

function toggleDetail(tr, idx) {
    const rows = getFiltered();
    rows.sort((a,b) => {
        const keys = ['Final Score','Band','Contact Name','Organisation','Category','Research Confidence','Recommended Action'];
        let av = a[keys[sortCol]], bv = b[keys[sortCol]];
        if (typeof av === 'number') return sortAsc ? av-bv : bv-av;
        av=(av||'').toString().toLowerCase(); bv=(bv||'').toString().toLowerCase();
        return sortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
    });
    const r = rows[idx];
    const existing = tr.nextElementSibling;
    if (existing && existing.classList.contains('detail-row')) { existing.remove(); expandedRow=null; return; }
    // Remove any other expanded
    document.querySelectorAll('.detail-row').forEach(el=>el.remove());
    const detail = document.createElement('tr');
    detail.className = 'detail-row';
    detail.innerHTML = '<td colspan="7"><div class="detail-content">'
        +'<h4>'+esc(r['Contact Name'])+' — '+esc(r.Organisation)+'</h4>'
        +'<div class="field"><span class="field-label">Email</span><span class="field-value">'+esc(r['Contact Email'])+'</span></div>'
        +'<div class="field"><span class="field-label">Source File</span><span class="field-value">'+esc(r['Source File'])+'</span></div>'
        +'<div class="score-breakdown">'
        +'<span class="score-chip">Fit: '+r['Strategic Fit']+'/5</span>'
        +'<span class="score-chip">Seniority: '+r.Seniority+'/5</span>'
        +'<span class="score-chip">Warmth: '+r['Engagement Warmth']+'/5</span>'
        +'<span class="score-chip">Market: '+r['Market Activity']+'/5</span>'
        +'<span class="score-chip">Stage: '+r['Conversation Stage']+'/5</span>'
        +'<span class="score-chip">Recency: '+r.Recency+'/5</span>'
        +'<span class="score-chip">Confidence: '+r['Research Confidence']+'/5</span>'
        +'<span class="score-chip" style="background:#d4efdf;font-weight:600">Composite: '+r['Composite Score']+'</span>'
        +'<span class="score-chip" style="background:#fadbd8;font-weight:600">Penalty: -'+r['Recency Penalty']+'</span>'
        +'</div>'
        +'<div class="field" style="grid-column:1/-1"><span class="field-label">Recommended Action</span><span class="field-value">'+esc(r['Recommended Action'])+'</span></div>'
        +'<div class="field" style="grid-column:1/-1"><span class="field-label">Rationale</span><span class="field-value">'+esc(r['Action Rationale'])+'</span></div>'
        +'</div></td>';
    tr.after(detail);
    expandedRow = idx;
}

function sortTable(col) {
    if (sortCol === col) sortAsc = !sortAsc;
    else { sortCol = col; sortAsc = col >= 2; }
    document.querySelectorAll('th').forEach(th=>th.classList.remove('sorted'));
    document.querySelectorAll('th')[col].classList.add('sorted');
    renderTable();
}

function filterTable() { renderTable(); }
function esc(s) { if (!s) return ''; const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }

// Init
renderChart();
sortTable(0);
</script>
</body>
</html>
"@


    $html | Set-Content -Path $htmlPath -Encoding UTF8
    return $htmlPath
}