${ticket.subject}
#${ticket.id}
Agent: ${agent ? agent.name : 'Unassigned'}
`;
cardContainer.appendChild(card);
});
board.appendChild(column);
});
}
function renderBottleneckAnalysis() {
const tableBody = document.getElementById('bottleneck-table');
tableBody.innerHTML = '';
Object.entries(state.avgTimeInStage).forEach(([stage, avgTime]) => {
const row = document.createElement('tr');
const ticketCount = state.tickets.filter(t => t.status === stage).length;
const isBottleneck = stage === state.topBottleneck;
row.innerHTML = `
${stage} |
${avgTime.toFixed(1)} |
${ticketCount} |
${isBottleneck ? 'Yes' : 'No'} |
`;
tableBody.appendChild(row);
});
}
function renderConfigForms() {
const agentSelect = document.getElementById('ticket-agent');
agentSelect.innerHTML = '
';
state.agents.forEach(a => {
const option = document.createElement('option');
option.value = a.id;
option.textContent = a.name;
agentSelect.appendChild(option);
});
}
// --- EVENT HANDLERS & LOGIC --- //
function attachEventListeners() {
prevBtn.addEventListener('click', () => changeTab(state.activeTab - 1));
nextBtn.addEventListener('click', () => changeTab(state.activeTab + 1));
downloadPdfBtn.addEventListener('click', handleDownloadPdf);
document.getElementById('add-ticket-form').addEventListener('submit', handleAddTicket);
}
window.changeTab = (tabIndex) => {
if (tabIndex < 0 || tabIndex >= tabButtons.length) return;
state.activeTab = tabIndex;
updateTabUI();
};
function updateTabUI() {
tabButtons.forEach((b, i) => b.classList.toggle('active', i === state.activeTab));
tabButtons.forEach((b, i) => b.classList.toggle('inactive', i !== state.activeTab));
tabPanes.forEach((p, i) => p.classList.toggle('hidden', i !== state.activeTab));
prevBtn.disabled = state.activeTab === 0;
nextBtn.disabled = state.activeTab === tabButtons.length - 1;
prevBtn.classList.toggle('opacity-50', prevBtn.disabled);
nextBtn.classList.toggle('opacity-50', nextBtn.disabled);
}
function handleAddTicket(e) {
e.preventDefault();
const form = e.target;
const newTicket = {
id: Math.max(...state.tickets.map(t => t.id)) + 1,
subject: form.elements['ticket-subject'].value,
agentId: parseInt(form.elements['ticket-agent'].value) || null,
status: 'New',
history: [{ stage: 'New', timestamp: new Date(today) }],
};
state.tickets.push(newTicket);
form.reset();
render();
changeTab(1);
}
async function handleDownloadPdf() {
const downloadBtn = document.getElementById('download-pdf-btn');
if (!downloadBtn || downloadBtn.disabled) return;
const originalBtnContent = downloadBtn.innerHTML;
downloadBtn.disabled = true;
downloadBtn.innerHTML = `
Generating PDF...`;
try {
const { jsPDF } = window.jspdf;
const contentToClone = document.getElementById('support-workflow-container');
const contentToExport = contentToClone.cloneNode(true);
contentToExport.querySelectorAll('.pdf-hide').forEach(el => el.remove());
contentToExport.querySelectorAll('.tab-pane:not(#tab-0-content)').forEach(el => el.remove());
contentToExport.querySelector('#tab-0-content').classList.remove('hidden');
document.body.appendChild(contentToExport);
contentToExport.style.cssText = 'position:absolute; left:0; top:0; width:1024px; z-index:-1; background-color:white; padding:2rem;';
const canvas = await html2canvas(contentToExport, { scale: 2, useCORS: true, logging: false });
document.body.removeChild(contentToExport);
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' });
const pdfWidth = pdf.internal.pageSize.getWidth();
const scaledHeight = (canvas.height * pdfWidth) / canvas.width;
pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, scaledHeight);
pdf.save('Support_Workflow_Summary.pdf');
} catch (error) {
console.error("Failed to generate PDF:", error);
} finally {
downloadBtn.disabled = false;
downloadBtn.innerHTML = originalBtnContent;
}
}
// --- UTILITY FUNCTIONS --- //
function calculateMetrics() {
const resolvedTickets = state.tickets.filter(t => t.status === 'Resolved');
state.avgLifecycleHours = resolvedTickets.length > 0 ? resolvedTickets.reduce((sum, t) => {
const start = t.history[0].timestamp;
const end = t.history[t.history.length - 1].timestamp;
return sum + (end - start);
}, 0) / resolvedTickets.length / (1000 * 60 * 60) : 0;
const fcrTickets = resolvedTickets.filter(t => t.history.length === 2).length;
state.fcrRate = resolvedTickets.length > 0 ? (fcrTickets / resolvedTickets.length) * 100 : 0;
const timeInStage = {};
state.stages.forEach(s => timeInStage[s] = { total: 0, count: 0 });
state.tickets.forEach(t => {
for (let i = 0; i < t.history.length - 1; i++) {
const stage = t.history[i].stage;
const duration = t.history[i+1].timestamp - t.history[i].timestamp;
timeInStage[stage].total += duration;
timeInStage[stage].count++;
}
// Add time for current stage
const currentStage = t.history[t.history.length - 1];
if (t.status !== 'Resolved') {
const duration = today - currentStage.timestamp;
timeInStage[currentStage.stage].total += duration;
timeInStage[currentStage.stage].count++;
}
});
state.avgTimeInStage = {};
let maxTime = 0;
state.topBottleneck = '-';
Object.keys(timeInStage).forEach(stage => {
const { total, count } = timeInStage[stage];
const avg = count > 0 ? (total / count) / (1000 * 60 * 60) : 0;
state.avgTimeInStage[stage] = avg;
if (avg > maxTime && stage !== 'Resolved') {
maxTime = avg;
state.topBottleneck = stage;
}
});
}
// --- START THE APP --- //
initialize();
});