/**
* AnalyticsModal.js
* Handles the display and POST fetch logic for the batch analysis feature.
*/
import { configState } from '../core/ConfigState.js';
export class AnalyticsModal {
constructor() {
this.btnAnalyze = document.getElementById('btn-analyze');
this.modal = document.getElementById('analytics-modal');
this.btnCloseModal = document.getElementById('btn-close-modal');
this.confirmationDiv = document.getElementById('analytics-confirmation');
this.configListDiv = document.getElementById('analytics-config-list');
this.btnRunConfirm = document.getElementById('btn-run-analysis-confirm');
this.title = document.getElementById('analytics-modal-title');
this.loadingDiv = document.getElementById('analytics-loading');
this.contentDiv = document.getElementById('analytics-content');
this.subtitle = document.getElementById('analytics-subtitle');
this.bindEvents();
}
bindEvents() {
if (this.btnAnalyze && this.modal) {
this.btnAnalyze.addEventListener('click', () => this.showConfirmation());
}
if (this.btnRunConfirm) {
this.btnRunConfirm.addEventListener('click', () => this.runAnalysis());
}
if (this.btnCloseModal && this.modal) {
this.btnCloseModal.addEventListener('click', () => {
this.modal.classList.remove('scrim--active');
});
}
}
showConfirmation() {
this.modal.classList.add('scrim--active');
if (this.title) this.title.innerText = "Batch Analytics Setup";
// Ensure state is fresh from DOM before reading
configState.refreshFromDOM();
const payload = configState.getConfig();
const shiftHours = payload.shiftHours || 2;
const reps = payload.replications || 10;
if (this.subtitle) {
this.subtitle.innerText = `Preparing to run ${reps} simulated days (${shiftHours} hours each).`;
}
if (this.configListDiv) {
// Fix: Fallback to defaults if somehow still undefined, and use correct ConfigState keys
const cashiers = payload.cashiers || 1;
const baristas = payload.baristas || 2;
const arrivalRate = payload.arrival || 45.0;
const decideMin = payload.decideMin || 10.0;
const decideMax = payload.decideMax || 60.0;
const baristaMin = payload.prepMin || 60.0;
const baristaMax = payload.prepMax || 180.0;
const balkThresh = payload.balkThreshold || 8;
const resProb = Math.round((payload.resProb || 0.2) * 100);
const resMin = payload.resArrivalMin || 30.0;
const resMax = payload.resArrivalMax || 180.0;
const takeoutProb = Math.round((payload.takeoutProb || 0.5) * 100);
const balkProb = Math.round((payload.balkProb || 0.5) * 100);
const renegeProb = Math.round((payload.renegeProb || 0.3) * 100);
const strikes = payload.maxStrikes || 3;
const warmup = payload.warmupTime || 0;
const payMin = payload.payMin || 2.0;
const payMax = payload.payMax || 10.0;
const dwellMin = payload.dwellMin || 900.0;
const dwellMax = payload.dwellMax || 3600.0;
this.configListDiv.innerHTML = `
Cashiers: ${cashiers} | Baristas: ${baristas} | Tables: ${payload.tables || 5}
Arrival Rate: 1 every ${arrivalRate.toFixed(1)}s (≈ ${Math.round(3600/arrivalRate)} / hr)
Cashier Service: Order: ${decideMin}s-${decideMax}s | Pay: ${payMin}s-${payMax}s
Barista Service: ${baristaMin}s - ${baristaMax}s
Dine-In Time: ${dwellMin}s - ${dwellMax}s
Logic & Reservations
Reservations: ${resProb}% (Arrival Window: ${resMin}s-${resMax}s)
Takeout: ${takeoutProb}%
Balking (Line too long): ${balkProb}% (Threshold: ${balkThresh})
Reneging (Tired of waiting): ${renegeProb}% (Strikes: ${strikes})
Warm-Up Time: ${warmup}s
`;
}
if (this.confirmationDiv) this.confirmationDiv.style.display = 'flex';
if (this.loadingDiv) this.loadingDiv.style.display = 'none';
if (this.contentDiv) this.contentDiv.style.display = 'none';
}
async runAnalysis() {
if (this.title) this.title.innerText = "Statistical Batch Results";
if (this.confirmationDiv) this.confirmationDiv.style.display = 'none';
if (this.loadingDiv && this.contentDiv) {
this.loadingDiv.style.display = 'block';
this.loadingDiv.innerHTML = 'Analyzing.Time elapsed: 0.0s
';
this.contentDiv.style.display = 'none';
}
// Start local timer for UI
const startTime = Date.now();
let animDots = 0;
const animInterval = setInterval(() => {
animDots = (animDots + 1) % 4;
const textEl = document.getElementById('analyzing-text');
if (textEl) {
textEl.innerText = 'Analyzing' + '.'.repeat(animDots);
}
}, 500);
const timerInterval = setInterval(() => {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
const timerEl = document.getElementById('analyzing-timer');
if (timerEl) {
timerEl.innerText = `Time elapsed: ${elapsed}s`;
}
}, 100);
try {
// Fetch configuration payload from central state
const payload = configState.getConfig();
// Format standard properties expected by analytics engine
const shiftHours = payload.shiftHours || 2;
const replicationsCount = payload.replications || 10;
payload.duration = Math.floor(shiftHours * 3600);
if (this.subtitle) {
this.subtitle.innerText = `Averaged over ${replicationsCount} independent simulated days (${shiftHours} hours each, minus warm-up).`;
}
// Send to FastAPI Backend
const res = await fetch('/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
clearInterval(animInterval);
clearInterval(timerInterval);
if (!res.ok) {
throw new Error("Server returned " + res.status);
}
const data = await res.json();
this.displayResults(data);
} catch (e) {
clearInterval(animInterval);
clearInterval(timerInterval);
if (this.loadingDiv) {
this.loadingDiv.innerHTML = `Failed to run analysis: ${e.message}`;
}
}
}
displayResults(data) {
if (this.loadingDiv) this.loadingDiv.style.display = 'none';
if (this.contentDiv) this.contentDiv.style.display = 'block';
const setText = (id, val) => {
const el = document.getElementById(id);
if (el) el.innerText = val;
};
setText('res-wait', (data.avg_wait_time).toFixed(1) + 's (~' + (data.avg_wait_time / 60).toFixed(1) + ' mins)');
setText('res-cycle', (data.avg_cycle_time).toFixed(1) + 's (~' + (data.avg_cycle_time / 60).toFixed(1) + ' mins)');
setText('res-lost-customers', (data.avg_lost_customers || 0).toFixed(1) + ' customers');
setText('res-reservations', (data.avg_reservations || 0).toFixed(1));
const payload = configState.getConfig();
const fallbackArrivals = Math.round(3600 / (parseFloat(payload.arrival) || 45.0));
setText('res-arrivals', (data.target_arrivals_per_hour || fallbackArrivals).toFixed(0) + ' / hr');
setText('res-throughput', data.throughput_per_hour.toFixed(0) + ' / hr');
// Breakdown logic
if (data.wait_breakdown && data.total_customers) {
const q_zero = ((data.wait_breakdown["0-2min"] / data.total_customers) * 100).toFixed(1);
const q_short = ((data.wait_breakdown["2-5min"] / data.total_customers) * 100).toFixed(1);
const q_med = ((data.wait_breakdown["5-10min"] / data.total_customers) * 100).toFixed(1);
const q_long = ((data.wait_breakdown[">10min"] / data.total_customers) * 100).toFixed(1);
const qHtml = `
0-2 mins: ${q_zero}%
2-5 mins: ${q_short}%
5-10 mins: ${q_med}%
>10 mins: ${q_long}%
`;
const brEl = document.getElementById('res-breakdown');
if (brEl) brEl.innerHTML = qHtml;
}
}
}
export const analyticsModal = new AnalyticsModal();