telvero_whatson_talpa/assets/js/calendar-init.js

686 lines
23 KiB
JavaScript

/**
* Calendar Initialization and Event Handlers
* FullCalendar.js configuration for TV planning
*/
document.addEventListener('DOMContentLoaded', function() {
const calendarEl = document.getElementById('calendar');
if (!calendarEl) {
console.error('Calendar element not found');
return;
}
// Initialize FullCalendar with resource columns
const calendar = new FullCalendar.Calendar(calendarEl, {
schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
initialView: 'resourceTimeGridDay',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'resourceTimeGridDay,resourceTimeGridWeek,resourceTimelineDay'
},
slotDuration: '00:15:00',
slotLabelInterval: '00:15:00',
snapDuration: '00:01:00',
slotMinTime: '00:00:00',
slotMaxTime: '24:00:00',
height: 'auto',
locale: 'nl',
allDaySlot: false,
nowIndicator: true,
resourceAreaHeaderContent: 'Zenders',
resourceAreaWidth: '120px',
resourceOrder: 'title',
// Resources (channels) - only SBS9 and NET5
resources: [
{
id: 'SBS9',
title: 'SBS9',
eventBackgroundColor: '#3498db',
eventBorderColor: '#2980b9'
},
{
id: 'NET5',
title: 'NET5',
eventBackgroundColor: '#e74c3c',
eventBorderColor: '#c0392b'
}
],
// Load events from API (including background blocks)
events: function(info, successCallback, failureCallback) {
const startDate = info.startStr.split('T')[0];
const endDate = info.endStr.split('T')[0];
// Load both transmissions and blocks
Promise.all([
fetch('api/get_transmissions.php?' + new URLSearchParams({
start: startDate,
end: endDate
})).then(r => r.json()),
fetch('api/get_daily_blocks.php?' + new URLSearchParams({
date: startDate
})).then(r => r.json())
])
.then(([transmissions, blocksData]) => {
// Combine transmissions with block background events
const allEvents = [
...transmissions,
...(blocksData.backgroundEvents || [])
];
successCallback(allEvents);
// Update block info display
updateBlockInfo(blocksData.blocks || []);
})
.catch(error => {
console.error('Error loading events:', error);
failureCallback(error);
});
},
// Event rendering - optimized for resource columns
eventContent: function(arg) {
const seriesCode = arg.event.extendedProps.series_code;
const title = arg.event.title;
const duration = arg.event.extendedProps.duration;
// Skip rendering for background events (blocks)
if (arg.event.display === 'background') {
return true; // Use default rendering
}
// For timeline view
if (arg.view.type.includes('timeline')) {
return {
html: `
<div class="fc-event-main-frame">
<div class="fc-event-time">${arg.timeText}</div>
<div class="fc-event-title-container">
<div class="fc-event-title">
${seriesCode ? '<strong>' + seriesCode + '</strong> - ' : ''}
${title}
</div>
<div class="fc-event-duration" style="font-size: 0.8em; opacity: 0.8;">
${duration}
</div>
</div>
</div>
`
};
}
// For vertical resource view - ultra compact single line
return {
html: `
<div class="fc-event-main-frame" style="padding: 1px 3px; line-height: 1.1;">
<div style="font-weight: 600; font-size: 0.75em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${seriesCode ? '<span style="background: rgba(0,0,0,0.15); padding: 0px 3px; border-radius: 2px; margin-right: 2px; font-size: 0.9em;">' + seriesCode + '</span>' : ''}
${title} <span style="opacity: 0.65; font-size: 0.95em; margin-left: 3px;">⏱${duration}</span>
</div>
</div>
`
};
},
// Enable drag and drop
editable: true,
droppable: true,
// Event drop (move existing event)
eventDrop: function(info) {
const event = info.event;
const resourceId = event.getResources()[0]?.id || 'SBS9';
// Show loading indicator
showAlert('info', 'Valideren...');
fetch('api/update_transmission.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: event.id,
start_date: event.start.toISOString().split('T')[0],
start_time: event.start.toTimeString().split(' ')[0],
channel: resourceId
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
info.revert();
showAlert('error', data.error || 'Fout bij opslaan');
// Show validation details if available
if (data.validation && data.validation.blocks) {
console.warn('Validation failed:', data.validation);
}
} else {
showAlert('success', 'Uitzending verplaatst');
calendar.refetchEvents();
}
})
.catch(error => {
console.error('Error:', error);
info.revert();
showAlert('error', 'Netwerkfout bij opslaan');
});
},
// Event resize (change duration)
eventResize: function(info) {
showAlert('warning', 'Duur aanpassen is niet toegestaan. Gebruik de commercial instellingen.');
info.revert();
},
// External event drop (from sidebar) - DISABLED to prevent duplicates
// The eventReceive handler below will handle the creation
drop: null,
// Event receive (when external event is dropped)
eventReceive: function(info) {
const event = info.event;
const commercialId = event.extendedProps.commercial_id || info.draggedEl.dataset.commercialId;
const resourceId = event.getResources()[0]?.id || 'SBS9';
// Remove the temporary event
event.remove();
// Calculate next available start time
const dropDate = info.event.start.toISOString().split('T')[0];
const dropTime = info.event.start.toTimeString().split(' ')[0];
// Get next start time based on existing transmissions
fetch('api/get_next_start_time.php?' + new URLSearchParams({
date: dropDate,
channel: resourceId,
requested_time: dropTime
}))
.then(response => response.json())
.then(data => {
const startTime = data.next_start_time || dropTime;
// Show loading indicator
showAlert('info', 'Toevoegen...');
return fetch('api/create_transmission.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
commercial_id: commercialId,
channel: resourceId,
start_date: dropDate,
start_time: startTime
})
});
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('success', 'Uitzending toegevoegd');
calendar.refetchEvents();
} else {
showAlert('error', data.error || 'Fout bij toevoegen');
// Show validation details if available
if (data.validation && data.validation.blocks) {
console.warn('Validation failed:', data.validation);
// Show available blocks in alert
let blockInfo = 'Beschikbare blokken: ';
data.validation.blocks.forEach(block => {
const start = block.actual_start_time.substring(0, 5);
const end = block.actual_end_time ? block.actual_end_time.substring(0, 5) : '∞';
blockInfo += `${start}-${end}, `;
});
showAlert('warning', blockInfo);
}
}
})
.catch(error => {
console.error('Error:', error);
showAlert('error', 'Netwerkfout bij toevoegen');
});
},
// Event click (show details/edit)
eventClick: function(info) {
const event = info.event;
const props = event.extendedProps;
// Skip for background events (blocks)
if (event.display === 'background') {
return;
}
const resources = event.getResources();
const channelName = resources.length > 0 ? resources[0].title : 'Onbekend';
const modal = new bootstrap.Modal(document.getElementById('eventModal'));
document.getElementById('eventTitle').textContent = event.title;
document.getElementById('eventDetails').innerHTML = `
<p><strong>Zender:</strong> ${channelName}</p>
<p><strong>Datum:</strong> ${event.start.toLocaleDateString('nl-NL')}</p>
<p><strong>Starttijd:</strong> ${event.start.toLocaleTimeString('nl-NL', {hour: '2-digit', minute: '2-digit'})}</p>
<p><strong>Duur:</strong> ${props.duration}</p>
<p><strong>Series Code:</strong> ${props.series_code || '-'}</p>
<p><strong>Template:</strong> ${props.template}</p>
<p><strong>API Status:</strong> <span class="badge status-${props.api_status}">${props.api_status}</span></p>
`;
// Set delete button
document.getElementById('deleteEventBtn').onclick = function() {
if (confirm('Weet je zeker dat je deze uitzending wilt verwijderen?')) {
deleteEvent(event.id);
modal.hide();
}
};
// Set sync button
document.getElementById('syncEventBtn').onclick = function() {
syncEvent(event.id);
};
modal.show();
},
// Date click (show block time editor)
dateClick: function(info) {
if (info.resource) {
showBlockTimeEditor(info.dateStr.split('T')[0], info.resource.id);
}
}
});
calendar.render();
// Make external events draggable
initDraggableCommercials();
// Store calendar instance globally
window.tvCalendar = calendar;
});
/**
* Initialize draggable commercials from sidebar
*/
function initDraggableCommercials() {
const commercialItems = document.querySelectorAll('.commercial-item');
commercialItems.forEach(item => {
new FullCalendar.Draggable(item, {
eventData: function(eventEl) {
return {
title: eventEl.dataset.title,
duration: eventEl.dataset.duration,
backgroundColor: eventEl.dataset.color,
borderColor: eventEl.dataset.color,
extendedProps: {
commercial_id: eventEl.dataset.commercialId,
series_code: eventEl.dataset.seriesCode
}
};
}
});
});
}
/**
* Show block time editor
*/
function showBlockTimeEditor(date, channel) {
const modal = new bootstrap.Modal(document.getElementById('blockTimeModal'));
document.getElementById('blockDate').value = date;
document.getElementById('blockChannel').value = channel;
// Load current block time
fetch(`api/get_block_time.php?date=${date}&channel=${channel}`)
.then(response => response.json())
.then(data => {
if (data.start_time) {
document.getElementById('blockStartTime').value = data.start_time;
}
});
modal.show();
}
/**
* Save block time
*/
function saveBlockTime() {
const date = document.getElementById('blockDate').value;
const channel = document.getElementById('blockChannel').value;
const startTime = document.getElementById('blockStartTime').value;
const recalculate = document.getElementById('recalculateTransmissions').checked;
fetch('api/update_block_time.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
date: date,
channel: channel,
start_time: startTime,
recalculate: recalculate
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('success', 'Blok starttijd bijgewerkt');
window.tvCalendar.refetchEvents();
bootstrap.Modal.getInstance(document.getElementById('blockTimeModal')).hide();
} else {
showAlert('error', data.error || 'Fout bij opslaan');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('error', 'Netwerkfout bij opslaan');
});
}
/**
* Delete event
*/
function deleteEvent(eventId) {
fetch('api/delete_transmission.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: eventId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('success', 'Uitzending verwijderd');
window.tvCalendar.refetchEvents();
} else {
showAlert('error', data.error || 'Fout bij verwijderen');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('error', 'Netwerkfout bij verwijderen');
});
}
/**
* Sync event to Talpa API
*/
function syncEvent(eventId) {
showAlert('info', 'Synchroniseren...');
// This would call your existing sync functionality
fetch('index.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `sync_item=1&sync_id=${eventId}`
})
.then(() => {
showAlert('success', 'Gesynchroniseerd met Talpa');
window.tvCalendar.refetchEvents();
})
.catch(error => {
console.error('Error:', error);
showAlert('error', 'Fout bij synchroniseren');
});
}
/**
* Show alert message
*/
function showAlert(type, message) {
const alertContainer = document.getElementById('alertContainer');
if (!alertContainer) return;
const alertClass = {
'success': 'alert-success',
'error': 'alert-danger',
'warning': 'alert-warning',
'info': 'alert-info'
}[type] || 'alert-info';
const alert = document.createElement('div');
alert.className = `alert ${alertClass} alert-dismissible fade show`;
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
alertContainer.appendChild(alert);
// Auto-dismiss after 5 seconds
setTimeout(() => {
alert.remove();
}, 5000);
}
/**
* Filter commercials in sidebar
*/
function filterCommercials(searchTerm) {
const items = document.querySelectorAll('.commercial-item');
const term = searchTerm.toLowerCase();
items.forEach(item => {
const title = item.dataset.title.toLowerCase();
const seriesCode = (item.dataset.seriesCode || '').toLowerCase();
if (title.includes(term) || seriesCode.includes(term)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
/**
* Update block info display
*/
function updateBlockInfo(blocks) {
const container = document.getElementById('blockInfoContainer');
if (!container) return;
if (blocks.length === 0) {
container.innerHTML = '<p class="text-muted small mb-0">Geen actieve blokken voor deze dag</p>';
return;
}
// Group blocks by channel
const blocksByChannel = {};
blocks.forEach(block => {
if (!blocksByChannel[block.channel]) {
blocksByChannel[block.channel] = [];
}
blocksByChannel[block.channel].push(block);
});
let html = '<div class="row g-2">';
for (const [channel, channelBlocks] of Object.entries(blocksByChannel)) {
const channelColors = {
'SBS9': 'primary',
'NET5': 'danger',
'VERONICA': 'secondary'
};
const color = channelColors[channel] || 'secondary';
html += `<div class="col-md-4">`;
html += `<div class="card border-${color}">`;
html += `<div class="card-header bg-${color} text-white py-1 px-2">`;
html += `<small class="fw-bold">${channel}</small>`;
html += `</div>`;
html += `<div class="card-body p-2">`;
channelBlocks.forEach(block => {
const startTime = block.actual_start_time.substring(0, 5);
const endTime = block.actual_end_time ? block.actual_end_time.substring(0, 5) : '∞';
html += `<div class="d-flex justify-content-between align-items-center mb-1">`;
html += `<small class="text-muted">${block.template_name || 'Blok'}</small>`;
html += `<small class="badge bg-light text-dark">${startTime} - ${endTime}</small>`;
html += `</div>`;
});
html += `</div></div></div>`;
}
html += '</div>';
container.innerHTML = html;
}
/**
* Adjust calendar zoom level
* Base: 10px per minute
*/
function adjustZoom(factor) {
const calendarEl = document.getElementById('calendar');
const currentZoom = parseFloat(calendarEl.dataset.zoom || '1.0');
let newZoom;
if (factor === 1.0) {
newZoom = 1.0; // Reset
} else {
newZoom = currentZoom * factor;
newZoom = Math.max(0.5, Math.min(3.0, newZoom)); // Limit between 50% and 300%
}
calendarEl.dataset.zoom = newZoom;
// Base: 10px per minute
const baseHeightPx = 10;
const newHeightPx = baseHeightPx * newZoom;
// Update all slot heights
const style = document.createElement('style');
style.id = 'zoom-style';
const existingStyle = document.getElementById('zoom-style');
if (existingStyle) {
existingStyle.remove();
}
style.textContent = `
.fc-timegrid-slot {
height: ${newHeightPx}px !important;
}
.fc-timegrid-event {
min-height: ${Math.max(12, 12 * newZoom)}px !important;
}
.fc-timegrid-event .fc-event-main-frame {
font-size: ${Math.max(0.65, 0.75 * newZoom)}em !important;
padding: ${Math.max(1, 2 * newZoom)}px ${Math.max(2, 3 * newZoom)}px !important;
}
.fc-timegrid-slot-label {
font-size: ${Math.max(0.6, 0.7 * newZoom)}em !important;
}
`;
document.head.appendChild(style);
// Update zoom level display
document.getElementById('zoomLevel').textContent = Math.round(newZoom * 100) + '%';
// Refresh calendar to apply changes
if (window.tvCalendar) {
window.tvCalendar.render();
}
}
/**
* Show sync block modal
*/
function showSyncBlockModal() {
const modal = new bootstrap.Modal(document.getElementById('syncBlockModal'));
// Set current date from calendar
if (window.tvCalendar) {
const currentDate = window.tvCalendar.getDate();
document.getElementById('syncBlockDate').value = currentDate.toISOString().split('T')[0];
}
modal.show();
}
/**
* Sync entire block to Talpa
*/
function syncBlock() {
const date = document.getElementById('syncBlockDate').value;
const channel = document.getElementById('syncBlockChannel').value;
if (!date || !channel) {
showAlert('error', 'Selecteer een datum en zender');
return;
}
// Show progress
document.getElementById('syncBlockProgress').classList.remove('d-none');
fetch('api/sync_block.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
date: date,
channel: channel
})
})
.then(response => response.json())
.then(data => {
// Hide progress
document.getElementById('syncBlockProgress').classList.add('d-none');
if (data.success) {
showAlert('success', data.message);
// Show detailed results if there were errors
if (data.errors && data.errors.length > 0) {
console.warn('Sync errors:', data.errors);
let errorMsg = 'Fouten bij synchronisatie:\n';
data.errors.forEach(err => {
errorMsg += `- ${err.time}: ${err.error}\n`;
});
showAlert('warning', errorMsg);
}
// Refresh calendar
if (window.tvCalendar) {
window.tvCalendar.refetchEvents();
}
// Close modal
bootstrap.Modal.getInstance(document.getElementById('syncBlockModal')).hide();
} else {
showAlert('error', data.error || 'Fout bij synchroniseren');
}
})
.catch(error => {
// Hide progress
document.getElementById('syncBlockProgress').classList.add('d-none');
console.error('Error:', error);
showAlert('error', 'Netwerkfout bij synchroniseren');
});
}
// Expose functions globally
window.saveBlockTime = saveBlockTime;
window.filterCommercials = filterCommercials;
window.updateBlockInfo = updateBlockInfo;
window.adjustZoom = adjustZoom;
window.showSyncBlockModal = showSyncBlockModal;
window.syncBlock = syncBlock;