Block syncing and copying added
This commit is contained in:
parent
ced3b87e1d
commit
9939300245
22
TalpaAPI.php
22
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) {
|
public function deleteEpisode($contentId) {
|
||||||
return $this->request('DELETE', '/content/v1/episodes/' . $contentId);
|
return $this->request('DELETE', '/content/v1/episodes/' . $contentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getMockResponse($endpoint, $data) {
|
private function getMockResponse($endpoint, $data) {
|
||||||
usleep(200000);
|
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, '/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 && !isset($data)) return ["mediaAssetLabel" => "TEL_MOCK_" . rand(100, 999)];
|
||||||
if (strpos($endpoint, '/mam/v1/mediaAssets') !== false) return ["id" => "MOCK_ASSET_" . time()];
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
170
api/copy_block.php
Normal file
170
api/copy_block.php
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Copy Block
|
||||||
|
* Copies a block (block times + transmissions) from source block to target block
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Get POST data
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
$sourceBlockId = $input['source_block_id'] ?? null;
|
||||||
|
$targetBlockId = $input['target_block_id'] ?? null;
|
||||||
|
$targetDate = $input['target_date'] ?? null;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (!$sourceBlockId || !$targetBlockId || !$targetDate) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Missing required parameters'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidDate($targetDate)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Invalid target date format'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transaction
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get source block info
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT * FROM daily_blocks WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$sourceBlockId]);
|
||||||
|
$sourceBlock = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$sourceBlock) {
|
||||||
|
throw new Exception('Source block not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get target block info
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT * FROM daily_blocks WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$targetBlockId]);
|
||||||
|
$targetBlock = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$targetBlock) {
|
||||||
|
throw new Exception('Target block not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that blocks are compatible (same channel)
|
||||||
|
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);
|
||||||
|
}
|
||||||
74
api/get_available_source_blocks.php
Normal file
74
api/get_available_source_blocks.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Get Available Source Blocks
|
||||||
|
* Returns available source blocks for a specific date, channel, and template
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
$date = $_GET['date'] ?? null;
|
||||||
|
$channel = $_GET['channel'] ?? null;
|
||||||
|
$templateId = $_GET['template_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$date || !$channel || !$templateId) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Missing parameters'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidDate($date)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Invalid date format'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure blocks exist for this date
|
||||||
|
ensureDailyBlocks($db, $date, $date);
|
||||||
|
|
||||||
|
// Get 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);
|
||||||
|
}
|
||||||
67
api/get_block_details.php
Normal file
67
api/get_block_details.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Get Block Details
|
||||||
|
* Returns detailed information about a specific block (for preview)
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
$blockId = $_GET['block_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$blockId) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Missing block_id'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get block info
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT db.*, bt.name as template_name
|
||||||
|
FROM daily_blocks db
|
||||||
|
LEFT JOIN block_templates bt ON db.template_id = bt.id
|
||||||
|
WHERE db.id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$blockId]);
|
||||||
|
$block = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$block) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Block not found'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get transmissions in this block
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.*, c.title, c.series_code
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN infomercials c ON t.infomercial_id = c.id
|
||||||
|
WHERE t.start_date = ?
|
||||||
|
AND t.channel = ?
|
||||||
|
AND t.start_time >= ?
|
||||||
|
AND t.start_time < ?
|
||||||
|
ORDER BY t.start_time ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$block['block_date'],
|
||||||
|
$block['channel'],
|
||||||
|
$block['actual_start_time'],
|
||||||
|
$block['actual_end_time'] ?? '23:59:59'
|
||||||
|
]);
|
||||||
|
$transmissions = $stmt->fetchAll();
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'block' => $block,
|
||||||
|
'transmissions' => $transmissions
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../TalpaAPI.php';
|
||||||
require_once __DIR__ . '/../helpers.php';
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
use Dotenv\Dotenv;
|
use Dotenv\Dotenv;
|
||||||
@ -80,6 +81,7 @@ try {
|
|||||||
|
|
||||||
// Recalculate all start times
|
// Recalculate all start times
|
||||||
$currentTime = $blockStartTime;
|
$currentTime = $blockStartTime;
|
||||||
|
$newTransmissionId = null;
|
||||||
|
|
||||||
foreach ($transmissions as $index => $tx) {
|
foreach ($transmissions as $index => $tx) {
|
||||||
if ($index === $position) {
|
if ($index === $position) {
|
||||||
@ -96,6 +98,7 @@ try {
|
|||||||
$currentTime,
|
$currentTime,
|
||||||
$duration
|
$duration
|
||||||
]);
|
]);
|
||||||
|
$newTransmissionId = $db->lastInsertId();
|
||||||
} else {
|
} else {
|
||||||
// Update existing transmission
|
// Update existing transmission
|
||||||
$stmt = $db->prepare("UPDATE transmissions SET start_time = ?, api_status = 'pending' WHERE id = ?");
|
$stmt = $db->prepare("UPDATE transmissions SET start_time = ?, api_status = 'pending' WHERE id = ?");
|
||||||
@ -105,9 +108,21 @@ try {
|
|||||||
$currentTime = addTimeToTime($currentTime, $tx['duration']);
|
$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([
|
jsonResponse([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Transmission inserted successfully'
|
'message' => 'Transmission inserted successfully',
|
||||||
|
'new_transmission_id' => $newTransmissionId,
|
||||||
|
'block_update' => $blockUpdateResult
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|||||||
@ -43,14 +43,14 @@ $channel = $input['channel'];
|
|||||||
$debugLog[] = ['step' => 'Parameters', 'date' => $date, 'channel' => $channel];
|
$debugLog[] = ['step' => 'Parameters', 'date' => $date, 'channel' => $channel];
|
||||||
|
|
||||||
try {
|
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("
|
$stmt = $db->prepare("
|
||||||
SELECT t.*, c.content_id, c.title as commercial_title
|
SELECT t.*, c.content_id, c.title as commercial_title
|
||||||
FROM transmissions t
|
FROM transmissions t
|
||||||
JOIN infomercials c ON t.infomercial_id = c.id
|
JOIN infomercials c ON t.infomercial_id = c.id
|
||||||
WHERE t.start_date = ?
|
WHERE t.start_date = ?
|
||||||
AND t.channel = ?
|
AND t.channel = ?
|
||||||
AND t.api_status != 'synced'
|
|
||||||
ORDER BY t.start_time ASC
|
ORDER BY t.start_time ASC
|
||||||
");
|
");
|
||||||
$stmt->execute([$date, $channel]);
|
$stmt->execute([$date, $channel]);
|
||||||
@ -79,7 +79,8 @@ try {
|
|||||||
$txDebug = [
|
$txDebug = [
|
||||||
'transmission_id' => $tx['id'],
|
'transmission_id' => $tx['id'],
|
||||||
'title' => $tx['commercial_title'],
|
'title' => $tx['commercial_title'],
|
||||||
'time' => $tx['start_time']
|
'time' => $tx['start_time'],
|
||||||
|
'talpa_transmission_id' => $tx['talpa_transmission_id']
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -94,23 +95,52 @@ try {
|
|||||||
|
|
||||||
$txDebug['request'] = $requestData;
|
$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['response'] = $res;
|
||||||
$txDebug['lastResponse'] = $api->lastResponse;
|
$txDebug['lastResponse'] = $api->lastResponse;
|
||||||
|
|
||||||
// Check if sync was successful
|
// 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['determined_status'] = $status;
|
||||||
|
$txDebug['talpa_transmission_id_saved'] = $talpaTransmissionId;
|
||||||
|
|
||||||
// Update transmission status
|
// Update transmission status and talpa_transmission_id
|
||||||
$updateStmt = $db->prepare("
|
$updateStmt = $db->prepare("
|
||||||
UPDATE transmissions
|
UPDATE transmissions
|
||||||
SET api_status = ?, api_response = ?
|
SET api_status = ?, api_response = ?, talpa_transmission_id = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
");
|
");
|
||||||
$updateStmt->execute([$status, json_encode($res), $tx['id']]);
|
$updateStmt->execute([$status, json_encode($res), $talpaTransmissionId, $tx['id']]);
|
||||||
|
|
||||||
if ($status === 'synced') {
|
if ($status === 'synced') {
|
||||||
$syncedCount++;
|
$syncedCount++;
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../TalpaAPI.php';
|
||||||
require_once __DIR__ . '/../helpers.php';
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
use Dotenv\Dotenv;
|
use Dotenv\Dotenv;
|
||||||
@ -155,9 +156,6 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as pending sync if changed
|
|
||||||
$updates[] = "api_status = 'pending'";
|
|
||||||
|
|
||||||
// Build and execute update query
|
// Build and execute update query
|
||||||
if (!empty($updates)) {
|
if (!empty($updates)) {
|
||||||
$sql = "UPDATE transmissions SET " . implode(', ', $updates) . " WHERE id = ?";
|
$sql = "UPDATE transmissions SET " . implode(', ', $updates) . " WHERE id = ?";
|
||||||
@ -167,13 +165,14 @@ try {
|
|||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get updated transmission
|
// Get updated transmission with all details including content_id
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT
|
SELECT
|
||||||
t.*,
|
t.*,
|
||||||
c.title,
|
c.title,
|
||||||
c.color_code,
|
c.color_code,
|
||||||
c.series_code
|
c.series_code,
|
||||||
|
c.content_id
|
||||||
FROM transmissions t
|
FROM transmissions t
|
||||||
JOIN infomercials c ON t.infomercial_id = c.id
|
JOIN infomercials c ON t.infomercial_id = c.id
|
||||||
WHERE t.id = ?
|
WHERE t.id = ?
|
||||||
@ -181,10 +180,54 @@ try {
|
|||||||
$stmt->execute([$input['id']]);
|
$stmt->execute([$input['id']]);
|
||||||
$updated = $stmt->fetch();
|
$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([
|
jsonResponse([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Transmission updated successfully',
|
'message' => 'Transmission updated successfully',
|
||||||
'transmission' => $updated
|
'transmission' => $updated,
|
||||||
|
'talpa_sync' => $talpaResult,
|
||||||
|
'block_update' => $blockUpdateResult
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|||||||
@ -3,9 +3,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--primary-color: #2c3e50;
|
--text-color: #404040;
|
||||||
--secondary-color: #34495e;
|
--brand-color: #32327D;
|
||||||
--accent-color: #3498db;
|
--accent-color: #32B4AA;
|
||||||
|
--primary-color: #32327D;
|
||||||
|
--secondary-color: #32327D;
|
||||||
--success-color: #2ecc71;
|
--success-color: #2ecc71;
|
||||||
--warning-color: #f39c12;
|
--warning-color: #f39c12;
|
||||||
--danger-color: #e74c3c;
|
--danger-color: #e74c3c;
|
||||||
@ -13,12 +15,22 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.3rem;
|
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 */
|
/* Cards */
|
||||||
@ -38,6 +50,10 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-header.bg-primary {
|
||||||
|
background-color: var(--brand-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn {
|
.btn {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@ -50,6 +66,31 @@ body {
|
|||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
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 */
|
/* Tables */
|
||||||
.table {
|
.table {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -461,16 +502,71 @@ body {
|
|||||||
.form-control:focus,
|
.form-control:focus,
|
||||||
.form-select:focus {
|
.form-select:focus {
|
||||||
border-color: var(--accent-color);
|
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 Styling */
|
||||||
.tooltip-inner {
|
.tooltip-inner {
|
||||||
background-color: var(--primary-color);
|
background-color: var(--brand-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px 12px;
|
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 */
|
/* Print Styles */
|
||||||
@media print {
|
@media print {
|
||||||
.navbar,
|
.navbar,
|
||||||
|
|||||||
@ -16,10 +16,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
|
schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
|
||||||
initialView: 'resourceTimeGridDay',
|
initialView: 'resourceTimeGridDay',
|
||||||
headerToolbar: {
|
headerToolbar: {
|
||||||
left: 'prev,next today',
|
left: 'prevDay,prev,next,nextDay today',
|
||||||
center: 'title',
|
center: 'title',
|
||||||
right: 'resourceTimeGridDay,resourceTimeGridWeek,resourceTimelineDay'
|
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',
|
slotDuration: '00:15:00',
|
||||||
slotLabelInterval: '00:15:00',
|
slotLabelInterval: '00:15:00',
|
||||||
snapDuration: '00:01:00',
|
snapDuration: '00:01:00',
|
||||||
|
|||||||
307
docs/IMPLEMENTATION_SUMMARY.md
Normal file
307
docs/IMPLEMENTATION_SUMMARY.md
Normal file
@ -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
|
||||||
373
docs/TRANSMISSION_SYNC_TESTING.md
Normal file
373
docs/TRANSMISSION_SYNC_TESTING.md
Normal file
@ -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
|
||||||
195
helpers.php
195
helpers.php
@ -385,3 +385,198 @@ function isValidDate($date) {
|
|||||||
$d = DateTime::createFromFormat('Y-m-d', $date);
|
$d = DateTime::createFromFormat('Y-m-d', $date);
|
||||||
return $d && $d->format('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;
|
||||||
|
}
|
||||||
|
|||||||
270
infomercials.php
270
infomercials.php
@ -20,6 +20,13 @@ $api = new TalpaApi();
|
|||||||
$db = getDbConnection();
|
$db = getDbConnection();
|
||||||
$apiLogs = [];
|
$apiLogs = [];
|
||||||
|
|
||||||
|
// Retrieve and clear refresh logs from session
|
||||||
|
session_start();
|
||||||
|
$refreshLogs = $_SESSION['refresh_logs'] ?? null;
|
||||||
|
if ($refreshLogs) {
|
||||||
|
unset($_SESSION['refresh_logs']);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle infomercial registration
|
// Handle infomercial registration
|
||||||
if (isset($_POST['add_commercial'])) {
|
if (isset($_POST['add_commercial'])) {
|
||||||
$apiLogs[] = ['step' => 'Start registration', 'input' => $_POST];
|
$apiLogs[] = ['step' => 'Start registration', 'input' => $_POST];
|
||||||
@ -84,21 +91,134 @@ if (isset($_POST['add_commercial'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle media asset update
|
// Handle infomercial update (sync to Talpa)
|
||||||
if (isset($_POST['update_media_asset'])) {
|
if (isset($_POST['update_infomercial'])) {
|
||||||
$stmt = $db->prepare("
|
// Get current infomercial data
|
||||||
UPDATE infomercials
|
$stmt = $db->prepare("SELECT content_id, title, duration FROM infomercials WHERE id = ?");
|
||||||
SET media_asset_label = ?, upload_status = ?, series_code = ?, color_code = ?
|
$stmt->execute([$_POST['infomercial_id']]);
|
||||||
WHERE id = ?
|
$current = $stmt->fetch();
|
||||||
");
|
|
||||||
$stmt->execute([
|
if ($current && $current['content_id']) {
|
||||||
$_POST['media_asset_label'],
|
// Update in Talpa if title or duration changed
|
||||||
$_POST['upload_status'],
|
$titleChanged = $current['title'] !== $_POST['title'];
|
||||||
$_POST['series_code'] ?? null,
|
$durationChanged = $current['duration'] !== $_POST['duration'];
|
||||||
$_POST['color_code'],
|
|
||||||
$_POST['infomercial_id']
|
if ($titleChanged || $durationChanged) {
|
||||||
]);
|
$result = $api->updateEpisode(
|
||||||
header('Location: infomercials.php?success=updated');
|
$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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,8 +305,10 @@ $infomercials = $db->query("
|
|||||||
<?php
|
<?php
|
||||||
$messages = [
|
$messages = [
|
||||||
'created' => 'Infomercial succesvol aangemaakt en geregistreerd bij Talpa!',
|
'created' => 'Infomercial succesvol aangemaakt en geregistreerd bij Talpa!',
|
||||||
'updated' => 'Infomercial succesvol bijgewerkt!',
|
'updated' => 'Infomercial succesvol bijgewerkt en gesynchroniseerd naar Talpa!',
|
||||||
'deleted' => 'Infomercial succesvol verwijderd!'
|
'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!';
|
echo $messages[$_GET['success']] ?? 'Actie succesvol uitgevoerd!';
|
||||||
?>
|
?>
|
||||||
@ -198,7 +320,8 @@ $infomercials = $db->query("
|
|||||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
<?php
|
<?php
|
||||||
$messages = [
|
$messages = [
|
||||||
'in_use' => 'Deze infomercial kan niet verwijderd worden omdat deze nog in gebruik is in de planning!'
|
'in_use' => 'Deze infomercial kan niet verwijderd worden omdat deze nog in gebruik is in de planning!',
|
||||||
|
'refresh_failed' => 'Kon infomercial niet verversen vanuit Talpa!'
|
||||||
];
|
];
|
||||||
echo $messages[$_GET['error']] ?? 'Er is een fout opgetreden!';
|
echo $messages[$_GET['error']] ?? 'Er is een fout opgetreden!';
|
||||||
?>
|
?>
|
||||||
@ -206,6 +329,52 @@ $infomercials = $db->query("
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($refreshLogs && count($refreshLogs) > 0): ?>
|
||||||
|
<div class="card shadow-sm mb-4 border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> Refresh Fouten
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-warning mb-3">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
De volgende infomercials konden niet worden ververst vanuit Talpa:
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Infomercial</th>
|
||||||
|
<th>Asset ID</th>
|
||||||
|
<th>Foutmelding</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($refreshLogs as $log): ?>
|
||||||
|
<tr class="table-danger">
|
||||||
|
<td><strong><?= htmlspecialchars($log['title']) ?></strong></td>
|
||||||
|
<td><code class="small"><?= htmlspecialchars($log['asset_id']) ?></code></td>
|
||||||
|
<td>
|
||||||
|
<span class="text-danger">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<?= htmlspecialchars($log['error']) ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" onclick="this.closest('.card').remove()">
|
||||||
|
<i class="bi bi-x"></i> Sluiten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Registration Form -->
|
<!-- Registration Form -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@ -259,8 +428,16 @@ $infomercials = $db->query("
|
|||||||
<!-- Infomercial List -->
|
<!-- Infomercial List -->
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-secondary text-white">
|
<div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Geregistreerde Infomercials</h5>
|
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Geregistreerde Infomercials</h5>
|
||||||
|
<?php if (!empty($infomercials)): ?>
|
||||||
|
<form method="POST" style="display:inline;"
|
||||||
|
onsubmit="return confirm('Weet je zeker dat je alle infomercials wilt verversen vanuit Talpa?');">
|
||||||
|
<button type="submit" name="refresh_all" class="btn btn-sm btn-light" title="Ververs alle infomercials vanuit Talpa">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> Check Alles
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<?php if (empty($infomercials)): ?>
|
<?php if (empty($infomercials)): ?>
|
||||||
@ -320,6 +497,14 @@ $infomercials = $db->query("
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
|
<form method="POST" style="display:inline;">
|
||||||
|
<input type="hidden" name="infomercial_id" value="<?= $c['id'] ?>">
|
||||||
|
<button type="submit" name="refresh_infomercial"
|
||||||
|
class="btn btn-outline-info"
|
||||||
|
title="Ververs vanuit Talpa">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-outline-primary"
|
class="btn btn-outline-primary"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -364,9 +549,22 @@ $infomercials = $db->query("
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Media Asset Label</label>
|
<label class="form-label">Titel</label>
|
||||||
<input type="text" name="media_asset_label" class="form-control"
|
<input type="text" name="title" class="form-control"
|
||||||
value="<?= htmlspecialchars($c['media_asset_label']) ?>">
|
value="<?= htmlspecialchars($c['title']) ?>" required
|
||||||
|
id="title_<?= $c['id'] ?>">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-cloud-arrow-up"></i> Wordt gesynchroniseerd naar Talpa
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Duur (HH:MM:SS)</label>
|
||||||
|
<input type="text" name="duration" class="form-control"
|
||||||
|
value="<?= htmlspecialchars($c['duration']) ?>"
|
||||||
|
pattern="[0-9]{2}:[0-9]{2}:[0-9]{2}" required>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-cloud-arrow-up"></i> Wordt gesynchroniseerd naar Talpa
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Upload Status</label>
|
<label class="form-label">Upload Status</label>
|
||||||
@ -389,8 +587,8 @@ $infomercials = $db->query("
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
|
||||||
<button type="submit" name="update_media_asset" class="btn btn-primary">
|
<button type="submit" name="update_infomercial" class="btn btn-primary">
|
||||||
<i class="bi bi-check-circle"></i> Opslaan
|
<i class="bi bi-cloud-arrow-up"></i> Opslaan & Sync naar Talpa
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -402,6 +600,30 @@ $infomercials = $db->query("
|
|||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// Debug: Log modal open events and check if title field is editable
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const modals = document.querySelectorAll('[id^="editModal"]');
|
||||||
|
modals.forEach(modal => {
|
||||||
|
modal.addEventListener('shown.bs.modal', function() {
|
||||||
|
const titleInput = this.querySelector('input[name="title"]');
|
||||||
|
console.log('Modal opened:', {
|
||||||
|
modalId: this.id,
|
||||||
|
titleInput: titleInput,
|
||||||
|
isDisabled: titleInput?.disabled,
|
||||||
|
isReadonly: titleInput?.readOnly,
|
||||||
|
value: titleInput?.value
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force enable the title field if it's disabled
|
||||||
|
if (titleInput) {
|
||||||
|
titleInput.disabled = false;
|
||||||
|
titleInput.readOnly = false;
|
||||||
|
console.log('Title field enabled for editing');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// API Logs to console with enhanced debugging
|
// API Logs to console with enhanced debugging
|
||||||
const apiLogs = <?= json_encode($apiLogs) ?>;
|
const apiLogs = <?= json_encode($apiLogs) ?>;
|
||||||
if (apiLogs.length > 0) {
|
if (apiLogs.length > 0) {
|
||||||
|
|||||||
22
migrations/002_add_talpa_transmission_id.sql
Normal file
22
migrations/002_add_talpa_transmission_id.sql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
-- Migration: Add Talpa Transmission ID to transmissions table
|
||||||
|
-- Date: 2026-01-16
|
||||||
|
-- Description: Adds column to store Talpa API transmission ID for updates
|
||||||
|
|
||||||
|
-- Add talpa_transmission_id column
|
||||||
|
ALTER TABLE transmissions
|
||||||
|
ADD COLUMN talpa_transmission_id VARCHAR(100) NULL
|
||||||
|
COMMENT 'Talpa API transmission ID for updates'
|
||||||
|
AFTER api_response;
|
||||||
|
|
||||||
|
-- Add index for faster lookups
|
||||||
|
ALTER TABLE transmissions
|
||||||
|
ADD INDEX idx_talpa_transmission_id (talpa_transmission_id);
|
||||||
|
|
||||||
|
-- Optional: Try to extract transmission_id from existing api_response JSON
|
||||||
|
-- This will only work if api_response contains the 'id' field
|
||||||
|
UPDATE transmissions
|
||||||
|
SET talpa_transmission_id = JSON_UNQUOTE(JSON_EXTRACT(api_response, '$.id'))
|
||||||
|
WHERE api_status = 'synced'
|
||||||
|
AND api_response IS NOT NULL
|
||||||
|
AND JSON_VALID(api_response)
|
||||||
|
AND JSON_EXTRACT(api_response, '$.id') IS NOT NULL;
|
||||||
313
planner.php
313
planner.php
@ -172,10 +172,9 @@ foreach ($allBlocks as $block) {
|
|||||||
$blocksByChannel[$block['channel']][] = $block;
|
$blocksByChannel[$block['channel']][] = $block;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all infomercials
|
// Get all infomercials (including pending status - they can be used in planner)
|
||||||
$infomercials = $db->query("
|
$infomercials = $db->query("
|
||||||
SELECT * FROM infomercials
|
SELECT * FROM infomercials
|
||||||
WHERE upload_status = 'uploaded'
|
|
||||||
ORDER BY title ASC
|
ORDER BY title ASC
|
||||||
")->fetchAll();
|
")->fetchAll();
|
||||||
|
|
||||||
@ -297,16 +296,30 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
|
|||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="GET" class="row g-3 align-items-end">
|
<form method="GET" class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-1">
|
||||||
|
<a href="?date=<?= date('Y-m-d', strtotime($selectedDate . ' -1 day')) ?>"
|
||||||
|
class="btn btn-outline-primary w-100"
|
||||||
|
title="Vorige dag">
|
||||||
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label"><strong>Selecteer Datum</strong></label>
|
<label class="form-label"><strong>Selecteer Datum</strong></label>
|
||||||
<input type="date" name="date" class="form-control" value="<?= $selectedDate ?>" required>
|
<input type="date" name="date" class="form-control" value="<?= $selectedDate ?>" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<a href="?date=<?= date('Y-m-d', strtotime($selectedDate . ' +1 day')) ?>"
|
||||||
|
class="btn btn-outline-primary w-100"
|
||||||
|
title="Volgende dag">
|
||||||
|
<i class="bi bi-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
<i class="bi bi-search"></i> Toon Planning
|
<i class="bi bi-search"></i> Toon Planning
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-7 text-end">
|
<div class="col-md-5 text-end">
|
||||||
<a href="calendar.php" class="btn btn-outline-secondary">
|
<a href="calendar.php" class="btn btn-outline-secondary">
|
||||||
<i class="bi bi-calendar-week"></i> Kalender View
|
<i class="bi bi-calendar-week"></i> Kalender View
|
||||||
</a>
|
</a>
|
||||||
@ -406,7 +419,8 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
|
|||||||
$blockEnd = $block['actual_end_time'] ?? '23:59:59';
|
$blockEnd = $block['actual_end_time'] ?? '23:59:59';
|
||||||
|
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT t.*, c.title, c.color_code, c.series_code
|
SELECT t.*, c.title, c.color_code, c.series_code,
|
||||||
|
t.api_status, t.talpa_transmission_id
|
||||||
FROM transmissions t
|
FROM transmissions t
|
||||||
JOIN infomercials c ON t.infomercial_id = c.id
|
JOIN infomercials c ON t.infomercial_id = c.id
|
||||||
WHERE t.start_date = ? AND t.channel = ?
|
WHERE t.start_date = ? AND t.channel = ?
|
||||||
@ -423,16 +437,60 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
|
|||||||
$usedMinutes += timeToSeconds($tx['duration']) / 60;
|
$usedMinutes += timeToSeconds($tx['duration']) / 60;
|
||||||
}
|
}
|
||||||
$remainingMinutes = $totalBlockMinutes - $usedMinutes;
|
$remainingMinutes = $totalBlockMinutes - $usedMinutes;
|
||||||
|
|
||||||
|
// Calculate block sync status
|
||||||
|
$totalTransmissions = count($blockTransmissions);
|
||||||
|
$syncedTransmissions = 0;
|
||||||
|
foreach ($blockTransmissions as $tx) {
|
||||||
|
if ($tx['api_status'] === 'synced' && !empty($tx['talpa_transmission_id'])) {
|
||||||
|
$syncedTransmissions++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine block sync status
|
||||||
|
$blockSyncClass = 'block-sync-empty';
|
||||||
|
$blockSyncLabel = 'Leeg';
|
||||||
|
$blockSyncIcon = 'bi-inbox';
|
||||||
|
|
||||||
|
if ($totalTransmissions > 0) {
|
||||||
|
if ($syncedTransmissions == $totalTransmissions) {
|
||||||
|
$blockSyncClass = 'block-sync-complete';
|
||||||
|
$blockSyncLabel = 'Gepland';
|
||||||
|
$blockSyncIcon = 'bi-check-circle-fill';
|
||||||
|
} elseif ($syncedTransmissions > 0) {
|
||||||
|
$blockSyncClass = 'block-sync-partial';
|
||||||
|
$blockSyncLabel = "Deels ({$syncedTransmissions}/{$totalTransmissions})";
|
||||||
|
$blockSyncIcon = 'bi-exclamation-triangle-fill';
|
||||||
|
} else {
|
||||||
|
$blockSyncClass = 'block-sync-none';
|
||||||
|
$blockSyncLabel = 'Niet Gepland';
|
||||||
|
$blockSyncIcon = 'bi-x-circle-fill';
|
||||||
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<div class="block-section" style="border-left-color: <?= $channel == 'SBS9' ? '#3498db' : '#e74c3c' ?>;">
|
<div class="block-section" style="border-left-color: <?= $channel == 'SBS9' ? '#3498db' : '#e74c3c' ?>;">
|
||||||
<div class="block-header d-flex justify-content-between align-items-center">
|
<div class="block-header d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<?= htmlspecialchars($block['template_name'] ?? 'Blok') ?>
|
<?= htmlspecialchars($block['template_name'] ?? 'Blok') ?>
|
||||||
|
|
||||||
|
<!-- Block Sync Status Badge -->
|
||||||
|
<span class="badge block-sync-status <?= $blockSyncClass ?> ms-2"
|
||||||
|
title="<?= $syncedTransmissions ?> van <?= $totalTransmissions ?> transmissions gesynchroniseerd"
|
||||||
|
data-bs-toggle="tooltip">
|
||||||
|
<i class="bi <?= $blockSyncIcon ?>"></i> <?= $blockSyncLabel ?>
|
||||||
|
</span>
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-dark ms-2"
|
<button type="button" class="btn btn-sm btn-outline-dark ms-2"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#blockTimeModal<?= $block['id'] ?>"
|
data-bs-target="#blockTimeModal<?= $block['id'] ?>"
|
||||||
title="Starttijd aanpassen">
|
title="Starttijd aanpassen">
|
||||||
<i class="bi bi-clock"></i> Tijd aanpassen
|
<i class="bi bi-clock"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-info ms-2"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#copyBlockModal<?= $block['id'] ?>"
|
||||||
|
title="Kopieer blok van andere dag">
|
||||||
|
<i class="bi bi-files"></i> Kopieer Blok
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-success ms-2"
|
<button type="button" class="btn btn-sm btn-success ms-2"
|
||||||
onclick="syncBlockPlanner('<?= $selectedDate ?>', '<?= $channel ?>')"
|
onclick="syncBlockPlanner('<?= $selectedDate ?>', '<?= $channel ?>')"
|
||||||
@ -456,6 +514,7 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
|
|||||||
<th style="width: 70px;">Start</th>
|
<th style="width: 70px;">Start</th>
|
||||||
<th style="width: 70px;">Eind</th>
|
<th style="width: 70px;">Eind</th>
|
||||||
<th style="width: 80px;">Restant</th>
|
<th style="width: 80px;">Restant</th>
|
||||||
|
<th style="width: 50px;" class="text-center">Talpa</th>
|
||||||
<th style="width: 100px;">Acties</th>
|
<th style="width: 100px;">Acties</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -497,6 +556,29 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
|
|||||||
<td class="text-center remaining-time">
|
<td class="text-center remaining-time">
|
||||||
<?= $txRemainingMinutes ?> min
|
<?= $txRemainingMinutes ?> min
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<?php
|
||||||
|
$syncClass = 'sync-pending';
|
||||||
|
$syncIcon = 'bi-circle-fill';
|
||||||
|
$syncTitle = 'Nog niet gesynchroniseerd';
|
||||||
|
|
||||||
|
if ($tx['api_status'] === 'synced' && !empty($tx['talpa_transmission_id'])) {
|
||||||
|
$syncClass = 'sync-success';
|
||||||
|
$syncTitle = 'Gesynchroniseerd met Talpa ID: ' . $tx['talpa_transmission_id'];
|
||||||
|
} elseif ($tx['api_status'] === 'error') {
|
||||||
|
$syncClass = 'sync-error';
|
||||||
|
$syncIcon = 'bi-exclamation-circle-fill';
|
||||||
|
$syncTitle = 'Sync fout - klik voor details';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<span class="sync-indicator <?= $syncClass ?>"
|
||||||
|
title="<?= htmlspecialchars($syncTitle) ?>"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="left"
|
||||||
|
data-bs-html="true">
|
||||||
|
<i class="bi <?= $syncIcon ?>"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
<?php if ($index > 0): ?>
|
<?php if ($index > 0): ?>
|
||||||
@ -592,7 +674,7 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
|
|||||||
<input type="hidden" name="block_id" value="<?= $block['id'] ?>">
|
<input type="hidden" name="block_id" value="<?= $block['id'] ?>">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">
|
<h5 class="modal-title">
|
||||||
<i class="bi bi-clock"></i> Blok Starttijd Aanpassen
|
<i class="bi bi-clock"></i> Starttijd Aanpassen
|
||||||
</h5>
|
</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
@ -633,9 +715,86 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
|
|||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<!-- Copy Block Modals -->
|
||||||
|
<?php foreach ($allBlocks as $block): ?>
|
||||||
|
<div class="modal fade" id="copyBlockModal<?= $block['id'] ?>" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="copyBlockForm<?= $block['id'] ?>">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="bi bi-files"></i> Kopieer Blok
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>Doelblok:</strong><br>
|
||||||
|
<?= htmlspecialchars($block['template_name'] ?? 'Blok') ?><br>
|
||||||
|
<?= htmlspecialchars($block['channel']) ?> - <?= formatDateDutch($selectedDate) ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<strong>Let op:</strong> Alle bestaande uitzendingen in dit blok worden overschreven!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Brondag</label>
|
||||||
|
<input type="date"
|
||||||
|
class="form-control"
|
||||||
|
id="sourceDate<?= $block['id'] ?>"
|
||||||
|
required
|
||||||
|
onchange="loadSourceBlocks(<?= $block['id'] ?>, '<?= $block['channel'] ?>', <?= $block['template_id'] ?>)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Bronblok</label>
|
||||||
|
<select class="form-select"
|
||||||
|
id="sourceBlock<?= $block['id'] ?>"
|
||||||
|
required
|
||||||
|
onchange="showSourceBlockPreview(<?= $block['id'] ?>)">
|
||||||
|
<option value="">Selecteer eerst een brondag...</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">
|
||||||
|
Alleen blokken van hetzelfde type (<?= htmlspecialchars($block['template_name'] ?? 'Blok') ?>) worden getoond
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sourceBlockPreview<?= $block['id'] ?>" class="mt-3" style="display: none;">
|
||||||
|
<h6>Preview Bronblok:</h6>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="sourceBlockInfo<?= $block['id'] ?>"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
Annuleren
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-files"></i> Kopieer Blok
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// Initialize Bootstrap tooltips
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Filter infomercials in sidebar
|
// Filter infomercials in sidebar
|
||||||
function filterPlannerCommercials(searchTerm) {
|
function filterPlannerCommercials(searchTerm) {
|
||||||
const items = document.querySelectorAll('.draggable-infomercial');
|
const items = document.querySelectorAll('.draggable-infomercial');
|
||||||
@ -656,7 +815,7 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
|
|||||||
// Make infomercial items draggable
|
// Make infomercial items draggable
|
||||||
document.querySelectorAll('.draggable-infomercial').forEach(item => {
|
document.querySelectorAll('.draggable-infomercial').forEach(item => {
|
||||||
item.addEventListener('dragstart', function(e) {
|
item.addEventListener('dragstart', function(e) {
|
||||||
e.dataTransfer.setData('infomercial_id', this.dataset.commercialId);
|
e.dataTransfer.setData('infomercial_id', this.dataset.infomercialId);
|
||||||
e.dataTransfer.setData('title', this.dataset.title);
|
e.dataTransfer.setData('title', this.dataset.title);
|
||||||
e.dataTransfer.setData('duration', this.dataset.duration);
|
e.dataTransfer.setData('duration', this.dataset.duration);
|
||||||
this.style.opacity = '0.5';
|
this.style.opacity = '0.5';
|
||||||
@ -820,6 +979,146 @@ function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
|
|||||||
alert('✗ Netwerkfout bij synchroniseren');
|
alert('✗ Netwerkfout bij synchroniseren');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load available source blocks when date is selected
|
||||||
|
function loadSourceBlocks(targetBlockId, channel, templateId) {
|
||||||
|
const sourceDate = document.getElementById(`sourceDate${targetBlockId}`).value;
|
||||||
|
const sourceBlockSelect = document.getElementById(`sourceBlock${targetBlockId}`);
|
||||||
|
|
||||||
|
if (!sourceDate) {
|
||||||
|
sourceBlockSelect.innerHTML = '<option value="">Selecteer eerst een brondag...</option>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading
|
||||||
|
sourceBlockSelect.innerHTML = '<option value="">Laden...</option>';
|
||||||
|
|
||||||
|
fetch(`api/get_available_source_blocks.php?date=${sourceDate}&channel=${channel}&template_id=${templateId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.blocks.length > 0) {
|
||||||
|
let options = '<option value="">Selecteer bronblok...</option>';
|
||||||
|
data.blocks.forEach(block => {
|
||||||
|
const transmissionCount = block.transmission_count || 0;
|
||||||
|
const startTime = block.actual_start_time ? block.actual_start_time.substring(0,5) : '??:??';
|
||||||
|
const endTime = block.actual_end_time ? block.actual_end_time.substring(0,5) : '∞';
|
||||||
|
options += `<option value="${block.id}">
|
||||||
|
${block.template_name} (${startTime} - ${endTime}) - ${transmissionCount} uitzendingen
|
||||||
|
</option>`;
|
||||||
|
});
|
||||||
|
sourceBlockSelect.innerHTML = options;
|
||||||
|
} else {
|
||||||
|
sourceBlockSelect.innerHTML = '<option value="">Geen blokken beschikbaar op deze dag</option>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading source blocks:', error);
|
||||||
|
sourceBlockSelect.innerHTML = '<option value="">Fout bij laden</option>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show preview of source block when selected
|
||||||
|
function showSourceBlockPreview(targetBlockId) {
|
||||||
|
const sourceBlockId = document.getElementById(`sourceBlock${targetBlockId}`).value;
|
||||||
|
const previewDiv = document.getElementById(`sourceBlockPreview${targetBlockId}`);
|
||||||
|
const infoDiv = document.getElementById(`sourceBlockInfo${targetBlockId}`);
|
||||||
|
|
||||||
|
if (!sourceBlockId) {
|
||||||
|
previewDiv.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 startTime = data.block.actual_start_time ? data.block.actual_start_time.substring(0,5) : '??:??';
|
||||||
|
const endTime = data.block.actual_end_time ? data.block.actual_end_time.substring(0,5) : '∞';
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<p><strong>Starttijd:</strong> ${startTime}</p>
|
||||||
|
<p><strong>Eindtijd:</strong> ${endTime}</p>
|
||||||
|
<p><strong>Aantal uitzendingen:</strong> ${data.transmissions.length}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (data.transmissions.length > 0) {
|
||||||
|
html += '<hr><p><strong>Uitzendingen:</strong></p><ul class="small">';
|
||||||
|
data.transmissions.forEach(tx => {
|
||||||
|
const txTime = tx.start_time ? tx.start_time.substring(0,5) : '??:??';
|
||||||
|
html += `<li>${txTime} - ${tx.title}</li>`;
|
||||||
|
});
|
||||||
|
html += '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
infoDiv.innerHTML = html;
|
||||||
|
previewDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading block details:', error);
|
||||||
|
infoDiv.innerHTML = '<p class="text-danger">Fout bij laden van preview</p>';
|
||||||
|
previewDiv.style.display = 'block';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle copy block form submission
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
document.querySelectorAll('[id^="copyBlockForm"]').forEach(form => {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const blockId = this.id.replace('copyBlockForm', '');
|
||||||
|
const sourceDate = document.getElementById(`sourceDate${blockId}`).value;
|
||||||
|
const sourceBlockId = document.getElementById(`sourceBlock${blockId}`).value;
|
||||||
|
|
||||||
|
if (!sourceDate || !sourceBlockId) {
|
||||||
|
alert('Selecteer een brondag en bronblok');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Weet je zeker dat je dit blok wilt kopiëren? Alle bestaande uitzendingen worden overschreven!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading
|
||||||
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
|
const originalHTML = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Kopiëren...';
|
||||||
|
|
||||||
|
// Call API
|
||||||
|
fetch('api/copy_block.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
source_block_id: sourceBlockId,
|
||||||
|
target_block_id: blockId,
|
||||||
|
target_date: '<?= $selectedDate ?>'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert(`✓ Blok succesvol gekopieerd!\n\nGekopieerd: ${data.copied_count} uitzendingen\nVerwijderd: ${data.deleted_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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
908
plans/copy-blocks-feature-plan.md
Normal file
908
plans/copy-blocks-feature-plan.md
Normal file
@ -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
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-info ms-2"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#copyBlockModal<?= $block['id'] ?>"
|
||||||
|
title="Kopieer blok van andere dag">
|
||||||
|
<i class="bi bi-files"></i> Kopieer Blok
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Modal voor Blok Kopiëren
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="modal fade" id="copyBlockModal<?= $block['id'] ?>" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form id="copyBlockForm<?= $block['id'] ?>">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="bi bi-files"></i> Kopieer Blok
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>Doelblok:</strong><br>
|
||||||
|
<?= htmlspecialchars($block['template_name']) ?><br>
|
||||||
|
<?= htmlspecialchars($block['channel']) ?> - <?= formatDateDutch($selectedDate) ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<strong>Let op:</strong> Alle bestaande uitzendingen in dit blok worden overschreven!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Brondag</label>
|
||||||
|
<input type="date"
|
||||||
|
class="form-control"
|
||||||
|
id="sourceDate<?= $block['id'] ?>"
|
||||||
|
required
|
||||||
|
onchange="loadSourceBlocks(<?= $block['id'] ?>, '<?= $block['channel'] ?>', <?= $block['template_id'] ?>)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Bronblok</label>
|
||||||
|
<select class="form-select"
|
||||||
|
id="sourceBlock<?= $block['id'] ?>"
|
||||||
|
required>
|
||||||
|
<option value="">Selecteer eerst een brondag...</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">
|
||||||
|
Alleen blokken van hetzelfde type (<?= htmlspecialchars($block['template_name']) ?>) worden getoond
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sourceBlockPreview<?= $block['id'] ?>" class="mt-3" style="display: none;">
|
||||||
|
<h6>Preview Bronblok:</h6>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="sourceBlockInfo<?= $block['id'] ?>"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
Annuleren
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-files"></i> Kopieer Blok
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript Functies
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Load available source blocks when date is selected
|
||||||
|
function loadSourceBlocks(targetBlockId, channel, templateId) {
|
||||||
|
const sourceDate = document.getElementById(`sourceDate${targetBlockId}`).value;
|
||||||
|
const sourceBlockSelect = document.getElementById(`sourceBlock${targetBlockId}`);
|
||||||
|
|
||||||
|
if (!sourceDate) {
|
||||||
|
sourceBlockSelect.innerHTML = '<option value="">Selecteer eerst een brondag...</option>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading
|
||||||
|
sourceBlockSelect.innerHTML = '<option value="">Laden...</option>';
|
||||||
|
|
||||||
|
fetch(`api/get_available_source_blocks.php?date=${sourceDate}&channel=${channel}&template_id=${templateId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.blocks.length > 0) {
|
||||||
|
let options = '<option value="">Selecteer bronblok...</option>';
|
||||||
|
data.blocks.forEach(block => {
|
||||||
|
const transmissionCount = block.transmission_count || 0;
|
||||||
|
options += `<option value="${block.id}">
|
||||||
|
${block.template_name} (${block.actual_start_time.substring(0,5)} - ${block.actual_end_time ? block.actual_end_time.substring(0,5) : '∞'})
|
||||||
|
- ${transmissionCount} uitzendingen
|
||||||
|
</option>`;
|
||||||
|
});
|
||||||
|
sourceBlockSelect.innerHTML = options;
|
||||||
|
} else {
|
||||||
|
sourceBlockSelect.innerHTML = '<option value="">Geen blokken beschikbaar op deze dag</option>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading source blocks:', error);
|
||||||
|
sourceBlockSelect.innerHTML = '<option value="">Fout bij laden</option>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show preview of source block when selected
|
||||||
|
document.querySelectorAll('[id^="sourceBlock"]').forEach(select => {
|
||||||
|
select.addEventListener('change', function() {
|
||||||
|
const blockId = this.id.replace('sourceBlock', '');
|
||||||
|
const sourceBlockId = this.value;
|
||||||
|
|
||||||
|
if (!sourceBlockId) {
|
||||||
|
document.getElementById(`sourceBlockPreview${blockId}`).style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load block details
|
||||||
|
fetch(`api/get_block_details.php?block_id=${sourceBlockId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const preview = document.getElementById(`sourceBlockInfo${blockId}`);
|
||||||
|
let html = `
|
||||||
|
<p><strong>Starttijd:</strong> ${data.block.actual_start_time.substring(0,5)}</p>
|
||||||
|
<p><strong>Eindtijd:</strong> ${data.block.actual_end_time ? data.block.actual_end_time.substring(0,5) : '∞'}</p>
|
||||||
|
<p><strong>Aantal uitzendingen:</strong> ${data.transmissions.length}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (data.transmissions.length > 0) {
|
||||||
|
html += '<hr><p><strong>Uitzendingen:</strong></p><ul class="small">';
|
||||||
|
data.transmissions.forEach(tx => {
|
||||||
|
html += `<li>${tx.start_time.substring(0,5)} - ${tx.title}</li>`;
|
||||||
|
});
|
||||||
|
html += '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
preview.innerHTML = html;
|
||||||
|
document.getElementById(`sourceBlockPreview${blockId}`).style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle copy block form submission
|
||||||
|
document.querySelectorAll('[id^="copyBlockForm"]').forEach(form => {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const blockId = this.id.replace('copyBlockForm', '');
|
||||||
|
const sourceDate = document.getElementById(`sourceDate${blockId}`).value;
|
||||||
|
const sourceBlockId = document.getElementById(`sourceBlock${blockId}`).value;
|
||||||
|
|
||||||
|
if (!sourceDate || !sourceBlockId) {
|
||||||
|
alert('Selecteer een brondag en bronblok');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Weet je zeker dat je dit blok wilt kopiëren? Alle bestaande uitzendingen worden overschreven!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading
|
||||||
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
|
const originalHTML = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Kopiëren...';
|
||||||
|
|
||||||
|
// Call API
|
||||||
|
fetch('api/copy_block.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
source_block_id: sourceBlockId,
|
||||||
|
target_block_id: blockId,
|
||||||
|
target_date: '<?= $selectedDate ?>'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert(`✓ Blok succesvol gekopieerd!\n\nGekopieerd: ${data.copied_count} uitzendingen`);
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('✗ Fout bij kopiëren: ' + (data.error || 'Onbekende fout'));
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalHTML;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('✗ Netwerkfout bij kopiëren');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalHTML;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Backend API Endpoints
|
||||||
|
|
||||||
|
#### API: `api/get_available_source_blocks.php`
|
||||||
|
|
||||||
|
**Doel**: Haal beschikbare bronblokken op voor een specifieke datum, zender en template
|
||||||
|
|
||||||
|
**Input (GET parameters)**:
|
||||||
|
- `date` (required): Brondag in Y-m-d formaat
|
||||||
|
- `channel` (required): Zender naam (SBS9, NET5, 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
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
$date = $_GET['date'] ?? null;
|
||||||
|
$channel = $_GET['channel'] ?? null;
|
||||||
|
$templateId = $_GET['template_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$date || !$channel || !$templateId) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Missing parameters'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidDate($date)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Invalid date format'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure blocks exist for this date
|
||||||
|
ensureDailyBlocks($db, $date, $date);
|
||||||
|
|
||||||
|
// Get blocks for this date, channel, and template
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT
|
||||||
|
db.*,
|
||||||
|
bt.name as template_name,
|
||||||
|
COUNT(t.id) as transmission_count
|
||||||
|
FROM daily_blocks db
|
||||||
|
LEFT JOIN block_templates bt ON db.template_id = bt.id
|
||||||
|
LEFT JOIN transmissions t ON t.start_date = db.block_date
|
||||||
|
AND t.channel = db.channel
|
||||||
|
AND t.start_time >= db.actual_start_time
|
||||||
|
AND t.start_time < COALESCE(db.actual_end_time, '23:59:59')
|
||||||
|
WHERE db.block_date = ?
|
||||||
|
AND db.channel = ?
|
||||||
|
AND db.template_id = ?
|
||||||
|
GROUP BY db.id
|
||||||
|
ORDER BY db.actual_start_time
|
||||||
|
");
|
||||||
|
$stmt->execute([$date, $channel, $templateId]);
|
||||||
|
$blocks = $stmt->fetchAll();
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'blocks' => $blocks
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API: `api/get_block_details.php`
|
||||||
|
|
||||||
|
**Doel**: Haal gedetailleerde informatie op over een specifiek blok (voor preview)
|
||||||
|
|
||||||
|
**Input (GET parameters)**:
|
||||||
|
- `block_id` (required): ID van het blok
|
||||||
|
|
||||||
|
**Output (JSON)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"block": {
|
||||||
|
"id": 123,
|
||||||
|
"template_name": "SBS9 Dagblok",
|
||||||
|
"channel": "SBS9",
|
||||||
|
"block_date": "2026-01-15",
|
||||||
|
"actual_start_time": "07:00:00",
|
||||||
|
"actual_end_time": "15:00:00"
|
||||||
|
},
|
||||||
|
"transmissions": [
|
||||||
|
{
|
||||||
|
"id": 456,
|
||||||
|
"title": "Product A",
|
||||||
|
"start_time": "07:00:00",
|
||||||
|
"duration": "00:28:30",
|
||||||
|
"series_code": "001a"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementatie**:
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
$blockId = $_GET['block_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$blockId) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Missing block_id'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get block info
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT db.*, bt.name as template_name
|
||||||
|
FROM daily_blocks db
|
||||||
|
LEFT JOIN block_templates bt ON db.template_id = bt.id
|
||||||
|
WHERE db.id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$blockId]);
|
||||||
|
$block = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$block) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Block not found'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get transmissions in this block
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.*, c.title, c.series_code
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN infomercials c ON t.infomercial_id = c.id
|
||||||
|
WHERE t.start_date = ?
|
||||||
|
AND t.channel = ?
|
||||||
|
AND t.start_time >= ?
|
||||||
|
AND t.start_time < ?
|
||||||
|
ORDER BY t.start_time ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$block['block_date'],
|
||||||
|
$block['channel'],
|
||||||
|
$block['actual_start_time'],
|
||||||
|
$block['actual_end_time'] ?? '23:59:59'
|
||||||
|
]);
|
||||||
|
$transmissions = $stmt->fetchAll();
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'block' => $block,
|
||||||
|
'transmissions' => $transmissions
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse(['success' => false, 'error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API: `api/copy_block.php`
|
||||||
|
|
||||||
|
**Doel**: Kopieer een blok (bloktijden + transmissions) van bronblok naar doelblok
|
||||||
|
|
||||||
|
**Input (POST JSON)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"source_block_id": 123,
|
||||||
|
"target_block_id": 456,
|
||||||
|
"target_date": "2026-01-16"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output (JSON)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Block copied successfully",
|
||||||
|
"copied_count": 12,
|
||||||
|
"deleted_count": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementatie**:
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Get POST data
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
$sourceBlockId = $input['source_block_id'] ?? null;
|
||||||
|
$targetBlockId = $input['target_block_id'] ?? null;
|
||||||
|
$targetDate = $input['target_date'] ?? null;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (!$sourceBlockId || !$targetBlockId || !$targetDate) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Missing required parameters'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidDate($targetDate)) {
|
||||||
|
jsonResponse(['success' => false, 'error' => 'Invalid target date format'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transaction
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get source block info
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT * FROM daily_blocks WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$sourceBlockId]);
|
||||||
|
$sourceBlock = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$sourceBlock) {
|
||||||
|
throw new Exception('Source block not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get target block info
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT * FROM daily_blocks WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$targetBlockId]);
|
||||||
|
$targetBlock = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$targetBlock) {
|
||||||
|
throw new Exception('Target block not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that blocks are compatible (same channel and template)
|
||||||
|
if ($sourceBlock['channel'] !== $targetBlock['channel']) {
|
||||||
|
throw new Exception('Source and target blocks must be on the same channel');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sourceBlock['template_id'] !== $targetBlock['template_id']) {
|
||||||
|
throw new Exception('Source and target blocks must use the same template');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Update target block times
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE daily_blocks
|
||||||
|
SET actual_start_time = ?,
|
||||||
|
actual_end_time = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$sourceBlock['actual_start_time'],
|
||||||
|
$sourceBlock['actual_end_time'],
|
||||||
|
$targetBlockId
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Step 2: Delete existing transmissions in target block
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
DELETE FROM transmissions
|
||||||
|
WHERE start_date = ?
|
||||||
|
AND channel = ?
|
||||||
|
AND start_time >= ?
|
||||||
|
AND start_time < ?
|
||||||
|
");
|
||||||
|
$deletedCount = $stmt->execute([
|
||||||
|
$targetDate,
|
||||||
|
$targetBlock['channel'],
|
||||||
|
$targetBlock['actual_start_time'],
|
||||||
|
$targetBlock['actual_end_time'] ?? '23:59:59'
|
||||||
|
]);
|
||||||
|
$deletedCount = $stmt->rowCount();
|
||||||
|
|
||||||
|
// Step 3: Get source transmissions
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.*, c.duration
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN infomercials c ON t.infomercial_id = c.id
|
||||||
|
WHERE t.start_date = ?
|
||||||
|
AND t.channel = ?
|
||||||
|
AND t.start_time >= ?
|
||||||
|
AND t.start_time < ?
|
||||||
|
ORDER BY t.start_time ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$sourceBlock['block_date'],
|
||||||
|
$sourceBlock['channel'],
|
||||||
|
$sourceBlock['actual_start_time'],
|
||||||
|
$sourceBlock['actual_end_time'] ?? '23:59:59'
|
||||||
|
]);
|
||||||
|
$sourceTransmissions = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Step 4: Copy transmissions to target block
|
||||||
|
$copiedCount = 0;
|
||||||
|
$insertStmt = $db->prepare("
|
||||||
|
INSERT INTO transmissions
|
||||||
|
(infomercial_id, channel, template, start_date, start_time, duration, api_status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, 'pending')
|
||||||
|
");
|
||||||
|
|
||||||
|
foreach ($sourceTransmissions as $tx) {
|
||||||
|
$insertStmt->execute([
|
||||||
|
$tx['infomercial_id'],
|
||||||
|
$tx['channel'],
|
||||||
|
$tx['template'],
|
||||||
|
$targetDate, // Use target date
|
||||||
|
$tx['start_time'],
|
||||||
|
$tx['duration'],
|
||||||
|
]);
|
||||||
|
$copiedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit transaction
|
||||||
|
$db->commit();
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Block copied successfully',
|
||||||
|
'copied_count' => $copiedCount,
|
||||||
|
'deleted_count' => $deletedCount,
|
||||||
|
'block_times_updated' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$db->rollBack();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Database Operaties
|
||||||
|
|
||||||
|
#### Transactie Flow
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Start transactie
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- 1. Update doelblok tijden
|
||||||
|
UPDATE daily_blocks
|
||||||
|
SET actual_start_time = '07:00:00',
|
||||||
|
actual_end_time = '15:00:00'
|
||||||
|
WHERE id = [target_block_id];
|
||||||
|
|
||||||
|
-- 2. Verwijder bestaande transmissions in doelblok
|
||||||
|
DELETE FROM transmissions
|
||||||
|
WHERE start_date = '2026-01-16'
|
||||||
|
AND channel = 'SBS9'
|
||||||
|
AND start_time >= '07:00:00'
|
||||||
|
AND start_time < '15:00:00';
|
||||||
|
|
||||||
|
-- 3. Kopieer transmissions van bronblok
|
||||||
|
INSERT INTO transmissions
|
||||||
|
(infomercial_id, channel, template, start_date, start_time, duration, api_status)
|
||||||
|
SELECT
|
||||||
|
infomercial_id,
|
||||||
|
channel,
|
||||||
|
template,
|
||||||
|
'2026-01-16' as start_date, -- Nieuwe datum
|
||||||
|
start_time,
|
||||||
|
duration,
|
||||||
|
'pending' as api_status -- Reset sync status
|
||||||
|
FROM transmissions
|
||||||
|
WHERE start_date = '2026-01-15'
|
||||||
|
AND channel = 'SBS9'
|
||||||
|
AND start_time >= '07:00:00'
|
||||||
|
AND start_time < '15:00:00';
|
||||||
|
|
||||||
|
-- Commit transactie
|
||||||
|
COMMIT;
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Edge Cases & Validatie
|
||||||
|
|
||||||
|
### Validatie Regels
|
||||||
|
|
||||||
|
1. **Bronblok bestaat**: Controleer of `source_block_id` bestaat in database
|
||||||
|
2. **Doelblok bestaat**: Controleer of `target_block_id` bestaat in database
|
||||||
|
3. **Zelfde zender**: Bronblok en doelblok moeten op dezelfde zender zijn
|
||||||
|
4. **Zelfde template**: Bronblok en doelblok moeten hetzelfde template gebruiken
|
||||||
|
5. **Geldige datum**: `target_date` moet een geldige datum zijn in Y-m-d formaat
|
||||||
|
6. **Niet in het verleden**: Optioneel - waarschuwing als doeldatum in het verleden ligt
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
| Scenario | Gedrag |
|
||||||
|
|----------|--------|
|
||||||
|
| Bronblok heeft geen transmissions | Doelblok wordt leeggemaakt, bloktijden worden gekopieerd |
|
||||||
|
| Doelblok heeft al transmissions | Alle bestaande transmissions worden verwijderd voor kopiëren |
|
||||||
|
| Bronblok heeft aangepaste bloktijden | Bloktijden worden gekopieerd naar doelblok |
|
||||||
|
| Infomercial bestaat niet meer | Fout - transactie wordt teruggedraaid |
|
||||||
|
| Database fout tijdens kopiëren | Transactie wordt teruggedraaid, geen data wordt gewijzigd |
|
||||||
|
| Gebruiker annuleert tijdens kopiëren | Geen effect - kopiëren gebeurt server-side in één transactie |
|
||||||
|
|
||||||
|
### Foutafhandeling
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Frontend error handling
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Copy block error:', error);
|
||||||
|
alert('Er is een fout opgetreden bij het kopiëren van het blok. Probeer het opnieuw.');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Backend error handling
|
||||||
|
try {
|
||||||
|
$db->beginTransaction();
|
||||||
|
// ... copy operations ...
|
||||||
|
$db->commit();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$db->rollBack();
|
||||||
|
error_log("Copy block error: " . $e->getMessage());
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Database error: ' . $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 UI/UX Overwegingen
|
||||||
|
|
||||||
|
### Visuele Feedback
|
||||||
|
|
||||||
|
1. **Loading States**:
|
||||||
|
- Spinner tijdens laden van bronblokken
|
||||||
|
- Spinner tijdens kopiëren
|
||||||
|
- Disabled buttons tijdens operaties
|
||||||
|
|
||||||
|
2. **Confirmatie**:
|
||||||
|
- Waarschuwing dat bestaande data wordt overschreven
|
||||||
|
- Bevestigingsdialoog voor kopiëren
|
||||||
|
|
||||||
|
3. **Success/Error Messages**:
|
||||||
|
- Success: "✓ Blok succesvol gekopieerd! Gekopieerd: X uitzendingen"
|
||||||
|
- Error: "✗ Fout bij kopiëren: [error message]"
|
||||||
|
|
||||||
|
4. **Preview**:
|
||||||
|
- Toon aantal uitzendingen in bronblok
|
||||||
|
- Toon bloktijden van bronblok
|
||||||
|
- Optioneel: lijst van uitzendingen
|
||||||
|
|
||||||
|
### Toegankelijkheid
|
||||||
|
|
||||||
|
- Keyboard navigatie in modal
|
||||||
|
- ARIA labels voor screen readers
|
||||||
|
- Focus management bij openen/sluiten modal
|
||||||
|
- Clear error messages
|
||||||
|
|
||||||
|
## 📝 Testing Checklist
|
||||||
|
|
||||||
|
### Functionele Tests
|
||||||
|
|
||||||
|
- [ ] Kopieer blok met transmissions werkt correct
|
||||||
|
- [ ] Kopieer leeg blok werkt correct
|
||||||
|
- [ ] Bloktijden worden correct gekopieerd
|
||||||
|
- [ ] Bestaande transmissions worden verwijderd
|
||||||
|
- [ ] `api_status` wordt gereset naar 'pending'
|
||||||
|
- [ ] `start_date` wordt correct aangepast
|
||||||
|
- [ ] Transactie rollback werkt bij fouten
|
||||||
|
- [ ] Validatie van zelfde zender werkt
|
||||||
|
- [ ] Validatie van zelfde template werkt
|
||||||
|
|
||||||
|
### UI Tests
|
||||||
|
|
||||||
|
- [ ] Modal opent correct
|
||||||
|
- [ ] Brondag selectie werkt
|
||||||
|
- [ ] Bronblok dropdown wordt correct gevuld
|
||||||
|
- [ ] Preview toont correcte informatie
|
||||||
|
- [ ] Loading states worden getoond
|
||||||
|
- [ ] Success message wordt getoond
|
||||||
|
- [ ] Error messages worden getoond
|
||||||
|
- [ ] Pagina herlaadt na succesvol kopiëren
|
||||||
|
|
||||||
|
### Edge Case Tests
|
||||||
|
|
||||||
|
- [ ] Kopieer van dag zonder blokken
|
||||||
|
- [ ] Kopieer naar dag in het verleden
|
||||||
|
- [ ] Kopieer met niet-bestaande infomercials
|
||||||
|
- [ ] Kopieer tijdens database fout
|
||||||
|
- [ ] Kopieer met ongeldige parameters
|
||||||
|
|
||||||
|
## 🚀 Implementatie Volgorde
|
||||||
|
|
||||||
|
1. **Backend API's** (eerst testen met Postman/curl):
|
||||||
|
- [ ] `api/get_available_source_blocks.php`
|
||||||
|
- [ ] `api/get_block_details.php`
|
||||||
|
- [ ] `api/copy_block.php`
|
||||||
|
|
||||||
|
2. **Frontend UI**:
|
||||||
|
- [ ] Voeg "Kopieer Blok" knop toe aan block headers
|
||||||
|
- [ ] Implementeer copy block modal
|
||||||
|
- [ ] Implementeer JavaScript functies
|
||||||
|
|
||||||
|
3. **Testing**:
|
||||||
|
- [ ] Test alle API endpoints
|
||||||
|
- [ ] Test UI flows
|
||||||
|
- [ ] Test edge cases
|
||||||
|
|
||||||
|
4. **Documentatie**:
|
||||||
|
- [ ] Update README.md met nieuwe functionaliteit
|
||||||
|
- [ ] Voeg screenshots toe (optioneel)
|
||||||
|
|
||||||
|
## 📚 Benodigde Bestanden
|
||||||
|
|
||||||
|
### Nieuwe Bestanden
|
||||||
|
- `api/get_available_source_blocks.php` - API voor beschikbare bronblokken
|
||||||
|
- `api/get_block_details.php` - API voor blok details (preview)
|
||||||
|
- `api/copy_block.php` - API voor blok kopiëren
|
||||||
|
|
||||||
|
### Te Wijzigen Bestanden
|
||||||
|
- `planner.php` - Voeg UI toe voor kopieer functionaliteit
|
||||||
|
- `README.md` - Documenteer nieuwe functionaliteit
|
||||||
|
|
||||||
|
### Geen Wijzigingen Nodig
|
||||||
|
- Database schema (bestaande tabellen zijn voldoende)
|
||||||
|
- `helpers.php` (bestaande functies zijn voldoende)
|
||||||
|
- Andere PHP bestanden
|
||||||
|
|
||||||
|
## 🔒 Beveiliging
|
||||||
|
|
||||||
|
1. **Input Validatie**:
|
||||||
|
- Valideer alle GET/POST parameters
|
||||||
|
- Gebruik prepared statements voor alle queries
|
||||||
|
- Valideer datum formaten
|
||||||
|
|
||||||
|
2. **Autorisatie**:
|
||||||
|
- Optioneel: voeg gebruikersauthenticatie toe
|
||||||
|
- Controleer of gebruiker rechten heeft om blokken te kopiëren
|
||||||
|
|
||||||
|
3. **Database**:
|
||||||
|
- Gebruik transacties voor atomaire operaties
|
||||||
|
- Rollback bij fouten
|
||||||
|
- Error logging zonder gevoelige data
|
||||||
|
|
||||||
|
## 📊 Performance Overwegingen
|
||||||
|
|
||||||
|
1. **Database Queries**:
|
||||||
|
- Gebruik indexes op `start_date`, `channel`, `start_time`
|
||||||
|
- Batch insert voor transmissions (indien mogelijk)
|
||||||
|
- Limit aantal transmissions per blok (praktisch max ~50)
|
||||||
|
|
||||||
|
2. **Frontend**:
|
||||||
|
- Lazy load bronblokken (alleen bij selectie datum)
|
||||||
|
- Debounce date picker events
|
||||||
|
- Cache block details voor preview
|
||||||
|
|
||||||
|
3. **API Response Times**:
|
||||||
|
- Target: < 500ms voor get_available_source_blocks
|
||||||
|
- Target: < 200ms voor get_block_details
|
||||||
|
- Target: < 1000ms voor copy_block (inclusief database operaties)
|
||||||
|
|
||||||
|
## ✅ Acceptatie Criteria
|
||||||
|
|
||||||
|
De functionaliteit is compleet wanneer:
|
||||||
|
|
||||||
|
1. ✅ Gebruiker kan een brondag selecteren in de planner
|
||||||
|
2. ✅ Gebruiker kan een bronblok selecteren (gefilterd op zelfde template)
|
||||||
|
3. ✅ Gebruiker ziet een preview van het bronblok
|
||||||
|
4. ✅ Gebruiker kan het blok kopiëren met één klik
|
||||||
|
5. ✅ Alle transmissions worden correct gekopieerd
|
||||||
|
6. ✅ Bloktijden worden correct gekopieerd
|
||||||
|
7. ✅ Bestaande transmissions in doelblok worden overschreven
|
||||||
|
8. ✅ `api_status` wordt gereset naar 'pending'
|
||||||
|
9. ✅ Pagina toont gekopieerde data na herladen
|
||||||
|
10. ✅ Foutmeldingen worden duidelijk getoond bij problemen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Geschatte Implementatietijd**: 4-6 uur
|
||||||
|
- Backend API's: 2-3 uur
|
||||||
|
- Frontend UI: 1.5-2 uur
|
||||||
|
- Testing & debugging: 0.5-1 uur
|
||||||
595
plans/sync-status-visual-indicators-plan.md
Normal file
595
plans/sync-status-visual-indicators-plan.md
Normal file
@ -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
|
||||||
|
<!-- Groen: Gesynchroniseerd -->
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="sync-indicator sync-success"
|
||||||
|
title="Gesynchroniseerd met Talpa"
|
||||||
|
data-bs-toggle="tooltip">
|
||||||
|
<i class="bi bi-circle-fill"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Rood: Niet gesynchroniseerd -->
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="sync-indicator sync-pending"
|
||||||
|
title="Nog niet gesynchroniseerd"
|
||||||
|
data-bs-toggle="tooltip">
|
||||||
|
<i class="bi bi-circle-fill"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Oranje: Error -->
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="sync-indicator sync-error"
|
||||||
|
title="Sync fout - klik voor details"
|
||||||
|
data-bs-toggle="tooltip">
|
||||||
|
<i class="bi bi-exclamation-circle-fill"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
<div class="block-header d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<?= htmlspecialchars($block['template_name'] ?? 'Blok') ?>
|
||||||
|
|
||||||
|
<!-- Block Sync Status Badge -->
|
||||||
|
<span class="badge block-sync-status block-sync-complete ms-2">
|
||||||
|
<i class="bi bi-check-circle-fill"></i> Gepland
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-dark ms-2">
|
||||||
|
<i class="bi bi-clock"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-success ms-2">
|
||||||
|
<i class="bi bi-cloud-upload"></i> Sync naar Talpa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<?= substr($blockStart, 0, 5) ?> - <?= substr($blockEnd, 0, 5) ?>
|
||||||
|
| Resterend: <strong><?= round($remainingMinutes) ?> min</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 40px;"></th>
|
||||||
|
<th style="width: 50px;">Code</th>
|
||||||
|
<th>Product</th>
|
||||||
|
<th style="width: 80px;">Duur</th>
|
||||||
|
<th style="width: 70px;">Start</th>
|
||||||
|
<th style="width: 70px;">Eind</th>
|
||||||
|
<th style="width: 80px;">Restant</th>
|
||||||
|
<th style="width: 50px;" class="text-center">Talpa</th> <!-- NIEUW -->
|
||||||
|
<th style="width: 100px;">Acties</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Stap 3: Update Transmission Row (planner.php)
|
||||||
|
Voeg sync indicator toe in transmission row:
|
||||||
|
```php
|
||||||
|
<tr style="background-color: <?= $tx['color_code'] ?>15;">
|
||||||
|
<!-- Bestaande kolommen -->
|
||||||
|
<td>
|
||||||
|
<div class="color-indicator" style="background-color: <?= htmlspecialchars($tx['color_code']) ?>;"></div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<small><strong><?= htmlspecialchars($tx['series_code'] ?? '-') ?></strong></small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong><?= htmlspecialchars($tx['title']) ?></strong>
|
||||||
|
</td>
|
||||||
|
<td class="duration-cell">
|
||||||
|
<?= $durationMinutes ?> min
|
||||||
|
</td>
|
||||||
|
<td class="time-cell text-success">
|
||||||
|
<?= substr($tx['start_time'], 0, 5) ?>
|
||||||
|
</td>
|
||||||
|
<td class="time-cell text-danger">
|
||||||
|
<?= substr($endTime, 0, 5) ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-center remaining-time">
|
||||||
|
<?= $txRemainingMinutes ?> min
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- NIEUWE KOLOM: Talpa Sync Status -->
|
||||||
|
<td class="text-center">
|
||||||
|
<?php
|
||||||
|
$syncClass = 'sync-pending';
|
||||||
|
$syncIcon = 'bi-circle-fill';
|
||||||
|
$syncTitle = 'Nog niet gesynchroniseerd';
|
||||||
|
|
||||||
|
if ($tx['api_status'] === 'synced' && !empty($tx['talpa_transmission_id'])) {
|
||||||
|
$syncClass = 'sync-success';
|
||||||
|
$syncTitle = 'Gesynchroniseerd met Talpa\nID: ' . $tx['talpa_transmission_id'];
|
||||||
|
} elseif ($tx['api_status'] === 'error') {
|
||||||
|
$syncClass = 'sync-error';
|
||||||
|
$syncIcon = 'bi-exclamation-circle-fill';
|
||||||
|
$syncTitle = 'Sync fout - klik voor details';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<span class="sync-indicator <?= $syncClass ?>"
|
||||||
|
title="<?= htmlspecialchars($syncTitle) ?>"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="left">
|
||||||
|
<i class="bi <?= $syncIcon ?>"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Bestaande acties kolom -->
|
||||||
|
<td>
|
||||||
|
<!-- ... bestaande buttons ... -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Stap 4: Update Block Header (planner.php)
|
||||||
|
Voeg block sync status badge toe:
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// Bereken block sync stats
|
||||||
|
$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 ({$blockSyncStats['synced']}/{$blockSyncStats['total']})";
|
||||||
|
$blockSyncIcon = 'bi-exclamation-triangle-fill';
|
||||||
|
} else {
|
||||||
|
$blockSyncClass = 'block-sync-none';
|
||||||
|
$blockSyncLabel = 'Niet Gepland';
|
||||||
|
$blockSyncIcon = 'bi-x-circle-fill';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="block-header d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<?= htmlspecialchars($block['template_name'] ?? 'Blok') ?>
|
||||||
|
|
||||||
|
<!-- Block Sync Status Badge -->
|
||||||
|
<span class="badge block-sync-status <?= $blockSyncClass ?> ms-2"
|
||||||
|
title="<?= $blockSyncStats['synced'] ?> van <?= $blockSyncStats['total'] ?> transmissions gesynchroniseerd"
|
||||||
|
data-bs-toggle="tooltip">
|
||||||
|
<i class="bi <?= $blockSyncIcon ?>"></i> <?= $blockSyncLabel ?>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Bestaande buttons -->
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-dark ms-2"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#blockTimeModal<?= $block['id'] ?>"
|
||||||
|
title="Starttijd aanpassen">
|
||||||
|
<i class="bi bi-clock"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-success ms-2"
|
||||||
|
onclick="syncBlockPlanner('<?= $selectedDate ?>', '<?= $channel ?>')"
|
||||||
|
title="Sync blok naar Talpa">
|
||||||
|
<i class="bi bi-cloud-upload"></i> Sync naar Talpa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<?= substr($blockStart, 0, 5) ?> - <?= substr($blockEnd, 0, 5) ?>
|
||||||
|
| Resterend: <strong><?= round($remainingMinutes) ?> min</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Stap 5: Initialize Tooltips (planner.php)
|
||||||
|
Voeg JavaScript toe om Bootstrap tooltips te activeren:
|
||||||
|
```javascript
|
||||||
|
<script>
|
||||||
|
// Initialize tooltips
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Existing functions...
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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)
|
||||||
271
plans/transmission-sync-update-plan.md
Normal file
271
plans/transmission-sync-update-plan.md
Normal file
@ -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)?
|
||||||
Loading…
x
Reference in New Issue
Block a user