add ics export support

This commit is contained in:
Ross
2025-12-29 22:22:29 +00:00
parent fe1414a7bf
commit 23529ba441
5 changed files with 840 additions and 8 deletions
+305
View File
@@ -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 = $('<div id="ics-export-buttons" style="margin-top: 20px;"><h3>Export ICS Files</h3></div>');
const setTimesButton = $(`<button id="set-shift-times" style="margin: 5px;">Set Shift Times</button>`);
icsButtonsDiv.append(setTimesButton);
// Shift type exports (with worker info)
const shiftExportsDiv = $('<div style="margin-bottom: 20px;"><h4>Export by Shift Type</h4></div>');
Object.keys(shiftDates).sort().forEach(shiftType => {
const button = $(`<button style="margin: 5px;">Download ${shiftType.replace(/_/g, ' ')} ICS</button>`);
button.on('click', () => {
const ics = generateICS(shiftType, shiftDates[shiftType]);
downloadICS(`${shiftType}.ics`, ics);
});
shiftExportsDiv.append(button);
});
icsButtonsDiv.append(shiftExportsDiv);
// Worker exports
const workerExportsDiv = $('<div style="margin-bottom: 20px;"><h4>Export by Worker</h4></div>');
Object.keys(workerShifts).sort().forEach(workerName => {
const workerShiftTypes = Object.keys(workerShifts[workerName]);
if (workerShiftTypes.length > 0) {
const button = $(`<button style="margin: 5px;">Download ${workerName} ICS</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 = $(`
<div style="margin: 8px 0;">
<input type="checkbox" id="${checkboxId}" checked>
<label for="${checkboxId}" style="margin-left: 8px;">
${shiftType.replace(/_/g, ' ').toUpperCase()} (${dates.length} shift${dates.length !== 1 ? 's' : ''})
</label>
</div>
`);
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 = $(`
<div id="worker-shift-modal" style="display:none; position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); z-index:10000; background:#fff; border:2px solid #888; border-radius:8px; box-shadow:0 4px 24px #0002; padding:24px; min-width:400px; max-height:80vh; overflow-y:auto;">
<h3 style="margin-top:0;" id="worker-shift-modal-title">Select Shifts for Export</h3>
<div id="worker-shift-checkboxes" style="margin: 16px 0;"></div>
<div style="text-align:right; margin-top:16px;">
<button id="worker-shift-cancel" style="margin-right:8px;">Cancel</button>
<button id="worker-shift-export">Export Selected</button>
</div>
</div>
`);
$('body').append(workerShiftModal);
// Modal for setting shift times
const shiftTimesModal = $(`
<div id="shift-times-modal" style="display:none; position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); z-index:10000; background:#fff; border:2px solid #888; border-radius:8px; box-shadow:0 4px 24px #0002; padding:24px; min-width:400px; max-height:80vh; overflow-y:auto;">
<h3 style="margin-top:0;">Set Shift Times</h3>
<div id="shift-times-controls"></div>
<div style="text-align:right; margin-top:16px;">
<button id="save-shift-times">Save</button>
<button id="close-shift-times">Close</button>
</div>
</div>
`);
$("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 = $(`
<div style="margin: 10px 0; padding: 10px; border: 1px solid #ccc; border-radius: 4px;">
<label><strong>${shiftType.replace(/_/g, ' ')}</strong></label><br>
<label>Start: <input type="time" class="shift-start" data-shift="${shiftType}" value="${time.start}"></label>
<label>End: <input type="time" class="shift-end" data-shift="${shiftType}" value="${time.end}"></label>
<label><input type="checkbox" class="shift-nextday" data-shift="${shiftType}" ${time.nextDay ? 'checked' : ''}> Ends next day</label>
</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 = [];