28 KiB
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 Planner view.
🎯 Functionaliteit Vereisten
Wat wordt gekopieerd?
- Transmissions: Alle uitzendingen binnen een specifiek blok
- Bloktijden: De
actual_start_timeenactual_end_timevan het bronblok
Gebruikersflow
- Gebruiker opent Planner (
planner.php) - Gebruiker klikt op "Kopieer Blok" knop bij een specifiek blok
- Modal opent met:
- Selectie van brondag (datepicker)
- Selectie van bronblok (dropdown, gefilterd op zelfde zender en template)
- Gebruiker bevestigt kopieeractie
- Systeem:
- Verwijdert alle bestaande transmissions in het doelblok
- Kopieert bloktijden van bronblok naar doelblok
- Kopieert alle transmissions van bronblok naar doelblok
- Past
start_dateaan naar doeldag - Zet
api_statusop 'pending' (moet opnieuw gesynchroniseerd worden)
- Pagina herlaadt met gekopieerde data
🏗️ Architectuur
Database Schema
Bestaande tabellen die gebruikt worden:
daily_blocks
- id (primary key)
- template_id (foreign key naar block_templates)
- channel (VARCHAR)
- block_date (DATE)
- actual_start_time (TIME)
- actual_end_time (TIME)
transmissions
- 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
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
<button type="button"
class="btn btn-sm btn-info ms-2"
data-bs-toggle="modal"
data-bs-target="#copyBlockModal<?= $block['id'] ?>"
title="Kopieer blok van andere dag">
<i class="bi bi-files"></i> Kopieer Blok
</button>
Modal voor Blok Kopiëren
<div class="modal fade" id="copyBlockModal<?= $block['id'] ?>" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form id="copyBlockForm<?= $block['id'] ?>">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-files"></i> Kopieer Blok
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<strong>Doelblok:</strong><br>
<?= htmlspecialchars($block['template_name']) ?><br>
<?= htmlspecialchars($block['channel']) ?> - <?= formatDateDutch($selectedDate) ?>
</div>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i>
<strong>Let op:</strong> Alle bestaande uitzendingen in dit blok worden overschreven!
</div>
<div class="mb-3">
<label class="form-label">Brondag</label>
<input type="date"
class="form-control"
id="sourceDate<?= $block['id'] ?>"
required
onchange="loadSourceBlocks(<?= $block['id'] ?>, '<?= $block['channel'] ?>', <?= $block['template_id'] ?>)">
</div>
<div class="mb-3">
<label class="form-label">Bronblok</label>
<select class="form-select"
id="sourceBlock<?= $block['id'] ?>"
required>
<option value="">Selecteer eerst een brondag...</option>
</select>
<small class="text-muted">
Alleen blokken van hetzelfde type (<?= htmlspecialchars($block['template_name']) ?>) worden getoond
</small>
</div>
<div id="sourceBlockPreview<?= $block['id'] ?>" class="mt-3" style="display: none;">
<h6>Preview Bronblok:</h6>
<div class="card">
<div class="card-body">
<div id="sourceBlockInfo<?= $block['id'] ?>"></div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Annuleren
</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-files"></i> Kopieer Blok
</button>
</div>
</form>
</div>
</div>
</div>
JavaScript Functies
// 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 = '<option value="">Selecteer eerst een brondag...</option>';
return;
}
// Show loading
sourceBlockSelect.innerHTML = '<option value="">Laden...</option>';
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 = '<option value="">Selecteer bronblok...</option>';
data.blocks.forEach(block => {
const transmissionCount = block.transmission_count || 0;
options += `<option value="${block.id}">
${block.template_name} (${block.actual_start_time.substring(0,5)} - ${block.actual_end_time ? block.actual_end_time.substring(0,5) : '∞'})
- ${transmissionCount} uitzendingen
</option>`;
});
sourceBlockSelect.innerHTML = options;
} else {
sourceBlockSelect.innerHTML = '<option value="">Geen blokken beschikbaar op deze dag</option>';
}
})
.catch(error => {
console.error('Error loading source blocks:', error);
sourceBlockSelect.innerHTML = '<option value="">Fout bij laden</option>';
});
}
// 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 = `
<p><strong>Starttijd:</strong> ${data.block.actual_start_time.substring(0,5)}</p>
<p><strong>Eindtijd:</strong> ${data.block.actual_end_time ? data.block.actual_end_time.substring(0,5) : '∞'}</p>
<p><strong>Aantal uitzendingen:</strong> ${data.transmissions.length}</p>
`;
if (data.transmissions.length > 0) {
html += '<hr><p><strong>Uitzendingen:</strong></p><ul class="small">';
data.transmissions.forEach(tx => {
html += `<li>${tx.start_time.substring(0,5)} - ${tx.title}</li>`;
});
html += '</ul>';
}
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 = '<span class="spinner-border spinner-border-sm"></span> 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: '<?= $selectedDate ?>'
})
})
.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 formaatchannel(required): Zender naam (SBS9, NET5, SBS6)template_id(required): Template ID
Output (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
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->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):
{
"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
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->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):
{
"source_block_id": 123,
"target_block_id": 456,
"target_date": "2026-01-16"
}
Output (JSON):
{
"success": true,
"message": "Block copied successfully",
"copied_count": 12,
"deleted_count": 5
}
Implementatie:
<?php
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../helpers.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->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
-- 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
- Bronblok bestaat: Controleer of
source_block_idbestaat in database - Doelblok bestaat: Controleer of
target_block_idbestaat in database - Zelfde zender: Bronblok en doelblok moeten op dezelfde zender zijn
- Zelfde template: Bronblok en doelblok moeten hetzelfde template gebruiken
- Geldige datum:
target_datemoet een geldige datum zijn in Y-m-d formaat - 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
// 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.');
});
// 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
-
Loading States:
- Spinner tijdens laden van bronblokken
- Spinner tijdens kopiëren
- Disabled buttons tijdens operaties
-
Confirmatie:
- Waarschuwing dat bestaande data wordt overschreven
- Bevestigingsdialoog voor kopiëren
-
Success/Error Messages:
- Success: "✓ Blok succesvol gekopieerd! Gekopieerd: X uitzendingen"
- Error: "✗ Fout bij kopiëren: [error message]"
-
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_statuswordt gereset naar 'pending'start_datewordt 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
-
Backend API's (eerst testen met Postman/curl):
api/get_available_source_blocks.phpapi/get_block_details.phpapi/copy_block.php
-
Frontend UI:
- Voeg "Kopieer Blok" knop toe aan block headers
- Implementeer copy block modal
- Implementeer JavaScript functies
-
Testing:
- Test alle API endpoints
- Test UI flows
- Test edge cases
-
Documentatie:
- Update README.md met nieuwe functionaliteit
- Voeg screenshots toe (optioneel)
📚 Benodigde Bestanden
Nieuwe Bestanden
api/get_available_source_blocks.php- API voor beschikbare bronblokkenapi/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 functionaliteitREADME.md- Documenteer nieuwe functionaliteit
Geen Wijzigingen Nodig
- Database schema (bestaande tabellen zijn voldoende)
helpers.php(bestaande functies zijn voldoende)- Andere PHP bestanden
🔒 Beveiliging
-
Input Validatie:
- Valideer alle GET/POST parameters
- Gebruik prepared statements voor alle queries
- Valideer datum formaten
-
Autorisatie:
- Optioneel: voeg gebruikersauthenticatie toe
- Controleer of gebruiker rechten heeft om blokken te kopiëren
-
Database:
- Gebruik transacties voor atomaire operaties
- Rollback bij fouten
- Error logging zonder gevoelige data
📊 Performance Overwegingen
-
Database Queries:
- Gebruik indexes op
start_date,channel,start_time - Batch insert voor transmissions (indien mogelijk)
- Limit aantal transmissions per blok (praktisch max ~50)
- Gebruik indexes op
-
Frontend:
- Lazy load bronblokken (alleen bij selectie datum)
- Debounce date picker events
- Cache block details voor preview
-
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:
- ✅ Gebruiker kan een brondag selecteren in de planner
- ✅ Gebruiker kan een bronblok selecteren (gefilterd op zelfde template)
- ✅ Gebruiker ziet een preview van het bronblok
- ✅ Gebruiker kan het blok kopiëren met één klik
- ✅ Alle transmissions worden correct gekopieerd
- ✅ Bloktijden worden correct gekopieerd
- ✅ Bestaande transmissions in doelblok worden overschreven
- ✅
api_statuswordt gereset naar 'pending' - ✅ Pagina toont gekopieerde data na herladen
- ✅ 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