baustelle-pwa/README.md
Eddy 64ebbf7850 Rauswurf aus dem Auftrag behoben, wartende Fotos am Auftrag sichtbar
Gemeldet 18.09.2026: nach "Fertig" in der Kamera stand die App auf der
Auftragsliste, und im Auftrag war von den Fotos nichts zu sehen.

Ursache Rauswurf (Prod-Log 17.09., Auftrag 111): closeModal() ruft
history.back() (asynchron), direkt danach setzt router.go() den Hash.
Der Ruecksprung laeuft erst nach dem Hash-Wechsel und springt hinter
den neuen Eintrag zurueck - angezeigt wird der Auftrag, in der Adresse
steht '#/orders'. Das naechste router.navigate() (Kamera "Fertig")
zeichnet dann die Liste. Betraf nur in derselben Sitzung ueber den
Plus-Knopf angelegte Auftraege.

- closeModal(): Zaehler _pendingBacks statt Boolean (KB #1209), History-
  Eintrag wird vor dem Cleanup zurueckgenommen
- router.go() und pushModal() warten ueber historySettled() auf
  ausstehende Ruecksprunge (Schutzzeit 1,5 s)
- Kamera und Galerie-Auswahl steuern nach dem Sichern ausdruecklich
  '#/orders/<id>' an statt den Hash der Adresszeile
- Auftragsseite: Block "Warten auf Upload (n)" mit Vorschaubildern aus
  der Warteschlange und Klartext; zieht nach photo-uploaded selbst nach
- Hinweis-Toast beim Schliessen der Kamera, wenn noch Fotos warten

Verifiziert lokal in Chromium mit echtem Offline-Modus; die Kamera
selbst (getUserMedia) am Handy ist noch nicht getestet. Kein Deploy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 23:36:14 +02:00

205 lines
14 KiB
Markdown

# Baustelle PWA
Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und Textnotizen, Skizzen, offline-fähig. Spricht die REST-API des Dolibarr-Bericht-Moduls.
## Stack
- **Vanilla JavaScript** — kein Build, keine Framework-Abhängigkeit
- **Service Worker** für App-Shell-Cache (Workbox-frei, eigene Implementierung)
- **IndexedDB** für die Offline-Upload-Queue (Auth-Session steckt im HttpOnly-Cookie, nicht in IndexedDB)
- **REST API** des `bericht`-Dolibarr-Moduls unter `/custom/bericht/api/`
## Features
**Aufträge**
- ✅ Login mit Dolibarr-Credentials → zentrales SSO (HttpOnly-Cookie `awl_sso`, same-origin; Single-Sign-On/-Logout über alle AWL-Apps). WebAuthn/Passkey-Login möglich
-**„Heute"-Ansicht**: offene Aufträge des Tages, gefiltert aus der normalen Auftragsliste, mit Route
für alle Termine auf einen Blick (öffnet Google Maps mit allen Adressen als Wegpunkten)
- ✅ Auftragsliste mit Suche, gefiltert auf eigene Aufträge (Multi-User)
- ✅ Toggle "Auch abgeschlossene" (Filter in localStorage persistiert)
- ✅ Auftragsdetail mit Kunde, Adresse, Telefon (Click-to-Call)
-**Neuer Auftrag** (FAB): Kunde per Suche/Zuletzt-verwendet wählen, dann **Ihr Zeichen** (Pflicht), **Auftragsbeschreibung** (optional), **Geplanter Liefertermin** (Datum + Uhrzeit) und Checkbox **Auftrag direkt freigeben**. Kunden-Defaults (Zahlungsbedingung/Zahlart, Ansprechpartner) werden übernommen. Bei „direkt freigeben" wird der Auftrag **ohne Position** validiert — die Leistungen kommen aus dem Stundenzettel. (Bis Bericht 1.3.1 hängte das Backend hier eine Std-Lohn-Zeile mit Menge 1 an; die verfälschte den Auftragswert, stand als offene Restmenge in der Lieferauflistung und im Kunden-PDF.)
**Foto-Upload**
- ✅ Live-Kamera-Modal (getUserMedia): bleibt für Serien-Aufnahmen offen — Auslöser + Filmstreifen mit Upload-Status je Foto (💾 gesichert → ⏳ lädt hoch → ✓ fertig), Front-/Rückkamera-Umschaltung, Fehlschuss löschbar
- ✅ Alternativ Aufnahme aus der Galerie
- ✅ Clientseitige Bild-Verkleinerung auf 2000px (JPEG q=0.85)
-**Persist-First / Datenverlust-Schutz**: jedes Foto wird beim Auslösen ZUERST in IndexedDB gesichert, Upload erst danach; Queue-Item wird nur nach bestätigtem Upload (HTTP 2xx mit gültigem `relpath`) gelöscht — überlebt fehlendes/schwaches Netz, hängende Uploads (45s-Timeout) und App-Kill. Erkennt 2xx-HTML (abgelaufene Session/Proxy-Loginseite) als Fehler statt als Erfolg
- ✅ Auto-Sync bei "online", periodisch (15s) und bei App-Fokus; Status-Badge (🟢 alles gesichert / 🟡 lädt hoch / 🔴 offline / ⚠️ fehlgeschlagen)
- ✅ Recovery: Tipp auf das Status-Badge öffnet die Warteschlange → erneut senden / teilen; Warnung beim Schließen mit noch ungesicherten Fotos
-**Wartende Fotos stehen am Auftrag**: Block „⏳ Warten auf Upload (n)" mit Vorschaubildern aus der Warteschlange und Klartext (gesichert auf dem Gerät, geht raus sobald Netz da ist); nach dem Upload zieht die Auftragsseite von selbst nach. Hinweis-Toast beim Schließen der Kamera, wenn noch etwas wartet
- ✅ Kacheln laden **serverseitige Thumbnails** (`photo.php?size=thumb`) statt der Originale — rund 10 KB statt mehrerer hundert KB je Kachel, parallel und mit `loading="lazy"`, Wiederholaufrufe enden mit 304
- ✅ Foto-Viewer mit Zoom + Swipe
- ✅ Foto-Skizze: Annotationen mit Pfeilen, Kreisen, Rechtecken, Text
**Notizen**
- ✅ Getippte Notiz zum Auftrag: Betreff + Text, Liste mit Vorschau und Zeitstempel, jederzeit wieder les- und änderbar
- ✅ Ablage als `notiz_<betreff>_<datum>.txt` im Auftragsordner — damit auch im Dolibarr-Auftrag und in der Anhänge-Spalte des Bericht-Editors sichtbar
- ✅ Löschen mit Rückfrage; in „Weitere Dokumente" tauchen Notizen nicht doppelt auf
**Sprachnotizen**
- ✅ Sprachaufnahme via MediaRecorder (webm)
- ✅ Whisper-Transkription (serverseitig, API-Aufruf)
**Berichte**
- ✅ Berichte pro Auftrag anzeigen
- ✅ Seitenkacheln zeigen das im Editor gebaute Seitenbild (`composite_path`) samt Anmerkungen — nicht mehr nur das Rohbild; Raster-Layouts sind mit „⊞ 4" gekennzeichnet
- ✅ Bericht erstellen mit Template-Auswahl
- ✅ Seiten umordnen per Drag&Drop
- ✅ Seite löschen, Notiz bearbeiten
- ✅ Touch-Unterschrift abnehmen (mit GPS + Name)
- ✅ Bericht finalisieren → PDF-Vorschau
- ✅ PDF-Download
**Lieferungen + Kundenunterschrift**
- ✅ Lieferungen-Liste pro Auftrag mit Status + Signed-Badge
- ✅ Lieferschein-PDF inline (PDF.js, alle Seiten)
- ✅ Vollbild-Querformat-Unterschrift mit HiDPI-Canvas, transparent (kein Hintergrund)
- ✅ Trimmt Canvas automatisch auf bemalte Fläche
- ✅ Name des Unterzeichners aus Kundendaten vorausgefüllt
- ✅ GPS-Koordinaten (optional, timeout 3s)
- ✅ Unterschrift wird via ODT-Hook in das Lieferschein-PDF gestempelt
- ✅ Expedition wird nach Unterschrift automatisch validiert + geschlossen
- ✅ Signed-PDF direkt nach Bestätigung als Vorschau
**Materialliste**
- ✅ Material pro Auftrag erfassen (Label, Menge, Einheit, Notiz)
- ✅ Material löschen
**PWA**
- ✅ Service Worker für Offline-Start
- ✅ Installierbar als PWA (Home-Screen-Icon)
-**Web Share Target** — aus WhatsApp & Co. Bilder **und** Beschreibungstext an „Baustelle" teilen:
anmelden (falls nötig, direkt in der Teilen-Seite), Auftrag suchen und wählen, ablegen. Bilder gehen
über die normale Upload-Queue, der Text wird als Notiz (`notiz_*.txt`) im Auftrag gespeichert
-**Serie ohne Sucherei**: die Teilen-Seite merkt sich die letzten drei benutzten Aufträge und zeigt
sie unter „Zuletzt benutzt" ganz oben. Wurde einer davon in den letzten 30 Minuten benutzt, ist er
**vorausgewählt** — die zweite und dritte Nachricht der Kundin kosten dann nur noch einen Tipp.
Danach steht er nur noch als Schnellzugriff da, damit nichts versehentlich im Auftrag von vorgestern landet
- ✅ Nativer Android-Share-Sheet für Fotos, PDFs und Dokumente (`navigator.share`) — direkt an WhatsApp / Signal / Mail
- ✅ Mehrfachauswahl in Foto- und Dokument-Liste mit Batch-Teilen
- ✅ Android-Hardware-Back-Button schließt Modale/Ansichten sauber (History-Modal-Stack), bricht Mehrfachauswahl ab statt App zu verlassen
- ✅ „Nochmal drücken zum Beenden"-Toast auf Top-Level-Routen
- ✅ PIN-Schutz (optional)
-**Push-Benachrichtigungen via ntfy** (statt klassischem Web-Push): eigener Server/Topic in den
Einstellungen, Browser-Notification-Permission, Reconnect bei Verbindungsabbruch
-**Bild-Cache** `baustelle-media` (cache-first, LRU 300): schon angesehene Fotos sind auch ohne Netz da; überlebt Deploys, wird beim Abmelden geleert
-**Keine Browser-Dialoge**`confirm`/`alert`/`prompt` sind durch eigene Modale ersetzt (`confirmDialog`, `alertDialog`, `inputDialog`); der Hardware-Zurück-Button schließt sie als Abbruch
**Ohne Netz** (Keller, Neubau, Funkloch — auf der Baustelle der Normalfall)
-**Datenspiegel**: Auftragsliste, Kundenliste, Berichte sowie geöffnete Auftrags- und
Kundendetails werden nach jedem erfolgreichen Abruf in IndexedDB gespiegelt und ohne Netz
von dort angezeigt (Details: die letzten 30). Zusammen mit dem Bild-Cache steht ein einmal
geöffneter Auftrag samt Fotos auch im Funkloch
-**Sichtbares Band** „📴 Kein Netz — angezeigt wird der Stand von HH:MM Uhr" — ohne den
Hinweis hält man alte Zahlen für aktuell. Kommt das Netz zurück, lädt die Ansicht neu und
das Band verschwindet
-**Suche greift auf den gespiegelten Stand zu**, statt ins Leere zu laufen (mit Hinweis)
-**Verständliche Meldungen**: ein `fetch()`, das nicht bis zum Server kommt, wird zu
„Keine Verbindung zum Server" — nicht mehr die rohe Browsermeldung „Failed to fetch".
Ein echter Serverfehler (401/403/500) wird weiterhin als solcher gemeldet
-**Start ohne Netz**: der Service Worker liefert die App-Hülle aus dem Cache; die Seite
meldet ihm nach dem Laden ihre tatsächlichen Asset-URLs zum Nachcachen, sonst wäre der
Cache direkt nach einem Deploy leer (`?v=<mtime>` verhindert eine feste Dateiliste)
-**Beim Abmelden wird der Spiegel gelöscht** — am geteilten Gerät sieht sonst der nächste
Benutzer offline die Kunden und Preise des vorherigen
-**Ändern braucht Netz** — bewusst keine Schreib-Warteschlange für Auftragsdaten
(bräuchte Idempotenz + Konfliktbehandlung). Fotos sind die Ausnahme: die gehen über die
Persist-First-Queue und werden nachgeliefert
## Hosting
Deploy per Forgejo-Pipeline (Commit-Tag `[deploy]`) nach `/mnt/appdata/firma/dolibarr-202509/modules/baustelle/`. Erreichbar unter `https://awl.data-it-solution.de/custom/baustelle/`. Cache-Busting automatisch via `filemtime()` in `index.php` (`?v=<mtime>` an JS/CSS) — kein manuelles Versions-Hochzählen nötig, die PWA aktualisiert sich beim nächsten Laden.
## Verzeichnis-Struktur
```
baustelle-pwa/
├── index.php App-Shell (Cache-Busting via filemtime → ?v=<mtime>)
├── share.html Web-Share-Target-Empfänger
├── manifest.webmanifest PWA Manifest
├── sw.js Service Worker (Network-First für Assets, API pass-through,
│ cache-first für photo.php?size=thumb → Cache baustelle-media)
├── app.js Hauptlogik (Routes, Views, Dialoge, Notizen)
├── app.css Mobile-first Styling
├── lib/
│ ├── idb.js IndexedDB-Wrapper (Foto-Queue)
│ ├── api.js REST-API-Client (SSO-Cookie same-origin, JSON-Guard, Timeout)
│ ├── offline.js Offline-Queue + Sync (Persist-First)
│ ├── router.js Hash-Router
│ ├── pdf.min.js PDF.js (Lieferschein-Vorschau)
│ └── pdf.worker.min.js PDF.js Worker
└── icons/
├── icon-192.png
└── icon-512.png
```
## API
Die App spricht die Bericht-API unter `/custom/bericht/api/`:
**Auth**
- `POST /auth.php` — Login mit Dolibarr-Credentials → setzt HttpOnly-Cookie `awl_sso` (zentrales SSO via awlauth). Folge-Requests authentisieren same-origin über das Cookie (kein Bearer-Token)
- `GET /verify.php` — prüft das Cookie und verlängert die Session (sliding renewal); läuft bei jedem Routenwechsel (`ensureAuth()`) und ist der Weg, wie `share.html` prüft, ob eine Anmeldung nötig ist
- `POST /logout.php` — löscht das `awl_sso`-Cookie (Single-Logout über alle AWL-Apps)
**Aufträge**
- `GET /orders.php` — Aufträge des Users (Filter: `?open=1`, `?q=suche`)
- `GET /orders.php?id=X` — Auftrag-Detail inkl. verknüpfte Berichte
- `GET /orders.php?id=X&action=photos` — Anhang-Liste (je Datei `taken_at` = EXIF-Aufnahmezeit, danach sortiert)
- `POST /orders.php?id=X&action=upload_photo` — Foto-Upload (multipart)
- `POST /orders.php?action=create` — Neuen Auftrag anlegen (Kunde, Ihr Zeichen, Beschreibung, Liefertermin, optional direkt freigeben)
**Kunden**
- `GET /customers.php` — Kundenliste (Filter: `?q=suche`)
- `GET /customers.php?id=X` — Kundendetail
**Berichte**
- `GET /reports.php` — Alle Berichte des Users
- `GET /reports.php?id=X` — Bericht-Detail mit Seiten (je Seite `composite_path`, `title`, `layout``composite_path` anzeigen, wenn gesetzt)
- `POST /reports.php?action=create` — Neuen Bericht anlegen
- `POST /reports.php?id=X&action=finalize` — Bericht finalisieren
- `DELETE /reports.php?id=X` — Bericht löschen
**Seiten**
- `DELETE /pages.php?id=X` — Seite löschen
- `POST /pages.php?id=X` — Seite aktualisieren (note, rotation)
- `POST /pages.php?action=signature&bericht_id=X` — Unterschrift hinzufügen
- `POST /pages.php?action=reorder` — Seiten umordnen
**Material**
- `GET /materials.php?element_type=X&element_id=Y` — Materialliste
- `POST /materials.php?element_type=X&element_id=Y` — Material hinzufügen
- `POST /materials.php?id=X&delete=1` — Material löschen
**Lieferungen**
- `GET /shipments.php?order_id=X` — Lieferungen zu einem Auftrag
- `GET /shipments.php?id=X` — Lieferung-Detail (inkl. bericht_id falls Unterschrift vorhanden)
- `GET /shipments.php?id=X&action=pdf[&variant=auto|signed|unsigned]` — Lieferschein-PDF
- `POST /shipments.php?id=X&action=confirm` — Kunden-Unterschrift einstempeln (FormData mit signature_png, signer_name, gps_lat, gps_lon, signed_at). Backend setzt signed_status=1 und schließt die Expedition
**Notizen**
- `GET /note.php?order_id=X` — Liste (Betreff, Vorschau, Zeitstempel)
- `GET /note.php?order_id=X&file=<name>` — eine Notiz mit vollem Text
- `POST /note.php?order_id=X` — anlegen `{ subject, text }`
- `POST /note.php?order_id=X&file=<name>` — ändern
- `POST /note.php?order_id=X&file=<name>&delete=1` — löschen
**Medien**
- `GET /photo.php?relpath=X` — Datei abrufen; Auth über `awl_sso`-Cookie (same-origin, auch für `<img>`-Tags — kein `?jwt=` mehr nötig)
- `GET /photo.php?relpath=X&size=thumb&w=<px>` — serverseitiges Thumbnail (GD, gecacht, ETag → 304). **Der Weg für Kachel-Ansichten.** `size=small` ist etwas anderes: das liefert nur ein von Dolibarr vorgefertigtes Thumb, das es für PWA-Uploads gar nicht gibt — dort kam bis Modul-Version 1.5.1 immer das Original zurück
- `POST /delete_photo.php` — Foto löschen
- `POST /voice.php?order_id=X` — Sprachnotiz hochladen
- `POST /transcribe.php` — Whisper-Transkription anfordern
- `GET /pdf.php?id=X` — PDF abrufen (Final oder Preview); Auth über `awl_sso`-Cookie (same-origin)
**Templates**
- `GET /templates.php` — Bericht-Vorlagen
- `GET /odt_templates.php` — ODT-Deckblatt-Vorlagen
**Konfiguration**
- `GET /config.php` — Admin-Vorgaben zur Fotoqualität (`photo_maxside`, `photo_quality`); wird beim Start und nach jedem Login neu geladen, zwischengespeichert in `localStorage` für den Offline-Fall
## Lizenz
GPL v3+