ymlin105 commited on
Commit
082e950
·
1 Parent(s): e48acb8

Refactor: Redesign UI to Professional SaaS Style

Browse files
Files changed (1) hide show
  1. src/frontend.py +370 -358
src/frontend.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
  Frontend HTML template for the Rossmann Store Sales Predictor.
3
- A clean, modern single-page interface for making predictions.
4
  """
5
 
6
  FRONTEND_HTML = """
@@ -9,384 +9,362 @@ FRONTEND_HTML = """
9
  <head>
10
  <meta charset="UTF-8">
11
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
- <title>Rossmann Sales Predictor</title>
13
- <link href="https://fonts.googleapis.com/css2?family=Patrick+Hand&display=swap" rel="stylesheet">
14
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
15
  <style>
16
  :root {
17
- /* Hand-Drawn / Sketchy Theme */
18
- --primary: #333; /* Pencil Overlay */
19
- --accent: #d93025; /* Red Marker for Rossmann */
20
- --paper: #fffdf5; /* Warm Paper */
21
- --ink: #1a1a1a;
22
- --border-ink: #2c2c2c;
23
- --highlight: #fef3c7; /* Yellow Highlighter */
24
- --shadow-ink: rgba(0,0,0,0.15);
 
 
 
25
  }
26
 
27
  * {
28
  margin: 0;
29
  padding: 0;
30
  box-sizing: border-box;
31
- font-family: 'Patrick Hand', cursive, sans-serif;
32
  }
33
 
34
  body {
35
- background-color: #f0f0f0;
36
- background-image: radial-gradient(#d1d1d1 1px, transparent 1px);
37
- background-size: 20px 20px;
38
- color: var(--ink);
39
  min-height: 100vh;
40
  display: flex;
 
 
 
 
 
 
 
 
 
 
41
  align-items: center;
42
- justify-content: center;
43
- padding: 2rem;
44
- font-size: 1.1rem;
45
  }
46
 
47
- .container {
48
- width: 100%;
49
- max-width: 1100px;
 
50
  display: flex;
51
- justify-content: center;
 
52
  }
 
 
53
 
54
- /* The Main "Sheet of Paper" */
55
- .card {
56
- background: var(--paper);
57
- /* Sketchy Border */
58
- border: 2px solid var(--border-ink);
59
- border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
60
- box-shadow: 5px 8px 15px var(--shadow-ink);
61
- padding: 3rem;
62
- width: 100%;
63
- position: relative;
64
  }
65
 
66
- .header {
67
- margin-bottom: 2.5rem;
68
- text-align: left;
69
- border-bottom: 2px dashed #ddd;
70
- padding-bottom: 1rem;
71
  }
72
 
73
- .header h1 {
74
- font-size: 2.2rem;
75
- font-weight: 700;
76
- color: var(--ink);
77
- letter-spacing: 1px;
78
- text-transform: uppercase;
 
 
 
 
79
  }
80
 
81
- .header h1 span {
82
- color: var(--accent);
 
 
83
  }
84
 
85
- .header p {
86
- color: #666;
87
- font-size: 1.2rem;
88
- margin-top: 0.5rem;
 
 
 
 
89
  }
90
 
91
- .badge {
92
- display: inline-block;
93
- padding: 0.25rem 1rem;
94
- border: 2px solid var(--border-ink);
95
- border-radius: 15px 255px 15px 255px / 255px 15px 225px 15px;
96
- background: #e0f2fe;
97
- color: #0369a1;
98
- font-weight: bold;
99
- margin-top: 1rem;
100
- transform: rotate(-2deg);
101
- box-shadow: 2px 2px 0px rgba(0,0,0,0.1);
102
  }
103
-
104
- /* Layout */
105
- .content {
106
- display: grid;
107
- grid-template-columns: 1.2fr 0.8fr;
108
- gap: 4rem;
109
- align-items: start;
110
  }
111
-
112
- @media (max-width: 850px) {
113
- .content {
114
- grid-template-columns: 1fr;
115
- gap: 2rem;
116
- }
117
  }
118
 
 
119
  .form-grid {
120
  display: grid;
121
- grid-template-columns: 1fr 1fr;
122
- column-gap: 2rem;
123
- row-gap: 1.5rem;
124
  }
125
 
126
  .form-group {
127
  display: flex;
128
  flex-direction: column;
129
- gap: 0.5rem;
130
  }
131
 
132
  .form-group label {
133
- font-size: 1.1rem;
134
- font-weight: bold;
 
135
  }
136
 
137
- .form-group .hint {
138
- font-family: sans-serif;
139
- font-size: 0.75rem;
140
- color: #777;
141
- text-transform: uppercase;
142
- letter-spacing: 0.5px;
143
- }
144
-
145
- .form-group input, .form-group select {
146
- padding: 0.75rem;
147
- background: transparent;
148
- border: none;
149
- border-bottom: 3px solid #ccc;
150
- font-size: 1.3rem;
151
- color: var(--accent);
152
- transition: all 0.2s;
153
- border-radius: 0;
154
- width: 100%;
155
  }
156
 
157
- .form-group input:focus, .form-group select:focus {
158
  outline: none;
159
- border-bottom-color: var(--accent);
160
- background: rgba(217, 48, 37, 0.05); /* faint red highlight */
161
- }
162
-
163
- /* Checkbox styling */
164
- .form-check {
165
- flex-direction: row;
166
- align-items: center;
167
- gap: 1rem;
168
- margin-top: 1rem;
169
- }
170
-
171
- .form-check input {
172
- width: auto;
173
- transform: scale(1.5);
174
- accent-color: var(--accent);
175
  }
176
 
177
- /* Sketchy Button */
178
- .btn {
179
- width: 100%;
180
- padding: 1rem;
181
- background: var(--ink);
182
  color: white;
183
- border: 2px solid var(--ink);
184
- /* Sketchy squircle */
185
- border-radius: 255px 15px 225px 15px / 15px 225px 15px 255px;
186
- font-size: 1.4rem;
187
- font-weight: bold;
188
  cursor: pointer;
189
- margin-top: 2.5rem;
190
- transition: transform 0.1s;
191
- box-shadow: 3px 4px 0px #888;
 
192
  }
193
 
194
- .btn:hover {
195
- transform: scale(1.02) rotate(-1deg);
196
- box-shadow: 4px 6px 0px #666;
197
  }
198
-
199
- .btn:active {
200
- transform: scale(0.98);
201
- box-shadow: 1px 1px 0px #888;
202
  }
203
 
204
- .btn:disabled {
205
- background: #999;
206
- border-color: #999;
207
  cursor: not-allowed;
208
  transform: none;
209
- box-shadow: none;
210
  }
211
 
212
- /* Result Panel: Sticky Note / Post-it Style */
213
- .result {
214
- margin-top: 1rem;
215
- padding: 2rem;
216
- background: #ffeb3b;
217
- background: linear-gradient(135deg, #fff9c4 0%, #fff176 100%);
218
- border: 1px solid #eab308;
219
- box-shadow: 5px 5px 10px rgba(0,0,0,0.2);
220
- transform: rotate(1deg);
221
- position: relative;
222
- min-height: 200px;
223
  display: flex;
224
  flex-direction: column;
225
- justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
226
  }
227
 
228
- .result::before {
229
  content: '';
230
  position: absolute;
231
- top: -15px;
232
- left: 50%;
233
- transform: translateX(-50%);
234
- width: 15px;
235
- height: 15px;
236
- background: var(--accent);
237
- border-radius: 50%;
238
- box-shadow: 1px 2px 3px rgba(0,0,0,0.3);
239
- z-index: 10;
240
  }
241
 
242
- .result-placeholder {
243
- text-align: center;
244
- opacity: 0.6;
245
- font-size: 1.2rem;
246
- }
247
-
248
- .result .label {
249
- font-size: 1rem;
250
- font-weight: bold;
251
- color: #854d0e;
252
  text-transform: uppercase;
253
- letter-spacing: 1px;
 
 
254
  margin-bottom: 0.5rem;
255
  }
256
 
257
- .result .price {
258
- font-size: 3.5rem;
259
- font-weight: 800;
260
- color: #1a1a1a;
261
- margin: 0.5rem 0;
262
- text-shadow: 2px 2px 0px rgba(255,255,255,0.5);
263
  }
264
 
265
- .result .meta {
266
- font-size: 0.9rem;
267
- color: #854d0e;
268
- border-top: 2px dashed #ca8a04;
269
- display: inline-block;
270
- padding-top: 0.5rem;
271
- margin-top: 1rem;
272
- width: 100%;
273
  }
274
-
275
- .chart-container {
276
- margin-top: 1rem;
277
- max-height: 200px;
278
- width: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  }
280
 
281
  /* Footer */
282
  .footer {
283
- margin-top: 3rem;
284
  text-align: center;
285
- font-size: 0.9rem;
286
- color: #666;
 
 
 
 
 
 
 
 
 
287
  }
288
 
289
- .footer a { color: var(--accent); text-decoration: none; border-bottom: 1px dashed var(--accent); }
290
 
291
- .tech-stack {
292
- display: flex;
293
- justify-content: center;
294
- gap: 1rem;
295
- margin-top: 1rem;
296
- flex-wrap: wrap;
297
  }
298
 
299
- .tech-badge {
300
- background: #fff;
301
- border: 1px solid #999;
302
- padding: 0.2rem 0.6rem;
303
- border-radius: 20px;
304
- font-size: 0.8rem;
305
- transform: rotate(var(--rot, 0deg));
306
- }
307
- .tech-badge:nth-child(odd) { transform: rotate(-2deg); }
308
- .tech-badge:nth-child(even) { transform: rotate(3deg); }
309
  </style>
310
  </head>
311
  <body>
 
 
 
 
 
 
 
 
 
 
312
  <div class="container">
313
- <div class="card">
314
- <div class="header">
315
- <h1><span>Rossmann</span> Sales Predictor</h1>
316
- <p>Forecast daily turnover for any store instantly.</p>
317
- <span class="badge" id="mode-badge">Loading...</span>
318
- </div>
319
-
320
- <div class="content">
321
  <form id="predict-form">
322
  <div class="form-grid">
323
  <div class="form-group">
324
  <label for="store">Store ID</label>
325
- <span class="hint">1 to 1115</span>
326
- <input type="number" id="store" name="store"
327
- value="1" min="1" max="1115" required>
328
  </div>
329
-
330
  <div class="form-group">
331
  <label for="date">Start Date</label>
332
- <span class="hint">Prediction Target</span>
333
- <input type="date" id="date" name="date" required>
334
  </div>
335
-
336
  <div class="form-group">
337
- <label for="horizon">Horizon</label>
338
- <span class="hint">Days to Predict</span>
339
- <select id="horizon" name="horizon">
340
  <option value="1">1 Day</option>
341
  <option value="7" selected>7 Days</option>
342
  <option value="14">14 Days</option>
343
  <option value="30">30 Days</option>
344
  </select>
345
  </div>
346
-
347
  <div class="form-group">
348
- <label for="promo">Promotion</label>
349
- <span class="hint">Is promo active?</span>
350
- <select id="promo" name="promo">
351
- <option value="0">No Promo</option>
352
  <option value="1" selected>Active Promo</option>
 
353
  </select>
354
  </div>
355
-
 
356
  <div class="form-group">
357
  <label for="state_holiday">State Holiday</label>
358
- <span class="hint">Type of holiday</span>
359
- <select id="state_holiday" name="state_holiday">
360
  <option value="0">None</option>
361
- <option value="a">Public Holiday (a)</option>
362
- <option value="b">Easter Holiday (b)</option>
363
- <option value="c">Christmas (c)</option>
364
  </select>
365
  </div>
366
 
367
  <div class="form-group">
368
  <label for="school_holiday">School Holiday</label>
369
- <span class="hint">Are schools closed?</span>
370
- <select id="school_holiday" name="school_holiday">
371
  <option value="0">No</option>
372
  <option value="1">Yes</option>
373
  </select>
374
  </div>
375
-
376
- <div class="form-group">
377
- <label for="assortment">Assortment</label>
378
- <span class="hint">Store assortment type</span>
379
- <select id="assortment" name="assortment">
380
- <option value="a">Basic (a)</option>
381
- <option value="b">Extra (b)</option>
382
- <option value="c">Extended (c)</option>
383
- </select>
384
- </div>
385
 
386
  <div class="form-group">
387
- <label for="store_type">Store Type</label>
388
- <span class="hint">Model of store</span>
389
- <select id="store_type" name="store_type">
390
  <option value="a">Type A</option>
391
  <option value="b">Type B</option>
392
  <option value="c">Type C</option>
@@ -395,84 +373,103 @@ FRONTEND_HTML = """
395
  </div>
396
 
397
  <div class="form-group">
398
- <label for="competition_distance">Competitor Dist.</label>
399
- <span class="hint">Distance in meters</span>
400
- <input type="number" id="competition_distance" name="competition_distance"
401
- value="1270" min="0">
 
 
 
 
 
 
 
402
  </div>
403
  </div>
404
-
405
- <button type="submit" class="btn" id="submit-btn">
406
- Calculate Sales Forecast
407
  </button>
408
  </form>
409
-
410
- <div class="result" id="result">
411
- <div id="result-content" style="display: none;">
412
- <div class="label">Forecasted Sales</div>
413
- <div class="price" id="sales_val">€0</div>
414
- <div class="meta" id="meta">
415
- <div class="chart-container">
416
- <canvas id="salesChart"></canvas>
417
- </div>
418
- </div>
419
- </div>
420
-
421
- <div id="result-placeholder" class="result-placeholder">
422
- <div style="font-size: 2rem; margin-bottom: 1rem;">📈</div>
423
- <p>Enter details to see the sales trend.</p>
424
- </div>
425
- </div>
426
  </div>
427
-
428
- <div class="footer">
429
- <div>
430
- <a href="/docs">API Documentation</a> |
431
- <a href="/health">Health Check</a> |
432
- <a href="https://github.com/sylvia-ymlin/Rossmann-Store-Sales" target="_blank">GitHub</a>
 
 
 
 
 
 
 
 
 
 
 
 
433
  </div>
434
- <div class="tech-stack">
435
- <span class="tech-badge">XGBoost</span>
436
- <span class="tech-badge">FastAPI</span>
437
- <span class="tech-badge">Chart.js</span>
438
- <span class="tech-badge">Time-Series</span>
439
- <span class="tech-badge">Docker</span>
 
 
 
 
440
  </div>
441
  </div>
442
- </div>
443
  </div>
444
-
 
 
 
 
 
 
 
445
  <script>
446
  // Set default date to today
447
  document.getElementById('date').valueAsDate = new Date();
448
  let chartInstance = null;
449
 
450
- // Check health on load
451
  fetch('/health')
452
- .then(res => res.json())
453
- .then(data => {
454
- const badge = document.getElementById('mode-badge');
455
- if (data.status === 'healthy') {
456
- badge.textContent = 'System Online';
457
- badge.style.background = '#dcfce7'; /* Green */
458
- badge.style.color = '#15803d';
459
- badge.style.borderColor = '#15803d';
460
- } else {
461
- badge.textContent = 'System Issues';
462
- }
463
- })
464
- .catch(() => {
465
- document.getElementById('mode-badge').textContent = 'Offline';
466
- });
467
-
468
- // Form submission
 
 
 
 
469
  document.getElementById('predict-form').addEventListener('submit', async (e) => {
470
  e.preventDefault();
471
 
472
  const btn = document.getElementById('submit-btn');
 
473
  btn.disabled = true;
474
- btn.textContent = 'Forecasting...';
475
-
 
476
  const features = {
477
  "Store": parseInt(document.getElementById('store').value),
478
  "Date": document.getElementById('date').value,
@@ -484,7 +481,7 @@ FRONTEND_HTML = """
484
  "CompetitionDistance": parseInt(document.getElementById('competition_distance').value) || 0,
485
  "ForecastDays": parseInt(document.getElementById('horizon').value)
486
  };
487
-
488
  try {
489
  const response = await fetch('/predict', {
490
  method: 'POST',
@@ -494,57 +491,68 @@ FRONTEND_HTML = """
494
 
495
  const data = await response.json();
496
 
497
- const result = document.getElementById('result');
498
- const resultContent = document.getElementById('result-content');
499
- const resultPlaceholder = document.getElementById('result-placeholder');
500
- const salesVal = document.getElementById('sales_val');
501
 
502
- // Show content
503
- resultContent.style.display = 'block';
504
- resultPlaceholder.style.display = 'none';
505
-
506
- salesVal.textContent = '€' + Math.round(data.PredictedSales).toLocaleString();
507
 
 
 
 
 
 
 
 
 
 
 
 
508
  // Render Chart
509
  renderChart(data.Forecast);
510
-
511
- result.classList.add('show');
512
-
513
  } catch (error) {
514
- alert('Error: ' + error.message);
515
  console.error(error);
516
  } finally {
517
  btn.disabled = false;
518
- btn.textContent = 'Calculate Sales Forecast';
519
  }
520
  });
521
-
 
522
  function renderChart(forecastData) {
523
  const ctx = document.getElementById('salesChart').getContext('2d');
524
-
525
  const labels = forecastData.map(d => {
526
  const date = new Date(d.date);
527
- return date.toLocaleDateString(undefined, {month:'short', day:'numeric'});
528
  });
529
  const dataPoints = forecastData.map(d => d.sales);
530
-
531
- if (chartInstance) {
532
- chartInstance.destroy();
533
- }
534
-
 
 
 
535
  chartInstance = new Chart(ctx, {
536
  type: 'line',
537
  data: {
538
  labels: labels,
539
  datasets: [{
540
- label: 'Sales Forecast',
541
  data: dataPoints,
542
- borderColor: '#d93025',
543
- backgroundColor: 'rgba(217, 48, 37, 0.1)',
544
  borderWidth: 2,
545
- pointBackgroundColor: '#1a1a1a',
 
 
546
  pointRadius: 4,
547
- tension: 0.3, // Smooth curves for "hand-drawn" feel
 
548
  fill: true
549
  }]
550
  },
@@ -554,33 +562,37 @@ FRONTEND_HTML = """
554
  plugins: {
555
  legend: { display: false },
556
  tooltip: {
557
- callbacks: {
558
- label: function(context) {
559
- return '' + Math.round(context.raw).toLocaleString();
560
- }
561
- },
562
- backgroundColor: '#fff',
563
- titleColor: '#333',
564
- bodyColor: '#333',
565
- borderColor: '#333',
566
- borderWidth: 1,
567
  displayColors: false,
568
- padding: 10,
569
- titleFont: { family: 'Patrick Hand', size: 14 },
570
- bodyFont: { family: 'Patrick Hand', size: 14 }
571
  }
572
  },
573
  scales: {
574
  x: {
575
  grid: { display: false },
576
- ticks: { font: { family: 'Patrick Hand', size: 12 }, color: '#666' }
577
  },
578
  y: {
579
- grid: { borderDash: [5, 5], color: '#e5e5e5' },
580
- ticks: { font: { family: 'Patrick Hand', size: 12 }, color: '#666' },
 
 
 
 
 
581
  beginAtZero: true
582
  }
583
- }
 
 
 
 
584
  }
585
  });
586
  }
 
1
  """
2
  Frontend HTML template for the Rossmann Store Sales Predictor.
3
+ A clean, modern professional dashboard for making predictions.
4
  """
5
 
6
  FRONTEND_HTML = """
 
9
  <head>
10
  <meta charset="UTF-8">
11
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
+ <title>Rossmann Sales Intelligence</title>
13
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
14
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
15
  <style>
16
  :root {
17
+ /* Professional SaaS Theme */
18
+ --primary: #2563eb; /* Royal Blue */
19
+ --primary-dark: #1e40af;
20
+ --secondary: #64748b; /* Slate Grey */
21
+ --bg-page: #f8fafc; /* Very light blue-grey */
22
+ --bg-card: #ffffff;
23
+ --text-main: #0f172a; /* Dark Slate */
24
+ --text-muted: #64748b;
25
+ --border: #e2e8f0;
26
+ --success: #10b981;
27
+ --accent-gradient: linear-gradient(135deg, #2563eb 0%, #4f46e5 100%);
28
  }
29
 
30
  * {
31
  margin: 0;
32
  padding: 0;
33
  box-sizing: border-box;
34
+ font-family: 'Inter', sans-serif;
35
  }
36
 
37
  body {
38
+ background-color: var(--bg-page);
39
+ color: var(--text-main);
 
 
40
  min-height: 100vh;
41
  display: flex;
42
+ flex-direction: column;
43
+ }
44
+
45
+ /* Navbar */
46
+ .navbar {
47
+ background: var(--bg-card);
48
+ border-bottom: 1px solid var(--border);
49
+ padding: 1rem 2rem;
50
+ display: flex;
51
+ justify-content: space-between;
52
  align-items: center;
 
 
 
53
  }
54
 
55
+ .brand {
56
+ font-weight: 700;
57
+ font-size: 1.25rem;
58
+ color: var(--text-main);
59
  display: flex;
60
+ align-items: center;
61
+ gap: 0.5rem;
62
  }
63
+
64
+ .brand span { color: var(--primary); }
65
 
66
+ .status-badge {
67
+ font-size: 0.85rem;
68
+ padding: 0.25rem 0.75rem;
69
+ border-radius: 9999px;
70
+ background: #e2e8f0;
71
+ color: var(--secondary);
72
+ font-weight: 500;
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 0.4rem;
76
  }
77
 
78
+ .status-dot {
79
+ width: 8px;
80
+ height: 8px;
81
+ border-radius: 50%;
82
+ background-color: #94a3b8;
83
  }
84
 
85
+ /* Main Layout */
86
+ .container {
87
+ flex: 1;
88
+ max-width: 1200px;
89
+ margin: 0 auto;
90
+ padding: 2rem;
91
+ width: 100%;
92
+ display: grid;
93
+ grid-template-columns: 350px 1fr;
94
+ gap: 2rem;
95
  }
96
 
97
+ @media (max-width: 900px) {
98
+ .container {
99
+ grid-template-columns: 1fr;
100
+ }
101
  }
102
 
103
+ /* Cards */
104
+ .card {
105
+ background: var(--bg-card);
106
+ border: 1px solid var(--border);
107
+ border-radius: 12px;
108
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
109
+ padding: 1.5rem;
110
+ height: 100%;
111
  }
112
 
113
+ .card-header {
114
+ margin-bottom: 1.5rem;
 
 
 
 
 
 
 
 
 
115
  }
116
+
117
+ .card-title {
118
+ font-size: 1.1rem;
119
+ font-weight: 600;
120
+ color: var(--text-main);
121
+ margin-bottom: 0.25rem;
 
122
  }
123
+
124
+ .card-subtitle {
125
+ font-size: 0.875rem;
126
+ color: var(--text-muted);
 
 
127
  }
128
 
129
+ /* Form Styling */
130
  .form-grid {
131
  display: grid;
132
+ gap: 1.25rem;
 
 
133
  }
134
 
135
  .form-group {
136
  display: flex;
137
  flex-direction: column;
138
+ gap: 0.4rem;
139
  }
140
 
141
  .form-group label {
142
+ font-size: 0.85rem;
143
+ font-weight: 500;
144
+ color: var(--text-main);
145
  }
146
 
147
+ .form-control {
148
+ padding: 0.6rem 0.8rem;
149
+ border: 1px solid var(--border);
150
+ border-radius: 6px;
151
+ font-size: 0.95rem;
152
+ color: var(--text-main);
153
+ background: #fff;
154
+ transition: border-color 0.15s, box-shadow 0.15s;
 
 
 
 
 
 
 
 
 
 
155
  }
156
 
157
+ .form-control:focus {
158
  outline: none;
159
+ border-color: var(--primary);
160
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  }
162
 
163
+ /* Button */
164
+ .btn-primary {
165
+ background: var(--accent-gradient);
 
 
166
  color: white;
167
+ border: none;
168
+ padding: 0.75rem;
169
+ border-radius: 8px;
170
+ font-weight: 500;
171
+ font-size: 0.95rem;
172
  cursor: pointer;
173
+ transition: opacity 0.2s, transform 0.1s;
174
+ margin-top: 1rem;
175
+ width: 100%;
176
+ box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.2);
177
  }
178
 
179
+ .btn-primary:hover {
180
+ opacity: 0.95;
 
181
  }
182
+
183
+ .btn-primary:active {
184
+ transform: translateY(1px);
 
185
  }
186
 
187
+ .btn-primary:disabled {
188
+ background: var(--secondary);
 
189
  cursor: not-allowed;
190
  transform: none;
 
191
  }
192
 
193
+ /* Results Area */
194
+ .results-container {
 
 
 
 
 
 
 
 
 
195
  display: flex;
196
  flex-direction: column;
197
+ gap: 1.5rem;
198
+ height: 100%;
199
+ }
200
+
201
+ .metric-card {
202
+ background: linear-gradient(to right, #ffffff, #f8fafc);
203
+ border: 1px solid var(--border);
204
+ border-radius: 12px;
205
+ padding: 2rem;
206
+ text-align: center;
207
+ position: relative;
208
+ overflow: hidden;
209
  }
210
 
211
+ .metric-card::before {
212
  content: '';
213
  position: absolute;
214
+ top: 0;
215
+ left: 0;
216
+ height: 100%;
217
+ width: 4px;
218
+ background: var(--primary);
 
 
 
 
219
  }
220
 
221
+ .metric-label {
222
+ font-size: 0.875rem;
 
 
 
 
 
 
 
 
223
  text-transform: uppercase;
224
+ letter-spacing: 0.05em;
225
+ color: var(--text-muted);
226
+ font-weight: 600;
227
  margin-bottom: 0.5rem;
228
  }
229
 
230
+ .metric-value {
231
+ font-size: 3rem;
232
+ font-weight: 700;
233
+ color: var(--text-main);
234
+ letter-spacing: -1px;
 
235
  }
236
 
237
+ .chart-wrapper {
238
+ flex: 1;
239
+ min-height: 400px;
240
+ background: #fff;
241
+ border-radius: 12px;
242
+ position: relative;
 
 
243
  }
244
+
245
+ /* Empty State */
246
+ .empty-state {
247
+ height: 100%;
248
+ display: flex;
249
+ flex-direction: column;
250
+ align-items: center;
251
+ justify-content: center;
252
+ color: var(--text-muted);
253
+ text-align: center;
254
+ padding: 2rem;
255
+ border: 2px dashed var(--border);
256
+ border-radius: 12px;
257
+ background: #fff;
258
+ }
259
+
260
+ .empty-icon {
261
+ font-size: 3rem;
262
+ margin-bottom: 1rem;
263
+ opacity: 0.5;
264
  }
265
 
266
  /* Footer */
267
  .footer {
 
268
  text-align: center;
269
+ padding: 2rem;
270
+ color: var(--text-muted);
271
+ font-size: 0.85rem;
272
+ border-top: 1px solid var(--border);
273
+ background: #fff;
274
+ margin-top: auto;
275
+ }
276
+
277
+ .footer a {
278
+ color: var(--primary);
279
+ text-decoration: none;
280
  }
281
 
282
+ .footer a:hover { text-decoration: underline; }
283
 
284
+ /* Smooth reveal */
285
+ .reveal {
286
+ animation: fadeIn 0.4s ease-out;
 
 
 
287
  }
288
 
289
+ @keyframes fadeIn {
290
+ from { opacity: 0; transform: translateY(10px); }
291
+ to { opacity: 1; transform: translateY(0); }
292
+ }
 
 
 
 
 
 
293
  </style>
294
  </head>
295
  <body>
296
+ <nav class="navbar">
297
+ <div class="brand">
298
+ <span>🔹</span> Rossmann Sales Intelligence
299
+ </div>
300
+ <div class="status-badge" id="mode-badge">
301
+ <div class="status-dot" id="status-dot"></div>
302
+ <span id="status-text">Connecting...</span>
303
+ </div>
304
+ </nav>
305
+
306
  <div class="container">
307
+ <!-- Sidebar / Configuration -->
308
+ <aside class="sidebar">
309
+ <div class="card">
310
+ <div class="card-header">
311
+ <h2 class="card-title">Prediction Parameters</h2>
312
+ <p class="card-subtitle">Configure store & scenario details.</p>
313
+ </div>
314
+
315
  <form id="predict-form">
316
  <div class="form-grid">
317
  <div class="form-group">
318
  <label for="store">Store ID</label>
319
+ <input type="number" id="store" class="form-control"
320
+ value="1" min="1" max="1115" required placeholder="e.g. 1">
 
321
  </div>
322
+
323
  <div class="form-group">
324
  <label for="date">Start Date</label>
325
+ <input type="date" id="date" class="form-control" required>
 
326
  </div>
327
+
328
  <div class="form-group">
329
+ <label for="horizon">Forecast Horizon</label>
330
+ <select id="horizon" class="form-control">
 
331
  <option value="1">1 Day</option>
332
  <option value="7" selected>7 Days</option>
333
  <option value="14">14 Days</option>
334
  <option value="30">30 Days</option>
335
  </select>
336
  </div>
337
+
338
  <div class="form-group">
339
+ <label for="promo">Promotion Status</label>
340
+ <select id="promo" class="form-control">
 
 
341
  <option value="1" selected>Active Promo</option>
342
+ <option value="0">No Promo</option>
343
  </select>
344
  </div>
345
+
346
+ <!-- Advanced grouped in details for simplicity? No, keep expanding -->
347
  <div class="form-group">
348
  <label for="state_holiday">State Holiday</label>
349
+ <select id="state_holiday" class="form-control">
 
350
  <option value="0">None</option>
351
+ <option value="a">Public Holiday</option>
352
+ <option value="b">Easter Holiday</option>
353
+ <option value="c">Christmas</option>
354
  </select>
355
  </div>
356
 
357
  <div class="form-group">
358
  <label for="school_holiday">School Holiday</label>
359
+ <select id="school_holiday" class="form-control">
 
360
  <option value="0">No</option>
361
  <option value="1">Yes</option>
362
  </select>
363
  </div>
 
 
 
 
 
 
 
 
 
 
364
 
365
  <div class="form-group">
366
+ <label for="store_type">Store Type</label>
367
+ <select id="store_type" class="form-control">
 
368
  <option value="a">Type A</option>
369
  <option value="b">Type B</option>
370
  <option value="c">Type C</option>
 
373
  </div>
374
 
375
  <div class="form-group">
376
+ <label for="assortment">Assortment</label>
377
+ <select id="assortment" class="form-control">
378
+ <option value="a">Basic</option>
379
+ <option value="b">Extra</option>
380
+ <option value="c">Extended</option>
381
+ </select>
382
+ </div>
383
+
384
+ <div class="form-group">
385
+ <label for="competition_distance">Competition Dist. (m)</label>
386
+ <input type="number" id="competition_distance" class="form-control" value="1270">
387
  </div>
388
  </div>
389
+
390
+ <button type="submit" class="btn-primary" id="submit-btn">
391
+ Result Generation
392
  </button>
393
  </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  </div>
395
+ </aside>
396
+
397
+ <!-- Main Content / Results -->
398
+ <main class="results-area">
399
+ <div id="result-placeholder" class="empty-state">
400
+ <div class="empty-icon">📊</div>
401
+ <h3>Ready to Forecast</h3>
402
+ <p>Enter parameters on the left to generate a sales prediction.</p>
403
+ </div>
404
+
405
+ <div id="results-content" class="results-container" style="display: none;">
406
+ <!-- Key Metric -->
407
+ <div class="metric-card reveal">
408
+ <div class="metric-label">Projected Sales (Start Date)</div>
409
+ <div class="metric-value" id="sales_val">--</div>
410
+ <div style="font-size: 0.9rem; color: var(--success); margin-top: 0.5rem;" id="trend-indicator">
411
+ <!-- Dynamic trend text -->
412
+ </div>
413
  </div>
414
+
415
+ <!-- Chart -->
416
+ <div class="card reveal" style="flex: 1; display: flex; flex-direction: column;">
417
+ <div class="card-header">
418
+ <h3 class="card-title">Forecast Trend</h3>
419
+ <p class="card-subtitle">Expected turnover over the selected horizon.</p>
420
+ </div>
421
+ <div class="chart-wrapper">
422
+ <canvas id="salesChart"></canvas>
423
+ </div>
424
  </div>
425
  </div>
426
+ </main>
427
  </div>
428
+
429
+ <footer class="footer">
430
+ <p>© 2026 Rossmann Intelligence System. Powered by XGBoost, FastAPI & Docker.</p>
431
+ <div style="margin-top: 0.5rem;">
432
+ <a href="/docs">API Docs</a> • <a href="https://github.com/sylvia-ymlin/Rossmann-Store-Sales">GitHub</a>
433
+ </div>
434
+ </footer>
435
+
436
  <script>
437
  // Set default date to today
438
  document.getElementById('date').valueAsDate = new Date();
439
  let chartInstance = null;
440
 
441
+ // Health Check
442
  fetch('/health')
443
+ .then(res => res.json())
444
+ .then(data => {
445
+ const badge = document.getElementById('mode-badge');
446
+ const dot = document.getElementById('status-dot');
447
+ const text = document.getElementById('status-text');
448
+
449
+ if (data.status === 'healthy') {
450
+ badge.style.background = '#dcfce7'; /* Green-100 */
451
+ badge.style.color = '#166534'; /* Green-800 */
452
+ dot.style.backgroundColor = '#166534';
453
+ text.textContent = 'System Online';
454
+ } else {
455
+ text.textContent = 'System Issues';
456
+ dot.style.backgroundColor = '#dc2626';
457
+ }
458
+ })
459
+ .catch(() => {
460
+ document.getElementById('status-text').textContent = 'Offline';
461
+ });
462
+
463
+ // Form Logic
464
  document.getElementById('predict-form').addEventListener('submit', async (e) => {
465
  e.preventDefault();
466
 
467
  const btn = document.getElementById('submit-btn');
468
+ const originalText = btn.textContent;
469
  btn.disabled = true;
470
+ btn.textContent = 'Processing...';
471
+
472
+ // Gather Data
473
  const features = {
474
  "Store": parseInt(document.getElementById('store').value),
475
  "Date": document.getElementById('date').value,
 
481
  "CompetitionDistance": parseInt(document.getElementById('competition_distance').value) || 0,
482
  "ForecastDays": parseInt(document.getElementById('horizon').value)
483
  };
484
+
485
  try {
486
  const response = await fetch('/predict', {
487
  method: 'POST',
 
491
 
492
  const data = await response.json();
493
 
494
+ // Switch View
495
+ document.getElementById('result-placeholder').style.display = 'none';
496
+ document.getElementById('results-content').style.display = 'flex';
 
497
 
498
+ // Update Metric
499
+ document.getElementById('sales_val').textContent = '' + Math.round(data.PredictedSales).toLocaleString();
 
 
 
500
 
501
+ // Simple Trend Indicator Logic (Compare first vs avg)
502
+ const avg = data.Forecast.reduce((acc, curr) => acc + curr.sales, 0) / data.Forecast.length;
503
+ const trendEl = document.getElementById('trend-indicator');
504
+ if (data.PredictedSales < avg) {
505
+ trendEl.innerHTML = 'Shop expects <strong>increasing</strong> demand over horizon';
506
+ trendEl.style.color = '#10b981'; // Green
507
+ } else {
508
+ trendEl.innerHTML = 'Shop expects <strong>stable/decreasing</strong> demand';
509
+ trendEl.style.color = '#64748b'; // Grey
510
+ }
511
+
512
  // Render Chart
513
  renderChart(data.Forecast);
514
+
 
 
515
  } catch (error) {
516
+ alert('Prediction Failed: ' + error.message);
517
  console.error(error);
518
  } finally {
519
  btn.disabled = false;
520
+ btn.textContent = originalText;
521
  }
522
  });
523
+
524
+ // Professional Chart
525
  function renderChart(forecastData) {
526
  const ctx = document.getElementById('salesChart').getContext('2d');
 
527
  const labels = forecastData.map(d => {
528
  const date = new Date(d.date);
529
+ return date.toLocaleDateString(undefined, {month: 'short', day: 'numeric'});
530
  });
531
  const dataPoints = forecastData.map(d => d.sales);
532
+
533
+ if (chartInstance) chartInstance.destroy();
534
+
535
+ // Gradient Fill
536
+ const gradient = ctx.createLinearGradient(0, 0, 0, 400);
537
+ gradient.addColorStop(0, 'rgba(37, 99, 235, 0.2)'); // Brand Blue
538
+ gradient.addColorStop(1, 'rgba(37, 99, 235, 0)');
539
+
540
  chartInstance = new Chart(ctx, {
541
  type: 'line',
542
  data: {
543
  labels: labels,
544
  datasets: [{
545
+ label: 'Projected Revenue (€)',
546
  data: dataPoints,
547
+ borderColor: '#2563eb',
548
+ backgroundColor: gradient,
549
  borderWidth: 2,
550
+ pointBackgroundColor: '#fff',
551
+ pointBorderColor: '#2563eb',
552
+ pointBorderWidth: 2,
553
  pointRadius: 4,
554
+ pointHoverRadius: 6,
555
+ tension: 0.4, // Smooth Spline
556
  fill: true
557
  }]
558
  },
 
562
  plugins: {
563
  legend: { display: false },
564
  tooltip: {
565
+ backgroundColor: '#1e293b',
566
+ padding: 12,
567
+ titleFont: { family: 'Inter', size: 13 },
568
+ bodyFont: { family: 'Inter', size: 14, weight: 'bold' },
569
+ cornerRadius: 8,
 
 
 
 
 
570
  displayColors: false,
571
+ callbacks: {
572
+ label: (ctx) => '' + Math.round(ctx.raw).toLocaleString()
573
+ }
574
  }
575
  },
576
  scales: {
577
  x: {
578
  grid: { display: false },
579
+ ticks: { font: { family: 'Inter' }, color: '#64748b' }
580
  },
581
  y: {
582
+ border: { display: false },
583
+ grid: { color: '#f1f5f9' },
584
+ ticks: {
585
+ font: { family: 'Inter' },
586
+ color: '#64748b',
587
+ callback: (value) => '€' + value.toLocaleString()
588
+ },
589
  beginAtZero: true
590
  }
591
+ },
592
+ interaction: {
593
+ intersect: false,
594
+ mode: 'index',
595
+ },
596
  }
597
  });
598
  }