909 lines
28 KiB
Markdown
909 lines
28 KiB
Markdown
# 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
|
|
<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
|
|
|
|
```html
|
|
<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
|
|
|
|
```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 = '<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 formaat
|
|
- `channel` (required): Zender naam (SBS9, NET5, SBS6)
|
|
- `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
|
|
<?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)**:
|
|
```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
|
|
<?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)**:
|
|
```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
|
|
<?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
|
|
|
|
```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
|