diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..95d1fcf
--- /dev/null
+++ b/.env.example
@@ -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
diff --git a/INSTALLATION.md b/INSTALLATION.md
new file mode 100644
index 0000000..b0c0eaa
--- /dev/null
+++ b/INSTALLATION.md
@@ -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
+
+ RewriteEngine On
+ RewriteBase /
+
+ # Redirect to HTTPS (productie)
+ # RewriteCond %{HTTPS} off
+ # RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
+
+
+# Prevent directory listing
+Options -Indexes
+
+# Protect .env file
+
+ Order allow,deny
+ Deny from all
+
+```
+
+**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! 🎉
diff --git a/README.md b/README.md
index f46ccbe..0a1e3ba 100644
--- a/README.md
+++ b/README.md
@@ -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**
diff --git a/TalpaAPI.php b/TalpaAPI.php
index be3eafb..7748bbb 100644
--- a/TalpaAPI.php
+++ b/TalpaAPI.php
@@ -100,9 +100,14 @@ class TalpaApi {
]);
}
+ public function deleteEpisode($contentId) {
+ return $this->request('DELETE', '/content/v1/episodes/' . $contentId);
+ }
+
private function getMockResponse($endpoint, $data) {
usleep(200000);
- if (strpos($endpoint, '/content/v1/episodes') !== false) 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) return ["id" => "MOCK_ASSET_" . time()];
if (strpos($endpoint, '/linearSchedule/v1/transmissions') !== false) return ["statusCode" => "201", "id" => "MOCK_TX_" . time()];
diff --git a/api/assign_color.php b/api/assign_color.php
new file mode 100644
index 0000000..fceda43
--- /dev/null
+++ b/api/assign_color.php
@@ -0,0 +1,97 @@
+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);
+}
diff --git a/api/create_transmission.php b/api/create_transmission.php
new file mode 100644
index 0000000..fbfef88
--- /dev/null
+++ b/api/create_transmission.php
@@ -0,0 +1,195 @@
+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);
+}
diff --git a/api/delete_transmission.php b/api/delete_transmission.php
new file mode 100644
index 0000000..621e10e
--- /dev/null
+++ b/api/delete_transmission.php
@@ -0,0 +1,60 @@
+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);
+}
diff --git a/api/get_block_time.php b/api/get_block_time.php
new file mode 100644
index 0000000..fa8ce72
--- /dev/null
+++ b/api/get_block_time.php
@@ -0,0 +1,63 @@
+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);
+}
diff --git a/api/get_daily_blocks.php b/api/get_daily_blocks.php
new file mode 100644
index 0000000..0dbed01
--- /dev/null
+++ b/api/get_daily_blocks.php
@@ -0,0 +1,95 @@
+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)';
+}
diff --git a/api/get_next_start_time.php b/api/get_next_start_time.php
new file mode 100644
index 0000000..3c47c9f
--- /dev/null
+++ b/api/get_next_start_time.php
@@ -0,0 +1,37 @@
+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);
+}
diff --git a/api/get_transmissions.php b/api/get_transmissions.php
new file mode 100644
index 0000000..5ebb747
--- /dev/null
+++ b/api/get_transmissions.php
@@ -0,0 +1,98 @@
+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);
+}
diff --git a/api/insert_transmission_at_position.php b/api/insert_transmission_at_position.php
new file mode 100644
index 0000000..bd5ed9c
--- /dev/null
+++ b/api/insert_transmission_at_position.php
@@ -0,0 +1,118 @@
+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);
+}
diff --git a/api/sync_block.php b/api/sync_block.php
new file mode 100644
index 0000000..8437b46
--- /dev/null
+++ b/api/sync_block.php
@@ -0,0 +1,172 @@
+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
+ ]);
+}
diff --git a/api/update_block_time.php b/api/update_block_time.php
new file mode 100644
index 0000000..0cdad56
--- /dev/null
+++ b/api/update_block_time.php
@@ -0,0 +1,143 @@
+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);
+}
diff --git a/api/update_transmission.php b/api/update_transmission.php
new file mode 100644
index 0000000..5c62428
--- /dev/null
+++ b/api/update_transmission.php
@@ -0,0 +1,195 @@
+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);
+}
diff --git a/api/validate_transmission_time.php b/api/validate_transmission_time.php
new file mode 100644
index 0000000..234991f
--- /dev/null
+++ b/api/validate_transmission_time.php
@@ -0,0 +1,130 @@
+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);
+}
diff --git a/assets/css/custom.css b/assets/css/custom.css
new file mode 100644
index 0000000..28cefbf
--- /dev/null
+++ b/assets/css/custom.css
@@ -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;
+ }
+}
diff --git a/assets/js/calendar-init.js b/assets/js/calendar-init.js
new file mode 100644
index 0000000..07abb7b
--- /dev/null
+++ b/assets/js/calendar-init.js
@@ -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: `
+
+
${arg.timeText}
+
+
+ ${seriesCode ? '' + seriesCode + ' - ' : ''}
+ ${title}
+
+
+ ${duration}
+
+
+
+ `
+ };
+ }
+
+ // For vertical resource view - ultra compact single line
+ return {
+ html: `
+
+
+ ${seriesCode ? '' + seriesCode + ' ' : ''}
+ ${title} ⏱${duration}
+
+
+ `
+ };
+ },
+
+ // 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 = `
+ Zender: ${channelName}
+ Datum: ${event.start.toLocaleDateString('nl-NL')}
+ Starttijd: ${event.start.toLocaleTimeString('nl-NL', {hour: '2-digit', minute: '2-digit'})}
+ Duur: ${props.duration}
+ Series Code: ${props.series_code || '-'}
+ Template: ${props.template}
+ API Status: ${props.api_status}
+ `;
+
+ // 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}
+
+ `;
+
+ 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 = 'Geen actieve blokken voor deze dag
';
+ return;
+ }
+
+ // Group blocks by channel
+ const blocksByChannel = {};
+ blocks.forEach(block => {
+ if (!blocksByChannel[block.channel]) {
+ blocksByChannel[block.channel] = [];
+ }
+ blocksByChannel[block.channel].push(block);
+ });
+
+ let html = '';
+
+ for (const [channel, channelBlocks] of Object.entries(blocksByChannel)) {
+ const channelColors = {
+ 'SBS9': 'primary',
+ 'NET5': 'danger',
+ 'VERONICA': 'secondary'
+ };
+ const color = channelColors[channel] || 'secondary';
+
+ html += `
`;
+ html += `
`;
+ html += ``;
+ html += `
`;
+
+ 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 += `
`;
+ html += `${block.template_name || 'Blok'} `;
+ html += `${startTime} - ${endTime} `;
+ html += `
`;
+ });
+
+ html += `
`;
+ }
+
+ html += '
';
+ 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;
diff --git a/blocks.php b/blocks.php
new file mode 100644
index 0000000..fe4728c
--- /dev/null
+++ b/blocks.php
@@ -0,0 +1,319 @@
+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'
+];
+?>
+
+
+
+
+
+
+ Blok Templates - Telvero Talpa
+
+
+
+
+
+
+
+
+
+
+
+
Blok Templates
+
+ Naar Kalender
+
+
+
+
+
+ 'Template succesvol aangemaakt!',
+ 'updated' => 'Template succesvol bijgewerkt!',
+ 'deleted' => 'Template succesvol verwijderd!'
+ ];
+ echo $messages[$_GET['success']] ?? 'Actie succesvol uitgevoerd!';
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nog geen templates aangemaakt
+
+
+
+
+
+
+ Zender
+ Naam
+ Dag
+ Starttijd
+ Eindtijd
+ Status
+ Acties
+
+
+
+
+
+
+ = htmlspecialchars($template['channel']) ?>
+
+ = htmlspecialchars($template['name']) ?>
+ = $dayNames[$template['day_of_week']] ?>
+ = substr($template['default_start_time'], 0, 5) ?>
+ = $template['default_end_time'] ? substr($template['default_end_time'], 0, 5) : '-' ?>
+
+
+ Actief
+
+ Inactief
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Wat zijn Blok Templates?
+
Blok templates definiëren terugkerende tijdslots voor uitzendingen. Bijvoorbeeld:
+
+ SBS9 Dagblok: Ma-Zo 07:00-15:00
+ SBS9 Nachtblok: Ma-Zo 23:30-02:00
+ Net5 Ochtend: Ma-Vr 07:30-11:30
+
+
+
Hoe werkt het?
+
+ Maak templates aan voor vaste blokken
+ Het systeem genereert automatisch dagelijkse blokken
+ In de kalender kun je per dag de starttijd aanpassen
+ Uitzendingen worden automatisch in deze blokken gepland
+
+
+
+
+
+
+
+
+
+
diff --git a/calendar.php b/calendar.php
new file mode 100644
index 0000000..bab7b68
--- /dev/null
+++ b/calendar.php
@@ -0,0 +1,355 @@
+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();
+?>
+
+
+
+
+
+
+ Kalender Planning - Telvero Talpa
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+ +
+
+
+
+ Huidige: 100%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sleep commercials naar de kalender om te plannen
+
+
+
+
+
+
+
+
+
+
+
+ $color): ?>
+
+
+
= htmlspecialchars($code) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nieuwe Starttijd
+
+
+
+
+
+
+ Herbereken alle uitzendingen in dit blok
+
+
+
+
+
+ Als je "herbereken" aanvinkt, worden alle uitzendingen in dit blok opnieuw getimed vanaf de nieuwe starttijd.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Datum
+
+
+
+
+ Zender
+
+ SBS9
+ NET5
+ VERONICA
+
+
+
+
+
+ Dit synchroniseert alle uitzendingen in het geselecteerde blok naar Talpa.
+ Alleen uitzendingen die nog niet gesynchroniseerd zijn worden verzonden.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commercials.php b/commercials.php
new file mode 100644
index 0000000..35411f9
--- /dev/null
+++ b/commercials.php
@@ -0,0 +1,437 @@
+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();
+?>
+
+
+
+
+
+
+ Commercials - Telvero Talpa
+
+
+
+
+
+
+
+
+
+
+
+
Commercial Management
+
+ Naar Kalender
+
+
+
+
+
+ 'Commercial succesvol aangemaakt en geregistreerd bij Talpa!',
+ 'updated' => 'Commercial succesvol bijgewerkt!',
+ 'deleted' => 'Commercial succesvol verwijderd!'
+ ];
+ echo $messages[$_GET['success']] ?? 'Actie succesvol uitgevoerd!';
+ ?>
+
+
+
+
+
+
+ 'Deze commercial kan niet verwijderd worden omdat deze nog in gebruik is in de planning!'
+ ];
+ echo $messages[$_GET['error']] ?? 'Er is een fout opgetreden!';
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dit registreert de commercial bij Talpa en maakt automatisch een media asset aan.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nog geen commercials geregistreerd
+
+
+
+
+
+
+ Kleur
+ Titel
+ Duur
+ Series
+ Label
+ Status
+ Gebruik
+ Acties
+
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($c['title']) ?>
+
+ ID: = htmlspecialchars($c['content_id']) ?>
+
+
+ = $c['duration'] ?>
+
+
+ = $c['series_code'] ? '' . htmlspecialchars($c['series_code']) . ' ' : '-' ?>
+
+
+ = htmlspecialchars($c['media_asset_label']) ?>
+
+
+
+ Uploaded
+
+ Pending
+
+
+
+
+ = $c['usage_count'] ?>x
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/helpers.php b/helpers.php
new file mode 100644
index 0000000..e3101ff
--- /dev/null
+++ b/helpers.php
@@ -0,0 +1,387 @@
+ 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;
+}
diff --git a/index.php b/index.php
index 3d4e150..5ccca34 100644
--- a/index.php
+++ b/index.php
@@ -101,13 +101,133 @@ if (isset($_GET['edit'])) {
- Telvero Talpa Control Panel
+
+ Dashboard - Telvero Talpa
+
+
-
-
Telvero Homeshopping Planner
-
+
+
+
+
+
+
+
+
+
+ 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();
+ ?>
+
+
+
+
= $totalCommercials ?>
+
Totaal Commercials
+
+
+
+
+
+
= $uploadedCommercials ?>
+
Geüpload
+
+
+
+
+
+
= $totalTransmissions ?>
+
Geplande Uitzendingen
+
+
+
+
+
+
= $pendingSync ?>
+
Wacht op Sync
+
+
+
+
+
Snelle Acties
+
+
+
Recente Activiteit
+
+
1. Commercial Registreren
@@ -160,15 +280,28 @@ if (isset($_GET['edit'])) {
-
+
-
Dagplanning: = htmlspecialchars($selectedDate) ?>
-
+
Vandaag's Planning
+
+
+
-
+
Tijd Product Duur Sync Status Acties
@@ -195,11 +328,15 @@ if (isset($_GET['edit'])) {
-
+
+
+
-
Media Asset Management
-
-
+ Media Asset Management
+
+
+
+
Titel
@@ -234,10 +371,19 @@ if (isset($_GET['edit'])) {
-
-
+
+
+
+
+
+
+ Telvero Talpa Planning System © 2026
+
+
+
+
+
+
+
+
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
index 056340f..274bc4b 100644
--- a/vendor/composer/installed.php
+++ b/vendor/composer/installed.php
@@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
- 'reference' => '4807b34bad8dee14a59e74f3b55faf2055d70888',
+ 'reference' => 'bfdd27fbccac0f215910524444f38fe5311aaf40',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -13,7 +13,7 @@
'__root__' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
- 'reference' => '4807b34bad8dee14a59e74f3b55faf2055d70888',
+ 'reference' => 'bfdd27fbccac0f215910524444f38fe5311aaf40',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),