ChurchService presenter software show creator

## Description

There is tool (churchtools - called cts from now on) to plan a church service, that contains all parts of the service (information, songs, sermon, prayer, etc.).

Use CTS API for services: <https://api.church.tools/package-CT.ChurchService.html>.

All the wording in the frontend and communication has to be in German with Du, not Sie.

## TechStack

- Laravel (Vue+Inertia) App, with Sqlite (switchable to MySQL)
- use DB for caching the API data and just "Update" the DB from the API
- use CTS API with existing env `CTS_API_TOKEN` (.env for LIVE, .env.example with same content for you) for auth - ONLY DO READS, NO WRITES OR CHANGES ARE ALLOWED

## General

- Login should be done via OAUTH for all churchtools users (<https://churchtools.academy/de/help/system-einstellungen/oauth-login-systemeinstellungen/oauth-zwischen-zwei-churchtools-systemen/>) and linked to an automatically created local user.
- There should be Button in the Top Bar, to refresh the Data from the CTS API and a timestamp with the latest refresh.
- LoggedIn User should be visible in the Top Bar
- every action should be immediately persistent, no separate "save" button required, unless explicitly described.
- ProPresenter `.pro` file parser/generator is implemented as a separate composer package (`propresenter/parser`) linked via path repository

## The Plan

We use the data from the API, to create a form to finalize the service setup and to create a file for the presenter software at the end.

1. Show all today or future services in a list, with details (Title, Preacher, 'beamer technican', qty of songs, last changed, finalized_at) and state of the setup (No or Yes at...):
   - x/y songs found and mapped
   - x/y songs verified arrangement
   - sermon slides uploaded
   - X info slides uploaded
   - finalized at

2. for every service, show Action Buttons:
   - if finalized: ReOpen and Download
   - if NOT finalized: Edit and Finalize

3. - ReOpen and Finalize just change the status of the service
   - Edit shows a form, with these Blocks to edit (details of these blocks are below)
     - information
     - moderation
     - sermon
     - songs

## Form Blocks

### Block: Information

- show list of thumbnails for all uploaded slides with a muted upload date field with uploader name, and a prominent Expiredate field
- each thumbnail could be delete (softdelete) or can inline change the date with a datepicker
- add big plus icon/area for drag'n'drop or click for fileupload new files with a datepicker for a date, that was added to all files as expire date
- automatically show these files to all future services, till the expire date is after the service date

### Block: Moderation

Same features as `Block Information` but without the datepicker and only relevant for this service.

### Block: Sermon

Same features as `Block Moderation`.

### Block: Songs

- Show all songs (Name, CCLI ID, has Translation ..) from the service in the right order.
- on every update from the CTS API, try to match the song with the CCLI-ID to an existing song from the DB
  - if NOT matched
    - show a button "request creation", which causes an EMAIL to a configured mail address, with the song and the CCLI Id and the ask for create the song
    - and show a searchable select field of all songs in the DB (CCLI ID included and searchable) to manually assign a song from the DB to this service song
  - if song found:
    - if song is translated, show checkbox (default:true) for use the version with translation
    - every song has a body for the arrangement selection/configuration
      - select field with all existing arrangements
      - "add" Button to create a new arangmengt (clone from master order) and ask for a name
      - "clone" Button to clone the selected arragenement and ask for a name
      - show the groups of the arrangement and make possible to rearange or add a group to the arrangement via drag'n'drop, like `rev/form-song-arangment-config.jpg`
- every song with a selected arrangement ("normal" should selected always as default) should have two buttons:
  - preview: show the text of the song in the order of the arrangement configuration, prominent highlighted which textpart was with group
  - download: download the preview as a nice pdf with header/footer and copyright footer from the Song DB.

## File Upload

- could be a zip file, contained multiple other files of types below, handle as mass upload
- could me multiple files, so handle each one as a single file for types below
- could be an image file (png, jpg, jpeg) -> always convert to jpg 1920x1080 (dont cut parts of an image, keep orientation and ratio)
- could be a ppt or pptx (powerpoint) -> convert to multiple JPG with 1920x1080 (see jpg convert)

## SongDB Import

There should be a menu item for songDB in the Top.

- it shows all songs from the DB, with created, last update, ccliID, last_used_in_service every song has a delete (soft_delete), download, translate and an edit button
- edit: shows a popup with Name, CCLI and copyright text (all that is available from song metadata) and the arrangement configurator from the service->song block
- download: download generated .pro file from the songDB for this song
- translate: allow add a full text or an URL to the Full text, and then start an editor, that shows two columns for every slide of every group. Left the original text, right a texteditor, with the imported text - always the same line qty of text from the original is used from the given translated text. Save this as translation for this song, and mark it as `with translation`.
- UploadArea for drag'n'drop and click for upload, to upload a .pro file, a zip file with multiple .pro files, or a bunch of .pro files, which should be parsed (this module was integrated later, so show an Exception here till this was finalized) and added into the song DB.

---

## Repository Structure

Two git repositories, both local (no remote):

| Repo | Path | Branch | Purpose |
|------|------|--------|---------|
| **pp-planer** | `/Users/thorsten/AI/pp-planer` | `cts-presenter-app` | Laravel app (main codebase) |
| **propresenter-work** | `/Users/thorsten/AI/propresenter-work/php` | `propresenter-parser` | ProPresenter .pro/.proplaylist parser (composer path dependency) |

The parser is linked via `composer.json` path repository: `"url": "../propresenter-work/php"`.

## Build, Test, Lint Commands

### pp-planer (Laravel App)

Local dev runs in **DDEV** (Docker). Site URL: `https://pp-planer.ddev.site`.

```bash
# Easy onboarding wrappers (no DDEV knowledge required):
./start_dev.sh          # ddev start + spawns dev workers in background
./stop_dev.sh           # stops workers and DDEV (use --keep-ddev to leave DDEV running)

# Or use DDEV directly:
ddev start              # composer install, key:generate, migrate, npm install, npm run build
ddev dev                # queue + log tail + vite HMR (foreground)
ddev stop               # stop the project

# Open a shell inside the web container
ddev ssh

# Frontend
ddev npm run build
ddev npm run dev

# Run ALL PHP tests (clears config cache first)
ddev composer test
ddev exec php artisan test

# Single test file
ddev exec php artisan test tests/Feature/ServiceControllerTest.php

# Single test method
ddev exec php artisan test --filter=test_service_kann_abgeschlossen_werden

# Test suite
ddev exec php artisan test --testsuite=Feature
ddev exec php artisan test --testsuite=Unit

# PHP formatting (Laravel Pint, default preset — no pint.json)
ddev exec ./vendor/bin/pint
ddev exec ./vendor/bin/pint --test   # check only

# E2E tests (requires `ddev start` running; baseURL is https://pp-planer.ddev.site)
npx playwright test
npx playwright test tests/e2e/service-list.spec.ts

# Migrations
ddev exec php artisan migrate
```

### propresenter-work (Parser Module)

```bash
cd /Users/thorsten/AI/propresenter-work/php

# Run all tests (230 tests)
./vendor/bin/phpunit

# Single test file
./vendor/bin/phpunit tests/ProFileReaderTest.php
```

## Architecture

```
pp-planer/
  app/Http/Controllers/     # Inertia controllers (Inertia::render or JSON)
  app/Models/               # Eloquent models (factories in database/factories/)
  app/Services/             # Business logic (ChurchToolsService, ProExportService, etc.)
  app/Jobs/                 # Queue jobs (PowerPoint conversion)
  app/Mail/                 # Mailable classes (German content)
  resources/js/Pages/       # Vue page components (mapped via Inertia::render)
  resources/js/Components/  # Reusable Vue components
  resources/js/Composables/ # Vue composables (useAutoSave)
  resources/js/Layouts/     # AuthenticatedLayout, GuestLayout
  tests/Feature/            # Pest v4 / PHPUnit feature tests
  tests/e2e/                # Playwright browser tests (TypeScript)

propresenter-work/php/
  src/                      # ProFileReader, ProFileGenerator, ProPlaylistGenerator, Song, Group, Slide, Arrangement
  tests/                    # PHPUnit 11 tests with #[Test] attributes
  ref/                      # .pro fixture files for testing
```

## Code Style -- PHP

- **Formatter**: Laravel Pint (default Laravel preset, no custom config)
- **Imports**: Fully qualified, one per line, alphabetical. App\ first, then Illuminate\, then external.
- **Constructors**: Promoted properties with `private readonly`. Empty body: `{}` on same line.
  ```php
  public function __construct(
      private readonly SongMatchingService $songMatchingService,
  ) {}
  ```
- **Return types**: Always present. Use union types for multiple returns: `Response|JsonResponse`.
- **String concat**: No spaces around `.` operator: `'Fehler: '.$e->getMessage()`
- **Null safety**: Nullsafe `?->` and null coalescing `??`. Never suppress with `@`.
- **Models**: `$fillable` array (never `$guarded`). `casts()` method (never `$casts` property). Typed relationship returns (`HasMany`, `BelongsTo`). `Attribute::get()` for computed accessors.
- **Migrations**: Anonymous class: `return new class extends Migration {`. Methods: `up(): void`, `down(): void`.
- **Error messages**: German, Du-form. Flash: `->with('success', '...')`. JSON: `'message'` key.
- **Validation**: Inline `$request->validate([...])` with `Rule::in()` for enums.
- **Transactions**: `DB::transaction(function () use (...): void { ... })` for multi-step writes.
- **Constants**: `private const NAME = [...]` for fixed value sets.

## Code Style -- Vue / Frontend

- **Vue 3 Composition API** only. Always `<script setup>`. No Options API.
- **Props**: `defineProps({ name: { type: Type, default: value } })`
- **Emits**: `defineEmits(['kebab-case-event'])`
- **Model**: `defineModel({ type: String, required: true })` for v-model binding.
- **Imports**: `@/` alias for `resources/js/`. Vue from `'vue'`, Inertia from `'@inertiajs/vue3'`.
- **Functions**: Prefer `function name() {}` declarations (not `const name = () => {}`).
- **State**: `ref()` for reactive, `computed()` for derived, `watch()` for side effects.
- **Routing**: `route('name', params)` via Ziggy. `router.post/get/delete` from Inertia with `{ preserveScroll: true }`.
- **Styling**: Tailwind CSS v4 utility classes inline. `<style scoped>` only when necessary (e.g. drag-and-drop).
- **Test IDs**: `data-testid="..."` on interactive elements for Playwright.
- **All UI text**: German, Du-form (not Sie).

## Code Style -- Tests

### PHP Tests (Pest v4 + PHPUnit)

Both styles are used:
- **Function-based Pest**: `test('snake_case german description', function () { ... });` with `beforeEach`
- **Class-based**: `final class NameTest extends TestCase { use RefreshDatabase; public function test_snake_case(): void {} }`
- **Auth**: `$this->actingAs(User::factory()->create())`
- **Vite**: Call `$this->withoutVite()` before Inertia page render tests
- **Time**: `Carbon::setTestNow('2026-03-01 10:00:00')` for deterministic dates
- **Inertia assertions**: `$response->assertInertia(fn ($page) => $page->component('...')->has('...')->where('...'))`
- **DB**: In-memory SQLite (`phpunit.xml`: `DB_CONNECTION=sqlite`, `DB_DATABASE=:memory:`)
- **Helper functions**: Define at file level for shared test setup (e.g. `function makePngUpload(...)`)
- **Storage**: `Storage::fake('public')` in `beforeEach` for file upload tests

### ProPresenter Parser Tests (PHPUnit 11)

- Uses `#[Test]` attribute (not `test_` prefix)
- `setUp()`/`tearDown()` for temp directory management
- Fixtures in `ref/` directory, loaded via `dirname(__DIR__, 2).'/ref/...'`
- Strict mode: `failOnRisky`, `failOnWarning`, `beStrictAboutOutputDuringTests`

### E2E Tests (Playwright)

- TypeScript in `tests/e2e/`, baseURL `https://pp-planer.ddev.site` (HTTPS, `ignoreHTTPSErrors: true` set in `playwright.config.ts`)
- Auth via `auth.setup.ts` (XSRF token + `/dev-login` endpoint, saves state to `.auth/user.json`)
- Selectors: `page.getByTestId('...')`, `page.getByText('...')`, `page.getByRole('...')`
- German text assertions: `await expect(page.getByText('Mit ChurchTools anmelden')).toBeVisible()`

## Key Constraints

- **CTS API is READ-ONLY** -- never write/modify data via ChurchTools API
- **Immediate persistence** -- all user actions save instantly (no separate save button)
- **German locale** -- `APP_LOCALE=de`, all UI text German Du-form
- **File uploads** -- images convert to JPG 1920x1080 (maintain aspect, no crop); PPT/PPTX to multiple JPGs
- **Named routes** -- all routes have names, use `route('name')` everywhere
- **No type suppression** -- never use `as any`, `@ts-ignore`, `@ts-expect-error` in frontend
- **SoftDeletes** -- used on Song and Slide models; use `whereNull('deleted_at')` in manual queries
