No data to visualize.
`;
return;
}
const statusCounts = { 'Pending': 0, 'En Route': 0, 'Completed': 0, 'Cancelled': 0 };
dispatches.forEach(item => {
if (statusCounts.hasOwnProperty(item.status)) {
statusCounts[item.status]++;
}
});
const maxCount = Math.max(...Object.values(statusCounts));
// Order of statuses for display
const orderedStatuses = ['Pending', 'En Route', 'Completed', 'Cancelled'];
orderedStatuses.forEach(status => {
const count = statusCounts[status];
const percentage = maxCount > 0 ? (count / maxCount) * 100 : 0;
const barItem = document.createElement('div');
barItem.classList.add('bar-item');
barItem.innerHTML = `
${status}:
${count}
`;
statusBarChartEl.appendChild(barItem);
});
}
/**
* Renders the dispatch items into the data configuration table with edit/delete actions.
*/
function renderManageDispatchesTable() {
if (!manageDispatchesTableBody) {
console.error('Manage dispatches table body not found.');
return;
}
manageDispatchesTableBody.innerHTML = ''; // Clear existing rows
if (dispatches.length === 0) {
manageDispatchesTableBody.innerHTML = `
| No dispatch tasks to manage. |
`;
return;
}
dispatches.forEach(dispatch => {
const startDate = dispatch.startTime ? dispatch.startTime.split('T')[0] : '';
const startTime = dispatch.startTime ? dispatch.startTime.split('T')[1] : '';
const endDate = dispatch.endTime ? dispatch.endTime.split('T')[0] : '';
const endTime = dispatch.endTime ? dispatch.endTime.split('T')[1] : '';
const row = manageDispatchesTableBody.insertRow();
row.setAttribute('data-dispatch-id', dispatch.id); // Set data attribute for easy lookup
row.classList.add('hover:bg-gray-50');
row.innerHTML = `
|
|
|
|
|
|
|
|
`;
});
}
/**
* Adds a new dispatch item from the input fields.
*/
function addDispatch() {
if (!newTaskDescriptionInput || !newStatusSelect || !newPrioritySelect || !newAssignedToInput || !newStartDateInput || !newStartTimeInput || !newDeliveryOutcomeSelect) {
console.error('New dispatch input elements not found.');
return;
}
const description = newTaskDescriptionInput.value.trim();
const status = newStatusSelect.value;
const priority = newPrioritySelect.value;
const assignedTo = newAssignedToInput.value.trim();
const startDate = newStartDateInput.value;
const startTime = newStartTimeInput.value;
const endDate = newEndDateInput.value;
const endTime = newEndTimeInput.value;
const deliveryOutcome = newDeliveryOutcomeSelect.value;
if (!description || !status || !priority || !assignedTo || !startDate || !startTime) {
alert('Please fill in all mandatory fields: Task Description, Status, Priority, Assigned To, Start Date, and Start Time.');
return;
}
const fullStartTime = `${startDate}T${startTime}`;
const fullEndTime = (endDate && endTime) ? `${endDate}T${endTime}` : '';
dispatches.push({
id: generateUniqueId(),
description: description,
status: status,
priority: priority,
assignedTo: assignedTo,
startTime: fullStartTime,
endTime: fullEndTime,
deliveryOutcome: deliveryOutcome
});
// Clear input fields
newTaskDescriptionInput.value = '';
newStatusSelect.value = 'Pending';
newPrioritySelect.value = 'Low';
newAssignedToInput.value = '';
newStartDateInput.value = '';
newStartTimeInput.value = '';
newEndDateInput.value = '';
newEndTimeInput.value = '';
newDeliveryOutcomeSelect.value = 'N/A';
// Re-render tables and update metrics
updateAllViews();
}
/**
* Updates an existing dispatch item based on changes in the manage table.
* This function is exposed globally for `onclick` attributes.
* @param {string} dispatchId - The ID of the dispatch to update.
* @param {HTMLElement} buttonElement - The button element that triggered the update.
*/
window.updateDispatch = function(dispatchId, buttonElement) {
const row = buttonElement.closest('tr');
if (!row) {
console.error('Could not find row for dispatchId:', dispatchId);
return;
}
const descriptionInput = row.querySelector('[data-field="description"]');
const statusSelect = row.querySelector('[data-field="status"]');
const prioritySelect = row.querySelector('[data-field="priority"]');
const assignedToInput = row.querySelector('[data-field="assignedTo"]');
const startDateInput = row.querySelector('[data-field="startDate"]');
const startTimeInput = row.querySelector('[data-field="startTime"]');
const endDateInput = row.querySelector('[data-field="endDate"]');
const endTimeInput = row.querySelector('[data-field="endTime"]');
const deliveryOutcomeSelect = row.querySelector('[data-field="deliveryOutcome"]');
if (!descriptionInput || !statusSelect || !prioritySelect || !assignedToInput || !startDateInput || !startTimeInput || !deliveryOutcomeSelect) {
console.error('Input fields not found in row for dispatchId:', dispatchId);
return;
}
const updatedDescription = descriptionInput.value.trim();
const updatedStatus = statusSelect.value;
const updatedPriority = prioritySelect.value;
const updatedAssignedTo = assignedToInput.value.trim();
const updatedStartDate = startDateInput.value;
const updatedStartTime = startTimeInput.value;
const updatedEndDate = endDateInput.value;
const updatedEndTime = endTimeInput.value;
const updatedDeliveryOutcome = deliveryOutcomeSelect.value;
if (!updatedDescription || !updatedStatus || !updatedPriority || !updatedAssignedTo || !updatedStartDate || !updatedStartTime) {
alert('Please ensure all mandatory fields are valid before saving.');
return;
}
const updatedFullStartTime = `${updatedStartDate}T${updatedStartTime}`;
const updatedFullEndTime = (updatedEndDate && updatedEndTime) ? `${updatedEndDate}T${updatedEndTime}` : '';
const dispatchIndex = dispatches.findIndex(d => d.id === dispatchId);
if (dispatchIndex !== -1) {
dispatches[dispatchIndex] = {
...dispatches[dispatchIndex],
description: updatedDescription,
status: updatedStatus,
priority: updatedPriority,
assignedTo: updatedAssignedTo,
startTime: updatedFullStartTime,
endTime: updatedFullEndTime,
deliveryOutcome: updatedDeliveryOutcome
};
updateAllViews();
} else {
console.warn('Dispatch item not found for update:', dispatchId);
}
};
/**
* Deletes a dispatch item from the list.
* This function is exposed globally for `onclick` attributes.
* @param {string} dispatchId - The ID of the dispatch to delete.
*/
window.deleteDispatch = function(dispatchId) {
const initialLength = dispatches.length;
dispatches = dispatches.filter(dispatch => dispatch.id !== dispatchId);
if (dispatches.length < initialLength) {
updateAllViews();
} else {
console.warn('Dispatch item not found for deletion:', dispatchId);
}
};
/**
* Updates all relevant views (metrics, dashboard table, manage table, bar chart).
*/
function updateAllViews() {
updateDashboardMetrics();
renderDispatchesTable();
renderStatusBarChart();
renderManageDispatchesTable();
}
/**
* Generates and downloads a PDF of the dashboard content.
*/
function downloadDashboardPdf() {
// Ensure jsPDF is available
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
console.error("jsPDF library not loaded. Cannot generate PDF.");
alert("PDF generation library not loaded. Please try again later.");
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF('landscape'); // Use landscape for wider tables
// Set font and color for consistency
doc.setFont('helvetica');
doc.setTextColor('#1f2937'); // Dark gray
// Title
doc.setFontSize(24);
doc.text('Dispatch Dashboard', doc.internal.pageSize.getWidth() / 2, 20, { align: 'center' });
// Add a small separator
doc.setDrawColor('#d1d5db');
doc.line(20, 25, doc.internal.pageSize.getWidth() - 20, 25);
// Dashboard Metrics
doc.setFontSize(14);
let yPos = 40;
const metrics = [
{ label: 'Total Dispatches:', value: totalDispatchesEl ? totalDispatchesEl.textContent : 'N/A' },
{ label: 'Active Dispatches:', value: activeDispatchesEl ? activeDispatchesEl.textContent : 'N/A' },
{ label: 'Completed Dispatches:', value: completedDispatchesEl ? completedDispatchesEl.textContent : 'N/A' },
{ label: 'Avg. Response Time (min):', value: avgResponseTimeEl ? avgResponseTimeEl.textContent : 'N/A' }
];
metrics.forEach(metric => {
doc.text(`${metric.label} ${metric.value}`, 20, yPos);
yPos += 10;
});
yPos += 15; // Extra space before table
// Dispatches by Status (as a table in PDF)
doc.setFontSize(16);
doc.text('Dispatches by Status', 20, yPos);
yPos += 10;
const statusTableColumn = ["Status", "Count"];
const statusTableRows = [];
const statusCountsForPdf = { 'Pending': 0, 'En Route': 0, 'Completed': 0, 'Cancelled': 0 };
dispatches.forEach(item => {
if (statusCountsForPdf.hasOwnProperty(item.status)) {
statusCountsForPdf[item.status]++;
}
});
['Pending', 'En Route', 'Completed', 'Cancelled'].forEach(status => {
statusTableRows.push([status, statusCountsForPdf[status]]);
});
doc.autoTable({
head: [statusTableColumn],
body: statusTableRows,
startY: yPos,
theme: 'grid',
styles: {
fontSize: 10,
cellPadding: 3,
textColor: '#1f2937',
lineColor: '#e5e7eb',
lineWidth: 0.1
},
headStyles: {
fillColor: '#f3f4f6',
textColor: '#4b5563',
fontStyle: 'bold'
},
alternateRowStyles: {
fillColor: '#ffffff'
},
margin: { left: 20, right: 20 },
didDrawPage: function(data) {
let str = "Page " + doc.internal.getNumberOfPages();
doc.setFontSize(10);
doc.setTextColor('#6b7280');
doc.text(str, doc.internal.pageSize.getWidth() - 20, doc.internal.pageSize.getHeight() - 10, { align: 'right' });
}
});
yPos = doc.autoTable.previous.finalY + 15; // Continue from where the last table ended
// Dispatch Overview Table
doc.setFontSize(16);
doc.text('Dispatch Overview', 20, yPos);
yPos += 10;
const overviewTableColumn = ["Task Description", "Status", "Priority", "Assigned To", "Start Time", "End Time", "Response Time (min)", "Delivery Outcome"];
const overviewTableRows = [];
dispatches.forEach(dispatch => {
const responseTime = calculateTimeDifferenceInMinutes(dispatch.startTime, dispatch.endTime);
overviewTableRows.push([
dispatch.description,
dispatch.status,
dispatch.priority,
dispatch.assignedTo,
dispatch.startTime ? new Date(dispatch.startTime).toLocaleString('en-US', { dateStyle: 'short', timeStyle: 'short' }) : 'N/A',
dispatch.endTime ? new Date(dispatch.endTime).toLocaleString('en-US', { dateStyle: 'short', timeStyle: 'short' }) : 'N/A',
typeof responseTime === 'number' ? responseTime : 'N/A',
dispatch.deliveryOutcome
]);
});
doc.autoTable({
head: [overviewTableColumn],
body: overviewTableRows,
startY: yPos,
theme: 'grid',
styles: {
fontSize: 6, // Further reduced font size for more columns in landscape
cellPadding: 1,
textColor: '#1f2937',
lineColor: '#e5e7eb',
lineWidth: 0.1
},
headStyles: {
fillColor: '#f3f4f6',
textColor: '#4b5563',
fontStyle: 'bold'
},
alternateRowStyles: {
fillColor: '#ffffff'
},
margin: { left: 10, right: 10 }, // Adjusted margins to fit more content
didDrawPage: function(data) {
let str = "Page " + doc.internal.getNumberOfPages();
doc.setFontSize(10);
doc.setTextColor('#6b7280');
doc.text(str, doc.internal.pageSize.getWidth() - 20, doc.internal.pageSize.getHeight() - 10, { align: 'right' });
}
});
// Save the PDF
doc.save('dispatch_dashboard.pdf');
}
// --- Event Listeners ---
// Tab button clicks
if (dashboardTabButton) {
dashboardTabButton.addEventListener('click', function() {
switchTab(0);
});
}
if (dataConfigTabButton) {
dataConfigTabButton.addEventListener('click', function() {
switchTab(1);
});
}
// Navigation button clicks
if (prevTabButton) {
prevTabButton.addEventListener('click', function() {
if (currentTabIndex > 0) {
switchTab(currentTabIndex - 1);
}
});
}
if (nextTabButton) {
nextTabButton.addEventListener('click', function() {
if (currentTabIndex < tabs.length - 1) {
switchTab(currentTabIndex + 1);
}
});
}
// Add Dispatch button click
if (addDispatchButton) {
addDispatchButton.addEventListener('click', addDispatch);
}
// PDF Download button click
if (downloadPdfButton) {
downloadPdfButton.addEventListener('click', downloadDashboardPdf);
}
// --- Initial Render ---
// Set initial tab to Dashboard
switchTab(0);
// Populate data and update views
updateAllViews();
});