Planner basic functionality

This commit is contained in:
Mark Pinkster 2026-01-14 11:44:11 +01:00
parent bfdd27fbcc
commit 999e76744b
26 changed files with 5854 additions and 21 deletions

17
.env.example Normal file
View File

@ -0,0 +1,17 @@
# Database Configuration
DB_HOST=localhost
DB_NAME=talpa_planning
DB_USER=your_database_username
DB_PASS=your_database_password
# Talpa API Configuration
TALPA_API_BASE=https://api.talpa.tv
TALPA_TOKEN=your_talpa_api_token_here
TALPA_MOCK_MODE=false
# TV Configuration
TV_SEASON_ID=your_season_id_here
# Application Settings
APP_ENV=production
APP_DEBUG=false

361
INSTALLATION.md Normal file
View File

@ -0,0 +1,361 @@
# Installatie Handleiding - Telvero Talpa Planning System
## Stap-voor-stap Installatie
### Stap 1: Vereisten Controleren
Zorg dat je systeem voldoet aan de volgende vereisten:
- ✅ PHP 7.4 of hoger
- ✅ MySQL 5.7 of hoger
- ✅ Composer
- ✅ Apache of Nginx webserver
- ✅ PHP extensies: PDO, PDO_MySQL, cURL, JSON
**Controleer PHP versie:**
```bash
php -v
```
**Controleer Composer:**
```bash
composer --version
```
### Stap 2: Project Setup
**2.1 Navigeer naar project directory:**
```bash
cd /Users/mark/Documents/GIT\ Projects/telvero_whatson_talpa
```
**2.2 Installeer PHP dependencies:**
```bash
composer install
```
Als je een foutmelding krijgt over ontbrekende dependencies, voer dan uit:
```bash
composer update
```
### Stap 3: Database Aanmaken
**3.1 Open MySQL:**
```bash
mysql -u root -p
```
**3.2 Maak database aan:**
```sql
CREATE DATABASE talpa_planning CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
**3.3 Maak database gebruiker aan (optioneel maar aanbevolen):**
```sql
CREATE USER 'talpa_user'@'localhost' IDENTIFIED BY 'jouw_wachtwoord';
GRANT ALL PRIVILEGES ON talpa_planning.* TO 'talpa_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
```
### Stap 4: Database Migratie
**4.1 Voer migratie uit:**
```bash
mysql -u talpa_user -p talpa_planning < migrations/001_add_blocks_and_colors.sql
```
**4.2 Controleer of tabellen zijn aangemaakt:**
```bash
mysql -u talpa_user -p talpa_planning -e "SHOW TABLES;"
```
Je zou moeten zien:
- block_templates
- commercials
- daily_blocks
- transmissions
### Stap 5: Environment Configuratie
**5.1 Kopieer environment file:**
```bash
cp .env.example .env
```
**5.2 Bewerk .env file:**
```bash
nano .env
```
Of open met je favoriete editor en vul in:
```env
# Database
DB_HOST=localhost
DB_NAME=talpa_planning
DB_USER=talpa_user
DB_PASS=jouw_wachtwoord
# Talpa API
TALPA_API_BASE=https://api.talpa.tv
TALPA_TOKEN=jouw_api_token
TALPA_MOCK_MODE=false
# TV Settings
TV_SEASON_ID=jouw_season_id
```
**Belangrijk:** Vraag de juiste API credentials aan bij je Talpa contactpersoon.
### Stap 6: Bestandspermissies
**6.1 Zet correcte permissies:**
```bash
chmod 755 api/
chmod 755 assets/
chmod 755 migrations/
chmod 644 api/*.php
chmod 644 assets/css/*.css
chmod 644 assets/js/*.js
```
**6.2 Maak log file aan:**
```bash
touch api_log.txt
chmod 666 api_log.txt
```
### Stap 7: Webserver Configuratie
#### Voor Apache:
**7.1 Maak .htaccess aan (indien nog niet aanwezig):**
```apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Redirect to HTTPS (productie)
# RewriteCond %{HTTPS} off
# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
# Prevent directory listing
Options -Indexes
# Protect .env file
<Files .env>
Order allow,deny
Deny from all
</Files>
```
**7.2 Herstart Apache:**
```bash
sudo apachectl restart
```
#### Voor Nginx:
**7.1 Voeg toe aan nginx config:**
```nginx
server {
listen 80;
server_name your-domain.com;
root /path/to/telvero_whatson_talpa;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.env {
deny all;
}
}
```
**7.2 Herstart Nginx:**
```bash
sudo systemctl restart nginx
```
### Stap 8: Test de Installatie
**8.1 Open in browser:**
```
http://localhost/telvero_whatson_talpa/
```
Of als je een virtuele host hebt ingesteld:
```
http://talpa-planning.local/
```
**8.2 Controleer of je het dashboard ziet met:**
- Navigatie menu (Dashboard, Kalender, Blokken, Commercials)
- Statistieken cards
- Snelle acties
**8.3 Test database connectie:**
- Ga naar "Blokken"
- Je zou de standaard templates moeten zien (SBS9, Net5)
### Stap 9: Eerste Gebruik
**9.1 Controleer Blok Templates:**
- Ga naar **Blokken**
- Verifieer dat de standaard templates zijn aangemaakt:
- SBS9 Dagblok (07:00-15:00)
- SBS9 Nachtblok (23:30-02:00)
- Net5 Ochtendblok (07:30-11:30)
- Net5 Middagblok (13:20-13:50)
**9.2 Test Commercial Registratie:**
- Ga naar **Commercials**
- Probeer een test commercial aan te maken:
- Titel: "Test Product"
- Duur: 00:30:00
- Series Code: TEST01
- Als `TALPA_MOCK_MODE=true` staat, zou dit moeten werken zonder echte API
**9.3 Test Kalender:**
- Ga naar **Kalender**
- Controleer of de kalender laadt
- Probeer een commercial te slepen (als je er een hebt aangemaakt)
## Troubleshooting
### Probleem: "Class 'Dotenv\Dotenv' not found"
**Oplossing:**
```bash
composer require vlucas/phpdotenv
```
### Probleem: Database connectie fout
**Oplossing:**
1. Controleer `.env` credentials
2. Test database connectie:
```bash
mysql -u talpa_user -p talpa_planning -e "SELECT 1;"
```
### Probleem: Kalender laadt niet
**Oplossing:**
1. Open browser console (F12)
2. Check voor JavaScript errors
3. Controleer of FullCalendar CDN bereikbaar is
4. Test API endpoint:
```bash
curl http://localhost/telvero_whatson_talpa/api/get_transmissions.php?start=2026-01-01&end=2026-01-31
```
### Probleem: Permissie errors
**Oplossing:**
```bash
# Geef webserver eigenaarschap
sudo chown -R www-data:www-data /path/to/project
# Of voor development
sudo chown -R $USER:www-data /path/to/project
```
### Probleem: API errors
**Oplossing:**
1. Zet `TALPA_MOCK_MODE=true` in `.env` voor testen
2. Check `api_log.txt` voor details:
```bash
tail -f api_log.txt
```
## Productie Deployment
### Extra stappen voor productie:
**1. Beveilig .env:**
```bash
chmod 600 .env
```
**2. Schakel debugging uit:**
```env
APP_DEBUG=false
TALPA_MOCK_MODE=false
```
**3. Gebruik HTTPS:**
- Installeer SSL certificaat
- Forceer HTTPS in .htaccess
**4. Database backup:**
```bash
# Maak backup script
cat > backup.sh << 'EOF'
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
mysqldump -u talpa_user -p talpa_planning > backups/backup_$DATE.sql
EOF
chmod +x backup.sh
```
**5. Monitoring:**
- Setup error logging
- Monitor `api_log.txt`
- Setup uptime monitoring
## Updates
Bij toekomstige updates:
```bash
# Pull laatste wijzigingen
git pull origin main
# Update dependencies
composer update
# Voer nieuwe migraties uit
mysql -u talpa_user -p talpa_planning < migrations/002_nieuwe_migratie.sql
# Clear cache (indien van toepassing)
# php artisan cache:clear
```
## Support
Bij problemen:
1. Check deze installatie handleiding
2. Bekijk [README.md](README.md) voor gebruik
3. Check `api_log.txt` voor API errors
4. Bekijk browser console voor JavaScript errors
## Checklist
- [ ] PHP 7.4+ geïnstalleerd
- [ ] MySQL database aangemaakt
- [ ] Composer dependencies geïnstalleerd
- [ ] Database migratie uitgevoerd
- [ ] .env file geconfigureerd
- [ ] Bestandspermissies ingesteld
- [ ] Webserver geconfigureerd
- [ ] Dashboard bereikbaar in browser
- [ ] Blok templates zichtbaar
- [ ] Test commercial aangemaakt
- [ ] Kalender laadt correct
Als alle items zijn afgevinkt, is de installatie succesvol! 🎉

345
README.md
View File

@ -1,2 +1,345 @@
# telvero_whatson_talpa # Telvero Talpa Planning System
Een geavanceerd TV-planning systeem voor het beheren en plannen van homeshopping uitzendingen op Talpa zenders (SBS9, Net5, Veronica).
## 🎯 Functionaliteiten
### ✅ Kalender Planning
- **Drag-and-drop interface** voor eenvoudige planning
- **Timeline view** met meerdere zenders tegelijk
- **Automatische tijdberekening** voor opeenvolgende uitzendingen
- **Kleurgecodeerde uitzendingen** voor overzichtelijkheid
- **Conflict detectie** bij overlappende uitzendingen
### 📺 Commercial Management
- Registratie van commercials via Talpa API
- Automatische media asset aanmaak
- Kleurcode toewijzing per commercial
- Series code voor groepering
- Upload status tracking
### 🕐 Blok Templates
- Definieer terugkerende tijdblokken per zender
- Dagelijkse aanpassing van starttijden mogelijk
- Automatische generatie van dagelijkse blokken
- Ondersteuning voor dag-specifieke templates
### 🔄 Talpa API Integratie
- Episode registratie
- Media asset management
- Transmission scheduling
- Sync status monitoring
## 📋 Vereisten
- PHP 7.4 of hoger
- MySQL 5.7 of hoger
- Composer
- Webserver (Apache/Nginx)
## 🚀 Installatie
### 1. Clone het project
```bash
cd /path/to/your/project
```
### 2. Installeer dependencies
```bash
composer install
```
### 3. Configureer environment
Kopieer `.env.example` naar `.env` en vul de gegevens in:
```env
# Database
DB_HOST=localhost
DB_NAME=talpa_planning
DB_USER=your_username
DB_PASS=your_password
# Talpa API
TALPA_API_BASE=https://api.talpa.tv
TALPA_TOKEN=your_api_token
TALPA_MOCK_MODE=false
# TV Settings
TV_SEASON_ID=your_season_id
```
### 4. Database Setup
Voer de migratie uit:
```bash
mysql -u your_username -p talpa_planning < migrations/001_add_blocks_and_colors.sql
```
Of via phpMyAdmin:
1. Open phpMyAdmin
2. Selecteer de database
3. Ga naar "Import"
4. Upload `migrations/001_add_blocks_and_colors.sql`
### 5. Bestandspermissies
Zorg dat de webserver schrijfrechten heeft:
```bash
chmod 755 api/
chmod 755 assets/
chmod 644 api/*.php
```
## 📁 Projectstructuur
```
/
├── index.php # Dashboard
├── calendar.php # Kalender planning view
├── blocks.php # Blok template management
├── commercials.php # Commercial management
├── helpers.php # Helper functies
├── TalpaAPI.php # API wrapper
├── /api # API endpoints
│ ├── get_transmissions.php
│ ├── create_transmission.php
│ ├── update_transmission.php
│ ├── delete_transmission.php
│ ├── update_block_time.php
│ ├── get_block_time.php
│ └── assign_color.php
├── /assets
│ ├── /css
│ │ └── custom.css # Custom styling
│ └── /js
│ └── calendar-init.js # Calendar JavaScript
├── /migrations
│ └── 001_add_blocks_and_colors.sql
└── README.md
```
## 🎨 Gebruik
### 1. Blok Templates Instellen
1. Ga naar **Blokken** in het menu
2. Klik op "Nieuw Template"
3. Vul de gegevens in:
- Zender (SBS9, NET5, VERONICA)
- Template naam (bijv. "SBS9 Dagblok")
- Dag van de week
- Standaard starttijd
- Optioneel: eindtijd
**Voorbeelden:**
- SBS9 Dagblok: Ma-Zo, 07:00-15:00
- SBS9 Nachtblok: Ma-Zo, 23:30-02:00
- Net5 Ochtend: Ma-Vr, 07:30-11:30
- Net5 Middag: Ma-Vr, 13:20-13:50
### 2. Commercials Registreren
1. Ga naar **Commercials** in het menu
2. Vul het formulier in:
- Product naam
- Duur (HH:MM:SS formaat)
- Optioneel: Series code (bijv. 006a)
3. Klik op "Registreren bij Talpa"
4. Het systeem:
- Maakt een episode aan via API
- Genereert een media asset
- Wijst automatisch een unieke kleur toe
5. Update de upload status naar "Uploaded" wanneer de video is geüpload
### 3. Planning Maken
1. Ga naar **Kalender** in het menu
2. Selecteer de gewenste week/dag view
3. **Optioneel:** Pas blok starttijd aan:
- Klik op een tijdslot
- Wijzig de starttijd
- Kies of je alle uitzendingen wilt herberekenen
4. **Plan uitzendingen:**
- Sleep een commercial uit de sidebar
- Drop deze op het gewenste tijdslot
- Het systeem berekent automatisch de volgende starttijd
5. **Bewerk uitzendingen:**
- Klik op een uitzending voor details
- Sleep om te verplaatsen
- Verwijder indien nodig
### 4. Synchroniseren met Talpa
1. Klik op een uitzending in de kalender
2. Klik op "Sync naar Talpa"
3. Monitor de status:
- 🟡 **Pending:** Nog niet gesynchroniseerd
- 🟢 **Synced:** Succesvol gesynchroniseerd
- 🔴 **Error:** Fout opgetreden
## 🎨 Kleurcode Systeem
Het systeem wijst automatisch unieke kleuren toe aan commercials voor visuele identificatie:
- Kleuren worden automatisch gegenereerd met voldoende contrast
- Elke commercial krijgt een unieke kleur
- Series codes kunnen dezelfde kleur delen
- Kleuren zijn handmatig aanpasbaar in Commercial Management
## 🔧 Geavanceerde Functies
### Automatische Tijdberekening
Het systeem berekent automatisch de starttijd van de volgende uitzending:
```
Volgende starttijd = Huidige starttijd + Duur huidige uitzending
```
### Conflict Detectie
Bij het plannen controleert het systeem op:
- Overlappende uitzendingen op dezelfde zender
- Uitzendingen buiten blok tijden
- Dubbele bookings
### Blok Herberekening
Wanneer je een blok starttijd aanpast:
1. Optie 1: Alleen de starttijd wijzigen
2. Optie 2: Alle uitzendingen in het blok herberekenen vanaf de nieuwe starttijd
## 🐛 Troubleshooting
### Kalender laadt niet
**Probleem:** Kalender toont geen events
**Oplossing:**
1. Check browser console voor JavaScript errors
2. Controleer of API endpoints bereikbaar zijn:
```bash
curl http://your-domain/api/get_transmissions.php?start=2026-01-01&end=2026-01-31
```
3. Controleer database connectie in `.env`
### Drag-and-drop werkt niet
**Probleem:** Commercials zijn niet sleepbaar
**Oplossing:**
1. Controleer of FullCalendar correct is geladen
2. Check browser console voor errors
3. Zorg dat commercials status "uploaded" hebben
### API Sync Errors
**Probleem:** Synchronisatie met Talpa faalt
**Oplossing:**
1. Controleer API credentials in `.env`
2. Check `api_log.txt` voor details
3. Verifieer dat content_id en media_asset_id correct zijn
4. Test met `TALPA_MOCK_MODE=true` voor debugging
### Database Errors
**Probleem:** SQL errors bij gebruik
**Oplossing:**
1. Controleer of migratie correct is uitgevoerd:
```sql
SHOW TABLES;
DESCRIBE block_templates;
DESCRIBE daily_blocks;
```
2. Controleer of alle kolommen bestaan:
```sql
SHOW COLUMNS FROM commercials LIKE 'color_code';
```
## 📊 Database Schema
### Nieuwe Tabellen
**block_templates**
- Definieert terugkerende tijdblokken
- Per zender en dag van de week
- Standaard start- en eindtijden
**daily_blocks**
- Dagelijkse instanties van templates
- Mogelijkheid tot aanpassing per dag
- Gekoppeld aan template_id
### Uitgebreide Kolommen
**commercials**
- `color_code` (VARCHAR(7)): Hex kleurcode
- `series_code` (VARCHAR(20)): Series identifier
## 🔐 Beveiliging
- Gebruik prepared statements voor alle database queries
- Valideer alle input (datum, tijd, kleuren)
- Sanitize output met `htmlspecialchars()`
- Gebruik HTTPS in productie
- Bewaar `.env` buiten webroot
- Voeg `.env` toe aan `.gitignore`
## 📈 Performance Tips
1. **Database Indexen:** Reeds toegevoegd in migratie
2. **Caching:** Overweeg Redis voor API responses
3. **Lazy Loading:** Kalender laadt alleen zichtbare periode
4. **Batch Operations:** Sync meerdere uitzendingen tegelijk
## 🔄 Updates & Migraties
Bij toekomstige updates:
1. Maak nieuwe migratie file: `002_description.sql`
2. Voer uit in volgorde
3. Update deze README met wijzigingen
## 📞 Support
Voor vragen of problemen:
- Check de troubleshooting sectie
- Bekijk `api_log.txt` voor API details
- Controleer browser console voor JavaScript errors
## 📝 Changelog
### Versie 1.0.0 (2026-01-13)
- ✅ Kalender planning met drag-and-drop
- ✅ Blok template management
- ✅ Commercial management met kleurcodes
- ✅ Automatische tijdberekening
- ✅ Talpa API integratie
- ✅ Conflict detectie
- ✅ Responsive design
## 🎯 Roadmap
Toekomstige features:
- [ ] Bulk import van commercials
- [ ] Excel export van planning
- [ ] Email notificaties bij sync errors
- [ ] Gebruikersbeheer en rechten
- [ ] Planning templates (kopieer week)
- [ ] Statistieken en rapportages
- [ ] Mobile app
## 📄 Licentie
Proprietary - Telvero © 2026
---
**Gemaakt met ❤️ voor Telvero Talpa Planning**

View File

@ -100,9 +100,14 @@ class TalpaApi {
]); ]);
} }
public function deleteEpisode($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) return ["id" => "MOCK_CONT_" . time()]; if (strpos($endpoint, '/content/v1/episodes') !== false && strpos($endpoint, 'DELETE') === false) return ["id" => "MOCK_CONT_" . time()];
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) return ["statusCode" => "201", "id" => "MOCK_TX_" . time()];

97
api/assign_color.php Normal file
View File

@ -0,0 +1,97 @@
<?php
/**
* API Endpoint: Assign Color to Commercial
* Assigns or updates color code for a commercial
*/
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);
if (!$input) {
$input = $_POST;
}
// Validate required fields
if (empty($input['commercial_id'])) {
jsonResponse([
'success' => false,
'error' => 'Missing commercial_id'
], 400);
}
// Check if commercial exists
$stmt = $db->prepare("SELECT id, color_code FROM commercials WHERE id = ?");
$stmt->execute([$input['commercial_id']]);
$commercial = $stmt->fetch();
if (!$commercial) {
jsonResponse([
'success' => false,
'error' => 'Commercial not found'
], 404);
}
// Determine color
$colorCode = null;
if (!empty($input['color_code'])) {
// Use provided color
$colorCode = $input['color_code'];
// Validate hex format
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $colorCode)) {
jsonResponse([
'success' => false,
'error' => 'Invalid color format. Use hex format like #FF5733'
], 400);
}
} else {
// Auto-generate distinct color
$stmt = $db->query("SELECT color_code FROM commercials WHERE color_code IS NOT NULL");
$existingColors = $stmt->fetchAll(PDO::FETCH_COLUMN);
$colorCode = generateDistinctColor($existingColors);
}
// Update commercial
$stmt = $db->prepare("
UPDATE commercials
SET color_code = ?, series_code = ?
WHERE id = ?
");
$stmt->execute([
$colorCode,
$input['series_code'] ?? null,
$input['commercial_id']
]);
// Get updated commercial
$stmt = $db->prepare("SELECT * FROM commercials WHERE id = ?");
$stmt->execute([$input['commercial_id']]);
$updated = $stmt->fetch();
jsonResponse([
'success' => true,
'message' => 'Color assigned successfully',
'commercial' => $updated
]);
} catch (Exception $e) {
jsonResponse([
'success' => false,
'error' => $e->getMessage()
], 500);
}

195
api/create_transmission.php Normal file
View File

@ -0,0 +1,195 @@
<?php
/**
* API Endpoint: Create Transmission
* Creates a new transmission (planning entry)
*/
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);
if (!$input) {
$input = $_POST;
}
// Validate required fields
$required = ['commercial_id', 'channel', 'start_date', 'start_time'];
foreach ($required as $field) {
if (empty($input[$field])) {
jsonResponse([
'success' => false,
'error' => "Missing required field: $field"
], 400);
}
}
// Validate date and time formats
if (!isValidDate($input['start_date'])) {
jsonResponse([
'success' => false,
'error' => 'Invalid date format'
], 400);
}
if (!isValidTime($input['start_time'])) {
jsonResponse([
'success' => false,
'error' => 'Invalid time format'
], 400);
}
// Get commercial details (duration)
$stmt = $db->prepare("SELECT duration, title FROM commercials WHERE id = ?");
$stmt->execute([$input['commercial_id']]);
$commercial = $stmt->fetch();
if (!$commercial) {
jsonResponse([
'success' => false,
'error' => 'Commercial not found'
], 404);
}
// Get template from input or use default
$template = $input['template'] ?? 'HOME030';
// Calculate end time
$endTime = addTimeToTime($input['start_time'], $commercial['duration']);
// Validate that transmission falls within a block (inline to avoid curl issues)
ensureDailyBlocks($db, $input['start_date'], $input['start_date']);
$stmt = $db->prepare("
SELECT * FROM daily_blocks
WHERE block_date = ? AND channel = ?
ORDER BY actual_start_time
");
$stmt->execute([$input['start_date'], $input['channel']]);
$blocks = $stmt->fetchAll();
$withinBlock = false;
foreach ($blocks as $block) {
$blockStart = $block['actual_start_time'];
$blockEnd = $block['actual_end_time'] ?? '23:59:59';
// Handle overnight blocks
$isOvernight = $blockEnd < $blockStart;
if ($isOvernight) {
if ($input['start_time'] >= $blockStart || $input['start_time'] < $blockEnd) {
if ($endTime <= $blockEnd || $endTime >= $blockStart) {
$withinBlock = true;
break;
}
}
} else {
if ($input['start_time'] >= $blockStart && $endTime <= $blockEnd) {
$withinBlock = true;
break;
}
}
}
if (!$withinBlock) {
$errorMessage = "Uitzending valt buiten blok tijden. ";
if (!empty($blocks)) {
$errorMessage .= "Beschikbare blokken: ";
foreach ($blocks as $block) {
$blockStart = substr($block['actual_start_time'], 0, 5);
$blockEnd = $block['actual_end_time'] ? substr($block['actual_end_time'], 0, 5) : '∞';
$errorMessage .= "{$blockStart}-{$blockEnd}, ";
}
$errorMessage = rtrim($errorMessage, ', ');
}
jsonResponse([
'success' => false,
'error' => $errorMessage,
'attempted_time' => $input['start_time'] . ' - ' . $endTime
], 400);
}
// Check for overlapping transmissions
$stmt = $db->prepare("
SELECT t.id, t.start_time, t.duration, c.title
FROM transmissions t
JOIN commercials c ON t.commercial_id = c.id
WHERE t.start_date = ?
AND t.channel = ?
AND t.id != ?
");
$stmt->execute([
$input['start_date'],
$input['channel'],
$input['id'] ?? 0
]);
$existing = $stmt->fetchAll();
foreach ($existing as $tx) {
$txEnd = addTimeToTime($tx['start_time'], $tx['duration']);
if (timeRangesOverlap($input['start_time'], $endTime, $tx['start_time'], $txEnd)) {
jsonResponse([
'success' => false,
'error' => "Overlap detected with: {$tx['title']} ({$tx['start_time']} - {$txEnd})",
'overlap' => true
], 409);
}
}
// Insert transmission
$stmt = $db->prepare("
INSERT INTO transmissions
(commercial_id, channel, template, start_date, start_time, duration, api_status)
VALUES (?, ?, ?, ?, ?, ?, 'pending')
");
$stmt->execute([
$input['commercial_id'],
$input['channel'],
$template,
$input['start_date'],
$input['start_time'],
$commercial['duration']
]);
$transmissionId = $db->lastInsertId();
// Get the created transmission with all details
$stmt = $db->prepare("
SELECT
t.*,
c.title,
c.color_code,
c.series_code
FROM transmissions t
JOIN commercials c ON t.commercial_id = c.id
WHERE t.id = ?
");
$stmt->execute([$transmissionId]);
$transmission = $stmt->fetch();
jsonResponse([
'success' => true,
'message' => 'Transmission created successfully',
'transmission' => $transmission
]);
} catch (Exception $e) {
jsonResponse([
'success' => false,
'error' => $e->getMessage()
], 500);
}

View File

@ -0,0 +1,60 @@
<?php
/**
* API Endpoint: Delete Transmission
* Deletes a transmission from the schedule
*/
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);
if (!$input) {
$input = $_POST;
}
// Validate required fields
if (empty($input['id'])) {
jsonResponse([
'success' => false,
'error' => 'Missing transmission ID'
], 400);
}
// Check if transmission exists
$stmt = $db->prepare("SELECT id FROM transmissions WHERE id = ?");
$stmt->execute([$input['id']]);
if (!$stmt->fetch()) {
jsonResponse([
'success' => false,
'error' => 'Transmission not found'
], 404);
}
// Delete transmission
$stmt = $db->prepare("DELETE FROM transmissions WHERE id = ?");
$stmt->execute([$input['id']]);
jsonResponse([
'success' => true,
'message' => 'Transmission deleted successfully'
]);
} catch (Exception $e) {
jsonResponse([
'success' => false,
'error' => $e->getMessage()
], 500);
}

63
api/get_block_time.php Normal file
View File

@ -0,0 +1,63 @@
<?php
/**
* API Endpoint: Get Block Time
* Returns the start time for a specific daily 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();
$date = $_GET['date'] ?? null;
$channel = $_GET['channel'] ?? null;
if (!$date || !$channel) {
jsonResponse([
'error' => 'Missing date or channel parameter'
], 400);
}
// Get daily block
$stmt = $db->prepare("
SELECT actual_start_time as start_time, actual_end_time as end_time
FROM daily_blocks
WHERE block_date = ? AND channel = ?
LIMIT 1
");
$stmt->execute([$date, $channel]);
$block = $stmt->fetch();
if ($block) {
jsonResponse($block);
} else {
// Return default from template
$dayOfWeek = ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'][date('w', strtotime($date))];
$stmt = $db->prepare("
SELECT default_start_time as start_time, default_end_time as end_time
FROM block_templates
WHERE channel = ?
AND (day_of_week = ? OR day_of_week = 'all')
AND is_active = 1
LIMIT 1
");
$stmt->execute([$channel, $dayOfWeek]);
$template = $stmt->fetch();
jsonResponse($template ?: ['start_time' => '07:00:00', 'end_time' => null]);
}
} catch (Exception $e) {
jsonResponse([
'error' => $e->getMessage()
], 500);
}

95
api/get_daily_blocks.php Normal file
View File

@ -0,0 +1,95 @@
<?php
/**
* API Endpoint: Get Daily Blocks
* Returns active blocks for a specific date
*/
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();
$date = $_GET['date'] ?? date('Y-m-d');
if (!isValidDate($date)) {
jsonResponse([
'error' => 'Invalid date format'
], 400);
}
// Ensure daily blocks exist for this date
ensureDailyBlocks($db, $date, $date);
// Get all blocks for this date
$stmt = $db->prepare("
SELECT
db.*,
bt.name as template_name,
bt.channel
FROM daily_blocks db
LEFT JOIN block_templates bt ON db.template_id = bt.id
WHERE db.block_date = ?
ORDER BY db.channel, db.actual_start_time
");
$stmt->execute([$date]);
$blocks = $stmt->fetchAll();
// Format for calendar background events - assign to correct resource
$backgroundEvents = [];
foreach ($blocks as $block) {
$startDateTime = $date . 'T' . $block['actual_start_time'];
$endDateTime = $date . 'T' . ($block['actual_end_time'] ?? '23:59:59');
// Handle overnight blocks
if ($block['actual_end_time'] && $block['actual_end_time'] < $block['actual_start_time']) {
$endDateTime = date('Y-m-d', strtotime($date . ' +1 day')) . 'T' . $block['actual_end_time'];
}
$backgroundEvents[] = [
'id' => 'block_' . $block['id'],
'title' => $block['template_name'] ?? 'Blok',
'start' => $startDateTime,
'end' => $endDateTime,
'resourceId' => $block['channel'], // Assign to specific resource/channel
'display' => 'background',
'backgroundColor' => getBlockColor($block['channel']),
'extendedProps' => [
'type' => 'block',
'block_id' => $block['id'],
'channel' => $block['channel'],
'template_name' => $block['template_name']
]
];
}
jsonResponse([
'blocks' => $blocks,
'backgroundEvents' => $backgroundEvents
]);
} catch (Exception $e) {
jsonResponse([
'error' => $e->getMessage()
], 500);
}
/**
* Get color for block background based on channel
*/
function getBlockColor($channel) {
$colors = [
'SBS9' => 'rgba(52, 152, 219, 0.1)', // Light blue
'NET5' => 'rgba(231, 76, 60, 0.1)', // Light red
'VERONICA' => 'rgba(155, 89, 182, 0.1)' // Light purple
];
return $colors[$channel] ?? 'rgba(200, 200, 200, 0.1)';
}

View File

@ -0,0 +1,37 @@
<?php
/**
* API Endpoint: Get Next Start Time
* Calculates the next available start time based on existing transmissions
*/
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();
$date = $_GET['date'] ?? date('Y-m-d');
$channel = $_GET['channel'] ?? 'SBS9';
$requestedTime = $_GET['requested_time'] ?? null;
// Get next start time using helper function
$nextStartTime = calculateNextStartTime($db, $date, $channel, $requestedTime);
jsonResponse([
'next_start_time' => $nextStartTime,
'date' => $date,
'channel' => $channel
]);
} catch (Exception $e) {
jsonResponse([
'error' => $e->getMessage()
], 500);
}

98
api/get_transmissions.php Normal file
View File

@ -0,0 +1,98 @@
<?php
/**
* API Endpoint: Get Transmissions
* Returns transmissions for calendar view in FullCalendar format
*/
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 date range from query parameters
$start = $_GET['start'] ?? date('Y-m-d', strtotime('-7 days'));
$end = $_GET['end'] ?? date('Y-m-d', strtotime('+30 days'));
$channel = $_GET['channel'] ?? null;
// Ensure daily blocks exist for this date range
ensureDailyBlocks($db, $start, $end);
// Build query
$sql = "
SELECT
t.id,
t.commercial_id,
t.channel,
t.template,
t.start_date,
t.start_time,
t.duration,
t.api_status,
c.title,
c.color_code,
c.series_code
FROM transmissions t
JOIN commercials c ON t.commercial_id = c.id
WHERE t.start_date BETWEEN ? AND ?
";
$params = [$start, $end];
if ($channel) {
$sql .= " AND t.channel = ?";
$params[] = $channel;
}
$sql .= " ORDER BY t.start_date, t.start_time";
$stmt = $db->prepare($sql);
$stmt->execute($params);
$transmissions = $stmt->fetchAll();
// Format for FullCalendar
$events = [];
foreach ($transmissions as $tx) {
// Calculate end time
$startDateTime = new DateTime($tx['start_date'] . ' ' . $tx['start_time']);
$endDateTime = clone $startDateTime;
if ($tx['duration']) {
list($h, $m, $s) = explode(':', $tx['duration']);
$endDateTime->add(new DateInterval("PT{$h}H{$m}M{$s}S"));
}
$events[] = [
'id' => $tx['id'],
'title' => $tx['title'],
'start' => $startDateTime->format('Y-m-d\TH:i:s'),
'end' => $endDateTime->format('Y-m-d\TH:i:s'),
'resourceId' => $tx['channel'],
'backgroundColor' => $tx['color_code'] ?? '#cccccc',
'borderColor' => $tx['color_code'] ?? '#cccccc',
'textColor' => '#ffffff',
'extendedProps' => [
'commercial_id' => $tx['commercial_id'],
'template' => $tx['template'],
'duration' => $tx['duration'],
'api_status' => $tx['api_status'],
'series_code' => $tx['series_code']
]
];
}
jsonResponse($events);
} catch (Exception $e) {
jsonResponse([
'error' => true,
'message' => $e->getMessage()
], 500);
}

View File

@ -0,0 +1,118 @@
<?php
/**
* API Endpoint: Insert Transmission at Position
* Inserts a transmission at a specific position in the 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);
if (!$input) {
$input = $_POST;
}
// Validate required fields
$required = ['commercial_id', 'channel', 'date', 'block_id', 'position'];
foreach ($required as $field) {
if (!isset($input[$field])) {
jsonResponse([
'success' => false,
'error' => "Missing required field: $field"
], 400);
}
}
// Get commercial duration
$stmt = $db->prepare("SELECT duration FROM commercials WHERE id = ?");
$stmt->execute([$input['commercial_id']]);
$duration = $stmt->fetchColumn();
if (!$duration) {
jsonResponse([
'success' => false,
'error' => 'Commercial not found'
], 404);
}
// Get block start and end time
$stmt = $db->prepare("SELECT actual_start_time, actual_end_time FROM daily_blocks WHERE id = ?");
$stmt->execute([$input['block_id']]);
$blockInfo = $stmt->fetch();
$blockStartTime = $blockInfo['actual_start_time'];
$blockEndTime = $blockInfo['actual_end_time'] ?? '23:59:59';
// Get all transmissions in THIS SPECIFIC block only
$stmt = $db->prepare("
SELECT t.id, t.start_time, c.duration
FROM transmissions t
JOIN commercials c ON t.commercial_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([$input['date'], $input['channel'], $blockStartTime, $blockEndTime]);
$transmissions = $stmt->fetchAll();
// Insert new transmission at position
$position = (int)$input['position'];
// Create temporary transmission entry
$newTransmission = [
'commercial_id' => $input['commercial_id'],
'duration' => $duration
];
// Insert at position
array_splice($transmissions, $position, 0, [$newTransmission]);
// Recalculate all start times
$currentTime = $blockStartTime;
foreach ($transmissions as $index => $tx) {
if ($index === $position) {
// Insert new transmission
$stmt = $db->prepare("
INSERT INTO transmissions
(commercial_id, channel, template, start_date, start_time, duration, api_status)
VALUES (?, ?, 'HOME030', ?, ?, ?, 'pending')
");
$stmt->execute([
$input['commercial_id'],
$input['channel'],
$input['date'],
$currentTime,
$duration
]);
} else {
// Update existing transmission
$stmt = $db->prepare("UPDATE transmissions SET start_time = ?, api_status = 'pending' WHERE id = ?");
$stmt->execute([$currentTime, $tx['id']]);
}
$currentTime = addTimeToTime($currentTime, $tx['duration']);
}
jsonResponse([
'success' => true,
'message' => 'Transmission inserted successfully'
]);
} catch (Exception $e) {
jsonResponse([
'success' => false,
'error' => $e->getMessage()
], 500);
}

172
api/sync_block.php Normal file
View File

@ -0,0 +1,172 @@
<?php
/**
* Sync Block to Talpa
* Synchronizes all transmissions in a specific block to Talpa API
*/
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../TalpaAPI.php';
require_once __DIR__ . '/../helpers.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
header('Content-Type: application/json');
$api = new TalpaApi();
$db = getDbConnection();
// Debug logging
$debugLog = [];
$debugLog[] = ['step' => 'Start', 'time' => date('Y-m-d H:i:s')];
// Get POST data
$input = json_decode(file_get_contents('php://input'), true);
$debugLog[] = ['step' => 'Input received', 'data' => $input];
if (!isset($input['date']) || !isset($input['channel'])) {
echo json_encode([
'success' => false,
'error' => 'Datum en channel zijn verplicht',
'debug' => $debugLog
]);
exit;
}
$date = $input['date'];
$channel = $input['channel'];
$debugLog[] = ['step' => 'Parameters', 'date' => $date, 'channel' => $channel];
try {
// Get all transmissions for this block that are not yet synced
$stmt = $db->prepare("
SELECT t.*, c.content_id, c.title as commercial_title
FROM transmissions t
JOIN commercials c ON t.commercial_id = c.id
WHERE t.start_date = ?
AND t.channel = ?
AND t.api_status != 'synced'
ORDER BY t.start_time ASC
");
$stmt->execute([$date, $channel]);
$transmissions = $stmt->fetchAll(PDO::FETCH_ASSOC);
$debugLog[] = ['step' => 'Query executed', 'count' => count($transmissions)];
if (empty($transmissions)) {
echo json_encode([
'success' => true,
'message' => 'Geen uitzendingen om te synchroniseren',
'synced' => 0,
'failed' => 0,
'debug' => $debugLog
]);
exit;
}
$syncedCount = 0;
$failedCount = 0;
$errors = [];
$apiCalls = [];
// Sync each transmission
foreach ($transmissions as $tx) {
$txDebug = [
'transmission_id' => $tx['id'],
'title' => $tx['commercial_title'],
'time' => $tx['start_time']
];
try {
$requestData = [
"channel" => $tx['channel'],
"template" => $tx['template'],
"startDate" => $tx['start_date'],
"startTime" => $tx['start_time'],
"duration" => $tx['duration'],
"contentId" => $tx['content_id']
];
$txDebug['request'] = $requestData;
$res = $api->createTransmission($requestData);
$txDebug['response'] = $res;
$txDebug['lastResponse'] = $api->lastResponse;
// Check if sync was successful
$status = (isset($res['id']) || (isset($res['statusCode']) && $res['statusCode'] == 201)) ? 'synced' : 'error';
$txDebug['determined_status'] = $status;
// Update transmission status
$updateStmt = $db->prepare("
UPDATE transmissions
SET api_status = ?, api_response = ?
WHERE id = ?
");
$updateStmt->execute([$status, json_encode($res), $tx['id']]);
if ($status === 'synced') {
$syncedCount++;
$txDebug['result'] = 'success';
} else {
$failedCount++;
$txDebug['result'] = 'failed';
$errors[] = [
'transmission_id' => $tx['id'],
'title' => $tx['commercial_title'],
'time' => $tx['start_time'],
'error' => $res['message'] ?? json_encode($res)
];
}
} catch (Exception $e) {
$failedCount++;
$txDebug['result'] = 'exception';
$txDebug['exception'] = $e->getMessage();
$errors[] = [
'transmission_id' => $tx['id'],
'title' => $tx['commercial_title'],
'time' => $tx['start_time'],
'error' => $e->getMessage()
];
// Update status to error
$updateStmt = $db->prepare("
UPDATE transmissions
SET api_status = 'error', api_response = ?
WHERE id = ?
");
$updateStmt->execute([json_encode(['error' => $e->getMessage()]), $tx['id']]);
}
$apiCalls[] = $txDebug;
}
$debugLog[] = ['step' => 'Sync completed', 'synced' => $syncedCount, 'failed' => $failedCount];
echo json_encode([
'success' => true,
'message' => "Synchronisatie voltooid: {$syncedCount} geslaagd, {$failedCount} mislukt",
'synced' => $syncedCount,
'failed' => $failedCount,
'errors' => $errors,
'debug' => $debugLog,
'api_calls' => $apiCalls
]);
} catch (Exception $e) {
$debugLog[] = ['step' => 'Exception caught', 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString()];
echo json_encode([
'success' => false,
'error' => 'Database fout: ' . $e->getMessage(),
'debug' => $debugLog
]);
}

143
api/update_block_time.php Normal file
View File

@ -0,0 +1,143 @@
<?php
/**
* API Endpoint: Update Block Time
* Updates the start time for a daily 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);
if (!$input) {
$input = $_POST;
}
// Validate required fields
$required = ['date', 'channel', 'start_time'];
foreach ($required as $field) {
if (empty($input[$field])) {
jsonResponse([
'success' => false,
'error' => "Missing required field: $field"
], 400);
}
}
// Validate formats
if (!isValidDate($input['date'])) {
jsonResponse([
'success' => false,
'error' => 'Invalid date format'
], 400);
}
if (!isValidTime($input['start_time'])) {
jsonResponse([
'success' => false,
'error' => 'Invalid time format'
], 400);
}
// Get template_id if provided, or find matching template
$templateId = $input['template_id'] ?? null;
if (!$templateId) {
// Try to find matching template
$dayOfWeek = ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'][date('w', strtotime($input['date']))];
$stmt = $db->prepare("
SELECT id FROM block_templates
WHERE channel = ?
AND (day_of_week = ? OR day_of_week = 'all')
AND is_active = 1
ORDER BY day_of_week DESC
LIMIT 1
");
$stmt->execute([$input['channel'], $dayOfWeek]);
$templateId = $stmt->fetchColumn();
}
// Check if daily block exists for this template
$stmt = $db->prepare("
SELECT id FROM daily_blocks
WHERE block_date = ? AND channel = ? AND template_id = ?
");
$stmt->execute([$input['date'], $input['channel'], $templateId]);
$block = $stmt->fetch();
if ($block) {
// Update existing block
$stmt = $db->prepare("
UPDATE daily_blocks
SET actual_start_time = ?, updated_at = NOW()
WHERE id = ?
");
$stmt->execute([$input['start_time'], $block['id']]);
} else {
// Update or insert - use ON DUPLICATE KEY UPDATE
$stmt = $db->prepare("
INSERT INTO daily_blocks
(template_id, channel, block_date, actual_start_time)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
actual_start_time = VALUES(actual_start_time),
updated_at = NOW()
");
$stmt->execute([
$templateId,
$input['channel'],
$input['date'],
$input['start_time']
]);
}
// Optionally recalculate all transmission times in this block
if (!empty($input['recalculate'])) {
$stmt = $db->prepare("
SELECT t.id, t.start_time, c.duration
FROM transmissions t
JOIN commercials c ON t.commercial_id = c.id
WHERE t.start_date = ? AND t.channel = ?
ORDER BY t.start_time ASC
");
$stmt->execute([$input['date'], $input['channel']]);
$transmissions = $stmt->fetchAll();
$currentTime = $input['start_time'];
foreach ($transmissions as $tx) {
// Update transmission start time
$stmt = $db->prepare("
UPDATE transmissions
SET start_time = ?, api_status = 'pending'
WHERE id = ?
");
$stmt->execute([$currentTime, $tx['id']]);
// Calculate next start time
$currentTime = addTimeToTime($currentTime, $tx['duration']);
}
}
jsonResponse([
'success' => true,
'message' => 'Block time updated successfully'
]);
} catch (Exception $e) {
jsonResponse([
'success' => false,
'error' => $e->getMessage()
], 500);
}

195
api/update_transmission.php Normal file
View File

@ -0,0 +1,195 @@
<?php
/**
* API Endpoint: Update Transmission
* Updates an existing transmission (drag-and-drop, time changes)
*/
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);
if (!$input) {
$input = $_POST;
}
// Validate required fields
if (empty($input['id'])) {
jsonResponse([
'success' => false,
'error' => 'Missing transmission ID'
], 400);
}
// Get existing transmission
$stmt = $db->prepare("
SELECT t.*, c.duration
FROM transmissions t
JOIN commercials c ON t.commercial_id = c.id
WHERE t.id = ?
");
$stmt->execute([$input['id']]);
$transmission = $stmt->fetch();
if (!$transmission) {
jsonResponse([
'success' => false,
'error' => 'Transmission not found'
], 404);
}
// Prepare update data
$updates = [];
$params = [];
if (isset($input['start_date']) && isValidDate($input['start_date'])) {
$updates[] = 'start_date = ?';
$params[] = $input['start_date'];
} else {
$params[] = $transmission['start_date'];
}
if (isset($input['start_time']) && isValidTime($input['start_time'])) {
$updates[] = 'start_time = ?';
$params[] = $input['start_time'];
} else {
$params[] = $transmission['start_time'];
}
if (isset($input['channel'])) {
$updates[] = 'channel = ?';
$params[] = $input['channel'];
} else {
$params[] = $transmission['channel'];
}
// Check for overlaps with new position
$checkDate = $params[0];
$checkTime = $params[1];
$checkChannel = $params[2];
$endTime = addTimeToTime($checkTime, $transmission['duration']);
// Validate that transmission falls within a block (inline to avoid curl issues)
ensureDailyBlocks($db, $checkDate, $checkDate);
$stmt = $db->prepare("
SELECT * FROM daily_blocks
WHERE block_date = ? AND channel = ?
ORDER BY actual_start_time
");
$stmt->execute([$checkDate, $checkChannel]);
$blocks = $stmt->fetchAll();
$withinBlock = false;
foreach ($blocks as $block) {
$blockStart = $block['actual_start_time'];
$blockEnd = $block['actual_end_time'] ?? '23:59:59';
// Handle overnight blocks
$isOvernight = $blockEnd < $blockStart;
if ($isOvernight) {
if ($checkTime >= $blockStart || $checkTime < $blockEnd) {
if ($endTime <= $blockEnd || $endTime >= $blockStart) {
$withinBlock = true;
break;
}
}
} else {
if ($checkTime >= $blockStart && $endTime <= $blockEnd) {
$withinBlock = true;
break;
}
}
}
if (!$withinBlock) {
$errorMessage = "Uitzending valt buiten blok tijden. ";
if (!empty($blocks)) {
$errorMessage .= "Beschikbare blokken: ";
foreach ($blocks as $block) {
$blockStart = substr($block['actual_start_time'], 0, 5);
$blockEnd = $block['actual_end_time'] ? substr($block['actual_end_time'], 0, 5) : '∞';
$errorMessage .= "{$blockStart}-{$blockEnd}, ";
}
$errorMessage = rtrim($errorMessage, ', ');
}
jsonResponse([
'success' => false,
'error' => $errorMessage,
'attempted_time' => $checkTime . ' - ' . $endTime
], 400);
}
$stmt = $db->prepare("
SELECT t.id, t.start_time, t.duration, c.title
FROM transmissions t
JOIN commercials c ON t.commercial_id = c.id
WHERE t.start_date = ?
AND t.channel = ?
AND t.id != ?
");
$stmt->execute([$checkDate, $checkChannel, $input['id']]);
$existing = $stmt->fetchAll();
foreach ($existing as $tx) {
$txEnd = addTimeToTime($tx['start_time'], $tx['duration']);
if (timeRangesOverlap($checkTime, $endTime, $tx['start_time'], $txEnd)) {
jsonResponse([
'success' => false,
'error' => "Overlap detected with: {$tx['title']} ({$tx['start_time']} - {$txEnd})",
'overlap' => true
], 409);
}
}
// Mark as pending sync if changed
$updates[] = "api_status = 'pending'";
// Build and execute update query
if (!empty($updates)) {
$sql = "UPDATE transmissions SET " . implode(', ', $updates) . " WHERE id = ?";
$params[] = $input['id'];
$stmt = $db->prepare($sql);
$stmt->execute($params);
}
// Get updated transmission
$stmt = $db->prepare("
SELECT
t.*,
c.title,
c.color_code,
c.series_code
FROM transmissions t
JOIN commercials c ON t.commercial_id = c.id
WHERE t.id = ?
");
$stmt->execute([$input['id']]);
$updated = $stmt->fetch();
jsonResponse([
'success' => true,
'message' => 'Transmission updated successfully',
'transmission' => $updated
]);
} catch (Exception $e) {
jsonResponse([
'success' => false,
'error' => $e->getMessage()
], 500);
}

View File

@ -0,0 +1,130 @@
<?php
/**
* API Endpoint: Validate Transmission Time
* Checks if a transmission falls within an active 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);
if (!$input) {
$input = $_POST;
}
// Validate required fields
$required = ['channel', 'start_date', 'start_time', 'duration'];
foreach ($required as $field) {
if (empty($input[$field])) {
jsonResponse([
'valid' => false,
'error' => "Missing required field: $field"
], 400);
}
}
$channel = $input['channel'];
$startDate = $input['start_date'];
$startTime = $input['start_time'];
$duration = $input['duration'];
// Calculate end time
$endTime = addTimeToTime($startTime, $duration);
// Ensure daily blocks exist
ensureDailyBlocks($db, $startDate, $startDate);
// Get all blocks for this channel and date
$stmt = $db->prepare("
SELECT * FROM daily_blocks
WHERE block_date = ? AND channel = ?
ORDER BY actual_start_time
");
$stmt->execute([$startDate, $channel]);
$blocks = $stmt->fetchAll();
if (empty($blocks)) {
jsonResponse([
'valid' => false,
'error' => "Geen actief blok gevonden voor {$channel} op {$startDate}",
'blocks' => []
]);
}
// Check if transmission falls within any block
$withinBlock = false;
$matchingBlock = null;
foreach ($blocks as $block) {
$blockStart = $block['actual_start_time'];
$blockEnd = $block['actual_end_time'] ?? '23:59:59';
// Handle overnight blocks
$isOvernight = $blockEnd < $blockStart;
if ($isOvernight) {
// For overnight blocks, check if time is after start OR before end
if ($startTime >= $blockStart || $startTime < $blockEnd) {
// Also check end time
if ($endTime <= $blockEnd || $endTime >= $blockStart) {
$withinBlock = true;
$matchingBlock = $block;
break;
}
}
} else {
// Normal block: check if both start and end are within block
if ($startTime >= $blockStart && $endTime <= $blockEnd) {
$withinBlock = true;
$matchingBlock = $block;
break;
}
}
}
if ($withinBlock) {
jsonResponse([
'valid' => true,
'message' => 'Uitzending valt binnen blok',
'block' => $matchingBlock
]);
} else {
// Find closest block for helpful error message
$closestBlock = $blocks[0];
$errorMessage = "Uitzending valt buiten blok tijden. ";
$errorMessage .= "Beschikbare blokken: ";
foreach ($blocks as $block) {
$blockStart = substr($block['actual_start_time'], 0, 5);
$blockEnd = $block['actual_end_time'] ? substr($block['actual_end_time'], 0, 5) : '∞';
$errorMessage .= "{$blockStart}-{$blockEnd}, ";
}
$errorMessage = rtrim($errorMessage, ', ');
jsonResponse([
'valid' => false,
'error' => $errorMessage,
'blocks' => $blocks,
'attempted_time' => $startTime . ' - ' . $endTime
]);
}
} catch (Exception $e) {
jsonResponse([
'valid' => false,
'error' => $e->getMessage()
], 500);
}

486
assets/css/custom.css Normal file
View File

@ -0,0 +1,486 @@
/**
* Custom Styles for Telvero Talpa Planning System
*/
:root {
--primary-color: #2c3e50;
--secondary-color: #34495e;
--accent-color: #3498db;
--success-color: #2ecc71;
--warning-color: #f39c12;
--danger-color: #e74c3c;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* Navigation */
.navbar-brand {
font-weight: 600;
font-size: 1.3rem;
}
/* Cards */
.card {
border: none;
border-radius: 8px;
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
}
.card-header {
border-radius: 8px 8px 0 0 !important;
font-weight: 600;
}
/* Buttons */
.btn {
border-radius: 6px;
font-weight: 500;
transition: all 0.2s;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
/* Tables */
.table {
border-radius: 8px;
overflow: hidden;
}
.table thead th {
font-weight: 600;
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 0.5px;
}
/* Badges */
.badge {
padding: 0.4em 0.8em;
font-weight: 500;
border-radius: 4px;
}
/* Calendar Specific Styles */
#calendar {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* Vertical Calendar (timeGrid) Styles - very compact */
.fc-timegrid-event {
border-radius: 3px;
padding: 1px 3px !important;
cursor: move;
transition: transform 0.2s, box-shadow 0.2s;
border-width: 1px;
min-height: 18px;
}
.fc-timegrid-event:hover {
transform: translateX(2px);
box-shadow: 0 3px 8px rgba(0,0,0,0.2);
z-index: 100;
}
.fc-timegrid-event .fc-event-main {
padding: 0px !important;
}
.fc-timegrid-event .fc-event-main-frame {
padding: 1px 2px !important;
}
/* Timeline Calendar Styles */
.fc-timeline-event {
border-radius: 4px;
padding: 2px 4px;
cursor: move;
transition: transform 0.2s;
}
.fc-timeline-event:hover {
transform: scale(1.02);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* General Event Styles */
.fc-event {
border-radius: 4px;
cursor: move;
transition: all 0.2s;
}
.fc-event-title {
font-weight: 600;
line-height: 1.3;
}
.fc-event-time {
font-size: 0.85em;
opacity: 0.9;
}
/* Make events more readable in vertical view */
.fc-timegrid-event-harness {
margin-right: 2px;
}
.fc-timegrid-col-events {
margin: 0 2px;
}
/* Improve time slot visibility - 150px per 15 minutes */
.fc-timegrid-slot {
height: 20px !important;
}
.fc-timegrid-slot-label {
font-weight: 500;
font-size: 0.85em;
padding: 4px 6px;
line-height: 1.3;
}
/* Make slot lines visible */
.fc-timegrid-slot-minor {
border-top-style: solid;
border-top-width: 1px;
border-top-color: #f0f0f0;
}
.fc-timegrid-slot-major {
border-top-width: 2px;
border-top-color: #dee2e6;
}
/* Current time indicator */
.fc-timegrid-now-indicator-line {
border-color: #e74c3c;
border-width: 2px;
}
.fc-timegrid-now-indicator-arrow {
border-color: #e74c3c;
}
/* Block background events */
.fc-bg-event {
opacity: 1 !important;
border: 1px dashed rgba(0,0,0,0.1);
}
.fc-timegrid .fc-bg-event {
opacity: 0.8 !important;
}
/* Block info cards */
#blockInfoContainer .card {
transition: transform 0.2s;
}
#blockInfoContainer .card:hover {
transform: translateY(-2px);
}
/* Resource TimeGrid specific styles */
.fc-resource-timeline-divider,
.fc-resource-timegrid-divider {
width: 2px;
background: #dee2e6;
}
.fc-col-header-cell {
background: #f8f9fa;
font-weight: 600;
border-right: 2px solid #dee2e6 !important;
}
.fc-timegrid-col {
border-right: 2px solid #dee2e6 !important;
}
/* Make resource columns more distinct */
.fc-timegrid-col.fc-resource {
background: rgba(255, 255, 255, 0.5);
}
.fc-timegrid-col.fc-resource:nth-child(even) {
background: rgba(248, 249, 250, 0.5);
}
/* Resource header styling */
.fc-col-header-cell-cushion {
padding: 8px 4px;
font-size: 0.95em;
}
/* Improve event spacing in resource columns */
.fc-timegrid-event-harness {
margin-right: 3px;
}
/* Resource background events (blocks) */
.fc-timegrid-col .fc-bg-event {
opacity: 0.15 !important;
border-left: 3px solid rgba(0,0,0,0.2);
border-right: 3px solid rgba(0,0,0,0.2);
}
/* Commercial Sidebar */
.commercial-sidebar {
max-height: 600px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
}
.commercial-item {
background: white;
border-radius: 6px;
padding: 12px;
margin-bottom: 10px;
cursor: move;
border-left: 4px solid;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.commercial-item:hover {
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.commercial-item.dragging {
opacity: 0.5;
}
.commercial-title {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 4px;
}
.commercial-duration {
font-size: 0.85rem;
color: #6c757d;
}
.commercial-series {
font-size: 0.8rem;
font-weight: 500;
opacity: 0.8;
}
/* Block Time Editor */
.block-time-editor {
background: #fff3cd;
border: 2px dashed #ffc107;
border-radius: 6px;
padding: 10px;
margin-bottom: 15px;
}
.block-time-input {
font-weight: 600;
font-size: 1.1rem;
border: 2px solid #ffc107;
}
/* Status Indicators */
.status-pending {
background-color: #ffc107;
color: #000;
}
.status-synced {
background-color: #28a745;
color: #fff;
}
.status-error {
background-color: #dc3545;
color: #fff;
}
/* Loading Spinner */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.spinner-border-lg {
width: 3rem;
height: 3rem;
}
/* Responsive */
@media (max-width: 768px) {
.commercial-sidebar {
max-height: 300px;
margin-bottom: 20px;
}
.card {
margin-bottom: 15px;
}
}
/* Scrollbar Styling */
.commercial-sidebar::-webkit-scrollbar {
width: 8px;
}
.commercial-sidebar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.commercial-sidebar::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.commercial-sidebar::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Alert Animations */
@keyframes slideInDown {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.alert {
animation: slideInDown 0.3s ease-out;
}
/* Color Picker */
.color-preview {
width: 40px;
height: 40px;
border-radius: 6px;
border: 2px solid #dee2e6;
cursor: pointer;
transition: transform 0.2s;
}
.color-preview:hover {
transform: scale(1.1);
border-color: #adb5bd;
}
/* Dashboard Stats */
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.stat-card.success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.stat-card.warning {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-card.info {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-number {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Timeline View Enhancements */
.fc-timeline-event {
border-radius: 4px;
}
.fc-timeline-event .fc-event-main {
padding: 4px 8px;
}
/* Conflict Warning */
.conflict-warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 12px;
border-radius: 4px;
margin-bottom: 15px;
}
/* Success Message */
.success-message {
background: #d4edda;
border-left: 4px solid #28a745;
padding: 12px;
border-radius: 4px;
margin-bottom: 15px;
}
/* Form Enhancements */
.form-control:focus,
.form-select:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
}
/* Tooltip Styling */
.tooltip-inner {
background-color: var(--primary-color);
border-radius: 4px;
padding: 8px 12px;
}
/* Print Styles */
@media print {
.navbar,
.btn,
.commercial-sidebar {
display: none !important;
}
.card {
box-shadow: none !important;
border: 1px solid #dee2e6 !important;
}
}

685
assets/js/calendar-init.js Normal file
View File

@ -0,0 +1,685 @@
/**
* Calendar Initialization and Event Handlers
* FullCalendar.js configuration for TV planning
*/
document.addEventListener('DOMContentLoaded', function() {
const calendarEl = document.getElementById('calendar');
if (!calendarEl) {
console.error('Calendar element not found');
return;
}
// Initialize FullCalendar with resource columns
const calendar = new FullCalendar.Calendar(calendarEl, {
schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
initialView: 'resourceTimeGridDay',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'resourceTimeGridDay,resourceTimeGridWeek,resourceTimelineDay'
},
slotDuration: '00:15:00',
slotLabelInterval: '00:15:00',
snapDuration: '00:01:00',
slotMinTime: '00:00:00',
slotMaxTime: '24:00:00',
height: 'auto',
locale: 'nl',
allDaySlot: false,
nowIndicator: true,
resourceAreaHeaderContent: 'Zenders',
resourceAreaWidth: '120px',
resourceOrder: 'title',
// Resources (channels) - only SBS9 and NET5
resources: [
{
id: 'SBS9',
title: 'SBS9',
eventBackgroundColor: '#3498db',
eventBorderColor: '#2980b9'
},
{
id: 'NET5',
title: 'NET5',
eventBackgroundColor: '#e74c3c',
eventBorderColor: '#c0392b'
}
],
// Load events from API (including background blocks)
events: function(info, successCallback, failureCallback) {
const startDate = info.startStr.split('T')[0];
const endDate = info.endStr.split('T')[0];
// Load both transmissions and blocks
Promise.all([
fetch('api/get_transmissions.php?' + new URLSearchParams({
start: startDate,
end: endDate
})).then(r => r.json()),
fetch('api/get_daily_blocks.php?' + new URLSearchParams({
date: startDate
})).then(r => r.json())
])
.then(([transmissions, blocksData]) => {
// Combine transmissions with block background events
const allEvents = [
...transmissions,
...(blocksData.backgroundEvents || [])
];
successCallback(allEvents);
// Update block info display
updateBlockInfo(blocksData.blocks || []);
})
.catch(error => {
console.error('Error loading events:', error);
failureCallback(error);
});
},
// Event rendering - optimized for resource columns
eventContent: function(arg) {
const seriesCode = arg.event.extendedProps.series_code;
const title = arg.event.title;
const duration = arg.event.extendedProps.duration;
// Skip rendering for background events (blocks)
if (arg.event.display === 'background') {
return true; // Use default rendering
}
// For timeline view
if (arg.view.type.includes('timeline')) {
return {
html: `
<div class="fc-event-main-frame">
<div class="fc-event-time">${arg.timeText}</div>
<div class="fc-event-title-container">
<div class="fc-event-title">
${seriesCode ? '<strong>' + seriesCode + '</strong> - ' : ''}
${title}
</div>
<div class="fc-event-duration" style="font-size: 0.8em; opacity: 0.8;">
${duration}
</div>
</div>
</div>
`
};
}
// For vertical resource view - ultra compact single line
return {
html: `
<div class="fc-event-main-frame" style="padding: 1px 3px; line-height: 1.1;">
<div style="font-weight: 600; font-size: 0.75em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${seriesCode ? '<span style="background: rgba(0,0,0,0.15); padding: 0px 3px; border-radius: 2px; margin-right: 2px; font-size: 0.9em;">' + seriesCode + '</span>' : ''}
${title} <span style="opacity: 0.65; font-size: 0.95em; margin-left: 3px;">${duration}</span>
</div>
</div>
`
};
},
// Enable drag and drop
editable: true,
droppable: true,
// Event drop (move existing event)
eventDrop: function(info) {
const event = info.event;
const resourceId = event.getResources()[0]?.id || 'SBS9';
// Show loading indicator
showAlert('info', 'Valideren...');
fetch('api/update_transmission.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: event.id,
start_date: event.start.toISOString().split('T')[0],
start_time: event.start.toTimeString().split(' ')[0],
channel: resourceId
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
info.revert();
showAlert('error', data.error || 'Fout bij opslaan');
// Show validation details if available
if (data.validation && data.validation.blocks) {
console.warn('Validation failed:', data.validation);
}
} else {
showAlert('success', 'Uitzending verplaatst');
calendar.refetchEvents();
}
})
.catch(error => {
console.error('Error:', error);
info.revert();
showAlert('error', 'Netwerkfout bij opslaan');
});
},
// Event resize (change duration)
eventResize: function(info) {
showAlert('warning', 'Duur aanpassen is niet toegestaan. Gebruik de commercial instellingen.');
info.revert();
},
// External event drop (from sidebar) - DISABLED to prevent duplicates
// The eventReceive handler below will handle the creation
drop: null,
// Event receive (when external event is dropped)
eventReceive: function(info) {
const event = info.event;
const commercialId = event.extendedProps.commercial_id || info.draggedEl.dataset.commercialId;
const resourceId = event.getResources()[0]?.id || 'SBS9';
// Remove the temporary event
event.remove();
// Calculate next available start time
const dropDate = info.event.start.toISOString().split('T')[0];
const dropTime = info.event.start.toTimeString().split(' ')[0];
// Get next start time based on existing transmissions
fetch('api/get_next_start_time.php?' + new URLSearchParams({
date: dropDate,
channel: resourceId,
requested_time: dropTime
}))
.then(response => response.json())
.then(data => {
const startTime = data.next_start_time || dropTime;
// Show loading indicator
showAlert('info', 'Toevoegen...');
return fetch('api/create_transmission.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
commercial_id: commercialId,
channel: resourceId,
start_date: dropDate,
start_time: startTime
})
});
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('success', 'Uitzending toegevoegd');
calendar.refetchEvents();
} else {
showAlert('error', data.error || 'Fout bij toevoegen');
// Show validation details if available
if (data.validation && data.validation.blocks) {
console.warn('Validation failed:', data.validation);
// Show available blocks in alert
let blockInfo = 'Beschikbare blokken: ';
data.validation.blocks.forEach(block => {
const start = block.actual_start_time.substring(0, 5);
const end = block.actual_end_time ? block.actual_end_time.substring(0, 5) : '∞';
blockInfo += `${start}-${end}, `;
});
showAlert('warning', blockInfo);
}
}
})
.catch(error => {
console.error('Error:', error);
showAlert('error', 'Netwerkfout bij toevoegen');
});
},
// Event click (show details/edit)
eventClick: function(info) {
const event = info.event;
const props = event.extendedProps;
// Skip for background events (blocks)
if (event.display === 'background') {
return;
}
const resources = event.getResources();
const channelName = resources.length > 0 ? resources[0].title : 'Onbekend';
const modal = new bootstrap.Modal(document.getElementById('eventModal'));
document.getElementById('eventTitle').textContent = event.title;
document.getElementById('eventDetails').innerHTML = `
<p><strong>Zender:</strong> ${channelName}</p>
<p><strong>Datum:</strong> ${event.start.toLocaleDateString('nl-NL')}</p>
<p><strong>Starttijd:</strong> ${event.start.toLocaleTimeString('nl-NL', {hour: '2-digit', minute: '2-digit'})}</p>
<p><strong>Duur:</strong> ${props.duration}</p>
<p><strong>Series Code:</strong> ${props.series_code || '-'}</p>
<p><strong>Template:</strong> ${props.template}</p>
<p><strong>API Status:</strong> <span class="badge status-${props.api_status}">${props.api_status}</span></p>
`;
// Set delete button
document.getElementById('deleteEventBtn').onclick = function() {
if (confirm('Weet je zeker dat je deze uitzending wilt verwijderen?')) {
deleteEvent(event.id);
modal.hide();
}
};
// Set sync button
document.getElementById('syncEventBtn').onclick = function() {
syncEvent(event.id);
};
modal.show();
},
// Date click (show block time editor)
dateClick: function(info) {
if (info.resource) {
showBlockTimeEditor(info.dateStr.split('T')[0], info.resource.id);
}
}
});
calendar.render();
// Make external events draggable
initDraggableCommercials();
// Store calendar instance globally
window.tvCalendar = calendar;
});
/**
* Initialize draggable commercials from sidebar
*/
function initDraggableCommercials() {
const commercialItems = document.querySelectorAll('.commercial-item');
commercialItems.forEach(item => {
new FullCalendar.Draggable(item, {
eventData: function(eventEl) {
return {
title: eventEl.dataset.title,
duration: eventEl.dataset.duration,
backgroundColor: eventEl.dataset.color,
borderColor: eventEl.dataset.color,
extendedProps: {
commercial_id: eventEl.dataset.commercialId,
series_code: eventEl.dataset.seriesCode
}
};
}
});
});
}
/**
* Show block time editor
*/
function showBlockTimeEditor(date, channel) {
const modal = new bootstrap.Modal(document.getElementById('blockTimeModal'));
document.getElementById('blockDate').value = date;
document.getElementById('blockChannel').value = channel;
// Load current block time
fetch(`api/get_block_time.php?date=${date}&channel=${channel}`)
.then(response => response.json())
.then(data => {
if (data.start_time) {
document.getElementById('blockStartTime').value = data.start_time;
}
});
modal.show();
}
/**
* Save block time
*/
function saveBlockTime() {
const date = document.getElementById('blockDate').value;
const channel = document.getElementById('blockChannel').value;
const startTime = document.getElementById('blockStartTime').value;
const recalculate = document.getElementById('recalculateTransmissions').checked;
fetch('api/update_block_time.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
date: date,
channel: channel,
start_time: startTime,
recalculate: recalculate
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('success', 'Blok starttijd bijgewerkt');
window.tvCalendar.refetchEvents();
bootstrap.Modal.getInstance(document.getElementById('blockTimeModal')).hide();
} else {
showAlert('error', data.error || 'Fout bij opslaan');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('error', 'Netwerkfout bij opslaan');
});
}
/**
* Delete event
*/
function deleteEvent(eventId) {
fetch('api/delete_transmission.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: eventId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('success', 'Uitzending verwijderd');
window.tvCalendar.refetchEvents();
} else {
showAlert('error', data.error || 'Fout bij verwijderen');
}
})
.catch(error => {
console.error('Error:', error);
showAlert('error', 'Netwerkfout bij verwijderen');
});
}
/**
* Sync event to Talpa API
*/
function syncEvent(eventId) {
showAlert('info', 'Synchroniseren...');
// This would call your existing sync functionality
fetch('index.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `sync_item=1&sync_id=${eventId}`
})
.then(() => {
showAlert('success', 'Gesynchroniseerd met Talpa');
window.tvCalendar.refetchEvents();
})
.catch(error => {
console.error('Error:', error);
showAlert('error', 'Fout bij synchroniseren');
});
}
/**
* Show alert message
*/
function showAlert(type, message) {
const alertContainer = document.getElementById('alertContainer');
if (!alertContainer) return;
const alertClass = {
'success': 'alert-success',
'error': 'alert-danger',
'warning': 'alert-warning',
'info': 'alert-info'
}[type] || 'alert-info';
const alert = document.createElement('div');
alert.className = `alert ${alertClass} alert-dismissible fade show`;
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
alertContainer.appendChild(alert);
// Auto-dismiss after 5 seconds
setTimeout(() => {
alert.remove();
}, 5000);
}
/**
* Filter commercials in sidebar
*/
function filterCommercials(searchTerm) {
const items = document.querySelectorAll('.commercial-item');
const term = searchTerm.toLowerCase();
items.forEach(item => {
const title = item.dataset.title.toLowerCase();
const seriesCode = (item.dataset.seriesCode || '').toLowerCase();
if (title.includes(term) || seriesCode.includes(term)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
/**
* Update block info display
*/
function updateBlockInfo(blocks) {
const container = document.getElementById('blockInfoContainer');
if (!container) return;
if (blocks.length === 0) {
container.innerHTML = '<p class="text-muted small mb-0">Geen actieve blokken voor deze dag</p>';
return;
}
// Group blocks by channel
const blocksByChannel = {};
blocks.forEach(block => {
if (!blocksByChannel[block.channel]) {
blocksByChannel[block.channel] = [];
}
blocksByChannel[block.channel].push(block);
});
let html = '<div class="row g-2">';
for (const [channel, channelBlocks] of Object.entries(blocksByChannel)) {
const channelColors = {
'SBS9': 'primary',
'NET5': 'danger',
'VERONICA': 'secondary'
};
const color = channelColors[channel] || 'secondary';
html += `<div class="col-md-4">`;
html += `<div class="card border-${color}">`;
html += `<div class="card-header bg-${color} text-white py-1 px-2">`;
html += `<small class="fw-bold">${channel}</small>`;
html += `</div>`;
html += `<div class="card-body p-2">`;
channelBlocks.forEach(block => {
const startTime = block.actual_start_time.substring(0, 5);
const endTime = block.actual_end_time ? block.actual_end_time.substring(0, 5) : '∞';
html += `<div class="d-flex justify-content-between align-items-center mb-1">`;
html += `<small class="text-muted">${block.template_name || 'Blok'}</small>`;
html += `<small class="badge bg-light text-dark">${startTime} - ${endTime}</small>`;
html += `</div>`;
});
html += `</div></div></div>`;
}
html += '</div>';
container.innerHTML = html;
}
/**
* Adjust calendar zoom level
* Base: 10px per minute
*/
function adjustZoom(factor) {
const calendarEl = document.getElementById('calendar');
const currentZoom = parseFloat(calendarEl.dataset.zoom || '1.0');
let newZoom;
if (factor === 1.0) {
newZoom = 1.0; // Reset
} else {
newZoom = currentZoom * factor;
newZoom = Math.max(0.5, Math.min(3.0, newZoom)); // Limit between 50% and 300%
}
calendarEl.dataset.zoom = newZoom;
// Base: 10px per minute
const baseHeightPx = 10;
const newHeightPx = baseHeightPx * newZoom;
// Update all slot heights
const style = document.createElement('style');
style.id = 'zoom-style';
const existingStyle = document.getElementById('zoom-style');
if (existingStyle) {
existingStyle.remove();
}
style.textContent = `
.fc-timegrid-slot {
height: ${newHeightPx}px !important;
}
.fc-timegrid-event {
min-height: ${Math.max(12, 12 * newZoom)}px !important;
}
.fc-timegrid-event .fc-event-main-frame {
font-size: ${Math.max(0.65, 0.75 * newZoom)}em !important;
padding: ${Math.max(1, 2 * newZoom)}px ${Math.max(2, 3 * newZoom)}px !important;
}
.fc-timegrid-slot-label {
font-size: ${Math.max(0.6, 0.7 * newZoom)}em !important;
}
`;
document.head.appendChild(style);
// Update zoom level display
document.getElementById('zoomLevel').textContent = Math.round(newZoom * 100) + '%';
// Refresh calendar to apply changes
if (window.tvCalendar) {
window.tvCalendar.render();
}
}
/**
* Show sync block modal
*/
function showSyncBlockModal() {
const modal = new bootstrap.Modal(document.getElementById('syncBlockModal'));
// Set current date from calendar
if (window.tvCalendar) {
const currentDate = window.tvCalendar.getDate();
document.getElementById('syncBlockDate').value = currentDate.toISOString().split('T')[0];
}
modal.show();
}
/**
* Sync entire block to Talpa
*/
function syncBlock() {
const date = document.getElementById('syncBlockDate').value;
const channel = document.getElementById('syncBlockChannel').value;
if (!date || !channel) {
showAlert('error', 'Selecteer een datum en zender');
return;
}
// Show progress
document.getElementById('syncBlockProgress').classList.remove('d-none');
fetch('api/sync_block.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
date: date,
channel: channel
})
})
.then(response => response.json())
.then(data => {
// Hide progress
document.getElementById('syncBlockProgress').classList.add('d-none');
if (data.success) {
showAlert('success', data.message);
// Show detailed results if there were errors
if (data.errors && data.errors.length > 0) {
console.warn('Sync errors:', data.errors);
let errorMsg = 'Fouten bij synchronisatie:\n';
data.errors.forEach(err => {
errorMsg += `- ${err.time}: ${err.error}\n`;
});
showAlert('warning', errorMsg);
}
// Refresh calendar
if (window.tvCalendar) {
window.tvCalendar.refetchEvents();
}
// Close modal
bootstrap.Modal.getInstance(document.getElementById('syncBlockModal')).hide();
} else {
showAlert('error', data.error || 'Fout bij synchroniseren');
}
})
.catch(error => {
// Hide progress
document.getElementById('syncBlockProgress').classList.add('d-none');
console.error('Error:', error);
showAlert('error', 'Netwerkfout bij synchroniseren');
});
}
// Expose functions globally
window.saveBlockTime = saveBlockTime;
window.filterCommercials = filterCommercials;
window.updateBlockInfo = updateBlockInfo;
window.adjustZoom = adjustZoom;
window.showSyncBlockModal = showSyncBlockModal;
window.syncBlock = syncBlock;

319
blocks.php Normal file
View File

@ -0,0 +1,319 @@
<?php
/**
* Block Template Management
* Manage recurring time blocks for channels
*/
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/helpers.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();
$db = getDbConnection();
// Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['create_template'])) {
$stmt = $db->prepare("
INSERT INTO block_templates
(channel, name, day_of_week, default_start_time, default_end_time, is_active)
VALUES (?, ?, ?, ?, ?, 1)
");
$stmt->execute([
$_POST['channel'],
$_POST['name'],
$_POST['day_of_week'],
$_POST['default_start_time'],
$_POST['default_end_time'] ?: null
]);
header('Location: blocks.php?success=created');
exit;
}
if (isset($_POST['update_template'])) {
$stmt = $db->prepare("
UPDATE block_templates
SET channel = ?, name = ?, day_of_week = ?,
default_start_time = ?, default_end_time = ?
WHERE id = ?
");
$stmt->execute([
$_POST['channel'],
$_POST['name'],
$_POST['day_of_week'],
$_POST['default_start_time'],
$_POST['default_end_time'] ?: null,
$_POST['template_id']
]);
header('Location: blocks.php?success=updated');
exit;
}
if (isset($_POST['toggle_active'])) {
$stmt = $db->prepare("UPDATE block_templates SET is_active = NOT is_active WHERE id = ?");
$stmt->execute([$_POST['template_id']]);
header('Location: blocks.php');
exit;
}
if (isset($_POST['delete_template'])) {
$stmt = $db->prepare("DELETE FROM block_templates WHERE id = ?");
$stmt->execute([$_POST['template_id']]);
header('Location: blocks.php?success=deleted');
exit;
}
}
// Get all templates
$templates = $db->query("SELECT * FROM block_templates ORDER BY channel, day_of_week, default_start_time")->fetchAll();
// Get template for editing
$editTemplate = null;
if (isset($_GET['edit'])) {
$stmt = $db->prepare("SELECT * FROM block_templates WHERE id = ?");
$stmt->execute([$_GET['edit']]);
$editTemplate = $stmt->fetch();
}
$dayNames = [
'all' => 'Alle dagen',
'ma' => 'Maandag',
'di' => 'Dinsdag',
'wo' => 'Woensdag',
'do' => 'Donderdag',
'vr' => 'Vrijdag',
'za' => 'Zaterdag',
'zo' => 'Zondag'
];
?>
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blok Templates - Telvero Talpa</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<div class="navbar-nav">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="planner.php">Excel Planner</a>
<a class="nav-link" href="calendar.php">Kalender</a>
<a class="nav-link active" href="blocks.php">Blokken</a>
<a class="nav-link" href="commercials.php">Commercials</a>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-calendar3"></i> Blok Templates</h1>
<a href="calendar.php" class="btn btn-primary">
<i class="bi bi-calendar-week"></i> Naar Kalender
</a>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?php
$messages = [
'created' => 'Template succesvol aangemaakt!',
'updated' => 'Template succesvol bijgewerkt!',
'deleted' => 'Template succesvol verwijderd!'
];
echo $messages[$_GET['success']] ?? 'Actie succesvol uitgevoerd!';
?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="row">
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="bi bi-plus-circle"></i>
<?= $editTemplate ? 'Template Bewerken' : 'Nieuw Template' ?>
</h5>
</div>
<div class="card-body">
<form method="POST">
<?php if ($editTemplate): ?>
<input type="hidden" name="template_id" value="<?= $editTemplate['id'] ?>">
<?php endif; ?>
<div class="mb-3">
<label class="form-label">Zender</label>
<select name="channel" class="form-select" required>
<option value="">Selecteer zender...</option>
<option value="SBS9" <?= ($editTemplate && $editTemplate['channel'] == 'SBS9') ? 'selected' : '' ?>>SBS9</option>
<option value="NET5" <?= ($editTemplate && $editTemplate['channel'] == 'NET5') ? 'selected' : '' ?>>NET5</option>
<option value="VERONICA" <?= ($editTemplate && $editTemplate['channel'] == 'VERONICA') ? 'selected' : '' ?>>VERONICA</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Template Naam</label>
<input type="text" name="name" class="form-control"
value="<?= htmlspecialchars($editTemplate['name'] ?? '') ?>"
placeholder="bijv. SBS9 Dagblok" required>
</div>
<div class="mb-3">
<label class="form-label">Dag van de Week</label>
<select name="day_of_week" class="form-select" required>
<?php foreach ($dayNames as $value => $label): ?>
<option value="<?= $value ?>"
<?= ($editTemplate && $editTemplate['day_of_week'] == $value) ? 'selected' : '' ?>>
<?= $label ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Standaard Starttijd</label>
<input type="time" name="default_start_time" class="form-control"
value="<?= $editTemplate['default_start_time'] ?? '' ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Standaard Eindtijd (optioneel)</label>
<input type="time" name="default_end_time" class="form-control"
value="<?= $editTemplate['default_end_time'] ?? '' ?>">
<small class="text-muted">Laat leeg als niet van toepassing</small>
</div>
<div class="d-grid gap-2">
<button type="submit" name="<?= $editTemplate ? 'update_template' : 'create_template' ?>"
class="btn btn-primary">
<i class="bi bi-<?= $editTemplate ? 'check' : 'plus' ?>-circle"></i>
<?= $editTemplate ? 'Bijwerken' : 'Aanmaken' ?>
</button>
<?php if ($editTemplate): ?>
<a href="blocks.php" class="btn btn-secondary">Annuleren</a>
<?php endif; ?>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Bestaande Templates</h5>
</div>
<div class="card-body p-0">
<?php if (empty($templates)): ?>
<div class="p-4 text-center text-muted">
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
<p class="mt-2">Nog geen templates aangemaakt</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Zender</th>
<th>Naam</th>
<th>Dag</th>
<th>Starttijd</th>
<th>Eindtijd</th>
<th>Status</th>
<th>Acties</th>
</tr>
</thead>
<tbody>
<?php foreach ($templates as $template): ?>
<tr class="<?= $template['is_active'] ? '' : 'table-secondary' ?>">
<td>
<span class="badge bg-info"><?= htmlspecialchars($template['channel']) ?></span>
</td>
<td><?= htmlspecialchars($template['name']) ?></td>
<td><?= $dayNames[$template['day_of_week']] ?></td>
<td><?= substr($template['default_start_time'], 0, 5) ?></td>
<td><?= $template['default_end_time'] ? substr($template['default_end_time'], 0, 5) : '-' ?></td>
<td>
<?php if ($template['is_active']): ?>
<span class="badge bg-success">Actief</span>
<?php else: ?>
<span class="badge bg-secondary">Inactief</span>
<?php endif; ?>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="?edit=<?= $template['id'] ?>"
class="btn btn-outline-primary" title="Bewerken">
<i class="bi bi-pencil"></i>
</a>
<form method="POST" style="display:inline;">
<input type="hidden" name="template_id" value="<?= $template['id'] ?>">
<button type="submit" name="toggle_active"
class="btn btn-outline-<?= $template['is_active'] ? 'warning' : 'success' ?>"
title="<?= $template['is_active'] ? 'Deactiveren' : 'Activeren' ?>">
<i class="bi bi-<?= $template['is_active'] ? 'pause' : 'play' ?>-circle"></i>
</button>
</form>
<form method="POST" style="display:inline;"
onsubmit="return confirm('Weet je zeker dat je dit template wilt verwijderen?');">
<input type="hidden" name="template_id" value="<?= $template['id'] ?>">
<button type="submit" name="delete_template"
class="btn btn-outline-danger" title="Verwijderen">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<div class="card shadow-sm mt-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Uitleg</h5>
</div>
<div class="card-body">
<h6>Wat zijn Blok Templates?</h6>
<p>Blok templates definiëren terugkerende tijdslots voor uitzendingen. Bijvoorbeeld:</p>
<ul>
<li><strong>SBS9 Dagblok:</strong> Ma-Zo 07:00-15:00</li>
<li><strong>SBS9 Nachtblok:</strong> Ma-Zo 23:30-02:00</li>
<li><strong>Net5 Ochtend:</strong> Ma-Vr 07:30-11:30</li>
</ul>
<h6 class="mt-3">Hoe werkt het?</h6>
<ol>
<li>Maak templates aan voor vaste blokken</li>
<li>Het systeem genereert automatisch dagelijkse blokken</li>
<li>In de kalender kun je per dag de starttijd aanpassen</li>
<li>Uitzendingen worden automatisch in deze blokken gepland</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

355
calendar.php Normal file
View File

@ -0,0 +1,355 @@
<?php
/**
* Calendar Planning View
* Main interface for drag-and-drop TV scheduling
*/
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/helpers.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();
$db = getDbConnection();
// Get all commercials with colors for sidebar
$commercials = $db->query("
SELECT id, title, duration, color_code, series_code, upload_status
FROM commercials
WHERE upload_status = 'uploaded'
ORDER BY title ASC
")->fetchAll();
// Auto-assign colors to commercials without colors
foreach ($commercials as $commercial) {
if (empty($commercial['color_code'])) {
$stmt = $db->query("SELECT color_code FROM commercials WHERE color_code IS NOT NULL");
$existingColors = $stmt->fetchAll(PDO::FETCH_COLUMN);
$newColor = generateDistinctColor($existingColors);
$stmt = $db->prepare("UPDATE commercials SET color_code = ? WHERE id = ?");
$stmt->execute([$newColor, $commercial['id']]);
}
}
// Refresh commercials after color assignment
$commercials = $db->query("
SELECT id, title, duration, color_code, series_code, upload_status
FROM commercials
WHERE upload_status = 'uploaded'
ORDER BY title ASC
")->fetchAll();
?>
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kalender Planning - Telvero Talpa</title>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<!-- FullCalendar -->
<link href='https://cdn.jsdelivr.net/npm/fullcalendar-scheduler@6.1.10/index.global.min.css' rel='stylesheet' />
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<div class="navbar-nav">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="planner.php">Excel Planner</a>
<a class="nav-link active" href="calendar.php">Kalender</a>
<a class="nav-link" href="blocks.php">Blokken</a>
<a class="nav-link" href="commercials.php">Commercials</a>
</div>
</div>
</nav>
<div class="container-fluid mt-4">
<!-- Alert Container -->
<div id="alertContainer"></div>
<!-- Block Info Display & Zoom Controls -->
<div class="row mb-3">
<div class="col-md-9">
<div class="card shadow-sm">
<div class="card-header bg-info text-white py-2 d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="bi bi-calendar3"></i> Actieve Blokken voor Vandaag
</h6>
<button type="button" class="btn btn-sm btn-light" onclick="showSyncBlockModal()">
<i class="bi bi-cloud-upload"></i> Sync Blok naar Talpa
</button>
</div>
<div class="card-body p-2" id="blockInfoContainer">
<p class="text-muted small mb-0">Laden...</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-header bg-secondary text-white py-2">
<h6 class="mb-0">
<i class="bi bi-zoom-in"></i> Zoom
</h6>
</div>
<div class="card-body p-2">
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="adjustZoom(0.8)">
<i class="bi bi-dash-circle"></i> -
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="adjustZoom(1.0)" title="Reset">
<i class="bi bi-arrow-clockwise"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="adjustZoom(1.2)">
<i class="bi bi-plus-circle"></i> +
</button>
</div>
<div class="mt-2 text-center">
<small class="text-muted">Huidige: <span id="zoomLevel">100%</span></small>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Sidebar with draggable commercials -->
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="bi bi-collection-play"></i> Beschikbare Commercials
</h5>
</div>
<div class="card-body p-2">
<!-- Search -->
<div class="mb-3">
<input type="text"
class="form-control form-control-sm"
placeholder="Zoeken..."
onkeyup="filterCommercials(this.value)">
</div>
<!-- Commercial List -->
<div class="commercial-sidebar">
<?php if (empty($commercials)): ?>
<div class="text-center text-muted p-3">
<i class="bi bi-inbox" style="font-size: 2rem;"></i>
<p class="mt-2 small">Geen commercials beschikbaar</p>
<a href="commercials.php" class="btn btn-sm btn-primary">
Voeg Commercial Toe
</a>
</div>
<?php else: ?>
<?php foreach ($commercials as $commercial): ?>
<div class="commercial-item"
style="border-left-color: <?= htmlspecialchars($commercial['color_code']) ?>;"
data-commercial-id="<?= $commercial['id'] ?>"
data-title="<?= htmlspecialchars($commercial['title']) ?>"
data-duration="<?= $commercial['duration'] ?>"
data-color="<?= htmlspecialchars($commercial['color_code']) ?>"
data-series-code="<?= htmlspecialchars($commercial['series_code'] ?? '') ?>">
<div class="d-flex align-items-center">
<div class="color-preview me-2"
style="background-color: <?= htmlspecialchars($commercial['color_code']) ?>; width: 20px; height: 20px; border-radius: 3px;">
</div>
<div class="flex-grow-1">
<div class="commercial-title">
<?= htmlspecialchars($commercial['title']) ?>
</div>
<div class="commercial-duration">
<i class="bi bi-clock"></i> <?= $commercial['duration'] ?>
</div>
<?php if ($commercial['series_code']): ?>
<div class="commercial-series">
<i class="bi bi-tag"></i> <?= htmlspecialchars($commercial['series_code']) ?>
</div>
<?php endif; ?>
</div>
<div>
<i class="bi bi-grip-vertical text-muted"></i>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<div class="mt-3 p-2 bg-light rounded">
<small class="text-muted">
<i class="bi bi-info-circle"></i>
Sleep commercials naar de kalender om te plannen
</small>
</div>
</div>
</div>
<!-- Legend -->
<div class="card shadow-sm mt-3">
<div class="card-header bg-secondary text-white">
<h6 class="mb-0"><i class="bi bi-palette"></i> Legenda</h6>
</div>
<div class="card-body p-2">
<div class="small">
<?php
$uniqueColors = [];
foreach ($commercials as $commercial) {
if ($commercial['series_code'] && !isset($uniqueColors[$commercial['series_code']])) {
$uniqueColors[$commercial['series_code']] = $commercial['color_code'];
}
}
?>
<?php foreach ($uniqueColors as $code => $color): ?>
<div class="d-flex align-items-center mb-2">
<div style="width: 20px; height: 20px; background-color: <?= $color ?>; border-radius: 3px; margin-right: 8px;"></div>
<span><?= htmlspecialchars($code) ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<!-- Calendar -->
<div class="col-md-9">
<div class="card shadow-sm">
<div class="card-body">
<div id="calendar"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Event Details Modal -->
<div class="modal fade" id="eventModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="eventTitle">Uitzending Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="eventDetails">
<!-- Details will be populated by JavaScript -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" id="deleteEventBtn">
<i class="bi bi-trash"></i> Verwijderen
</button>
<button type="button" class="btn btn-success" id="syncEventBtn">
<i class="bi bi-cloud-upload"></i> Sync naar Talpa
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Sluiten</button>
</div>
</div>
</div>
</div>
<!-- Block Time Editor Modal -->
<div class="modal fade" id="blockTimeModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Blok Starttijd Aanpassen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="blockDate">
<input type="hidden" id="blockChannel">
<div class="mb-3">
<label class="form-label">Nieuwe Starttijd</label>
<input type="time" class="form-control" id="blockStartTime">
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="recalculateTransmissions" checked>
<label class="form-check-label" for="recalculateTransmissions">
Herbereken alle uitzendingen in dit blok
</label>
</div>
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle"></i>
Als je "herbereken" aanvinkt, worden alle uitzendingen in dit blok opnieuw getimed vanaf de nieuwe starttijd.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
<button type="button" class="btn btn-primary" onclick="saveBlockTime()">
<i class="bi bi-check-circle"></i> Opslaan
</button>
</div>
</div>
</div>
</div>
<!-- Sync Block Modal -->
<div class="modal fade" id="syncBlockModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Blok Synchroniseren naar Talpa</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Datum</label>
<input type="date" class="form-control" id="syncBlockDate" value="<?= date('Y-m-d') ?>">
</div>
<div class="mb-3">
<label class="form-label">Zender</label>
<select class="form-select" id="syncBlockChannel">
<option value="SBS9">SBS9</option>
<option value="NET5">NET5</option>
<option value="VERONICA">VERONICA</option>
</select>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
Dit synchroniseert alle uitzendingen in het geselecteerde blok naar Talpa.
Alleen uitzendingen die nog niet gesynchroniseerd zijn worden verzonden.
</div>
<div id="syncBlockProgress" class="d-none">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 100%"></div>
</div>
<p class="text-center mt-2 mb-0">Synchroniseren...</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
<button type="button" class="btn btn-primary" onclick="syncBlock()">
<i class="bi bi-cloud-upload"></i> Synchroniseren
</button>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<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/fullcalendar-scheduler@6.1.10/index.global.min.js'></script>
<script src="assets/js/calendar-init.js"></script>
</body>
</html>

437
commercials.php Normal file
View File

@ -0,0 +1,437 @@
<?php
/**
* Commercial Management
* Enhanced version of commercial registration with color management
*/
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/TalpaAPI.php';
require_once __DIR__ . '/helpers.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();
$api = new TalpaApi();
$db = getDbConnection();
$apiLogs = [];
// Handle commercial registration
if (isset($_POST['add_commercial'])) {
$apiLogs[] = ['step' => 'Start registration', 'input' => $_POST];
$ep = $api->createEpisode($_POST['title'], $_POST['duration'], $_POST['season_id']);
$apiLogs[] = ['call' => 'Create Episode', 'request' => [
'title' => $_POST['title'],
'duration' => $_POST['duration'],
'season_id' => $_POST['season_id']
], 'response' => $api->lastResponse];
if (isset($ep['id'])) {
$apiLogs[] = ['step' => 'Episode created', 'episode_id' => $ep['id']];
$asset = $api->createMediaAsset($ep['id']);
$apiLogs[] = ['call' => 'Create Media Asset', 'request' => [
'content_id' => $ep['id']
], 'response' => $api->lastResponse];
if (isset($asset['id'])) {
$apiLogs[] = ['step' => 'Media asset created', 'asset_id' => $asset['id']];
$details = $api->getMediaAssetDetails($asset['id']);
$apiLogs[] = ['call' => 'Get Media Asset Details', 'request' => [
'asset_id' => $asset['id']
], 'response' => $api->lastResponse];
$label = $details['mediaAssetLabel'] ?? 'Pending';
$apiLogs[] = ['step' => 'Media asset label', 'label' => $label];
// Auto-generate color
$stmt = $db->query("SELECT color_code FROM commercials WHERE color_code IS NOT NULL");
$existingColors = $stmt->fetchAll(PDO::FETCH_COLUMN);
$colorCode = generateDistinctColor($existingColors);
$apiLogs[] = ['step' => 'Color generated', 'color' => $colorCode];
$stmt = $db->prepare("
INSERT INTO commercials
(title, duration, season_id, content_id, media_asset_id, media_asset_label, upload_status, color_code, series_code)
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)
");
$stmt->execute([
$_POST['title'],
$_POST['duration'],
$_POST['season_id'],
$ep['id'],
$asset['id'],
$label,
$colorCode,
$_POST['series_code'] ?? null
]);
$apiLogs[] = ['step' => 'Database insert', 'success' => true];
header('Location: commercials.php?success=created');
exit;
} else {
$apiLogs[] = ['step' => 'Media asset creation failed', 'response' => $asset];
}
} else {
$apiLogs[] = ['step' => 'Episode creation failed', 'response' => $ep];
}
}
// Handle media asset update
if (isset($_POST['update_media_asset'])) {
$stmt = $db->prepare("
UPDATE commercials
SET media_asset_label = ?, upload_status = ?, series_code = ?, color_code = ?
WHERE id = ?
");
$stmt->execute([
$_POST['media_asset_label'],
$_POST['upload_status'],
$_POST['series_code'] ?? null,
$_POST['color_code'],
$_POST['commercial_id']
]);
header('Location: commercials.php?success=updated');
exit;
}
// Handle delete
if (isset($_POST['delete_commercial'])) {
// Check if commercial is used in transmissions
$stmt = $db->prepare("SELECT COUNT(*) FROM transmissions WHERE commercial_id = ?");
$stmt->execute([$_POST['commercial_id']]);
$count = $stmt->fetchColumn();
if ($count > 0) {
header('Location: commercials.php?error=in_use');
exit;
}
// Get commercial details before deletion
$stmt = $db->prepare("SELECT content_id, media_asset_id FROM commercials WHERE id = ?");
$stmt->execute([$_POST['commercial_id']]);
$commercial = $stmt->fetch();
// Delete from Talpa API if content_id exists
if ($commercial && $commercial['content_id']) {
try {
$api->deleteEpisode($commercial['content_id']);
$apiLogs[] = ['call' => 'Delete Episode', 'response' => $api->lastResponse];
} catch (Exception $e) {
// Log error but continue with local deletion
error_log("Failed to delete episode from Talpa: " . $e->getMessage());
}
}
// Delete from local database
$stmt = $db->prepare("DELETE FROM commercials WHERE id = ?");
$stmt->execute([$_POST['commercial_id']]);
header('Location: commercials.php?success=deleted');
exit;
}
// Get all commercials
$commercials = $db->query("
SELECT c.*,
(SELECT COUNT(*) FROM transmissions WHERE commercial_id = c.id) as usage_count
FROM commercials c
ORDER BY c.created_at DESC
")->fetchAll();
?>
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Commercials - Telvero Talpa</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<div class="navbar-nav">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="planner.php">Excel Planner</a>
<a class="nav-link" href="calendar.php">Kalender</a>
<a class="nav-link" href="blocks.php">Blokken</a>
<a class="nav-link active" href="commercials.php">Commercials</a>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-collection-play"></i> Commercial Management</h1>
<a href="calendar.php" class="btn btn-primary">
<i class="bi bi-calendar-week"></i> Naar Kalender
</a>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?php
$messages = [
'created' => 'Commercial succesvol aangemaakt en geregistreerd bij Talpa!',
'updated' => 'Commercial succesvol bijgewerkt!',
'deleted' => 'Commercial succesvol verwijderd!'
];
echo $messages[$_GET['success']] ?? 'Actie succesvol uitgevoerd!';
?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (isset($_GET['error'])): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?php
$messages = [
'in_use' => 'Deze commercial kan niet verwijderd worden omdat deze nog in gebruik is in de planning!'
];
echo $messages[$_GET['error']] ?? 'Er is een fout opgetreden!';
?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="row">
<!-- Registration Form -->
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="bi bi-plus-circle"></i> Nieuwe Commercial Registreren
</h5>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label class="form-label">Product Naam</label>
<input type="text" name="title" class="form-control"
placeholder="bijv. Clever Cane" required>
</div>
<div class="mb-3">
<label class="form-label">Duur (HH:MM:SS)</label>
<input type="text" name="duration" class="form-control"
placeholder="00:30:00" pattern="[0-9]{2}:[0-9]{2}:[0-9]{2}" required>
<small class="text-muted">Formaat: UU:MM:SS</small>
</div>
<div class="mb-3">
<label class="form-label">Series Code (optioneel)</label>
<input type="text" name="series_code" class="form-control"
placeholder="bijv. 006a">
<small class="text-muted">Voor groepering in kalender</small>
</div>
<input type="hidden" name="season_id" value="<?= $_ENV['TV_SEASON_ID'] ?>">
<div class="d-grid">
<button type="submit" name="add_commercial" class="btn btn-primary">
<i class="bi bi-cloud-upload"></i> Registreren bij Talpa
</button>
</div>
</form>
<div class="mt-3 p-2 bg-light rounded">
<small class="text-muted">
<i class="bi bi-info-circle"></i>
Dit registreert de commercial bij Talpa en maakt automatisch een media asset aan.
</small>
</div>
</div>
</div>
</div>
<!-- Commercial List -->
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Geregistreerde Commercials</h5>
</div>
<div class="card-body p-0">
<?php if (empty($commercials)): ?>
<div class="p-4 text-center text-muted">
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
<p class="mt-2">Nog geen commercials geregistreerd</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Kleur</th>
<th>Titel</th>
<th>Duur</th>
<th>Series</th>
<th>Label</th>
<th>Status</th>
<th>Gebruik</th>
<th>Acties</th>
</tr>
</thead>
<tbody>
<?php foreach ($commercials as $c): ?>
<tr>
<td>
<div class="color-preview"
style="background-color: <?= htmlspecialchars($c['color_code'] ?? '#cccccc') ?>;"
title="<?= htmlspecialchars($c['color_code'] ?? '#cccccc') ?>">
</div>
</td>
<td>
<strong><?= htmlspecialchars($c['title']) ?></strong>
<br>
<small class="text-muted">ID: <?= htmlspecialchars($c['content_id']) ?></small>
</td>
<td>
<span class="badge bg-info"><?= $c['duration'] ?></span>
</td>
<td>
<?= $c['series_code'] ? '<span class="badge bg-secondary">' . htmlspecialchars($c['series_code']) . '</span>' : '-' ?>
</td>
<td>
<code class="small"><?= htmlspecialchars($c['media_asset_label']) ?></code>
</td>
<td>
<?php if ($c['upload_status'] == 'uploaded'): ?>
<span class="badge bg-success">Uploaded</span>
<?php else: ?>
<span class="badge bg-warning text-dark">Pending</span>
<?php endif; ?>
</td>
<td>
<span class="badge bg-<?= $c['usage_count'] > 0 ? 'primary' : 'secondary' ?>">
<?= $c['usage_count'] ?>x
</span>
</td>
<td>
<div class="btn-group btn-group-sm">
<button type="button"
class="btn btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#editModal<?= $c['id'] ?>"
title="Bewerken">
<i class="bi bi-pencil"></i>
</button>
<?php if ($c['usage_count'] == 0): ?>
<form method="POST" style="display:inline;"
onsubmit="return confirm('Weet je zeker dat je deze commercial wilt verwijderen?');">
<input type="hidden" name="commercial_id" value="<?= $c['id'] ?>">
<button type="submit" name="delete_commercial"
class="btn btn-outline-danger" title="Verwijderen">
<i class="bi bi-trash"></i>
</button>
</form>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- Edit Modals (outside the table loop) -->
<?php foreach ($commercials as $c): ?>
<div class="modal fade" id="editModal<?= $c['id'] ?>" tabindex="-1" aria-labelledby="editModalLabel<?= $c['id'] ?>" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST">
<input type="hidden" name="commercial_id" value="<?= $c['id'] ?>">
<div class="modal-header">
<h5 class="modal-title">Bewerk: <?= htmlspecialchars($c['title']) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Media Asset Label</label>
<input type="text" name="media_asset_label" class="form-control"
value="<?= htmlspecialchars($c['media_asset_label']) ?>">
</div>
<div class="mb-3">
<label class="form-label">Upload Status</label>
<select name="upload_status" class="form-select">
<option value="pending" <?= $c['upload_status'] == 'pending' ? 'selected' : '' ?>>Pending</option>
<option value="uploaded" <?= $c['upload_status'] == 'uploaded' ? 'selected' : '' ?>>Uploaded</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Series Code</label>
<input type="text" name="series_code" class="form-control"
value="<?= htmlspecialchars($c['series_code'] ?? '') ?>"
placeholder="bijv. 006a">
</div>
<div class="mb-3">
<label class="form-label">Kleurcode</label>
<input type="color" name="color_code" class="form-control form-control-color"
value="<?= htmlspecialchars($c['color_code'] ?? '#cccccc') ?>">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
<button type="submit" name="update_media_asset" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Opslaan
</button>
</div>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// API Logs to console with enhanced debugging
const apiLogs = <?= json_encode($apiLogs) ?>;
if (apiLogs.length > 0) {
console.group("🔍 Talpa API Debug Logs - Commercial Registration");
apiLogs.forEach((log, index) => {
if (log.call) {
console.group(`${index + 1}. ${log.call}`);
if (log.request) {
console.log("%cRequest:", "color: #3498db; font-weight: bold;", log.request);
}
console.log("%cResponse:", "color: #2ecc71; font-weight: bold;", log.response);
console.groupEnd();
} else if (log.step) {
console.log(`%c${log.step}`, "color: #9b59b6; font-weight: bold;", log);
}
});
console.groupEnd();
// Show summary
const hasErrors = apiLogs.some(log =>
(log.step && log.step.includes('failed')) ||
(log.response && log.response.error)
);
if (hasErrors) {
console.warn("⚠️ Er zijn fouten opgetreden tijdens de registratie. Zie bovenstaande logs voor details.");
} else {
console.log("✅ Commercial registratie succesvol voltooid");
}
}
</script>
</body>
</html>

387
helpers.php Normal file
View File

@ -0,0 +1,387 @@
<?php
/**
* Helper Functions for Telvero Talpa Planning System
* Contains utility functions for color generation, time calculations, and data formatting
*/
/**
* Generate a distinct color that doesn't exist in the provided array
* @param array $existingColors Array of existing hex color codes
* @return string Hex color code
*/
function generateDistinctColor($existingColors = []) {
// Predefined palette with good contrast and visibility
$colorPalette = [
'#3498db', // Blue
'#e74c3c', // Red
'#9b59b6', // Purple
'#2ecc71', // Green
'#f39c12', // Orange
'#1abc9c', // Turquoise
'#e67e22', // Dark Orange
'#34495e', // Dark Gray
'#16a085', // Sea Green
'#c0392b', // Dark Red
'#8e44ad', // Dark Purple
'#27ae60', // Dark Green
'#d35400', // Pumpkin
'#2980b9', // Belize Blue
'#8e44ad', // Wisteria
'#16a085', // Green Sea
'#c0392b', // Pomegranate
'#f39c12', // Sun Flower
];
// Find first color not in existing colors
foreach ($colorPalette as $color) {
if (!in_array($color, $existingColors)) {
return $color;
}
}
// If all predefined colors are used, generate random distinct color
return generateRandomColor($existingColors);
}
/**
* Generate a random color with good saturation and lightness
* @param array $existingColors Array of existing colors to avoid
* @return string Hex color code
*/
function generateRandomColor($existingColors = []) {
$maxAttempts = 50;
$attempt = 0;
while ($attempt < $maxAttempts) {
$hue = rand(0, 360);
$saturation = rand(60, 90); // Good saturation
$lightness = rand(45, 65); // Good visibility
$color = hslToHex($hue, $saturation, $lightness);
// Check if color is distinct enough from existing colors
if (isColorDistinct($color, $existingColors)) {
return $color;
}
$attempt++;
}
// Fallback to a random color
return sprintf('#%06X', mt_rand(0, 0xFFFFFF));
}
/**
* Check if a color is distinct from existing colors
* @param string $newColor Hex color code
* @param array $existingColors Array of existing hex color codes
* @return bool True if distinct enough
*/
function isColorDistinct($newColor, $existingColors, $threshold = 50) {
if (empty($existingColors)) {
return true;
}
list($r1, $g1, $b1) = sscanf($newColor, "#%02x%02x%02x");
foreach ($existingColors as $existingColor) {
list($r2, $g2, $b2) = sscanf($existingColor, "#%02x%02x%02x");
// Calculate color distance (Euclidean distance in RGB space)
$distance = sqrt(
pow($r1 - $r2, 2) +
pow($g1 - $g2, 2) +
pow($b1 - $b2, 2)
);
if ($distance < $threshold) {
return false;
}
}
return true;
}
/**
* Convert HSL to Hex color
* @param int $h Hue (0-360)
* @param int $s Saturation (0-100)
* @param int $l Lightness (0-100)
* @return string Hex color code
*/
function hslToHex($h, $s, $l) {
$h /= 360;
$s /= 100;
$l /= 100;
if ($s == 0) {
$r = $g = $b = $l;
} else {
$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
$p = 2 * $l - $q;
$r = hueToRgb($p, $q, $h + 1/3);
$g = hueToRgb($p, $q, $h);
$b = hueToRgb($p, $q, $h - 1/3);
}
return sprintf("#%02x%02x%02x", round($r * 255), round($g * 255), round($b * 255));
}
/**
* Helper function for HSL to RGB conversion
*/
function hueToRgb($p, $q, $t) {
if ($t < 0) $t += 1;
if ($t > 1) $t -= 1;
if ($t < 1/6) return $p + ($q - $p) * 6 * $t;
if ($t < 1/2) return $q;
if ($t < 2/3) return $p + ($q - $p) * (2/3 - $t) * 6;
return $p;
}
/**
* Calculate the next start time based on previous transmissions in a block
* @param PDO $db Database connection
* @param string $blockDate Date in Y-m-d format
* @param string $channel Channel name
* @param string $blockStartTime Optional block start time override
* @return string Time in H:i:s format
*/
function calculateNextStartTime($db, $blockDate, $channel, $blockStartTime = null) {
// Get all transmissions for this block, ordered by time
$stmt = $db->prepare("
SELECT start_time, duration
FROM transmissions
WHERE start_date = ? AND channel = ?
ORDER BY start_time ASC
");
$stmt->execute([$blockDate, $channel]);
$transmissions = $stmt->fetchAll(PDO::FETCH_ASSOC);
// If no transmissions, return block start time
if (empty($transmissions)) {
if ($blockStartTime) {
return $blockStartTime;
}
// Get block start time from daily_blocks or template
$stmt = $db->prepare("
SELECT actual_start_time
FROM daily_blocks
WHERE block_date = ? AND channel = ?
LIMIT 1
");
$stmt->execute([$blockDate, $channel]);
$time = $stmt->fetchColumn();
return $time ?: '07:00:00'; // Default fallback
}
// Calculate end time of last transmission
$lastTx = end($transmissions);
$startTime = new DateTime($lastTx['start_time']);
// Add duration to get end time
list($h, $m, $s) = explode(':', $lastTx['duration']);
$interval = new DateInterval("PT{$h}H{$m}M{$s}S");
$startTime->add($interval);
return $startTime->format('H:i:s');
}
/**
* Convert time string to seconds
* @param string $time Time in HH:MM:SS format
* @return int Seconds
*/
function timeToSeconds($time) {
list($h, $m, $s) = explode(':', $time);
return ($h * 3600) + ($m * 60) + $s;
}
/**
* Convert seconds to time string
* @param int $seconds Seconds
* @return string Time in HH:MM:SS format
*/
function secondsToTime($seconds) {
$hours = floor($seconds / 3600);
$minutes = floor(($seconds % 3600) / 60);
$secs = $seconds % 60;
return sprintf('%02d:%02d:%02d', $hours, $minutes, $secs);
}
/**
* Add time duration to a time string
* @param string $startTime Time in HH:MM:SS format
* @param string $duration Duration in HH:MM:SS format
* @return string Resulting time in HH:MM:SS format
*/
function addTimeToTime($startTime, $duration) {
$startSeconds = timeToSeconds($startTime);
$durationSeconds = timeToSeconds($duration);
$totalSeconds = $startSeconds + $durationSeconds;
return secondsToTime($totalSeconds);
}
/**
* Check if two time ranges overlap
* @param string $start1 Start time 1
* @param string $end1 End time 1
* @param string $start2 Start time 2
* @param string $end2 End time 2
* @return bool True if overlap exists
*/
function timeRangesOverlap($start1, $end1, $start2, $end2) {
$s1 = timeToSeconds($start1);
$e1 = timeToSeconds($end1);
$s2 = timeToSeconds($start2);
$e2 = timeToSeconds($end2);
return ($s1 < $e2) && ($e1 > $s2);
}
/**
* Get or create daily blocks for a specific date range
* @param PDO $db Database connection
* @param string $startDate Start date
* @param string $endDate End date
* @return array Array of daily blocks
*/
function ensureDailyBlocks($db, $startDate, $endDate) {
$start = new DateTime($startDate);
$end = new DateTime($endDate);
$interval = new DateInterval('P1D');
$period = new DatePeriod($start, $interval, $end->modify('+1 day'));
$dayMap = [
'Monday' => 'ma',
'Tuesday' => 'di',
'Wednesday' => 'wo',
'Thursday' => 'do',
'Friday' => 'vr',
'Saturday' => 'za',
'Sunday' => 'zo'
];
foreach ($period as $date) {
$dateStr = $date->format('Y-m-d');
$dayOfWeek = $dayMap[$date->format('l')];
// Get active templates for this day
$stmt = $db->prepare("
SELECT * FROM block_templates
WHERE is_active = 1
AND (day_of_week = ? OR day_of_week = 'all')
");
$stmt->execute([$dayOfWeek]);
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($templates as $template) {
// Check if daily block already exists for this template
$stmt = $db->prepare("
SELECT id FROM daily_blocks
WHERE block_date = ? AND channel = ? AND template_id = ?
");
$stmt->execute([
$dateStr,
$template['channel'],
$template['id']
]);
if (!$stmt->fetch()) {
// Create daily block from template
$stmt = $db->prepare("
INSERT IGNORE INTO daily_blocks
(template_id, channel, block_date, actual_start_time, actual_end_time)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([
$template['id'],
$template['channel'],
$dateStr,
$template['default_start_time'],
$template['default_end_time']
]);
}
}
}
}
/**
* Format date for display in Dutch
* @param string $date Date string
* @return string Formatted date
*/
function formatDateDutch($date) {
$dt = new DateTime($date);
$dayNames = ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'];
$monthNames = [
'', 'januari', 'februari', 'maart', 'april', 'mei', 'juni',
'juli', 'augustus', 'september', 'oktober', 'november', 'december'
];
$dayOfWeek = $dayNames[$dt->format('w')];
$day = $dt->format('j');
$month = $monthNames[(int)$dt->format('n')];
$year = $dt->format('Y');
return ucfirst($dayOfWeek) . ' ' . $day . ' ' . $month . ' ' . $year;
}
/**
* Get database connection
* @return PDO Database connection
*/
function getDbConnection() {
static $db = null;
if ($db === null) {
$db = new PDO(
"mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']};charset=utf8mb4",
$_ENV['DB_USER'],
$_ENV['DB_PASS'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
}
return $db;
}
/**
* Send JSON response
* @param mixed $data Data to send
* @param int $statusCode HTTP status code
*/
function jsonResponse($data, $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
/**
* Validate time format (HH:MM:SS or HH:MM)
* @param string $time Time string
* @return bool True if valid
*/
function isValidTime($time) {
return preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/', $time);
}
/**
* Validate date format (YYYY-MM-DD)
* @param string $date Date string
* @return bool True if valid
*/
function isValidDate($date) {
$d = DateTime::createFromFormat('Y-m-d', $date);
return $d && $d->format('Y-m-d') === $date;
}

172
index.php
View File

@ -101,13 +101,133 @@ if (isset($_GET['edit'])) {
<html lang="nl"> <html lang="nl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Telvero Talpa Control Panel</title> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Telvero Talpa</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="assets/css/custom.css">
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container mt-5"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<h1>Telvero Homeshopping Planner</h1> <div class="container-fluid">
<div class="row mt-4"> <a class="navbar-brand" href="index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<div class="navbar-nav">
<a class="nav-link active" href="index.php">Dashboard</a>
<a class="nav-link" href="planner.php">Excel Planner</a>
<a class="nav-link" href="calendar.php">Kalender</a>
<a class="nav-link" href="blocks.php">Blokken</a>
<a class="nav-link" href="commercials.php">Commercials</a>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-speedometer2"></i> Dashboard</h1>
<a href="calendar.php" class="btn btn-primary btn-lg">
<i class="bi bi-calendar-week"></i> Open Kalender
</a>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<?php
$totalCommercials = $db->query("SELECT COUNT(*) FROM commercials")->fetchColumn();
$uploadedCommercials = $db->query("SELECT COUNT(*) FROM commercials WHERE upload_status = 'uploaded'")->fetchColumn();
$totalTransmissions = $db->query("SELECT COUNT(*) FROM transmissions")->fetchColumn();
$pendingSync = $db->query("SELECT COUNT(*) FROM transmissions WHERE api_status = 'pending'")->fetchColumn();
$syncedTransmissions = $db->query("SELECT COUNT(*) FROM transmissions WHERE api_status = 'synced'")->fetchColumn();
?>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number"><?= $totalCommercials ?></div>
<div class="stat-label">Totaal Commercials</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card success">
<div class="stat-number"><?= $uploadedCommercials ?></div>
<div class="stat-label">Geüpload</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card info">
<div class="stat-number"><?= $totalTransmissions ?></div>
<div class="stat-label">Geplande Uitzendingen</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card warning">
<div class="stat-number"><?= $pendingSync ?></div>
<div class="stat-label">Wacht op Sync</div>
</div>
</div>
</div>
<h2 class="mb-3"><i class="bi bi-grid-3x3"></i> Snelle Acties</h2>
<div class="row mb-4">
<div class="col-md-3">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<i class="bi bi-table" style="font-size: 3rem; color: #2ecc71;"></i>
<h5 class="mt-3">Excel Planner</h5>
<p class="text-muted">Tabel-gebaseerde planning</p>
<a href="planner.php" class="btn btn-success">
<i class="bi bi-arrow-right-circle"></i> Open Planner
</a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<i class="bi bi-calendar-plus" style="font-size: 3rem; color: #3498db;"></i>
<h5 class="mt-3">Kalender View</h5>
<p class="text-muted">Visuele tijdlijn planning</p>
<a href="calendar.php" class="btn btn-primary">
<i class="bi bi-arrow-right-circle"></i> Open Kalender
</a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<i class="bi bi-collection-play" style="font-size: 3rem; color: #e74c3c;"></i>
<h5 class="mt-3">Commercials</h5>
<p class="text-muted">Registreer en beheer</p>
<a href="commercials.php" class="btn btn-danger">
<i class="bi bi-arrow-right-circle"></i> Naar Commercials
</a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<i class="bi bi-calendar3" style="font-size: 3rem; color: #9b59b6;"></i>
<h5 class="mt-3">Blok Templates</h5>
<p class="text-muted">Beheer tijdblokken</p>
<a href="blocks.php" class="btn btn-secondary">
<i class="bi bi-arrow-right-circle"></i> Naar Blokken
</a>
</div>
</div>
</div>
</div>
<h2 class="mb-3"><i class="bi bi-clock-history"></i> Recente Activiteit</h2>
<div class="row">
<div class="col-md-4"> <div class="col-md-4">
<div class="card p-3 shadow-sm"> <div class="card p-3 shadow-sm">
<h5>1. Commercial Registreren</h5> <h5>1. Commercial Registreren</h5>
@ -160,15 +280,28 @@ if (isset($_GET['edit'])) {
</div> </div>
</div> </div>
<hr class="my-5"> <hr class="my-4">
<h3>Dagplanning: <?= htmlspecialchars($selectedDate) ?></h3> <h2 class="mb-3"><i class="bi bi-calendar-check"></i> Vandaag's Planning</h2>
<div class="card shadow-sm">
<div class="card-body">
<form method="GET" class="row g-2 mb-3"> <form method="GET" class="row g-2 mb-3">
<div class="col-md-3"><input type="date" name="view_date" class="form-control" value="<?= $selectedDate ?>"></div> <div class="col-md-3">
<div class="col-md-2"><button type="submit" class="btn btn-secondary">Bekijk Datum</button></div> <input type="date" name="view_date" class="form-control" value="<?= $selectedDate ?>">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-secondary">
<i class="bi bi-search"></i> Bekijk Datum
</button>
</div>
<div class="col-md-7 text-end">
<a href="calendar.php" class="btn btn-primary">
<i class="bi bi-calendar-week"></i> Open in Kalender View
</a>
</div>
</form> </form>
<table class="table table-bordered bg-white mb-5 text-center align-middle"> <table class="table table-hover mb-0">
<thead class="table-dark"> <thead class="table-dark">
<tr><th>Tijd</th><th>Product</th><th>Duur</th><th>Sync Status</th><th>Acties</th></tr> <tr><th>Tijd</th><th>Product</th><th>Duur</th><th>Sync Status</th><th>Acties</th></tr>
</thead> </thead>
@ -196,10 +329,14 @@ if (isset($_GET['edit'])) {
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div>
</div>
<h3>Media Asset Management</h3> <h2 class="mt-4 mb-3"><i class="bi bi-file-earmark-text"></i> Media Asset Management</h2>
<div class="card p-3 shadow-sm">
<table class="table table-striped bg-white align-middle"> <div class="card shadow-sm">
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-secondary"> <thead class="table-secondary">
<tr> <tr>
<th>Titel</th> <th>Titel</th>
@ -235,8 +372,17 @@ if (isset($_GET['edit'])) {
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div>
</div> </div>
</div>
</div>
<footer class="mt-5 py-4 bg-dark text-white text-center">
<div class="container">
<p class="mb-0">
<i class="bi bi-tv"></i> Telvero Talpa Planning System &copy; 2026
</p>
</div>
</footer>
<script> <script>
// Bestaande JS voor duur selectie // Bestaande JS voor duur selectie

View File

@ -0,0 +1,64 @@
-- Migration: Add block templates, daily blocks, and color coding
-- Date: 2026-01-13
-- Description: Extends existing schema for TV planning system
-- Add color and series code to commercials table
ALTER TABLE commercials
ADD COLUMN color_code VARCHAR(7) DEFAULT NULL COMMENT 'Hex color code for visual identification',
ADD COLUMN series_code VARCHAR(20) DEFAULT NULL COMMENT 'Series identifier (e.g., 006a, 007a)';
-- Create block templates table for recurring time slots
CREATE TABLE IF NOT EXISTS block_templates (
id INT PRIMARY KEY AUTO_INCREMENT,
channel VARCHAR(50) NOT NULL COMMENT 'Channel name (e.g., SBS9, NET5)',
name VARCHAR(100) NOT NULL COMMENT 'Template name (e.g., "SBS9 Dagblok")',
day_of_week ENUM('ma','di','wo','do','vr','za','zo','all') DEFAULT 'all' COMMENT 'Day of week or all days',
default_start_time TIME NOT NULL COMMENT 'Default start time for this block',
default_end_time TIME DEFAULT NULL COMMENT 'Optional end time',
is_active BOOLEAN DEFAULT 1 COMMENT 'Whether this template is currently active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_channel (channel),
INDEX idx_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create daily blocks table for actual daily scheduling
CREATE TABLE IF NOT EXISTS daily_blocks (
id INT PRIMARY KEY AUTO_INCREMENT,
template_id INT DEFAULT NULL COMMENT 'Reference to template (nullable for manual blocks)',
channel VARCHAR(50) NOT NULL,
block_date DATE NOT NULL,
actual_start_time TIME NOT NULL COMMENT 'Actual start time for this specific day',
actual_end_time TIME DEFAULT NULL,
is_locked BOOLEAN DEFAULT 0 COMMENT 'Prevent accidental modifications',
notes TEXT DEFAULT NULL COMMENT 'Optional notes for this block',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (template_id) REFERENCES block_templates(id) ON DELETE SET NULL,
UNIQUE KEY unique_channel_date_template (channel, block_date, template_id),
INDEX idx_date (block_date),
INDEX idx_channel_date (channel, block_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insert default block templates for SBS9
INSERT INTO block_templates (channel, name, day_of_week, default_start_time, default_end_time, is_active) VALUES
('SBS9', 'SBS9 Dagblok', 'all', '07:00:00', '15:00:00', 1),
('SBS9', 'SBS9 Nachtblok', 'all', '23:30:00', '02:00:00', 1);
-- Insert default block templates for Net5
INSERT INTO block_templates (channel, name, day_of_week, default_start_time, default_end_time, is_active) VALUES
('NET5', 'Net5 Ochtendblok', 'ma', '07:30:00', '11:30:00', 1),
('NET5', 'Net5 Ochtendblok', 'di', '07:30:00', '11:30:00', 1),
('NET5', 'Net5 Ochtendblok', 'wo', '07:30:00', '11:30:00', 1),
('NET5', 'Net5 Ochtendblok', 'do', '07:30:00', '11:30:00', 1),
('NET5', 'Net5 Ochtendblok', 'vr', '07:30:00', '11:30:00', 1),
('NET5', 'Net5 Middagblok', 'ma', '13:20:00', '13:50:00', 1),
('NET5', 'Net5 Middagblok', 'di', '13:20:00', '13:50:00', 1),
('NET5', 'Net5 Middagblok', 'wo', '13:20:00', '13:50:00', 1),
('NET5', 'Net5 Middagblok', 'do', '13:20:00', '13:50:00', 1),
('NET5', 'Net5 Middagblok', 'vr', '13:20:00', '13:50:00', 1);
-- Add index to transmissions for better performance
ALTER TABLE transmissions
ADD INDEX idx_channel_date (channel, start_date),
ADD INDEX idx_date_time (start_date, start_time);

825
planner.php Normal file
View File

@ -0,0 +1,825 @@
<?php
/**
* Excel-Style Planning View
* Table-based planning interface - both channels side by side
*/
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/helpers.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();
$db = getDbConnection();
// Handle adding transmission to block
if (isset($_POST['add_to_block'])) {
$commercialId = $_POST['commercial_id'];
$channel = $_POST['channel'];
$date = $_POST['date'];
$blockId = $_POST['block_id'] ?? null;
// Get commercial duration
$stmt = $db->prepare("SELECT duration FROM commercials WHERE id = ?");
$stmt->execute([$commercialId]);
$duration = $stmt->fetchColumn();
// Calculate next start time for this specific block
$nextStartTime = calculateNextStartTimeForBlock($db, $date, $channel, $blockId);
// Insert transmission
$stmt = $db->prepare("
INSERT INTO transmissions
(commercial_id, channel, template, start_date, start_time, duration, api_status)
VALUES (?, ?, 'HOME030', ?, ?, ?, 'pending')
");
$stmt->execute([$commercialId, $channel, $date, $nextStartTime, $duration]);
header("Location: planner.php?date=$date&success=added");
exit;
}
// Handle remove transmission
if (isset($_POST['remove_transmission'])) {
$stmt = $db->prepare("DELETE FROM transmissions WHERE id = ?");
$stmt->execute([$_POST['transmission_id']]);
header("Location: planner.php?date={$_POST['date']}&success=removed");
exit;
}
// Handle reorder (move up/down)
if (isset($_POST['reorder'])) {
$transmissionId = $_POST['transmission_id'];
$direction = $_POST['direction']; // 'up' or 'down'
$date = $_POST['date'];
$channel = $_POST['channel'];
$blockId = $_POST['block_id'];
// Get block info including end time
$stmt = $db->prepare("SELECT actual_start_time, actual_end_time FROM daily_blocks WHERE id = ?");
$stmt->execute([$blockId]);
$blockInfo = $stmt->fetch();
$blockStartTime = $blockInfo['actual_start_time'];
$blockEndTime = $blockInfo['actual_end_time'] ?? '23:59:59';
// Get all transmissions for THIS SPECIFIC block only
$stmt = $db->prepare("
SELECT t.id, t.start_time, c.duration
FROM transmissions t
JOIN commercials c ON t.commercial_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, $blockStartTime, $blockEndTime]);
$transmissions = $stmt->fetchAll();
// Find current position
$currentIndex = array_search($transmissionId, array_column($transmissions, 'id'));
if ($currentIndex !== false) {
if ($direction === 'up' && $currentIndex > 0) {
// Swap with previous
$temp = $transmissions[$currentIndex];
$transmissions[$currentIndex] = $transmissions[$currentIndex - 1];
$transmissions[$currentIndex - 1] = $temp;
} elseif ($direction === 'down' && $currentIndex < count($transmissions) - 1) {
// Swap with next
$temp = $transmissions[$currentIndex];
$transmissions[$currentIndex] = $transmissions[$currentIndex + 1];
$transmissions[$currentIndex + 1] = $temp;
}
// Recalculate all start times
$currentTime = $blockStartTime;
foreach ($transmissions as $tx) {
$stmt = $db->prepare("UPDATE transmissions SET start_time = ?, api_status = 'pending' WHERE id = ?");
$stmt->execute([$currentTime, $tx['id']]);
$currentTime = addTimeToTime($currentTime, $tx['duration']);
}
}
header("Location: planner.php?date=$date");
exit;
}
// Handle block time update
if (isset($_POST['update_block_time'])) {
$blockId = $_POST['block_id'];
$newStartTime = $_POST['new_start_time'];
$recalculate = isset($_POST['recalculate']);
// Update block time
$stmt = $db->prepare("UPDATE daily_blocks SET actual_start_time = ? WHERE id = ?");
$stmt->execute([$newStartTime, $blockId]);
// Optionally recalculate transmissions
if ($recalculate) {
$stmt = $db->prepare("SELECT block_date, channel FROM daily_blocks WHERE id = ?");
$stmt->execute([$blockId]);
$blockInfo = $stmt->fetch();
$stmt = $db->prepare("
SELECT t.id, c.duration
FROM transmissions t
JOIN commercials c ON t.commercial_id = c.id
WHERE t.start_date = ? AND t.channel = ? AND t.start_time >= ?
ORDER BY t.start_time ASC
");
$stmt->execute([$blockInfo['block_date'], $blockInfo['channel'], $newStartTime]);
$transmissions = $stmt->fetchAll();
$currentTime = $newStartTime;
foreach ($transmissions as $tx) {
$stmt = $db->prepare("UPDATE transmissions SET start_time = ?, api_status = 'pending' WHERE id = ?");
$stmt->execute([$currentTime, $tx['id']]);
$currentTime = addTimeToTime($currentTime, $tx['duration']);
}
}
header("Location: planner.php?date={$blockInfo['block_date']}&success=block_updated");
exit;
}
// Get selected date
$selectedDate = $_GET['date'] ?? date('Y-m-d');
// Ensure daily blocks exist
ensureDailyBlocks($db, $selectedDate, $selectedDate);
// Get all blocks for this date (both channels, all blocks)
$stmt = $db->prepare("
SELECT db.*, bt.name as template_name, bt.channel
FROM daily_blocks db
LEFT JOIN block_templates bt ON db.template_id = bt.id
WHERE db.block_date = ?
ORDER BY db.channel, db.actual_start_time
");
$stmt->execute([$selectedDate]);
$allBlocks = $stmt->fetchAll();
// Group blocks by channel
$blocksByChannel = [];
foreach ($allBlocks as $block) {
$blocksByChannel[$block['channel']][] = $block;
}
// Get all commercials
$commercials = $db->query("
SELECT * FROM commercials
WHERE upload_status = 'uploaded'
ORDER BY title ASC
")->fetchAll();
// Helper function to calculate next start time for a specific block
function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
if ($blockId) {
$stmt = $db->prepare("SELECT actual_start_time, actual_end_time FROM daily_blocks WHERE id = ?");
$stmt->execute([$blockId]);
$blockInfo = $stmt->fetch();
$blockStartTime = $blockInfo['actual_start_time'];
$blockEndTime = $blockInfo['actual_end_time'] ?? '23:59:59';
} else {
$blockStartTime = '07:00:00'; // Default
$blockEndTime = '23:59:59';
}
// Get all transmissions in THIS SPECIFIC block only
$stmt = $db->prepare("
SELECT start_time, duration
FROM transmissions
WHERE start_date = ? AND channel = ?
AND start_time >= ? AND start_time < ?
ORDER BY start_time DESC
LIMIT 1
");
$stmt->execute([$date, $channel, $blockStartTime, $blockEndTime]);
$lastTx = $stmt->fetch();
if ($lastTx) {
return addTimeToTime($lastTx['start_time'], $lastTx['duration']);
}
return $blockStartTime;
}
?>
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excel Planning - Telvero Talpa</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="assets/css/custom.css">
<style>
.planning-table {
font-size: 0.85em;
}
.planning-table td {
vertical-align: middle;
padding: 6px;
}
.color-indicator {
width: 25px;
height: 25px;
border-radius: 3px;
display: inline-block;
}
.time-cell {
font-family: 'Courier New', monospace;
font-weight: 600;
font-size: 0.9em;
}
.duration-cell {
text-align: center;
font-weight: 600;
}
.remaining-time {
font-size: 0.85em;
color: #6c757d;
}
.block-section {
background: #f8f9fa;
border-left: 4px solid;
margin-bottom: 20px;
}
.block-header {
background: #e9ecef;
padding: 8px 12px;
font-weight: 600;
border-bottom: 2px solid #dee2e6;
}
</style>
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">
<i class="bi bi-tv"></i> Telvero Talpa Planner
</a>
<div class="navbar-nav">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link active" href="planner.php">Excel Planner</a>
<a class="nav-link" href="calendar.php">Kalender</a>
<a class="nav-link" href="blocks.php">Blokken</a>
<a class="nav-link" href="commercials.php">Commercials</a>
</div>
</div>
</nav>
<div class="container-fluid mt-4">
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?php
$messages = [
'added' => 'Uitzending toegevoegd!',
'removed' => 'Uitzending verwijderd!'
];
echo $messages[$_GET['success']] ?? 'Actie succesvol!';
?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<!-- Date Selector -->
<div class="row mb-3">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-body">
<form method="GET" class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label"><strong>Selecteer Datum</strong></label>
<input type="date" name="date" class="form-control" value="<?= $selectedDate ?>" required>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-search"></i> Toon Planning
</button>
</div>
<div class="col-md-7 text-end">
<a href="calendar.php" class="btn btn-outline-secondary">
<i class="bi bi-calendar-week"></i> Kalender View
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<h3 class="mb-3"><?= formatDateDutch($selectedDate) ?></h3>
<!-- Both Channels Side by Side -->
<div class="row">
<!-- Commercial Sidebar -->
<div class="col-md-2">
<div class="card shadow-sm sticky-top" style="top: 20px;">
<div class="card-header bg-success text-white">
<h6 class="mb-0">
<i class="bi bi-collection-play"></i> Commercials
</h6>
</div>
<div class="card-body p-2">
<!-- Search -->
<div class="mb-2">
<input type="text"
class="form-control form-control-sm"
id="commercialSearch"
placeholder="🔍 Zoeken..."
onkeyup="filterPlannerCommercials(this.value)">
</div>
<!-- Commercial List -->
<div class="commercial-sidebar" style="max-height: 70vh;">
<?php if (empty($commercials)): ?>
<div class="text-center text-muted p-2">
<i class="bi bi-inbox"></i>
<p class="small mt-2">Geen commercials</p>
</div>
<?php else: ?>
<?php foreach ($commercials as $commercial): ?>
<div class="commercial-item draggable-commercial"
style="border-left-color: <?= htmlspecialchars($commercial['color_code']) ?>; padding: 8px; margin-bottom: 8px;"
draggable="true"
data-commercial-id="<?= $commercial['id'] ?>"
data-title="<?= htmlspecialchars($commercial['title']) ?>"
data-duration="<?= $commercial['duration'] ?>"
data-color="<?= htmlspecialchars($commercial['color_code']) ?>"
data-series-code="<?= htmlspecialchars($commercial['series_code'] ?? '') ?>">
<div class="d-flex align-items-center">
<div style="background-color: <?= htmlspecialchars($commercial['color_code']) ?>; width: 15px; height: 15px; border-radius: 2px; margin-right: 6px;">
</div>
<div class="flex-grow-1">
<div style="font-weight: 600; font-size: 0.8em;">
<?= htmlspecialchars($commercial['title']) ?>
</div>
<div style="font-size: 0.7em; color: #6c757d;">
<?= round(timeToSeconds($commercial['duration']) / 60) ?> min
</div>
</div>
<div>
<i class="bi bi-grip-vertical text-muted"></i>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="col-md-10">
<div class="row">
<?php foreach (['SBS9', 'NET5'] as $channel): ?>
<div class="col-md-6">
<div class="card shadow-sm mb-4">
<div class="card-header <?= $channel == 'SBS9' ? 'bg-primary' : 'bg-danger' ?> text-white">
<h5 class="mb-0">
<i class="bi bi-tv"></i> <?= $channel ?>
</h5>
</div>
<div class="card-body p-0">
<?php
$channelBlocks = $blocksByChannel[$channel] ?? [];
if (empty($channelBlocks)): ?>
<div class="p-4 text-center text-muted">
<i class="bi bi-inbox"></i>
<p>Geen blokken voor deze dag</p>
</div>
<?php else: ?>
<?php foreach ($channelBlocks as $block):
// Get transmissions for this block
$blockStart = $block['actual_start_time'];
$blockEnd = $block['actual_end_time'] ?? '23:59:59';
$stmt = $db->prepare("
SELECT t.*, c.title, c.color_code, c.series_code
FROM transmissions t
JOIN commercials c ON t.commercial_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([$selectedDate, $channel, $blockStart, $blockEnd]);
$blockTransmissions = $stmt->fetchAll();
// Calculate block stats
$totalBlockMinutes = (timeToSeconds($blockEnd) - timeToSeconds($blockStart)) / 60;
$usedMinutes = 0;
foreach ($blockTransmissions as $tx) {
$usedMinutes += timeToSeconds($tx['duration']) / 60;
}
$remainingMinutes = $totalBlockMinutes - $usedMinutes;
?>
<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>
<?= htmlspecialchars($block['template_name'] ?? 'Blok') ?>
<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> Tijd aanpassen
</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>
<table class="table table-sm table-hover planning-table mb-0">
<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: 100px;">Acties</th>
</tr>
</thead>
<tbody data-block-id="<?= $block['id'] ?>" data-channel="<?= $channel ?>" data-date="<?= $selectedDate ?>">
<?php if (empty($blockTransmissions)): ?>
<tr>
<td colspan="8" class="text-center text-muted py-3">
<small>Nog geen uitzendingen in dit blok</small>
</td>
</tr>
<?php else: ?>
<?php foreach ($blockTransmissions as $index => $tx):
$durationMinutes = round(timeToSeconds($tx['duration']) / 60);
$endTime = addTimeToTime($tx['start_time'], $tx['duration']);
// Calculate remaining time in block after this transmission
$remainingSeconds = timeToSeconds($blockEnd) - timeToSeconds($endTime);
$txRemainingMinutes = round($remainingSeconds / 60);
?>
<tr style="background-color: <?= $tx['color_code'] ?>15;">
<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>
<td>
<div class="btn-group btn-group-sm" role="group">
<?php if ($index > 0): ?>
<form method="POST" style="display:inline;">
<input type="hidden" name="transmission_id" value="<?= $tx['id'] ?>">
<input type="hidden" name="direction" value="up">
<input type="hidden" name="date" value="<?= $selectedDate ?>">
<input type="hidden" name="channel" value="<?= $channel ?>">
<input type="hidden" name="block_id" value="<?= $block['id'] ?>">
<button type="submit" name="reorder" class="btn btn-outline-secondary" title="Omhoog">
<i class="bi bi-arrow-up"></i>
</button>
</form>
<?php endif; ?>
<?php if ($index < count($blockTransmissions) - 1): ?>
<form method="POST" style="display:inline;">
<input type="hidden" name="transmission_id" value="<?= $tx['id'] ?>">
<input type="hidden" name="direction" value="down">
<input type="hidden" name="date" value="<?= $selectedDate ?>">
<input type="hidden" name="channel" value="<?= $channel ?>">
<input type="hidden" name="block_id" value="<?= $block['id'] ?>">
<button type="submit" name="reorder" class="btn btn-outline-secondary" title="Omlaag">
<i class="bi bi-arrow-down"></i>
</button>
</form>
<?php endif; ?>
<form method="POST" style="display:inline;" onsubmit="return confirm('Verwijderen?');">
<input type="hidden" name="transmission_id" value="<?= $tx['id'] ?>">
<input type="hidden" name="date" value="<?= $selectedDate ?>">
<button type="submit" name="remove_transmission" class="btn btn-outline-danger" title="Verwijderen">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
<!-- Add Commercial Row (also a drop zone) -->
<tr class="table-success drop-zone-end"
data-block-id="<?= $block['id'] ?>"
data-channel="<?= $channel ?>"
data-date="<?= $selectedDate ?>">
<td colspan="8">
<form method="POST" class="row g-2 align-items-center">
<input type="hidden" name="date" value="<?= $selectedDate ?>">
<input type="hidden" name="channel" value="<?= $channel ?>">
<input type="hidden" name="block_id" value="<?= $block['id'] ?>">
<div class="col-md-8">
<select name="commercial_id" class="form-select form-select-sm" required>
<option value="">+ Voeg commercial toe...</option>
<?php foreach ($commercials as $c): ?>
<option value="<?= $c['id'] ?>">
<?= htmlspecialchars($c['series_code'] ?? '') ?> -
<?= htmlspecialchars($c['title']) ?>
(<?= round(timeToSeconds($c['duration']) / 60) ?> min)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<button type="submit" name="add_to_block" class="btn btn-success btn-sm w-100">
<i class="bi bi-plus-circle"></i> Toevoegen
</button>
</div>
</form>
</td>
</tr>
</tbody>
</table>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<!-- Block Time Edit Modals -->
<?php foreach ($allBlocks as $block): ?>
<div class="modal fade" id="blockTimeModal<?= $block['id'] ?>" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST">
<input type="hidden" name="block_id" value="<?= $block['id'] ?>">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-clock"></i> Blok Starttijd Aanpassen
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<strong><?= htmlspecialchars($block['template_name'] ?? 'Blok') ?></strong><br>
<?= htmlspecialchars($block['channel']) ?> - <?= formatDateDutch($selectedDate) ?>
</div>
<div class="mb-3">
<label class="form-label">Nieuwe Starttijd</label>
<input type="time" name="new_start_time" class="form-control"
value="<?= $block['actual_start_time'] ?>" required>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="recalculate"
id="recalc<?= $block['id'] ?>" checked>
<label class="form-check-label" for="recalc<?= $block['id'] ?>">
Herbereken alle uitzendingen in dit blok
</label>
</div>
<div class="alert alert-warning mt-3 small">
<i class="bi bi-exclamation-triangle"></i>
Als je "herbereken" aanvinkt, worden alle uitzendingen in dit blok opnieuw getimed vanaf de nieuwe starttijd.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
<button type="submit" name="update_block_time" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Opslaan
</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/sortablejs@1.15.0/Sortable.min.js"></script>
<script>
// Filter commercials in sidebar
function filterPlannerCommercials(searchTerm) {
const items = document.querySelectorAll('.draggable-commercial');
const term = searchTerm.toLowerCase();
items.forEach(item => {
const title = item.dataset.title.toLowerCase();
const seriesCode = (item.dataset.seriesCode || '').toLowerCase();
if (title.includes(term) || seriesCode.includes(term)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
// Make commercial items draggable
document.querySelectorAll('.draggable-commercial').forEach(item => {
item.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('commercial_id', this.dataset.commercialId);
e.dataTransfer.setData('title', this.dataset.title);
e.dataTransfer.setData('duration', this.dataset.duration);
this.style.opacity = '0.5';
});
item.addEventListener('dragend', function(e) {
this.style.opacity = '1';
});
});
// Make table rows drop targets
document.querySelectorAll('.planning-table tbody').forEach(tbody => {
const blockId = tbody.dataset.blockId;
const channel = tbody.dataset.channel;
const date = tbody.dataset.date;
// Make each row a drop zone
tbody.querySelectorAll('tr:not(.table-success)').forEach((row, index) => {
row.addEventListener('dragover', function(e) {
e.preventDefault();
this.style.borderTop = '3px solid #28a745';
});
row.addEventListener('dragleave', function(e) {
this.style.borderTop = '';
});
row.addEventListener('drop', function(e) {
e.preventDefault();
this.style.borderTop = '';
const commercialId = e.dataTransfer.getData('commercial_id');
// Insert at this position
fetch('api/insert_transmission_at_position.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
commercial_id: commercialId,
channel: channel,
date: date,
block_id: blockId,
position: index
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.reload();
} else {
alert('Fout: ' + data.error);
}
});
});
});
});
// Make "Add Commercial" rows also drop zones (for adding at end)
document.querySelectorAll('.drop-zone-end').forEach(row => {
const blockId = row.dataset.blockId;
const channel = row.dataset.channel;
const date = row.dataset.date;
row.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
this.style.backgroundColor = '#d4edda';
});
row.addEventListener('dragleave', function(e) {
this.style.backgroundColor = '';
});
row.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
this.style.backgroundColor = '';
const commercialId = e.dataTransfer.getData('commercial_id');
if (!commercialId) return;
// Add at end (use existing add_to_block logic)
const form = this.querySelector('form');
if (form) {
const select = form.querySelector('select[name="commercial_id"]');
if (select) {
select.value = commercialId;
form.submit();
}
}
});
});
// Sync block to Talpa
function syncBlockPlanner(date, channel) {
if (!confirm(`Wilt u alle uitzendingen in dit blok synchroniseren naar Talpa?\n\nDatum: ${date}\nZender: ${channel}`)) {
return;
}
// Show loading indicator
const btn = event.target.closest('button');
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span> Synchroniseren...';
fetch('api/sync_block.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
date: date,
channel: channel
})
})
.then(response => response.json())
.then(data => {
btn.disabled = false;
btn.innerHTML = originalHTML;
// Log debug information to console
console.group('🔍 Sync Block Debug Info');
console.log('Full Response:', data);
if (data.debug) {
console.log('Debug Log:', data.debug);
}
if (data.api_calls) {
console.log('API Calls:', data.api_calls);
}
if (data.errors) {
console.log('Errors:', data.errors);
}
console.groupEnd();
if (data.success) {
let message = `✓ Synchronisatie voltooid!\n\nGeslaagd: ${data.synced}\nMislukt: ${data.failed}`;
if (data.errors && data.errors.length > 0) {
message += '\n\nFouten:\n';
data.errors.forEach(err => {
message += `- ${err.title} (${err.time}): ${err.error}\n`;
});
message += '\n\nZie console (F12) voor gedetailleerde debug informatie.';
}
alert(message);
// Reload page to show updated status
window.location.reload();
} else {
alert('✗ Fout bij synchroniseren: ' + (data.error || 'Onbekende fout') + '\n\nZie console (F12) voor debug informatie.');
}
})
.catch(error => {
btn.disabled = false;
btn.innerHTML = originalHTML;
console.error('Error:', error);
alert('✗ Netwerkfout bij synchroniseren');
});
}
</script>
</body>
</html>

View File

@ -3,7 +3,7 @@
'name' => '__root__', 'name' => '__root__',
'pretty_version' => 'dev-main', 'pretty_version' => 'dev-main',
'version' => 'dev-main', 'version' => 'dev-main',
'reference' => '4807b34bad8dee14a59e74f3b55faf2055d70888', 'reference' => 'bfdd27fbccac0f215910524444f38fe5311aaf40',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@ -13,7 +13,7 @@
'__root__' => array( '__root__' => array(
'pretty_version' => 'dev-main', 'pretty_version' => 'dev-main',
'version' => 'dev-main', 'version' => 'dev-main',
'reference' => '4807b34bad8dee14a59e74f3b55faf2055d70888', 'reference' => 'bfdd27fbccac0f215910524444f38fe5311aaf40',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),