Rossmann-Store-Sales / src /frontend.py
ymlin105's picture
debug: log results to browser console
062e752
"""
Frontend HTML template for the Rossmann Store Sales Predictor.
A clean, modern professional dashboard for making predictions.
"""
FRONTEND_HTML = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rossmann Sales Analytics | Decision Support</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--primary: #3b82f6;
--primary-dark: #2563eb;
--bg-sidebar: #1e293b;
--bg-input: #334155;
--bg-page: #f8fafc;
--text-light: #f1f5f9;
--text-dark: #0f172a;
--text-muted: #64748b;
--success: #10b981;
--danger: #ef4444;
--border: #e2e8f0;
--shadow-sm: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
* { margin:0; padding:0; box-sizing: border-box; }
body { background: var(--bg-page); color: var(--text-dark); font-family: 'Inter', sans-serif; height: 100vh; overflow: hidden; display: flex; }
/* Sidebar - Dark Professional */
.sidebar { width: 340px; background: var(--bg-sidebar); padding: 1.5rem; display: flex; flex-direction: column; overflow-y: auto; color: white; border-right: 1px solid rgba(255,255,255,0.1); }
.sidebar-header { margin-bottom: 2rem; }
.sidebar-header h1 { font-family: 'Outfit'; font-size: 1.25rem; background: linear-gradient(90deg, #60a5fa, #3b82f6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.param-section { background: rgba(255,255,255,0.05); padding: 1.25rem; border-radius: 16px; margin-bottom: 1.5rem; }
.section-header { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--primary); margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
.input-group { margin-bottom: 1rem; }
label { display: block; font-size: 0.75rem; color: #94a3b8; margin-bottom: 0.25rem; font-weight: 500; }
.input-styled { width: 100%; background: var(--bg-input); border: 1px solid rgba(255,255,255,0.05); color: white; padding: 0.6rem 0.75rem; border-radius: 8px; font-size: 0.9rem; transition: 0.2s; }
.input-styled:focus { outline: none; border-color: var(--primary); ring: 2px solid var(--primary); }
.btn-update { width: 100%; background: var(--primary); color: white; border: none; padding: 0.8rem; border-radius: 10px; font-weight: 700; cursor: pointer; transition: 0.2s; margin-top: 1rem; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); }
.btn-update:hover { background: var(--primary-dark); transform: translateY(-1px); }
/* Main Workspace */
.main { flex: 1; padding: 2rem; display: flex; flex-direction: column; gap: 1.5rem; overflow-y: auto; }
.header-bar { display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid var(--border); }
.breadcrumb { font-size: 0.85rem; color: var(--text-muted); }
/* Dashboard Grid */
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
.card { background: white; border-radius: 20px; padding: 1.5rem; box-shadow: var(--shadow-sm); border: 1px solid var(--border); display: flex; flex-direction: column; }
/* KPI Styling */
.kpi-title { font-size: 0.85rem; color: var(--text-muted); font-weight: 600; margin-bottom: 1rem; }
.kpi-value { font-size: 3.5rem; font-family: 'Outfit'; font-weight: 700; color: #1e293b; margin-bottom: 0.5rem; }
.pill { display: inline-flex; align-items: center; background: #dcfce7; color: #166534; padding: 0.35rem 0.85rem; border-radius: 99px; font-size: 0.75rem; font-weight: 700; }
.pill.blue { background: #dbeafe; color: #1e40af; }
/* Impact Drivers */
.driver-row { margin-bottom: 1rem; }
.driver-header { display: flex; justify-content: space-between; margin-bottom: 0.35rem; font-size: 0.8rem; font-weight: 600; }
.progress-bar-bg { height: 8px; background: #f1f5f9; border-radius: 4px; overflow: hidden; }
.progress-bar-fill { height: 100%; border-radius: 4px; transition: 1s cubic-bezier(0.18, 0.89, 0.32, 1.28); }
/* Chart Canvas */
.chart-container { flex: 1; min-height: 400px; position: relative; margin-top: 1rem; }
/* Hidden initially */
#dashboard-grid { display: none; }
.empty-state { height: 80vh; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--text-muted); text-align: center; }
#loading-overlay { display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.7); backdrop-filter: blur(4px); z-index: 1000; align-items: center; justify-content: center; color: white; }
</style>
</head>
<body>
<div id="loading-overlay">
<div style="text-align: center;">
<div style="width: 40px; height: 40px; border: 4px solid var(--primary); border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 1rem;"></div>
<p>Processing Revenue Models...</p>
</div>
</div>
<aside class="sidebar">
<div class="sidebar-header">
<h1>Rossmann Analytics</h1>
<p style="font-size: 0.7rem; color: #64748b; margin-top: 5px;">AI-POWERED REVENUE FORECASTING</p>
</div>
<form id="prediction-form">
<div class="param-section">
<div class="section-header"><span></span> Parameter Context</div>
<div class="input-group">
<label>Store Identifier</label>
<input type="number" id="store_id" class="input-styled" value="1" min="1" max="1115">
</div>
<div class="input-group">
<label>Forecast Start Date</label>
<input type="date" id="start_date" class="input-styled">
</div>
<div class="input-group">
<label>Prediction Horizon</label>
<select id="horizon" class="input-styled">
<option value="7" selected>7-Day Outlook</option>
<option value="14">14-Day Outlook</option>
<option value="30">30-Day Outlook</option>
</select>
</div>
</div>
<div class="param-section">
<div class="section-header"><span></span> Operational Shifts</div>
<div class="input-group">
<label>Promotion Active (Y/N)</label>
<select id="promo" class="input-styled">
<option value="1" selected>Active Sales Promo</option>
<option value="0">No Promotion</option>
</select>
</div>
<div class="input-group">
<label>State Holiday Context</label>
<select id="state_holiday" class="input-styled">
<option value="0" selected>Normal Operations</option>
<option value="a">Public Holiday</option>
<option value="b">Easter Period</option>
<option value="c">Christmas Period</option>
</select>
</div>
<div class="input-group">
<label>School Holiday Active</label>
<select id="school_holiday" class="input-styled">
<option value="0">No</option>
<option value="1">Yes</option>
</select>
</div>
</div>
<div class="param-section">
<div class="section-header"><span></span> Store Profile</div>
<div class="input-group">
<label>Competitor Distance (m)</label>
<input type="number" id="comp_dist" class="input-styled" value="1270">
</div>
<div class="input-group">
<label>Store Category</label>
<select id="store_type" class="input-styled">
<option value="a">Type A</option>
<option value="b">Type B</option>
<option value="c">Type C</option>
<option value="d">Type D</option>
</select>
</div>
<div class="input-group">
<label>Assortment Level</label>
<select id="assortment" class="input-styled">
<option value="a">Basic</option>
<option value="b">Extra</option>
<option value="c">Extended</option>
</select>
</div>
</div>
<button type="submit" class="btn-update">Update Prediction</button>
</form>
</aside>
<main class="main">
<div class="header-bar">
<div>
<span class="breadcrumb">Store Analytics / Forecast Dashboard</span>
<h2 style="font-family: Outfit; font-size: 1.5rem; margin-top: 5px;" id="page-title">Real-time Revenue Outlook</h2>
</div>
<div style="display: flex; gap: 1rem;">
<!-- Extra UI icons/buttons could go here -->
</div>
</div>
<div id="welcome-view" class="empty-state">
<div style="background: white; padding: 3rem; border-radius: 30px; border: 2px dashed #e2e8f0; max-width: 500px;">
<span style="font-size: 3rem;">🚀</span>
<h2 style="margin: 1rem 0;">Ready to Forecast</h2>
<p>Configure prediction parameters in the sidebar to generate a high-precision sales outlook based on current store constraints.</p>
</div>
</div>
<div id="dashboard-grid">
<div class="grid">
<!-- Sales KPI Card -->
<div class="card" style="grid-column: span 1.5;">
<span class="kpi-title">PROJECTED FIRST-DAY REVENUE</span>
<h2 class="kpi-value" id="kpi-sales">€0</h2>
<div>
<span class="pill blue" id="ci-pill">95% Confidence Interval: €0 - €0</span>
</div>
<div style="margin-top: auto; padding-top: 1rem; color: var(--text-muted); font-size: 0.8rem; border-top: 1px solid #f1f5f9;">
Confidence intervals calculated via historical model precision (RMSPE).
</div>
</div>
<!-- Drivers Card -->
<div class="card" style="grid-column: span 1.5;">
<span class="kpi-title">IMPACT DRIVERS (TOP FACTORS)</span>
<div id="drivers-container">
<!-- Populated by JS -->
</div>
</div>
<!-- Chart Card -->
<div class="card" style="grid-column: span 3;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<span class="kpi-title">SALES FORECAST TREND (WITH CONFIDENCE BAND)</span>
</div>
<div class="chart-container">
<canvas id="forecast-chart"></canvas>
</div>
</div>
</div>
</div>
</main>
<script>
// Set default date to today
document.getElementById('start_date').valueAsDate = new Date();
let forecastChart = null;
// Form handler
document.getElementById('prediction-form').addEventListener('submit', async (e) => {
e.preventDefault();
document.getElementById('loading-overlay').style.display = 'flex';
const payload = {
Store: parseInt(document.getElementById('store_id').value),
Date: document.getElementById('start_date').value,
Promo: parseInt(document.getElementById('promo').value),
StateHoliday: document.getElementById('state_holiday').value,
SchoolHoliday: parseInt(document.getElementById('school_holiday').value),
Assortment: document.getElementById('assortment').value,
StoreType: document.getElementById('store_type').value,
CompetitionDistance: parseInt(document.getElementById('comp_dist').value) || 0,
ForecastDays: parseInt(document.getElementById('horizon').value)
};
try {
const response = await fetch('/predict', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
console.log("Prediction Result:", data);
// Switch views
document.getElementById('welcome-view').style.display = 'none';
document.getElementById('dashboard-grid').style.display = 'block';
// Update KPI
document.getElementById('kpi-sales').textContent = '€' + data.PredictedSales.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2});
document.getElementById('ci-pill').textContent = `95% Confidence Interval: €${data.ConfidenceInterval[0].toLocaleString()} - €${data.ConfidenceInterval[1].toLocaleString()}`;
document.getElementById('page-title').textContent = `Store ${payload.Store} Outlook`;
// Update Drivers
const driversContainer = document.getElementById('drivers-container');
driversContainer.innerHTML = '';
data.Explanation.forEach(item => {
const absImpact = Math.abs(item.impact);
const color = item.impact >= 0 ? 'var(--primary)' : 'var(--danger)';
const row = document.createElement('div');
row.className = 'driver-row';
row.innerHTML = `
<div class="driver-header">
<span>${item.feature}</span>
<span style="color:${color}">${item.impact >= 0 ? '+' : ''}${item.impact.toFixed(1)}%</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" style="width: ${Math.min(absImpact * 1.5, 100)}%; background: ${color};"></div>
</div>
`;
driversContainer.appendChild(row);
});
// Update Chart
updateChart(data.Forecast);
} catch (err) {
console.error(err);
alert("Internal Engine Error: Could not generate prediction. Check console.");
} finally {
document.getElementById('loading-overlay').style.display = 'none';
}
});
function updateChart(forecast) {
const ctx = document.getElementById('forecast-chart').getContext('2d');
const labels = forecast.map(f => new Date(f.date).toLocaleDateString(undefined, {month: 'short', day: 'numeric'}));
const sales = forecast.map(f => f.sales);
const lbs = forecast.map(f => f.lb);
const ubs = forecast.map(f => f.ub);
if (forecastChart) forecastChart.destroy();
forecastChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Projection',
data: sales,
borderColor: '#3b82f6',
borderWidth: 4,
pointRadius: 4,
pointBackgroundColor: '#fff',
pointBorderColor: '#3b82f6',
pointBorderWidth: 2,
tension: 0.3,
fill: false,
zIndex: 10
},
{
label: 'Upper Bound',
data: ubs,
borderColor: 'transparent',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: '+1', // Fill to lower bound
pointRadius: 0,
tension: 0.3
},
{
label: 'Lower Bound',
data: lbs,
borderColor: 'transparent',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: false,
pointRadius: 0,
tension: 0.3
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#0f172a',
padding: 12,
cornerRadius: 8,
titleFont: { size: 14, weight: 'bold' },
bodyFont: { size: 14 },
callbacks: {
label: function(context) {
if (context.datasetIndex === 0) {
return 'Revenue: €' + context.raw.toLocaleString(undefined, {minimumFractionDigits: 2});
}
return null;
}
}
}
},
scales: {
x: { grid: { display: false } },
y: {
ticks: {
callback: value => '€' + value.toLocaleString()
},
grid: { color: '#f1f5f9' }
}
}
}
});
}
// CSS Animation
const styleSheet = document.createElement("style");
styleSheet.innerText = `@keyframes spin { to { transform: rotate(360deg); } }`;
document.head.appendChild(styleSheet);
</script>
</body>
</html>
"""