# Plan: Blokken Kopiëren Functionaliteit ## 📋 Overzicht Implementatie van een functie om blokken (inclusief transmissions en bloktijden) te kopiëren van andere dagen naar de huidige dag in de Excel Planner view. ## 🎯 Functionaliteit Vereisten ### Wat wordt gekopieerd? - **Transmissions**: Alle uitzendingen binnen een specifiek blok - **Bloktijden**: De `actual_start_time` en `actual_end_time` van het bronblok ### Gebruikersflow 1. Gebruiker opent Excel Planner ([`planner.php`](planner.php)) 2. Gebruiker klikt op "Kopieer Blok" knop bij een specifiek blok 3. Modal opent met: - Selectie van **brondag** (datepicker) - Selectie van **bronblok** (dropdown, gefilterd op zelfde zender en template) 4. Gebruiker bevestigt kopieeractie 5. Systeem: - Verwijdert alle bestaande transmissions in het doelblok - Kopieert bloktijden van bronblok naar doelblok - Kopieert alle transmissions van bronblok naar doelblok - Past `start_date` aan naar doeldag - Zet `api_status` op 'pending' (moet opnieuw gesynchroniseerd worden) 6. Pagina herlaadt met gekopieerde data ## 🏗️ Architectuur ### Database Schema Bestaande tabellen die gebruikt worden: **`daily_blocks`** ```sql - id (primary key) - template_id (foreign key naar block_templates) - channel (VARCHAR) - block_date (DATE) - actual_start_time (TIME) - actual_end_time (TIME) ``` **`transmissions`** ```sql - id (primary key) - infomercial_id (foreign key) - channel (VARCHAR) - template (VARCHAR) - start_date (DATE) - start_time (TIME) - duration (TIME) - api_status (ENUM: pending, synced, error) - talpa_transmission_id (INT, nullable) ``` ### Dataflow Diagram ```mermaid graph TD A[Gebruiker klikt Kopieer Blok] --> B[Modal opent] B --> C[Selecteer Brondag] C --> D[Selecteer Bronblok] D --> E[Bevestig Kopiëren] E --> F[API: copy_block.php] F --> G{Validatie} G -->|Invalid| H[Toon Foutmelding] G -->|Valid| I[Start Database Transactie] I --> J[Verwijder Bestaande Transmissions] J --> K[Update Bloktijden] K --> L[Kopieer Transmissions] L --> M[Commit Transactie] M --> N[Return Success] N --> O[Herlaad Pagina] ``` ## 🔧 Implementatie Details ### 1. Frontend UI (planner.php) #### Nieuwe Knop in Block Header Locatie: In de block header, naast de bestaande "Sync naar Talpa" knop ```html ``` #### Modal voor Blok Kopiëren ```html ``` #### JavaScript Functies ```javascript // Load available source blocks when date is selected function loadSourceBlocks(targetBlockId, channel, templateId) { const sourceDate = document.getElementById(`sourceDate${targetBlockId}`).value; const sourceBlockSelect = document.getElementById(`sourceBlock${targetBlockId}`); if (!sourceDate) { sourceBlockSelect.innerHTML = ''; return; } // Show loading sourceBlockSelect.innerHTML = ''; fetch(`api/get_available_source_blocks.php?date=${sourceDate}&channel=${channel}&template_id=${templateId}`) .then(response => response.json()) .then(data => { if (data.success && data.blocks.length > 0) { let options = ''; data.blocks.forEach(block => { const transmissionCount = block.transmission_count || 0; options += ``; }); sourceBlockSelect.innerHTML = options; } else { sourceBlockSelect.innerHTML = ''; } }) .catch(error => { console.error('Error loading source blocks:', error); sourceBlockSelect.innerHTML = ''; }); } // Show preview of source block when selected document.querySelectorAll('[id^="sourceBlock"]').forEach(select => { select.addEventListener('change', function() { const blockId = this.id.replace('sourceBlock', ''); const sourceBlockId = this.value; if (!sourceBlockId) { document.getElementById(`sourceBlockPreview${blockId}`).style.display = 'none'; return; } // Load block details fetch(`api/get_block_details.php?block_id=${sourceBlockId}`) .then(response => response.json()) .then(data => { if (data.success) { const preview = document.getElementById(`sourceBlockInfo${blockId}`); let html = `

Starttijd: ${data.block.actual_start_time.substring(0,5)}

Eindtijd: ${data.block.actual_end_time ? data.block.actual_end_time.substring(0,5) : '∞'}

Aantal uitzendingen: ${data.transmissions.length}

`; if (data.transmissions.length > 0) { html += '

Uitzendingen:

'; } preview.innerHTML = html; document.getElementById(`sourceBlockPreview${blockId}`).style.display = 'block'; } }); }); }); // Handle copy block form submission document.querySelectorAll('[id^="copyBlockForm"]').forEach(form => { form.addEventListener('submit', function(e) { e.preventDefault(); const blockId = this.id.replace('copyBlockForm', ''); const sourceDate = document.getElementById(`sourceDate${blockId}`).value; const sourceBlockId = document.getElementById(`sourceBlock${blockId}`).value; if (!sourceDate || !sourceBlockId) { alert('Selecteer een brondag en bronblok'); return; } if (!confirm('Weet je zeker dat je dit blok wilt kopiëren? Alle bestaande uitzendingen worden overschreven!')) { return; } // Show loading const submitBtn = this.querySelector('button[type="submit"]'); const originalHTML = submitBtn.innerHTML; submitBtn.disabled = true; submitBtn.innerHTML = ' Kopiëren...'; // Call API fetch('api/copy_block.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source_block_id: sourceBlockId, target_block_id: blockId, target_date: '' }) }) .then(response => response.json()) .then(data => { if (data.success) { alert(`✓ Blok succesvol gekopieerd!\n\nGekopieerd: ${data.copied_count} uitzendingen`); window.location.reload(); } else { alert('✗ Fout bij kopiëren: ' + (data.error || 'Onbekende fout')); submitBtn.disabled = false; submitBtn.innerHTML = originalHTML; } }) .catch(error => { console.error('Error:', error); alert('✗ Netwerkfout bij kopiëren'); submitBtn.disabled = false; submitBtn.innerHTML = originalHTML; }); }); }); ``` ### 2. Backend API Endpoints #### API: `api/get_available_source_blocks.php` **Doel**: Haal beschikbare bronblokken op voor een specifieke datum, zender en template **Input (GET parameters)**: - `date` (required): Brondag in Y-m-d formaat - `channel` (required): Zender naam (SBS9, NET5, VERONICA) - `template_id` (required): Template ID **Output (JSON)**: ```json { "success": true, "blocks": [ { "id": 123, "template_id": 5, "template_name": "SBS9 Dagblok", "channel": "SBS9", "block_date": "2026-01-15", "actual_start_time": "07:00:00", "actual_end_time": "15:00:00", "transmission_count": 12 } ] } ``` **Implementatie**: ```php load(); header('Content-Type: application/json'); try { $db = getDbConnection(); // Validate input $date = $_GET['date'] ?? null; $channel = $_GET['channel'] ?? null; $templateId = $_GET['template_id'] ?? null; if (!$date || !$channel || !$templateId) { jsonResponse(['success' => false, 'error' => 'Missing parameters'], 400); } if (!isValidDate($date)) { jsonResponse(['success' => false, 'error' => 'Invalid date format'], 400); } // Ensure blocks exist for this date ensureDailyBlocks($db, $date, $date); // Get blocks for this date, channel, and template $stmt = $db->prepare(" SELECT db.*, bt.name as template_name, COUNT(t.id) as transmission_count FROM daily_blocks db LEFT JOIN block_templates bt ON db.template_id = bt.id LEFT JOIN transmissions t ON t.start_date = db.block_date AND t.channel = db.channel AND t.start_time >= db.actual_start_time AND t.start_time < COALESCE(db.actual_end_time, '23:59:59') WHERE db.block_date = ? AND db.channel = ? AND db.template_id = ? GROUP BY db.id ORDER BY db.actual_start_time "); $stmt->execute([$date, $channel, $templateId]); $blocks = $stmt->fetchAll(); jsonResponse([ 'success' => true, 'blocks' => $blocks ]); } catch (Exception $e) { jsonResponse(['success' => false, 'error' => $e->getMessage()], 500); } ``` #### API: `api/get_block_details.php` **Doel**: Haal gedetailleerde informatie op over een specifiek blok (voor preview) **Input (GET parameters)**: - `block_id` (required): ID van het blok **Output (JSON)**: ```json { "success": true, "block": { "id": 123, "template_name": "SBS9 Dagblok", "channel": "SBS9", "block_date": "2026-01-15", "actual_start_time": "07:00:00", "actual_end_time": "15:00:00" }, "transmissions": [ { "id": 456, "title": "Product A", "start_time": "07:00:00", "duration": "00:28:30", "series_code": "001a" } ] } ``` **Implementatie**: ```php load(); header('Content-Type: application/json'); try { $db = getDbConnection(); $blockId = $_GET['block_id'] ?? null; if (!$blockId) { jsonResponse(['success' => false, 'error' => 'Missing block_id'], 400); } // Get block info $stmt = $db->prepare(" SELECT db.*, bt.name as template_name FROM daily_blocks db LEFT JOIN block_templates bt ON db.template_id = bt.id WHERE db.id = ? "); $stmt->execute([$blockId]); $block = $stmt->fetch(); if (!$block) { jsonResponse(['success' => false, 'error' => 'Block not found'], 404); } // Get transmissions in this block $stmt = $db->prepare(" SELECT t.*, c.title, c.series_code FROM transmissions t JOIN infomercials c ON t.infomercial_id = c.id WHERE t.start_date = ? AND t.channel = ? AND t.start_time >= ? AND t.start_time < ? ORDER BY t.start_time ASC "); $stmt->execute([ $block['block_date'], $block['channel'], $block['actual_start_time'], $block['actual_end_time'] ?? '23:59:59' ]); $transmissions = $stmt->fetchAll(); jsonResponse([ 'success' => true, 'block' => $block, 'transmissions' => $transmissions ]); } catch (Exception $e) { jsonResponse(['success' => false, 'error' => $e->getMessage()], 500); } ``` #### API: `api/copy_block.php` **Doel**: Kopieer een blok (bloktijden + transmissions) van bronblok naar doelblok **Input (POST JSON)**: ```json { "source_block_id": 123, "target_block_id": 456, "target_date": "2026-01-16" } ``` **Output (JSON)**: ```json { "success": true, "message": "Block copied successfully", "copied_count": 12, "deleted_count": 5 } ``` **Implementatie**: ```php load(); header('Content-Type: application/json'); try { $db = getDbConnection(); // Get POST data $input = json_decode(file_get_contents('php://input'), true); $sourceBlockId = $input['source_block_id'] ?? null; $targetBlockId = $input['target_block_id'] ?? null; $targetDate = $input['target_date'] ?? null; // Validate input if (!$sourceBlockId || !$targetBlockId || !$targetDate) { jsonResponse(['success' => false, 'error' => 'Missing required parameters'], 400); } if (!isValidDate($targetDate)) { jsonResponse(['success' => false, 'error' => 'Invalid target date format'], 400); } // Start transaction $db->beginTransaction(); try { // Get source block info $stmt = $db->prepare(" SELECT * FROM daily_blocks WHERE id = ? "); $stmt->execute([$sourceBlockId]); $sourceBlock = $stmt->fetch(); if (!$sourceBlock) { throw new Exception('Source block not found'); } // Get target block info $stmt = $db->prepare(" SELECT * FROM daily_blocks WHERE id = ? "); $stmt->execute([$targetBlockId]); $targetBlock = $stmt->fetch(); if (!$targetBlock) { throw new Exception('Target block not found'); } // Validate that blocks are compatible (same channel and template) if ($sourceBlock['channel'] !== $targetBlock['channel']) { throw new Exception('Source and target blocks must be on the same channel'); } if ($sourceBlock['template_id'] !== $targetBlock['template_id']) { throw new Exception('Source and target blocks must use the same template'); } // Step 1: Update target block times $stmt = $db->prepare(" UPDATE daily_blocks SET actual_start_time = ?, actual_end_time = ? WHERE id = ? "); $stmt->execute([ $sourceBlock['actual_start_time'], $sourceBlock['actual_end_time'], $targetBlockId ]); // Step 2: Delete existing transmissions in target block $stmt = $db->prepare(" DELETE FROM transmissions WHERE start_date = ? AND channel = ? AND start_time >= ? AND start_time < ? "); $deletedCount = $stmt->execute([ $targetDate, $targetBlock['channel'], $targetBlock['actual_start_time'], $targetBlock['actual_end_time'] ?? '23:59:59' ]); $deletedCount = $stmt->rowCount(); // Step 3: Get source transmissions $stmt = $db->prepare(" SELECT t.*, c.duration FROM transmissions t JOIN infomercials c ON t.infomercial_id = c.id WHERE t.start_date = ? AND t.channel = ? AND t.start_time >= ? AND t.start_time < ? ORDER BY t.start_time ASC "); $stmt->execute([ $sourceBlock['block_date'], $sourceBlock['channel'], $sourceBlock['actual_start_time'], $sourceBlock['actual_end_time'] ?? '23:59:59' ]); $sourceTransmissions = $stmt->fetchAll(); // Step 4: Copy transmissions to target block $copiedCount = 0; $insertStmt = $db->prepare(" INSERT INTO transmissions (infomercial_id, channel, template, start_date, start_time, duration, api_status) VALUES (?, ?, ?, ?, ?, ?, 'pending') "); foreach ($sourceTransmissions as $tx) { $insertStmt->execute([ $tx['infomercial_id'], $tx['channel'], $tx['template'], $targetDate, // Use target date $tx['start_time'], $tx['duration'], ]); $copiedCount++; } // Commit transaction $db->commit(); jsonResponse([ 'success' => true, 'message' => 'Block copied successfully', 'copied_count' => $copiedCount, 'deleted_count' => $deletedCount, 'block_times_updated' => true ]); } catch (Exception $e) { $db->rollBack(); throw $e; } } catch (Exception $e) { jsonResponse([ 'success' => false, 'error' => $e->getMessage() ], 500); } ``` ### 3. Database Operaties #### Transactie Flow ```sql -- Start transactie BEGIN; -- 1. Update doelblok tijden UPDATE daily_blocks SET actual_start_time = '07:00:00', actual_end_time = '15:00:00' WHERE id = [target_block_id]; -- 2. Verwijder bestaande transmissions in doelblok DELETE FROM transmissions WHERE start_date = '2026-01-16' AND channel = 'SBS9' AND start_time >= '07:00:00' AND start_time < '15:00:00'; -- 3. Kopieer transmissions van bronblok INSERT INTO transmissions (infomercial_id, channel, template, start_date, start_time, duration, api_status) SELECT infomercial_id, channel, template, '2026-01-16' as start_date, -- Nieuwe datum start_time, duration, 'pending' as api_status -- Reset sync status FROM transmissions WHERE start_date = '2026-01-15' AND channel = 'SBS9' AND start_time >= '07:00:00' AND start_time < '15:00:00'; -- Commit transactie COMMIT; ``` ## ⚠️ Edge Cases & Validatie ### Validatie Regels 1. **Bronblok bestaat**: Controleer of `source_block_id` bestaat in database 2. **Doelblok bestaat**: Controleer of `target_block_id` bestaat in database 3. **Zelfde zender**: Bronblok en doelblok moeten op dezelfde zender zijn 4. **Zelfde template**: Bronblok en doelblok moeten hetzelfde template gebruiken 5. **Geldige datum**: `target_date` moet een geldige datum zijn in Y-m-d formaat 6. **Niet in het verleden**: Optioneel - waarschuwing als doeldatum in het verleden ligt ### Edge Cases | Scenario | Gedrag | |----------|--------| | Bronblok heeft geen transmissions | Doelblok wordt leeggemaakt, bloktijden worden gekopieerd | | Doelblok heeft al transmissions | Alle bestaande transmissions worden verwijderd voor kopiëren | | Bronblok heeft aangepaste bloktijden | Bloktijden worden gekopieerd naar doelblok | | Infomercial bestaat niet meer | Fout - transactie wordt teruggedraaid | | Database fout tijdens kopiëren | Transactie wordt teruggedraaid, geen data wordt gewijzigd | | Gebruiker annuleert tijdens kopiëren | Geen effect - kopiëren gebeurt server-side in één transactie | ### Foutafhandeling ```javascript // Frontend error handling .catch(error => { console.error('Copy block error:', error); alert('Er is een fout opgetreden bij het kopiëren van het blok. Probeer het opnieuw.'); }); ``` ```php // Backend error handling try { $db->beginTransaction(); // ... copy operations ... $db->commit(); } catch (Exception $e) { $db->rollBack(); error_log("Copy block error: " . $e->getMessage()); jsonResponse([ 'success' => false, 'error' => 'Database error: ' . $e->getMessage() ], 500); } ``` ## 🎨 UI/UX Overwegingen ### Visuele Feedback 1. **Loading States**: - Spinner tijdens laden van bronblokken - Spinner tijdens kopiëren - Disabled buttons tijdens operaties 2. **Confirmatie**: - Waarschuwing dat bestaande data wordt overschreven - Bevestigingsdialoog voor kopiëren 3. **Success/Error Messages**: - Success: "✓ Blok succesvol gekopieerd! Gekopieerd: X uitzendingen" - Error: "✗ Fout bij kopiëren: [error message]" 4. **Preview**: - Toon aantal uitzendingen in bronblok - Toon bloktijden van bronblok - Optioneel: lijst van uitzendingen ### Toegankelijkheid - Keyboard navigatie in modal - ARIA labels voor screen readers - Focus management bij openen/sluiten modal - Clear error messages ## 📝 Testing Checklist ### Functionele Tests - [ ] Kopieer blok met transmissions werkt correct - [ ] Kopieer leeg blok werkt correct - [ ] Bloktijden worden correct gekopieerd - [ ] Bestaande transmissions worden verwijderd - [ ] `api_status` wordt gereset naar 'pending' - [ ] `start_date` wordt correct aangepast - [ ] Transactie rollback werkt bij fouten - [ ] Validatie van zelfde zender werkt - [ ] Validatie van zelfde template werkt ### UI Tests - [ ] Modal opent correct - [ ] Brondag selectie werkt - [ ] Bronblok dropdown wordt correct gevuld - [ ] Preview toont correcte informatie - [ ] Loading states worden getoond - [ ] Success message wordt getoond - [ ] Error messages worden getoond - [ ] Pagina herlaadt na succesvol kopiëren ### Edge Case Tests - [ ] Kopieer van dag zonder blokken - [ ] Kopieer naar dag in het verleden - [ ] Kopieer met niet-bestaande infomercials - [ ] Kopieer tijdens database fout - [ ] Kopieer met ongeldige parameters ## 🚀 Implementatie Volgorde 1. **Backend API's** (eerst testen met Postman/curl): - [ ] `api/get_available_source_blocks.php` - [ ] `api/get_block_details.php` - [ ] `api/copy_block.php` 2. **Frontend UI**: - [ ] Voeg "Kopieer Blok" knop toe aan block headers - [ ] Implementeer copy block modal - [ ] Implementeer JavaScript functies 3. **Testing**: - [ ] Test alle API endpoints - [ ] Test UI flows - [ ] Test edge cases 4. **Documentatie**: - [ ] Update README.md met nieuwe functionaliteit - [ ] Voeg screenshots toe (optioneel) ## 📚 Benodigde Bestanden ### Nieuwe Bestanden - `api/get_available_source_blocks.php` - API voor beschikbare bronblokken - `api/get_block_details.php` - API voor blok details (preview) - `api/copy_block.php` - API voor blok kopiëren ### Te Wijzigen Bestanden - `planner.php` - Voeg UI toe voor kopieer functionaliteit - `README.md` - Documenteer nieuwe functionaliteit ### Geen Wijzigingen Nodig - Database schema (bestaande tabellen zijn voldoende) - `helpers.php` (bestaande functies zijn voldoende) - Andere PHP bestanden ## 🔒 Beveiliging 1. **Input Validatie**: - Valideer alle GET/POST parameters - Gebruik prepared statements voor alle queries - Valideer datum formaten 2. **Autorisatie**: - Optioneel: voeg gebruikersauthenticatie toe - Controleer of gebruiker rechten heeft om blokken te kopiëren 3. **Database**: - Gebruik transacties voor atomaire operaties - Rollback bij fouten - Error logging zonder gevoelige data ## 📊 Performance Overwegingen 1. **Database Queries**: - Gebruik indexes op `start_date`, `channel`, `start_time` - Batch insert voor transmissions (indien mogelijk) - Limit aantal transmissions per blok (praktisch max ~50) 2. **Frontend**: - Lazy load bronblokken (alleen bij selectie datum) - Debounce date picker events - Cache block details voor preview 3. **API Response Times**: - Target: < 500ms voor get_available_source_blocks - Target: < 200ms voor get_block_details - Target: < 1000ms voor copy_block (inclusief database operaties) ## ✅ Acceptatie Criteria De functionaliteit is compleet wanneer: 1. ✅ Gebruiker kan een brondag selecteren in de planner 2. ✅ Gebruiker kan een bronblok selecteren (gefilterd op zelfde template) 3. ✅ Gebruiker ziet een preview van het bronblok 4. ✅ Gebruiker kan het blok kopiëren met één klik 5. ✅ Alle transmissions worden correct gekopieerd 6. ✅ Bloktijden worden correct gekopieerd 7. ✅ Bestaande transmissions in doelblok worden overschreven 8. ✅ `api_status` wordt gereset naar 'pending' 9. ✅ Pagina toont gekopieerde data na herladen 10. ✅ Foutmeldingen worden duidelijk getoond bij problemen --- **Geschatte Implementatietijd**: 4-6 uur - Backend API's: 2-3 uur - Frontend UI: 1.5-2 uur - Testing & debugging: 0.5-1 uur