Toggle "Auch abgeschlossene" für Auftragsliste + Doku aktualisiert
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 5s
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 5s
PWA: - Checkbox in /orders Route hinzugefügt - Filter in localStorage persistiert - CSS für .filter-toggle Doku: - README.md komplett aktualisiert mit allen Features - API-Dokumentation erweitert [deploy] Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0a9c4d4db3
commit
295ac82f43
3 changed files with 139 additions and 31 deletions
88
README.md
88
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
|
- **IndexedDB** für JWT-Storage und Offline-Upload-Queue
|
||||||
- **REST API** des `bericht`-Dolibarr-Moduls unter `/custom/bericht/api/`
|
- **REST API** des `bericht`-Dolibarr-Moduls unter `/custom/bericht/api/`
|
||||||
|
|
||||||
## Features (MVP)
|
## Features
|
||||||
|
|
||||||
|
**Aufträge**
|
||||||
- ✅ Login mit Dolibarr-Credentials → JWT (7 Tage gültig)
|
- ✅ Login mit Dolibarr-Credentials → JWT (7 Tage gültig)
|
||||||
- ✅ Auftragsliste mit Suche, gefiltert auf eigene Aufträge (Multi-User)
|
- ✅ 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)
|
- ✅ Auftragsdetail mit Kunde, Adresse, Telefon (Click-to-Call)
|
||||||
|
|
||||||
|
**Foto-Upload**
|
||||||
- ✅ Foto-Aufnahme direkt aus der Kamera oder aus Galerie
|
- ✅ Foto-Aufnahme direkt aus der Kamera oder aus Galerie
|
||||||
- ✅ Clientseitige Bild-Verkleinerung auf 2000px (JPEG q=0.85)
|
- ✅ Clientseitige Bild-Verkleinerung auf 2000px (JPEG q=0.85)
|
||||||
- ✅ Offline-Queue: bei fehlgeschlagenem Upload landet das Foto in IndexedDB
|
- ✅ Offline-Queue: bei fehlgeschlagenem Upload landet das Foto in IndexedDB
|
||||||
- ✅ Auto-Sync wenn wieder online (mit Status-Badge)
|
- ✅ 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
|
- ✅ Service Worker für Offline-Start
|
||||||
- ✅ Installierbar als PWA (Home-Screen-Icon)
|
- ✅ 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
|
- Push-Notifications für neue Aufträge
|
||||||
- Web Share Target API
|
- Kunden-Schnellsuche
|
||||||
|
|
||||||
## Hosting
|
## Hosting
|
||||||
|
|
||||||
|
|
@ -56,13 +78,49 @@ baustelle-pwa/
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
Die App spricht ausschließlich die Bericht-API:
|
Die App spricht die Bericht-API unter `/custom/bericht/api/`:
|
||||||
- `POST /api/auth.php` — Login mit Dolibarr-Credentials
|
|
||||||
- `GET /api/orders.php` — Aufträge des Users
|
**Auth**
|
||||||
- `GET /api/orders.php?id=X` — Auftrag-Detail
|
- `POST /auth.php` — Login mit Dolibarr-Credentials → JWT
|
||||||
- `GET /api/orders.php?id=X&action=photos` — Anhang-Liste
|
|
||||||
- `POST /api/orders.php?id=X&action=upload_photo` — Foto-Upload
|
**Aufträge**
|
||||||
- `GET /api/reports.php?id=X` — Bericht-Detail
|
- `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
|
## Lizenz
|
||||||
|
|
||||||
|
|
|
||||||
16
app.css
16
app.css
|
|
@ -129,7 +129,7 @@ body {
|
||||||
.search-bar {
|
.search-bar {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.search-bar input {
|
.search-bar input[type="search"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
|
|
@ -139,6 +139,20 @@ body {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
-webkit-appearance: none;
|
-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 {
|
.order-card {
|
||||||
background: #25252b;
|
background: #25252b;
|
||||||
|
|
|
||||||
54
app.js
54
app.js
|
|
@ -272,27 +272,63 @@ router.on('/orders', async () => {
|
||||||
setBack(false);
|
setBack(false);
|
||||||
showLoader('Lade Aufträge…');
|
showLoader('Lade Aufträge…');
|
||||||
|
|
||||||
|
// 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 {
|
try {
|
||||||
const data = await api.listOrders({ open: 1 });
|
const data = await loadOrders();
|
||||||
if (!data.orders.length) {
|
if (!data.orders.length) {
|
||||||
main().innerHTML = '<div class="empty-state"><div class="icon">📭</div>Keine offenen Aufträge</div>';
|
main().innerHTML = `
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="search" id="order-search" placeholder="🔍 Suchen…">
|
||||||
|
<label class="filter-toggle"><input type="checkbox" id="show-all-toggle" ${showAllOrders ? 'checked' : ''}> Auch abgeschlossene</label>
|
||||||
|
</div>
|
||||||
|
<div class="empty-state"><div class="icon">📭</div>${showAllOrders ? 'Keine Aufträge gefunden' : 'Keine offenen Aufträge'}</div>
|
||||||
|
`;
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const html = `
|
const html = `
|
||||||
<div class="search-bar"><input type="search" id="order-search" placeholder="🔍 Suchen…"></div>
|
<div class="search-bar">
|
||||||
|
<input type="search" id="order-search" placeholder="🔍 Suchen…">
|
||||||
|
<label class="filter-toggle"><input type="checkbox" id="show-all-toggle" ${showAllOrders ? 'checked' : ''}> Auch abgeschlossene</label>
|
||||||
|
</div>
|
||||||
<div id="order-list">${renderOrderList(data.orders)}</div>
|
<div id="order-list">${renderOrderList(data.orders)}</div>
|
||||||
`;
|
`;
|
||||||
main().innerHTML = html;
|
main().innerHTML = html;
|
||||||
document.querySelectorAll('.order-card').forEach(c => {
|
bindOrderCards();
|
||||||
c.addEventListener('click', () => router.go('#/orders/' + c.dataset.id));
|
|
||||||
});
|
|
||||||
document.getElementById('order-search').addEventListener('input', async (e) => {
|
document.getElementById('order-search').addEventListener('input', async (e) => {
|
||||||
const q = e.target.value;
|
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.getElementById('order-list').innerHTML = renderOrderList(d.orders);
|
||||||
document.querySelectorAll('.order-card').forEach(c => {
|
bindOrderCards();
|
||||||
c.addEventListener('click', () => router.go('#/orders/' + c.dataset.id));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) {
|
} catch (e) {
|
||||||
main().innerHTML = '<div class="empty-state"><div class="icon">⚠️</div>' + e.message + '</div>';
|
main().innerHTML = '<div class="empty-state"><div class="icon">⚠️</div>' + e.message + '</div>';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue