/** * 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: 'prevDay,prev,next,nextDay today', center: 'title', right: 'resourceTimeGridDay,resourceTimeGridWeek,resourceTimelineDay' }, customButtons: { prevDay: { text: '◀', hint: 'Vorige dag', click: function() { calendar.getDate().setDate(calendar.getDate().getDate() - 1); calendar.gotoDate(calendar.getDate()); } }, nextDay: { text: '▶', hint: 'Volgende dag', click: function() { calendar.getDate().setDate(calendar.getDate().getDate() + 1); calendar.gotoDate(calendar.getDate()); } } }, 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: `
${arg.timeText}
${seriesCode ? '' + seriesCode + ' - ' : ''} ${title}
${duration}
` }; } // For vertical resource view - ultra compact single line return { html: `
${seriesCode ? '' + seriesCode + '' : ''} ${title} ⏱${duration}
` }; }, // 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 infomercial 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.infomercial_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({ infomercial_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 = `

Zender: ${channelName}

Datum: ${event.start.toLocaleDateString('nl-NL')}

Starttijd: ${event.start.toLocaleTimeString('nl-NL', {hour: '2-digit', minute: '2-digit'})}

Duur: ${props.duration}

Series Code: ${props.series_code || '-'}

Template: ${props.template}

API Status: ${props.api_status}

`; // 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 infomercials from sidebar */ function initDraggableCommercials() { const commercialItems = document.querySelectorAll('.infomercial-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: { infomercial_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} `; alertContainer.appendChild(alert); // Auto-dismiss after 5 seconds setTimeout(() => { alert.remove(); }, 5000); } /** * Filter infomercials in sidebar */ function filterCommercials(searchTerm) { const items = document.querySelectorAll('.infomercial-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 = '

Geen actieve blokken voor deze dag

'; return; } // Group blocks by channel const blocksByChannel = {}; blocks.forEach(block => { if (!blocksByChannel[block.channel]) { blocksByChannel[block.channel] = []; } blocksByChannel[block.channel].push(block); }); let html = '
'; for (const [channel, channelBlocks] of Object.entries(blocksByChannel)) { const channelColors = { 'SBS9': 'primary', 'NET5': 'danger', 'VERONICA': 'secondary' }; const color = channelColors[channel] || 'secondary'; html += `
`; html += `
`; html += `
`; html += `${channel}`; html += `
`; html += `
`; 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 += `
`; html += `${block.template_name || 'Blok'}`; html += `${startTime} - ${endTime}`; html += `
`; }); html += `
`; } html += '
'; 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;