No performances scheduled yet.
`;
return;
}
// Check for conflicts and mark rows
const scheduleWithConflicts = sortedSchedule.map(slot => {
const slotStart = timeToMinutes(slot.time);
const slotEnd = slotStart + slot.duration;
const isConflict = sortedSchedule.some(other => {
if (slot.id === other.id || slot.stageId !== other.stageId) return false;
const otherStart = timeToMinutes(other.time);
const otherEnd = otherStart + other.duration;
// Check for overlap: [Start1 < End2] AND [End1 > Start2]
return (slotStart < otherEnd && slotEnd > otherStart);
});
return { ...slot, isConflict };
});
scheduleWithConflicts.forEach(slot => {
const tr = document.createElement('tr');
tr.dataset.id = slot.id;
tr.classList.toggle('bg-red-100', slot.isConflict);
tr.classList.toggle('border-red-500', slot.isConflict);
const endTime = calculateEndTime(slot.time, slot.duration);
const stageName = findName(slot.stageId, stages);
const performerName = findName(slot.performerId, performers);
tr.innerHTML = `
${slot.time} |
${endTime} |
${stageName} |
${performerName} |
${slot.duration} |
${slot.isConflict ? `` : ''}
|
`;
scheduleTableBody.appendChild(tr);
});
// Update button state (for edit mode)
scheduleSaveBtn.classList.toggle('rfsb-btn-primary', !editIdInput.value);
scheduleSaveBtn.classList.toggle('rfsb-btn-secondary', !!editIdInput.value);
scheduleSaveBtn.innerHTML = editIdInput.value ? '
Update Slot' : '
Add Slot';
}
/** Adds/Updates a performance slot */
window.rfsbAddPerformance = () => {
if (!scheduleForm || !scheduleTime || !scheduleDuration || !scheduleStageSelect || !schedulePerformerSelect) return;
const id = editIdInput.value;
const time = scheduleTime.value;
const duration = parseInt(scheduleDuration.value);
const stageId = scheduleStageSelect.value;
const performerId = schedulePerformerSelect.value;
if (duration <= 0 || !stageId || !performerId) {
showMessage("Invalid input. Check duration and selections.", true);
return;
}
const newSlot = { id: id || 'sch' + Date.now(), time, duration, stageId, performerId };
// Conflict Check (Mandatory before adding)
const newSlotStart = timeToMinutes(time);
const newSlotEnd = newSlotStart + duration;
const existingConflict = schedule.some(slot => {
// Ignore self if editing
if (slot.id === id) return false;
// Only check slots on the same stage
if (slot.stageId !== stageId) return false;
const otherStart = timeToMinutes(slot.time);
const otherEnd = otherStart + slot.duration;
// Check for overlap: [Start1 < End2] AND [End1 > Start2]
return (newSlotStart < otherEnd && newSlotEnd > otherStart);
});
if (existingConflict) {
showMessage("Conflict detected! This performance overlaps with an existing schedule slot on the same stage.", true);
return;
}
if (id) {
// Update existing
const index = schedule.findIndex(slot => slot.id === id);
if (index !== -1) schedule[index] = newSlot;
showMessage("Schedule slot updated successfully.", false);
} else {
// Add new
schedule.push(newSlot);
showMessage("Performance added to schedule.", false);
}
scheduleForm.reset();
editIdInput.value = ''; // Clear edit mode
renderScheduleTable();
};
/** Populates the form for editing */
window.rfsbEditSlot = (id) => {
const slot = schedule.find(s => s.id === id);
if (!slot) return;
editIdInput.value = slot.id;
scheduleTime.value = slot.time;
scheduleDuration.value = slot.duration;
scheduleStageSelect.value = slot.stageId;
schedulePerformerSelect.value = slot.performerId;
scheduleSaveBtn.classList.replace('rfsb-btn-primary', 'rfsb-btn-secondary'); // Visual hint of edit mode
scheduleSaveBtn.innerHTML = '
Update Slot';
showMessage("Editing existing slot.", false);
};
/** Deletes a performance slot */
window.rfsbDeleteSlot = (id) => {
if (!confirm('Delete this schedule slot?')) return;
schedule = schedule.filter(slot => slot.id !== id);
if (editIdInput.value === id) pscClearForm(); // Clear form if slot being edited was deleted
renderScheduleTable();
};
/** Clears all schedule slots */
window.clearSchedule = () => {
if (schedule.length === 0) { showMessage("Schedule is already empty.", false); return; }
if (!confirm('Are you sure you want to clear the entire schedule?')) return;
schedule = [];
renderScheduleTable();
pscClearForm();
};
// --- TAB AND NAV LOGIC ---
/** Switches tabs */
window.rfsbShowTab = (tabId, element) => {
if (!container || !tabLinks) return;
container.querySelectorAll('.rfsb-tab-content').forEach(tab => tab.classList.remove('rfsb-active'));
container.querySelectorAll('.rfsb-tab-link').forEach(link => link.classList.remove('rfsb-active'));
const tabToShow = container.querySelector('#' + tabId);
if (tabToShow) tabToShow.classList.add('rfsb-active');
if (element) element.classList.add('rfsb-active');
currentTabId = tabId;
updateNavButtons();
// Re-render table/selects when switching views
if (tabId === 'rfsb-tab-setup') {
renderSetupLists();
} else if (tabId === 'rfsb-tab-scheduler') {
renderScheduleTable();
}
};
/** Handles nav buttons */
window.rfsbNavigateTabs = (isNext) => {
if (!container || !tabLinks) return;
// Simple validation check before proceeding from Setup
if (currentTabId === 'rfsb-tab-setup' && isNext && (performers.length === 0 || stages.length === 0)) {
showMessage("Please add at least one Performer and one Stage before scheduling.", true);
return;
}
const targetTabId = isNext ? 'rfsb-tab-scheduler' : 'rfsb-tab-setup';
const targetTabLink = container.querySelector(`.rfsb-tab-link[onclick*="'${targetTabId}'"]`);
if(targetTabLink) targetTabLink.click();
};
/** Updates nav button states */
function updateNavButtons() {
if (!prevBtn || !nextBtn) return;
prevBtn.disabled = currentTabId === 'rfsb-tab-setup';
nextBtn.disabled = currentTabId === 'rfsb-tab-scheduler';
}
// --- PDF EXPORT ---
window.rfsbDownloadPDF = () => {
const jsPDF = window.jsPDF;
if (typeof jsPDF === 'undefined' || !jsPDF.autoTable) { showMessage("PDF library not loaded.", true); return; }
if (schedule.length === 0) { showMessage("Schedule is empty. Add slots before downloading.", true); return; }
const sortedSchedule = [...schedule].sort((a, b) => a.time.localeCompare(b.time));
try {
const doc = new jsPDF({
orientation: 'l', // Landscape for better schedule display
unit: 'pt',
format: 'a4'
});
const pageMargin = 40;
const pageWidth = doc.internal.pageSize.getWidth();
let y = pageMargin;
// --- PDF Colors ---
const PRIMARY_COLOR_HEX = '#0d9488'; // Teal
const ACCENT_COLOR_HEX = '#fbbf24'; // Gold
const DARK_GRAY_HEX = '#4b5563';
// 1. Title Block
doc.setFontSize(22);
doc.setFont(undefined, 'bold');
doc.setTextColor(PRIMARY_COLOR_HEX);
doc.text("Renaissance Faire Master Schedule", pageWidth / 2, y, { align: 'center' });
y += 30;
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.setTextColor(DARK_GRAY_HEX);
doc.text(`Schedule Date: ${new Date().toLocaleDateString('en-US')}`, pageMargin, y);
doc.text(`Total Slots: ${schedule.length}`, pageWidth - pageMargin, y, { align: 'right' });
y += 20;
// 2. Schedule Table
const tableHead = [["Time", "End Time", "Stage", "Performer / Troupe", "Duration (min)"]];
const tableBody = sortedSchedule.map(slot => {
const endTime = calculateEndTime(slot.time, slot.duration);
return [
slot.time,
endTime,
findName(slot.stageId, stages),
findName(slot.performerId, performers),
slot.duration.toString()
];
});
doc.autoTable({
startY: y,
head: tableHead,
body: tableBody,
theme: 'striped',
styles: { fontSize: 9, cellPadding: 4, valign: 'middle' },
headStyles: { fillColor: PRIMARY_COLOR_HEX, textColor: 255 },
columnStyles: {
0: { cellWidth: 50, fontStyle: 'bold' }, // Time
1: { cellWidth: 50 }, // End Time
4: { cellWidth: 60, halign: 'center' } // Duration
}
});
doc.save(`RenFaire_Schedule_${new Date().toLocaleDateString('en-US').replace(/\//g, '-')}.pdf`);
showMessage("PDF Master Schedule downloaded successfully!", false);
} catch (e) {
console.error("Error generating PDF:", e);
showMessage("An error occurred while generating the PDF. Check console for details.", true);
}
};
// --- Initial Load ---
renderSetupLists();
renderScheduleTable();
updateNavButtons();
});