tom-l-pm-dashboard/dashboard.html
DJP a6db845054 Initial commit - PM dashboard with CSV and email versions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:01:21 -05:00

839 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project Margin Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #000000;
color: #333;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
color: #FFC407;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
font-weight: 800;
text-shadow: 2px 2px 4px rgba(255, 196, 7, 0.3);
}
.kpi-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.kpi-card {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 4px 6px rgba(255, 196, 7, 0.2);
transition: transform 0.2s;
}
.kpi-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(255, 196, 7, 0.4);
}
.kpi-label {
font-size: 0.9em;
color: #666;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
font-weight: 600;
}
.kpi-value {
font-size: 2.5em;
font-weight: 800;
color: #FFC407;
}
.kpi-card.danger .kpi-value {
color: #e53e3e;
}
.kpi-card.warning .kpi-value {
color: #ed8936;
}
.kpi-card.success .kpi-value {
color: #38a169;
}
.filters {
background: white;
border-radius: 12px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(255, 196, 7, 0.2);
}
.filters h2 {
margin-bottom: 20px;
color: #FFC407;
font-weight: 700;
}
.filter-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.filter-item {
display: flex;
flex-direction: column;
}
.filter-item label {
margin-bottom: 8px;
font-weight: 600;
color: #555;
}
.filter-item select,
.filter-item input {
padding: 10px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1em;
font-family: 'Montserrat', sans-serif;
transition: border-color 0.2s;
}
.filter-item select:focus,
.filter-item input:focus {
outline: none;
border-color: #FFC407;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 1px;
font-family: 'Montserrat', sans-serif;
}
.btn-primary {
background: #FFC407;
color: #000;
}
.btn-primary:hover {
background: #e6b006;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(255, 196, 7, 0.4);
}
.btn-secondary {
background: #e2e8f0;
color: #555;
margin-left: 10px;
}
.btn-secondary:hover {
background: #cbd5e0;
}
.charts-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.chart-card {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 4px 6px rgba(255, 196, 7, 0.2);
}
.chart-card h3 {
margin-bottom: 20px;
color: #FFC407;
font-weight: 700;
}
.table-container {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 4px 6px rgba(255, 196, 7, 0.2);
overflow-x: auto;
}
.table-container h2 {
margin-bottom: 20px;
color: #FFC407;
font-weight: 700;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9em;
}
thead {
background: #FFC407;
color: #000;
}
th {
padding: 15px;
text-align: left;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.85em;
cursor: pointer;
user-select: none;
}
th:hover {
background: #e6b006;
}
td {
padding: 12px 15px;
border-bottom: 1px solid #e2e8f0;
}
tbody tr:hover {
background: #fffbf0;
}
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 600;
}
.badge-danger {
background: #fed7d7;
color: #c53030;
}
.badge-warning {
background: #feebc8;
color: #c05621;
}
.badge-success {
background: #c6f6d5;
color: #22543d;
}
.margin-negative {
color: #e53e3e;
font-weight: 600;
}
.margin-positive {
color: #38a169;
font-weight: 600;
}
.no-data {
text-align: center;
padding: 40px;
color: #999;
font-size: 1.2em;
}
@media (max-width: 768px) {
h1 {
font-size: 1.8em;
}
.kpi-container {
grid-template-columns: 1fr;
}
.charts-container {
grid-template-columns: 1fr;
}
.filter-group {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>📊 Project Margin Dashboard</h1>
<div class="kpi-container" id="kpiContainer"></div>
<div class="filters">
<h2>🔍 Filters</h2>
<div class="filter-group">
<div class="filter-item">
<label for="alertTypeFilter">Alert Type</label>
<select id="alertTypeFilter">
<option value="all">All</option>
<option value="Overburning">Overburning</option>
<option value="Tracking Behind">Tracking Behind</option>
</select>
</div>
<div class="filter-item">
<label for="recipientFilter">Recipient</label>
<select id="recipientFilter">
<option value="all">All Recipients</option>
</select>
</div>
<div class="filter-item">
<label for="completionFilter">Completion Status</label>
<select id="completionFilter">
<option value="all">All</option>
<option value="complete">100% Complete</option>
<option value="incomplete">In Progress</option>
</select>
</div>
<div class="filter-item">
<label for="searchFilter">Search Projects</label>
<input type="text" id="searchFilter" placeholder="Search by project ID or name...">
</div>
</div>
<div>
<button class="btn btn-primary" onclick="applyFilters()">Apply Filters</button>
<button class="btn btn-secondary" onclick="resetFilters()">Reset</button>
</div>
</div>
<div class="charts-container">
<div class="chart-card">
<h3>Alert Type Distribution</h3>
<canvas id="alertTypeChart"></canvas>
</div>
<div class="chart-card">
<h3>Project Completion Status</h3>
<canvas id="completionChart"></canvas>
</div>
<div class="chart-card">
<h3>Top 10 Projects by Margin Gap</h3>
<canvas id="marginGapChart"></canvas>
</div>
<div class="chart-card">
<h3>Margin Performance Overview</h3>
<canvas id="marginOverviewChart"></canvas>
</div>
</div>
<div class="table-container">
<h2>📋 Project Details (<span id="projectCount">0</span> projects)</h2>
<table id="projectTable">
<thead>
<tr>
<th onclick="sortTable('project_id')">Project ID</th>
<th onclick="sortTable('project_name')">Project Name</th>
<th onclick="sortTable('recipient_name')">Recipient</th>
<th onclick="sortTable('alert_type')">Alert Type</th>
<th onclick="sortTable('margin_target')">Target %</th>
<th onclick="sortTable('margin_to_date')">To Date %</th>
<th onclick="sortTable('margin_at_completion')">At Completion %</th>
<th onclick="sortTable('percent_complete')">Complete %</th>
</tr>
</thead>
<tbody id="projectTableBody"></tbody>
</table>
</div>
</div>
<script>
let allData = [];
let filteredData = [];
let charts = {};
// Embedded CSV data to avoid CORS issues
const csvData = `source_file,email_date,recipient_name,alert_type,project_id,project_name,margin_target,margin_to_date,margin_at_completion,percent_complete
Project Margin Update - Overburning.eml,"Mon, 26 Jan 2026 07:06:44 +0000",Lauren Gray,Overburning,OMG2141622,Hellmann's Ranch Poland Adapt,36.02%,-77.59%,25.07%,25%
Project Margin Update - Overburning[12].eml,"Mon, 26 Jan 2026 07:02:54 +0000",Jodi Gordon,Overburning,OMG2140372,IOI_Baileys_Minis_Social_Content_H2,41.95%,-132.41%,-132.41%,50%
Project Margin Update - Overburning[19].eml,"Mon, 26 Jan 2026 07:10:03 +0000",Taskeen Motala,Overburning,OMG2097042,OMO - ZA - Back To School - Asset Creation,36.02%,-14.15%,-14.15%,99%
Project Margin Update - Overburning[21].eml,"Mon, 26 Jan 2026 07:06:57 +0000",Sibongile Sikhosana,Overburning,OMG2168810,IOI_Q3_F26_Smirnoff ICE on Draught Tap Logo and Wrap,41.95%,-204.14%,-204.14%,50%
Project Margin Update - Overburning[31].eml,"Mon, 26 Jan 2026 07:07:50 +0000",Nicole Terblanche,Overburning,OMG2207815,GB_POSM_OffTrade_Tanqueray_Festival_Blocker_P1-2,41.95%,-11.03%,-10.53%,100%
Project Margin Update - Overburning[32].eml,"Mon, 26 Jan 2026 07:04:43 +0000",Nicole Terblanche,Overburning,OMG2172359,GB_POSM_OffTrade_Smirnoff_Wholesale_CTU,41.95%,-32.33%,-32.24%,100%
Project Margin Update - Overburning[35].eml,"Mon, 26 Jan 2026 07:02:13 +0000",Jeremy Pinches,Overburning,OMG2085206,Project Thunderstruck Window Film Launch Campaign EXECUTION,39.80%,-5.97%,36.35%,22%
Project Margin Update - Overburning[39].eml,"Mon, 26 Jan 2026 07:04:19 +0000",Keshika Paken,Overburning,OMG2052502,Rajah 2026 Q1 & Q2 Digital Costs,36.02%,-23.74%,-23.74%,30%
Project Margin Update - Overburning[45].eml,"Mon, 26 Jan 2026 07:07:22 +0000",Adele du Prey,Overburning,OMG2119347,Magnum & Cornetto x McFlurry,36.02%,-141.11%,-94.92%,25%
Project Margin Update - Overburning[49].eml,"Mon, 26 Jan 2026 07:10:39 +0000",Arissa Lidia Eva,Overburning,OMG2201200,LUX - Replica Flashtalking Upload - ID - GMAL,36.02%,-66.50%,-66.50%,100%
Project Margin Update - Overburning[52].eml,"Mon, 26 Jan 2026 07:10:20 +0000",Andy Mynes,Overburning,OMG2102382,National Lottery Scratchcards Category 2026,42.00%,-93.77%,22.57%,50%
Project Margin Update - Overburning[55].eml,"Mon, 26 Jan 2026 07:05:49 +0000",Zeina Iskandarani,Overburning,OMG2167157,Dove Sugar Cookie - 6x Social assets creation + 1 teaser,36.02%,-14.08%,19.78%,60%
Project Margin Update - Overburning[57].eml,"Mon, 26 Jan 2026 07:05:32 +0000",Rafika Madjid,Overburning,OMG2201140,PEPSODENT KIDS - Pepsodent Kids 7OA Adaptation - ID - GMAL,36.02%,-28.13%,-28.13%,100%
Project Margin Update - Overburning[66].eml,"Mon, 26 Jan 2026 07:10:14 +0000",Matt Goodenday,Overburning,OMG2191433,Onvation - 2 Page Overview Flyer Update_TRANSLATION FEE,53.51%,-101.98%,-102.06%,100%
Project Margin Update - Overburning[6].eml,"Mon, 26 Jan 2026 07:06:08 +0000",Max Hooper,Overburning,OMG2126068,Unilever International - Freshness Toolkit POSM Adaptations for Africa and NALI,36.02%,-9.41%,-9.36%,100%
Project Margin Update - Overburning[72].eml,"Mon, 26 Jan 2026 07:01:58 +0000",Queeneth Brown,Overburning,OMG1905107,Final Mighty Dragon Costs,53.51%,-73.49%,16.90%,40%
Project Margin Update - Overburning[74].eml,"Mon, 26 Jan 2026 07:05:06 +0000",Sneha Singh,Overburning,OMG1919717,GBT_TheBar_F25_H2_Q4_TheBar_Cocktail_Recipe_Visual_Asset,41.95%,-51.42%,-51.42%,40%
Project Margin Update - Overburning[84].eml,"Mon, 26 Jan 2026 07:05:04 +0000",Hailey Chang,Overburning,OMG2210889,OOS - Global - Alpro MTG - TV Tag Rework,48.35%,-173.10%,30.81%,10%
Project Margin Update - Overburning[86].eml,"Mon, 26 Jan 2026 07:08:51 +0000",Caroline Richard Hutchison,Overburning,OMG2162025,Airwallex - EMEA - UK - New London offices videoshoot of opening event + employee interviews + office footage,42.00%,-36.07%,19.91%,30%
Project Margin Update - Overburning[92].eml,"Mon, 26 Jan 2026 07:03:43 +0000",Nicole Terblanche,Overburning,OMG2154115,GB_POSM_OffTrade_CM_Gordons_Tesco_1L_MU,41.95%,-99.99%,-100.00%,100%
Project Margin Update - Tracking Behind.eml,"Mon, 26 Jan 2026 07:12:45 +0000",Nora Kovalenkinaite,Tracking Behind,OMG2197268,Leadership Conference 2026 (OOS),40.99%,27.22%,27.22%,50%
Project Margin Update - Tracking Behind[13].eml,"Mon, 26 Jan 2026 07:02:34 +0000",Zeina Iskandarani,Tracking Behind,OMG2169721,"Knorr x Sofrat Panda Key Visual Creation, Logos creation & Hashtags",36.02%,17.17%,17.17%,100%
Project Margin Update - Tracking Behind[19].eml,"Mon, 26 Jan 2026 07:05:43 +0000",Chris Graham,Tracking Behind,OMG2114029,CX740 Explorer and Capri BEP Content,55.70%,24.68%,91.32%,75%
Project Margin Update - Tracking Behind[1].eml,"Mon, 26 Jan 2026 07:04:06 +0000",Jennifer Giordano,Tracking Behind,OMG1841632,2025 Marriott Organic Social - Ad hoc,58.95%,37.36%,37.36%,100%
Project Margin Update - Tracking Behind[26].eml,"Mon, 26 Jan 2026 07:07:07 +0000",Myrto Mitillou,Tracking Behind,OMG2207853,EMEA MS Heritage Content Batch 1,100.00%,57.49%,57.49%,100%
Project Margin Update - Tracking Behind[34].eml,"Mon, 26 Jan 2026 07:10:54 +0000",Nicole Terblanche,Tracking Behind,OMG2174332,GB_POSM_OffTrade_Smirnoff_Flavours_Wholesale_Wobbler,41.95%,12.24%,12.12%,100%
Project Margin Update - Tracking Behind[35].eml,"Mon, 26 Jan 2026 07:10:27 +0000",Emek Bayrak,Tracking Behind,OMG2159646,UFS Dec Digital Assets 25,36.02%,10.82%,10.82%,100%
Project Margin Update - Tracking Behind[37].eml,"Mon, 26 Jan 2026 07:10:10 +0000",Rick Bywater,Tracking Behind,OMG2156721,RED - C&M - Deposit Boost Scheme campaign assets,42.98%,14.25%,14.29%,100%
Project Margin Update - Tracking Behind[38].eml,"Mon, 26 Jan 2026 07:08:34 +0000",Zoe Garnett,Tracking Behind,OMG1948810,Fleet Range and DSE Content,55.70%,17.19%,17.18%,100%
Project Margin Update - Tracking Behind[40].eml,"Mon, 26 Jan 2026 07:00:10 +0000",Selcuk Seymen,Tracking Behind,OMG2122121,DOMESTOS - December 25 - Digital Asset - TR,36.02%,19.88%,19.88%,100%
Project Margin Update - Tracking Behind[55].eml,"Mon, 26 Jan 2026 07:09:25 +0000",Sibongile Sikhosana,Tracking Behind,OMG2122428,IOI_F26 _Q2_Dunnes Stores Cocktail Page Updates,41.95%,10.37%,10.39%,100%
Project Margin Update - Tracking Behind[58].eml,"Mon, 26 Jan 2026 07:03:28 +0000",Jacques Moolman,Tracking Behind,OMG2191040,GB_POSM_OnTrade_Multibrand_00_Banner_600x300,41.95%,13.09%,12.98%,100%
Project Margin Update - Tracking Behind[69].eml,"Mon, 26 Jan 2026 07:07:33 +0000",Jacques Moolman,Tracking Behind,OMG2152925,GB_POSM_OffTrade_Baileys_Easter_Tesco_P1_P2_MU,41.95%,29.15%,29.20%,100%
Project Margin Update - Tracking Behind[76].eml,"Mon, 26 Jan 2026 07:10:31 +0000",Nadya Chrisanti,Tracking Behind,OMG2152842,CITRA - Citra Asset Bank (PART1-PART4) by Mooia - ID - GMAL,36.02%,7.54%,7.54%,100%
Project Margin Update - Tracking Behind[79].eml,"Mon, 26 Jan 2026 07:04:17 +0000",Nicole Terblanche,Tracking Behind,OMG2202007,GB_POSM_OffTrade_Smirnoff_Crush_Nisa P5_POS_Assets,41.95%,17.98%,17.89%,100%
Project Margin Update - Tracking Behind[81].eml,"Mon, 26 Jan 2026 07:00:17 +0000",Kate Kaplan,Tracking Behind,OMG2087299,BAIS - TRESemmé Thena EURO,36.02%,1.14%,9.79%,60%
Project Margin Update - Tracking Behind[92].eml,"Mon, 26 Jan 2026 07:01:09 +0000",Neil Barton,Tracking Behind,OMG1932144,P703 Ranger 26.5MY content pack,55.70%,31.68%,34.72%,80%
Project Margin Update - Tracking Behind[98].eml,"Mon, 26 Jan 2026 07:12:29 +0000",Myrto Mitillou,Tracking Behind,OMG2196809,H-D.com - European Bike Week,100.00%,51.10%,51.10%,100%`;
// Load and parse CSV data
function loadData() {
try {
allData = parseCSV(csvData);
filteredData = [...allData];
initializeDashboard();
} catch (error) {
console.error('Error loading data:', error);
document.getElementById('projectTableBody').innerHTML =
'<tr><td colspan="8" class="no-data">Error loading data.</td></tr>';
}
}
function parseCSV(csv) {
const lines = csv.trim().split('\n');
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
const data = [];
for (let i = 1; i < lines.length; i++) {
const values = parseCSVLine(lines[i]);
if (values.length === headers.length) {
const row = {};
headers.forEach((header, index) => {
row[header] = values[index].trim().replace(/^"|"$/g, '');
});
data.push(row);
}
}
return data;
}
function parseCSVLine(line) {
const result = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
result.push(current);
current = '';
} else {
current += char;
}
}
result.push(current);
return result;
}
function initializeDashboard() {
populateRecipientFilter();
updateKPIs();
updateCharts();
updateTable();
}
function populateRecipientFilter() {
const recipients = [...new Set(allData.map(d => d.recipient_name))].sort();
const select = document.getElementById('recipientFilter');
recipients.forEach(recipient => {
const option = document.createElement('option');
option.value = recipient;
option.textContent = recipient;
select.appendChild(option);
});
}
function updateKPIs() {
const data = filteredData;
const totalProjects = data.length;
const overburning = data.filter(d => d.alert_type === 'Overburning').length;
const trackingBehind = data.filter(d => d.alert_type === 'Tracking Behind').length;
const avgCompletion = data.reduce((sum, d) => sum + parseFloat(d.percent_complete.replace('%', '')), 0) / totalProjects;
const criticalProjects = data.filter(d => {
const marginToDate = parseFloat(d.margin_to_date.replace('%', ''));
return marginToDate < -50;
}).length;
const kpiHTML = `
<div class="kpi-card">
<div class="kpi-label">Total Projects</div>
<div class="kpi-value">${totalProjects}</div>
</div>
<div class="kpi-card danger">
<div class="kpi-label">Overburning</div>
<div class="kpi-value">${overburning}</div>
</div>
<div class="kpi-card warning">
<div class="kpi-label">Tracking Behind</div>
<div class="kpi-value">${trackingBehind}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Avg Completion</div>
<div class="kpi-value">${avgCompletion.toFixed(1)}%</div>
</div>
<div class="kpi-card danger">
<div class="kpi-label">Critical (&lt;-50%)</div>
<div class="kpi-value">${criticalProjects}</div>
</div>
`;
document.getElementById('kpiContainer').innerHTML = kpiHTML;
}
function updateCharts() {
updateAlertTypeChart();
updateCompletionChart();
updateMarginGapChart();
updateMarginOverviewChart();
}
function updateAlertTypeChart() {
const ctx = document.getElementById('alertTypeChart');
const overburning = filteredData.filter(d => d.alert_type === 'Overburning').length;
const trackingBehind = filteredData.filter(d => d.alert_type === 'Tracking Behind').length;
if (charts.alertType) charts.alertType.destroy();
charts.alertType = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Overburning', 'Tracking Behind'],
datasets: [{
data: [overburning, trackingBehind],
backgroundColor: ['#e53e3e', '#ed8936'],
borderWidth: 0
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: {
font: {
family: 'Montserrat',
weight: '600'
}
}
}
}
}
});
}
function updateCompletionChart() {
const ctx = document.getElementById('completionChart');
const ranges = {
'0-25%': 0,
'26-50%': 0,
'51-75%': 0,
'76-99%': 0,
'100%': 0
};
filteredData.forEach(d => {
const completion = parseFloat(d.percent_complete.replace('%', ''));
if (completion <= 25) ranges['0-25%']++;
else if (completion <= 50) ranges['26-50%']++;
else if (completion <= 75) ranges['51-75%']++;
else if (completion < 100) ranges['76-99%']++;
else ranges['100%']++;
});
if (charts.completion) charts.completion.destroy();
charts.completion = new Chart(ctx, {
type: 'bar',
data: {
labels: Object.keys(ranges),
datasets: [{
label: 'Projects',
data: Object.values(ranges),
backgroundColor: '#FFC407',
borderRadius: 8
}]
},
options: {
responsive: true,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1,
font: {
family: 'Montserrat'
}
}
},
x: {
ticks: {
font: {
family: 'Montserrat'
}
}
}
}
}
});
}
function updateMarginGapChart() {
const ctx = document.getElementById('marginGapChart');
const sorted = [...filteredData].sort((a, b) => {
const gapA = parseFloat(a.margin_target.replace('%', '')) - parseFloat(a.margin_to_date.replace('%', ''));
const gapB = parseFloat(b.margin_target.replace('%', '')) - parseFloat(b.margin_to_date.replace('%', ''));
return gapB - gapA;
}).slice(0, 10);
const labels = sorted.map(d => d.project_id);
const gaps = sorted.map(d => {
const target = parseFloat(d.margin_target.replace('%', ''));
const toDate = parseFloat(d.margin_to_date.replace('%', ''));
return target - toDate;
});
if (charts.marginGap) charts.marginGap.destroy();
charts.marginGap = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Margin Gap (%)',
data: gaps,
backgroundColor: gaps.map(g => g > 50 ? '#e53e3e' : g > 20 ? '#ed8936' : '#f6ad55'),
borderRadius: 8
}]
},
options: {
indexAxis: 'y',
responsive: true,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
beginAtZero: true,
ticks: {
font: {
family: 'Montserrat'
}
}
},
y: {
ticks: {
font: {
family: 'Montserrat'
}
}
}
}
}
});
}
function updateMarginOverviewChart() {
const ctx = document.getElementById('marginOverviewChart');
const data = filteredData.slice(0, 15).map(d => ({
x: d.project_id,
target: parseFloat(d.margin_target.replace('%', '')),
toDate: parseFloat(d.margin_to_date.replace('%', ''))
}));
if (charts.marginOverview) charts.marginOverview.destroy();
charts.marginOverview = new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => d.x),
datasets: [
{
label: 'Target',
data: data.map(d => d.target),
borderColor: '#38a169',
backgroundColor: 'rgba(56, 161, 105, 0.1)',
tension: 0.4
},
{
label: 'To Date',
data: data.map(d => d.toDate),
borderColor: '#e53e3e',
backgroundColor: 'rgba(229, 62, 62, 0.1)',
tension: 0.4
}
]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: {
font: {
family: 'Montserrat',
weight: '600'
}
}
}
},
scales: {
y: {
beginAtZero: false,
ticks: {
font: {
family: 'Montserrat'
}
}
},
x: {
ticks: {
font: {
family: 'Montserrat'
}
}
}
}
}
});
}
function updateTable() {
const tbody = document.getElementById('projectTableBody');
document.getElementById('projectCount').textContent = filteredData.length;
if (filteredData.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="no-data">No projects match the current filters</td></tr>';
return;
}
const rows = filteredData.map(d => {
const marginToDate = parseFloat(d.margin_to_date.replace('%', ''));
const marginClass = marginToDate < 0 ? 'margin-negative' : 'margin-positive';
const alertBadge = d.alert_type === 'Overburning' ? 'badge-danger' : 'badge-warning';
return `
<tr>
<td><strong>${d.project_id}</strong></td>
<td>${d.project_name}</td>
<td>${d.recipient_name}</td>
<td><span class="badge ${alertBadge}">${d.alert_type}</span></td>
<td>${d.margin_target}</td>
<td class="${marginClass}">${d.margin_to_date}</td>
<td class="${marginClass}">${d.margin_at_completion}</td>
<td>${d.percent_complete}</td>
</tr>
`;
}).join('');
tbody.innerHTML = rows;
}
function applyFilters() {
const alertType = document.getElementById('alertTypeFilter').value;
const recipient = document.getElementById('recipientFilter').value;
const completion = document.getElementById('completionFilter').value;
const search = document.getElementById('searchFilter').value.toLowerCase();
filteredData = allData.filter(d => {
if (alertType !== 'all' && d.alert_type !== alertType) return false;
if (recipient !== 'all' && d.recipient_name !== recipient) return false;
if (completion === 'complete' && d.percent_complete !== '100%') return false;
if (completion === 'incomplete' && d.percent_complete === '100%') return false;
if (search && !d.project_id.toLowerCase().includes(search) && !d.project_name.toLowerCase().includes(search)) return false;
return true;
});
updateKPIs();
updateCharts();
updateTable();
}
function resetFilters() {
document.getElementById('alertTypeFilter').value = 'all';
document.getElementById('recipientFilter').value = 'all';
document.getElementById('completionFilter').value = 'all';
document.getElementById('searchFilter').value = '';
filteredData = [...allData];
updateKPIs();
updateCharts();
updateTable();
}
let sortDirection = {};
function sortTable(column) {
if (!sortDirection[column]) sortDirection[column] = 'asc';
else sortDirection[column] = sortDirection[column] === 'asc' ? 'desc' : 'asc';
filteredData.sort((a, b) => {
let valA = a[column];
let valB = b[column];
if (column.includes('margin') || column.includes('percent')) {
valA = parseFloat(valA.replace('%', ''));
valB = parseFloat(valB.replace('%', ''));
}
if (sortDirection[column] === 'asc') {
return valA > valB ? 1 : -1;
} else {
return valA < valB ? 1 : -1;
}
});
updateTable();
}
// Load data on page load
loadData();
</script>
</body>
</html>