diff --git a/README.md b/README.md index 9b55a84..9b10a6f 100644 --- a/README.md +++ b/README.md @@ -9,27 +9,49 @@ Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprachnotiz - **IndexedDB** für JWT-Storage und Offline-Upload-Queue - **REST API** des `bericht`-Dolibarr-Moduls unter `/custom/bericht/api/` -## Features (MVP) +## Features +**Aufträge** - ✅ Login mit Dolibarr-Credentials → JWT (7 Tage gültig) - ✅ 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) + +**Foto-Upload** - ✅ Foto-Aufnahme direkt aus der Kamera oder aus Galerie - ✅ Clientseitige Bild-Verkleinerung auf 2000px (JPEG q=0.85) - ✅ Offline-Queue: bei fehlgeschlagenem Upload landet das Foto in IndexedDB - ✅ Auto-Sync wenn wieder online (mit Status-Badge) +- ✅ Foto-Viewer mit Zoom + Swipe +- ✅ Foto-Skizze: Annotationen mit Pfeilen, Kreisen, Rechtecken, Text + +**Sprachnotizen** +- ✅ Sprachaufnahme via MediaRecorder (webm) +- ✅ Whisper-Transkription (serverseitig, API-Aufruf) + +**Berichte** +- ✅ Berichte pro Auftrag anzeigen +- ✅ 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 + +**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 API (Fotos aus anderer App teilen) +- ✅ PIN-Schutz (optional) -## Geplant (Phase 4) +## Geplant -- Sprachnotizen via MediaRecorder -- Touch-Skizzen-Editor für Bilder -- Schnell-Bericht direkt in der PWA erstellen -- Touch-Unterschrift abnehmen -- PIN-Schutz / WebAuthn - Push-Notifications für neue Aufträge -- Web Share Target API +- Kunden-Schnellsuche ## Hosting @@ -56,13 +78,49 @@ baustelle-pwa/ ## API -Die App spricht ausschließlich die Bericht-API: -- `POST /api/auth.php` — Login mit Dolibarr-Credentials -- `GET /api/orders.php` — Aufträge des Users -- `GET /api/orders.php?id=X` — Auftrag-Detail -- `GET /api/orders.php?id=X&action=photos` — Anhang-Liste -- `POST /api/orders.php?id=X&action=upload_photo` — Foto-Upload -- `GET /api/reports.php?id=X` — Bericht-Detail +Die App spricht die Bericht-API unter `/custom/bericht/api/`: + +**Auth** +- `POST /auth.php` — Login mit Dolibarr-Credentials → JWT + +**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 +- `POST /orders.php?id=X&action=upload_photo` — Foto-Upload (multipart) + +**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 +- `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 + +**Medien** +- `GET /photo.php?relpath=X&jwt=Y` — Foto abrufen (mit Thumbnail: `&size=thumb`) +- `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&jwt=Y` — PDF abrufen (Final oder Preview) + +**Templates** +- `GET /templates.php` — Bericht-Vorlagen +- `GET /odt_templates.php` — ODT-Deckblatt-Vorlagen ## Lizenz diff --git a/app.css b/app.css index 4a5dfb9..e2c9b40 100644 --- a/app.css +++ b/app.css @@ -129,7 +129,7 @@ body { .search-bar { margin-bottom: 12px; } -.search-bar input { +.search-bar input[type="search"] { width: 100%; padding: 12px 16px; border-radius: 24px; @@ -139,6 +139,20 @@ body { font-size: 15px; -webkit-appearance: none; } +.filter-toggle { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + font-size: 14px; + color: #aaa; + cursor: pointer; +} +.filter-toggle input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: #7aa2f7; +} .order-card { background: #25252b; diff --git a/app.js b/app.js index 17265ad..cee135e 100644 --- a/app.js +++ b/app.js @@ -272,27 +272,63 @@ router.on('/orders', async () => { setBack(false); showLoader('Lade Aufträge…'); - try { - const data = await api.listOrders({ open: 1 }); - if (!data.orders.length) { - main().innerHTML = '
📭
Keine offenen Aufträge
'; - return; - } - const html = ` - -
${renderOrderList(data.orders)}
- `; - main().innerHTML = html; + // Filter-Status aus localStorage laden (persistiert) + let showAllOrders = localStorage.getItem('pwa_show_all_orders') === '1'; + + async function loadOrders(q = '') { + const opts = q ? { q } : {}; + if (!showAllOrders) opts.open = 1; + return api.listOrders(opts); + } + + function bindOrderCards() { document.querySelectorAll('.order-card').forEach(c => { c.addEventListener('click', () => router.go('#/orders/' + c.dataset.id)); }); + } + + try { + const data = await loadOrders(); + if (!data.orders.length) { + main().innerHTML = ` + +
📭
${showAllOrders ? 'Keine Aufträge gefunden' : 'Keine offenen Aufträge'}
+ `; + document.getElementById('show-all-toggle').addEventListener('change', async (e) => { + showAllOrders = e.target.checked; + localStorage.setItem('pwa_show_all_orders', showAllOrders ? '1' : '0'); + showLoader('Lade Aufträge…'); + router.go('#/orders'); + }); + return; + } + const html = ` + +
${renderOrderList(data.orders)}
+ `; + main().innerHTML = html; + bindOrderCards(); + document.getElementById('order-search').addEventListener('input', async (e) => { const q = e.target.value; - const d = await api.listOrders({ q, open: 1 }); + const d = await loadOrders(q); document.getElementById('order-list').innerHTML = renderOrderList(d.orders); - document.querySelectorAll('.order-card').forEach(c => { - c.addEventListener('click', () => router.go('#/orders/' + c.dataset.id)); - }); + bindOrderCards(); + }); + + document.getElementById('show-all-toggle').addEventListener('change', async (e) => { + showAllOrders = e.target.checked; + localStorage.setItem('pwa_show_all_orders', showAllOrders ? '1' : '0'); + const q = document.getElementById('order-search').value; + const d = await loadOrders(q); + document.getElementById('order-list').innerHTML = renderOrderList(d.orders); + bindOrderCards(); }); } catch (e) { main().innerHTML = '
⚠️
' + e.message + '
';