diff --git a/output/block_shifts/timetable.css b/output/block_shifts/timetable.css
index 8fda8b5..11e56f8 100644
--- a/output/block_shifts/timetable.css
+++ b/output/block_shifts/timetable.css
@@ -1,6 +1,26 @@
html {
+ --bg-color: white;
+ --text-color: black;
+ --table-bg: #f9f9f9;
+ --border-color: #ddd;
+ --header-bg: #e0e0e0;
+ --highlight-color: #ffff99;
+ --worker-bg: #f0f0f0;
+}
- /* color: red; */
+html.dark-mode {
+ --bg-color: #121212;
+ --text-color: #e0e0e0;
+ --table-bg: #1e1e1e;
+ --border-color: #333;
+ --header-bg: #2a2a2a;
+ --highlight-color: #333300;
+ --worker-bg: #2a2a2a;
+}
+
+body {
+ background-color: var(--bg-color);
+ color: var(--text-color);
}
table {
@@ -14,7 +34,8 @@ table {
position: relative; */
border-collapse: separate;
border-spacing: 0;
-
+ background-color: var(--table-bg);
+ border: 1px solid var(--border-color);
}
#main-table {
@@ -72,12 +93,12 @@ th {
}
.table-div tr:hover {
- background-color: lightblue;
+ background-color: var(--highlight-color);
}
.table-div tr:hover .unavailable {
- background-color: lightgray;
+ background-color: var(--worker-bg);
}
.Sat {
diff --git a/output/block_shifts/timetable.js b/output/block_shifts/timetable.js
index a980ff9..112736a 100644
--- a/output/block_shifts/timetable.js
+++ b/output/block_shifts/timetable.js
@@ -513,6 +513,311 @@ $("#gen-table").each(function () {
});
});
+// ICS Export functionality
+let customShiftTimes = {}; // Will store user-defined times
+
+function getShiftTimes() {
+ // Merge default with custom
+ const defaultTimes = {
+ 'night_weekday': { start: '20:00', end: '08:15', nextDay: true },
+ 'night_weekend': { start: '20:00', end: '08:15', nextDay: true },
+ 'exeter_twilight': { start: '14:00', end: '02:30', nextDay: true },
+ 'truro_twilight': { start: '14:00', end: '02:30', nextDay: true },
+ 'torbay_twilight': { start: '14:00', end: '02:30', nextDay: true },
+ 'plymouth_twilight': { start: '14:00', end: '02:30', nextDay: true },
+ 'weekend_exeter': { start: '08:00', end: '20:30', nextDay: false },
+ 'weekend_truro': { start: '08:00', end: '20:30', nextDay: false },
+ 'weekend_torbay': { start: '08:00', end: '20:30', nextDay: false },
+ 'weekend_plymouth1': { start: '08:00', end: '16:00', nextDay: false },
+ 'weekend_plymouth2': { start: '16:00', end: '00:00', nextDay: false },
+ 'plymouth_bank_holidays': { start: '08:00', end: '16:00', nextDay: false },
+ };
+ return { ...defaultTimes, ...customShiftTimes };
+}
+
+function generateICS(shiftType, shiftData) {
+ let icsContent = `BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Rota Generator//EN
+`;
+
+ const shiftTimes = getShiftTimes();
+ const time = shiftTimes[shiftType] || { start: '09:00', end: '17:00', nextDay: false };
+
+ shiftData.forEach(entry => {
+ const date = entry.date;
+ const workerName = entry.worker;
+
+ const startDate = new Date(date + 'T' + time.start + ':00');
+ let endDate = new Date(date + 'T' + time.end + ':00');
+ if (time.nextDay) {
+ endDate.setDate(endDate.getDate() + 1);
+ }
+
+ const formatDate = (d) => d.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
+
+ const shiftTitle = shiftType.replace(/_/g, ' ').toUpperCase();
+ const summary = workerName ? `${shiftTitle} - ${workerName}` : shiftTitle;
+ const description = workerName ?
+ `Shift: ${shiftType}\nWorker: ${workerName}` :
+ `Shift: ${shiftType}`;
+
+ icsContent += `BEGIN:VEVENT
+UID:${workerName ? workerName + '-' : ''}${shiftType}-${date}@rota
+DTSTAMP:${formatDate(new Date())}
+DTSTART:${formatDate(startDate)}
+DTEND:${formatDate(endDate)}
+SUMMARY:${summary}
+DESCRIPTION:${description}
+END:VEVENT
+`;
+ });
+
+ icsContent += 'END:VCALENDAR';
+ return icsContent;
+}
+
+function downloadICS(filename, content) {
+ const blob = new Blob([content], { type: 'text/calendar;charset=utf-8' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+}
+
+// Collect shift dates and worker assignments
+const shiftDates = {};
+const workerShifts = {};
+
+$('#main-table tr.worker-row').each(function() {
+ const workerName = $(this).find('td.worker').attr('data-worker');
+ if (workerName) {
+ workerShifts[workerName] = {};
+
+ $(this).find('td.rota-day').each(function() {
+ const shift = $(this).attr('data-shift');
+ const date = $(this).attr('data-date');
+ if (shift && shift !== '' && date) {
+ const shifts = shift.split(',').map(s => s.trim()).filter(s => s);
+ shifts.forEach(s => {
+ // Collect by shift type with worker info
+ if (!shiftDates[s]) {
+ shiftDates[s] = [];
+ }
+ // Check if we already have this date for this shift type
+ const existingEntry = shiftDates[s].find(entry => entry.date === date);
+ if (!existingEntry) {
+ shiftDates[s].push({ date: date, worker: workerName });
+ }
+
+ // Collect by worker
+ if (!workerShifts[workerName][s]) {
+ workerShifts[workerName][s] = [];
+ }
+ if (!workerShifts[workerName][s].includes(date)) {
+ workerShifts[workerName][s].push(date);
+ }
+ });
+ }
+ });
+ }
+});
+
+// Add ICS export buttons
+const icsButtonsDiv = $('
Export ICS Files
');
+const setTimesButton = $(``);
+icsButtonsDiv.append(setTimesButton);
+
+// Shift type exports (with worker info)
+const shiftExportsDiv = $('Export by Shift Type
');
+Object.keys(shiftDates).sort().forEach(shiftType => {
+ const button = $(``);
+ button.on('click', () => {
+ const ics = generateICS(shiftType, shiftDates[shiftType]);
+ downloadICS(`${shiftType}.ics`, ics);
+ });
+ shiftExportsDiv.append(button);
+});
+icsButtonsDiv.append(shiftExportsDiv);
+
+// Worker exports
+const workerExportsDiv = $('Export by Worker
');
+Object.keys(workerShifts).sort().forEach(workerName => {
+ const workerShiftTypes = Object.keys(workerShifts[workerName]);
+ if (workerShiftTypes.length > 0) {
+ const button = $(``);
+ button.on('click', () => {
+ showWorkerShiftSelectionModal(workerName, workerShifts[workerName]);
+ });
+ workerExportsDiv.append(button);
+ }
+});
+icsButtonsDiv.append(workerExportsDiv);
+
+$('#export-div').append(icsButtonsDiv);
+
+// Function to show worker shift selection modal
+function showWorkerShiftSelectionModal(workerName, workerShiftData) {
+ const checkboxesDiv = $('#worker-shift-checkboxes');
+ checkboxesDiv.empty();
+
+ $('#worker-shift-modal-title').text(`Select Shifts for ${workerName}`);
+
+ // Create checkboxes for each shift type
+ Object.keys(workerShiftData).sort().forEach(shiftType => {
+ const dates = workerShiftData[shiftType];
+ const checkboxId = `shift-${shiftType}`;
+ const checkbox = $(`
+
+
+
+
+ `);
+ checkboxesDiv.append(checkbox);
+ });
+
+ // Store worker data for export
+ $('#worker-shift-export').data('workerName', workerName);
+ $('#worker-shift-export').data('workerShiftData', workerShiftData);
+
+ // Show modal
+ workerShiftModal.show();
+}
+
+// Modal for worker shift selection
+const workerShiftModal = $(`
+
+
Select Shifts for Export
+
+
+
+
+
+
+`);
+$('body').append(workerShiftModal);
+
+// Modal for setting shift times
+const shiftTimesModal = $(`
+
+
Set Shift Times
+
+
+
+
+
+
+`);
+$("body").append(shiftTimesModal);
+
+$('#set-shift-times').on('click', function() {
+ $('#shift-times-modal').show();
+ populateShiftTimesModal();
+});
+
+$('#close-shift-times').on('click', function() {
+ $('#shift-times-modal').hide();
+});
+
+$('#save-shift-times').on('click', function() {
+ saveShiftTimes();
+ $('#shift-times-modal').hide();
+});
+
+// Worker shift modal event handlers
+$('#worker-shift-cancel').on('click', function() {
+ workerShiftModal.hide();
+});
+
+$('#worker-shift-export').on('click', function() {
+ const workerName = $(this).data('workerName');
+ const workerShiftData = $(this).data('workerShiftData');
+
+ // Get selected shift types
+ const selectedShifts = [];
+ $('#worker-shift-checkboxes input[type="checkbox"]:checked').each(function() {
+ const shiftType = $(this).attr('id').replace('shift-', '');
+ selectedShifts.push(shiftType);
+ });
+
+ if (selectedShifts.length === 0) {
+ alert('Please select at least one shift type to export.');
+ return;
+ }
+
+ // Generate ICS for selected shifts
+ let icsContent = `BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Rota Generator//EN
+`;
+
+ selectedShifts.forEach(shiftType => {
+ const dates = workerShiftData[shiftType];
+ const shiftTimes = getShiftTimes();
+ const time = shiftTimes[shiftType] || { start: '09:00', end: '17:00', nextDay: false };
+
+ dates.forEach(date => {
+ const startDate = new Date(date + 'T' + time.start + ':00');
+ let endDate = new Date(date + 'T' + time.end + ':00');
+ if (time.nextDay) {
+ endDate.setDate(endDate.getDate() + 1);
+ }
+
+ const formatDate = (d) => d.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
+
+ const shiftTitle = shiftType.replace(/_/g, ' ').toUpperCase();
+
+ icsContent += `BEGIN:VEVENT
+UID:${workerName}-${shiftType}-${date}@rota
+DTSTAMP:${formatDate(new Date())}
+DTSTART:${formatDate(startDate)}
+DTEND:${formatDate(endDate)}
+SUMMARY:${shiftTitle} - ${workerName}
+DESCRIPTION:Shift: ${shiftType}\nWorker: ${workerName}
+END:VEVENT
+`;
+ });
+ });
+
+ icsContent += 'END:VCALENDAR';
+ downloadICS(`${workerName.replace(/[^a-zA-Z0-9]/g, '_')}.ics`, icsContent);
+
+ workerShiftModal.hide();
+});
+
+function populateShiftTimesModal() {
+ const controls = $('#shift-times-controls');
+ controls.empty();
+ const shiftTimes = getShiftTimes();
+ Object.keys(shiftDates).sort().forEach(shiftType => {
+ const time = shiftTimes[shiftType] || { start: '09:00', end: '17:00', nextDay: false };
+ const div = $(`
+
+
+
+
+
+
+ `);
+ controls.append(div);
+ });
+}
+
+function saveShiftTimes() {
+ customShiftTimes = {};
+ $('#shift-times-controls .shift-start').each(function() {
+ const shift = $(this).data('shift');
+ const start = $(this).val();
+ const end = $(`input.shift-end[data-shift="${shift}"]`).val();
+ const nextDay = $(`input.shift-nextday[data-shift="${shift}"]`).is(':checked');
+ customShiftTimes[shift] = { start, end, nextDay };
+ });
+}
+
function hsv2rgb(h, s, v) {
// adapted from http://schinckel.net/2012/01/10/hsv-to-rgb-in-javascript/
var rgb, i, data = [];
diff --git a/output/timetable.css b/output/timetable.css
index fe8a251..cfed804 100644
--- a/output/timetable.css
+++ b/output/timetable.css
@@ -1,6 +1,26 @@
html {
+ --bg-color: white;
+ --text-color: black;
+ --table-bg: #f9f9f9;
+ --border-color: #ddd;
+ --header-bg: #e0e0e0;
+ --highlight-color: #ffff99;
+ --worker-bg: #f0f0f0;
+}
- /* color: red; */
+html.dark-mode {
+ --bg-color: #121212;
+ --text-color: #e0e0e0;
+ --table-bg: #1e1e1e;
+ --border-color: #333;
+ --header-bg: #2a2a2a;
+ --highlight-color: #333300;
+ --worker-bg: #2a2a2a;
+}
+
+body {
+ background-color: var(--bg-color);
+ color: var(--text-color);
}
table {
@@ -14,7 +34,8 @@ table {
position: relative; */
border-collapse: separate;
border-spacing: 0;
-
+ background-color: var(--table-bg);
+ border: 1px solid var(--border-color);
}
#main-table {
@@ -72,12 +93,12 @@ th {
}
#main-table tr:hover {
- background-color: lightblue;
+ background-color: var(--highlight-color);
}
#main-table tr:hover .unavailable {
- background-color: lightgray;
+ background-color: var(--worker-bg);
}
.Sat {
diff --git a/output/timetable.js b/output/timetable.js
index 1d54b9f..89547a7 100644
--- a/output/timetable.js
+++ b/output/timetable.js
@@ -1162,6 +1162,311 @@ $("#gen-table").each(function () {
});
});
+// ICS Export functionality
+let customShiftTimes = {}; // Will store user-defined times
+
+function getShiftTimes() {
+ // Merge default with custom
+ const defaultTimes = {
+ 'night_weekday': { start: '20:00', end: '08:15', nextDay: true },
+ 'night_weekend': { start: '20:00', end: '08:15', nextDay: true },
+ 'exeter_twilight': { start: '14:00', end: '02:30', nextDay: true },
+ 'truro_twilight': { start: '14:00', end: '02:30', nextDay: true },
+ 'torbay_twilight': { start: '14:00', end: '02:30', nextDay: true },
+ 'plymouth_twilight': { start: '14:00', end: '02:30', nextDay: true },
+ 'weekend_exeter': { start: '08:00', end: '20:30', nextDay: false },
+ 'weekend_truro': { start: '08:00', end: '20:30', nextDay: false },
+ 'weekend_torbay': { start: '08:00', end: '20:30', nextDay: false },
+ 'weekend_plymouth1': { start: '08:00', end: '16:00', nextDay: false },
+ 'weekend_plymouth2': { start: '16:00', end: '00:00', nextDay: false },
+ 'plymouth_bank_holidays': { start: '08:00', end: '16:00', nextDay: false },
+ };
+ return { ...defaultTimes, ...customShiftTimes };
+}
+
+function generateICS(shiftType, shiftData) {
+ let icsContent = `BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Rota Generator//EN
+`;
+
+ const shiftTimes = getShiftTimes();
+ const time = shiftTimes[shiftType] || { start: '09:00', end: '17:00', nextDay: false };
+
+ shiftData.forEach(entry => {
+ const date = entry.date;
+ const workerName = entry.worker;
+
+ const startDate = new Date(date + 'T' + time.start + ':00');
+ let endDate = new Date(date + 'T' + time.end + ':00');
+ if (time.nextDay) {
+ endDate.setDate(endDate.getDate() + 1);
+ }
+
+ const formatDate = (d) => d.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
+
+ const shiftTitle = shiftType.replace(/_/g, ' ').toUpperCase();
+ const summary = workerName ? `${shiftTitle} - ${workerName}` : shiftTitle;
+ const description = workerName ?
+ `Shift: ${shiftType}\nWorker: ${workerName}` :
+ `Shift: ${shiftType}`;
+
+ icsContent += `BEGIN:VEVENT
+UID:${workerName ? workerName + '-' : ''}${shiftType}-${date}@rota
+DTSTAMP:${formatDate(new Date())}
+DTSTART:${formatDate(startDate)}
+DTEND:${formatDate(endDate)}
+SUMMARY:${summary}
+DESCRIPTION:${description}
+END:VEVENT
+`;
+ });
+
+ icsContent += 'END:VCALENDAR';
+ return icsContent;
+}
+
+function downloadICS(filename, content) {
+ const blob = new Blob([content], { type: 'text/calendar;charset=utf-8' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+}
+
+// Collect shift dates and worker assignments
+const shiftDates = {};
+const workerShifts = {};
+
+$('#main-table tr.worker-row').each(function() {
+ const workerName = $(this).find('td.worker').attr('data-worker');
+ if (workerName) {
+ workerShifts[workerName] = {};
+
+ $(this).find('td.rota-day').each(function() {
+ const shift = $(this).attr('data-shift');
+ const date = $(this).attr('data-date');
+ if (shift && shift !== '' && date) {
+ const shifts = shift.split(',').map(s => s.trim()).filter(s => s);
+ shifts.forEach(s => {
+ // Collect by shift type with worker info
+ if (!shiftDates[s]) {
+ shiftDates[s] = [];
+ }
+ // Check if we already have this date for this shift type
+ const existingEntry = shiftDates[s].find(entry => entry.date === date);
+ if (!existingEntry) {
+ shiftDates[s].push({ date: date, worker: workerName });
+ }
+
+ // Collect by worker
+ if (!workerShifts[workerName][s]) {
+ workerShifts[workerName][s] = [];
+ }
+ if (!workerShifts[workerName][s].includes(date)) {
+ workerShifts[workerName][s].push(date);
+ }
+ });
+ }
+ });
+ }
+});
+
+// Add ICS export buttons
+const icsButtonsDiv = $('Export ICS Files
');
+const setTimesButton = $(``);
+icsButtonsDiv.append(setTimesButton);
+
+// Shift type exports (with worker info)
+const shiftExportsDiv = $('Export by Shift Type
');
+Object.keys(shiftDates).sort().forEach(shiftType => {
+ const button = $(``);
+ button.on('click', () => {
+ const ics = generateICS(shiftType, shiftDates[shiftType]);
+ downloadICS(`${shiftType}.ics`, ics);
+ });
+ shiftExportsDiv.append(button);
+});
+icsButtonsDiv.append(shiftExportsDiv);
+
+// Worker exports
+const workerExportsDiv = $('Export by Worker
');
+Object.keys(workerShifts).sort().forEach(workerName => {
+ const workerShiftTypes = Object.keys(workerShifts[workerName]);
+ if (workerShiftTypes.length > 0) {
+ const button = $(``);
+ button.on('click', () => {
+ showWorkerShiftSelectionModal(workerName, workerShifts[workerName]);
+ });
+ workerExportsDiv.append(button);
+ }
+});
+icsButtonsDiv.append(workerExportsDiv);
+
+$('#export-div').append(icsButtonsDiv);
+
+// Function to show worker shift selection modal
+function showWorkerShiftSelectionModal(workerName, workerShiftData) {
+ const checkboxesDiv = $('#worker-shift-checkboxes');
+ checkboxesDiv.empty();
+
+ $('#worker-shift-modal-title').text(`Select Shifts for ${workerName}`);
+
+ // Create checkboxes for each shift type
+ Object.keys(workerShiftData).sort().forEach(shiftType => {
+ const dates = workerShiftData[shiftType];
+ const checkboxId = `shift-${shiftType}`;
+ const checkbox = $(`
+
+
+
+
+ `);
+ checkboxesDiv.append(checkbox);
+ });
+
+ // Store worker data for export
+ $('#worker-shift-export').data('workerName', workerName);
+ $('#worker-shift-export').data('workerShiftData', workerShiftData);
+
+ // Show modal
+ workerShiftModal.show();
+}
+
+// Modal for worker shift selection
+const workerShiftModal = $(`
+
+
Select Shifts for Export
+
+
+
+
+
+
+`);
+$('body').append(workerShiftModal);
+
+// Modal for setting shift times
+const shiftTimesModal = $(`
+
+
Set Shift Times
+
+
+
+
+
+
+`);
+$("body").append(shiftTimesModal);
+
+$('#set-shift-times').on('click', function() {
+ $('#shift-times-modal').show();
+ populateShiftTimesModal();
+});
+
+$('#close-shift-times').on('click', function() {
+ $('#shift-times-modal').hide();
+});
+
+$('#save-shift-times').on('click', function() {
+ saveShiftTimes();
+ $('#shift-times-modal').hide();
+});
+
+// Worker shift modal event handlers
+$('#worker-shift-cancel').on('click', function() {
+ workerShiftModal.hide();
+});
+
+$('#worker-shift-export').on('click', function() {
+ const workerName = $(this).data('workerName');
+ const workerShiftData = $(this).data('workerShiftData');
+
+ // Get selected shift types
+ const selectedShifts = [];
+ $('#worker-shift-checkboxes input[type="checkbox"]:checked').each(function() {
+ const shiftType = $(this).attr('id').replace('shift-', '');
+ selectedShifts.push(shiftType);
+ });
+
+ if (selectedShifts.length === 0) {
+ alert('Please select at least one shift type to export.');
+ return;
+ }
+
+ // Generate ICS for selected shifts
+ let icsContent = `BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Rota Generator//EN
+`;
+
+ selectedShifts.forEach(shiftType => {
+ const dates = workerShiftData[shiftType];
+ const shiftTimes = getShiftTimes();
+ const time = shiftTimes[shiftType] || { start: '09:00', end: '17:00', nextDay: false };
+
+ dates.forEach(date => {
+ const startDate = new Date(date + 'T' + time.start + ':00');
+ let endDate = new Date(date + 'T' + time.end + ':00');
+ if (time.nextDay) {
+ endDate.setDate(endDate.getDate() + 1);
+ }
+
+ const formatDate = (d) => d.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
+
+ const shiftTitle = shiftType.replace(/_/g, ' ').toUpperCase();
+
+ icsContent += `BEGIN:VEVENT
+UID:${workerName}-${shiftType}-${date}@rota
+DTSTAMP:${formatDate(new Date())}
+DTSTART:${formatDate(startDate)}
+DTEND:${formatDate(endDate)}
+SUMMARY:${shiftTitle} - ${workerName}
+DESCRIPTION:Shift: ${shiftType}\nWorker: ${workerName}
+END:VEVENT
+`;
+ });
+ });
+
+ icsContent += 'END:VCALENDAR';
+ downloadICS(`${workerName.replace(/[^a-zA-Z0-9]/g, '_')}.ics`, icsContent);
+
+ workerShiftModal.hide();
+});
+
+function populateShiftTimesModal() {
+ const controls = $('#shift-times-controls');
+ controls.empty();
+ const shiftTimes = getShiftTimes();
+ Object.keys(shiftDates).sort().forEach(shiftType => {
+ const time = shiftTimes[shiftType] || { start: '09:00', end: '17:00', nextDay: false };
+ const div = $(`
+
+
+
+
+
+
+ `);
+ controls.append(div);
+ });
+}
+
+function saveShiftTimes() {
+ customShiftTimes = {};
+ $('#shift-times-controls .shift-start').each(function() {
+ const shift = $(this).data('shift');
+ const start = $(this).val();
+ const end = $(`input.shift-end[data-shift="${shift}"]`).val();
+ const nextDay = $(`input.shift-nextday[data-shift="${shift}"]`).is(':checked');
+ customShiftTimes[shift] = { start, end, nextDay };
+ });
+}
+
function hsv2rgb(h, s, v) {
// adapted from http://schinckel.net/2012/01/10/hsv-to-rgb-in-javascript/
var rgb, i, data = [];
@@ -1398,4 +1703,26 @@ daysOfWeek.forEach(day => {
$(function() {
updateAllHighlights();
+});
+
+// --- Dark Mode Toggle ---
+$(function() {
+ // Add dark mode toggle button to the page
+ const darkModeButton = $('');
+ $('body').append(darkModeButton);
+
+ // Check for saved theme preference or default to light mode
+ const currentTheme = localStorage.getItem('theme') || 'light';
+ if (currentTheme === 'dark') {
+ $('html').addClass('dark-mode');
+ darkModeButton.text('☀️ Light Mode');
+ }
+
+ // Toggle dark mode
+ darkModeButton.on('click', function() {
+ $('html').toggleClass('dark-mode');
+ const isDark = $('html').hasClass('dark-mode');
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
+ $(this).text(isDark ? '☀️ Light Mode' : '🌙 Dark Mode');
+ });
});
\ No newline at end of file
diff --git a/test_rota.py b/test_rota.py
new file mode 100644
index 0000000..e5bc94b
--- /dev/null
+++ b/test_rota.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+"""
+Test script to generate a small rota for testing ICS export functionality.
+"""
+
+import datetime
+import os
+import sys
+from pathlib import Path
+
+# Add the project root to Python path
+sys.path.insert(0, str(Path(__file__).parent))
+
+from rota_generator.shifts import RotaBuilder, SingleShift, WorkerRequirement, days, NightConstraint, PreShiftConstraint
+from rota_generator.workers import Worker, NotAvailableToWork, NonWorkingDays
+
+def create_test_rota():
+ """Create a small test rota with a few shifts and workers."""
+
+ # Define sites
+ sites = ("truro", "exeter", "plymouth")
+
+ # Create rota builder
+ rota_start_date = datetime.date(2025, 12, 29) # Start from today-ish
+ Rota = RotaBuilder(
+ rota_start_date,
+ weeks_to_rota=4, # Small rota for testing
+ balance_offset_modifier=2,
+ name="test_rota",
+ )
+
+ # Add some shifts
+ Rota.add_shifts(
+ SingleShift(
+ sites=("truro", "exeter", "plymouth"),
+ name="night_weekday",
+ length=12.25,
+ days=days[:5], # Mon-Fri
+ balance_offset=3.9,
+ workers_required=1,
+ force_as_block=True,
+ constraints=[NightConstraint(), PreShiftConstraint(days=1)],
+ ),
+ SingleShift(
+ sites=("truro", "exeter", "plymouth"),
+ name="weekend_truro",
+ length=12.5,
+ days=days[5:], # Sat-Sun
+ balance_offset=3,
+ workers_required=1,
+ force_as_block=True,
+ constraints=[PreShiftConstraint(days=1)],
+ ),
+ SingleShift(
+ sites=("plymouth",),
+ name="plymouth_twilight",
+ length=12.5,
+ days=days[:5],
+ balance_offset=4,
+ workers_required=1,
+ ),
+ )
+
+ # Add some test workers
+ workers = [
+ Worker(
+ name="Alice",
+ site="truro",
+ fte=100, # 1.0 FTE
+ start_date=rota_start_date,
+ end_date=None,
+ grade=1,
+ initials="AL",
+ ),
+ Worker(
+ name="Bob",
+ site="exeter",
+ fte=100, # 1.0 FTE
+ start_date=rota_start_date,
+ end_date=None,
+ grade=1,
+ initials="BO",
+ ),
+ Worker(
+ name="Charlie",
+ site="plymouth",
+ fte=100, # 1.0 FTE
+ start_date=rota_start_date,
+ end_date=None,
+ grade=1,
+ initials="CH",
+ ),
+ Worker(
+ name="Chester",
+ site="plymouth",
+ fte=100, # 1.0 FTE
+ start_date=rota_start_date,
+ end_date=None,
+ grade=1,
+ initials="CH",
+ ),
+ Worker(
+ name="Dino",
+ site="truro",
+ fte=100, # 0.8 FTE
+ start_date=rota_start_date,
+ end_date=None,
+ grade=2,
+ initials="DI",
+ ),
+ Worker(
+ name="Diana",
+ site="truro",
+ fte=80, # 0.8 FTE
+ start_date=rota_start_date,
+ end_date=None,
+ grade=2,
+ initials="DI",
+ ),
+ ]
+
+ Rota.add_workers(workers)
+
+ # Build and solve
+ Rota.build_and_solve(options={"ratio": 0.1, "seconds": 30, "threads": 10})
+
+ return Rota
+
+def generate_html_output(rota, output_dir="output/test"):
+ """Generate HTML output for the rota."""
+
+ os.makedirs(output_dir, exist_ok=True)
+
+ # Use the built-in export method
+ rota.export_rota_to_html(filename="index")
+
+ html_path = os.path.join("output", "test", "index.html")
+
+ print(f"Test rota HTML generated at: {html_path}")
+ print("Open this file in a web browser to test the ICS export functionality.")
+
+ return html_path
+
+if __name__ == "__main__":
+ print("Generating test rota...")
+
+ try:
+ rota = create_test_rota()
+ html_path = generate_html_output(rota)
+
+ print("\nTest rota generation complete!")
+ print(f"To test ICS export, open: file://{os.path.abspath(html_path)}")
+ print("Then use the 'Set Shift Times' button to configure times and download ICS files.")
+
+ except Exception as e:
+ print(f"Error generating test rota: {e}")
+ import traceback
+ traceback.print_exc()
\ No newline at end of file