From 99393002458830f6210ff5adbcebd4ffc1a05a6d Mon Sep 17 00:00:00 2001 From: Mark Pinkster Date: Fri, 16 Jan 2026 12:16:43 +0100 Subject: [PATCH] Block syncing and copying added --- TalpaAPI.php | 22 +- api/copy_block.php | 170 ++++ api/get_available_source_blocks.php | 74 ++ api/get_block_details.php | 67 ++ api/insert_transmission_at_position.php | 21 +- api/sync_block.php | 46 +- api/update_transmission.php | 57 +- assets/css/custom.css | 106 ++- assets/js/calendar-init.js | 20 +- docs/IMPLEMENTATION_SUMMARY.md | 307 +++++++ docs/TRANSMISSION_SYNC_TESTING.md | 373 ++++++++ helpers.php | 195 ++++ infomercials.php | 284 +++++- migrations/002_add_talpa_transmission_id.sql | 22 + planner.php | 317 ++++++- plans/copy-blocks-feature-plan.md | 908 +++++++++++++++++++ plans/sync-status-visual-indicators-plan.md | 595 ++++++++++++ plans/transmission-sync-update-plan.md | 271 ++++++ 18 files changed, 3789 insertions(+), 66 deletions(-) create mode 100644 api/copy_block.php create mode 100644 api/get_available_source_blocks.php create mode 100644 api/get_block_details.php create mode 100644 docs/IMPLEMENTATION_SUMMARY.md create mode 100644 docs/TRANSMISSION_SYNC_TESTING.md create mode 100644 migrations/002_add_talpa_transmission_id.sql create mode 100644 plans/copy-blocks-feature-plan.md create mode 100644 plans/sync-status-visual-indicators-plan.md create mode 100644 plans/transmission-sync-update-plan.md diff --git a/TalpaAPI.php b/TalpaAPI.php index 7748bbb..83152fc 100644 --- a/TalpaAPI.php +++ b/TalpaAPI.php @@ -100,17 +100,35 @@ class TalpaApi { ]); } + public function updateTransmission($transmissionId, $data) { + return $this->request('PUT', '/linearSchedule/v1/transmissions/' . $transmissionId, [ + "startDate" => $data['startDate'], + "startTime" => $data['startTime'], + "duration" => $data['duration'] + ]); + } + + public function updateEpisode($contentId, $title, $duration) { + return $this->request('PUT', '/content/v1/episodes/' . $contentId, [ + "title" => $title, + "duration" => $duration, + "department" => "Homeshopping" + ]); + } + public function deleteEpisode($contentId) { return $this->request('DELETE', '/content/v1/episodes/' . $contentId); } private function getMockResponse($endpoint, $data) { usleep(200000); - if (strpos($endpoint, '/content/v1/episodes') !== false && strpos($endpoint, 'DELETE') === false) return ["id" => "MOCK_CONT_" . time()]; + if (strpos($endpoint, '/content/v1/episodes') !== false && strpos($endpoint, 'DELETE') === false && !isset($data['title'])) return ["id" => "MOCK_CONT_" . time()]; + if (strpos($endpoint, '/content/v1/episodes') !== false && isset($data['title'])) return ["statusCode" => "200", "message" => "Episode updated"]; if (strpos($endpoint, '/content/v1/episodes') !== false) return ["statusCode" => "200", "message" => "Episode deleted"]; if (strpos($endpoint, '/mam/v1/mediaAssets') !== false && !isset($data)) return ["mediaAssetLabel" => "TEL_MOCK_" . rand(100, 999)]; if (strpos($endpoint, '/mam/v1/mediaAssets') !== false) return ["id" => "MOCK_ASSET_" . time()]; - if (strpos($endpoint, '/linearSchedule/v1/transmissions') !== false) return ["statusCode" => "201", "id" => "MOCK_TX_" . time()]; + if (strpos($endpoint, '/linearSchedule/v1/transmissions') !== false && strpos($endpoint, 'PUT') === false) return ["statusCode" => "201", "id" => "MOCK_TX_" . time()]; + if (strpos($endpoint, '/linearSchedule/v1/transmissions') !== false) return ["statusCode" => "200", "message" => "Transmission updated"]; return []; } } diff --git a/api/copy_block.php b/api/copy_block.php new file mode 100644 index 0000000..2e7f919 --- /dev/null +++ b/api/copy_block.php @@ -0,0 +1,170 @@ +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) + if ($sourceBlock['channel'] !== $targetBlock['channel']) { + throw new Exception('Source and target blocks must be on the same channel'); + } + + // Get template names to compare (allows copying between different template IDs with same name) + $stmt = $db->prepare("SELECT name FROM block_templates WHERE id = ?"); + $stmt->execute([$sourceBlock['template_id']]); + $sourceTemplateName = $stmt->fetchColumn(); + + $stmt = $db->prepare("SELECT name FROM block_templates WHERE id = ?"); + $stmt->execute([$targetBlock['template_id']]); + $targetTemplateName = $stmt->fetchColumn(); + + if ($sourceTemplateName !== $targetTemplateName) { + throw new Exception('Source and target blocks must have the same template name'); + } + + // 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 + // Use the OLD target block times to find transmissions to delete + $stmt = $db->prepare(" + DELETE FROM transmissions + WHERE start_date = ? + AND channel = ? + AND start_time >= ? + AND start_time < ? + "); + $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); +} diff --git a/api/get_available_source_blocks.php b/api/get_available_source_blocks.php new file mode 100644 index 0000000..43964e0 --- /dev/null +++ b/api/get_available_source_blocks.php @@ -0,0 +1,74 @@ +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 the template name from the target template + $stmt = $db->prepare("SELECT name FROM block_templates WHERE id = ?"); + $stmt->execute([$templateId]); + $targetTemplateName = $stmt->fetchColumn(); + + if (!$targetTemplateName) { + jsonResponse(['success' => false, 'error' => 'Target template not found'], 404); + } + + // Get blocks for this date and channel with matching template NAME (not ID) + // This allows copying between different template IDs that have the same name + $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 bt.name = ? + GROUP BY db.id + ORDER BY db.actual_start_time + "); + $stmt->execute([$date, $channel, $targetTemplateName]); + $blocks = $stmt->fetchAll(); + + jsonResponse([ + 'success' => true, + 'blocks' => $blocks + ]); + +} catch (Exception $e) { + jsonResponse(['success' => false, 'error' => $e->getMessage()], 500); +} diff --git a/api/get_block_details.php b/api/get_block_details.php new file mode 100644 index 0000000..085fea7 --- /dev/null +++ b/api/get_block_details.php @@ -0,0 +1,67 @@ +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); +} diff --git a/api/insert_transmission_at_position.php b/api/insert_transmission_at_position.php index e18faea..c46a3ec 100644 --- a/api/insert_transmission_at_position.php +++ b/api/insert_transmission_at_position.php @@ -5,6 +5,7 @@ */ require_once __DIR__ . '/../vendor/autoload.php'; +require_once __DIR__ . '/../TalpaAPI.php'; require_once __DIR__ . '/../helpers.php'; use Dotenv\Dotenv; @@ -80,13 +81,14 @@ try { // Recalculate all start times $currentTime = $blockStartTime; + $newTransmissionId = null; foreach ($transmissions as $index => $tx) { if ($index === $position) { // Insert new transmission $stmt = $db->prepare(" - INSERT INTO transmissions - (infomercial_id, channel, template, start_date, start_time, duration, api_status) + INSERT INTO transmissions + (infomercial_id, channel, template, start_date, start_time, duration, api_status) VALUES (?, ?, 'HOME030', ?, ?, ?, 'pending') "); $stmt->execute([ @@ -96,6 +98,7 @@ try { $currentTime, $duration ]); + $newTransmissionId = $db->lastInsertId(); } else { // Update existing transmission $stmt = $db->prepare("UPDATE transmissions SET start_time = ?, api_status = 'pending' WHERE id = ?"); @@ -105,9 +108,21 @@ try { $currentTime = addTimeToTime($currentTime, $tx['duration']); } + // Sync all transmissions in the block to Talpa + $api = new TalpaApi(); + $blockUpdateResult = updateBlockTransmissionsToTalpa( + $db, + $api, + $input['date'], + $input['channel'], + $input['block_id'] + ); + jsonResponse([ 'success' => true, - 'message' => 'Transmission inserted successfully' + 'message' => 'Transmission inserted successfully', + 'new_transmission_id' => $newTransmissionId, + 'block_update' => $blockUpdateResult ]); } catch (Exception $e) { diff --git a/api/sync_block.php b/api/sync_block.php index 3426485..d387a32 100644 --- a/api/sync_block.php +++ b/api/sync_block.php @@ -43,14 +43,14 @@ $channel = $input['channel']; $debugLog[] = ['step' => 'Parameters', 'date' => $date, 'channel' => $channel]; try { - // Get all transmissions for this block that are not yet synced + // Get all transmissions for this block + // Include both new (pending) and existing (synced) transmissions $stmt = $db->prepare(" SELECT t.*, c.content_id, c.title as commercial_title FROM transmissions t JOIN infomercials c ON t.infomercial_id = c.id WHERE t.start_date = ? AND t.channel = ? - AND t.api_status != 'synced' ORDER BY t.start_time ASC "); $stmt->execute([$date, $channel]); @@ -79,7 +79,8 @@ try { $txDebug = [ 'transmission_id' => $tx['id'], 'title' => $tx['commercial_title'], - 'time' => $tx['start_time'] + 'time' => $tx['start_time'], + 'talpa_transmission_id' => $tx['talpa_transmission_id'] ]; try { @@ -94,23 +95,52 @@ try { $txDebug['request'] = $requestData; - $res = $api->createTransmission($requestData); + // Determine if this is a new transmission or an update + if (!empty($tx['talpa_transmission_id'])) { + // Update existing transmission in Talpa + $txDebug['action'] = 'update'; + $res = $api->updateTransmission($tx['talpa_transmission_id'], $requestData); + } else { + // Create new transmission in Talpa + $txDebug['action'] = 'create'; + $res = $api->createTransmission($requestData); + } $txDebug['response'] = $res; $txDebug['lastResponse'] = $api->lastResponse; // Check if sync was successful - $status = (isset($res['id']) || (isset($res['statusCode']) && $res['statusCode'] == 201)) ? 'synced' : 'error'; + $status = 'error'; + $talpaTransmissionId = $tx['talpa_transmission_id']; + + if ($txDebug['action'] === 'create') { + // For create: check for id in response + if (isset($res['id'])) { + $status = 'synced'; + $talpaTransmissionId = $res['id']; + } elseif (isset($res['statusCode']) && $res['statusCode'] == 201) { + $status = 'synced'; + $talpaTransmissionId = $res['id'] ?? $talpaTransmissionId; + } + } else { + // For update: check for success status + if (isset($res['statusCode']) && ($res['statusCode'] == 200 || $res['statusCode'] == '200')) { + $status = 'synced'; + } elseif (isset($res['message']) && strpos($res['message'], 'updated') !== false) { + $status = 'synced'; + } + } $txDebug['determined_status'] = $status; + $txDebug['talpa_transmission_id_saved'] = $talpaTransmissionId; - // Update transmission status + // Update transmission status and talpa_transmission_id $updateStmt = $db->prepare(" UPDATE transmissions - SET api_status = ?, api_response = ? + SET api_status = ?, api_response = ?, talpa_transmission_id = ? WHERE id = ? "); - $updateStmt->execute([$status, json_encode($res), $tx['id']]); + $updateStmt->execute([$status, json_encode($res), $talpaTransmissionId, $tx['id']]); if ($status === 'synced') { $syncedCount++; diff --git a/api/update_transmission.php b/api/update_transmission.php index 8c1b34b..5fb3111 100644 --- a/api/update_transmission.php +++ b/api/update_transmission.php @@ -5,6 +5,7 @@ */ require_once __DIR__ . '/../vendor/autoload.php'; +require_once __DIR__ . '/../TalpaAPI.php'; require_once __DIR__ . '/../helpers.php'; use Dotenv\Dotenv; @@ -155,9 +156,6 @@ try { } } - // Mark as pending sync if changed - $updates[] = "api_status = 'pending'"; - // Build and execute update query if (!empty($updates)) { $sql = "UPDATE transmissions SET " . implode(', ', $updates) . " WHERE id = ?"; @@ -167,13 +165,14 @@ try { $stmt->execute($params); } - // Get updated transmission + // Get updated transmission with all details including content_id $stmt = $db->prepare(" - SELECT + SELECT t.*, c.title, c.color_code, - c.series_code + c.series_code, + c.content_id FROM transmissions t JOIN infomercials c ON t.infomercial_id = c.id WHERE t.id = ? @@ -181,10 +180,54 @@ try { $stmt->execute([$input['id']]); $updated = $stmt->fetch(); + // Sync to Talpa if transmission has talpa_transmission_id + $talpaResult = null; + if (!empty($updated['talpa_transmission_id'])) { + $api = new TalpaApi(); + $talpaResult = syncTransmissionToTalpa($db, $api, $updated); + + // If sync failed, log it but don't fail the request + if (!$talpaResult['success']) { + error_log("Talpa sync failed for transmission {$updated['id']}: " . $talpaResult['error']); + } + } + + // Determine which block this transmission belongs to + $stmt = $db->prepare(" + SELECT id FROM daily_blocks + WHERE block_date = ? + AND channel = ? + AND actual_start_time <= ? + AND (actual_end_time IS NULL OR actual_end_time > ?) + LIMIT 1 + "); + $stmt->execute([ + $updated['start_date'], + $updated['channel'], + $updated['start_time'], + $updated['start_time'] + ]); + $block = $stmt->fetch(); + + // Update all other transmissions in the same block + $blockUpdateResult = null; + if ($block && !empty($updated['talpa_transmission_id'])) { + $api = new TalpaApi(); + $blockUpdateResult = updateBlockTransmissionsToTalpa( + $db, + $api, + $updated['start_date'], + $updated['channel'], + $block['id'] + ); + } + jsonResponse([ 'success' => true, 'message' => 'Transmission updated successfully', - 'transmission' => $updated + 'transmission' => $updated, + 'talpa_sync' => $talpaResult, + 'block_update' => $blockUpdateResult ]); } catch (Exception $e) { diff --git a/assets/css/custom.css b/assets/css/custom.css index 87aa5f9..2b0b756 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -3,9 +3,11 @@ */ :root { - --primary-color: #2c3e50; - --secondary-color: #34495e; - --accent-color: #3498db; + --text-color: #404040; + --brand-color: #32327D; + --accent-color: #32B4AA; + --primary-color: #32327D; + --secondary-color: #32327D; --success-color: #2ecc71; --warning-color: #f39c12; --danger-color: #e74c3c; @@ -13,12 +15,22 @@ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: var(--text-color); } /* Navigation */ .navbar-brand { font-weight: 600; font-size: 1.3rem; + color: var(--brand-color) !important; +} + +.navbar-dark .navbar-brand { + color: #ffffff !important; +} + +.navbar-dark { + background-color: var(--brand-color) !important; } /* Cards */ @@ -38,6 +50,10 @@ body { font-weight: 600; } +.card-header.bg-primary { + background-color: var(--brand-color) !important; +} + /* Buttons */ .btn { border-radius: 6px; @@ -50,6 +66,31 @@ body { box-shadow: 0 2px 8px rgba(0,0,0,0.15); } +.btn-primary { + background-color: var(--brand-color); + border-color: var(--brand-color); +} + +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active { + background-color: #28286a; + border-color: #28286a; +} + +.btn-outline-primary { + color: var(--brand-color); + border-color: var(--brand-color); +} + +.btn-outline-primary:hover, +.btn-outline-primary:focus, +.btn-outline-primary:active { + background-color: var(--brand-color); + border-color: var(--brand-color); + color: #ffffff; +} + /* Tables */ .table { border-radius: 8px; @@ -461,16 +502,71 @@ body { .form-control:focus, .form-select:focus { border-color: var(--accent-color); - box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25); + box-shadow: 0 0 0 0.2rem rgba(50, 180, 170, 0.25); } /* Tooltip Styling */ .tooltip-inner { - background-color: var(--primary-color); + background-color: var(--brand-color); border-radius: 4px; padding: 8px 12px; } +/* Sync Status Indicators */ +.sync-indicator { + font-size: 1.2rem; + cursor: help; + display: inline-block; + transition: transform 0.2s; +} + +.sync-indicator.sync-success { + color: #28a745; +} + +.sync-indicator.sync-pending { + color: #dc3545; +} + +.sync-indicator.sync-error { + color: #ffc107; +} + +.sync-indicator:hover { + transform: scale(1.2); +} + +/* Block Sync Status Badges */ +.block-sync-status { + font-size: 0.85rem; + padding: 0.35em 0.65em; + font-weight: 600; + border-radius: 4px; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.block-sync-status.block-sync-complete { + background-color: #28a745; + color: white; +} + +.block-sync-status.block-sync-partial { + background-color: #ffc107; + color: #000; +} + +.block-sync-status.block-sync-none { + background-color: #dc3545; + color: white; +} + +.block-sync-status.block-sync-empty { + background-color: #6c757d; + color: white; +} + /* Print Styles */ @media print { .navbar, diff --git a/assets/js/calendar-init.js b/assets/js/calendar-init.js index ed57468..2b5989d 100644 --- a/assets/js/calendar-init.js +++ b/assets/js/calendar-init.js @@ -16,10 +16,28 @@ document.addEventListener('DOMContentLoaded', function() { schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source', initialView: 'resourceTimeGridDay', headerToolbar: { - left: 'prev,next today', + 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', diff --git a/docs/IMPLEMENTATION_SUMMARY.md b/docs/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..4eb7aa1 --- /dev/null +++ b/docs/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,307 @@ +# Transmission Synchronisatie - Implementatie Samenvatting + +## Overzicht + +Deze implementatie zorgt ervoor dat transmissions in de planning automatisch worden bijgewerkt in Talpa wanneer ze worden verplaatst. Dit geldt voor zowel de verplaatste transmission als alle andere transmissions in hetzelfde uitzendblok. + +## Geïmplementeerde Wijzigingen + +### 1. Database Migratie +**Bestand**: [`migrations/002_add_talpa_transmission_id.sql`](../migrations/002_add_talpa_transmission_id.sql) + +- Nieuwe kolom `talpa_transmission_id` toegevoegd aan `transmissions` tabel +- Index toegevoegd voor snellere lookups +- Automatische migratie van bestaande data uit `api_response` JSON + +**Uitvoeren**: +```bash +mysql -u [user] -p [database] < migrations/002_add_talpa_transmission_id.sql +``` + +### 2. TalpaAPI Uitbreiding +**Bestand**: [`TalpaAPI.php`](../TalpaAPI.php) + +**Nieuwe methode**: +```php +public function updateTransmission($transmissionId, $data) +``` + +**Functionaliteit**: +- PUT request naar `/linearSchedule/v1/transmissions/{id}` +- Update startDate, startTime en duration +- Mock response voor testing + +### 3. Sync Block Update +**Bestand**: [`api/sync_block.php`](../api/sync_block.php) + +**Wijzigingen**: +- Detecteert of transmission nieuw is (POST) of bestaand (PUT) +- Slaat `talpa_transmission_id` op na succesvolle sync +- Ondersteunt zowel create als update operaties +- Uitgebreide debug logging + +**Logica**: +``` +IF talpa_transmission_id IS NULL: + → POST nieuwe transmission + → Sla transmission_id op +ELSE: + → PUT update bestaande transmission +``` + +### 4. Helper Functies +**Bestand**: [`helpers.php`](../helpers.php) + +**Nieuwe functies**: + +#### `updateBlockTransmissionsToTalpa($db, $api, $date, $channel, $blockId)` +- Herberekent starttijden van alle transmissions in een blok +- Update lokale database +- Synchroniseert naar Talpa (PUT requests) +- Retourneert resultaat met success/failure counts + +#### `syncTransmissionToTalpa($db, $api, $transmission)` +- Sync één transmission naar Talpa +- Automatische detectie van create vs update +- Error handling en status updates +- Retourneert talpa_transmission_id + +### 5. Update Transmission +**Bestand**: [`api/update_transmission.php`](../api/update_transmission.php) + +**Nieuwe functionaliteit**: +- Direct sync naar Talpa na lokale update +- Cascade update van alle transmissions in hetzelfde blok +- Bepaalt automatisch welk blok de transmission behoort +- Retourneert sync resultaten in response + +**Flow**: +1. Update lokale transmission +2. Sync transmission naar Talpa (als talpa_transmission_id bestaat) +3. Bepaal block_id +4. Update alle andere transmissions in blok +5. Return resultaten + +### 6. Insert at Position +**Bestand**: [`api/insert_transmission_at_position.php`](../api/insert_transmission_at_position.php) + +**Nieuwe functionaliteit**: +- Herberekent starttijden na insert +- Synchroniseert hele blok naar Talpa +- Retourneert block update resultaten + +**Flow**: +1. Insert nieuwe transmission op positie +2. Herbereken alle starttijden in blok +3. Update lokale database +4. Sync hele blok naar Talpa +5. Return resultaten + +## API Response Wijzigingen + +### sync_block.php Response +```json +{ + "success": true, + "message": "Synchronisatie voltooid: 3 geslaagd, 0 mislukt", + "synced": 3, + "failed": 0, + "errors": [], + "api_calls": [ + { + "transmission_id": 1, + "action": "update", + "talpa_transmission_id": "MOCK_TX_123", + "determined_status": "synced" + } + ] +} +``` + +### update_transmission.php Response +```json +{ + "success": true, + "message": "Transmission updated successfully", + "transmission": { ... }, + "talpa_sync": { + "success": true, + "talpa_transmission_id": "MOCK_TX_123" + }, + "block_update": { + "success": true, + "updated": 2, + "failed": 0 + } +} +``` + +### insert_transmission_at_position.php Response +```json +{ + "success": true, + "message": "Transmission inserted successfully", + "new_transmission_id": 456, + "block_update": { + "success": true, + "updated": 3, + "failed": 0 + } +} +``` + +## Gebruik Scenario's + +### Scenario 1: Eerste Sync +``` +1. Maak transmissions aan in planning +2. Klik "Sync Block" +3. Transmissions krijgen talpa_transmission_id +4. Status wordt 'synced' +``` + +### Scenario 2: Transmission Verplaatsen +``` +1. Drag transmission naar nieuwe tijd +2. API update wordt getriggerd +3. Transmission wordt geüpdatet in Talpa (PUT) +4. Alle volgende transmissions in blok worden ook geüpdatet +5. Status blijft 'synced' +``` + +### Scenario 3: Insert at Position +``` +1. Insert nieuwe transmission op positie 2 +2. Nieuwe transmission wordt aangemaakt (lokaal) +3. Transmissions 2, 3, 4... worden verschoven +4. Alle bestaande transmissions worden geüpdatet in Talpa +5. Nieuwe transmission krijgt status 'pending' (nog geen talpa_transmission_id) +6. Bij volgende sync: nieuwe transmission wordt aangemaakt in Talpa +``` + +## Error Handling + +### Talpa Transmission Niet Gevonden +``` +1. PUT request faalt (404) +2. talpa_transmission_id wordt NULL +3. api_status wordt 'error' of 'pending' +4. Bij volgende sync: POST create nieuwe transmission +``` + +### Netwerk Failure +``` +1. API call faalt +2. Status blijft 'pending' +3. Error wordt gelogd in api_response +4. Bij volgende sync: retry +``` + +### Validation Errors +``` +1. Lokale validatie faalt (overlap, buiten blok) +2. Update wordt geweigerd +3. Error response naar client +4. Geen Talpa call +``` + +## Logging + +Alle API calls worden gelogd in `api_log.txt`: +``` +[2026-01-16 10:20:15] REQUEST: PUT /linearSchedule/v1/transmissions/MOCK_TX_123 | Data: {"startDate":"2026-01-20","startTime":"08:15:00","duration":"00:30:00"} +[2026-01-16 10:20:15] RESPONSE (HTTP 200): {"statusCode":"200","message":"Transmission updated"} +``` + +## Testing + +Zie [`docs/TRANSMISSION_SYNC_TESTING.md`](TRANSMISSION_SYNC_TESTING.md) voor: +- 7 gedetailleerde test scenario's +- Database verificatie queries +- API endpoint voorbeelden +- Troubleshooting guide + +## Configuratie + +### .env Settings +```env +# Talpa API +TALPA_API_BASE=https://api.talpa.nl +TALPA_TOKEN=your_token_here + +# Mock mode voor testing (geen echte API calls) +TALPA_MOCK_MODE=true +``` + +### Mock Mode +Wanneer `TALPA_MOCK_MODE=true`: +- Geen echte API calls naar Talpa +- Gesimuleerde responses met delays +- Transmission IDs: `MOCK_TX_[timestamp]` +- Altijd success responses + +## Performance + +### Overwegingen +- Elke transmission update triggert meerdere PUT requests +- Bij grote blokken (>10 transmissions): meerdere seconden +- Overweeg rate limiting bij productie gebruik + +### Optimalisaties +- Batch updates mogelijk in toekomst +- Async processing voor grote blokken +- Caching van block data + +## Rollback Plan + +Bij problemen: +```sql +-- Verwijder nieuwe kolom +ALTER TABLE transmissions DROP COLUMN talpa_transmission_id; + +-- Restore oude code files +git checkout HEAD~1 TalpaAPI.php api/sync_block.php api/update_transmission.php api/insert_transmission_at_position.php helpers.php +``` + +## Volgende Stappen + +### Optionele Verbeteringen +1. **Audit Log**: Bijhouden van alle sync operaties +2. **Retry Mechanisme**: Automatische retry bij failures +3. **Batch Updates**: Meerdere transmissions in één API call +4. **Webhooks**: Talpa notificaties bij wijzigingen +5. **Conflict Resolution**: Wat als Talpa data afwijkt van lokaal? + +### Monitoring +- Check `api_log.txt` regelmatig +- Monitor failed syncs in database +- Alert bij hoge failure rate +- Performance metrics voor API calls + +## Support + +Bij vragen of problemen: +1. Check [`plans/transmission-sync-update-plan.md`](../plans/transmission-sync-update-plan.md) voor architectuur +2. Check [`docs/TRANSMISSION_SYNC_TESTING.md`](TRANSMISSION_SYNC_TESTING.md) voor testing +3. Review `api_log.txt` voor API call details +4. Check database `api_response` kolom voor error details + +## Bestanden Overzicht + +| Bestand | Wijziging | Beschrijving | +|---------|-----------|--------------| +| [`migrations/002_add_talpa_transmission_id.sql`](../migrations/002_add_talpa_transmission_id.sql) | Nieuw | Database migratie | +| [`TalpaAPI.php`](../TalpaAPI.php) | Gewijzigd | updateTransmission() methode | +| [`api/sync_block.php`](../api/sync_block.php) | Gewijzigd | Create/Update logica | +| [`api/update_transmission.php`](../api/update_transmission.php) | Gewijzigd | Cascade updates | +| [`api/insert_transmission_at_position.php`](../api/insert_transmission_at_position.php) | Gewijzigd | Block sync | +| [`helpers.php`](../helpers.php) | Gewijzigd | Nieuwe helper functies | +| [`plans/transmission-sync-update-plan.md`](../plans/transmission-sync-update-plan.md) | Nieuw | Architectuur plan | +| [`docs/TRANSMISSION_SYNC_TESTING.md`](TRANSMISSION_SYNC_TESTING.md) | Nieuw | Testing guide | +| [`docs/IMPLEMENTATION_SUMMARY.md`](IMPLEMENTATION_SUMMARY.md) | Nieuw | Dit document | + +## Versie Informatie + +- **Implementatie Datum**: 2026-01-16 +- **Versie**: 1.0 +- **Status**: Klaar voor testing diff --git a/docs/TRANSMISSION_SYNC_TESTING.md b/docs/TRANSMISSION_SYNC_TESTING.md new file mode 100644 index 0000000..2c14c43 --- /dev/null +++ b/docs/TRANSMISSION_SYNC_TESTING.md @@ -0,0 +1,373 @@ +# Transmission Synchronisatie Testing Guide + +## Overzicht + +Dit document beschrijft hoe je de nieuwe transmission synchronisatie functionaliteit kunt testen. De implementatie zorgt ervoor dat wanneer transmissions in de planning worden verplaatst, deze automatisch worden bijgewerkt in Talpa. + +## Vereisten + +1. Database migratie uitvoeren: [`migrations/002_add_talpa_transmission_id.sql`](../migrations/002_add_talpa_transmission_id.sql) +2. Talpa API configuratie in `.env` (of MOCK_MODE enabled) +3. Bestaande infomercials in de database + +## Test Scenario's + +### Scenario 1: Nieuwe Transmission Aanmaken en Synchroniseren + +**Doel**: Verifieer dat nieuwe transmissions correct worden aangemaakt en gesynchroniseerd naar Talpa. + +**Stappen**: +1. Maak een nieuwe transmission aan via de planning interface +2. Klik op "Sync Block" voor het betreffende blok +3. Controleer de database: + ```sql + SELECT id, talpa_transmission_id, api_status + FROM transmissions + WHERE id = [transmission_id]; + ``` + +**Verwacht Resultaat**: +- `talpa_transmission_id` is gevuld met een waarde (bijv. "MOCK_TX_1234567890") +- `api_status` = 'synced' +- `api_response` bevat de Talpa response + +**Verificatie**: +- Check `api_log.txt` voor de POST request naar `/linearSchedule/v1/transmissions` +- Response moet een `id` bevatten + +--- + +### Scenario 2: Bestaande Transmission Verplaatsen + +**Doel**: Verifieer dat verplaatsen van een transmission deze en alle volgende transmissions in het blok update. + +**Setup**: +1. Maak 3 transmissions in een blok (bijv. 07:00, 07:30, 08:00) +2. Sync het blok zodat alle transmissions een `talpa_transmission_id` hebben + +**Stappen**: +1. Verplaats de tweede transmission naar een latere tijd (bijv. 07:45) +2. Gebruik de drag-and-drop functionaliteit of update API + +**Verwacht Resultaat**: +- Transmission 2 heeft nieuwe start_time (07:45) +- Transmission 3 is automatisch verschoven (08:15) +- Beide transmissions zijn geüpdatet in Talpa (PUT requests) +- `api_status` blijft 'synced' voor beide + +**Verificatie**: +```sql +SELECT id, start_time, talpa_transmission_id, api_status +FROM transmissions +WHERE start_date = '[date]' AND channel = '[channel]' +ORDER BY start_time; +``` + +Check `api_log.txt` voor: +- PUT request naar `/linearSchedule/v1/transmissions/[id]` voor transmission 2 +- PUT request voor transmission 3 + +--- + +### Scenario 3: Insert at Position + +**Doel**: Verifieer dat inserting een transmission op een specifieke positie alle volgende transmissions update. + +**Setup**: +1. Maak een blok met 3 transmissions (07:00, 07:30, 08:00) +2. Sync het blok + +**Stappen**: +1. Insert een nieuwe transmission op positie 1 (tussen eerste en tweede) +2. Gebruik de insert API endpoint + +**Verwacht Resultaat**: +- Nieuwe transmission is aangemaakt op positie 1 +- Transmissions 2 en 3 zijn verschoven +- Nieuwe transmission heeft `talpa_transmission_id` = NULL (nog niet gesynchroniseerd) +- Bestaande transmissions zijn geüpdatet in Talpa + +**Verificatie**: +```sql +SELECT id, start_time, talpa_transmission_id, api_status +FROM transmissions +WHERE start_date = '[date]' AND channel = '[channel]' +ORDER BY start_time; +``` + +Expected times: +- TX1: 07:00 (unchanged) +- NEW: 07:30 (new, no talpa_transmission_id yet) +- TX2: 08:00 (updated from 07:30) +- TX3: 08:30 (updated from 08:00) + +--- + +### Scenario 4: Re-sync na Wijzigingen + +**Doel**: Verifieer dat re-sync correct omgaat met bestaande transmissions. + +**Setup**: +1. Maak en sync een blok met transmissions +2. Verplaats enkele transmissions (zonder direct te syncen) + +**Stappen**: +1. Klik op "Sync Block" opnieuw +2. Observeer het gedrag + +**Verwacht Resultaat**: +- Transmissions met `talpa_transmission_id`: PUT update naar Talpa +- Transmissions zonder `talpa_transmission_id`: POST create naar Talpa +- Alle transmissions krijgen status 'synced' + +**Verificatie**: +Check response van sync_block.php: +```json +{ + "success": true, + "synced": 3, + "failed": 0, + "api_calls": [ + { + "transmission_id": 1, + "action": "update", + "determined_status": "synced" + }, + { + "transmission_id": 2, + "action": "create", + "determined_status": "synced" + } + ] +} +``` + +--- + +### Scenario 5: Error Handling - Talpa Transmission Niet Gevonden + +**Doel**: Verifieer dat het systeem correct omgaat met een transmission die lokaal bestaat maar niet in Talpa. + +**Setup**: +1. Maak een transmission met een fake `talpa_transmission_id` (bijv. "FAKE_ID_123") +2. Zet `api_status` op 'synced' + +**Stappen**: +1. Probeer de transmission te verplaatsen +2. Of: sync het blok opnieuw + +**Verwacht Resultaat**: +- PUT request faalt (404 of error response) +- `talpa_transmission_id` wordt NULL +- `api_status` wordt 'pending' of 'error' +- Bij volgende sync: POST create nieuwe transmission + +**Verificatie**: +```sql +SELECT id, talpa_transmission_id, api_status, api_response +FROM transmissions +WHERE id = [transmission_id]; +``` + +--- + +### Scenario 6: Overnight Block + +**Doel**: Verifieer dat transmissions in overnight blocks (bijv. 23:30 - 02:00) correct worden behandeld. + +**Setup**: +1. Maak een overnight block (SBS9 Nachtblok: 23:30 - 02:00) +2. Voeg transmissions toe die over middernacht lopen + +**Stappen**: +1. Maak transmission om 23:30 +2. Maak transmission om 00:30 +3. Sync het blok +4. Verplaats een transmission + +**Verwacht Resultaat**: +- Beide transmissions worden correct gesynchroniseerd +- Tijd berekeningen werken correct over middernacht +- Updates worden correct naar Talpa gestuurd + +--- + +### Scenario 7: Cascade Update bij Meerdere Transmissions + +**Doel**: Verifieer dat een wijziging aan één transmission alle volgende transmissions in het blok update. + +**Setup**: +1. Maak een blok met 5 transmissions (elk 30 minuten) +2. Sync het blok + +**Stappen**: +1. Verplaats transmission 2 naar 15 minuten later +2. Observeer de cascade + +**Verwacht Resultaat**: +- Transmission 2: nieuwe tijd +- Transmissions 3, 4, 5: elk 15 minuten later +- Alle 4 transmissions zijn geüpdatet in Talpa (PUT requests) + +**Verificatie**: +Check `api_log.txt` voor 4 PUT requests: +``` +PUT /linearSchedule/v1/transmissions/[id2] +PUT /linearSchedule/v1/transmissions/[id3] +PUT /linearSchedule/v1/transmissions/[id4] +PUT /linearSchedule/v1/transmissions/[id5] +``` + +--- + +## Mock Mode Testing + +Voor testen zonder echte Talpa API verbinding, zet in `.env`: +``` +TALPA_MOCK_MODE=true +``` + +Mock responses: +- **POST create**: `{"statusCode": "201", "id": "MOCK_TX_[timestamp]"}` +- **PUT update**: `{"statusCode": "200", "message": "Transmission updated"}` + +--- + +## Database Queries voor Verificatie + +### Check Sync Status van Blok +```sql +SELECT + t.id, + t.start_time, + t.talpa_transmission_id, + t.api_status, + c.title +FROM transmissions t +JOIN infomercials c ON t.infomercial_id = c.id +WHERE t.start_date = '2026-01-20' +AND t.channel = 'SBS9' +ORDER BY t.start_time; +``` + +### Check Transmissions Zonder Talpa ID +```sql +SELECT id, start_time, api_status +FROM transmissions +WHERE talpa_transmission_id IS NULL +AND api_status = 'synced'; +``` +Dit zou leeg moeten zijn - alle synced transmissions moeten een talpa_transmission_id hebben. + +### Check Failed Syncs +```sql +SELECT + t.id, + t.start_time, + t.api_status, + t.api_response, + c.title +FROM transmissions t +JOIN infomercials c ON t.infomercial_id = c.id +WHERE t.api_status = 'error' +ORDER BY t.start_date DESC, t.start_time DESC +LIMIT 10; +``` + +--- + +## API Endpoints voor Testing + +### Sync Block +```bash +curl -X POST http://localhost/api/sync_block.php \ + -H "Content-Type: application/json" \ + -d '{ + "date": "2026-01-20", + "channel": "SBS9" + }' +``` + +### Update Transmission +```bash +curl -X POST http://localhost/api/update_transmission.php \ + -H "Content-Type: application/json" \ + -d '{ + "id": 123, + "start_time": "08:15:00" + }' +``` + +### Insert at Position +```bash +curl -X POST http://localhost/api/insert_transmission_at_position.php \ + -H "Content-Type: application/json" \ + -d '{ + "infomercial_id": 5, + "channel": "SBS9", + "date": "2026-01-20", + "block_id": 10, + "position": 2 + }' +``` + +--- + +## Troubleshooting + +### Transmission blijft op 'pending' +**Oorzaak**: Sync is niet uitgevoerd of gefaald +**Oplossing**: +1. Check `api_log.txt` voor errors +2. Voer sync_block.php opnieuw uit +3. Check `api_response` kolom voor error details + +### Talpa Transmission ID is NULL na sync +**Oorzaak**: Talpa response bevat geen `id` veld +**Oplossing**: +1. Check `api_response` in database +2. Verify Talpa API response format +3. Check mock response in [`TalpaAPI.php`](../TalpaAPI.php) line 130 + +### Cascade update werkt niet +**Oorzaak**: Block ID niet correct bepaald +**Oplossing**: +1. Verify daily_blocks tabel heeft correcte data +2. Check block time ranges +3. Verify transmission valt binnen block + +### PUT request faalt met 404 +**Oorzaak**: Transmission bestaat niet (meer) in Talpa +**Oplossing**: +1. Systeem zou automatisch `talpa_transmission_id` op NULL moeten zetten +2. Re-sync maakt nieuwe transmission aan (POST) +3. Verify error handling in [`helpers.php`](../helpers.php) `syncTransmissionToTalpa()` + +--- + +## Performance Overwegingen + +Bij grote blokken (>10 transmissions): +- Elke update triggert meerdere PUT requests naar Talpa +- Overweeg batch updates of rate limiting +- Monitor `api_log.txt` voor response times + +--- + +## Volgende Stappen + +Na succesvolle testing: +1. ✅ Migratie uitvoeren op productie database +2. ✅ Backup maken voor rollback +3. ✅ Monitor eerste sync operaties +4. ✅ Check Talpa API logs voor errors +5. ✅ Verify data consistency tussen lokaal en Talpa + +--- + +## Support + +Bij problemen: +- Check [`api_log.txt`](../api_log.txt) voor API calls +- Check PHP error logs +- Review [`plans/transmission-sync-update-plan.md`](../plans/transmission-sync-update-plan.md) voor architectuur details diff --git a/helpers.php b/helpers.php index e3101ff..f18f8ec 100644 --- a/helpers.php +++ b/helpers.php @@ -385,3 +385,198 @@ function isValidDate($date) { $d = DateTime::createFromFormat('Y-m-d', $date); return $d && $d->format('Y-m-d') === $date; } + +/** + * Update all transmissions in a block to Talpa after a change + * This function recalculates start times and syncs to Talpa + * + * @param PDO $db Database connection + * @param TalpaApi $api Talpa API instance + * @param string $date Block date (Y-m-d) + * @param string $channel Channel name + * @param int $blockId Block ID + * @return array Result with success status and details + */ +function updateBlockTransmissionsToTalpa($db, $api, $date, $channel, $blockId) { + $results = [ + 'success' => true, + 'updated' => 0, + 'failed' => 0, + 'errors' => [] + ]; + + try { + // Get block info + $stmt = $db->prepare(" + SELECT actual_start_time, actual_end_time + FROM daily_blocks + WHERE id = ? + "); + $stmt->execute([$blockId]); + $block = $stmt->fetch(); + + if (!$block) { + $results['success'] = false; + $results['errors'][] = 'Block not found'; + return $results; + } + + $blockStart = $block['actual_start_time']; + $blockEnd = $block['actual_end_time'] ?? '23:59:59'; + + // Get all transmissions in this block, ordered by start time + $stmt = $db->prepare(" + SELECT t.*, c.content_id, c.duration as infomercial_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([$date, $channel, $blockStart, $blockEnd]); + $transmissions = $stmt->fetchAll(); + + // Recalculate start times from block start + $currentTime = $blockStart; + + foreach ($transmissions as $tx) { + // Update local start time if it changed + if ($tx['start_time'] !== $currentTime) { + $updateStmt = $db->prepare(" + UPDATE transmissions + SET start_time = ?, api_status = 'pending' + WHERE id = ? + "); + $updateStmt->execute([$currentTime, $tx['id']]); + } + + // Sync to Talpa if transmission has talpa_transmission_id + if (!empty($tx['talpa_transmission_id'])) { + try { + $requestData = [ + "startDate" => $date, + "startTime" => $currentTime, + "duration" => $tx['duration'] + ]; + + $res = $api->updateTransmission($tx['talpa_transmission_id'], $requestData); + + // Check if update was successful + $status = 'error'; + if (isset($res['statusCode']) && ($res['statusCode'] == 200 || $res['statusCode'] == '200')) { + $status = 'synced'; + $results['updated']++; + } elseif (isset($res['message']) && strpos($res['message'], 'updated') !== false) { + $status = 'synced'; + $results['updated']++; + } else { + $results['failed']++; + $results['errors'][] = [ + 'transmission_id' => $tx['id'], + 'error' => $res['message'] ?? 'Unknown error' + ]; + } + + // Update status + $updateStmt = $db->prepare(" + UPDATE transmissions + SET api_status = ?, api_response = ? + WHERE id = ? + "); + $updateStmt->execute([$status, json_encode($res), $tx['id']]); + + } catch (Exception $e) { + $results['failed']++; + $results['errors'][] = [ + 'transmission_id' => $tx['id'], + 'error' => $e->getMessage() + ]; + } + } + + // Move to next time slot + $currentTime = addTimeToTime($currentTime, $tx['duration']); + } + + } catch (Exception $e) { + $results['success'] = false; + $results['errors'][] = $e->getMessage(); + } + + return $results; +} + +/** + * Sync a single transmission to Talpa (create or update) + * + * @param PDO $db Database connection + * @param TalpaApi $api Talpa API instance + * @param array $transmission Transmission data + * @return array Result with success status and talpa_transmission_id + */ +function syncTransmissionToTalpa($db, $api, $transmission) { + $result = [ + 'success' => false, + 'talpa_transmission_id' => $transmission['talpa_transmission_id'] ?? null, + 'error' => null + ]; + + try { + $requestData = [ + "channel" => $transmission['channel'], + "template" => $transmission['template'], + "startDate" => $transmission['start_date'], + "startTime" => $transmission['start_time'], + "duration" => $transmission['duration'], + "contentId" => $transmission['content_id'] + ]; + + // Determine if this is a create or update + if (!empty($transmission['talpa_transmission_id'])) { + // Update existing transmission + $res = $api->updateTransmission($transmission['talpa_transmission_id'], $requestData); + + if (isset($res['statusCode']) && ($res['statusCode'] == 200 || $res['statusCode'] == '200')) { + $result['success'] = true; + } elseif (isset($res['message']) && strpos($res['message'], 'updated') !== false) { + $result['success'] = true; + } else { + $result['error'] = $res['message'] ?? 'Update failed'; + } + } else { + // Create new transmission + $res = $api->createTransmission($requestData); + + if (isset($res['id'])) { + $result['success'] = true; + $result['talpa_transmission_id'] = $res['id']; + } elseif (isset($res['statusCode']) && $res['statusCode'] == 201) { + $result['success'] = true; + $result['talpa_transmission_id'] = $res['id'] ?? null; + } else { + $result['error'] = $res['message'] ?? 'Create failed'; + } + } + + // Update database + $status = $result['success'] ? 'synced' : 'error'; + $updateStmt = $db->prepare(" + UPDATE transmissions + SET api_status = ?, api_response = ?, talpa_transmission_id = ? + WHERE id = ? + "); + $updateStmt->execute([ + $status, + json_encode($res), + $result['talpa_transmission_id'], + $transmission['id'] + ]); + + } catch (Exception $e) { + $result['error'] = $e->getMessage(); + } + + return $result; +} diff --git a/infomercials.php b/infomercials.php index ae2dcb9..3903399 100644 --- a/infomercials.php +++ b/infomercials.php @@ -20,6 +20,13 @@ $api = new TalpaApi(); $db = getDbConnection(); $apiLogs = []; +// Retrieve and clear refresh logs from session +session_start(); +$refreshLogs = $_SESSION['refresh_logs'] ?? null; +if ($refreshLogs) { + unset($_SESSION['refresh_logs']); +} + // Handle infomercial registration if (isset($_POST['add_commercial'])) { $apiLogs[] = ['step' => 'Start registration', 'input' => $_POST]; @@ -84,21 +91,134 @@ if (isset($_POST['add_commercial'])) { } } -// Handle media asset update -if (isset($_POST['update_media_asset'])) { - $stmt = $db->prepare(" - UPDATE infomercials - SET media_asset_label = ?, upload_status = ?, series_code = ?, color_code = ? - WHERE id = ? - "); - $stmt->execute([ - $_POST['media_asset_label'], - $_POST['upload_status'], - $_POST['series_code'] ?? null, - $_POST['color_code'], - $_POST['infomercial_id'] - ]); - header('Location: infomercials.php?success=updated'); +// Handle infomercial update (sync to Talpa) +if (isset($_POST['update_infomercial'])) { + // Get current infomercial data + $stmt = $db->prepare("SELECT content_id, title, duration FROM infomercials WHERE id = ?"); + $stmt->execute([$_POST['infomercial_id']]); + $current = $stmt->fetch(); + + if ($current && $current['content_id']) { + // Update in Talpa if title or duration changed + $titleChanged = $current['title'] !== $_POST['title']; + $durationChanged = $current['duration'] !== $_POST['duration']; + + if ($titleChanged || $durationChanged) { + $result = $api->updateEpisode( + $current['content_id'], + $_POST['title'], + $_POST['duration'] + ); + $apiLogs[] = ['call' => 'Update Episode', 'request' => [ + 'content_id' => $current['content_id'], + 'title' => $_POST['title'], + 'duration' => $_POST['duration'] + ], 'response' => $api->lastResponse]; + } + + // Update in local database + $stmt = $db->prepare(" + UPDATE infomercials + SET title = ?, duration = ?, upload_status = ?, series_code = ?, color_code = ? + WHERE id = ? + "); + $stmt->execute([ + $_POST['title'], + $_POST['duration'], + $_POST['upload_status'], + $_POST['series_code'] ?? null, + $_POST['color_code'], + $_POST['infomercial_id'] + ]); + + header('Location: infomercials.php?success=updated'); + exit; + } +} + +// Handle refresh single infomercial from Talpa +if (isset($_POST['refresh_infomercial'])) { + $stmt = $db->prepare("SELECT title, media_asset_id FROM infomercials WHERE id = ?"); + $stmt->execute([$_POST['infomercial_id']]); + $infomercial = $stmt->fetch(); + + if ($infomercial && $infomercial['media_asset_id']) { + $details = $api->getMediaAssetDetails($infomercial['media_asset_id']); + $apiLogs[] = ['call' => 'Refresh Media Asset Details', 'request' => [ + 'asset_id' => $infomercial['media_asset_id'] + ], 'response' => $api->lastResponse]; + + if (isset($details['mediaAssetLabel'])) { + $stmt = $db->prepare(" + UPDATE infomercials + SET media_asset_label = ? + WHERE id = ? + "); + $stmt->execute([ + $details['mediaAssetLabel'], + $_POST['infomercial_id'] + ]); + + header('Location: infomercials.php?success=refreshed'); + exit; + } else { + // Store failed refresh log + session_start(); + $_SESSION['refresh_logs'] = [[ + 'status' => 'failed', + 'title' => $infomercial['title'], + 'asset_id' => $infomercial['media_asset_id'], + 'error' => $api->lastResponse['message'] ?? 'Geen mediaAssetLabel gevonden in response' + ]]; + } + } + header('Location: infomercials.php?error=refresh_failed'); + exit; +} + +// Handle refresh all infomercials from Talpa +if (isset($_POST['refresh_all'])) { + $stmt = $db->query("SELECT id, title, media_asset_id FROM infomercials WHERE media_asset_id IS NOT NULL"); + $infomercials_to_refresh = $stmt->fetchAll(); + + $refreshed = 0; + $failed = 0; + $refreshLogs = []; + + foreach ($infomercials_to_refresh as $inf) { + $details = $api->getMediaAssetDetails($inf['media_asset_id']); + $apiLogs[] = ['call' => 'Refresh All - Media Asset Details', 'request' => [ + 'asset_id' => $inf['media_asset_id'] + ], 'response' => $api->lastResponse]; + + if (isset($details['mediaAssetLabel'])) { + $stmt = $db->prepare(" + UPDATE infomercials + SET media_asset_label = ? + WHERE id = ? + "); + $stmt->execute([ + $details['mediaAssetLabel'], + $inf['id'] + ]); + $refreshed++; + } else { + $failed++; + // Only log failures + $refreshLogs[] = [ + 'status' => 'failed', + 'title' => $inf['title'], + 'asset_id' => $inf['media_asset_id'], + 'error' => $api->lastResponse['message'] ?? 'Geen mediaAssetLabel gevonden in response' + ]; + } + } + + // Store logs in session for display + session_start(); + $_SESSION['refresh_logs'] = $refreshLogs; + + header("Location: infomercials.php?success=refreshed_all&count=$refreshed&failed=$failed"); exit; } @@ -185,8 +305,10 @@ $infomercials = $db->query(" 'Infomercial succesvol aangemaakt en geregistreerd bij Talpa!', - 'updated' => 'Infomercial succesvol bijgewerkt!', - 'deleted' => 'Infomercial succesvol verwijderd!' + 'updated' => 'Infomercial succesvol bijgewerkt en gesynchroniseerd naar Talpa!', + 'deleted' => 'Infomercial succesvol verwijderd!', + 'refreshed' => 'Infomercial succesvol ververst vanuit Talpa!', + 'refreshed_all' => 'Alle infomercials succesvol ververst! (' . ($_GET['count'] ?? 0) . ' gelukt' . (($_GET['failed'] ?? 0) > 0 ? ', ' . $_GET['failed'] . ' mislukt' : '') . ')' ]; echo $messages[$_GET['success']] ?? 'Actie succesvol uitgevoerd!'; ?> @@ -198,7 +320,8 @@ $infomercials = $db->query(" + 0): ?> +
+
+
+ Refresh Fouten +
+
+
+
+ + De volgende infomercials konden niet worden ververst vanuit Talpa: +
+
+ + + + + + + + + + + + + + + + + +
InfomercialAsset IDFoutmelding
+ + + + +
+
+
+ +
+
+
+ +
@@ -259,8 +428,16 @@ $infomercials = $db->query("
-
+
Geregistreerde Infomercials
+ +
+ +
+
@@ -320,18 +497,26 @@ $infomercials = $db->query("
- + + -
- @@ -364,9 +549,22 @@ $infomercials = $db->query("
@@ -402,6 +600,30 @@ $infomercials = $db->query(" diff --git a/plans/copy-blocks-feature-plan.md b/plans/copy-blocks-feature-plan.md new file mode 100644 index 0000000..d39a32b --- /dev/null +++ b/plans/copy-blocks-feature-plan.md @@ -0,0 +1,908 @@ +# 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:

    '; + data.transmissions.forEach(tx => { + html += `
  • ${tx.start_time.substring(0,5)} - ${tx.title}
  • `; + }); + html += '
'; + } + + 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 diff --git a/plans/sync-status-visual-indicators-plan.md b/plans/sync-status-visual-indicators-plan.md new file mode 100644 index 0000000..b573e4d --- /dev/null +++ b/plans/sync-status-visual-indicators-plan.md @@ -0,0 +1,595 @@ +# Plan: Visuele Sync Status Indicators in Planner + +## Overzicht + +Dit plan beschrijft de implementatie van visuele sync status indicators in de planner interface. Gebruikers kunnen direct zien of een transmission gesynchroniseerd is met Talpa of alleen lokaal bestaat. + +## Huidige Situatie + +### Database Schema +De `transmissions` tabel heeft de volgende relevante kolommen: +- `api_status` - Status van de sync: 'pending', 'synced', 'error' +- `talpa_transmission_id` - ID van de transmission in Talpa (NULL als nog niet gesynchroniseerd) +- `api_response` - JSON response van Talpa API + +### Sync Logica +- **Nieuwe transmissions**: `api_status = 'pending'`, `talpa_transmission_id = NULL` +- **Gesynchroniseerde transmissions**: `api_status = 'synced'`, `talpa_transmission_id` heeft waarde +- **Gefaalde sync**: `api_status = 'error'`, `talpa_transmission_id` kan NULL zijn + +## Vereisten + +### Functionele Vereisten +1. **Per Transmission Indicator**: Elke transmission moet een visuele indicator tonen + - 🔴 Rood bolletje: Niet gesynchroniseerd (pending/error) + - 🟢 Groen bolletje: Gesynchroniseerd met Talpa + +2. **Block-Level Indicator**: Elk blok moet een overall status tonen + - Alle transmissions gesynchroniseerd → Groen "Gepland" badge + - Één of meer transmissions niet gesynchroniseerd → Oranje "Deels Gepland" badge + - Geen transmissions gesynchroniseerd → Rood "Niet Gepland" badge + - Geen transmissions in blok → Grijs "Leeg" badge + +3. **Tooltip Informatie**: Bij hover over indicator extra details tonen + - Sync status + - Talpa transmission ID (indien beschikbaar) + - Laatste sync tijd (indien beschikbaar) + +### Niet-Functionele Vereisten +- Minimale impact op bestaande code +- Geen extra database queries (gebruik bestaande data) +- Responsive design (werkt op mobile) +- Consistent met bestaande UI styling + +## Ontwerp + +### Visuele Elementen + +#### 1. Transmission-Level Indicator (Kolom "Talpa") + +```html + + + + + + + + + + + + + + + + + + + + +``` + +**CSS Styling:** +```css +.sync-indicator { + font-size: 1.2rem; + cursor: help; +} + +.sync-indicator.sync-success { + color: #28a745; /* Groen */ +} + +.sync-indicator.sync-pending { + color: #dc3545; /* Rood */ +} + +.sync-indicator.sync-error { + color: #ffc107; /* Oranje/Geel */ +} + +.sync-indicator i { + transition: transform 0.2s; +} + +.sync-indicator:hover i { + transform: scale(1.2); +} +``` + +#### 2. Block-Level Indicator (In Header) + +```html +
+
+ + + + + Gepland + + + + + +
+
+ - + | Resterend: min +
+
+``` + +**Badge Varianten:** +```css +/* Volledig gesynchroniseerd */ +.block-sync-status.block-sync-complete { + background-color: #28a745; + color: white; +} + +/* Deels gesynchroniseerd */ +.block-sync-status.block-sync-partial { + background-color: #ffc107; + color: #000; +} + +/* Niet gesynchroniseerd */ +.block-sync-status.block-sync-none { + background-color: #dc3545; + color: white; +} + +/* Leeg blok */ +.block-sync-status.block-sync-empty { + background-color: #6c757d; + color: white; +} + +.block-sync-status { + font-size: 0.85rem; + padding: 0.35em 0.65em; + font-weight: 600; +} +``` + +### Database Queries + +#### Query voor Block Sync Status +```php +// In planner.php, binnen de block loop +$stmt = $db->prepare(" + SELECT + COUNT(*) as total, + SUM(CASE WHEN api_status = 'synced' AND talpa_transmission_id IS NOT NULL THEN 1 ELSE 0 END) as synced, + SUM(CASE WHEN api_status = 'error' THEN 1 ELSE 0 END) as errors + FROM transmissions t + WHERE t.start_date = ? + AND t.channel = ? + AND t.start_time >= ? + AND t.start_time < ? +"); +$stmt->execute([$selectedDate, $channel, $blockStart, $blockEnd]); +$blockSyncStats = $stmt->fetch(); + +// Bepaal block status +$blockSyncClass = 'block-sync-empty'; +$blockSyncLabel = 'Leeg'; +$blockSyncIcon = 'bi-inbox'; + +if ($blockSyncStats['total'] > 0) { + if ($blockSyncStats['synced'] == $blockSyncStats['total']) { + $blockSyncClass = 'block-sync-complete'; + $blockSyncLabel = 'Gepland'; + $blockSyncIcon = 'bi-check-circle-fill'; + } elseif ($blockSyncStats['synced'] > 0) { + $blockSyncClass = 'block-sync-partial'; + $blockSyncLabel = "Deels Gepland ({$blockSyncStats['synced']}/{$blockSyncStats['total']})"; + $blockSyncIcon = 'bi-exclamation-triangle-fill'; + } else { + $blockSyncClass = 'block-sync-none'; + $blockSyncLabel = 'Niet Gepland'; + $blockSyncIcon = 'bi-x-circle-fill'; + } +} +``` + +#### Query voor Transmission Details (Tooltip) +```php +// Reeds beschikbaar in bestaande query +// Voeg toe aan SELECT: +$stmt = $db->prepare(" + SELECT t.*, + c.title, + c.color_code, + c.series_code, + t.api_status, + t.talpa_transmission_id, + t.api_response + 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 +"); +``` + +### Implementatie Stappen + +#### Stap 1: Update CSS (assets/css/custom.css) +Voeg nieuwe CSS classes toe voor sync indicators: +```css +/* Sync Status Indicators */ +.sync-indicator { + font-size: 1.2rem; + cursor: help; + display: inline-block; + transition: transform 0.2s; +} + +.sync-indicator.sync-success { + color: #28a745; +} + +.sync-indicator.sync-pending { + color: #dc3545; +} + +.sync-indicator.sync-error { + color: #ffc107; +} + +.sync-indicator:hover { + transform: scale(1.2); +} + +/* Block Sync Status Badges */ +.block-sync-status { + font-size: 0.85rem; + padding: 0.35em 0.65em; + font-weight: 600; + border-radius: 4px; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.block-sync-status.block-sync-complete { + background-color: #28a745; + color: white; +} + +.block-sync-status.block-sync-partial { + background-color: #ffc107; + color: #000; +} + +.block-sync-status.block-sync-none { + background-color: #dc3545; + color: white; +} + +.block-sync-status.block-sync-empty { + background-color: #6c757d; + color: white; +} +``` + +#### Stap 2: Update Table Header (planner.php) +Voeg nieuwe kolom toe in table header (na "Restant", voor "Acties"): +```php + + + + Code + Product + Duur + Start + Eind + Restant + Talpa + Acties + + +``` + +#### Stap 3: Update Transmission Row (planner.php) +Voeg sync indicator toe in transmission row: +```php + + + +
+ + + + + + + + + min + + + + + + + + + min + + + + + + + + + + + + + + + +``` + +#### Stap 4: Update Block Header (planner.php) +Voeg block sync status badge toe: +```php +prepare(" + SELECT + COUNT(*) as total, + SUM(CASE WHEN api_status = 'synced' AND talpa_transmission_id IS NOT NULL THEN 1 ELSE 0 END) as synced, + SUM(CASE WHEN api_status = 'error' THEN 1 ELSE 0 END) as errors + FROM transmissions t + WHERE t.start_date = ? + AND t.channel = ? + AND t.start_time >= ? + AND t.start_time < ? +"); +$stmt->execute([$selectedDate, $channel, $blockStart, $blockEnd]); +$blockSyncStats = $stmt->fetch(); + +// Bepaal block status +$blockSyncClass = 'block-sync-empty'; +$blockSyncLabel = 'Leeg'; +$blockSyncIcon = 'bi-inbox'; + +if ($blockSyncStats['total'] > 0) { + if ($blockSyncStats['synced'] == $blockSyncStats['total']) { + $blockSyncClass = 'block-sync-complete'; + $blockSyncLabel = 'Gepland'; + $blockSyncIcon = 'bi-check-circle-fill'; + } elseif ($blockSyncStats['synced'] > 0) { + $blockSyncClass = 'block-sync-partial'; + $blockSyncLabel = "Deels ({$blockSyncStats['synced']}/{$blockSyncStats['total']})"; + $blockSyncIcon = 'bi-exclamation-triangle-fill'; + } else { + $blockSyncClass = 'block-sync-none'; + $blockSyncLabel = 'Niet Gepland'; + $blockSyncIcon = 'bi-x-circle-fill'; + } +} +?> + +
+
+ + + + + + + + + + +
+
+ - + | Resterend: min +
+
+``` + +#### Stap 5: Initialize Tooltips (planner.php) +Voeg JavaScript toe om Bootstrap tooltips te activeren: +```javascript + +``` + +#### Stap 6: Update na Sync Operatie +Zorg dat na een sync operatie de pagina wordt herladen om nieuwe status te tonen: +```javascript +// In syncBlockPlanner functie (reeds aanwezig) +if (data.success) { + // ... bestaande alert code ... + + // Reload page to show updated status + window.location.reload(); +} +``` + +## Mermaid Diagram: Sync Status Flow + +```mermaid +graph TD + A[Transmission Aangemaakt] --> B{api_status?} + B -->|pending| C[Rood Bolletje] + B -->|synced| D{talpa_transmission_id?} + B -->|error| E[Oranje Waarschuwing] + + D -->|NULL| C + D -->|Heeft waarde| F[Groen Bolletje] + + G[Block Status Check] --> H{Alle transmissions?} + H -->|Geen| I[Grijs: Leeg] + H -->|Alle synced| J[Groen: Gepland] + H -->|Deels synced| K[Oranje: Deels Gepland] + H -->|Geen synced| L[Rood: Niet Gepland] + + M[Sync Button Click] --> N[API Call] + N --> O{Success?} + O -->|Ja| P[Update Status] + O -->|Nee| Q[Toon Error] + P --> R[Reload Page] + R --> G +``` + +## Testing Checklist + +### Visuele Tests +- [ ] Rood bolletje toont voor nieuwe transmissions (pending) +- [ ] Groen bolletje toont voor gesynchroniseerde transmissions +- [ ] Oranje waarschuwing toont voor error status +- [ ] Tooltips tonen correcte informatie bij hover +- [ ] Block badge toont "Leeg" voor lege blokken +- [ ] Block badge toont "Gepland" wanneer alle transmissions synced zijn +- [ ] Block badge toont "Deels Gepland" met correcte telling +- [ ] Block badge toont "Niet Gepland" wanneer geen transmissions synced zijn + +### Functionele Tests +- [ ] Na sync operatie worden indicators bijgewerkt +- [ ] Indicators werken correct na drag-and-drop +- [ ] Indicators werken correct na reorder (up/down) +- [ ] Indicators werken correct na insert at position +- [ ] Tooltips werken op desktop +- [ ] Tooltips werken op mobile (touch) + +### Edge Cases +- [ ] Block met 0 transmissions +- [ ] Block met 1 transmission (synced) +- [ ] Block met 1 transmission (pending) +- [ ] Block met mix van synced/pending/error +- [ ] Transmission met zeer lange talpa_transmission_id +- [ ] Transmission zonder talpa_transmission_id maar met status 'synced' (data inconsistentie) + +## Performance Overwegingen + +### Database Queries +- **Huidige situatie**: 1 query per block voor transmissions +- **Nieuwe situatie**: +1 query per block voor sync stats +- **Optimalisatie**: Combineer beide queries in één: + +```php +$stmt = $db->prepare(" + SELECT + t.*, + c.title, + c.color_code, + c.series_code, + COUNT(*) OVER() as block_total, + SUM(CASE WHEN t.api_status = 'synced' AND t.talpa_transmission_id IS NOT NULL THEN 1 ELSE 0 END) OVER() as block_synced + 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 +"); +``` + +Dit gebruikt window functions om block stats te berekenen zonder extra query. + +### Frontend Performance +- Tooltips worden lazy geïnitialiseerd (alleen bij hover) +- Geen extra JavaScript libraries nodig (Bootstrap reeds aanwezig) +- Minimale CSS overhead (< 1KB) + +## Rollout Plan + +### Fase 1: Development +1. Implementeer CSS changes +2. Implementeer transmission-level indicators +3. Test in development environment + +### Fase 2: Block-Level Indicators +1. Implementeer block sync stats query +2. Implementeer block badge +3. Test met verschillende scenario's + +### Fase 3: Testing +1. Uitvoeren van alle tests in checklist +2. Performance testing met grote blokken (>20 transmissions) +3. Cross-browser testing (Chrome, Firefox, Safari) + +### Fase 4: Production +1. Deploy naar productie +2. Monitor voor errors +3. Verzamel gebruiker feedback + +## Toekomstige Verbeteringen + +### Fase 2 Features (Optioneel) +1. **Click-to-Sync**: Klik op rood bolletje om direct te syncen +2. **Batch Sync**: Selecteer meerdere transmissions en sync in één keer +3. **Auto-Refresh**: Automatisch status updaten zonder page reload (AJAX) +4. **Sync History**: Toon laatste sync tijd in tooltip +5. **Error Details Modal**: Klik op error indicator voor gedetailleerde foutmelding + +### Technische Verbeteringen +1. **Caching**: Cache block sync stats voor betere performance +2. **WebSocket**: Real-time updates van sync status +3. **Background Sync**: Automatisch syncen op achtergrond +4. **Conflict Detection**: Waarschuw bij conflicten tussen lokaal en Talpa + +## Conclusie + +Deze implementatie voegt duidelijke visuele feedback toe aan de planner zonder de bestaande functionaliteit te verstoren. Gebruikers kunnen in één oogopslag zien welke transmissions gesynchroniseerd zijn en welke blokken volledig gepland zijn in Talpa. + +De implementatie is: +- ✅ Minimaal invasief (kleine code changes) +- ✅ Performance-vriendelijk (minimale extra queries) +- ✅ Gebruiksvriendelijk (duidelijke visuele feedback) +- ✅ Uitbreidbaar (basis voor toekomstige features) diff --git a/plans/transmission-sync-update-plan.md b/plans/transmission-sync-update-plan.md new file mode 100644 index 0000000..d8d0314 --- /dev/null +++ b/plans/transmission-sync-update-plan.md @@ -0,0 +1,271 @@ +# Plan: Transmission Synchronisatie met Talpa Updates + +## Probleem Beschrijving + +Wanneer infomercials in de planning worden verplaatst, moeten de transmissions in Talpa ook worden bijgewerkt. Dit geldt voor: +- De verplaatste infomercial zelf +- Alle andere infomercials in hetzelfde uitzendblok (omdat hun starttijden verschuiven) + +Momenteel wordt alleen bij de eerste synchronisatie een transmission aangemaakt in Talpa, maar er is geen mechanisme om bestaande transmissions te updaten wanneer de planning wijzigt. + +## Huidige Situatie + +### Database Schema +De `transmissions` tabel heeft: +- `id` - lokale database ID +- `api_status` - status: 'pending', 'synced', 'error' +- `api_response` - JSON response van Talpa API +- **GEEN** dedicated kolom voor Talpa transmission ID + +### Huidige Flow +1. **Aanmaken**: [`create_transmission.php`](api/create_transmission.php) maakt lokale transmission aan met status 'pending' +2. **Sync**: [`sync_block.php`](api/sync_block.php) synchroniseert naar Talpa en zet status op 'synced' +3. **Update**: [`update_transmission.php`](api/update_transmission.php) update lokale data en zet status terug naar 'pending' +4. **Probleem**: Er is geen mechanisme om Talpa te updaten na wijzigingen + +### Talpa API +- `POST /linearSchedule/v1/transmissions` - Aanmaken (retourneert `id` in response) +- `PUT /linearSchedule/v1/transmissions/{id}` - Update (nog niet geïmplementeerd) + +## Oplossing Architectuur + +### 1. Database Wijzigingen + +Voeg nieuwe kolom toe aan `transmissions` tabel: +```sql +ALTER TABLE transmissions +ADD COLUMN talpa_transmission_id VARCHAR(100) NULL +COMMENT 'Talpa API transmission ID voor updates'; +``` + +**Rationale**: +- Dedicated kolom is beter dan JSON parsing uit `api_response` +- Makkelijker te indexeren en queryen +- Duidelijker in code + +### 2. TalpaAPI Uitbreiding + +Voeg nieuwe methode toe aan [`TalpaAPI.php`](TalpaAPI.php): +```php +public function updateTransmission($transmissionId, $data) { + return $this->request('PUT', '/linearSchedule/v1/transmissions/' . $transmissionId, [ + "startDate" => $data['startDate'], + "startTime" => $data['startTime'], + "duration" => $data['duration'] + ]); +} +``` + +### 3. Sync Block Wijzigingen + +Update [`sync_block.php`](api/sync_block.php): +- Bij succesvolle sync: extract `id` uit response en sla op in `talpa_transmission_id` +- Ondersteun zowel nieuwe transmissions (POST) als bestaande (PUT) + +**Logica**: +``` +IF talpa_transmission_id IS NULL: + → POST nieuwe transmission + → Sla transmission_id op +ELSE: + → PUT update bestaande transmission +``` + +### 4. Update Transmission Wijzigingen + +Update [`update_transmission.php`](api/update_transmission.php): +- Na lokale update: check of `talpa_transmission_id` bestaat +- Zo ja: direct Talpa updaten (niet wachten op sync) +- Update ook andere transmissions in hetzelfde blok + +**Cascade Update Logica**: +``` +1. Bepaal welk blok de transmission behoort +2. Haal alle transmissions in dat blok op (gesorteerd op tijd) +3. Herbereken starttijden vanaf de gewijzigde transmission +4. Update alle affected transmissions: + - Lokaal: nieuwe start_time + - Talpa: PUT update (als talpa_transmission_id bestaat) +``` + +### 5. Insert at Position Wijzigingen + +Update [`insert_transmission_at_position.php`](api/insert_transmission_at_position.php): +- Na insert: herbereken alle volgende transmissions in blok +- Update Talpa voor alle transmissions met `talpa_transmission_id` + +## Implementatie Details + +### Scenario 1: Nieuwe Transmission +```mermaid +graph LR + A[Create Transmission] --> B[Status: pending] + B --> C[Sync Block] + C --> D[POST to Talpa] + D --> E[Save talpa_transmission_id] + E --> F[Status: synced] +``` + +### Scenario 2: Update Bestaande Transmission +```mermaid +graph LR + A[Update Transmission] --> B[Check talpa_transmission_id] + B -->|Exists| C[PUT to Talpa] + B -->|NULL| D[Status: pending] + C --> E[Update Cascade] + E --> F[Update Other Transmissions in Block] + F --> G[PUT each to Talpa] +``` + +### Scenario 3: Insert at Position +```mermaid +graph LR + A[Insert at Position] --> B[Create New Transmission] + B --> C[Recalculate Block Times] + C --> D[Update All Following Transmissions] + D --> E[POST new to Talpa] + E --> F[PUT updates to Talpa] +``` + +## Benodigde Functies + +### Helper Function: updateBlockTransmissions() +```php +function updateBlockTransmissions($db, $api, $blockId, $date, $channel) { + // 1. Get all transmissions in block (ordered by time) + // 2. Recalculate start times + // 3. Update local database + // 4. Update Talpa (if talpa_transmission_id exists) + // 5. Return results +} +``` + +### Helper Function: syncTransmissionToTalpa() +```php +function syncTransmissionToTalpa($api, $transmission) { + if ($transmission['talpa_transmission_id']) { + // PUT update + return $api->updateTransmission($transmission['talpa_transmission_id'], $data); + } else { + // POST create + $result = $api->createTransmission($data); + return $result['id']; + } +} +``` + +## API Status Flow + +``` +pending → synced → pending → synced + ↓ ↓ ↓ ↓ + CREATE SYNC UPDATE RE-SYNC +``` + +**Status Betekenis**: +- `pending`: Lokale wijziging, nog niet gesynchroniseerd +- `synced`: In sync met Talpa +- `error`: Synchronisatie gefaald + +## Edge Cases + +### 1. Talpa Transmission ID Ontbreekt +**Situatie**: Oude data zonder `talpa_transmission_id` +**Oplossing**: Behandel als nieuwe transmission (POST) + +### 2. Talpa Update Faalt +**Situatie**: PUT request faalt (transmission bestaat niet meer in Talpa) +**Oplossing**: +- Zet `talpa_transmission_id` op NULL +- Zet status op 'pending' +- Bij volgende sync: POST nieuwe transmission + +### 3. Blok Overlap +**Situatie**: Transmission verplaatsen veroorzaakt overlap +**Oplossing**: Validatie blijft zoals nu (reject met error) + +### 4. Overnight Blocks +**Situatie**: Blok loopt over middernacht (bijv. 23:30 - 02:00) +**Oplossing**: Bestaande logica blijft werken (al geïmplementeerd) + +## Testing Scenario's + +### Test 1: Nieuwe Transmission Sync +1. Maak nieuwe transmission aan +2. Sync block +3. Verify: `talpa_transmission_id` is gevuld +4. Verify: `api_status` = 'synced' + +### Test 2: Update Transmission +1. Maak en sync transmission +2. Verplaats transmission naar andere tijd +3. Verify: Talpa is geüpdatet (PUT call) +4. Verify: Andere transmissions in blok zijn ook geüpdatet + +### Test 3: Insert at Position +1. Maak blok met 3 transmissions (alle synced) +2. Insert nieuwe transmission op positie 2 +3. Verify: Nieuwe transmission is aangemaakt in Talpa +4. Verify: Transmissions 2 en 3 zijn geüpdatet in Talpa + +### Test 4: Cascade Update +1. Maak blok met 5 transmissions +2. Verplaats transmission 2 naar latere tijd +3. Verify: Transmissions 3, 4, 5 hebben nieuwe starttijden +4. Verify: Alle updates zijn naar Talpa gestuurd + +### Test 5: Error Recovery +1. Maak transmission met talpa_transmission_id +2. Simuleer Talpa error (transmission bestaat niet) +3. Verify: `talpa_transmission_id` wordt NULL +4. Verify: Status wordt 'pending' +5. Re-sync: nieuwe transmission wordt aangemaakt + +## Migratie Strategie + +### Voor Bestaande Data +```sql +-- Optioneel: Probeer transmission_id uit api_response te extracten +UPDATE transmissions +SET talpa_transmission_id = JSON_EXTRACT(api_response, '$.id') +WHERE api_status = 'synced' +AND api_response IS NOT NULL +AND JSON_VALID(api_response); +``` + +**Note**: Dit werkt alleen als `api_response` het `id` veld bevat. + +## Voordelen van Deze Aanpak + +1. **Incrementeel**: Werkt met bestaande data +2. **Robuust**: Fallback naar POST als PUT faalt +3. **Efficiënt**: Direct updaten ipv wachten op sync +4. **Consistent**: Talpa blijft in sync met lokale planning +5. **Traceerbaar**: Duidelijke status en logging + +## Risico's en Mitigatie + +| Risico | Impact | Mitigatie | +|--------|--------|-----------| +| Talpa API rate limiting | Medium | Batch updates, retry logic | +| Netwerk failures | Medium | Error handling, status tracking | +| Data inconsistentie | Hoog | Transaction wrapping, rollback | +| Oude data zonder ID | Laag | Behandel als nieuwe transmission | + +## Implementatie Volgorde + +1. ✅ Database migratie (nieuwe kolom) +2. ✅ TalpaAPI uitbreiding (updateTransmission) +3. ✅ sync_block.php update (save transmission_id) +4. ✅ Helper functions (updateBlockTransmissions, syncTransmissionToTalpa) +5. ✅ update_transmission.php update (cascade updates) +6. ✅ insert_transmission_at_position.php update +7. ✅ Testing en validatie +8. ✅ Documentatie + +## Vragen voor Gebruiker + +1. **Moet er een retry mechanisme komen** voor gefaalde Talpa updates? +2. **Moeten we een audit log bijhouden** van alle Talpa synchronisaties? +3. **Wat gebeurt er als een transmission in Talpa is verwijderd** maar lokaal nog bestaat? +4. **Moeten we batch updates ondersteunen** (meerdere transmissions in één API call)?