| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/seaside2.pacim.de/web/ |
Upload File : |
# PaCIM Check-In-App — Anforderungs-Spezifikation
Ein eigenständige mobile App (Tablet + Phone) für den operativen Check-in-Counter
am Kreuzfahrtterminal. Sie ist ein dünner Client und arbeitet ausschließlich über
die [PaCIM REST-API v1](#api-referenz) mit dem SaaS-Backend.
> Diese Datei ist Brief für die App-Entwicklung. Sie beschreibt **Zweck, Screens,
> UX-Prinzipien, API-Verträge und Anpassungen am Backend**, die für den vollen
> Funktionsumfang nötig sind. Der Web-Check-In-Modal in `?page=arrivals` ist die
> Referenz-Implementierung der gewünschten Counter-UX.
---
## 1. Zweck & Zielgruppe
| Punkt | Wert |
|----------------|------|
| Zielgruppe | Counter-Mitarbeiter am Terminal (Schicht-Personal, oft wechselnd) |
| Primärgerät | iPad / Android-Tablet (10–13"), querformat-tauglich |
| Sekundärgerät | Phone (Walk-around-Modus) |
| Operations-Charakter | POS-/Airport-Style — wenig Text, große Tap-Targets, schneller Flow |
| Sitzung | 8–12 h Schicht, oft mit instabilem WLAN (Außenbereich) |
| Anzahl Buchungen pro Tag | typ. 100–300 pro Cruise |
**Erfolgskriterium:** Ein Fahrzeug muss in **< 8 Sekunden** vom Erscheinen am
Counter bis zum bestätigten Check-in (inkl. Zahlung-Statusabfrage) durchgeführt
werden können.
---
## 2. Technologie
- Frei wählbar (Flutter, React Native, Swift/Kotlin native).
- Pflicht-Anforderungen:
- Persistenter Token-Storage über System-Keychain (iOS) / EncryptedSharedPrefs (Android)
- Funktionsfähige Offline-Queue für Schreib-Aktionen (siehe §6)
- Barcode/QR-Reader (Onboarding + Buchungs-Suche per Kennzeichen oder Booking-No-Code)
- Push-Notifications (optional, für „Anreise eingetroffen"-Hinweise vom Backend)
---
## 3. Onboarding & Auth
### 3.1 Token-Erzeugung (Admin im Web)
1. Admin öffnet `?page=settings/api`
2. Vergibt einen Namen (z.B. „iPad-Counter-1") + Scope `read,write`
3. System erzeugt `pcm_<48 hex>` und zeigt ihn als QR-Code an *(neu: aktuell ist
nur Klartext mit Copy-Button — siehe §10 Backend-Anpassungen)*
### 3.2 App-Setup
- Erststart zeigt: **Setup-Screen** mit zwei Optionen
- „QR-Code scannen" → liest `pcm_…`-Token aus dem QR
- „Token manuell einfügen" → Textfeld + Paste
- Optional: Server-URL eintragen (Default `https://meinpacim.example.com/`)
- Validierung per `GET /?page=api/v1/events&limit=1` mit dem Token. Bei 200 OK
wird der Token in der Keychain abgelegt.
- App merkt sich Server-URL + Token-Name (für UI-Anzeige in der Titelleiste).
### 3.3 Token-Verlust / -Sperrung
- Bei 401 mit `error: invalid_token` → App löscht lokal alle Daten, zeigt
Setup-Screen, Hinweis: *„Token wurde widerrufen. Bitte beim Admin neuen Schlüssel anfordern."*
---
## 4. Screens & Flows
### 4.1 Tagesübersicht (`/today`)
Default-Startscreen nach dem Login.
**Inhalt**
- Datum + Tag-Navigation (`‹` Vortag · Datum · Folgetag `›`)
- KPI-Strip: Anreisen heute · eingecheckt · offen · storniert · Brutto offen
- Liste der Anreisen sortiert nach Boarding-Zeit (`arrival_time − 30 min`)
- Pro Eintrag: Status-Icon · Name · Kennzeichen · Schiff · Stellplatz · Betrag
- Farbcode am linken Rand: grün=bezahlt+eingecheckt · gelb=Zahlung offen ·
blau=eingecheckt · rot=storniert
- Filter-Chips: „Offen" · „Angereist" · „Halle" · „Außen" · „Valet"
(gegenseitiger Ausschluss Halle ↔ Außen wie im Web)
- Suche im Header (Volltext: Kennzeichen, Name, Booking-No)
**Quellen-API**
- `GET /?page=api/v1/bookings&date=<heute>` (200er Limit, Server-side sort by `arrival_time`)
- Live-Refresh alle 60 s (Polling); Pull-to-Refresh manuell
### 4.2 Buchungs-Detail (`/bookings/:id`)
Klick auf einen Eintrag öffnet die Counter-Karte — die ist die operative
Hauptansicht (Pendant zum Web-Check-In-Modal).
**Header-Strip (immer sichtbar):**
- Kennzeichen (groß, im License-Plate-Style; tappable → editieren)
- Kunde (Name + E-Mail + Telefon, Anrufen/Mailen per Tap)
- Event (Schiff + Reisezeitraum + Dauer; tappable → Event-Detail)
- Stellplatz (aktueller Typ Halle/Außen/Valet; tappable → Swap-Picker)
**Tabs**
- **Produkte** — Positionsliste mit Inline-Edit (Menge, Preis, Halle↔Außen-Swap), „Produkt hinzufügen"-Picker (zeigt Staffelpreise für die aktuelle Reisedauer)
- **Zahlung** — Liste der erfassten Zahlungen mit Inline-Edit/Löschen, Rabatt-Dropdown (frei änderbar: kein/10% Frühbucher/5€ Bestandskunde), Neue Zahlung erfassen (Betrag, Methode, Datum)
- **Notizen** — chronologische Liste mit Speichern-Button (mehrere Einträge mit Timestamp), Sort DESC/ASC, je Eintrag editier-/löschbar
**Sidebar (oder Bottom-Sheet auf Phone):**
- Zusammenfassung (Netto / MwSt / Brutto / Bezahlt / Offen)
- Status-Badges (Zahlung · Check-in)
**Footer (Hauptaktionen):**
- ⭐ Favorit
- ❌ Buchung/Rechnung stornieren (öffnet Reason-Sheet: Nicht erschienen / Kunde storniert / Sonstige…)
- ✏️ Bearbeiten (öffnet Edit-Sheet für Reisezeitraum/Anreisezeit/Gäste)
- ✅ **Check-in durchführen** (volle Breite, grün, prominent)
- Disabled solange Zahlung offen — Tap zeigt Hinweis „Erst Zahlung erfassen"
### 4.3 Live-Suche (`/search`)
Volltext über alle Buchungen (Kennzeichen, Booking-No, Name, Telefon, E-Mail).
Zusätzlicher Schnellzugriff per **Kamera-Scan** auf das Buchungs-QR-Schlüsselschild.
### 4.4 Favoriten (`/favorites`)
Spiegelung von `?page=events/favorites` aus dem Web — gefiltert nach Anreisedatum
und Schiff. Storno-Einträge bleiben sichtbar mit rotem Indicator.
### 4.5 Event-Detail (`/events/:id`)
- Kapazitäten (Halle/Außen/Valet) + aktuelle Auslastung
- Liste aller Buchungen für das Event (paginiert)
- Mini-Stats: angereist / offen / abgereist
- Quick-Action „Live-Anreise" → öffnet 4.1 mit Event-Filter
### 4.6 Einstellungen (`/settings`)
- Token-Name + maskierter Schlüssel (kein Reveal — App ist „Read-only" für den Token)
- Server-URL (read-only)
- Logout / Token entfernen (löscht Keychain-Eintrag)
- Offline-Queue-Anzeige (siehe §6)
- Auto-Refresh-Intervall (15 s / 30 s / 60 s / manuell)
- Dark-Mode-Toggle (System / Hell / Dunkel)
---
## 5. UX-Prinzipien
| Prinzip | Konkret |
|---------|---------|
| **Schnelle Erfassung** | Max. 2 Taps von der Liste bis zur Aktion. Keine Multi-Page-Wizards. |
| **Klare Hierarchie** | Eine prominente Primär-Aktion pro Screen (z.B. „Check-in"). Sekundäre Aktionen visuell zurückgenommen. |
| **Große Tap-Targets** | Min. 44 × 44 pt (iOS) / 48 × 48 dp (Android), auch in Listen. |
| **Status-Toasts oben rechts** | Bestätigung jeder Aktion mit Buchungs-Präfix („Max Mustermann: Kennzeichen geändert"). |
| **Optimistic UI** | Schreib-Aktionen aktualisieren das UI sofort, im Hintergrund läuft der API-Call; bei Fehler Toast + Rollback. |
| **Keine Modals-in-Modals** | Sheets/Drawers stattdessen verwenden. |
| **Farbcodes konsistent** | Grün = bezahlt/eingecheckt · Gelb = offen · Rot = storniert/Fehler · Blau = neutral. |
| **Keine Tabellen auf Phone** | Listen als Karten mit klarer Zeilen-Hierarchie. |
---
## 6. Offline-Verhalten
Counter-WLAN bricht oft ab (Stahlrumpf, weite Außenflächen). Die App muss
operative Aktionen für **mindestens 10 Minuten** überbrücken:
- **Lese-Cache:** Letzter Tagesabruf (`/today`) wird lokal persistiert. Beim
Öffnen ohne Netz wird der Cache angezeigt mit Banner „Offline — letzte
Aktualisierung 14:32".
- **Schreib-Queue:** Check-in, Notizen, Zahlungserfassung, Plate-Update werden
in eine lokale Queue gelegt und beim nächsten erfolgreichen Ping abgespielt.
Konfliktbehandlung beim Replay:
- Server liefert 409 (z.B. Buchung inzwischen storniert) → Queue-Eintrag wird
als „nicht zugestellt" markiert und im Settings-Screen aufgelistet.
- Sonst: stillschweigend abgearbeitet.
- **Auflösung über Schreib-Idempotenz:** Jeder Queue-Eintrag bekommt eine UUID
(`Idempotency-Key`-Header), Server akzeptiert Doppel-Posts idempotent.
*(Backend-Anpassung — siehe §10)*
---
## 7. Push & Real-Time-Sync
Optional, kann in v1 weggelassen werden:
- **Server → App (FCM/APNS):**
- „Neue Anreise eingetroffen" (sobald Kassen-System die Buchung als „vor Ort" markiert)
- „Buchung storniert" (Admin storniert webseitig)
- Alternativ: simples Polling alle 30 s + WebSocket optional in v2.
---
## 8. API-Referenz (Stand heute)
Diese Endpunkte existieren bereits ([siehe Wiki](/?page=settings/api/docs)):
| Methode | Pfad | Scope | Zweck |
|---------|------|-------|-------|
| GET | `/?page=api/v1/events` | read | Event-Liste (Filter `from`, `to`, `ship`) |
| GET | `/?page=api/v1/events&id=<id>` | read | Einzelner Event |
| POST | `/?page=api/v1/events` | write | Event anlegen (JSON-Body) |
| GET | `/?page=api/v1/bookings` | read | Buchungs-Liste (Filter `date`, `event_id`, `limit`, `offset`) |
| GET | `/?page=api/v1/bookings&id=<id>` | read | Einzelne Buchung inkl. `products[]` |
Auth über `Authorization: Bearer pcm_…` (siehe Wiki).
---
## 9. API-Endpunkte — zu ergänzen
Damit die App alle in §4 beschriebenen Aktionen ausführen kann, muss das Backend
folgende Endpunkte zusätzlich öffentlich exponieren. Die zugrundeliegenden
internen AJAX-Routen (`/?page=bookings/ajax/…`) existieren bereits — sie müssen
nur unter `api/v1/…` mit Token-Auth zur Verfügung gestellt werden.
### 9.1 Buchungs-Mutations
| Methode | Pfad | Scope | Zweck |
|---------|------|-------|-------|
| POST | `/api/v1/bookings/:id/checkin` | write | Check-in (creates checkin event) |
| POST | `/api/v1/bookings/:id/checkout` | write | Check-out |
| POST | `/api/v1/bookings/:id/cancel` | write | Storno mit `cancel_reason` |
| POST | `/api/v1/bookings/:id/uncancel` | write | Storno aufheben |
| PATCH | `/api/v1/bookings/:id/plate` | write | Kennzeichen ändern |
| POST | `/api/v1/bookings/:id/favorite` | write | Favorit toggeln (action: add/remove/toggle) |
| POST | `/api/v1/bookings/:id/discount` | write | Rabatt setzen/entfernen |
### 9.2 Positionen
| Methode | Pfad | Scope | Zweck |
|---------|------|-------|-------|
| GET | `/api/v1/bookings/:id/items` | read | Positionsliste (redundant zu Booking-GET, einzeln für Refresh) |
| POST | `/api/v1/bookings/:id/items` | write | Position hinzufügen (Body: `product_id`, optional `price_gross`, `quantity`) |
| PATCH | `/api/v1/bookings/:id/items/:itemId` | write | Menge/Preis ändern |
| POST | `/api/v1/bookings/:id/items/:itemId/swap` | write | Produkt umstellen (Body: `new_product_id`) |
| DELETE | `/api/v1/bookings/:id/items/:itemId` | write | Position entfernen |
### 9.3 Zahlungen
| Methode | Pfad | Scope | Zweck |
|---------|------|-------|-------|
| GET | `/api/v1/bookings/:id/payments` | read | Zahlungs-Verlauf |
| POST | `/api/v1/bookings/:id/payments` | write | Zahlung erfassen (`amount`, `payment_method`, `paid_at`) |
| PATCH | `/api/v1/payments/:paymentId` | write | Zahlung ändern |
| DELETE | `/api/v1/payments/:paymentId` | write | Zahlung löschen |
### 9.4 Notizen
| Methode | Pfad | Scope | Zweck |
|---------|------|-------|-------|
| GET | `/api/v1/bookings/:id/notes` | read | Notizen-Liste |
| POST | `/api/v1/bookings/:id/notes` | write | Notiz anlegen (`body`) |
| PATCH | `/api/v1/notes/:noteId` | write | Notiz aktualisieren |
| DELETE | `/api/v1/notes/:noteId` | write | Notiz löschen |
### 9.5 Produkte (Read-only für Picker)
| Methode | Pfad | Scope | Zweck |
|---------|------|-------|-------|
| GET | `/api/v1/products` | read | Aktive Produkte mit Festpreis |
| GET | `/api/v1/products?booking_id=<id>` | read | Aktive Produkte mit Staffelpreis für die Buchungs-Reisedauer |
### 9.6 Suche
| Methode | Pfad | Scope | Zweck |
|---------|------|-------|-------|
| GET | `/api/v1/search?q=<text>&type=bookings` | read | Volltext-Suche (analog zur Web-Live-Suche) |
### 9.7 Favoriten-Liste
| Methode | Pfad | Scope | Zweck |
|---------|------|-------|-------|
| GET | `/api/v1/favorites?date=&ship=` | read | Favoriten des aktuellen Tokens — gefiltert |
> **Hinweis zur Token-User-Zuordnung:** Favoriten sind im DB-Schema user-spezifisch
> (`booking_favorites.user_id`). Für die App muss entweder der Token einem User
> zugeordnet sein (Spalte `api_tokens.user_id` ergänzen) oder Favoriten werden
> token-spezifisch. Empfehlung: **Token → User** verlinken, damit auch Audit-Log-
> Einträge der App den korrekten Counter-Mitarbeiter zeigen.
---
## 10. Backend-Anpassungen (Pflicht)
Damit die App produktiv geht, sind die folgenden Erweiterungen am SaaS-Backend
nötig:
1. **API-Endpunkte aus §9** öffentlich machen — Bearer-Token-Auth + `write`-Scope-Check.
Logging in `api_logs` läuft automatisch via existierendem `$apiOut`-Pattern.
2. **`Idempotency-Key`-Header** (UUID) auf allen POST/PATCH/DELETE-Endpunkten:
Wenn derselbe Key innerhalb von 24 h erneut auftritt → originale Antwort
zurückgeben statt zweimal ausführen. Tabelle `api_idempotency` mit
`(token_id, key, response_status, response_body, created_at)`.
3. **`api_tokens.user_id`** ergänzen — Counter-Audit-Log und Favoriten-Zuordnung.
4. **QR-Token-Anzeige** im Web-Admin (`?page=settings/api`): nach dem Erzeugen
eines Tokens zusätzlich einen QR-Code rendern, damit das App-Onboarding ohne
Tippen funktioniert. Lib: `BaconQrCode` oder serverseitig SVG.
5. **Webhook-Support** (optional, für Push): `POST /api/v1/webhooks` mit
`url` + `events[]`, ausgelöst bei `booking.checked_in`, `booking.cancelled`,
`payment.recorded`. Out-of-scope für v1.
---
## 11. Datenmodell-Übersicht (relevante Felder)
### Booking (`/api/v1/bookings`)
```json
{
"id": 347,
"booking_no": "160526-A-6304",
"customer_id": 342,
"event_id": 36,
"travel_from": "2026-05-16",
"travel_to": "2026-05-23",
"arrival_time": "2026-05-16 09:30:00",
"license_plate": "B-DI-2411",
"status": "confirmed", // confirmed | cancelled
"payment_status": "paid", // paid | partial | open
"checkin_status": "pending", // pending | checked_in | checked_out
"total_net": 75.63,
"total_vat": 14.37,
"total_gross": 90.00,
"source": null, // "AIDA" wenn importiert
"discount_label": null,
"discount_amount": 0,
"c_first_name": "Eugen",
"c_last_name": "Götz Pietsch",
"event_title": "AIDAprima Mai 2026",
"event_ship": "AIDAprima",
"products": [
{ "id": 370, "product_id": 2, "title": "Außen", "quantity": 1,
"price_gross": 90.00, "vat_rate": 19, "total_gross": 90.00 }
]
}
```
### Event
```json
{
"id": 36,
"title": "AIDAprima Mai 2026",
"ship_name": "AIDAprima",
"terminal": "Steinwerder",
"starts_at": "2026-05-16 10:00:00",
"ends_at": "2026-05-23 10:00:00",
"arrival_from": "2026-05-16 06:00:00",
"arrival_to": "2026-05-16 09:30:00",
"capacity_indoor": 50,
"capacity_outdoor": 200,
"capacity_valet": 30,
"status": "scheduled"
}
```
---
## 12. Fehlerbehandlung (App-Sicht)
| HTTP | Server-`error` | App-Verhalten |
|------|--------------------------|---------------|
| 401 | `unauthorized` / `invalid_token` | Setup-Screen, Token-Reset, Admin informieren |
| 403 | `forbidden` | Toast „Diese Aktion ist mit deinem Zugang nicht erlaubt." |
| 404 | `not_found` | Lokalen Cache-Eintrag entfernen, Liste reload |
| 405 | `method_not_allowed` | Hartes Bug — Sentry/Crash-Report |
| 409 | `conflict` | Bei Schreib-Replay: Queue-Eintrag als „verworfen" markieren, im Settings-Screen anzeigen |
| 422 | `validation_failed` | Toast mit `fields`-Map, Form-Felder markieren |
| 5xx | `server_error` | Retry mit exponential backoff (max. 3×), dann Toast „Server nicht erreichbar" |
| Network-Error | — | Stiller Wechsel in Offline-Modus, Schreib-Aktion in Queue legen |
---
## 13. Sicherheit
- **Token-Storage:** ausschließlich Keychain/EncryptedSharedPrefs, niemals in
Plain-Files oder UserDefaults/SharedPreferences. Kein Backup-Export.
- **TLS:** App akzeptiert nur HTTPS-URLs (außer `http://localhost` für Dev).
- **Certificate Pinning:** in v1 nicht zwingend, ab v2 empfohlen.
- **Auto-Lock:** nach 15 min Inaktivität soll die App entweder Bio-Auth verlangen
(Touch/Face ID) oder das Token löschen. Konfigurierbar im Settings-Screen.
- **Audit:** Jede schreibende API-Aktion landet automatisch in `api_logs` mit
Token-Name → von dort aus rückverfolgbar.
---
## 14. Telemetrie & Health
- Beim Login: einmal `GET /api/v1/bookings?limit=1` zur Validierung.
- Heartbeat: alle 5 min ein leichter `GET` (z.B. Events-Count), damit
`api_tokens.last_used_at` im Web-Admin aktuell bleibt.
- Crashlytics/Sentry empfohlen — die App ist im Schichtbetrieb, jede Sekunde Downtime kostet.
---
## 15. Roadmap-Vorschlag
| Sprint | Inhalt |
|--------|--------|
| 1 | Setup, Token-Onboarding, Liste der Anreisen (read-only), Buchungs-Detail (read-only) |
| 2 | Check-in / Check-out, Storno, Notizen (CRUD), Favoriten |
| 3 | Produkt-Tabelle (Swap, Add, Remove, Inline-Edit), Rabatt |
| 4 | Zahlungen (Erfassen, Verlauf, Edit/Delete), Offline-Queue |
| 5 | Suche, Event-Detail, Settings-Polish, QR-Onboarding |
| 6 | Push-Notifications (optional), Webhooks-Konfig |
---
## 16. Out-of-Scope (v1)
- Rechnungs-PDFs aus der App heraus generieren (öffnet ggf. den Web-Link)
- E-Mail-Versand von Buchungsbestätigungen (läuft serverseitig automatisch)
- Tagesabschluss-Funktion (`?page=daily-closing`) — bleibt Web-only
- Multi-Tenant / mehrere Server-Profile gleichzeitig
---
**Ansprechpartner Backend:** siehe `README.md`.
**Web-Referenz für UX:** `?page=arrivals` → Klick auf eine Buchung → Check-In-Modal.