From 999e76744b1221a416609cd75e64c68299412a93 Mon Sep 17 00:00:00 2001 From: Mark Pinkster Date: Wed, 14 Jan 2026 11:44:11 +0100 Subject: [PATCH] Planner basic functionality --- .env.example | 17 + INSTALLATION.md | 361 ++++++++++ README.md | 345 +++++++++- TalpaAPI.php | 7 +- api/assign_color.php | 97 +++ api/create_transmission.php | 195 ++++++ api/delete_transmission.php | 60 ++ api/get_block_time.php | 63 ++ api/get_daily_blocks.php | 95 +++ api/get_next_start_time.php | 37 + api/get_transmissions.php | 98 +++ api/insert_transmission_at_position.php | 118 ++++ api/sync_block.php | 172 +++++ api/update_block_time.php | 143 ++++ api/update_transmission.php | 195 ++++++ api/validate_transmission_time.php | 130 ++++ assets/css/custom.css | 486 +++++++++++++ assets/js/calendar-init.js | 685 +++++++++++++++++++ blocks.php | 319 +++++++++ calendar.php | 355 ++++++++++ commercials.php | 437 ++++++++++++ helpers.php | 387 +++++++++++ index.php | 180 ++++- migrations/001_add_blocks_and_colors.sql | 64 ++ planner.php | 825 +++++++++++++++++++++++ vendor/composer/installed.php | 4 +- 26 files changed, 5854 insertions(+), 21 deletions(-) create mode 100644 .env.example create mode 100644 INSTALLATION.md create mode 100644 api/assign_color.php create mode 100644 api/create_transmission.php create mode 100644 api/delete_transmission.php create mode 100644 api/get_block_time.php create mode 100644 api/get_daily_blocks.php create mode 100644 api/get_next_start_time.php create mode 100644 api/get_transmissions.php create mode 100644 api/insert_transmission_at_position.php create mode 100644 api/sync_block.php create mode 100644 api/update_block_time.php create mode 100644 api/update_transmission.php create mode 100644 api/validate_transmission_time.php create mode 100644 assets/css/custom.css create mode 100644 assets/js/calendar-init.js create mode 100644 blocks.php create mode 100644 calendar.php create mode 100644 commercials.php create mode 100644 helpers.php create mode 100644 migrations/001_add_blocks_and_colors.sql create mode 100644 planner.php 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 += `${channel}`; + 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 + +
+ + + + + +
+
+
+
+
+ + +
+
+
+
+ + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + Laat leeg als niet van toepassing +
+ +
+ + + Annuleren + +
+
+
+
+
+ +
+
+
+
Bestaande Templates
+
+
+ +
+ +

Nog geen templates aangemaakt

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ZenderNaamDagStarttijdEindtijdStatusActies
+ + + + Actief + + Inactief + + +
+ + + +
+ + +
+
+ + +
+
+
+
+ +
+
+ +
+
+
Uitleg
+
+
+
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?
+
    +
  1. Maak templates aan voor vaste blokken
  2. +
  3. Het systeem genereert automatisch dagelijkse blokken
  4. +
  5. In de kalender kun je per dag de starttijd aanpassen
  6. +
  7. Uitzendingen worden automatisch in deze blokken gepland
  8. +
+
+
+
+
+
+ + + + 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 + + + + + + + + + + + + + + +
+ +
+ + +
+
+
+
+
+ Actieve Blokken voor Vandaag +
+ +
+
+

Laden...

+
+
+
+
+
+
+
+ Zoom +
+
+
+
+ + + +
+
+ Huidige: 100% +
+
+
+
+
+ +
+ +
+
+
+
+ Beschikbare Commercials +
+
+
+ +
+ +
+ + +
+ +
+ +

Geen commercials beschikbaar

+ + Voeg Commercial Toe + +
+ + +
+ +
+
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+ +
+ + + Sleep commercials naar de kalender om te plannen + +
+
+
+ + +
+
+
Legenda
+
+
+
+ + $color): ?> +
+
+ +
+ +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + 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 + +
+ + + + + + + + + +
+ +
+
+
+
+ Nieuwe Commercial Registreren +
+
+
+
+
+ + +
+ +
+ + + Formaat: UU:MM:SS +
+ +
+ + + Voor groepering in kalender +
+ + + +
+ +
+
+ +
+ + + Dit registreert de commercial bij Talpa en maakt automatisch een media asset aan. + +
+
+
+
+ + +
+
+
+
Geregistreerde Commercials
+
+
+ +
+ +

Nog geen commercials geregistreerd

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KleurTitelDuurSeriesLabelStatusGebruikActies
+
+
+
+ +
+ ID: +
+ + + ' . htmlspecialchars($c['series_code']) . '' : '-' ?> + + + + + Uploaded + + Pending + + + + 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

-
+ + +
+
+

Dashboard

+ + Open Kalender + +
+ + +
+ 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(); + ?> + +
+
+
+
Totaal Commercials
+
+
+ +
+
+
+
Geüpload
+
+
+ +
+
+
+
Geplande Uitzendingen
+
+
+ +
+
+
+
Wacht op Sync
+
+
+
+ +

Snelle Acties

+
+
+
+
+ +
Excel Planner
+

Tabel-gebaseerde planning

+ + Open Planner + +
+
+
+ +
+
+
+ +
Kalender View
+

Visuele tijdlijn planning

+ + Open Kalender + +
+
+
+ +
+
+
+ +
Commercials
+

Registreer en beheer

+ + Naar Commercials + +
+
+
+ +
+
+
+ +
Blok Templates
+

Beheer tijdblokken

+ + Naar Blokken + +
+
+
+
+ +

Recente Activiteit

+ +
1. Commercial Registreren
@@ -160,15 +280,28 @@ if (isset($_GET['edit'])) {
-
+
-

Dagplanning:

-
-
-
-
+

Vandaag's Planning

+
+
+
+
+ +
+
+ +
+ +
- +
@@ -195,11 +328,15 @@ if (isset($_GET['edit'])) { -
TijdProductDuurSync StatusActies
+ +
+
-

Media Asset Management

-
- +

Media Asset Management

+ +
+
+
@@ -234,10 +371,19 @@ if (isset($_GET['edit'])) { -
Titel
-
+ +
+
+
+
+

+ 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(),