Shift Schedule Generator
Schedule Period
Team Members
Shift Types
Build Schedule Grid
Click on a cell in the grid to assign or change a shift for an employee on a specific date. Ensure period, team, and shifts are set up first.
Final Schedule Overview
This is a read-only view of your schedule. Use the 'Build Schedule' tab to make changes.
×
Assign Shift
For:
Date:
Please set a valid schedule period in the Setup tab.
"; return; } if (ssg_teamMembers.length === 0) { container.innerHTML = "Please add team members in the Setup tab.
"; return; } const table = document.createElement('table'); table.className = 'ssg-schedule-table'; const thead = table.createTHead(); const tbody = table.createTBody(); const headerRow = thead.insertRow(); headerRow.insertCell().textContent = 'Employee'; // Corner cell const dates = []; let currentDate = new Date(ssg_schedulePeriod.startDate + 'T00:00:00'); // Ensure local time const endDateObj = new Date(ssg_schedulePeriod.endDate + 'T00:00:00'); while (currentDate <= endDateObj) { dates.push(new Date(currentDate)); // Store date objects const th = headerRow.insertCell(); th.textContent = `${currentDate.getDate()}/${currentDate.getMonth() + 1}`; // DD/MM format currentDate.setDate(currentDate.getDate() + 1); } ssg_teamMembers.forEach(member => { const row = tbody.insertRow(); const nameCell = row.insertCell(); nameCell.textContent = member.name; nameCell.className = 'employee-name-cell'; dates.forEach(dateObj => { const cell = row.insertCell(); const dateString = dateObj.toISOString().split('T')[0]; const assignmentKey = `${member.id}_${dateString}`; const shiftId = ssg_assignments[assignmentKey]; const shift = ssg_shiftTypes.find(s => s.id == shiftId); if (shift) { cell.textContent = shift.abbreviation; cell.style.backgroundColor = shift.color; // Make text color readable on dark backgrounds const hexColor = shift.color.replace('#', ''); const r = parseInt(hexColor.substring(0,2), 16); const g = parseInt(hexColor.substring(2,4), 16); const b = parseInt(hexColor.substring(4,6), 16); const brightness = (r * 299 + g * 587 + b * 114) / 1000; cell.style.color = brightness > 125 ? 'black' : 'white'; } else { cell.textContent = '-'; // Unassigned } if (interactive) { cell.classList.add('shift-cell'); cell.onclick = () => ssg_openModal(member.id, member.name, dateString); } }); }); container.appendChild(table); } // Modal Logic function ssg_populateModalShiftSelect() { const select = document.getElementById('ssg_modalShiftSelect'); select.innerHTML = ''; // Clear ssg_shiftTypes.forEach(s => { const option = document.createElement('option'); option.value = s.id; option.textContent = `${s.name} (${s.abbreviation})`; select.appendChild(option); }); } function ssg_openModal(employeeId, employeeName, dateString) { document.getElementById('ssg_modalEmployeeId').value = employeeId; document.getElementById('ssg_modalEmployeeName').textContent = employeeName; document.getElementById('ssg_modalDate').textContent = dateString; document.getElementById('ssg_modalFullDateString').value = dateString; const assignmentKey = `${employeeId}_${dateString}`; const currentShiftId = ssg_assignments[assignmentKey]; document.getElementById('ssg_modalShiftSelect').value = currentShiftId || ssg_shiftTypes.find(s => s.isDefault)?.id || ''; // Default to Day Off or first if not assigned document.getElementById('ssg_shiftAssignmentModal').style.display = 'block'; } function ssg_closeModal() { document.getElementById('ssg_shiftAssignmentModal').style.display = 'none'; } function ssg_assignShiftFromModal() { const employeeId = document.getElementById('ssg_modalEmployeeId').value; const dateString = document.getElementById('ssg_modalFullDateString').value; const shiftId = document.getElementById('ssg_modalShiftSelect').value; const assignmentKey = `${employeeId}_${dateString}`; if (shiftId) { ssg_assignments[assignmentKey] = shiftId; } else { // Should not happen if select is populated delete ssg_assignments[assignmentKey]; } ssg_closeModal(); ssg_renderScheduleGrid('ssg_scheduleGridContainer', true); // Re-render interactive grid } // PDF Download function ssg_downloadPDF() { if (typeof jsPDF === 'undefined' || typeof jsPDF.API === 'undefined' || typeof jsPDF.API.autoTable === 'undefined') { alert("PDF generation library is not loaded."); console.error("jsPDF or jsPDF-AutoTable is not loaded."); return; } ssg_updateSchedulePeriod(); // Ensure period is current if (!ssg_schedulePeriod.startDate || !ssg_schedulePeriod.endDate || new Date(ssg_schedulePeriod.endDate) < new Date(ssg_schedulePeriod.startDate)) { alert("Please set a valid schedule period."); return; } const doc = new jsPDF({ orientation: 'landscape' }); // Landscape often better for schedules const primaryColorPDF = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); const textColorPDF = getComputedStyle(document.documentElement).getPropertyValue('--text-color').trim(); const whiteColorPDF = '#FFFFFF'; doc.setFontSize(18); doc.setTextColor(primaryColorPDF); doc.text("Team Shift Schedule", 14, 15); doc.setFontSize(10); doc.setTextColor(textColorPDF); doc.text(`Period: ${ssg_schedulePeriod.startDate} to ${ssg_schedulePeriod.endDate}`, 14, 22); doc.text(`Generated On: ${SSG_CONTEXT_DATE}`, 14, 27); let currentY = 35; // Shift Legend doc.setFontSize(12); doc.setTextColor(primaryColorPDF); doc.text("Shift Legend:", 14, currentY); currentY += 6; const legendCols = ["Abbr.", "Shift Name", "Time"]; const legendRows = ssg_shiftTypes.map(s => [ s.abbreviation, s.name, (s.startTime && s.endTime) ? `${s.startTime}-${s.endTime}` : '-' ]); doc.autoTable({ head: [legendCols], body: legendRows, startY: currentY, theme: 'grid', headStyles: { fillColor: primaryColorPDF, textColor: whiteColorPDF, fontSize: 8 }, styles: { fontSize: 7, cellPadding: 1.5, textColor: textColorPDF }, columnStyles: { 0: { cellWidth: 15 }, 1: { cellWidth: 'auto'}, 2: {cellWidth: 30} } }); currentY = doc.lastAutoTable.finalY + 8; // Schedule Table doc.setFontSize(12); doc.setTextColor(primaryColorPDF); doc.text("Schedule:", 14, currentY); currentY += 6; const datesHeader = ['Employee']; const datesForGrid = []; let tempDate = new Date(ssg_schedulePeriod.startDate + 'T00:00:00'); const endDateObj = new Date(ssg_schedulePeriod.endDate + 'T00:00:00'); while(tempDate <= endDateObj) { datesHeader.push(`${tempDate.getDate()}/${tempDate.getMonth()+1}`); datesForGrid.push(new Date(tempDate)); tempDate.setDate(tempDate.getDate() + 1); } const scheduleTableRows = ssg_teamMembers.map(member => { const row = [member.name]; datesForGrid.forEach(dateObj => { const dateString = dateObj.toISOString().split('T')[0]; const assignmentKey = `${member.id}_${dateString}`; const shiftId = ssg_assignments[assignmentKey]; const shift = ssg_shiftTypes.find(s => s.id == shiftId); row.push(shift ? shift.abbreviation : '-'); }); return row; }); doc.autoTable({ head: [datesHeader], body: scheduleTableRows, startY: currentY, theme: 'grid', headStyles: { fillColor: primaryColorPDF, textColor: whiteColorPDF, fontSize: 7, halign: 'center' }, styles: { fontSize: 7, cellPadding: 1, textColor: textColorPDF, halign: 'center' }, didDrawCell: function(data) { // For cell coloring if (data.section === 'body' && data.column.index > 0) { // Not employee name column const member = ssg_teamMembers[data.row.index]; const dateObj = datesForGrid[data.column.index - 1]; const dateString = dateObj.toISOString().split('T')[0]; const assignmentKey = `${member.id}_${dateString}`; const shiftId = ssg_assignments[assignmentKey]; const shift = ssg_shiftTypes.find(s => s.id == shiftId); if (shift && shift.color) { doc.setFillColor(shift.color); doc.rect(data.cell.x, data.cell.y, data.cell.width, data.cell.height, 'F'); // Adjust text color for readability on cell background const hexColor = shift.color.replace('#', ''); const r = parseInt(hexColor.substring(0,2), 16); const g = parseInt(hexColor.substring(2,4), 16); const b = parseInt(hexColor.substring(4,6), 16); const brightness = (r * 299 + g * 587 + b * 114) / 1000; const cellTextColor = brightness > 125 ? [0,0,0] : [255,255,255]; doc.setTextColor(cellTextColor[0], cellTextColor[1], cellTextColor[2]); // Note: autoTable might redraw text, so this text color change might be tricky. // It's often better to handle text color in `addPageContent` or prepare data with styles. // For simplicity here, default text color is used by autoTable after fill. // A more robust solution involves deeper customization of jsPDF-autoTable drawing. } } }, // Make employee name column wider and stick if possible (not native to autotable) columnStyles: { 0: { fontStyle: 'bold', cellWidth: 40, halign: 'left' } } }); doc.save(`Team_Shift_Schedule_${ssg_schedulePeriod.startDate}_to_${ssg_schedulePeriod.endDate}.pdf`); }