686 lines
23 KiB
JavaScript
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 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 = `
|
|
<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 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}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
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 = '<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;
|