pp-planer/app/Models/Service.php
Thorsten Bus 27f8402ae8 feat: Wave 4 - Song DB Management + Finalization (T20-T24)
T20: Song DB Page
- Songs/Index.vue with search, action buttons, pagination
- Upload area for .pro files (calls T23 placeholder)
- Song-Datenbank nav link added to AuthenticatedLayout
- Tests: 9 new (44 assertions)

T21: Song DB Edit Popup
- SongEditModal.vue with metadata + ArrangementConfigurator
- Auto-save with fetch (500ms debounce for text, immediate on blur)
- Tests: 11 new (53 assertions)

T22: Song DB Translate Page
- Songs/Translate.vue with two-column editor
- URL fetch or manual paste, line-count constraints
- Group headers with colors, save marks has_translation=true
- Tests: 1 new (12 assertions)

T23: .pro File Placeholders
- ProParserNotImplementedException with HTTP 501
- ProFileController with importPro/downloadPro placeholders
- German error messages
- Tests: 5 new (7 assertions)

T24: Service Finalization + Status
- Two-step finalization with warnings (unmatched songs, missing slides)
- Download placeholder toast
- isReadyToFinalize accessor on Service model
- Tests: 11 new (30 assertions)

All tests passing: 174/174 (905 assertions)
Build: ✓ Vite production build successful
German UI: All user-facing text in German with 'Du' form
2026-03-01 20:30:07 +01:00

82 lines
2.1 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Service extends Model
{
use HasFactory;
protected $fillable = [
'cts_event_id',
'title',
'date',
'preacher_name',
'beamer_tech_name',
'finalized_at',
'last_synced_at',
'cts_data',
];
protected function casts(): array
{
return [
'date' => 'date',
'finalized_at' => 'datetime',
'last_synced_at' => 'datetime',
'cts_data' => 'array',
];
}
public function serviceSongs(): HasMany
{
return $this->hasMany(ServiceSong::class);
}
public function slides(): HasMany
{
return $this->hasMany(Slide::class);
}
/**
* Check finalization prerequisites and return warnings.
*
* @return array{ready: bool, warnings: string[]}
*/
public function finalizationStatus(): array
{
$warnings = [];
$totalSongs = $this->serviceSongs()->count();
$mappedSongs = $this->serviceSongs()->whereNotNull('song_id')->count();
$arrangedSongs = $this->serviceSongs()->whereNotNull('song_arrangement_id')->count();
$sermonSlides = $this->slides()->where('type', 'sermon')->count();
if ($totalSongs > 0 && $mappedSongs < $totalSongs) {
$warnings[] = "Nur {$mappedSongs} von {$totalSongs} Songs sind zugeordnet.";
}
if ($totalSongs > 0 && $arrangedSongs < $totalSongs) {
$warnings[] = "Nur {$arrangedSongs} von {$totalSongs} Songs haben ein Arrangement.";
}
if ($sermonSlides === 0) {
$warnings[] = 'Es wurden keine Predigtfolien hochgeladen.';
}
return [
'ready' => empty($warnings),
'warnings' => $warnings,
];
}
protected function isReadyToFinalize(): Attribute
{
return Attribute::get(fn () => $this->finalizationStatus()['ready']);
}
}