Planner basic functionality
This commit is contained in:
parent
bfdd27fbcc
commit
999e76744b
17
.env.example
Normal file
17
.env.example
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Database Configuration
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_NAME=talpa_planning
|
||||||
|
DB_USER=your_database_username
|
||||||
|
DB_PASS=your_database_password
|
||||||
|
|
||||||
|
# Talpa API Configuration
|
||||||
|
TALPA_API_BASE=https://api.talpa.tv
|
||||||
|
TALPA_TOKEN=your_talpa_api_token_here
|
||||||
|
TALPA_MOCK_MODE=false
|
||||||
|
|
||||||
|
# TV Configuration
|
||||||
|
TV_SEASON_ID=your_season_id_here
|
||||||
|
|
||||||
|
# Application Settings
|
||||||
|
APP_ENV=production
|
||||||
|
APP_DEBUG=false
|
||||||
361
INSTALLATION.md
Normal file
361
INSTALLATION.md
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
# Installatie Handleiding - Telvero Talpa Planning System
|
||||||
|
|
||||||
|
## Stap-voor-stap Installatie
|
||||||
|
|
||||||
|
### Stap 1: Vereisten Controleren
|
||||||
|
|
||||||
|
Zorg dat je systeem voldoet aan de volgende vereisten:
|
||||||
|
|
||||||
|
- ✅ PHP 7.4 of hoger
|
||||||
|
- ✅ MySQL 5.7 of hoger
|
||||||
|
- ✅ Composer
|
||||||
|
- ✅ Apache of Nginx webserver
|
||||||
|
- ✅ PHP extensies: PDO, PDO_MySQL, cURL, JSON
|
||||||
|
|
||||||
|
**Controleer PHP versie:**
|
||||||
|
```bash
|
||||||
|
php -v
|
||||||
|
```
|
||||||
|
|
||||||
|
**Controleer Composer:**
|
||||||
|
```bash
|
||||||
|
composer --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stap 2: Project Setup
|
||||||
|
|
||||||
|
**2.1 Navigeer naar project directory:**
|
||||||
|
```bash
|
||||||
|
cd /Users/mark/Documents/GIT\ Projects/telvero_whatson_talpa
|
||||||
|
```
|
||||||
|
|
||||||
|
**2.2 Installeer PHP dependencies:**
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
Als je een foutmelding krijgt over ontbrekende dependencies, voer dan uit:
|
||||||
|
```bash
|
||||||
|
composer update
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stap 3: Database Aanmaken
|
||||||
|
|
||||||
|
**3.1 Open MySQL:**
|
||||||
|
```bash
|
||||||
|
mysql -u root -p
|
||||||
|
```
|
||||||
|
|
||||||
|
**3.2 Maak database aan:**
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE talpa_planning CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
**3.3 Maak database gebruiker aan (optioneel maar aanbevolen):**
|
||||||
|
```sql
|
||||||
|
CREATE USER 'talpa_user'@'localhost' IDENTIFIED BY 'jouw_wachtwoord';
|
||||||
|
GRANT ALL PRIVILEGES ON talpa_planning.* TO 'talpa_user'@'localhost';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
EXIT;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stap 4: Database Migratie
|
||||||
|
|
||||||
|
**4.1 Voer migratie uit:**
|
||||||
|
```bash
|
||||||
|
mysql -u talpa_user -p talpa_planning < migrations/001_add_blocks_and_colors.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
**4.2 Controleer of tabellen zijn aangemaakt:**
|
||||||
|
```bash
|
||||||
|
mysql -u talpa_user -p talpa_planning -e "SHOW TABLES;"
|
||||||
|
```
|
||||||
|
|
||||||
|
Je zou moeten zien:
|
||||||
|
- block_templates
|
||||||
|
- commercials
|
||||||
|
- daily_blocks
|
||||||
|
- transmissions
|
||||||
|
|
||||||
|
### Stap 5: Environment Configuratie
|
||||||
|
|
||||||
|
**5.1 Kopieer environment file:**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**5.2 Bewerk .env file:**
|
||||||
|
```bash
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Of open met je favoriete editor en vul in:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Database
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_NAME=talpa_planning
|
||||||
|
DB_USER=talpa_user
|
||||||
|
DB_PASS=jouw_wachtwoord
|
||||||
|
|
||||||
|
# Talpa API
|
||||||
|
TALPA_API_BASE=https://api.talpa.tv
|
||||||
|
TALPA_TOKEN=jouw_api_token
|
||||||
|
TALPA_MOCK_MODE=false
|
||||||
|
|
||||||
|
# TV Settings
|
||||||
|
TV_SEASON_ID=jouw_season_id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Belangrijk:** Vraag de juiste API credentials aan bij je Talpa contactpersoon.
|
||||||
|
|
||||||
|
### Stap 6: Bestandspermissies
|
||||||
|
|
||||||
|
**6.1 Zet correcte permissies:**
|
||||||
|
```bash
|
||||||
|
chmod 755 api/
|
||||||
|
chmod 755 assets/
|
||||||
|
chmod 755 migrations/
|
||||||
|
chmod 644 api/*.php
|
||||||
|
chmod 644 assets/css/*.css
|
||||||
|
chmod 644 assets/js/*.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**6.2 Maak log file aan:**
|
||||||
|
```bash
|
||||||
|
touch api_log.txt
|
||||||
|
chmod 666 api_log.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stap 7: Webserver Configuratie
|
||||||
|
|
||||||
|
#### Voor Apache:
|
||||||
|
|
||||||
|
**7.1 Maak .htaccess aan (indien nog niet aanwezig):**
|
||||||
|
```apache
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteBase /
|
||||||
|
|
||||||
|
# Redirect to HTTPS (productie)
|
||||||
|
# RewriteCond %{HTTPS} off
|
||||||
|
# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Prevent directory listing
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Protect .env file
|
||||||
|
<Files .env>
|
||||||
|
Order allow,deny
|
||||||
|
Deny from all
|
||||||
|
</Files>
|
||||||
|
```
|
||||||
|
|
||||||
|
**7.2 Herstart Apache:**
|
||||||
|
```bash
|
||||||
|
sudo apachectl restart
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Voor Nginx:
|
||||||
|
|
||||||
|
**7.1 Voeg toe aan nginx config:**
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
root /path/to/telvero_whatson_talpa;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.env {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**7.2 Herstart Nginx:**
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stap 8: Test de Installatie
|
||||||
|
|
||||||
|
**8.1 Open in browser:**
|
||||||
|
```
|
||||||
|
http://localhost/telvero_whatson_talpa/
|
||||||
|
```
|
||||||
|
|
||||||
|
Of als je een virtuele host hebt ingesteld:
|
||||||
|
```
|
||||||
|
http://talpa-planning.local/
|
||||||
|
```
|
||||||
|
|
||||||
|
**8.2 Controleer of je het dashboard ziet met:**
|
||||||
|
- Navigatie menu (Dashboard, Kalender, Blokken, Commercials)
|
||||||
|
- Statistieken cards
|
||||||
|
- Snelle acties
|
||||||
|
|
||||||
|
**8.3 Test database connectie:**
|
||||||
|
- Ga naar "Blokken"
|
||||||
|
- Je zou de standaard templates moeten zien (SBS9, Net5)
|
||||||
|
|
||||||
|
### Stap 9: Eerste Gebruik
|
||||||
|
|
||||||
|
**9.1 Controleer Blok Templates:**
|
||||||
|
- Ga naar **Blokken**
|
||||||
|
- Verifieer dat de standaard templates zijn aangemaakt:
|
||||||
|
- SBS9 Dagblok (07:00-15:00)
|
||||||
|
- SBS9 Nachtblok (23:30-02:00)
|
||||||
|
- Net5 Ochtendblok (07:30-11:30)
|
||||||
|
- Net5 Middagblok (13:20-13:50)
|
||||||
|
|
||||||
|
**9.2 Test Commercial Registratie:**
|
||||||
|
- Ga naar **Commercials**
|
||||||
|
- Probeer een test commercial aan te maken:
|
||||||
|
- Titel: "Test Product"
|
||||||
|
- Duur: 00:30:00
|
||||||
|
- Series Code: TEST01
|
||||||
|
- Als `TALPA_MOCK_MODE=true` staat, zou dit moeten werken zonder echte API
|
||||||
|
|
||||||
|
**9.3 Test Kalender:**
|
||||||
|
- Ga naar **Kalender**
|
||||||
|
- Controleer of de kalender laadt
|
||||||
|
- Probeer een commercial te slepen (als je er een hebt aangemaakt)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Probleem: "Class 'Dotenv\Dotenv' not found"
|
||||||
|
|
||||||
|
**Oplossing:**
|
||||||
|
```bash
|
||||||
|
composer require vlucas/phpdotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
### Probleem: Database connectie fout
|
||||||
|
|
||||||
|
**Oplossing:**
|
||||||
|
1. Controleer `.env` credentials
|
||||||
|
2. Test database connectie:
|
||||||
|
```bash
|
||||||
|
mysql -u talpa_user -p talpa_planning -e "SELECT 1;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Probleem: Kalender laadt niet
|
||||||
|
|
||||||
|
**Oplossing:**
|
||||||
|
1. Open browser console (F12)
|
||||||
|
2. Check voor JavaScript errors
|
||||||
|
3. Controleer of FullCalendar CDN bereikbaar is
|
||||||
|
4. Test API endpoint:
|
||||||
|
```bash
|
||||||
|
curl http://localhost/telvero_whatson_talpa/api/get_transmissions.php?start=2026-01-01&end=2026-01-31
|
||||||
|
```
|
||||||
|
|
||||||
|
### Probleem: Permissie errors
|
||||||
|
|
||||||
|
**Oplossing:**
|
||||||
|
```bash
|
||||||
|
# Geef webserver eigenaarschap
|
||||||
|
sudo chown -R www-data:www-data /path/to/project
|
||||||
|
|
||||||
|
# Of voor development
|
||||||
|
sudo chown -R $USER:www-data /path/to/project
|
||||||
|
```
|
||||||
|
|
||||||
|
### Probleem: API errors
|
||||||
|
|
||||||
|
**Oplossing:**
|
||||||
|
1. Zet `TALPA_MOCK_MODE=true` in `.env` voor testen
|
||||||
|
2. Check `api_log.txt` voor details:
|
||||||
|
```bash
|
||||||
|
tail -f api_log.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Productie Deployment
|
||||||
|
|
||||||
|
### Extra stappen voor productie:
|
||||||
|
|
||||||
|
**1. Beveilig .env:**
|
||||||
|
```bash
|
||||||
|
chmod 600 .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Schakel debugging uit:**
|
||||||
|
```env
|
||||||
|
APP_DEBUG=false
|
||||||
|
TALPA_MOCK_MODE=false
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Gebruik HTTPS:**
|
||||||
|
- Installeer SSL certificaat
|
||||||
|
- Forceer HTTPS in .htaccess
|
||||||
|
|
||||||
|
**4. Database backup:**
|
||||||
|
```bash
|
||||||
|
# Maak backup script
|
||||||
|
cat > backup.sh << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
mysqldump -u talpa_user -p talpa_planning > backups/backup_$DATE.sql
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. Monitoring:**
|
||||||
|
- Setup error logging
|
||||||
|
- Monitor `api_log.txt`
|
||||||
|
- Setup uptime monitoring
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
Bij toekomstige updates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull laatste wijzigingen
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Update dependencies
|
||||||
|
composer update
|
||||||
|
|
||||||
|
# Voer nieuwe migraties uit
|
||||||
|
mysql -u talpa_user -p talpa_planning < migrations/002_nieuwe_migratie.sql
|
||||||
|
|
||||||
|
# Clear cache (indien van toepassing)
|
||||||
|
# php artisan cache:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Bij problemen:
|
||||||
|
1. Check deze installatie handleiding
|
||||||
|
2. Bekijk [README.md](README.md) voor gebruik
|
||||||
|
3. Check `api_log.txt` voor API errors
|
||||||
|
4. Bekijk browser console voor JavaScript errors
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] PHP 7.4+ geïnstalleerd
|
||||||
|
- [ ] MySQL database aangemaakt
|
||||||
|
- [ ] Composer dependencies geïnstalleerd
|
||||||
|
- [ ] Database migratie uitgevoerd
|
||||||
|
- [ ] .env file geconfigureerd
|
||||||
|
- [ ] Bestandspermissies ingesteld
|
||||||
|
- [ ] Webserver geconfigureerd
|
||||||
|
- [ ] Dashboard bereikbaar in browser
|
||||||
|
- [ ] Blok templates zichtbaar
|
||||||
|
- [ ] Test commercial aangemaakt
|
||||||
|
- [ ] Kalender laadt correct
|
||||||
|
|
||||||
|
Als alle items zijn afgevinkt, is de installatie succesvol! 🎉
|
||||||
345
README.md
345
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**
|
||||||
|
|||||||
@ -100,9 +100,14 @@ class TalpaApi {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteEpisode($contentId) {
|
||||||
|
return $this->request('DELETE', '/content/v1/episodes/' . $contentId);
|
||||||
|
}
|
||||||
|
|
||||||
private function getMockResponse($endpoint, $data) {
|
private function getMockResponse($endpoint, $data) {
|
||||||
usleep(200000);
|
usleep(200000);
|
||||||
if (strpos($endpoint, '/content/v1/episodes') !== false) return ["id" => "MOCK_CONT_" . time()];
|
if (strpos($endpoint, '/content/v1/episodes') !== false && strpos($endpoint, 'DELETE') === false) return ["id" => "MOCK_CONT_" . time()];
|
||||||
|
if (strpos($endpoint, '/content/v1/episodes') !== false) return ["statusCode" => "200", "message" => "Episode deleted"];
|
||||||
if (strpos($endpoint, '/mam/v1/mediaAssets') !== false && !isset($data)) return ["mediaAssetLabel" => "TEL_MOCK_" . rand(100, 999)];
|
if (strpos($endpoint, '/mam/v1/mediaAssets') !== false && !isset($data)) return ["mediaAssetLabel" => "TEL_MOCK_" . rand(100, 999)];
|
||||||
if (strpos($endpoint, '/mam/v1/mediaAssets') !== false) return ["id" => "MOCK_ASSET_" . time()];
|
if (strpos($endpoint, '/mam/v1/mediaAssets') !== false) return ["id" => "MOCK_ASSET_" . time()];
|
||||||
if (strpos($endpoint, '/linearSchedule/v1/transmissions') !== false) return ["statusCode" => "201", "id" => "MOCK_TX_" . time()];
|
if (strpos($endpoint, '/linearSchedule/v1/transmissions') !== false) return ["statusCode" => "201", "id" => "MOCK_TX_" . time()];
|
||||||
|
|||||||
97
api/assign_color.php
Normal file
97
api/assign_color.php
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Assign Color to Commercial
|
||||||
|
* Assigns or updates color code for a commercial
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Get POST data
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (empty($input['commercial_id'])) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Missing commercial_id'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if commercial exists
|
||||||
|
$stmt = $db->prepare("SELECT id, color_code FROM commercials WHERE id = ?");
|
||||||
|
$stmt->execute([$input['commercial_id']]);
|
||||||
|
$commercial = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$commercial) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Commercial not found'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine color
|
||||||
|
$colorCode = null;
|
||||||
|
|
||||||
|
if (!empty($input['color_code'])) {
|
||||||
|
// Use provided color
|
||||||
|
$colorCode = $input['color_code'];
|
||||||
|
|
||||||
|
// Validate hex format
|
||||||
|
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $colorCode)) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Invalid color format. Use hex format like #FF5733'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Auto-generate distinct color
|
||||||
|
$stmt = $db->query("SELECT color_code FROM commercials WHERE color_code IS NOT NULL");
|
||||||
|
$existingColors = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
$colorCode = generateDistinctColor($existingColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update commercial
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE commercials
|
||||||
|
SET color_code = ?, series_code = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$colorCode,
|
||||||
|
$input['series_code'] ?? null,
|
||||||
|
$input['commercial_id']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get updated commercial
|
||||||
|
$stmt = $db->prepare("SELECT * FROM commercials WHERE id = ?");
|
||||||
|
$stmt->execute([$input['commercial_id']]);
|
||||||
|
$updated = $stmt->fetch();
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Color assigned successfully',
|
||||||
|
'commercial' => $updated
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
195
api/create_transmission.php
Normal file
195
api/create_transmission.php
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Create Transmission
|
||||||
|
* Creates a new transmission (planning entry)
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Get POST data
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
$required = ['commercial_id', 'channel', 'start_date', 'start_time'];
|
||||||
|
foreach ($required as $field) {
|
||||||
|
if (empty($input[$field])) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => "Missing required field: $field"
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date and time formats
|
||||||
|
if (!isValidDate($input['start_date'])) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Invalid date format'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidTime($input['start_time'])) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Invalid time format'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get commercial details (duration)
|
||||||
|
$stmt = $db->prepare("SELECT duration, title FROM commercials WHERE id = ?");
|
||||||
|
$stmt->execute([$input['commercial_id']]);
|
||||||
|
$commercial = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$commercial) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Commercial not found'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get template from input or use default
|
||||||
|
$template = $input['template'] ?? 'HOME030';
|
||||||
|
|
||||||
|
// Calculate end time
|
||||||
|
$endTime = addTimeToTime($input['start_time'], $commercial['duration']);
|
||||||
|
|
||||||
|
// Validate that transmission falls within a block (inline to avoid curl issues)
|
||||||
|
ensureDailyBlocks($db, $input['start_date'], $input['start_date']);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT * FROM daily_blocks
|
||||||
|
WHERE block_date = ? AND channel = ?
|
||||||
|
ORDER BY actual_start_time
|
||||||
|
");
|
||||||
|
$stmt->execute([$input['start_date'], $input['channel']]);
|
||||||
|
$blocks = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$withinBlock = false;
|
||||||
|
foreach ($blocks as $block) {
|
||||||
|
$blockStart = $block['actual_start_time'];
|
||||||
|
$blockEnd = $block['actual_end_time'] ?? '23:59:59';
|
||||||
|
|
||||||
|
// Handle overnight blocks
|
||||||
|
$isOvernight = $blockEnd < $blockStart;
|
||||||
|
|
||||||
|
if ($isOvernight) {
|
||||||
|
if ($input['start_time'] >= $blockStart || $input['start_time'] < $blockEnd) {
|
||||||
|
if ($endTime <= $blockEnd || $endTime >= $blockStart) {
|
||||||
|
$withinBlock = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($input['start_time'] >= $blockStart && $endTime <= $blockEnd) {
|
||||||
|
$withinBlock = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$withinBlock) {
|
||||||
|
$errorMessage = "Uitzending valt buiten blok tijden. ";
|
||||||
|
if (!empty($blocks)) {
|
||||||
|
$errorMessage .= "Beschikbare blokken: ";
|
||||||
|
foreach ($blocks as $block) {
|
||||||
|
$blockStart = substr($block['actual_start_time'], 0, 5);
|
||||||
|
$blockEnd = $block['actual_end_time'] ? substr($block['actual_end_time'], 0, 5) : '∞';
|
||||||
|
$errorMessage .= "{$blockStart}-{$blockEnd}, ";
|
||||||
|
}
|
||||||
|
$errorMessage = rtrim($errorMessage, ', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $errorMessage,
|
||||||
|
'attempted_time' => $input['start_time'] . ' - ' . $endTime
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for overlapping transmissions
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.id, t.start_time, t.duration, c.title
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.start_date = ?
|
||||||
|
AND t.channel = ?
|
||||||
|
AND t.id != ?
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$input['start_date'],
|
||||||
|
$input['channel'],
|
||||||
|
$input['id'] ?? 0
|
||||||
|
]);
|
||||||
|
$existing = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($existing as $tx) {
|
||||||
|
$txEnd = addTimeToTime($tx['start_time'], $tx['duration']);
|
||||||
|
if (timeRangesOverlap($input['start_time'], $endTime, $tx['start_time'], $txEnd)) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => "Overlap detected with: {$tx['title']} ({$tx['start_time']} - {$txEnd})",
|
||||||
|
'overlap' => true
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert transmission
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO transmissions
|
||||||
|
(commercial_id, channel, template, start_date, start_time, duration, api_status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, 'pending')
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$input['commercial_id'],
|
||||||
|
$input['channel'],
|
||||||
|
$template,
|
||||||
|
$input['start_date'],
|
||||||
|
$input['start_time'],
|
||||||
|
$commercial['duration']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$transmissionId = $db->lastInsertId();
|
||||||
|
|
||||||
|
// Get the created transmission with all details
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
c.title,
|
||||||
|
c.color_code,
|
||||||
|
c.series_code
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$transmissionId]);
|
||||||
|
$transmission = $stmt->fetch();
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Transmission created successfully',
|
||||||
|
'transmission' => $transmission
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
60
api/delete_transmission.php
Normal file
60
api/delete_transmission.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Delete Transmission
|
||||||
|
* Deletes a transmission from the schedule
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Get POST data
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (empty($input['id'])) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Missing transmission ID'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if transmission exists
|
||||||
|
$stmt = $db->prepare("SELECT id FROM transmissions WHERE id = ?");
|
||||||
|
$stmt->execute([$input['id']]);
|
||||||
|
|
||||||
|
if (!$stmt->fetch()) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Transmission not found'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete transmission
|
||||||
|
$stmt = $db->prepare("DELETE FROM transmissions WHERE id = ?");
|
||||||
|
$stmt->execute([$input['id']]);
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Transmission deleted successfully'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
63
api/get_block_time.php
Normal file
63
api/get_block_time.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Get Block Time
|
||||||
|
* Returns the start time for a specific daily block
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
$date = $_GET['date'] ?? null;
|
||||||
|
$channel = $_GET['channel'] ?? null;
|
||||||
|
|
||||||
|
if (!$date || !$channel) {
|
||||||
|
jsonResponse([
|
||||||
|
'error' => 'Missing date or channel parameter'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get daily block
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT actual_start_time as start_time, actual_end_time as end_time
|
||||||
|
FROM daily_blocks
|
||||||
|
WHERE block_date = ? AND channel = ?
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute([$date, $channel]);
|
||||||
|
$block = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($block) {
|
||||||
|
jsonResponse($block);
|
||||||
|
} else {
|
||||||
|
// Return default from template
|
||||||
|
$dayOfWeek = ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'][date('w', strtotime($date))];
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT default_start_time as start_time, default_end_time as end_time
|
||||||
|
FROM block_templates
|
||||||
|
WHERE channel = ?
|
||||||
|
AND (day_of_week = ? OR day_of_week = 'all')
|
||||||
|
AND is_active = 1
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute([$channel, $dayOfWeek]);
|
||||||
|
$template = $stmt->fetch();
|
||||||
|
|
||||||
|
jsonResponse($template ?: ['start_time' => '07:00:00', 'end_time' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
95
api/get_daily_blocks.php
Normal file
95
api/get_daily_blocks.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Get Daily Blocks
|
||||||
|
* Returns active blocks for a specific date
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
$date = $_GET['date'] ?? date('Y-m-d');
|
||||||
|
|
||||||
|
if (!isValidDate($date)) {
|
||||||
|
jsonResponse([
|
||||||
|
'error' => 'Invalid date format'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure daily blocks exist for this date
|
||||||
|
ensureDailyBlocks($db, $date, $date);
|
||||||
|
|
||||||
|
// Get all blocks for this date
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT
|
||||||
|
db.*,
|
||||||
|
bt.name as template_name,
|
||||||
|
bt.channel
|
||||||
|
FROM daily_blocks db
|
||||||
|
LEFT JOIN block_templates bt ON db.template_id = bt.id
|
||||||
|
WHERE db.block_date = ?
|
||||||
|
ORDER BY db.channel, db.actual_start_time
|
||||||
|
");
|
||||||
|
$stmt->execute([$date]);
|
||||||
|
$blocks = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Format for calendar background events - assign to correct resource
|
||||||
|
$backgroundEvents = [];
|
||||||
|
foreach ($blocks as $block) {
|
||||||
|
$startDateTime = $date . 'T' . $block['actual_start_time'];
|
||||||
|
$endDateTime = $date . 'T' . ($block['actual_end_time'] ?? '23:59:59');
|
||||||
|
|
||||||
|
// Handle overnight blocks
|
||||||
|
if ($block['actual_end_time'] && $block['actual_end_time'] < $block['actual_start_time']) {
|
||||||
|
$endDateTime = date('Y-m-d', strtotime($date . ' +1 day')) . 'T' . $block['actual_end_time'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$backgroundEvents[] = [
|
||||||
|
'id' => 'block_' . $block['id'],
|
||||||
|
'title' => $block['template_name'] ?? 'Blok',
|
||||||
|
'start' => $startDateTime,
|
||||||
|
'end' => $endDateTime,
|
||||||
|
'resourceId' => $block['channel'], // Assign to specific resource/channel
|
||||||
|
'display' => 'background',
|
||||||
|
'backgroundColor' => getBlockColor($block['channel']),
|
||||||
|
'extendedProps' => [
|
||||||
|
'type' => 'block',
|
||||||
|
'block_id' => $block['id'],
|
||||||
|
'channel' => $block['channel'],
|
||||||
|
'template_name' => $block['template_name']
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'blocks' => $blocks,
|
||||||
|
'backgroundEvents' => $backgroundEvents
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color for block background based on channel
|
||||||
|
*/
|
||||||
|
function getBlockColor($channel) {
|
||||||
|
$colors = [
|
||||||
|
'SBS9' => 'rgba(52, 152, 219, 0.1)', // Light blue
|
||||||
|
'NET5' => 'rgba(231, 76, 60, 0.1)', // Light red
|
||||||
|
'VERONICA' => 'rgba(155, 89, 182, 0.1)' // Light purple
|
||||||
|
];
|
||||||
|
|
||||||
|
return $colors[$channel] ?? 'rgba(200, 200, 200, 0.1)';
|
||||||
|
}
|
||||||
37
api/get_next_start_time.php
Normal file
37
api/get_next_start_time.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Get Next Start Time
|
||||||
|
* Calculates the next available start time based on existing transmissions
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
$date = $_GET['date'] ?? date('Y-m-d');
|
||||||
|
$channel = $_GET['channel'] ?? 'SBS9';
|
||||||
|
$requestedTime = $_GET['requested_time'] ?? null;
|
||||||
|
|
||||||
|
// Get next start time using helper function
|
||||||
|
$nextStartTime = calculateNextStartTime($db, $date, $channel, $requestedTime);
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'next_start_time' => $nextStartTime,
|
||||||
|
'date' => $date,
|
||||||
|
'channel' => $channel
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
98
api/get_transmissions.php
Normal file
98
api/get_transmissions.php
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Get Transmissions
|
||||||
|
* Returns transmissions for calendar view in FullCalendar format
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Get date range from query parameters
|
||||||
|
$start = $_GET['start'] ?? date('Y-m-d', strtotime('-7 days'));
|
||||||
|
$end = $_GET['end'] ?? date('Y-m-d', strtotime('+30 days'));
|
||||||
|
$channel = $_GET['channel'] ?? null;
|
||||||
|
|
||||||
|
// Ensure daily blocks exist for this date range
|
||||||
|
ensureDailyBlocks($db, $start, $end);
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.commercial_id,
|
||||||
|
t.channel,
|
||||||
|
t.template,
|
||||||
|
t.start_date,
|
||||||
|
t.start_time,
|
||||||
|
t.duration,
|
||||||
|
t.api_status,
|
||||||
|
c.title,
|
||||||
|
c.color_code,
|
||||||
|
c.series_code
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.start_date BETWEEN ? AND ?
|
||||||
|
";
|
||||||
|
|
||||||
|
$params = [$start, $end];
|
||||||
|
|
||||||
|
if ($channel) {
|
||||||
|
$sql .= " AND t.channel = ?";
|
||||||
|
$params[] = $channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY t.start_date, t.start_time";
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$transmissions = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Format for FullCalendar
|
||||||
|
$events = [];
|
||||||
|
foreach ($transmissions as $tx) {
|
||||||
|
// Calculate end time
|
||||||
|
$startDateTime = new DateTime($tx['start_date'] . ' ' . $tx['start_time']);
|
||||||
|
$endDateTime = clone $startDateTime;
|
||||||
|
|
||||||
|
if ($tx['duration']) {
|
||||||
|
list($h, $m, $s) = explode(':', $tx['duration']);
|
||||||
|
$endDateTime->add(new DateInterval("PT{$h}H{$m}M{$s}S"));
|
||||||
|
}
|
||||||
|
|
||||||
|
$events[] = [
|
||||||
|
'id' => $tx['id'],
|
||||||
|
'title' => $tx['title'],
|
||||||
|
'start' => $startDateTime->format('Y-m-d\TH:i:s'),
|
||||||
|
'end' => $endDateTime->format('Y-m-d\TH:i:s'),
|
||||||
|
'resourceId' => $tx['channel'],
|
||||||
|
'backgroundColor' => $tx['color_code'] ?? '#cccccc',
|
||||||
|
'borderColor' => $tx['color_code'] ?? '#cccccc',
|
||||||
|
'textColor' => '#ffffff',
|
||||||
|
'extendedProps' => [
|
||||||
|
'commercial_id' => $tx['commercial_id'],
|
||||||
|
'template' => $tx['template'],
|
||||||
|
'duration' => $tx['duration'],
|
||||||
|
'api_status' => $tx['api_status'],
|
||||||
|
'series_code' => $tx['series_code']
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse($events);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'error' => true,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
118
api/insert_transmission_at_position.php
Normal file
118
api/insert_transmission_at_position.php
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Insert Transmission at Position
|
||||||
|
* Inserts a transmission at a specific position in the block
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Get POST data
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
$required = ['commercial_id', 'channel', 'date', 'block_id', 'position'];
|
||||||
|
foreach ($required as $field) {
|
||||||
|
if (!isset($input[$field])) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => "Missing required field: $field"
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get commercial duration
|
||||||
|
$stmt = $db->prepare("SELECT duration FROM commercials WHERE id = ?");
|
||||||
|
$stmt->execute([$input['commercial_id']]);
|
||||||
|
$duration = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if (!$duration) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Commercial not found'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get block start and end time
|
||||||
|
$stmt = $db->prepare("SELECT actual_start_time, actual_end_time FROM daily_blocks WHERE id = ?");
|
||||||
|
$stmt->execute([$input['block_id']]);
|
||||||
|
$blockInfo = $stmt->fetch();
|
||||||
|
$blockStartTime = $blockInfo['actual_start_time'];
|
||||||
|
$blockEndTime = $blockInfo['actual_end_time'] ?? '23:59:59';
|
||||||
|
|
||||||
|
// Get all transmissions in THIS SPECIFIC block only
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.id, t.start_time, c.duration
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.start_date = ? AND t.channel = ?
|
||||||
|
AND t.start_time >= ? AND t.start_time < ?
|
||||||
|
ORDER BY t.start_time ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$input['date'], $input['channel'], $blockStartTime, $blockEndTime]);
|
||||||
|
$transmissions = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Insert new transmission at position
|
||||||
|
$position = (int)$input['position'];
|
||||||
|
|
||||||
|
// Create temporary transmission entry
|
||||||
|
$newTransmission = [
|
||||||
|
'commercial_id' => $input['commercial_id'],
|
||||||
|
'duration' => $duration
|
||||||
|
];
|
||||||
|
|
||||||
|
// Insert at position
|
||||||
|
array_splice($transmissions, $position, 0, [$newTransmission]);
|
||||||
|
|
||||||
|
// Recalculate all start times
|
||||||
|
$currentTime = $blockStartTime;
|
||||||
|
|
||||||
|
foreach ($transmissions as $index => $tx) {
|
||||||
|
if ($index === $position) {
|
||||||
|
// Insert new transmission
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO transmissions
|
||||||
|
(commercial_id, channel, template, start_date, start_time, duration, api_status)
|
||||||
|
VALUES (?, ?, 'HOME030', ?, ?, ?, 'pending')
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$input['commercial_id'],
|
||||||
|
$input['channel'],
|
||||||
|
$input['date'],
|
||||||
|
$currentTime,
|
||||||
|
$duration
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Update existing transmission
|
||||||
|
$stmt = $db->prepare("UPDATE transmissions SET start_time = ?, api_status = 'pending' WHERE id = ?");
|
||||||
|
$stmt->execute([$currentTime, $tx['id']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentTime = addTimeToTime($currentTime, $tx['duration']);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Transmission inserted successfully'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
172
api/sync_block.php
Normal file
172
api/sync_block.php
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Sync Block to Talpa
|
||||||
|
* Synchronizes all transmissions in a specific block to Talpa API
|
||||||
|
*/
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../TalpaAPI.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$api = new TalpaApi();
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
$debugLog = [];
|
||||||
|
$debugLog[] = ['step' => 'Start', 'time' => date('Y-m-d H:i:s')];
|
||||||
|
|
||||||
|
// Get POST data
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$debugLog[] = ['step' => 'Input received', 'data' => $input];
|
||||||
|
|
||||||
|
if (!isset($input['date']) || !isset($input['channel'])) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Datum en channel zijn verplicht',
|
||||||
|
'debug' => $debugLog
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$date = $input['date'];
|
||||||
|
$channel = $input['channel'];
|
||||||
|
$debugLog[] = ['step' => 'Parameters', 'date' => $date, 'channel' => $channel];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all transmissions for this block that are not yet synced
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.*, c.content_id, c.title as commercial_title
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.start_date = ?
|
||||||
|
AND t.channel = ?
|
||||||
|
AND t.api_status != 'synced'
|
||||||
|
ORDER BY t.start_time ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$date, $channel]);
|
||||||
|
$transmissions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$debugLog[] = ['step' => 'Query executed', 'count' => count($transmissions)];
|
||||||
|
|
||||||
|
if (empty($transmissions)) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Geen uitzendingen om te synchroniseren',
|
||||||
|
'synced' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
'debug' => $debugLog
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$syncedCount = 0;
|
||||||
|
$failedCount = 0;
|
||||||
|
$errors = [];
|
||||||
|
$apiCalls = [];
|
||||||
|
|
||||||
|
// Sync each transmission
|
||||||
|
foreach ($transmissions as $tx) {
|
||||||
|
$txDebug = [
|
||||||
|
'transmission_id' => $tx['id'],
|
||||||
|
'title' => $tx['commercial_title'],
|
||||||
|
'time' => $tx['start_time']
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$requestData = [
|
||||||
|
"channel" => $tx['channel'],
|
||||||
|
"template" => $tx['template'],
|
||||||
|
"startDate" => $tx['start_date'],
|
||||||
|
"startTime" => $tx['start_time'],
|
||||||
|
"duration" => $tx['duration'],
|
||||||
|
"contentId" => $tx['content_id']
|
||||||
|
];
|
||||||
|
|
||||||
|
$txDebug['request'] = $requestData;
|
||||||
|
|
||||||
|
$res = $api->createTransmission($requestData);
|
||||||
|
|
||||||
|
$txDebug['response'] = $res;
|
||||||
|
$txDebug['lastResponse'] = $api->lastResponse;
|
||||||
|
|
||||||
|
// Check if sync was successful
|
||||||
|
$status = (isset($res['id']) || (isset($res['statusCode']) && $res['statusCode'] == 201)) ? 'synced' : 'error';
|
||||||
|
|
||||||
|
$txDebug['determined_status'] = $status;
|
||||||
|
|
||||||
|
// Update transmission status
|
||||||
|
$updateStmt = $db->prepare("
|
||||||
|
UPDATE transmissions
|
||||||
|
SET api_status = ?, api_response = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$updateStmt->execute([$status, json_encode($res), $tx['id']]);
|
||||||
|
|
||||||
|
if ($status === 'synced') {
|
||||||
|
$syncedCount++;
|
||||||
|
$txDebug['result'] = 'success';
|
||||||
|
} else {
|
||||||
|
$failedCount++;
|
||||||
|
$txDebug['result'] = 'failed';
|
||||||
|
$errors[] = [
|
||||||
|
'transmission_id' => $tx['id'],
|
||||||
|
'title' => $tx['commercial_title'],
|
||||||
|
'time' => $tx['start_time'],
|
||||||
|
'error' => $res['message'] ?? json_encode($res)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$failedCount++;
|
||||||
|
$txDebug['result'] = 'exception';
|
||||||
|
$txDebug['exception'] = $e->getMessage();
|
||||||
|
|
||||||
|
$errors[] = [
|
||||||
|
'transmission_id' => $tx['id'],
|
||||||
|
'title' => $tx['commercial_title'],
|
||||||
|
'time' => $tx['start_time'],
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
];
|
||||||
|
|
||||||
|
// Update status to error
|
||||||
|
$updateStmt = $db->prepare("
|
||||||
|
UPDATE transmissions
|
||||||
|
SET api_status = 'error', api_response = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$updateStmt->execute([json_encode(['error' => $e->getMessage()]), $tx['id']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiCalls[] = $txDebug;
|
||||||
|
}
|
||||||
|
|
||||||
|
$debugLog[] = ['step' => 'Sync completed', 'synced' => $syncedCount, 'failed' => $failedCount];
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => "Synchronisatie voltooid: {$syncedCount} geslaagd, {$failedCount} mislukt",
|
||||||
|
'synced' => $syncedCount,
|
||||||
|
'failed' => $failedCount,
|
||||||
|
'errors' => $errors,
|
||||||
|
'debug' => $debugLog,
|
||||||
|
'api_calls' => $apiCalls
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$debugLog[] = ['step' => 'Exception caught', 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString()];
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Database fout: ' . $e->getMessage(),
|
||||||
|
'debug' => $debugLog
|
||||||
|
]);
|
||||||
|
}
|
||||||
143
api/update_block_time.php
Normal file
143
api/update_block_time.php
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Update Block Time
|
||||||
|
* Updates the start time for a daily block
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Get POST data
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
$required = ['date', 'channel', 'start_time'];
|
||||||
|
foreach ($required as $field) {
|
||||||
|
if (empty($input[$field])) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => "Missing required field: $field"
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate formats
|
||||||
|
if (!isValidDate($input['date'])) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Invalid date format'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidTime($input['start_time'])) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Invalid time format'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get template_id if provided, or find matching template
|
||||||
|
$templateId = $input['template_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$templateId) {
|
||||||
|
// Try to find matching template
|
||||||
|
$dayOfWeek = ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'][date('w', strtotime($input['date']))];
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT id FROM block_templates
|
||||||
|
WHERE channel = ?
|
||||||
|
AND (day_of_week = ? OR day_of_week = 'all')
|
||||||
|
AND is_active = 1
|
||||||
|
ORDER BY day_of_week DESC
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute([$input['channel'], $dayOfWeek]);
|
||||||
|
$templateId = $stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if daily block exists for this template
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT id FROM daily_blocks
|
||||||
|
WHERE block_date = ? AND channel = ? AND template_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$input['date'], $input['channel'], $templateId]);
|
||||||
|
$block = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($block) {
|
||||||
|
// Update existing block
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE daily_blocks
|
||||||
|
SET actual_start_time = ?, updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$input['start_time'], $block['id']]);
|
||||||
|
} else {
|
||||||
|
// Update or insert - use ON DUPLICATE KEY UPDATE
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO daily_blocks
|
||||||
|
(template_id, channel, block_date, actual_start_time)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
actual_start_time = VALUES(actual_start_time),
|
||||||
|
updated_at = NOW()
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$templateId,
|
||||||
|
$input['channel'],
|
||||||
|
$input['date'],
|
||||||
|
$input['start_time']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally recalculate all transmission times in this block
|
||||||
|
if (!empty($input['recalculate'])) {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.id, t.start_time, c.duration
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.start_date = ? AND t.channel = ?
|
||||||
|
ORDER BY t.start_time ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$input['date'], $input['channel']]);
|
||||||
|
$transmissions = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$currentTime = $input['start_time'];
|
||||||
|
|
||||||
|
foreach ($transmissions as $tx) {
|
||||||
|
// Update transmission start time
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE transmissions
|
||||||
|
SET start_time = ?, api_status = 'pending'
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$currentTime, $tx['id']]);
|
||||||
|
|
||||||
|
// Calculate next start time
|
||||||
|
$currentTime = addTimeToTime($currentTime, $tx['duration']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Block time updated successfully'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
195
api/update_transmission.php
Normal file
195
api/update_transmission.php
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Update Transmission
|
||||||
|
* Updates an existing transmission (drag-and-drop, time changes)
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Get POST data
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (empty($input['id'])) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Missing transmission ID'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing transmission
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.*, c.duration
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$input['id']]);
|
||||||
|
$transmission = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$transmission) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Transmission not found'
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare update data
|
||||||
|
$updates = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (isset($input['start_date']) && isValidDate($input['start_date'])) {
|
||||||
|
$updates[] = 'start_date = ?';
|
||||||
|
$params[] = $input['start_date'];
|
||||||
|
} else {
|
||||||
|
$params[] = $transmission['start_date'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($input['start_time']) && isValidTime($input['start_time'])) {
|
||||||
|
$updates[] = 'start_time = ?';
|
||||||
|
$params[] = $input['start_time'];
|
||||||
|
} else {
|
||||||
|
$params[] = $transmission['start_time'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($input['channel'])) {
|
||||||
|
$updates[] = 'channel = ?';
|
||||||
|
$params[] = $input['channel'];
|
||||||
|
} else {
|
||||||
|
$params[] = $transmission['channel'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for overlaps with new position
|
||||||
|
$checkDate = $params[0];
|
||||||
|
$checkTime = $params[1];
|
||||||
|
$checkChannel = $params[2];
|
||||||
|
$endTime = addTimeToTime($checkTime, $transmission['duration']);
|
||||||
|
|
||||||
|
// Validate that transmission falls within a block (inline to avoid curl issues)
|
||||||
|
ensureDailyBlocks($db, $checkDate, $checkDate);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT * FROM daily_blocks
|
||||||
|
WHERE block_date = ? AND channel = ?
|
||||||
|
ORDER BY actual_start_time
|
||||||
|
");
|
||||||
|
$stmt->execute([$checkDate, $checkChannel]);
|
||||||
|
$blocks = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$withinBlock = false;
|
||||||
|
foreach ($blocks as $block) {
|
||||||
|
$blockStart = $block['actual_start_time'];
|
||||||
|
$blockEnd = $block['actual_end_time'] ?? '23:59:59';
|
||||||
|
|
||||||
|
// Handle overnight blocks
|
||||||
|
$isOvernight = $blockEnd < $blockStart;
|
||||||
|
|
||||||
|
if ($isOvernight) {
|
||||||
|
if ($checkTime >= $blockStart || $checkTime < $blockEnd) {
|
||||||
|
if ($endTime <= $blockEnd || $endTime >= $blockStart) {
|
||||||
|
$withinBlock = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($checkTime >= $blockStart && $endTime <= $blockEnd) {
|
||||||
|
$withinBlock = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$withinBlock) {
|
||||||
|
$errorMessage = "Uitzending valt buiten blok tijden. ";
|
||||||
|
if (!empty($blocks)) {
|
||||||
|
$errorMessage .= "Beschikbare blokken: ";
|
||||||
|
foreach ($blocks as $block) {
|
||||||
|
$blockStart = substr($block['actual_start_time'], 0, 5);
|
||||||
|
$blockEnd = $block['actual_end_time'] ? substr($block['actual_end_time'], 0, 5) : '∞';
|
||||||
|
$errorMessage .= "{$blockStart}-{$blockEnd}, ";
|
||||||
|
}
|
||||||
|
$errorMessage = rtrim($errorMessage, ', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $errorMessage,
|
||||||
|
'attempted_time' => $checkTime . ' - ' . $endTime
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.id, t.start_time, t.duration, c.title
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.start_date = ?
|
||||||
|
AND t.channel = ?
|
||||||
|
AND t.id != ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$checkDate, $checkChannel, $input['id']]);
|
||||||
|
$existing = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($existing as $tx) {
|
||||||
|
$txEnd = addTimeToTime($tx['start_time'], $tx['duration']);
|
||||||
|
if (timeRangesOverlap($checkTime, $endTime, $tx['start_time'], $txEnd)) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => "Overlap detected with: {$tx['title']} ({$tx['start_time']} - {$txEnd})",
|
||||||
|
'overlap' => true
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as pending sync if changed
|
||||||
|
$updates[] = "api_status = 'pending'";
|
||||||
|
|
||||||
|
// Build and execute update query
|
||||||
|
if (!empty($updates)) {
|
||||||
|
$sql = "UPDATE transmissions SET " . implode(', ', $updates) . " WHERE id = ?";
|
||||||
|
$params[] = $input['id'];
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get updated transmission
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
c.title,
|
||||||
|
c.color_code,
|
||||||
|
c.series_code
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$input['id']]);
|
||||||
|
$updated = $stmt->fetch();
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Transmission updated successfully',
|
||||||
|
'transmission' => $updated
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
130
api/validate_transmission_time.php
Normal file
130
api/validate_transmission_time.php
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Endpoint: Validate Transmission Time
|
||||||
|
* Checks if a transmission falls within an active block
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Get POST data
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$input) {
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
$required = ['channel', 'start_date', 'start_time', 'duration'];
|
||||||
|
foreach ($required as $field) {
|
||||||
|
if (empty($input[$field])) {
|
||||||
|
jsonResponse([
|
||||||
|
'valid' => false,
|
||||||
|
'error' => "Missing required field: $field"
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$channel = $input['channel'];
|
||||||
|
$startDate = $input['start_date'];
|
||||||
|
$startTime = $input['start_time'];
|
||||||
|
$duration = $input['duration'];
|
||||||
|
|
||||||
|
// Calculate end time
|
||||||
|
$endTime = addTimeToTime($startTime, $duration);
|
||||||
|
|
||||||
|
// Ensure daily blocks exist
|
||||||
|
ensureDailyBlocks($db, $startDate, $startDate);
|
||||||
|
|
||||||
|
// Get all blocks for this channel and date
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT * FROM daily_blocks
|
||||||
|
WHERE block_date = ? AND channel = ?
|
||||||
|
ORDER BY actual_start_time
|
||||||
|
");
|
||||||
|
$stmt->execute([$startDate, $channel]);
|
||||||
|
$blocks = $stmt->fetchAll();
|
||||||
|
|
||||||
|
if (empty($blocks)) {
|
||||||
|
jsonResponse([
|
||||||
|
'valid' => false,
|
||||||
|
'error' => "Geen actief blok gevonden voor {$channel} op {$startDate}",
|
||||||
|
'blocks' => []
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if transmission falls within any block
|
||||||
|
$withinBlock = false;
|
||||||
|
$matchingBlock = null;
|
||||||
|
|
||||||
|
foreach ($blocks as $block) {
|
||||||
|
$blockStart = $block['actual_start_time'];
|
||||||
|
$blockEnd = $block['actual_end_time'] ?? '23:59:59';
|
||||||
|
|
||||||
|
// Handle overnight blocks
|
||||||
|
$isOvernight = $blockEnd < $blockStart;
|
||||||
|
|
||||||
|
if ($isOvernight) {
|
||||||
|
// For overnight blocks, check if time is after start OR before end
|
||||||
|
if ($startTime >= $blockStart || $startTime < $blockEnd) {
|
||||||
|
// Also check end time
|
||||||
|
if ($endTime <= $blockEnd || $endTime >= $blockStart) {
|
||||||
|
$withinBlock = true;
|
||||||
|
$matchingBlock = $block;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal block: check if both start and end are within block
|
||||||
|
if ($startTime >= $blockStart && $endTime <= $blockEnd) {
|
||||||
|
$withinBlock = true;
|
||||||
|
$matchingBlock = $block;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($withinBlock) {
|
||||||
|
jsonResponse([
|
||||||
|
'valid' => true,
|
||||||
|
'message' => 'Uitzending valt binnen blok',
|
||||||
|
'block' => $matchingBlock
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Find closest block for helpful error message
|
||||||
|
$closestBlock = $blocks[0];
|
||||||
|
$errorMessage = "Uitzending valt buiten blok tijden. ";
|
||||||
|
$errorMessage .= "Beschikbare blokken: ";
|
||||||
|
|
||||||
|
foreach ($blocks as $block) {
|
||||||
|
$blockStart = substr($block['actual_start_time'], 0, 5);
|
||||||
|
$blockEnd = $block['actual_end_time'] ? substr($block['actual_end_time'], 0, 5) : '∞';
|
||||||
|
$errorMessage .= "{$blockStart}-{$blockEnd}, ";
|
||||||
|
}
|
||||||
|
|
||||||
|
$errorMessage = rtrim($errorMessage, ', ');
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'valid' => false,
|
||||||
|
'error' => $errorMessage,
|
||||||
|
'blocks' => $blocks,
|
||||||
|
'attempted_time' => $startTime . ' - ' . $endTime
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse([
|
||||||
|
'valid' => false,
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
486
assets/css/custom.css
Normal file
486
assets/css/custom.css
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
/**
|
||||||
|
* Custom Styles for Telvero Talpa Planning System
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #2c3e50;
|
||||||
|
--secondary-color: #34495e;
|
||||||
|
--accent-color: #3498db;
|
||||||
|
--success-color: #2ecc71;
|
||||||
|
--warning-color: #f39c12;
|
||||||
|
--danger-color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
border-radius: 8px 8px 0 0 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.table {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.badge {
|
||||||
|
padding: 0.4em 0.8em;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Specific Styles */
|
||||||
|
#calendar {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical Calendar (timeGrid) Styles - very compact */
|
||||||
|
.fc-timegrid-event {
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 3px !important;
|
||||||
|
cursor: move;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
border-width: 1px;
|
||||||
|
min-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timegrid-event:hover {
|
||||||
|
transform: translateX(2px);
|
||||||
|
box-shadow: 0 3px 8px rgba(0,0,0,0.2);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timegrid-event .fc-event-main {
|
||||||
|
padding: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timegrid-event .fc-event-main-frame {
|
||||||
|
padding: 1px 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timeline Calendar Styles */
|
||||||
|
.fc-timeline-event {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
cursor: move;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timeline-event:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* General Event Styles */
|
||||||
|
.fc-event {
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: move;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-event-title {
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-event-time {
|
||||||
|
font-size: 0.85em;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make events more readable in vertical view */
|
||||||
|
.fc-timegrid-event-harness {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timegrid-col-events {
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improve time slot visibility - 150px per 15 minutes */
|
||||||
|
.fc-timegrid-slot {
|
||||||
|
height: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timegrid-slot-label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 4px 6px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make slot lines visible */
|
||||||
|
.fc-timegrid-slot-minor {
|
||||||
|
border-top-style: solid;
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-top-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timegrid-slot-major {
|
||||||
|
border-top-width: 2px;
|
||||||
|
border-top-color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Current time indicator */
|
||||||
|
.fc-timegrid-now-indicator-line {
|
||||||
|
border-color: #e74c3c;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timegrid-now-indicator-arrow {
|
||||||
|
border-color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Block background events */
|
||||||
|
.fc-bg-event {
|
||||||
|
opacity: 1 !important;
|
||||||
|
border: 1px dashed rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timegrid .fc-bg-event {
|
||||||
|
opacity: 0.8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Block info cards */
|
||||||
|
#blockInfoContainer .card {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blockInfoContainer .card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resource TimeGrid specific styles */
|
||||||
|
.fc-resource-timeline-divider,
|
||||||
|
.fc-resource-timegrid-divider {
|
||||||
|
width: 2px;
|
||||||
|
background: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-col-header-cell {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
border-right: 2px solid #dee2e6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timegrid-col {
|
||||||
|
border-right: 2px solid #dee2e6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make resource columns more distinct */
|
||||||
|
.fc-timegrid-col.fc-resource {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timegrid-col.fc-resource:nth-child(even) {
|
||||||
|
background: rgba(248, 249, 250, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resource header styling */
|
||||||
|
.fc-col-header-cell-cushion {
|
||||||
|
padding: 8px 4px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improve event spacing in resource columns */
|
||||||
|
.fc-timegrid-event-harness {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resource background events (blocks) */
|
||||||
|
.fc-timegrid-col .fc-bg-event {
|
||||||
|
opacity: 0.15 !important;
|
||||||
|
border-left: 3px solid rgba(0,0,0,0.2);
|
||||||
|
border-right: 3px solid rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Commercial Sidebar */
|
||||||
|
.commercial-sidebar {
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commercial-item {
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
cursor: move;
|
||||||
|
border-left: 4px solid;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commercial-item:hover {
|
||||||
|
transform: translateX(5px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commercial-item.dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commercial-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commercial-duration {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commercial-series {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Block Time Editor */
|
||||||
|
.block-time-editor {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 2px dashed #ffc107;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-time-input {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
border: 2px solid #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Indicators */
|
||||||
|
.status-pending {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-synced {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Spinner */
|
||||||
|
.loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border-lg {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.commercial-sidebar {
|
||||||
|
max-height: 300px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Styling */
|
||||||
|
.commercial-sidebar::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commercial-sidebar::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commercial-sidebar::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commercial-sidebar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert Animations */
|
||||||
|
@keyframes slideInDown {
|
||||||
|
from {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
animation: slideInDown 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color Picker */
|
||||||
|
.color-preview {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-preview:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
border-color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Stats */
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.success {
|
||||||
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.warning {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.info {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timeline View Enhancements */
|
||||||
|
.fc-timeline-event {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-timeline-event .fc-event-main {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conflict Warning */
|
||||||
|
.conflict-warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success Message */
|
||||||
|
.success-message {
|
||||||
|
background: #d4edda;
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Enhancements */
|
||||||
|
.form-control:focus,
|
||||||
|
.form-select:focus {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip Styling */
|
||||||
|
.tooltip-inner {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print Styles */
|
||||||
|
@media print {
|
||||||
|
.navbar,
|
||||||
|
.btn,
|
||||||
|
.commercial-sidebar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
box-shadow: none !important;
|
||||||
|
border: 1px solid #dee2e6 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
685
assets/js/calendar-init.js
Normal file
685
assets/js/calendar-init.js
Normal file
@ -0,0 +1,685 @@
|
|||||||
|
/**
|
||||||
|
* Calendar Initialization and Event Handlers
|
||||||
|
* FullCalendar.js configuration for TV planning
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const calendarEl = document.getElementById('calendar');
|
||||||
|
|
||||||
|
if (!calendarEl) {
|
||||||
|
console.error('Calendar element not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize FullCalendar with resource columns
|
||||||
|
const calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
|
||||||
|
initialView: 'resourceTimeGridDay',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'resourceTimeGridDay,resourceTimeGridWeek,resourceTimelineDay'
|
||||||
|
},
|
||||||
|
slotDuration: '00:15:00',
|
||||||
|
slotLabelInterval: '00:15:00',
|
||||||
|
snapDuration: '00:01:00',
|
||||||
|
slotMinTime: '00:00:00',
|
||||||
|
slotMaxTime: '24:00:00',
|
||||||
|
height: 'auto',
|
||||||
|
locale: 'nl',
|
||||||
|
allDaySlot: false,
|
||||||
|
nowIndicator: true,
|
||||||
|
resourceAreaHeaderContent: 'Zenders',
|
||||||
|
resourceAreaWidth: '120px',
|
||||||
|
resourceOrder: 'title',
|
||||||
|
|
||||||
|
// Resources (channels) - only SBS9 and NET5
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
id: 'SBS9',
|
||||||
|
title: 'SBS9',
|
||||||
|
eventBackgroundColor: '#3498db',
|
||||||
|
eventBorderColor: '#2980b9'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'NET5',
|
||||||
|
title: 'NET5',
|
||||||
|
eventBackgroundColor: '#e74c3c',
|
||||||
|
eventBorderColor: '#c0392b'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// Load events from API (including background blocks)
|
||||||
|
events: function(info, successCallback, failureCallback) {
|
||||||
|
const startDate = info.startStr.split('T')[0];
|
||||||
|
const endDate = info.endStr.split('T')[0];
|
||||||
|
|
||||||
|
// Load both transmissions and blocks
|
||||||
|
Promise.all([
|
||||||
|
fetch('api/get_transmissions.php?' + new URLSearchParams({
|
||||||
|
start: startDate,
|
||||||
|
end: endDate
|
||||||
|
})).then(r => r.json()),
|
||||||
|
|
||||||
|
fetch('api/get_daily_blocks.php?' + new URLSearchParams({
|
||||||
|
date: startDate
|
||||||
|
})).then(r => r.json())
|
||||||
|
])
|
||||||
|
.then(([transmissions, blocksData]) => {
|
||||||
|
// Combine transmissions with block background events
|
||||||
|
const allEvents = [
|
||||||
|
...transmissions,
|
||||||
|
...(blocksData.backgroundEvents || [])
|
||||||
|
];
|
||||||
|
successCallback(allEvents);
|
||||||
|
|
||||||
|
// Update block info display
|
||||||
|
updateBlockInfo(blocksData.blocks || []);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading events:', error);
|
||||||
|
failureCallback(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Event rendering - optimized for resource columns
|
||||||
|
eventContent: function(arg) {
|
||||||
|
const seriesCode = arg.event.extendedProps.series_code;
|
||||||
|
const title = arg.event.title;
|
||||||
|
const duration = arg.event.extendedProps.duration;
|
||||||
|
|
||||||
|
// Skip rendering for background events (blocks)
|
||||||
|
if (arg.event.display === 'background') {
|
||||||
|
return true; // Use default rendering
|
||||||
|
}
|
||||||
|
|
||||||
|
// For timeline view
|
||||||
|
if (arg.view.type.includes('timeline')) {
|
||||||
|
return {
|
||||||
|
html: `
|
||||||
|
<div class="fc-event-main-frame">
|
||||||
|
<div class="fc-event-time">${arg.timeText}</div>
|
||||||
|
<div class="fc-event-title-container">
|
||||||
|
<div class="fc-event-title">
|
||||||
|
${seriesCode ? '<strong>' + seriesCode + '</strong> - ' : ''}
|
||||||
|
${title}
|
||||||
|
</div>
|
||||||
|
<div class="fc-event-duration" style="font-size: 0.8em; opacity: 0.8;">
|
||||||
|
${duration}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// For vertical resource view - ultra compact single line
|
||||||
|
return {
|
||||||
|
html: `
|
||||||
|
<div class="fc-event-main-frame" style="padding: 1px 3px; line-height: 1.1;">
|
||||||
|
<div style="font-weight: 600; font-size: 0.75em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||||
|
${seriesCode ? '<span style="background: rgba(0,0,0,0.15); padding: 0px 3px; border-radius: 2px; margin-right: 2px; font-size: 0.9em;">' + seriesCode + '</span>' : ''}
|
||||||
|
${title} <span style="opacity: 0.65; font-size: 0.95em; margin-left: 3px;">⏱${duration}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enable drag and drop
|
||||||
|
editable: true,
|
||||||
|
droppable: true,
|
||||||
|
|
||||||
|
// Event drop (move existing event)
|
||||||
|
eventDrop: function(info) {
|
||||||
|
const event = info.event;
|
||||||
|
const resourceId = event.getResources()[0]?.id || 'SBS9';
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
showAlert('info', 'Valideren...');
|
||||||
|
|
||||||
|
fetch('api/update_transmission.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: event.id,
|
||||||
|
start_date: event.start.toISOString().split('T')[0],
|
||||||
|
start_time: event.start.toTimeString().split(' ')[0],
|
||||||
|
channel: resourceId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
info.revert();
|
||||||
|
showAlert('error', data.error || 'Fout bij opslaan');
|
||||||
|
|
||||||
|
// Show validation details if available
|
||||||
|
if (data.validation && data.validation.blocks) {
|
||||||
|
console.warn('Validation failed:', data.validation);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showAlert('success', 'Uitzending verplaatst');
|
||||||
|
calendar.refetchEvents();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
info.revert();
|
||||||
|
showAlert('error', 'Netwerkfout bij opslaan');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Event resize (change duration)
|
||||||
|
eventResize: function(info) {
|
||||||
|
showAlert('warning', 'Duur aanpassen is niet toegestaan. Gebruik de commercial instellingen.');
|
||||||
|
info.revert();
|
||||||
|
},
|
||||||
|
|
||||||
|
// External event drop (from sidebar) - DISABLED to prevent duplicates
|
||||||
|
// The eventReceive handler below will handle the creation
|
||||||
|
drop: null,
|
||||||
|
|
||||||
|
// Event receive (when external event is dropped)
|
||||||
|
eventReceive: function(info) {
|
||||||
|
const event = info.event;
|
||||||
|
const commercialId = event.extendedProps.commercial_id || info.draggedEl.dataset.commercialId;
|
||||||
|
const resourceId = event.getResources()[0]?.id || 'SBS9';
|
||||||
|
|
||||||
|
// Remove the temporary event
|
||||||
|
event.remove();
|
||||||
|
|
||||||
|
// Calculate next available start time
|
||||||
|
const dropDate = info.event.start.toISOString().split('T')[0];
|
||||||
|
const dropTime = info.event.start.toTimeString().split(' ')[0];
|
||||||
|
|
||||||
|
// Get next start time based on existing transmissions
|
||||||
|
fetch('api/get_next_start_time.php?' + new URLSearchParams({
|
||||||
|
date: dropDate,
|
||||||
|
channel: resourceId,
|
||||||
|
requested_time: dropTime
|
||||||
|
}))
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const startTime = data.next_start_time || dropTime;
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
showAlert('info', 'Toevoegen...');
|
||||||
|
|
||||||
|
return fetch('api/create_transmission.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
commercial_id: commercialId,
|
||||||
|
channel: resourceId,
|
||||||
|
start_date: dropDate,
|
||||||
|
start_time: startTime
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showAlert('success', 'Uitzending toegevoegd');
|
||||||
|
calendar.refetchEvents();
|
||||||
|
} else {
|
||||||
|
showAlert('error', data.error || 'Fout bij toevoegen');
|
||||||
|
|
||||||
|
// Show validation details if available
|
||||||
|
if (data.validation && data.validation.blocks) {
|
||||||
|
console.warn('Validation failed:', data.validation);
|
||||||
|
|
||||||
|
// Show available blocks in alert
|
||||||
|
let blockInfo = 'Beschikbare blokken: ';
|
||||||
|
data.validation.blocks.forEach(block => {
|
||||||
|
const start = block.actual_start_time.substring(0, 5);
|
||||||
|
const end = block.actual_end_time ? block.actual_end_time.substring(0, 5) : '∞';
|
||||||
|
blockInfo += `${start}-${end}, `;
|
||||||
|
});
|
||||||
|
showAlert('warning', blockInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showAlert('error', 'Netwerkfout bij toevoegen');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Event click (show details/edit)
|
||||||
|
eventClick: function(info) {
|
||||||
|
const event = info.event;
|
||||||
|
const props = event.extendedProps;
|
||||||
|
|
||||||
|
// Skip for background events (blocks)
|
||||||
|
if (event.display === 'background') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resources = event.getResources();
|
||||||
|
const channelName = resources.length > 0 ? resources[0].title : 'Onbekend';
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('eventModal'));
|
||||||
|
document.getElementById('eventTitle').textContent = event.title;
|
||||||
|
document.getElementById('eventDetails').innerHTML = `
|
||||||
|
<p><strong>Zender:</strong> ${channelName}</p>
|
||||||
|
<p><strong>Datum:</strong> ${event.start.toLocaleDateString('nl-NL')}</p>
|
||||||
|
<p><strong>Starttijd:</strong> ${event.start.toLocaleTimeString('nl-NL', {hour: '2-digit', minute: '2-digit'})}</p>
|
||||||
|
<p><strong>Duur:</strong> ${props.duration}</p>
|
||||||
|
<p><strong>Series Code:</strong> ${props.series_code || '-'}</p>
|
||||||
|
<p><strong>Template:</strong> ${props.template}</p>
|
||||||
|
<p><strong>API Status:</strong> <span class="badge status-${props.api_status}">${props.api_status}</span></p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Set delete button
|
||||||
|
document.getElementById('deleteEventBtn').onclick = function() {
|
||||||
|
if (confirm('Weet je zeker dat je deze uitzending wilt verwijderen?')) {
|
||||||
|
deleteEvent(event.id);
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set sync button
|
||||||
|
document.getElementById('syncEventBtn').onclick = function() {
|
||||||
|
syncEvent(event.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Date click (show block time editor)
|
||||||
|
dateClick: function(info) {
|
||||||
|
if (info.resource) {
|
||||||
|
showBlockTimeEditor(info.dateStr.split('T')[0], info.resource.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
|
|
||||||
|
// Make external events draggable
|
||||||
|
initDraggableCommercials();
|
||||||
|
|
||||||
|
// Store calendar instance globally
|
||||||
|
window.tvCalendar = calendar;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize draggable commercials from sidebar
|
||||||
|
*/
|
||||||
|
function initDraggableCommercials() {
|
||||||
|
const commercialItems = document.querySelectorAll('.commercial-item');
|
||||||
|
|
||||||
|
commercialItems.forEach(item => {
|
||||||
|
new FullCalendar.Draggable(item, {
|
||||||
|
eventData: function(eventEl) {
|
||||||
|
return {
|
||||||
|
title: eventEl.dataset.title,
|
||||||
|
duration: eventEl.dataset.duration,
|
||||||
|
backgroundColor: eventEl.dataset.color,
|
||||||
|
borderColor: eventEl.dataset.color,
|
||||||
|
extendedProps: {
|
||||||
|
commercial_id: eventEl.dataset.commercialId,
|
||||||
|
series_code: eventEl.dataset.seriesCode
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show block time editor
|
||||||
|
*/
|
||||||
|
function showBlockTimeEditor(date, channel) {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('blockTimeModal'));
|
||||||
|
document.getElementById('blockDate').value = date;
|
||||||
|
document.getElementById('blockChannel').value = channel;
|
||||||
|
|
||||||
|
// Load current block time
|
||||||
|
fetch(`api/get_block_time.php?date=${date}&channel=${channel}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.start_time) {
|
||||||
|
document.getElementById('blockStartTime').value = data.start_time;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save block time
|
||||||
|
*/
|
||||||
|
function saveBlockTime() {
|
||||||
|
const date = document.getElementById('blockDate').value;
|
||||||
|
const channel = document.getElementById('blockChannel').value;
|
||||||
|
const startTime = document.getElementById('blockStartTime').value;
|
||||||
|
const recalculate = document.getElementById('recalculateTransmissions').checked;
|
||||||
|
|
||||||
|
fetch('api/update_block_time.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
date: date,
|
||||||
|
channel: channel,
|
||||||
|
start_time: startTime,
|
||||||
|
recalculate: recalculate
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showAlert('success', 'Blok starttijd bijgewerkt');
|
||||||
|
window.tvCalendar.refetchEvents();
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('blockTimeModal')).hide();
|
||||||
|
} else {
|
||||||
|
showAlert('error', data.error || 'Fout bij opslaan');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showAlert('error', 'Netwerkfout bij opslaan');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete event
|
||||||
|
*/
|
||||||
|
function deleteEvent(eventId) {
|
||||||
|
fetch('api/delete_transmission.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ id: eventId })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
showAlert('success', 'Uitzending verwijderd');
|
||||||
|
window.tvCalendar.refetchEvents();
|
||||||
|
} else {
|
||||||
|
showAlert('error', data.error || 'Fout bij verwijderen');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showAlert('error', 'Netwerkfout bij verwijderen');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync event to Talpa API
|
||||||
|
*/
|
||||||
|
function syncEvent(eventId) {
|
||||||
|
showAlert('info', 'Synchroniseren...');
|
||||||
|
|
||||||
|
// This would call your existing sync functionality
|
||||||
|
fetch('index.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: `sync_item=1&sync_id=${eventId}`
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
showAlert('success', 'Gesynchroniseerd met Talpa');
|
||||||
|
window.tvCalendar.refetchEvents();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showAlert('error', 'Fout bij synchroniseren');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show alert message
|
||||||
|
*/
|
||||||
|
function showAlert(type, message) {
|
||||||
|
const alertContainer = document.getElementById('alertContainer');
|
||||||
|
if (!alertContainer) return;
|
||||||
|
|
||||||
|
const alertClass = {
|
||||||
|
'success': 'alert-success',
|
||||||
|
'error': 'alert-danger',
|
||||||
|
'warning': 'alert-warning',
|
||||||
|
'info': 'alert-info'
|
||||||
|
}[type] || 'alert-info';
|
||||||
|
|
||||||
|
const alert = document.createElement('div');
|
||||||
|
alert.className = `alert ${alertClass} alert-dismissible fade show`;
|
||||||
|
alert.innerHTML = `
|
||||||
|
${message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
alertContainer.appendChild(alert);
|
||||||
|
|
||||||
|
// Auto-dismiss after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
alert.remove();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter commercials in sidebar
|
||||||
|
*/
|
||||||
|
function filterCommercials(searchTerm) {
|
||||||
|
const items = document.querySelectorAll('.commercial-item');
|
||||||
|
const term = searchTerm.toLowerCase();
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const title = item.dataset.title.toLowerCase();
|
||||||
|
const seriesCode = (item.dataset.seriesCode || '').toLowerCase();
|
||||||
|
|
||||||
|
if (title.includes(term) || seriesCode.includes(term)) {
|
||||||
|
item.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update block info display
|
||||||
|
*/
|
||||||
|
function updateBlockInfo(blocks) {
|
||||||
|
const container = document.getElementById('blockInfoContainer');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
container.innerHTML = '<p class="text-muted small mb-0">Geen actieve blokken voor deze dag</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group blocks by channel
|
||||||
|
const blocksByChannel = {};
|
||||||
|
blocks.forEach(block => {
|
||||||
|
if (!blocksByChannel[block.channel]) {
|
||||||
|
blocksByChannel[block.channel] = [];
|
||||||
|
}
|
||||||
|
blocksByChannel[block.channel].push(block);
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = '<div class="row g-2">';
|
||||||
|
|
||||||
|
for (const [channel, channelBlocks] of Object.entries(blocksByChannel)) {
|
||||||
|
const channelColors = {
|
||||||
|
'SBS9': 'primary',
|
||||||
|
'NET5': 'danger',
|
||||||
|
'VERONICA': 'secondary'
|
||||||
|
};
|
||||||
|
const color = channelColors[channel] || 'secondary';
|
||||||
|
|
||||||
|
html += `<div class="col-md-4">`;
|
||||||
|
html += `<div class="card border-${color}">`;
|
||||||
|
html += `<div class="card-header bg-${color} text-white py-1 px-2">`;
|
||||||
|
html += `<small class="fw-bold">${channel}</small>`;
|
||||||
|
html += `</div>`;
|
||||||
|
html += `<div class="card-body p-2">`;
|
||||||
|
|
||||||
|
channelBlocks.forEach(block => {
|
||||||
|
const startTime = block.actual_start_time.substring(0, 5);
|
||||||
|
const endTime = block.actual_end_time ? block.actual_end_time.substring(0, 5) : '∞';
|
||||||
|
|
||||||
|
html += `<div class="d-flex justify-content-between align-items-center mb-1">`;
|
||||||
|
html += `<small class="text-muted">${block.template_name || 'Blok'}</small>`;
|
||||||
|
html += `<small class="badge bg-light text-dark">${startTime} - ${endTime}</small>`;
|
||||||
|
html += `</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `</div></div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust calendar zoom level
|
||||||
|
* Base: 10px per minute
|
||||||
|
*/
|
||||||
|
function adjustZoom(factor) {
|
||||||
|
const calendarEl = document.getElementById('calendar');
|
||||||
|
const currentZoom = parseFloat(calendarEl.dataset.zoom || '1.0');
|
||||||
|
|
||||||
|
let newZoom;
|
||||||
|
if (factor === 1.0) {
|
||||||
|
newZoom = 1.0; // Reset
|
||||||
|
} else {
|
||||||
|
newZoom = currentZoom * factor;
|
||||||
|
newZoom = Math.max(0.5, Math.min(3.0, newZoom)); // Limit between 50% and 300%
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarEl.dataset.zoom = newZoom;
|
||||||
|
|
||||||
|
// Base: 10px per minute
|
||||||
|
const baseHeightPx = 10;
|
||||||
|
const newHeightPx = baseHeightPx * newZoom;
|
||||||
|
|
||||||
|
// Update all slot heights
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'zoom-style';
|
||||||
|
const existingStyle = document.getElementById('zoom-style');
|
||||||
|
if (existingStyle) {
|
||||||
|
existingStyle.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
style.textContent = `
|
||||||
|
.fc-timegrid-slot {
|
||||||
|
height: ${newHeightPx}px !important;
|
||||||
|
}
|
||||||
|
.fc-timegrid-event {
|
||||||
|
min-height: ${Math.max(12, 12 * newZoom)}px !important;
|
||||||
|
}
|
||||||
|
.fc-timegrid-event .fc-event-main-frame {
|
||||||
|
font-size: ${Math.max(0.65, 0.75 * newZoom)}em !important;
|
||||||
|
padding: ${Math.max(1, 2 * newZoom)}px ${Math.max(2, 3 * newZoom)}px !important;
|
||||||
|
}
|
||||||
|
.fc-timegrid-slot-label {
|
||||||
|
font-size: ${Math.max(0.6, 0.7 * newZoom)}em !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// Update zoom level display
|
||||||
|
document.getElementById('zoomLevel').textContent = Math.round(newZoom * 100) + '%';
|
||||||
|
|
||||||
|
// Refresh calendar to apply changes
|
||||||
|
if (window.tvCalendar) {
|
||||||
|
window.tvCalendar.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show sync block modal
|
||||||
|
*/
|
||||||
|
function showSyncBlockModal() {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('syncBlockModal'));
|
||||||
|
|
||||||
|
// Set current date from calendar
|
||||||
|
if (window.tvCalendar) {
|
||||||
|
const currentDate = window.tvCalendar.getDate();
|
||||||
|
document.getElementById('syncBlockDate').value = currentDate.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync entire block to Talpa
|
||||||
|
*/
|
||||||
|
function syncBlock() {
|
||||||
|
const date = document.getElementById('syncBlockDate').value;
|
||||||
|
const channel = document.getElementById('syncBlockChannel').value;
|
||||||
|
|
||||||
|
if (!date || !channel) {
|
||||||
|
showAlert('error', 'Selecteer een datum en zender');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show progress
|
||||||
|
document.getElementById('syncBlockProgress').classList.remove('d-none');
|
||||||
|
|
||||||
|
fetch('api/sync_block.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
date: date,
|
||||||
|
channel: channel
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Hide progress
|
||||||
|
document.getElementById('syncBlockProgress').classList.add('d-none');
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showAlert('success', data.message);
|
||||||
|
|
||||||
|
// Show detailed results if there were errors
|
||||||
|
if (data.errors && data.errors.length > 0) {
|
||||||
|
console.warn('Sync errors:', data.errors);
|
||||||
|
let errorMsg = 'Fouten bij synchronisatie:\n';
|
||||||
|
data.errors.forEach(err => {
|
||||||
|
errorMsg += `- ${err.time}: ${err.error}\n`;
|
||||||
|
});
|
||||||
|
showAlert('warning', errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh calendar
|
||||||
|
if (window.tvCalendar) {
|
||||||
|
window.tvCalendar.refetchEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('syncBlockModal')).hide();
|
||||||
|
} else {
|
||||||
|
showAlert('error', data.error || 'Fout bij synchroniseren');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Hide progress
|
||||||
|
document.getElementById('syncBlockProgress').classList.add('d-none');
|
||||||
|
console.error('Error:', error);
|
||||||
|
showAlert('error', 'Netwerkfout bij synchroniseren');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose functions globally
|
||||||
|
window.saveBlockTime = saveBlockTime;
|
||||||
|
window.filterCommercials = filterCommercials;
|
||||||
|
window.updateBlockInfo = updateBlockInfo;
|
||||||
|
window.adjustZoom = adjustZoom;
|
||||||
|
window.showSyncBlockModal = showSyncBlockModal;
|
||||||
|
window.syncBlock = syncBlock;
|
||||||
319
blocks.php
Normal file
319
blocks.php
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Block Template Management
|
||||||
|
* Manage recurring time blocks for channels
|
||||||
|
*/
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__);
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Handle form submissions
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (isset($_POST['create_template'])) {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO block_templates
|
||||||
|
(channel, name, day_of_week, default_start_time, default_end_time, is_active)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 1)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$_POST['channel'],
|
||||||
|
$_POST['name'],
|
||||||
|
$_POST['day_of_week'],
|
||||||
|
$_POST['default_start_time'],
|
||||||
|
$_POST['default_end_time'] ?: null
|
||||||
|
]);
|
||||||
|
header('Location: blocks.php?success=created');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['update_template'])) {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE block_templates
|
||||||
|
SET channel = ?, name = ?, day_of_week = ?,
|
||||||
|
default_start_time = ?, default_end_time = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$_POST['channel'],
|
||||||
|
$_POST['name'],
|
||||||
|
$_POST['day_of_week'],
|
||||||
|
$_POST['default_start_time'],
|
||||||
|
$_POST['default_end_time'] ?: null,
|
||||||
|
$_POST['template_id']
|
||||||
|
]);
|
||||||
|
header('Location: blocks.php?success=updated');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['toggle_active'])) {
|
||||||
|
$stmt = $db->prepare("UPDATE block_templates SET is_active = NOT is_active WHERE id = ?");
|
||||||
|
$stmt->execute([$_POST['template_id']]);
|
||||||
|
header('Location: blocks.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['delete_template'])) {
|
||||||
|
$stmt = $db->prepare("DELETE FROM block_templates WHERE id = ?");
|
||||||
|
$stmt->execute([$_POST['template_id']]);
|
||||||
|
header('Location: blocks.php?success=deleted');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all templates
|
||||||
|
$templates = $db->query("SELECT * FROM block_templates ORDER BY channel, day_of_week, default_start_time")->fetchAll();
|
||||||
|
|
||||||
|
// Get template for editing
|
||||||
|
$editTemplate = null;
|
||||||
|
if (isset($_GET['edit'])) {
|
||||||
|
$stmt = $db->prepare("SELECT * FROM block_templates WHERE id = ?");
|
||||||
|
$stmt->execute([$_GET['edit']]);
|
||||||
|
$editTemplate = $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
$dayNames = [
|
||||||
|
'all' => 'Alle dagen',
|
||||||
|
'ma' => 'Maandag',
|
||||||
|
'di' => 'Dinsdag',
|
||||||
|
'wo' => 'Woensdag',
|
||||||
|
'do' => 'Donderdag',
|
||||||
|
'vr' => 'Vrijdag',
|
||||||
|
'za' => 'Zaterdag',
|
||||||
|
'zo' => 'Zondag'
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Blok Templates - Telvero Talpa</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="index.php">
|
||||||
|
<i class="bi bi-tv"></i> Telvero Talpa Planner
|
||||||
|
</a>
|
||||||
|
<div class="navbar-nav">
|
||||||
|
<a class="nav-link" href="index.php">Dashboard</a>
|
||||||
|
<a class="nav-link" href="planner.php">Excel Planner</a>
|
||||||
|
<a class="nav-link" href="calendar.php">Kalender</a>
|
||||||
|
<a class="nav-link active" href="blocks.php">Blokken</a>
|
||||||
|
<a class="nav-link" href="commercials.php">Commercials</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><i class="bi bi-calendar3"></i> Blok Templates</h1>
|
||||||
|
<a href="calendar.php" class="btn btn-primary">
|
||||||
|
<i class="bi bi-calendar-week"></i> Naar Kalender
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($_GET['success'])): ?>
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<?php
|
||||||
|
$messages = [
|
||||||
|
'created' => 'Template succesvol aangemaakt!',
|
||||||
|
'updated' => 'Template succesvol bijgewerkt!',
|
||||||
|
'deleted' => 'Template succesvol verwijderd!'
|
||||||
|
];
|
||||||
|
echo $messages[$_GET['success']] ?? 'Actie succesvol uitgevoerd!';
|
||||||
|
?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-plus-circle"></i>
|
||||||
|
<?= $editTemplate ? 'Template Bewerken' : 'Nieuw Template' ?>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
<?php if ($editTemplate): ?>
|
||||||
|
<input type="hidden" name="template_id" value="<?= $editTemplate['id'] ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Zender</label>
|
||||||
|
<select name="channel" class="form-select" required>
|
||||||
|
<option value="">Selecteer zender...</option>
|
||||||
|
<option value="SBS9" <?= ($editTemplate && $editTemplate['channel'] == 'SBS9') ? 'selected' : '' ?>>SBS9</option>
|
||||||
|
<option value="NET5" <?= ($editTemplate && $editTemplate['channel'] == 'NET5') ? 'selected' : '' ?>>NET5</option>
|
||||||
|
<option value="VERONICA" <?= ($editTemplate && $editTemplate['channel'] == 'VERONICA') ? 'selected' : '' ?>>VERONICA</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Template Naam</label>
|
||||||
|
<input type="text" name="name" class="form-control"
|
||||||
|
value="<?= htmlspecialchars($editTemplate['name'] ?? '') ?>"
|
||||||
|
placeholder="bijv. SBS9 Dagblok" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Dag van de Week</label>
|
||||||
|
<select name="day_of_week" class="form-select" required>
|
||||||
|
<?php foreach ($dayNames as $value => $label): ?>
|
||||||
|
<option value="<?= $value ?>"
|
||||||
|
<?= ($editTemplate && $editTemplate['day_of_week'] == $value) ? 'selected' : '' ?>>
|
||||||
|
<?= $label ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Standaard Starttijd</label>
|
||||||
|
<input type="time" name="default_start_time" class="form-control"
|
||||||
|
value="<?= $editTemplate['default_start_time'] ?? '' ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Standaard Eindtijd (optioneel)</label>
|
||||||
|
<input type="time" name="default_end_time" class="form-control"
|
||||||
|
value="<?= $editTemplate['default_end_time'] ?? '' ?>">
|
||||||
|
<small class="text-muted">Laat leeg als niet van toepassing</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" name="<?= $editTemplate ? 'update_template' : 'create_template' ?>"
|
||||||
|
class="btn btn-primary">
|
||||||
|
<i class="bi bi-<?= $editTemplate ? 'check' : 'plus' ?>-circle"></i>
|
||||||
|
<?= $editTemplate ? 'Bijwerken' : 'Aanmaken' ?>
|
||||||
|
</button>
|
||||||
|
<?php if ($editTemplate): ?>
|
||||||
|
<a href="blocks.php" class="btn btn-secondary">Annuleren</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-secondary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Bestaande Templates</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<?php if (empty($templates)): ?>
|
||||||
|
<div class="p-4 text-center text-muted">
|
||||||
|
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
|
||||||
|
<p class="mt-2">Nog geen templates aangemaakt</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Zender</th>
|
||||||
|
<th>Naam</th>
|
||||||
|
<th>Dag</th>
|
||||||
|
<th>Starttijd</th>
|
||||||
|
<th>Eindtijd</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Acties</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($templates as $template): ?>
|
||||||
|
<tr class="<?= $template['is_active'] ? '' : 'table-secondary' ?>">
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info"><?= htmlspecialchars($template['channel']) ?></span>
|
||||||
|
</td>
|
||||||
|
<td><?= htmlspecialchars($template['name']) ?></td>
|
||||||
|
<td><?= $dayNames[$template['day_of_week']] ?></td>
|
||||||
|
<td><?= substr($template['default_start_time'], 0, 5) ?></td>
|
||||||
|
<td><?= $template['default_end_time'] ? substr($template['default_end_time'], 0, 5) : '-' ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($template['is_active']): ?>
|
||||||
|
<span class="badge bg-success">Actief</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge bg-secondary">Inactief</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="?edit=<?= $template['id'] ?>"
|
||||||
|
class="btn btn-outline-primary" title="Bewerken">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="POST" style="display:inline;">
|
||||||
|
<input type="hidden" name="template_id" value="<?= $template['id'] ?>">
|
||||||
|
<button type="submit" name="toggle_active"
|
||||||
|
class="btn btn-outline-<?= $template['is_active'] ? 'warning' : 'success' ?>"
|
||||||
|
title="<?= $template['is_active'] ? 'Deactiveren' : 'Activeren' ?>">
|
||||||
|
<i class="bi bi-<?= $template['is_active'] ? 'pause' : 'play' ?>-circle"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" style="display:inline;"
|
||||||
|
onsubmit="return confirm('Weet je zeker dat je dit template wilt verwijderen?');">
|
||||||
|
<input type="hidden" name="template_id" value="<?= $template['id'] ?>">
|
||||||
|
<button type="submit" name="delete_template"
|
||||||
|
class="btn btn-outline-danger" title="Verwijderen">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm mt-4">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Uitleg</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6>Wat zijn Blok Templates?</h6>
|
||||||
|
<p>Blok templates definiëren terugkerende tijdslots voor uitzendingen. Bijvoorbeeld:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>SBS9 Dagblok:</strong> Ma-Zo 07:00-15:00</li>
|
||||||
|
<li><strong>SBS9 Nachtblok:</strong> Ma-Zo 23:30-02:00</li>
|
||||||
|
<li><strong>Net5 Ochtend:</strong> Ma-Vr 07:30-11:30</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h6 class="mt-3">Hoe werkt het?</h6>
|
||||||
|
<ol>
|
||||||
|
<li>Maak templates aan voor vaste blokken</li>
|
||||||
|
<li>Het systeem genereert automatisch dagelijkse blokken</li>
|
||||||
|
<li>In de kalender kun je per dag de starttijd aanpassen</li>
|
||||||
|
<li>Uitzendingen worden automatisch in deze blokken gepland</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
355
calendar.php
Normal file
355
calendar.php
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Calendar Planning View
|
||||||
|
* Main interface for drag-and-drop TV scheduling
|
||||||
|
*/
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__);
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Get all commercials with colors for sidebar
|
||||||
|
$commercials = $db->query("
|
||||||
|
SELECT id, title, duration, color_code, series_code, upload_status
|
||||||
|
FROM commercials
|
||||||
|
WHERE upload_status = 'uploaded'
|
||||||
|
ORDER BY title ASC
|
||||||
|
")->fetchAll();
|
||||||
|
|
||||||
|
// Auto-assign colors to commercials without colors
|
||||||
|
foreach ($commercials as $commercial) {
|
||||||
|
if (empty($commercial['color_code'])) {
|
||||||
|
$stmt = $db->query("SELECT color_code FROM commercials WHERE color_code IS NOT NULL");
|
||||||
|
$existingColors = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
$newColor = generateDistinctColor($existingColors);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("UPDATE commercials SET color_code = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$newColor, $commercial['id']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh commercials after color assignment
|
||||||
|
$commercials = $db->query("
|
||||||
|
SELECT id, title, duration, color_code, series_code, upload_status
|
||||||
|
FROM commercials
|
||||||
|
WHERE upload_status = 'uploaded'
|
||||||
|
ORDER BY title ASC
|
||||||
|
")->fetchAll();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Kalender Planning - Telvero Talpa</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
|
|
||||||
|
<!-- FullCalendar -->
|
||||||
|
<link href='https://cdn.jsdelivr.net/npm/fullcalendar-scheduler@6.1.10/index.global.min.css' rel='stylesheet' />
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="index.php">
|
||||||
|
<i class="bi bi-tv"></i> Telvero Talpa Planner
|
||||||
|
</a>
|
||||||
|
<div class="navbar-nav">
|
||||||
|
<a class="nav-link" href="index.php">Dashboard</a>
|
||||||
|
<a class="nav-link" href="planner.php">Excel Planner</a>
|
||||||
|
<a class="nav-link active" href="calendar.php">Kalender</a>
|
||||||
|
<a class="nav-link" href="blocks.php">Blokken</a>
|
||||||
|
<a class="nav-link" href="commercials.php">Commercials</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<!-- Alert Container -->
|
||||||
|
<div id="alertContainer"></div>
|
||||||
|
|
||||||
|
<!-- Block Info Display & Zoom Controls -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-info text-white py-2 d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bi bi-calendar3"></i> Actieve Blokken voor Vandaag
|
||||||
|
</h6>
|
||||||
|
<button type="button" class="btn btn-sm btn-light" onclick="showSyncBlockModal()">
|
||||||
|
<i class="bi bi-cloud-upload"></i> Sync Blok naar Talpa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2" id="blockInfoContainer">
|
||||||
|
<p class="text-muted small mb-0">Laden...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-secondary text-white py-2">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bi bi-zoom-in"></i> Zoom
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="btn-group w-100" role="group">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="adjustZoom(0.8)">
|
||||||
|
<i class="bi bi-dash-circle"></i> -
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="adjustZoom(1.0)" title="Reset">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="adjustZoom(1.2)">
|
||||||
|
<i class="bi bi-plus-circle"></i> +
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-center">
|
||||||
|
<small class="text-muted">Huidige: <span id="zoomLevel">100%</span></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Sidebar with draggable commercials -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-collection-play"></i> Beschikbare Commercials
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="text"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
placeholder="Zoeken..."
|
||||||
|
onkeyup="filterCommercials(this.value)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Commercial List -->
|
||||||
|
<div class="commercial-sidebar">
|
||||||
|
<?php if (empty($commercials)): ?>
|
||||||
|
<div class="text-center text-muted p-3">
|
||||||
|
<i class="bi bi-inbox" style="font-size: 2rem;"></i>
|
||||||
|
<p class="mt-2 small">Geen commercials beschikbaar</p>
|
||||||
|
<a href="commercials.php" class="btn btn-sm btn-primary">
|
||||||
|
Voeg Commercial Toe
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($commercials as $commercial): ?>
|
||||||
|
<div class="commercial-item"
|
||||||
|
style="border-left-color: <?= htmlspecialchars($commercial['color_code']) ?>;"
|
||||||
|
data-commercial-id="<?= $commercial['id'] ?>"
|
||||||
|
data-title="<?= htmlspecialchars($commercial['title']) ?>"
|
||||||
|
data-duration="<?= $commercial['duration'] ?>"
|
||||||
|
data-color="<?= htmlspecialchars($commercial['color_code']) ?>"
|
||||||
|
data-series-code="<?= htmlspecialchars($commercial['series_code'] ?? '') ?>">
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="color-preview me-2"
|
||||||
|
style="background-color: <?= htmlspecialchars($commercial['color_code']) ?>; width: 20px; height: 20px; border-radius: 3px;">
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="commercial-title">
|
||||||
|
<?= htmlspecialchars($commercial['title']) ?>
|
||||||
|
</div>
|
||||||
|
<div class="commercial-duration">
|
||||||
|
<i class="bi bi-clock"></i> <?= $commercial['duration'] ?>
|
||||||
|
</div>
|
||||||
|
<?php if ($commercial['series_code']): ?>
|
||||||
|
<div class="commercial-series">
|
||||||
|
<i class="bi bi-tag"></i> <?= htmlspecialchars($commercial['series_code']) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-grip-vertical text-muted"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 p-2 bg-light rounded">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
Sleep commercials naar de kalender om te plannen
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<div class="card shadow-sm mt-3">
|
||||||
|
<div class="card-header bg-secondary text-white">
|
||||||
|
<h6 class="mb-0"><i class="bi bi-palette"></i> Legenda</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<div class="small">
|
||||||
|
<?php
|
||||||
|
$uniqueColors = [];
|
||||||
|
foreach ($commercials as $commercial) {
|
||||||
|
if ($commercial['series_code'] && !isset($uniqueColors[$commercial['series_code']])) {
|
||||||
|
$uniqueColors[$commercial['series_code']] = $commercial['color_code'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php foreach ($uniqueColors as $code => $color): ?>
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<div style="width: 20px; height: 20px; background-color: <?= $color ?>; border-radius: 3px; margin-right: 8px;"></div>
|
||||||
|
<span><?= htmlspecialchars($code) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calendar -->
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="calendar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event Details Modal -->
|
||||||
|
<div class="modal fade" id="eventModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="eventTitle">Uitzending Details</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="eventDetails">
|
||||||
|
<!-- Details will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger" id="deleteEventBtn">
|
||||||
|
<i class="bi bi-trash"></i> Verwijderen
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-success" id="syncEventBtn">
|
||||||
|
<i class="bi bi-cloud-upload"></i> Sync naar Talpa
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Sluiten</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Block Time Editor Modal -->
|
||||||
|
<div class="modal fade" id="blockTimeModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Blok Starttijd Aanpassen</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="blockDate">
|
||||||
|
<input type="hidden" id="blockChannel">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Nieuwe Starttijd</label>
|
||||||
|
<input type="time" class="form-control" id="blockStartTime">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="recalculateTransmissions" checked>
|
||||||
|
<label class="form-check-label" for="recalculateTransmissions">
|
||||||
|
Herbereken alle uitzendingen in dit blok
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info mt-3">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
Als je "herbereken" aanvinkt, worden alle uitzendingen in dit blok opnieuw getimed vanaf de nieuwe starttijd.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="saveBlockTime()">
|
||||||
|
<i class="bi bi-check-circle"></i> Opslaan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sync Block Modal -->
|
||||||
|
<div class="modal fade" id="syncBlockModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Blok Synchroniseren naar Talpa</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Datum</label>
|
||||||
|
<input type="date" class="form-control" id="syncBlockDate" value="<?= date('Y-m-d') ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Zender</label>
|
||||||
|
<select class="form-select" id="syncBlockChannel">
|
||||||
|
<option value="SBS9">SBS9</option>
|
||||||
|
<option value="NET5">NET5</option>
|
||||||
|
<option value="VERONICA">VERONICA</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
Dit synchroniseert alle uitzendingen in het geselecteerde blok naar Talpa.
|
||||||
|
Alleen uitzendingen die nog niet gesynchroniseerd zijn worden verzonden.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="syncBlockProgress" class="d-none">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
role="progressbar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center mt-2 mb-0">Synchroniseren...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="syncBlock()">
|
||||||
|
<i class="bi bi-cloud-upload"></i> Synchroniseren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar-scheduler@6.1.10/index.global.min.js'></script>
|
||||||
|
<script src="assets/js/calendar-init.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
437
commercials.php
Normal file
437
commercials.php
Normal file
@ -0,0 +1,437 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Commercial Management
|
||||||
|
* Enhanced version of commercial registration with color management
|
||||||
|
*/
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/TalpaAPI.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__);
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
$api = new TalpaApi();
|
||||||
|
$db = getDbConnection();
|
||||||
|
$apiLogs = [];
|
||||||
|
|
||||||
|
// Handle commercial registration
|
||||||
|
if (isset($_POST['add_commercial'])) {
|
||||||
|
$apiLogs[] = ['step' => 'Start registration', 'input' => $_POST];
|
||||||
|
|
||||||
|
$ep = $api->createEpisode($_POST['title'], $_POST['duration'], $_POST['season_id']);
|
||||||
|
$apiLogs[] = ['call' => 'Create Episode', 'request' => [
|
||||||
|
'title' => $_POST['title'],
|
||||||
|
'duration' => $_POST['duration'],
|
||||||
|
'season_id' => $_POST['season_id']
|
||||||
|
], 'response' => $api->lastResponse];
|
||||||
|
|
||||||
|
if (isset($ep['id'])) {
|
||||||
|
$apiLogs[] = ['step' => 'Episode created', 'episode_id' => $ep['id']];
|
||||||
|
|
||||||
|
$asset = $api->createMediaAsset($ep['id']);
|
||||||
|
$apiLogs[] = ['call' => 'Create Media Asset', 'request' => [
|
||||||
|
'content_id' => $ep['id']
|
||||||
|
], 'response' => $api->lastResponse];
|
||||||
|
|
||||||
|
if (isset($asset['id'])) {
|
||||||
|
$apiLogs[] = ['step' => 'Media asset created', 'asset_id' => $asset['id']];
|
||||||
|
|
||||||
|
$details = $api->getMediaAssetDetails($asset['id']);
|
||||||
|
$apiLogs[] = ['call' => 'Get Media Asset Details', 'request' => [
|
||||||
|
'asset_id' => $asset['id']
|
||||||
|
], 'response' => $api->lastResponse];
|
||||||
|
|
||||||
|
$label = $details['mediaAssetLabel'] ?? 'Pending';
|
||||||
|
$apiLogs[] = ['step' => 'Media asset label', 'label' => $label];
|
||||||
|
|
||||||
|
// Auto-generate color
|
||||||
|
$stmt = $db->query("SELECT color_code FROM commercials WHERE color_code IS NOT NULL");
|
||||||
|
$existingColors = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
$colorCode = generateDistinctColor($existingColors);
|
||||||
|
$apiLogs[] = ['step' => 'Color generated', 'color' => $colorCode];
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO commercials
|
||||||
|
(title, duration, season_id, content_id, media_asset_id, media_asset_label, upload_status, color_code, series_code)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$_POST['title'],
|
||||||
|
$_POST['duration'],
|
||||||
|
$_POST['season_id'],
|
||||||
|
$ep['id'],
|
||||||
|
$asset['id'],
|
||||||
|
$label,
|
||||||
|
$colorCode,
|
||||||
|
$_POST['series_code'] ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$apiLogs[] = ['step' => 'Database insert', 'success' => true];
|
||||||
|
|
||||||
|
header('Location: commercials.php?success=created');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$apiLogs[] = ['step' => 'Media asset creation failed', 'response' => $asset];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$apiLogs[] = ['step' => 'Episode creation failed', 'response' => $ep];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle media asset update
|
||||||
|
if (isset($_POST['update_media_asset'])) {
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
UPDATE commercials
|
||||||
|
SET media_asset_label = ?, upload_status = ?, series_code = ?, color_code = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$_POST['media_asset_label'],
|
||||||
|
$_POST['upload_status'],
|
||||||
|
$_POST['series_code'] ?? null,
|
||||||
|
$_POST['color_code'],
|
||||||
|
$_POST['commercial_id']
|
||||||
|
]);
|
||||||
|
header('Location: commercials.php?success=updated');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle delete
|
||||||
|
if (isset($_POST['delete_commercial'])) {
|
||||||
|
// Check if commercial is used in transmissions
|
||||||
|
$stmt = $db->prepare("SELECT COUNT(*) FROM transmissions WHERE commercial_id = ?");
|
||||||
|
$stmt->execute([$_POST['commercial_id']]);
|
||||||
|
$count = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($count > 0) {
|
||||||
|
header('Location: commercials.php?error=in_use');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get commercial details before deletion
|
||||||
|
$stmt = $db->prepare("SELECT content_id, media_asset_id FROM commercials WHERE id = ?");
|
||||||
|
$stmt->execute([$_POST['commercial_id']]);
|
||||||
|
$commercial = $stmt->fetch();
|
||||||
|
|
||||||
|
// Delete from Talpa API if content_id exists
|
||||||
|
if ($commercial && $commercial['content_id']) {
|
||||||
|
try {
|
||||||
|
$api->deleteEpisode($commercial['content_id']);
|
||||||
|
$apiLogs[] = ['call' => 'Delete Episode', 'response' => $api->lastResponse];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Log error but continue with local deletion
|
||||||
|
error_log("Failed to delete episode from Talpa: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete from local database
|
||||||
|
$stmt = $db->prepare("DELETE FROM commercials WHERE id = ?");
|
||||||
|
$stmt->execute([$_POST['commercial_id']]);
|
||||||
|
header('Location: commercials.php?success=deleted');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all commercials
|
||||||
|
$commercials = $db->query("
|
||||||
|
SELECT c.*,
|
||||||
|
(SELECT COUNT(*) FROM transmissions WHERE commercial_id = c.id) as usage_count
|
||||||
|
FROM commercials c
|
||||||
|
ORDER BY c.created_at DESC
|
||||||
|
")->fetchAll();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Commercials - Telvero Talpa</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="index.php">
|
||||||
|
<i class="bi bi-tv"></i> Telvero Talpa Planner
|
||||||
|
</a>
|
||||||
|
<div class="navbar-nav">
|
||||||
|
<a class="nav-link" href="index.php">Dashboard</a>
|
||||||
|
<a class="nav-link" href="planner.php">Excel Planner</a>
|
||||||
|
<a class="nav-link" href="calendar.php">Kalender</a>
|
||||||
|
<a class="nav-link" href="blocks.php">Blokken</a>
|
||||||
|
<a class="nav-link active" href="commercials.php">Commercials</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><i class="bi bi-collection-play"></i> Commercial Management</h1>
|
||||||
|
<a href="calendar.php" class="btn btn-primary">
|
||||||
|
<i class="bi bi-calendar-week"></i> Naar Kalender
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($_GET['success'])): ?>
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<?php
|
||||||
|
$messages = [
|
||||||
|
'created' => 'Commercial succesvol aangemaakt en geregistreerd bij Talpa!',
|
||||||
|
'updated' => 'Commercial succesvol bijgewerkt!',
|
||||||
|
'deleted' => 'Commercial succesvol verwijderd!'
|
||||||
|
];
|
||||||
|
echo $messages[$_GET['success']] ?? 'Actie succesvol uitgevoerd!';
|
||||||
|
?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (isset($_GET['error'])): ?>
|
||||||
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
|
<?php
|
||||||
|
$messages = [
|
||||||
|
'in_use' => 'Deze commercial kan niet verwijderd worden omdat deze nog in gebruik is in de planning!'
|
||||||
|
];
|
||||||
|
echo $messages[$_GET['error']] ?? 'Er is een fout opgetreden!';
|
||||||
|
?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Registration Form -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-plus-circle"></i> Nieuwe Commercial Registreren
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Product Naam</label>
|
||||||
|
<input type="text" name="title" class="form-control"
|
||||||
|
placeholder="bijv. Clever Cane" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Duur (HH:MM:SS)</label>
|
||||||
|
<input type="text" name="duration" class="form-control"
|
||||||
|
placeholder="00:30:00" pattern="[0-9]{2}:[0-9]{2}:[0-9]{2}" required>
|
||||||
|
<small class="text-muted">Formaat: UU:MM:SS</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Series Code (optioneel)</label>
|
||||||
|
<input type="text" name="series_code" class="form-control"
|
||||||
|
placeholder="bijv. 006a">
|
||||||
|
<small class="text-muted">Voor groepering in kalender</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="season_id" value="<?= $_ENV['TV_SEASON_ID'] ?>">
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" name="add_commercial" class="btn btn-primary">
|
||||||
|
<i class="bi bi-cloud-upload"></i> Registreren bij Talpa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-3 p-2 bg-light rounded">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
Dit registreert de commercial bij Talpa en maakt automatisch een media asset aan.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Commercial List -->
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-secondary text-white">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Geregistreerde Commercials</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<?php if (empty($commercials)): ?>
|
||||||
|
<div class="p-4 text-center text-muted">
|
||||||
|
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
|
||||||
|
<p class="mt-2">Nog geen commercials geregistreerd</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Kleur</th>
|
||||||
|
<th>Titel</th>
|
||||||
|
<th>Duur</th>
|
||||||
|
<th>Series</th>
|
||||||
|
<th>Label</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Gebruik</th>
|
||||||
|
<th>Acties</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($commercials as $c): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="color-preview"
|
||||||
|
style="background-color: <?= htmlspecialchars($c['color_code'] ?? '#cccccc') ?>;"
|
||||||
|
title="<?= htmlspecialchars($c['color_code'] ?? '#cccccc') ?>">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong><?= htmlspecialchars($c['title']) ?></strong>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">ID: <?= htmlspecialchars($c['content_id']) ?></small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info"><?= $c['duration'] ?></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?= $c['series_code'] ? '<span class="badge bg-secondary">' . htmlspecialchars($c['series_code']) . '</span>' : '-' ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code class="small"><?= htmlspecialchars($c['media_asset_label']) ?></code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if ($c['upload_status'] == 'uploaded'): ?>
|
||||||
|
<span class="badge bg-success">Uploaded</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge bg-warning text-dark">Pending</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-<?= $c['usage_count'] > 0 ? 'primary' : 'secondary' ?>">
|
||||||
|
<?= $c['usage_count'] ?>x
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#editModal<?= $c['id'] ?>"
|
||||||
|
title="Bewerken">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
<?php if ($c['usage_count'] == 0): ?>
|
||||||
|
<form method="POST" style="display:inline;"
|
||||||
|
onsubmit="return confirm('Weet je zeker dat je deze commercial wilt verwijderen?');">
|
||||||
|
<input type="hidden" name="commercial_id" value="<?= $c['id'] ?>">
|
||||||
|
<button type="submit" name="delete_commercial"
|
||||||
|
class="btn btn-outline-danger" title="Verwijderen">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Modals (outside the table loop) -->
|
||||||
|
<?php foreach ($commercials as $c): ?>
|
||||||
|
<div class="modal fade" id="editModal<?= $c['id'] ?>" tabindex="-1" aria-labelledby="editModalLabel<?= $c['id'] ?>" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="commercial_id" value="<?= $c['id'] ?>">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Bewerk: <?= htmlspecialchars($c['title']) ?></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Media Asset Label</label>
|
||||||
|
<input type="text" name="media_asset_label" class="form-control"
|
||||||
|
value="<?= htmlspecialchars($c['media_asset_label']) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Upload Status</label>
|
||||||
|
<select name="upload_status" class="form-select">
|
||||||
|
<option value="pending" <?= $c['upload_status'] == 'pending' ? 'selected' : '' ?>>Pending</option>
|
||||||
|
<option value="uploaded" <?= $c['upload_status'] == 'uploaded' ? 'selected' : '' ?>>Uploaded</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Series Code</label>
|
||||||
|
<input type="text" name="series_code" class="form-control"
|
||||||
|
value="<?= htmlspecialchars($c['series_code'] ?? '') ?>"
|
||||||
|
placeholder="bijv. 006a">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Kleurcode</label>
|
||||||
|
<input type="color" name="color_code" class="form-control form-control-color"
|
||||||
|
value="<?= htmlspecialchars($c['color_code'] ?? '#cccccc') ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
|
||||||
|
<button type="submit" name="update_media_asset" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-circle"></i> Opslaan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// API Logs to console with enhanced debugging
|
||||||
|
const apiLogs = <?= json_encode($apiLogs) ?>;
|
||||||
|
if (apiLogs.length > 0) {
|
||||||
|
console.group("🔍 Talpa API Debug Logs - Commercial Registration");
|
||||||
|
apiLogs.forEach((log, index) => {
|
||||||
|
if (log.call) {
|
||||||
|
console.group(`${index + 1}. ${log.call}`);
|
||||||
|
if (log.request) {
|
||||||
|
console.log("%cRequest:", "color: #3498db; font-weight: bold;", log.request);
|
||||||
|
}
|
||||||
|
console.log("%cResponse:", "color: #2ecc71; font-weight: bold;", log.response);
|
||||||
|
console.groupEnd();
|
||||||
|
} else if (log.step) {
|
||||||
|
console.log(`%c${log.step}`, "color: #9b59b6; font-weight: bold;", log);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
// Show summary
|
||||||
|
const hasErrors = apiLogs.some(log =>
|
||||||
|
(log.step && log.step.includes('failed')) ||
|
||||||
|
(log.response && log.response.error)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
console.warn("⚠️ Er zijn fouten opgetreden tijdens de registratie. Zie bovenstaande logs voor details.");
|
||||||
|
} else {
|
||||||
|
console.log("✅ Commercial registratie succesvol voltooid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
387
helpers.php
Normal file
387
helpers.php
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Helper Functions for Telvero Talpa Planning System
|
||||||
|
* Contains utility functions for color generation, time calculations, and data formatting
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a distinct color that doesn't exist in the provided array
|
||||||
|
* @param array $existingColors Array of existing hex color codes
|
||||||
|
* @return string Hex color code
|
||||||
|
*/
|
||||||
|
function generateDistinctColor($existingColors = []) {
|
||||||
|
// Predefined palette with good contrast and visibility
|
||||||
|
$colorPalette = [
|
||||||
|
'#3498db', // Blue
|
||||||
|
'#e74c3c', // Red
|
||||||
|
'#9b59b6', // Purple
|
||||||
|
'#2ecc71', // Green
|
||||||
|
'#f39c12', // Orange
|
||||||
|
'#1abc9c', // Turquoise
|
||||||
|
'#e67e22', // Dark Orange
|
||||||
|
'#34495e', // Dark Gray
|
||||||
|
'#16a085', // Sea Green
|
||||||
|
'#c0392b', // Dark Red
|
||||||
|
'#8e44ad', // Dark Purple
|
||||||
|
'#27ae60', // Dark Green
|
||||||
|
'#d35400', // Pumpkin
|
||||||
|
'#2980b9', // Belize Blue
|
||||||
|
'#8e44ad', // Wisteria
|
||||||
|
'#16a085', // Green Sea
|
||||||
|
'#c0392b', // Pomegranate
|
||||||
|
'#f39c12', // Sun Flower
|
||||||
|
];
|
||||||
|
|
||||||
|
// Find first color not in existing colors
|
||||||
|
foreach ($colorPalette as $color) {
|
||||||
|
if (!in_array($color, $existingColors)) {
|
||||||
|
return $color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all predefined colors are used, generate random distinct color
|
||||||
|
return generateRandomColor($existingColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random color with good saturation and lightness
|
||||||
|
* @param array $existingColors Array of existing colors to avoid
|
||||||
|
* @return string Hex color code
|
||||||
|
*/
|
||||||
|
function generateRandomColor($existingColors = []) {
|
||||||
|
$maxAttempts = 50;
|
||||||
|
$attempt = 0;
|
||||||
|
|
||||||
|
while ($attempt < $maxAttempts) {
|
||||||
|
$hue = rand(0, 360);
|
||||||
|
$saturation = rand(60, 90); // Good saturation
|
||||||
|
$lightness = rand(45, 65); // Good visibility
|
||||||
|
|
||||||
|
$color = hslToHex($hue, $saturation, $lightness);
|
||||||
|
|
||||||
|
// Check if color is distinct enough from existing colors
|
||||||
|
if (isColorDistinct($color, $existingColors)) {
|
||||||
|
return $color;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attempt++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to a random color
|
||||||
|
return sprintf('#%06X', mt_rand(0, 0xFFFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a color is distinct from existing colors
|
||||||
|
* @param string $newColor Hex color code
|
||||||
|
* @param array $existingColors Array of existing hex color codes
|
||||||
|
* @return bool True if distinct enough
|
||||||
|
*/
|
||||||
|
function isColorDistinct($newColor, $existingColors, $threshold = 50) {
|
||||||
|
if (empty($existingColors)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
list($r1, $g1, $b1) = sscanf($newColor, "#%02x%02x%02x");
|
||||||
|
|
||||||
|
foreach ($existingColors as $existingColor) {
|
||||||
|
list($r2, $g2, $b2) = sscanf($existingColor, "#%02x%02x%02x");
|
||||||
|
|
||||||
|
// Calculate color distance (Euclidean distance in RGB space)
|
||||||
|
$distance = sqrt(
|
||||||
|
pow($r1 - $r2, 2) +
|
||||||
|
pow($g1 - $g2, 2) +
|
||||||
|
pow($b1 - $b2, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($distance < $threshold) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert HSL to Hex color
|
||||||
|
* @param int $h Hue (0-360)
|
||||||
|
* @param int $s Saturation (0-100)
|
||||||
|
* @param int $l Lightness (0-100)
|
||||||
|
* @return string Hex color code
|
||||||
|
*/
|
||||||
|
function hslToHex($h, $s, $l) {
|
||||||
|
$h /= 360;
|
||||||
|
$s /= 100;
|
||||||
|
$l /= 100;
|
||||||
|
|
||||||
|
if ($s == 0) {
|
||||||
|
$r = $g = $b = $l;
|
||||||
|
} else {
|
||||||
|
$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
|
||||||
|
$p = 2 * $l - $q;
|
||||||
|
|
||||||
|
$r = hueToRgb($p, $q, $h + 1/3);
|
||||||
|
$g = hueToRgb($p, $q, $h);
|
||||||
|
$b = hueToRgb($p, $q, $h - 1/3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf("#%02x%02x%02x", round($r * 255), round($g * 255), round($b * 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for HSL to RGB conversion
|
||||||
|
*/
|
||||||
|
function hueToRgb($p, $q, $t) {
|
||||||
|
if ($t < 0) $t += 1;
|
||||||
|
if ($t > 1) $t -= 1;
|
||||||
|
if ($t < 1/6) return $p + ($q - $p) * 6 * $t;
|
||||||
|
if ($t < 1/2) return $q;
|
||||||
|
if ($t < 2/3) return $p + ($q - $p) * (2/3 - $t) * 6;
|
||||||
|
return $p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the next start time based on previous transmissions in a block
|
||||||
|
* @param PDO $db Database connection
|
||||||
|
* @param string $blockDate Date in Y-m-d format
|
||||||
|
* @param string $channel Channel name
|
||||||
|
* @param string $blockStartTime Optional block start time override
|
||||||
|
* @return string Time in H:i:s format
|
||||||
|
*/
|
||||||
|
function calculateNextStartTime($db, $blockDate, $channel, $blockStartTime = null) {
|
||||||
|
// Get all transmissions for this block, ordered by time
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT start_time, duration
|
||||||
|
FROM transmissions
|
||||||
|
WHERE start_date = ? AND channel = ?
|
||||||
|
ORDER BY start_time ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$blockDate, $channel]);
|
||||||
|
$transmissions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// If no transmissions, return block start time
|
||||||
|
if (empty($transmissions)) {
|
||||||
|
if ($blockStartTime) {
|
||||||
|
return $blockStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get block start time from daily_blocks or template
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT actual_start_time
|
||||||
|
FROM daily_blocks
|
||||||
|
WHERE block_date = ? AND channel = ?
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute([$blockDate, $channel]);
|
||||||
|
$time = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
return $time ?: '07:00:00'; // Default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate end time of last transmission
|
||||||
|
$lastTx = end($transmissions);
|
||||||
|
$startTime = new DateTime($lastTx['start_time']);
|
||||||
|
|
||||||
|
// Add duration to get end time
|
||||||
|
list($h, $m, $s) = explode(':', $lastTx['duration']);
|
||||||
|
$interval = new DateInterval("PT{$h}H{$m}M{$s}S");
|
||||||
|
$startTime->add($interval);
|
||||||
|
|
||||||
|
return $startTime->format('H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert time string to seconds
|
||||||
|
* @param string $time Time in HH:MM:SS format
|
||||||
|
* @return int Seconds
|
||||||
|
*/
|
||||||
|
function timeToSeconds($time) {
|
||||||
|
list($h, $m, $s) = explode(':', $time);
|
||||||
|
return ($h * 3600) + ($m * 60) + $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert seconds to time string
|
||||||
|
* @param int $seconds Seconds
|
||||||
|
* @return string Time in HH:MM:SS format
|
||||||
|
*/
|
||||||
|
function secondsToTime($seconds) {
|
||||||
|
$hours = floor($seconds / 3600);
|
||||||
|
$minutes = floor(($seconds % 3600) / 60);
|
||||||
|
$secs = $seconds % 60;
|
||||||
|
|
||||||
|
return sprintf('%02d:%02d:%02d', $hours, $minutes, $secs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add time duration to a time string
|
||||||
|
* @param string $startTime Time in HH:MM:SS format
|
||||||
|
* @param string $duration Duration in HH:MM:SS format
|
||||||
|
* @return string Resulting time in HH:MM:SS format
|
||||||
|
*/
|
||||||
|
function addTimeToTime($startTime, $duration) {
|
||||||
|
$startSeconds = timeToSeconds($startTime);
|
||||||
|
$durationSeconds = timeToSeconds($duration);
|
||||||
|
$totalSeconds = $startSeconds + $durationSeconds;
|
||||||
|
|
||||||
|
return secondsToTime($totalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if two time ranges overlap
|
||||||
|
* @param string $start1 Start time 1
|
||||||
|
* @param string $end1 End time 1
|
||||||
|
* @param string $start2 Start time 2
|
||||||
|
* @param string $end2 End time 2
|
||||||
|
* @return bool True if overlap exists
|
||||||
|
*/
|
||||||
|
function timeRangesOverlap($start1, $end1, $start2, $end2) {
|
||||||
|
$s1 = timeToSeconds($start1);
|
||||||
|
$e1 = timeToSeconds($end1);
|
||||||
|
$s2 = timeToSeconds($start2);
|
||||||
|
$e2 = timeToSeconds($end2);
|
||||||
|
|
||||||
|
return ($s1 < $e2) && ($e1 > $s2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create daily blocks for a specific date range
|
||||||
|
* @param PDO $db Database connection
|
||||||
|
* @param string $startDate Start date
|
||||||
|
* @param string $endDate End date
|
||||||
|
* @return array Array of daily blocks
|
||||||
|
*/
|
||||||
|
function ensureDailyBlocks($db, $startDate, $endDate) {
|
||||||
|
$start = new DateTime($startDate);
|
||||||
|
$end = new DateTime($endDate);
|
||||||
|
$interval = new DateInterval('P1D');
|
||||||
|
$period = new DatePeriod($start, $interval, $end->modify('+1 day'));
|
||||||
|
|
||||||
|
$dayMap = [
|
||||||
|
'Monday' => 'ma',
|
||||||
|
'Tuesday' => 'di',
|
||||||
|
'Wednesday' => 'wo',
|
||||||
|
'Thursday' => 'do',
|
||||||
|
'Friday' => 'vr',
|
||||||
|
'Saturday' => 'za',
|
||||||
|
'Sunday' => 'zo'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($period as $date) {
|
||||||
|
$dateStr = $date->format('Y-m-d');
|
||||||
|
$dayOfWeek = $dayMap[$date->format('l')];
|
||||||
|
|
||||||
|
// Get active templates for this day
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT * FROM block_templates
|
||||||
|
WHERE is_active = 1
|
||||||
|
AND (day_of_week = ? OR day_of_week = 'all')
|
||||||
|
");
|
||||||
|
$stmt->execute([$dayOfWeek]);
|
||||||
|
$templates = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
foreach ($templates as $template) {
|
||||||
|
// Check if daily block already exists for this template
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT id FROM daily_blocks
|
||||||
|
WHERE block_date = ? AND channel = ? AND template_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$dateStr,
|
||||||
|
$template['channel'],
|
||||||
|
$template['id']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$stmt->fetch()) {
|
||||||
|
// Create daily block from template
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT IGNORE INTO daily_blocks
|
||||||
|
(template_id, channel, block_date, actual_start_time, actual_end_time)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
$template['id'],
|
||||||
|
$template['channel'],
|
||||||
|
$dateStr,
|
||||||
|
$template['default_start_time'],
|
||||||
|
$template['default_end_time']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date for display in Dutch
|
||||||
|
* @param string $date Date string
|
||||||
|
* @return string Formatted date
|
||||||
|
*/
|
||||||
|
function formatDateDutch($date) {
|
||||||
|
$dt = new DateTime($date);
|
||||||
|
$dayNames = ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'];
|
||||||
|
$monthNames = [
|
||||||
|
'', 'januari', 'februari', 'maart', 'april', 'mei', 'juni',
|
||||||
|
'juli', 'augustus', 'september', 'oktober', 'november', 'december'
|
||||||
|
];
|
||||||
|
|
||||||
|
$dayOfWeek = $dayNames[$dt->format('w')];
|
||||||
|
$day = $dt->format('j');
|
||||||
|
$month = $monthNames[(int)$dt->format('n')];
|
||||||
|
$year = $dt->format('Y');
|
||||||
|
|
||||||
|
return ucfirst($dayOfWeek) . ' ' . $day . ' ' . $month . ' ' . $year;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database connection
|
||||||
|
* @return PDO Database connection
|
||||||
|
*/
|
||||||
|
function getDbConnection() {
|
||||||
|
static $db = null;
|
||||||
|
|
||||||
|
if ($db === null) {
|
||||||
|
$db = new PDO(
|
||||||
|
"mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']};charset=utf8mb4",
|
||||||
|
$_ENV['DB_USER'],
|
||||||
|
$_ENV['DB_PASS'],
|
||||||
|
[
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send JSON response
|
||||||
|
* @param mixed $data Data to send
|
||||||
|
* @param int $statusCode HTTP status code
|
||||||
|
*/
|
||||||
|
function jsonResponse($data, $statusCode = 200) {
|
||||||
|
http_response_code($statusCode);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($data);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate time format (HH:MM:SS or HH:MM)
|
||||||
|
* @param string $time Time string
|
||||||
|
* @return bool True if valid
|
||||||
|
*/
|
||||||
|
function isValidTime($time) {
|
||||||
|
return preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/', $time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate date format (YYYY-MM-DD)
|
||||||
|
* @param string $date Date string
|
||||||
|
* @return bool True if valid
|
||||||
|
*/
|
||||||
|
function isValidDate($date) {
|
||||||
|
$d = DateTime::createFromFormat('Y-m-d', $date);
|
||||||
|
return $d && $d->format('Y-m-d') === $date;
|
||||||
|
}
|
||||||
172
index.php
172
index.php
@ -101,13 +101,133 @@ if (isset($_GET['edit'])) {
|
|||||||
<html lang="nl">
|
<html lang="nl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Telvero Talpa Control Panel</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard - Telvero Talpa</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<div class="container mt-5">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
<h1>Telvero Homeshopping Planner</h1>
|
<div class="container-fluid">
|
||||||
<div class="row mt-4">
|
<a class="navbar-brand" href="index.php">
|
||||||
|
<i class="bi bi-tv"></i> Telvero Talpa Planner
|
||||||
|
</a>
|
||||||
|
<div class="navbar-nav">
|
||||||
|
<a class="nav-link active" href="index.php">Dashboard</a>
|
||||||
|
<a class="nav-link" href="planner.php">Excel Planner</a>
|
||||||
|
<a class="nav-link" href="calendar.php">Kalender</a>
|
||||||
|
<a class="nav-link" href="blocks.php">Blokken</a>
|
||||||
|
<a class="nav-link" href="commercials.php">Commercials</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><i class="bi bi-speedometer2"></i> Dashboard</h1>
|
||||||
|
<a href="calendar.php" class="btn btn-primary btn-lg">
|
||||||
|
<i class="bi bi-calendar-week"></i> Open Kalender
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics Cards -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<?php
|
||||||
|
$totalCommercials = $db->query("SELECT COUNT(*) FROM commercials")->fetchColumn();
|
||||||
|
$uploadedCommercials = $db->query("SELECT COUNT(*) FROM commercials WHERE upload_status = 'uploaded'")->fetchColumn();
|
||||||
|
$totalTransmissions = $db->query("SELECT COUNT(*) FROM transmissions")->fetchColumn();
|
||||||
|
$pendingSync = $db->query("SELECT COUNT(*) FROM transmissions WHERE api_status = 'pending'")->fetchColumn();
|
||||||
|
$syncedTransmissions = $db->query("SELECT COUNT(*) FROM transmissions WHERE api_status = 'synced'")->fetchColumn();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number"><?= $totalCommercials ?></div>
|
||||||
|
<div class="stat-label">Totaal Commercials</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card success">
|
||||||
|
<div class="stat-number"><?= $uploadedCommercials ?></div>
|
||||||
|
<div class="stat-label">Geüpload</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card info">
|
||||||
|
<div class="stat-number"><?= $totalTransmissions ?></div>
|
||||||
|
<div class="stat-label">Geplande Uitzendingen</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="stat-card warning">
|
||||||
|
<div class="stat-number"><?= $pendingSync ?></div>
|
||||||
|
<div class="stat-label">Wacht op Sync</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mb-3"><i class="bi bi-grid-3x3"></i> Snelle Acties</h2>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-table" style="font-size: 3rem; color: #2ecc71;"></i>
|
||||||
|
<h5 class="mt-3">Excel Planner</h5>
|
||||||
|
<p class="text-muted">Tabel-gebaseerde planning</p>
|
||||||
|
<a href="planner.php" class="btn btn-success">
|
||||||
|
<i class="bi bi-arrow-right-circle"></i> Open Planner
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-calendar-plus" style="font-size: 3rem; color: #3498db;"></i>
|
||||||
|
<h5 class="mt-3">Kalender View</h5>
|
||||||
|
<p class="text-muted">Visuele tijdlijn planning</p>
|
||||||
|
<a href="calendar.php" class="btn btn-primary">
|
||||||
|
<i class="bi bi-arrow-right-circle"></i> Open Kalender
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-collection-play" style="font-size: 3rem; color: #e74c3c;"></i>
|
||||||
|
<h5 class="mt-3">Commercials</h5>
|
||||||
|
<p class="text-muted">Registreer en beheer</p>
|
||||||
|
<a href="commercials.php" class="btn btn-danger">
|
||||||
|
<i class="bi bi-arrow-right-circle"></i> Naar Commercials
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="bi bi-calendar3" style="font-size: 3rem; color: #9b59b6;"></i>
|
||||||
|
<h5 class="mt-3">Blok Templates</h5>
|
||||||
|
<p class="text-muted">Beheer tijdblokken</p>
|
||||||
|
<a href="blocks.php" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-arrow-right-circle"></i> Naar Blokken
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mb-3"><i class="bi bi-clock-history"></i> Recente Activiteit</h2>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card p-3 shadow-sm">
|
<div class="card p-3 shadow-sm">
|
||||||
<h5>1. Commercial Registreren</h5>
|
<h5>1. Commercial Registreren</h5>
|
||||||
@ -160,15 +280,28 @@ if (isset($_GET['edit'])) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-5">
|
<hr class="my-4">
|
||||||
|
|
||||||
<h3>Dagplanning: <?= htmlspecialchars($selectedDate) ?></h3>
|
<h2 class="mb-3"><i class="bi bi-calendar-check"></i> Vandaag's Planning</h2>
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
<form method="GET" class="row g-2 mb-3">
|
<form method="GET" class="row g-2 mb-3">
|
||||||
<div class="col-md-3"><input type="date" name="view_date" class="form-control" value="<?= $selectedDate ?>"></div>
|
<div class="col-md-3">
|
||||||
<div class="col-md-2"><button type="submit" class="btn btn-secondary">Bekijk Datum</button></div>
|
<input type="date" name="view_date" class="form-control" value="<?= $selectedDate ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-search"></i> Bekijk Datum
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-7 text-end">
|
||||||
|
<a href="calendar.php" class="btn btn-primary">
|
||||||
|
<i class="bi bi-calendar-week"></i> Open in Kalender View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<table class="table table-bordered bg-white mb-5 text-center align-middle">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
<tr><th>Tijd</th><th>Product</th><th>Duur</th><th>Sync Status</th><th>Acties</th></tr>
|
<tr><th>Tijd</th><th>Product</th><th>Duur</th><th>Sync Status</th><th>Acties</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -196,10 +329,14 @@ if (isset($_GET['edit'])) {
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Media Asset Management</h3>
|
<h2 class="mt-4 mb-3"><i class="bi bi-file-earmark-text"></i> Media Asset Management</h2>
|
||||||
<div class="card p-3 shadow-sm">
|
|
||||||
<table class="table table-striped bg-white align-middle">
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-secondary">
|
<thead class="table-secondary">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Titel</th>
|
<th>Titel</th>
|
||||||
@ -235,8 +372,17 @@ if (isset($_GET['edit'])) {
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="mt-5 py-4 bg-dark text-white text-center">
|
||||||
|
<div class="container">
|
||||||
|
<p class="mb-0">
|
||||||
|
<i class="bi bi-tv"></i> Telvero Talpa Planning System © 2026
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Bestaande JS voor duur selectie
|
// Bestaande JS voor duur selectie
|
||||||
|
|||||||
64
migrations/001_add_blocks_and_colors.sql
Normal file
64
migrations/001_add_blocks_and_colors.sql
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
-- Migration: Add block templates, daily blocks, and color coding
|
||||||
|
-- Date: 2026-01-13
|
||||||
|
-- Description: Extends existing schema for TV planning system
|
||||||
|
|
||||||
|
-- Add color and series code to commercials table
|
||||||
|
ALTER TABLE commercials
|
||||||
|
ADD COLUMN color_code VARCHAR(7) DEFAULT NULL COMMENT 'Hex color code for visual identification',
|
||||||
|
ADD COLUMN series_code VARCHAR(20) DEFAULT NULL COMMENT 'Series identifier (e.g., 006a, 007a)';
|
||||||
|
|
||||||
|
-- Create block templates table for recurring time slots
|
||||||
|
CREATE TABLE IF NOT EXISTS block_templates (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
channel VARCHAR(50) NOT NULL COMMENT 'Channel name (e.g., SBS9, NET5)',
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT 'Template name (e.g., "SBS9 Dagblok")',
|
||||||
|
day_of_week ENUM('ma','di','wo','do','vr','za','zo','all') DEFAULT 'all' COMMENT 'Day of week or all days',
|
||||||
|
default_start_time TIME NOT NULL COMMENT 'Default start time for this block',
|
||||||
|
default_end_time TIME DEFAULT NULL COMMENT 'Optional end time',
|
||||||
|
is_active BOOLEAN DEFAULT 1 COMMENT 'Whether this template is currently active',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_channel (channel),
|
||||||
|
INDEX idx_active (is_active)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Create daily blocks table for actual daily scheduling
|
||||||
|
CREATE TABLE IF NOT EXISTS daily_blocks (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
template_id INT DEFAULT NULL COMMENT 'Reference to template (nullable for manual blocks)',
|
||||||
|
channel VARCHAR(50) NOT NULL,
|
||||||
|
block_date DATE NOT NULL,
|
||||||
|
actual_start_time TIME NOT NULL COMMENT 'Actual start time for this specific day',
|
||||||
|
actual_end_time TIME DEFAULT NULL,
|
||||||
|
is_locked BOOLEAN DEFAULT 0 COMMENT 'Prevent accidental modifications',
|
||||||
|
notes TEXT DEFAULT NULL COMMENT 'Optional notes for this block',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (template_id) REFERENCES block_templates(id) ON DELETE SET NULL,
|
||||||
|
UNIQUE KEY unique_channel_date_template (channel, block_date, template_id),
|
||||||
|
INDEX idx_date (block_date),
|
||||||
|
INDEX idx_channel_date (channel, block_date)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Insert default block templates for SBS9
|
||||||
|
INSERT INTO block_templates (channel, name, day_of_week, default_start_time, default_end_time, is_active) VALUES
|
||||||
|
('SBS9', 'SBS9 Dagblok', 'all', '07:00:00', '15:00:00', 1),
|
||||||
|
('SBS9', 'SBS9 Nachtblok', 'all', '23:30:00', '02:00:00', 1);
|
||||||
|
|
||||||
|
-- Insert default block templates for Net5
|
||||||
|
INSERT INTO block_templates (channel, name, day_of_week, default_start_time, default_end_time, is_active) VALUES
|
||||||
|
('NET5', 'Net5 Ochtendblok', 'ma', '07:30:00', '11:30:00', 1),
|
||||||
|
('NET5', 'Net5 Ochtendblok', 'di', '07:30:00', '11:30:00', 1),
|
||||||
|
('NET5', 'Net5 Ochtendblok', 'wo', '07:30:00', '11:30:00', 1),
|
||||||
|
('NET5', 'Net5 Ochtendblok', 'do', '07:30:00', '11:30:00', 1),
|
||||||
|
('NET5', 'Net5 Ochtendblok', 'vr', '07:30:00', '11:30:00', 1),
|
||||||
|
('NET5', 'Net5 Middagblok', 'ma', '13:20:00', '13:50:00', 1),
|
||||||
|
('NET5', 'Net5 Middagblok', 'di', '13:20:00', '13:50:00', 1),
|
||||||
|
('NET5', 'Net5 Middagblok', 'wo', '13:20:00', '13:50:00', 1),
|
||||||
|
('NET5', 'Net5 Middagblok', 'do', '13:20:00', '13:50:00', 1),
|
||||||
|
('NET5', 'Net5 Middagblok', 'vr', '13:20:00', '13:50:00', 1);
|
||||||
|
|
||||||
|
-- Add index to transmissions for better performance
|
||||||
|
ALTER TABLE transmissions
|
||||||
|
ADD INDEX idx_channel_date (channel, start_date),
|
||||||
|
ADD INDEX idx_date_time (start_date, start_time);
|
||||||
825
planner.php
Normal file
825
planner.php
Normal file
@ -0,0 +1,825 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Excel-Style Planning View
|
||||||
|
* Table-based planning interface - both channels side by side
|
||||||
|
*/
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__);
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
$db = getDbConnection();
|
||||||
|
|
||||||
|
// Handle adding transmission to block
|
||||||
|
if (isset($_POST['add_to_block'])) {
|
||||||
|
$commercialId = $_POST['commercial_id'];
|
||||||
|
$channel = $_POST['channel'];
|
||||||
|
$date = $_POST['date'];
|
||||||
|
$blockId = $_POST['block_id'] ?? null;
|
||||||
|
|
||||||
|
// Get commercial duration
|
||||||
|
$stmt = $db->prepare("SELECT duration FROM commercials WHERE id = ?");
|
||||||
|
$stmt->execute([$commercialId]);
|
||||||
|
$duration = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
// Calculate next start time for this specific block
|
||||||
|
$nextStartTime = calculateNextStartTimeForBlock($db, $date, $channel, $blockId);
|
||||||
|
|
||||||
|
// Insert transmission
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO transmissions
|
||||||
|
(commercial_id, channel, template, start_date, start_time, duration, api_status)
|
||||||
|
VALUES (?, ?, 'HOME030', ?, ?, ?, 'pending')
|
||||||
|
");
|
||||||
|
$stmt->execute([$commercialId, $channel, $date, $nextStartTime, $duration]);
|
||||||
|
|
||||||
|
header("Location: planner.php?date=$date&success=added");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle remove transmission
|
||||||
|
if (isset($_POST['remove_transmission'])) {
|
||||||
|
$stmt = $db->prepare("DELETE FROM transmissions WHERE id = ?");
|
||||||
|
$stmt->execute([$_POST['transmission_id']]);
|
||||||
|
|
||||||
|
header("Location: planner.php?date={$_POST['date']}&success=removed");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle reorder (move up/down)
|
||||||
|
if (isset($_POST['reorder'])) {
|
||||||
|
$transmissionId = $_POST['transmission_id'];
|
||||||
|
$direction = $_POST['direction']; // 'up' or 'down'
|
||||||
|
$date = $_POST['date'];
|
||||||
|
$channel = $_POST['channel'];
|
||||||
|
$blockId = $_POST['block_id'];
|
||||||
|
|
||||||
|
// Get block info including end time
|
||||||
|
$stmt = $db->prepare("SELECT actual_start_time, actual_end_time FROM daily_blocks WHERE id = ?");
|
||||||
|
$stmt->execute([$blockId]);
|
||||||
|
$blockInfo = $stmt->fetch();
|
||||||
|
$blockStartTime = $blockInfo['actual_start_time'];
|
||||||
|
$blockEndTime = $blockInfo['actual_end_time'] ?? '23:59:59';
|
||||||
|
|
||||||
|
// Get all transmissions for THIS SPECIFIC block only
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.id, t.start_time, c.duration
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.start_date = ? AND t.channel = ?
|
||||||
|
AND t.start_time >= ? AND t.start_time < ?
|
||||||
|
ORDER BY t.start_time ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$date, $channel, $blockStartTime, $blockEndTime]);
|
||||||
|
$transmissions = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Find current position
|
||||||
|
$currentIndex = array_search($transmissionId, array_column($transmissions, 'id'));
|
||||||
|
|
||||||
|
if ($currentIndex !== false) {
|
||||||
|
if ($direction === 'up' && $currentIndex > 0) {
|
||||||
|
// Swap with previous
|
||||||
|
$temp = $transmissions[$currentIndex];
|
||||||
|
$transmissions[$currentIndex] = $transmissions[$currentIndex - 1];
|
||||||
|
$transmissions[$currentIndex - 1] = $temp;
|
||||||
|
} elseif ($direction === 'down' && $currentIndex < count($transmissions) - 1) {
|
||||||
|
// Swap with next
|
||||||
|
$temp = $transmissions[$currentIndex];
|
||||||
|
$transmissions[$currentIndex] = $transmissions[$currentIndex + 1];
|
||||||
|
$transmissions[$currentIndex + 1] = $temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate all start times
|
||||||
|
$currentTime = $blockStartTime;
|
||||||
|
|
||||||
|
foreach ($transmissions as $tx) {
|
||||||
|
$stmt = $db->prepare("UPDATE transmissions SET start_time = ?, api_status = 'pending' WHERE id = ?");
|
||||||
|
$stmt->execute([$currentTime, $tx['id']]);
|
||||||
|
|
||||||
|
$currentTime = addTimeToTime($currentTime, $tx['duration']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header("Location: planner.php?date=$date");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle block time update
|
||||||
|
if (isset($_POST['update_block_time'])) {
|
||||||
|
$blockId = $_POST['block_id'];
|
||||||
|
$newStartTime = $_POST['new_start_time'];
|
||||||
|
$recalculate = isset($_POST['recalculate']);
|
||||||
|
|
||||||
|
// Update block time
|
||||||
|
$stmt = $db->prepare("UPDATE daily_blocks SET actual_start_time = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$newStartTime, $blockId]);
|
||||||
|
|
||||||
|
// Optionally recalculate transmissions
|
||||||
|
if ($recalculate) {
|
||||||
|
$stmt = $db->prepare("SELECT block_date, channel FROM daily_blocks WHERE id = ?");
|
||||||
|
$stmt->execute([$blockId]);
|
||||||
|
$blockInfo = $stmt->fetch();
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.id, c.duration
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.start_date = ? AND t.channel = ? AND t.start_time >= ?
|
||||||
|
ORDER BY t.start_time ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$blockInfo['block_date'], $blockInfo['channel'], $newStartTime]);
|
||||||
|
$transmissions = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$currentTime = $newStartTime;
|
||||||
|
foreach ($transmissions as $tx) {
|
||||||
|
$stmt = $db->prepare("UPDATE transmissions SET start_time = ?, api_status = 'pending' WHERE id = ?");
|
||||||
|
$stmt->execute([$currentTime, $tx['id']]);
|
||||||
|
$currentTime = addTimeToTime($currentTime, $tx['duration']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header("Location: planner.php?date={$blockInfo['block_date']}&success=block_updated");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get selected date
|
||||||
|
$selectedDate = $_GET['date'] ?? date('Y-m-d');
|
||||||
|
|
||||||
|
// Ensure daily blocks exist
|
||||||
|
ensureDailyBlocks($db, $selectedDate, $selectedDate);
|
||||||
|
|
||||||
|
// Get all blocks for this date (both channels, all blocks)
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT db.*, bt.name as template_name, bt.channel
|
||||||
|
FROM daily_blocks db
|
||||||
|
LEFT JOIN block_templates bt ON db.template_id = bt.id
|
||||||
|
WHERE db.block_date = ?
|
||||||
|
ORDER BY db.channel, db.actual_start_time
|
||||||
|
");
|
||||||
|
$stmt->execute([$selectedDate]);
|
||||||
|
$allBlocks = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Group blocks by channel
|
||||||
|
$blocksByChannel = [];
|
||||||
|
foreach ($allBlocks as $block) {
|
||||||
|
$blocksByChannel[$block['channel']][] = $block;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all commercials
|
||||||
|
$commercials = $db->query("
|
||||||
|
SELECT * FROM commercials
|
||||||
|
WHERE upload_status = 'uploaded'
|
||||||
|
ORDER BY title ASC
|
||||||
|
")->fetchAll();
|
||||||
|
|
||||||
|
// Helper function to calculate next start time for a specific block
|
||||||
|
function calculateNextStartTimeForBlock($db, $date, $channel, $blockId) {
|
||||||
|
if ($blockId) {
|
||||||
|
$stmt = $db->prepare("SELECT actual_start_time, actual_end_time FROM daily_blocks WHERE id = ?");
|
||||||
|
$stmt->execute([$blockId]);
|
||||||
|
$blockInfo = $stmt->fetch();
|
||||||
|
$blockStartTime = $blockInfo['actual_start_time'];
|
||||||
|
$blockEndTime = $blockInfo['actual_end_time'] ?? '23:59:59';
|
||||||
|
} else {
|
||||||
|
$blockStartTime = '07:00:00'; // Default
|
||||||
|
$blockEndTime = '23:59:59';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all transmissions in THIS SPECIFIC block only
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT start_time, duration
|
||||||
|
FROM transmissions
|
||||||
|
WHERE start_date = ? AND channel = ?
|
||||||
|
AND start_time >= ? AND start_time < ?
|
||||||
|
ORDER BY start_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute([$date, $channel, $blockStartTime, $blockEndTime]);
|
||||||
|
$lastTx = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($lastTx) {
|
||||||
|
return addTimeToTime($lastTx['start_time'], $lastTx['duration']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $blockStartTime;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Excel Planning - Telvero Talpa</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css">
|
||||||
|
<style>
|
||||||
|
.planning-table {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
.planning-table td {
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
.color-indicator {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.time-cell {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.duration-cell {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.remaining-time {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.block-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-left: 4px solid;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.block-header {
|
||||||
|
background: #e9ecef;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="index.php">
|
||||||
|
<i class="bi bi-tv"></i> Telvero Talpa Planner
|
||||||
|
</a>
|
||||||
|
<div class="navbar-nav">
|
||||||
|
<a class="nav-link" href="index.php">Dashboard</a>
|
||||||
|
<a class="nav-link active" href="planner.php">Excel Planner</a>
|
||||||
|
<a class="nav-link" href="calendar.php">Kalender</a>
|
||||||
|
<a class="nav-link" href="blocks.php">Blokken</a>
|
||||||
|
<a class="nav-link" href="commercials.php">Commercials</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<?php if (isset($_GET['success'])): ?>
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<?php
|
||||||
|
$messages = [
|
||||||
|
'added' => 'Uitzending toegevoegd!',
|
||||||
|
'removed' => 'Uitzending verwijderd!'
|
||||||
|
];
|
||||||
|
echo $messages[$_GET['success']] ?? 'Actie succesvol!';
|
||||||
|
?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Date Selector -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="GET" class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label"><strong>Selecteer Datum</strong></label>
|
||||||
|
<input type="date" name="date" class="form-control" value="<?= $selectedDate ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
<i class="bi bi-search"></i> Toon Planning
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-7 text-end">
|
||||||
|
<a href="calendar.php" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-calendar-week"></i> Kalender View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="mb-3"><?= formatDateDutch($selectedDate) ?></h3>
|
||||||
|
|
||||||
|
<!-- Both Channels Side by Side -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Commercial Sidebar -->
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card shadow-sm sticky-top" style="top: 20px;">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="bi bi-collection-play"></i> Commercials
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<input type="text"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
id="commercialSearch"
|
||||||
|
placeholder="🔍 Zoeken..."
|
||||||
|
onkeyup="filterPlannerCommercials(this.value)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Commercial List -->
|
||||||
|
<div class="commercial-sidebar" style="max-height: 70vh;">
|
||||||
|
<?php if (empty($commercials)): ?>
|
||||||
|
<div class="text-center text-muted p-2">
|
||||||
|
<i class="bi bi-inbox"></i>
|
||||||
|
<p class="small mt-2">Geen commercials</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($commercials as $commercial): ?>
|
||||||
|
<div class="commercial-item draggable-commercial"
|
||||||
|
style="border-left-color: <?= htmlspecialchars($commercial['color_code']) ?>; padding: 8px; margin-bottom: 8px;"
|
||||||
|
draggable="true"
|
||||||
|
data-commercial-id="<?= $commercial['id'] ?>"
|
||||||
|
data-title="<?= htmlspecialchars($commercial['title']) ?>"
|
||||||
|
data-duration="<?= $commercial['duration'] ?>"
|
||||||
|
data-color="<?= htmlspecialchars($commercial['color_code']) ?>"
|
||||||
|
data-series-code="<?= htmlspecialchars($commercial['series_code'] ?? '') ?>">
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div style="background-color: <?= htmlspecialchars($commercial['color_code']) ?>; width: 15px; height: 15px; border-radius: 2px; margin-right: 6px;">
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div style="font-weight: 600; font-size: 0.8em;">
|
||||||
|
<?= htmlspecialchars($commercial['title']) ?>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.7em; color: #6c757d;">
|
||||||
|
<?= round(timeToSeconds($commercial['duration']) / 60) ?> min
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-grip-vertical text-muted"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-10">
|
||||||
|
<div class="row">
|
||||||
|
<?php foreach (['SBS9', 'NET5'] as $channel): ?>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header <?= $channel == 'SBS9' ? 'bg-primary' : 'bg-danger' ?> text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-tv"></i> <?= $channel ?>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<?php
|
||||||
|
$channelBlocks = $blocksByChannel[$channel] ?? [];
|
||||||
|
|
||||||
|
if (empty($channelBlocks)): ?>
|
||||||
|
<div class="p-4 text-center text-muted">
|
||||||
|
<i class="bi bi-inbox"></i>
|
||||||
|
<p>Geen blokken voor deze dag</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($channelBlocks as $block):
|
||||||
|
// Get transmissions for this block
|
||||||
|
$blockStart = $block['actual_start_time'];
|
||||||
|
$blockEnd = $block['actual_end_time'] ?? '23:59:59';
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT t.*, c.title, c.color_code, c.series_code
|
||||||
|
FROM transmissions t
|
||||||
|
JOIN commercials c ON t.commercial_id = c.id
|
||||||
|
WHERE t.start_date = ? AND t.channel = ?
|
||||||
|
AND t.start_time >= ? AND t.start_time < ?
|
||||||
|
ORDER BY t.start_time ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$selectedDate, $channel, $blockStart, $blockEnd]);
|
||||||
|
$blockTransmissions = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Calculate block stats
|
||||||
|
$totalBlockMinutes = (timeToSeconds($blockEnd) - timeToSeconds($blockStart)) / 60;
|
||||||
|
$usedMinutes = 0;
|
||||||
|
foreach ($blockTransmissions as $tx) {
|
||||||
|
$usedMinutes += timeToSeconds($tx['duration']) / 60;
|
||||||
|
}
|
||||||
|
$remainingMinutes = $totalBlockMinutes - $usedMinutes;
|
||||||
|
?>
|
||||||
|
<div class="block-section" style="border-left-color: <?= $channel == 'SBS9' ? '#3498db' : '#e74c3c' ?>;">
|
||||||
|
<div class="block-header d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<?= htmlspecialchars($block['template_name'] ?? 'Blok') ?>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-dark ms-2"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#blockTimeModal<?= $block['id'] ?>"
|
||||||
|
title="Starttijd aanpassen">
|
||||||
|
<i class="bi bi-clock"></i> Tijd aanpassen
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-success ms-2"
|
||||||
|
onclick="syncBlockPlanner('<?= $selectedDate ?>', '<?= $channel ?>')"
|
||||||
|
title="Sync blok naar Talpa">
|
||||||
|
<i class="bi bi-cloud-upload"></i> Sync naar Talpa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<?= substr($blockStart, 0, 5) ?> - <?= substr($blockEnd, 0, 5) ?>
|
||||||
|
| Resterend: <strong><?= round($remainingMinutes) ?> min</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-sm table-hover planning-table mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 40px;"></th>
|
||||||
|
<th style="width: 50px;">Code</th>
|
||||||
|
<th>Product</th>
|
||||||
|
<th style="width: 80px;">Duur</th>
|
||||||
|
<th style="width: 70px;">Start</th>
|
||||||
|
<th style="width: 70px;">Eind</th>
|
||||||
|
<th style="width: 80px;">Restant</th>
|
||||||
|
<th style="width: 100px;">Acties</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody data-block-id="<?= $block['id'] ?>" data-channel="<?= $channel ?>" data-date="<?= $selectedDate ?>">
|
||||||
|
<?php if (empty($blockTransmissions)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center text-muted py-3">
|
||||||
|
<small>Nog geen uitzendingen in dit blok</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($blockTransmissions as $index => $tx):
|
||||||
|
$durationMinutes = round(timeToSeconds($tx['duration']) / 60);
|
||||||
|
$endTime = addTimeToTime($tx['start_time'], $tx['duration']);
|
||||||
|
|
||||||
|
// Calculate remaining time in block after this transmission
|
||||||
|
$remainingSeconds = timeToSeconds($blockEnd) - timeToSeconds($endTime);
|
||||||
|
$txRemainingMinutes = round($remainingSeconds / 60);
|
||||||
|
?>
|
||||||
|
<tr style="background-color: <?= $tx['color_code'] ?>15;">
|
||||||
|
<td>
|
||||||
|
<div class="color-indicator" style="background-color: <?= htmlspecialchars($tx['color_code']) ?>;"></div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<small><strong><?= htmlspecialchars($tx['series_code'] ?? '-') ?></strong></small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong><?= htmlspecialchars($tx['title']) ?></strong>
|
||||||
|
</td>
|
||||||
|
<td class="duration-cell">
|
||||||
|
<?= $durationMinutes ?> min
|
||||||
|
</td>
|
||||||
|
<td class="time-cell text-success">
|
||||||
|
<?= substr($tx['start_time'], 0, 5) ?>
|
||||||
|
</td>
|
||||||
|
<td class="time-cell text-danger">
|
||||||
|
<?= substr($endTime, 0, 5) ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-center remaining-time">
|
||||||
|
<?= $txRemainingMinutes ?> min
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<?php if ($index > 0): ?>
|
||||||
|
<form method="POST" style="display:inline;">
|
||||||
|
<input type="hidden" name="transmission_id" value="<?= $tx['id'] ?>">
|
||||||
|
<input type="hidden" name="direction" value="up">
|
||||||
|
<input type="hidden" name="date" value="<?= $selectedDate ?>">
|
||||||
|
<input type="hidden" name="channel" value="<?= $channel ?>">
|
||||||
|
<input type="hidden" name="block_id" value="<?= $block['id'] ?>">
|
||||||
|
<button type="submit" name="reorder" class="btn btn-outline-secondary" title="Omhoog">
|
||||||
|
<i class="bi bi-arrow-up"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($index < count($blockTransmissions) - 1): ?>
|
||||||
|
<form method="POST" style="display:inline;">
|
||||||
|
<input type="hidden" name="transmission_id" value="<?= $tx['id'] ?>">
|
||||||
|
<input type="hidden" name="direction" value="down">
|
||||||
|
<input type="hidden" name="date" value="<?= $selectedDate ?>">
|
||||||
|
<input type="hidden" name="channel" value="<?= $channel ?>">
|
||||||
|
<input type="hidden" name="block_id" value="<?= $block['id'] ?>">
|
||||||
|
<button type="submit" name="reorder" class="btn btn-outline-secondary" title="Omlaag">
|
||||||
|
<i class="bi bi-arrow-down"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" style="display:inline;" onsubmit="return confirm('Verwijderen?');">
|
||||||
|
<input type="hidden" name="transmission_id" value="<?= $tx['id'] ?>">
|
||||||
|
<input type="hidden" name="date" value="<?= $selectedDate ?>">
|
||||||
|
<button type="submit" name="remove_transmission" class="btn btn-outline-danger" title="Verwijderen">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Add Commercial Row (also a drop zone) -->
|
||||||
|
<tr class="table-success drop-zone-end"
|
||||||
|
data-block-id="<?= $block['id'] ?>"
|
||||||
|
data-channel="<?= $channel ?>"
|
||||||
|
data-date="<?= $selectedDate ?>">
|
||||||
|
<td colspan="8">
|
||||||
|
<form method="POST" class="row g-2 align-items-center">
|
||||||
|
<input type="hidden" name="date" value="<?= $selectedDate ?>">
|
||||||
|
<input type="hidden" name="channel" value="<?= $channel ?>">
|
||||||
|
<input type="hidden" name="block_id" value="<?= $block['id'] ?>">
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<select name="commercial_id" class="form-select form-select-sm" required>
|
||||||
|
<option value="">+ Voeg commercial toe...</option>
|
||||||
|
<?php foreach ($commercials as $c): ?>
|
||||||
|
<option value="<?= $c['id'] ?>">
|
||||||
|
<?= htmlspecialchars($c['series_code'] ?? '') ?> -
|
||||||
|
<?= htmlspecialchars($c['title']) ?>
|
||||||
|
(<?= round(timeToSeconds($c['duration']) / 60) ?> min)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button type="submit" name="add_to_block" class="btn btn-success btn-sm w-100">
|
||||||
|
<i class="bi bi-plus-circle"></i> Toevoegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Block Time Edit Modals -->
|
||||||
|
<?php foreach ($allBlocks as $block): ?>
|
||||||
|
<div class="modal fade" id="blockTimeModal<?= $block['id'] ?>" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="block_id" value="<?= $block['id'] ?>">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="bi bi-clock"></i> Blok Starttijd Aanpassen
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong><?= htmlspecialchars($block['template_name'] ?? 'Blok') ?></strong><br>
|
||||||
|
<?= htmlspecialchars($block['channel']) ?> - <?= formatDateDutch($selectedDate) ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Nieuwe Starttijd</label>
|
||||||
|
<input type="time" name="new_start_time" class="form-control"
|
||||||
|
value="<?= $block['actual_start_time'] ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="recalculate"
|
||||||
|
id="recalc<?= $block['id'] ?>" checked>
|
||||||
|
<label class="form-check-label" for="recalc<?= $block['id'] ?>">
|
||||||
|
Herbereken alle uitzendingen in dit blok
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning mt-3 small">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
Als je "herbereken" aanvinkt, worden alle uitzendingen in dit blok opnieuw getimed vanaf de nieuwe starttijd.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
|
||||||
|
<button type="submit" name="update_block_time" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-circle"></i> Opslaan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Filter commercials in sidebar
|
||||||
|
function filterPlannerCommercials(searchTerm) {
|
||||||
|
const items = document.querySelectorAll('.draggable-commercial');
|
||||||
|
const term = searchTerm.toLowerCase();
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const title = item.dataset.title.toLowerCase();
|
||||||
|
const seriesCode = (item.dataset.seriesCode || '').toLowerCase();
|
||||||
|
|
||||||
|
if (title.includes(term) || seriesCode.includes(term)) {
|
||||||
|
item.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make commercial items draggable
|
||||||
|
document.querySelectorAll('.draggable-commercial').forEach(item => {
|
||||||
|
item.addEventListener('dragstart', function(e) {
|
||||||
|
e.dataTransfer.setData('commercial_id', this.dataset.commercialId);
|
||||||
|
e.dataTransfer.setData('title', this.dataset.title);
|
||||||
|
e.dataTransfer.setData('duration', this.dataset.duration);
|
||||||
|
this.style.opacity = '0.5';
|
||||||
|
});
|
||||||
|
|
||||||
|
item.addEventListener('dragend', function(e) {
|
||||||
|
this.style.opacity = '1';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make table rows drop targets
|
||||||
|
document.querySelectorAll('.planning-table tbody').forEach(tbody => {
|
||||||
|
const blockId = tbody.dataset.blockId;
|
||||||
|
const channel = tbody.dataset.channel;
|
||||||
|
const date = tbody.dataset.date;
|
||||||
|
|
||||||
|
// Make each row a drop zone
|
||||||
|
tbody.querySelectorAll('tr:not(.table-success)').forEach((row, index) => {
|
||||||
|
row.addEventListener('dragover', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.style.borderTop = '3px solid #28a745';
|
||||||
|
});
|
||||||
|
|
||||||
|
row.addEventListener('dragleave', function(e) {
|
||||||
|
this.style.borderTop = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
row.addEventListener('drop', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.style.borderTop = '';
|
||||||
|
|
||||||
|
const commercialId = e.dataTransfer.getData('commercial_id');
|
||||||
|
|
||||||
|
// Insert at this position
|
||||||
|
fetch('api/insert_transmission_at_position.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
commercial_id: commercialId,
|
||||||
|
channel: channel,
|
||||||
|
date: date,
|
||||||
|
block_id: blockId,
|
||||||
|
position: index
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Fout: ' + data.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make "Add Commercial" rows also drop zones (for adding at end)
|
||||||
|
document.querySelectorAll('.drop-zone-end').forEach(row => {
|
||||||
|
const blockId = row.dataset.blockId;
|
||||||
|
const channel = row.dataset.channel;
|
||||||
|
const date = row.dataset.date;
|
||||||
|
|
||||||
|
row.addEventListener('dragover', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.style.backgroundColor = '#d4edda';
|
||||||
|
});
|
||||||
|
|
||||||
|
row.addEventListener('dragleave', function(e) {
|
||||||
|
this.style.backgroundColor = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
row.addEventListener('drop', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.style.backgroundColor = '';
|
||||||
|
|
||||||
|
const commercialId = e.dataTransfer.getData('commercial_id');
|
||||||
|
|
||||||
|
if (!commercialId) return;
|
||||||
|
|
||||||
|
// Add at end (use existing add_to_block logic)
|
||||||
|
const form = this.querySelector('form');
|
||||||
|
if (form) {
|
||||||
|
const select = form.querySelector('select[name="commercial_id"]');
|
||||||
|
if (select) {
|
||||||
|
select.value = commercialId;
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync block to Talpa
|
||||||
|
function syncBlockPlanner(date, channel) {
|
||||||
|
if (!confirm(`Wilt u alle uitzendingen in dit blok synchroniseren naar Talpa?\n\nDatum: ${date}\nZender: ${channel}`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
const btn = event.target.closest('button');
|
||||||
|
const originalHTML = btn.innerHTML;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span> Synchroniseren...';
|
||||||
|
|
||||||
|
fetch('api/sync_block.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
date: date,
|
||||||
|
channel: channel
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalHTML;
|
||||||
|
|
||||||
|
// Log debug information to console
|
||||||
|
console.group('🔍 Sync Block Debug Info');
|
||||||
|
console.log('Full Response:', data);
|
||||||
|
if (data.debug) {
|
||||||
|
console.log('Debug Log:', data.debug);
|
||||||
|
}
|
||||||
|
if (data.api_calls) {
|
||||||
|
console.log('API Calls:', data.api_calls);
|
||||||
|
}
|
||||||
|
if (data.errors) {
|
||||||
|
console.log('Errors:', data.errors);
|
||||||
|
}
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
let message = `✓ Synchronisatie voltooid!\n\nGeslaagd: ${data.synced}\nMislukt: ${data.failed}`;
|
||||||
|
|
||||||
|
if (data.errors && data.errors.length > 0) {
|
||||||
|
message += '\n\nFouten:\n';
|
||||||
|
data.errors.forEach(err => {
|
||||||
|
message += `- ${err.title} (${err.time}): ${err.error}\n`;
|
||||||
|
});
|
||||||
|
message += '\n\nZie console (F12) voor gedetailleerde debug informatie.';
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(message);
|
||||||
|
|
||||||
|
// Reload page to show updated status
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('✗ Fout bij synchroniseren: ' + (data.error || 'Onbekende fout') + '\n\nZie console (F12) voor debug informatie.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalHTML;
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('✗ Netwerkfout bij synchroniseren');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4
vendor/composer/installed.php
vendored
4
vendor/composer/installed.php
vendored
@ -3,7 +3,7 @@
|
|||||||
'name' => '__root__',
|
'name' => '__root__',
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => 'dev-main',
|
'version' => 'dev-main',
|
||||||
'reference' => '4807b34bad8dee14a59e74f3b55faf2055d70888',
|
'reference' => 'bfdd27fbccac0f215910524444f38fe5311aaf40',
|
||||||
'type' => 'library',
|
'type' => 'library',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
@ -13,7 +13,7 @@
|
|||||||
'__root__' => array(
|
'__root__' => array(
|
||||||
'pretty_version' => 'dev-main',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => 'dev-main',
|
'version' => 'dev-main',
|
||||||
'reference' => '4807b34bad8dee14a59e74f3b55faf2055d70888',
|
'reference' => 'bfdd27fbccac0f215910524444f38fe5311aaf40',
|
||||||
'type' => 'library',
|
'type' => 'library',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user