Farbige Symbole statt Emojis auf allen Knöpfen [deploy]
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 13s

Eddy: „so dass es gut aussieht", zu rein weißen Symbolen dann
„viel zu schlicht". Emojis erscheinen je nach Handy als Kästchen.

- icon() in app.js (Material-Symbole), Farbe je Symbol in app.css
  (.ikon-<name>): große Knöpfe = weißes Symbol im farbigen runden
  Feld (Kamera blau, Galerie grün, Sprachnotiz rot, Notizen gelb,
  Material orange, Bericht lila, Lieferungen türkis …), kleine
  Knöpfe = farbiges Symbol (Teilen blau, Löschen rot, Stift/Blitz/
  Hilfe gelb, Download/Telefon grün).
- Untere Leiste: farbiges Symbol über dem Wort, Heute als Sonne,
  Zahnrad heißt „Einstellungen".
- Titel und Überschriften ohne Emoji; Telefonnummer lesbar.
- .icon-btn[hidden] bleibt versteckt (inline-flex hätte hidden ausgehebelt).

Im Android-Emulator angesehen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-10-20 15:55:30 +02:00
parent 73a481fa8f
commit 5a54a64b7e
5 changed files with 202 additions and 73 deletions

View file

@ -20,6 +20,8 @@ Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und
- ✅ 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.)
**Aussehen:** Knöpfe tragen farbige Symbole statt Emojis (`icon()` in app.js, Material Icons; Farben je Symbol in app.css `.ikon-<name>`) — Emojis erscheinen je nach Handy als Kästchen. Große Knöpfe: weißes Symbol im farbigen runden Feld; kleine Knöpfe: farbiges Symbol. Untere Leiste: farbiges Symbol über dem Wort. Überschriften fett in normaler Schreibweise, ohne Versalien und ohne Emoji (ROADMAP Punkt 12).
**Foto- und Video-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 (nur bei zwei Kameras), Fehlschuss löschbar
- ✅ **Volle Kameraauflösung**: ausgelöst wird über `ImageCapture.takePhoto()` (native JPEG der Kamera inkl. EXIF) — ein Standbild aus dem Video-Stream hat nur Stream-Auflösung und ist nur noch die Rückfallebene (iOS/Safari, ältere Browser)

View file

@ -61,6 +61,26 @@ EV_ABS:ABS_MT_TRACKING_ID:<neu> EV_ABS:ABS_MT_POSITION_X:<0..32767> … EV_SYN:0
`BTN_TOUCH` und frische Tracking-IDs sind Pflicht, sonst meldet Android keine Berührung bzw. alte Finger „hängen").
Skript: KB-Eintrag zur Baustellen-Kamera.
## 12. Farbige Symbole statt Emojis (Eddy 19./20.10.2026: „so dass es gut aussieht", dann zu reinem Weiß: „viel zu schlicht")
Anlass: der 🔄-Wechselknopf erschien als blaues Kästchen („den hässlichen Wechselbutton lässt du da doch nicht").
- [x] ✅ 20.10.2026: Alle Knöpfe mit Symbol nutzen `icon()` (app.js, Material Icons, `fill=currentColor`): Kopfzeilen der
Ansichten (Teilen, Zeichnen, Löschen, Download), Auswahlleiste (Alle, Teilen), Kopfzeile oben (Installieren,
Hilfe statt rotem ❓), Blitz-Knopf (Aus/Auto/An/Licht als Blitz-Symbole), die großen Knöpfe am Auftrag
(Kamera, Galerie, Mikrofon, Notizen, Liste, Bericht, Lieferung), Einstellungen, Unterschrift, Warteschlange,
Telefonnummer (jetzt lesbar blau statt dunkelblau unterstrichen).
- [x] ✅ 20.10.2026 nach „viel zu schlicht": **Farbe zurück, ohne Emojis** — große Knöpfe tragen ein weißes Symbol im
farbigen runden Feld (Kamera blau, Galerie grün, Sprachnotiz rot, Notizen gelb, Material orange, Bericht lila,
Lieferungen türkis …), kleine Knöpfe ein farbiges Symbol (Teilen blau, Löschen rot, Stift/Blitz/Hilfe gelb,
Download/Telefon grün). Farben je Symbol in `app.css` (`.ikon-<name>`: `--ikon-farbe`, `--ikon-grund`).
- [x] ✅ Untere Leiste wie in Handy-Apps: Symbol über dem Wort, farbig (Heute als Sonne), aktive Seite blau; das Zahnrad heißt jetzt „Einstellungen".
- [x] ✅ Titel der Vollbild-Ansichten und Abschnitts-Überschriften ohne Emoji (reiner Text).
- Bewusst gelassen: Zustandsanzeige 🟢/🟡/🔴 (Farbe ist dort die Aussage), Stempel im Skizzen-Editor (werden so ins Bild
gezeichnet), Bilder in leeren Listen, Emojis in Meldungen.
- [ ] Offen, nicht angefasst: Die untere Leiste steht seit v1.0.0 am **Seitenende** statt fest unten (`#app` nur
`min-height: 100dvh`, `#main` wächst mit) — bei langen Listen erst nach Scrollen erreichbar. Wenn gewünscht:
Leiste fest unten, dabei FAB und „Geteiltes ablegen"-Leiste (Punkt 8) mit hochrücken.
## 11. Videos aufnehmen (gewünscht 2026-10-19)
Eddy: *„Bei Fotos aufnehmen auch Videos unterstützen bitte"*.

75
app.css
View file

@ -45,7 +45,14 @@ body {
padding: 4px 8px;
cursor: pointer;
-webkit-appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
}
/* Weisse Symbole statt Emojis (icon() in app.js) — folgen der Schriftfarbe */
.icon-btn svg { display: block; flex: 0 0 auto; }
.icon-btn[hidden] { display: none; } /* inline-flex wuerde das hidden-Attribut sonst aushebeln */
#status-badge { font-size: 14px; }
#main {
@ -67,10 +74,18 @@ body {
background: transparent;
border: none;
color: #888;
padding: 14px 8px;
font-size: 12px;
padding: 8px 2px;
font-size: 11px;
cursor: pointer;
/* Symbol ueber dem Wort, wie in Handy-Apps ueblich (vorher bunte Emojis) */
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
white-space: nowrap;
}
#bottom-nav button svg { display: block; opacity: 0.7; }
#bottom-nav button.active svg { opacity: 1; }
#bottom-nav button.active { color: #7aa2f7; }
/* ----- FAB (Neuer Auftrag) ----- */
@ -176,6 +191,58 @@ body {
transition: transform 0.1s;
}
.btn:active { transform: scale(0.97); }
/* ---- Farbige Symbole (icon() in app.js, Klasse .ikon-<name>) ----
Eddy 20.10.2026: Emojis erscheinen je nach Handy als Kaestchen, rein weisse Symbole waren
„viel zu schlicht". Also: kleine Knoepfe = farbiges Symbol (--ikon-farbe), grosse Knoepfe =
weisses Symbol im farbigen runden Feld (--ikon-grund). */
.ikon { color: var(--ikon-farbe, currentColor); }
.btn:has(> .ikon) { display: flex; align-items: center; justify-content: center; gap: 12px; }
.btn > .ikon {
flex: 0 0 auto;
box-sizing: content-box;
padding: 7px;
border-radius: 50%;
color: #fff;
background: var(--ikon-grund, #555);
}
.btn-large > .ikon { background: rgba(255, 255, 255, 0.22); } /* blauer Hauptknopf: helles Feld */
.btn-small:has(> .ikon) { gap: 6px; }
.btn-small > .ikon { padding: 3px; }
.ikon-kamera { --ikon-farbe: #60a5fa; --ikon-grund: #3b82f6; }
.ikon-galerie { --ikon-farbe: #4ade80; --ikon-grund: #16a34a; }
.ikon-mikrofon { --ikon-farbe: #f87171; --ikon-grund: #dc2626; }
.ikon-notizen { --ikon-farbe: #facc15; --ikon-grund: #ca8a04; }
.ikon-liste { --ikon-farbe: #fb923c; --ikon-grund: #ea580c; }
.ikon-bericht { --ikon-farbe: #c084fc; --ikon-grund: #9333ea; }
.ikon-lieferung { --ikon-farbe: #2dd4bf; --ikon-grund: #0d9488; }
.ikon-karte { --ikon-farbe: #34d399; --ikon-grund: #059669; }
.ikon-zeichnen { --ikon-farbe: #fbbf24; --ikon-grund: #d97706; }
.ikon-oeffnen { --ikon-farbe: #818cf8; --ikon-grund: #4f46e5; }
.ikon-ansehen { --ikon-farbe: #a78bfa; --ikon-grund: #7c3aed; }
.ikon-speichern { --ikon-farbe: #60a5fa; --ikon-grund: #2563eb; }
.ikon-glocke { --ikon-farbe: #fbbf24; --ikon-grund: #d97706; }
.ikon-schluessel { --ikon-farbe: #facc15; --ikon-grund: #ca8a04; }
.ikon-sync { --ikon-farbe: #22d3ee; --ikon-grund: #0891b2; }
.ikon-abmelden { --ikon-farbe: #f87171; --ikon-grund: #dc2626; }
.ikon-loeschen { --ikon-farbe: #f87171; --ikon-grund: #dc2626; }
.ikon-teilen { --ikon-farbe: #60a5fa; --ikon-grund: #2563eb; }
.ikon-download { --ikon-farbe: #4ade80; --ikon-grund: #16a34a; }
.ikon-auswahl { --ikon-farbe: #60a5fa; --ikon-grund: #2563eb; }
.ikon-alle { --ikon-farbe: #c4b5fd; }
.ikon-telefon { --ikon-farbe: #4ade80; }
.ikon-hilfe { --ikon-farbe: #fbbf24; }
.ikon-installieren { --ikon-farbe: #60a5fa; }
.ikon-wechseln { --ikon-farbe: #93c5fd; }
.ikon-blitzAus { --ikon-farbe: #d1d5db; }
.ikon-blitzAuto, .ikon-blitzAn, .ikon-licht { --ikon-farbe: #facc15; }
/* untere Leiste */
.ikon-heute { --ikon-farbe: #fbbf24; }
.ikon-auftraege { --ikon-farbe: #60a5fa; }
.ikon-kunden { --ikon-farbe: #4ade80; }
.ikon-berichte { --ikon-farbe: #c084fc; }
.ikon-einstellungen { --ikon-farbe: #cbd5e1; }
.tel-link { display: inline-flex; align-items: center; gap: 6px; color: #7aa2f7; text-decoration: none; font-weight: 600; }
.btn-secondary {
background: #2a2a30;
border: 1px solid #444;
@ -1383,9 +1450,7 @@ body {
}
/* Blitz-Knopf: Text statt nur Symbol, damit der Zustand lesbar ist (Aus/Auto/An/Licht) */
.camera-modal .cam-blitz { font-size: 15px; white-space: nowrap; }
/* Kamera wechseln: weisses Symbol wie ✕, kein Emoji (das erschien als blaues Kaestchen) */
.camera-modal .cam-flip { display: inline-flex; align-items: center; }
.camera-modal .cam-flip svg { display: block; }
/* Kamera wechseln: weisses Symbol wie ✕, kein Emoji (das erschien als blaues Kaestchen) — Stil: .icon-btn */
/* Scharfstellen per Antippen */
.camera-modal .cam-fokus-ring {
position: absolute;

164
app.js
View file

@ -665,8 +665,8 @@ router.on('/today', async () => {
const waypoints = todays.map(o => encodeURIComponent([o.customer.address, o.customer.zip, o.customer.town].filter(Boolean).join(' '))).join('/');
html += `
<div class="detail-section">
<h3>🚗 Route für heute (${todays.length})</h3>
<a class="btn" href="https://www.google.com/maps/dir/${waypoints}" target="_blank" style="text-decoration:none;text-align:center;">🗺 Alle in Google Maps öffnen</a>
<h3>Route für heute (${todays.length})</h3>
<a class="btn" href="https://www.google.com/maps/dir/${waypoints}" target="_blank" style="text-decoration:none;text-align:center;">${icon('karte', 20)}Alle in Google Maps öffnen</a>
</div>
<h4 style="opacity:0.7;margin:16px 0 8px;">Heute (${todays.length})</h4>
${renderOrderList(todays)}
@ -839,7 +839,7 @@ router.on('/orders/:id', async (args) => {
<p><strong>${escapeHtml(data.customer.name)}</strong></p>
<p>${escapeHtml(data.customer.address || '')}</p>
<p>${escapeHtml((data.customer.zip || '') + ' ' + (data.customer.town || ''))}</p>
${data.customer.phone ? `<p><a href="tel:${escapeHtml(data.customer.phone)}">📞 ${escapeHtml(data.customer.phone)}</a></p>` : ''}
${data.customer.phone ? `<p><a class="tel-link" href="tel:${escapeHtml(data.customer.phone)}">${icon('telefon', 18)}${escapeHtml(data.customer.phone)}</a></p>` : ''}
</div>
${data.order.auftragsbeschreibung ? `
@ -848,22 +848,22 @@ router.on('/orders/:id', async (args) => {
<p>${escapeHtml(data.order.auftragsbeschreibung)}</p>
</div>` : ''}
<button class="btn btn-large" id="btn-take-photo">📷 Foto aufnehmen</button>
<button class="btn btn-large" id="btn-take-photo">${icon('kamera', 20)}Foto aufnehmen</button>
<input type="file" id="camera-input" class="hidden-input" accept="image/*" capture="environment" multiple>
<button class="btn btn-secondary" id="btn-pick-photo">📂 Aus Galerie wählen</button>
<button class="btn btn-secondary" id="btn-pick-photo">${icon('galerie', 20)}Aus Galerie wählen</button>
<input type="file" id="gallery-input" class="hidden-input" accept="image/*,video/*" multiple>
<button class="btn btn-secondary" id="btn-voice">🎙 Sprachnotiz aufnehmen</button>
<button class="btn btn-secondary" id="btn-notes">📝 Notizen${notesCount ? ' (' + notesCount + ')' : ''}</button>
<button class="btn btn-secondary" id="btn-material">📦 Materialliste</button>
<button class="btn btn-secondary" id="btn-new-report">📑 Neuen Bericht anlegen…</button>
<button class="btn btn-secondary" id="btn-shipments">🚚 Lieferungen</button>
<button class="btn btn-secondary" id="btn-voice">${icon('mikrofon', 20)}Sprachnotiz aufnehmen</button>
<button class="btn btn-secondary" id="btn-notes">${icon('notizen', 20)}Notizen${notesCount ? ' (' + notesCount + ')' : ''}</button>
<button class="btn btn-secondary" id="btn-material">${icon('liste', 20)}Materialliste</button>
<button class="btn btn-secondary" id="btn-new-report">${icon('bericht', 20)}Neuen Bericht anlegen…</button>
<button class="btn btn-secondary" id="btn-shipments">${icon('lieferung', 20)}Lieferungen</button>
<div id="pending-photos" data-order-id="${escapeHtml(String(args.id))}" style="margin-top:16px;"></div>
<div class="detail-section">
<div class="photo-section-head">
<h3 style="margin:0;">Hochgeladene Fotos (${imagePhotos.length})</h3>
${(imagePhotos.length + videoFiles.length + otherDocs.length) ? '<button class="btn btn-small" id="btn-select-mode" title="Mehrfachauswahl">☑ Auswählen</button>' : ''}
${(imagePhotos.length + videoFiles.length + otherDocs.length) ? '<button class="btn btn-small" id="btn-select-mode" title="Mehrfachauswahl">' + icon('auswahl', 18) + 'Auswählen</button>' : ''}
</div>
<div class="photo-grid" id="photo-grid">
${imagePhotos.map(p => `<div class="thumb" data-relpath="${escapeHtml(p.relpath)}"><div class="thumb-placeholder">⏳</div><div class="thumb-check">✓</div></div>`).join('')}
@ -871,7 +871,7 @@ router.on('/orders/:id', async (args) => {
</div>
${videoFiles.length ? `
<div class="detail-section">
<h3>🎬 Videos (${videoFiles.length})</h3>
<h3>Videos (${videoFiles.length})</h3>
<div class="video-grid" id="video-grid">
${videoFiles.map(v => `<div class="thumb video-thumb" data-relpath="${escapeHtml(v.relpath)}" data-filename="${escapeHtml(v.filename)}">
<video class="video-poster" muted playsinline preload="metadata" data-vorschau src="${escapeHtml(api.photoUrl(v.relpath))}#t=0.1"></video>
@ -883,7 +883,7 @@ router.on('/orders/:id', async (args) => {
</div>` : ''}
${audioFiles.length ? `
<div class="detail-section">
<h3>🎙 Sprachnotizen (${audioFiles.length})</h3>
<h3>Sprachnotizen (${audioFiles.length})</h3>
<div class="audio-list">
${audioFiles.map(a => `<div class="audio-item" data-relpath="${escapeHtml(a.relpath)}" data-mime="${escapeHtml(a.mime || 'audio/webm')}">
<span class="audio-name">${escapeHtml(a.filename)}</span>
@ -979,8 +979,8 @@ router.on('/orders/:id', async (args) => {
bar.innerHTML = `
<button class="icon-btn" id="sb-cancel" title="Abbrechen">✕</button>
<div class="sb-info" id="sb-info">0 ausgewählt</div>
<button class="icon-btn" id="sb-all" title="Alle">◎</button>
<button class="icon-btn" id="sb-share" title="Teilen" disabled>📤</button>
<button class="icon-btn" id="sb-all" title="Alle auswählen" aria-label="Alle auswählen">${icon('alle')}</button>
<button class="icon-btn" id="sb-share" title="Teilen" aria-label="Teilen" disabled>${icon('teilen')}</button>
`;
document.body.appendChild(bar);
bar.querySelector('#sb-cancel').onclick = cancelViaBack;
@ -1186,9 +1186,9 @@ function openVideoPlayer(orderId, relpath, filename) {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="vp-close">✕</button>
<div class="fs-title">🎬 ${escapeHtml(filename || 'Video')}</div>
<button class="icon-btn" id="vp-share" title="Teilen">📤</button>
<button class="icon-btn" id="vp-delete" title="Löschen">🗑</button>
<div class="fs-title">${escapeHtml(filename || 'Video')}</div>
<button class="icon-btn" id="vp-share" title="Teilen" aria-label="Teilen">${icon('teilen')}</button>
<button class="icon-btn" id="vp-delete" title="Löschen" aria-label="Löschen">${icon('loeschen')}</button>
</div>
<div class="fs-body"><video controls playsinline autoplay preload="auto"></video></div>
`;
@ -1282,7 +1282,7 @@ async function renderPendingPhotos() {
const more = items.length - shown.length;
box.innerHTML = '<div class="detail-section pending-section">'
+ '<h3>⏳ Warten auf Upload (' + items.length + ')</h3>'
+ '<h3>Warten auf Upload (' + items.length + ')</h3>'
+ lines.map(l => '<p>' + escapeHtml(l) + '</p>').join('')
+ '<div class="photo-grid pending-grid">' + thumbs
+ (more > 0 ? '<div class="thumb pending-thumb pending-more">+' + more + '</div>' : '')
@ -1580,7 +1580,7 @@ function renderCustomerList(list) {
<div class="ref">${escapeHtml(c.name || '')}</div>
<div class="name">${escapeHtml((c.zip || '') + ' ' + (c.town || '')).trim() || '—'}</div>
<div class="meta">
<span>${c.phone ? '📞 ' + escapeHtml(c.phone) : ''}</span>
<span>${c.phone ? icon('telefon', 14) + ' ' + escapeHtml(c.phone) : ''}</span>
${c.bericht_count > 0 ? `<span class="badge">📑 ${c.bericht_count}</span>` : ''}
</div>
</div>
@ -1614,11 +1614,11 @@ router.on('/customers/:id', async (args) => {
<p><strong>${escapeHtml(data.customer.name)}</strong></p>
${data.customer.code ? `<p class="label">Kundennr: ${escapeHtml(data.customer.code)}</p>` : ''}
${addr ? `<p>${escapeHtml(addr).replace(/\n/g, '<br>')}</p>` : ''}
${data.customer.phone ? `<p><a href="tel:${escapeHtml(data.customer.phone)}">📞 ${escapeHtml(data.customer.phone)}</a></p>` : ''}
${data.customer.phone ? `<p><a class="tel-link" href="tel:${escapeHtml(data.customer.phone)}">${icon('telefon', 18)}${escapeHtml(data.customer.phone)}</a></p>` : ''}
${data.customer.email ? `<p><a href="mailto:${escapeHtml(data.customer.email)}">✉ ${escapeHtml(data.customer.email)}</a></p>` : ''}
${data.customer.siret || data.customer.vat ? `<p class="label">USt-ID: ${escapeHtml(data.customer.vat || '')}</p>` : ''}
<div class="customer-actions">
${data.customer.address || data.customer.town ? `<a class="btn btn-secondary" href="https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(data.customer.name + ' ' + (data.customer.address||'') + ' ' + (data.customer.zip||'') + ' ' + (data.customer.town||''))}" target="_blank">🗺 Route</a>` : ''}
${data.customer.address || data.customer.town ? `<a class="btn btn-secondary" href="https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(data.customer.name + ' ' + (data.customer.address||'') + ' ' + (data.customer.zip||'') + ' ' + (data.customer.town||''))}" target="_blank">${icon('karte', 20)}Route</a>` : ''}
</div>
</div>
@ -1755,9 +1755,9 @@ router.on('/reports/:id', async (args) => {
</div>` : '<div class="empty-state"><div class="icon">📭</div>Dieser Bericht hat noch keine Seiten. Fotos aufnehmen, um Seiten hinzuzufügen.</div>'}
<button class="btn btn-large" id="btn-finalize" ${hasPages ? '' : 'disabled'}>${finalizeLabel}</button>
${hasPages ? '<button class="btn btn-secondary" id="btn-view-pdf">👁 PDF-Vorschau</button>' : ''}
<button class="btn btn-secondary" id="btn-signature">✍️ Kunden-Unterschrift hinzufügen</button>
<button class="btn btn-secondary" id="btn-open-editor">✏️ Im Desktop-Editor öffnen</button>
${hasPages ? '<button class="btn btn-secondary" id="btn-view-pdf">' + icon('ansehen', 20) + 'PDF-Vorschau</button>' : ''}
<button class="btn btn-secondary" id="btn-signature">${icon('zeichnen', 20)}Kunden-Unterschrift hinzufügen</button>
<button class="btn btn-secondary" id="btn-open-editor">${icon('oeffnen', 20)}Im Desktop-Editor öffnen</button>
<button class="btn btn-danger" id="btn-delete-report">Bericht löschen</button>
`;
@ -1857,12 +1857,12 @@ router.on('/settings', async () => {
</div>
<div class="detail-section">
<h3>🔔 Benachrichtigungen</h3>
<h3>Benachrichtigungen</h3>
<p class="label">Per ntfy-Topic. Wenn gesetzt, zeigt die App Notifications bei neuen Nachrichten (z. B. neue Aufträge, Team-Pings).</p>
<input type="text" id="ntfy-server" placeholder="https://notify.data-it-solution.de" value="${escapeHtml(ntfyServer)}" style="width:100%;padding:10px;background:#2a2a30;color:#fff;border:1px solid #444;border-radius:6px;box-sizing:border-box;margin-bottom:6px;">
<input type="text" id="ntfy-topic" placeholder="z. B. baustelle-eddy" value="${escapeHtml(ntfyTopic)}" style="width:100%;padding:10px;background:#2a2a30;color:#fff;border:1px solid #444;border-radius:6px;box-sizing:border-box;">
<button class="btn btn-secondary" id="btn-ntfy-save" style="margin-top:6px;">💾 Speichern & Abonnieren</button>
<button class="btn btn-secondary" id="btn-ntfy-test">🔔 Test-Nachricht senden</button>
<button class="btn btn-secondary" id="btn-ntfy-save" style="margin-top:6px;">${icon('speichern', 20)}Speichern & Abonnieren</button>
<button class="btn btn-secondary" id="btn-ntfy-test">${icon('glocke', 20)}Test-Nachricht senden</button>
<p class="label" id="ntfy-status"></p>
</div>
@ -1873,11 +1873,11 @@ router.on('/settings', async () => {
<input type="checkbox" id="pin-toggle" ${pinEnabled ? 'checked' : ''}>
<span>PIN beim Öffnen abfragen</span>
</label>
${pinEnabled ? '<button class="btn btn-secondary" id="btn-pin-change">🔑 PIN ändern</button>' : ''}
${pinEnabled ? '<button class="btn btn-secondary" id="btn-pin-change">' + icon('schluessel', 20) + 'PIN ändern</button>' : ''}
</div>
<button class="btn btn-secondary" id="btn-sync">🔄 Offline-Queue synchronisieren</button>
<button class="btn btn-secondary" id="btn-logout">🚪 Abmelden</button>
<button class="btn btn-secondary" id="btn-sync">${icon('sync', 20)}Offline-Queue synchronisieren</button>
<button class="btn btn-secondary" id="btn-logout">${icon('abmelden', 20)}Abmelden</button>
<p class="label" style="text-align:center;margin-top:24px;">Baustelle PWA v1.0</p>
`;
document.getElementById('btn-sync').onclick = async () => {
@ -2112,7 +2112,7 @@ async function openMaterialModal(elementType, elementId) {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="mt-close">✕</button>
<div class="fs-title">📦 Materialliste</div>
<div class="fs-title">Materialliste</div>
<span></span>
</div>
<div class="fs-body" style="flex-direction:column;gap:12px;padding:16px;align-items:stretch;overflow-y:auto;">
@ -2203,7 +2203,7 @@ async function openNewReportModal(orderId) {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="nr-close">✕</button>
<div class="fs-title">📑 Neuer Bericht</div>
<div class="fs-title">Neuer Bericht</div>
<button class="icon-btn" id="nr-save" title="Anlegen">✓</button>
</div>
<div class="fs-body" style="flex-direction:column;gap:12px;padding:16px;align-items:stretch;">
@ -2291,7 +2291,7 @@ async function openNewOrderModal() {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="no-close" aria-label="Schliessen">✕</button>
<div class="fs-title">➕ Neuer Auftrag</div>
<div class="fs-title">Neuer Auftrag</div>
<button class="icon-btn" id="no-save" title="Anlegen & Fotos machen" disabled>✓</button>
</div>
<div class="fs-body" style="flex-direction:column;gap:10px;padding:16px;align-items:stretch;justify-content:flex-start;">
@ -2501,7 +2501,7 @@ async function openPageActionsModal(berichtId, pageId, relpath, note, title) {
<div class="fs-header">
<button class="icon-btn" id="pa-close">✕</button>
<div class="fs-title">Seite bearbeiten</div>
<button class="icon-btn" id="pa-delete" title="Seite löschen">🗑️</button>
<button class="icon-btn" id="pa-delete" title="Seite löschen" aria-label="Seite löschen">${icon('loeschen')}</button>
</div>
<div class="fs-body" style="flex-direction:column;gap:12px;padding:16px;">
${url ? `<img src="${url}" style="max-height:40vh;">` : '<div class="thumb-placeholder">⚠</div>'}
@ -2509,7 +2509,7 @@ async function openPageActionsModal(berichtId, pageId, relpath, note, title) {
<input type="text" id="pa-title" placeholder="z. B. 'Vor Beginn der Arbeiten'" value="${escapeHtml(title || '')}" style="width:100%;padding:10px;background:#2a2a30;color:#fff;border:1px solid #444;border-radius:6px;box-sizing:border-box;">
<label class="label" style="align-self:flex-start;">Notiz zur Seite (unten auf der Seite):</label>
<textarea id="pa-note" rows="3" style="width:100%;background:#2a2a30;color:#fff;border:1px solid #444;border-radius:6px;padding:10px;font-size:14px;box-sizing:border-box;">${escapeHtml(note || '')}</textarea>
<button class="btn" id="pa-save">💾 Speichern</button>
<button class="btn" id="pa-save">${icon('speichern', 20)}Speichern</button>
</div>
`;
document.body.appendChild(modal);
@ -2572,9 +2572,9 @@ function openPdfModal(blobUrl, filename, beiDownload) {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn pdfv-close" id="pdf-close" aria-label="Schließen">✕</button>
<div class="fs-title" id="pdfv-title">📑 ${escapeHtml(fname)}</div>
<button class="icon-btn" id="pdf-share" title="Teilen">📤</button>
<a class="icon-btn" id="pdf-download" href="${blobUrl}" download="${escapeHtml(fname)}" title="Download">⬇</a>
<div class="fs-title" id="pdfv-title">${escapeHtml(fname)}</div>
<button class="icon-btn" id="pdf-share" title="Teilen" aria-label="Teilen">${icon('teilen')}</button>
<a class="icon-btn" id="pdf-download" href="${blobUrl}" download="${escapeHtml(fname)}" title="Download" aria-label="Download">${icon('download')}</a>
</div>
<div class="pdfv-scroll" id="pdfv-scroll"><div class="pdfv-stack" id="pdfv-stack"></div></div>
<div class="pdfv-bar">
@ -2751,7 +2751,7 @@ function openSignatureModal(berichtId) {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="sig-close">✕</button>
<div class="fs-title">✍️ Kunden-Unterschrift</div>
<div class="fs-title">Kunden-Unterschrift</div>
<button class="icon-btn" id="sig-save" title="Speichern">✓</button>
</div>
<div class="signature-meta">
@ -2927,7 +2927,7 @@ router.on('/shipments/:id', async (args) => {
: '<p class="label" style="color:#f0ad4e;">Noch nicht unterschrieben</p>'}
</div>
<div id="shipment-pdf-wrap" class="pdf-inline-wrap"></div>
<button class="btn btn-large" id="btn-sign-shipment" ${isSigned ? 'disabled' : ''}>✍️ Lieferung unterschreiben lassen</button>
<button class="btn btn-large" id="btn-sign-shipment" ${isSigned ? 'disabled' : ''}>${icon('zeichnen', 20)}Lieferung unterschreiben lassen</button>
`;
// PDF inline rendern (PDF.js Canvas wie bei Reports)
@ -3056,7 +3056,7 @@ function openShipmentSignatureModal(shipmentId, info) {
<p class="ss-legal">Mit der Unterschrift bestätige ich den ordnungsgemäßen Erhalt der Lieferung. Server-Zeitstempel und GPS werden mitgespeichert.</p>
<div class="ss-error" id="ss-error" style="display:none;background:#5e1e1e;border:1px solid #c04040;color:#fff;padding:10px 12px;border-radius:6px;font-size:13px;white-space:pre-wrap;word-break:break-word;"></div>
<div class="ss-actions">
<button class="btn btn-secondary" id="ss-clear">🗑 Leeren</button>
<button class="btn btn-secondary" id="ss-clear">${icon('loeschen', 20)}Leeren</button>
<button class="btn btn-secondary" id="ss-cancel">Abbrechen</button>
<button class="btn btn-large" id="ss-save">✓ Bestätigen</button>
</div>
@ -3228,7 +3228,7 @@ function openHelpModal() {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="help-close">✕</button>
<div class="fs-title">❓ Hilfe / Anleitung</div>
<div class="fs-title">Hilfe / Anleitung</div>
<span></span>
</div>
<div class="help-body">
@ -3322,7 +3322,7 @@ function openHelpModal() {
<section>
<h3>8. Einstellungen</h3>
<p>Im Reiter <strong>⚙️</strong>:</p>
<p>Im Reiter <strong>Einstellungen</strong> (Zahnrad unten rechts):</p>
<ul>
<li>🔄 Offline-Queue manuell synchronisieren</li>
<li>🚪 Abmelden</li>
@ -3353,6 +3353,46 @@ function layoutBadge(layout) {
}
}
/* Weisse Symbole fuer Knoepfe (Material Icons, fill = currentColor → folgt der Schriftfarbe).
* Emojis erscheinen je nach Handy bunt oder als blaues Kaestchen — Eddy (19.10.2026) beim
* Kamera-Wechselknopf: „den hässlichen Wechselbutton lässt du da doch nicht", dann fuer alle
* Knoepfe: „so dass es gut aussieht". Knoepfe nehmen deshalb diese Symbole, keine Emojis. */
const ICON_PFADE = {
teilen: 'M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z',
loeschen: 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z',
zeichnen: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z',
download: 'M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z',
alle: 'M3 5h2V3c-1.1 0-2 .9-2 2zm0 8h2v-2H3v2zm4 8h2v-2H7v2zM3 9h2V7H3v2zm10-6h-2v2h2V3zm6 0v2h2c0-1.1-.9-2-2-2zM5 21v-2H3c0 1.1.9 2 2 2zm-2-4h2v-2H3v2zM9 3H7v2h2V3zm2 18h2v-2h-2v2zm8-8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2zm0-12h2V7h-2v2zm0 8h2v-2h-2v2zm-4 4h2v-2h-2v2zm0-16h2V3h-2v2zM7 17h10V7H7v10zm2-8h6v6H9V9z',
blitzAus: 'M3.27 3L2 4.27l5 5V13h3v9l3.58-6.14L17.73 20 19 18.73 3.27 3zM17 10h-4l4-8H7v2.18l8.46 8.46L17 10z',
blitzAuto: 'M3 2v12h3v9l7-12H9l4-9H3zm16 0h-2l-3.2 9h1.9l.7-2h3.2l.7 2h1.9L19 2zm-2.15 5.65L18 4l1.15 3.65h-2.3z',
blitzAn: 'M7 2v11h3v9l7-12h-4l4-8z',
licht: 'M6 2h12v3H6zm0 5v1l2 3v11h8V11l2-3V7H6zm6 8.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z',
kamera: 'M12 15.2a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4zM9 2L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2H9zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z',
galerie: 'M22 16V4c0-1.1-.9-2-2-2H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2zm-11-4l2.03 2.71L16 11l4 5H8l3-4zM2 6v14c0 1.1.9 2 2 2h14v-2H4V6H2z',
mikrofon: 'M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z',
notizen: 'M3 18h12v-2H3v2zM3 6v2h18V6H3zm0 7h18v-2H3v2z',
liste: 'M19 5v14H5V5h14m1.1-2H3.9c-.5 0-.9.4-.9.9v16.2c0 .4.4.9.9.9h16.2c.4 0 .9-.5.9-.9V3.9c0-.5-.5-.9-.9-.9zM11 7h6v2h-6V7zm0 4h6v2h-6v-2zm0 4h6v2h-6v-2zM7 7h2v2H7V7zm0 4h2v2H7v-2zm0 4h2v2H7v-2z',
bericht: 'M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 14h-3v3h-2v-3H8v-2h3v-3h2v3h3v2zm-3-7V3.5L18.5 9H13z',
lieferung: 'M20 8h-3V4H3c-1.1 0-2 .9-2 2v11h2c0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3h2v-5l-3-4zM6 18.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5-9l1.96 2.5H17V9.5h2.5zm-1.5 9c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z',
karte: 'M20.5 3l-.16.03L15 5.1 9 3 3.36 4.9c-.21.07-.36.25-.36.48V20.5c0 .28.22.5.5.5l.16-.03L9 18.9l6 2.1 5.64-1.9c.21-.07.36-.25.36-.48V3.5c0-.28-.22-.5-.5-.5zM15 19l-6-2.11V5l6 2.11V19z',
telefon: 'M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z',
auswahl: 'M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z',
ansehen: 'M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z',
oeffnen: 'M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z',
speichern: 'M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z',
glocke: 'M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z',
schluessel: 'M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z',
sync: 'M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z',
abmelden: 'M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z',
};
/* Farbe kommt aus app.css (.ikon-<name>): klein = farbiges Symbol, im grossen Knopf = weisses
* Symbol im farbigen runden Feld. Eddy (20.10.2026) zu rein weissen Symbolen: „viel zu schlicht". */
function icon(name, size) {
const g = size || 24;
return '<svg class="ikon ikon-' + name + '" viewBox="0 0 24 24" width="' + g + '" height="' + g
+ '" fill="currentColor" aria-hidden="true"><path d="' + ICON_PFADE[name] + '"/></svg>';
}
function escapeHtml(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
@ -3418,7 +3458,7 @@ function openFileViewer({ url, blob, mime }, filename, relpath) {
<div class="fs-header">
<button class="icon-btn" id="dv-close">✕</button>
<div class="fs-title">${escapeHtml(filename || '')}</div>
<button class="icon-btn" id="dv-share" title="Teilen">📤</button>
<button class="icon-btn" id="dv-share" title="Teilen" aria-label="Teilen">${icon('teilen')}</button>
</div>
<div class="dv-body">
<img src="${url}" style="max-width:100%;max-height:100%;object-fit:contain;">
@ -3468,9 +3508,9 @@ async function openPhotoModal(orderId, relpath) {
<button class="icon-btn" id="fs-close">✕</button>
<div class="fs-title" id="pv-title"></div>
<div class="pv-counter" id="pv-counter"></div>
<button class="icon-btn" id="fs-share" title="Teilen">📤</button>
<button class="icon-btn" id="fs-sketch" title="Zeichnen">✏️</button>
<button class="icon-btn" id="fs-delete" title="Löschen">🗑️</button>
<button class="icon-btn" id="fs-share" title="Teilen" aria-label="Teilen">${icon('teilen')}</button>
<button class="icon-btn" id="fs-sketch" title="Zeichnen" aria-label="Zeichnen">${icon('zeichnen')}</button>
<button class="icon-btn" id="fs-delete" title="Löschen" aria-label="Löschen">${icon('loeschen')}</button>
</div>
<div class="pv-body" id="pv-body">
<img id="pv-image" src="" draggable="false">
@ -3650,7 +3690,7 @@ async function openVoiceModal(orderId) {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="v-close">✕</button>
<div class="fs-title">🎙 Sprachnotiz</div>
<div class="fs-title">Sprachnotiz</div>
<span></span>
</div>
<div class="voice-body">
@ -3751,10 +3791,10 @@ async function openCameraModal(orderId) {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="cam-close">✕</button>
<div class="fs-title">📷 Kamera</div>
<div class="fs-title">Kamera</div>
<button class="icon-btn cam-blitz" id="cam-blitz" title="Blitz" hidden></button>
<button class="icon-btn cam-flip" id="cam-flip" title="Kamera wechseln" aria-label="Kamera wechseln" hidden>
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="M16 7h-1l-1-1h-4L9 7H8c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zm-4 7c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/><path d="M8.57.51l4.48 4.48V2.04c4.72.47 8.48 4.23 8.95 8.95h2C23.34 3.02 15.49-1.59 8.57.51zm2.38 21.45c-4.72-.47-8.48-4.23-8.95-8.95H0c.66 7.97 8.51 12.58 15.43 10.48l-4.48-4.48v2.95z"/></svg>
<svg class="ikon ikon-wechseln" viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="M16 7h-1l-1-1h-4L9 7H8c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zm-4 7c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/><path d="M8.57.51l4.48 4.48V2.04c4.72.47 8.48 4.23 8.95 8.95h2C23.34 3.02 15.49-1.59 8.57.51zm2.38 21.45c-4.72-.47-8.48-4.23-8.95-8.95H0c.66 7.97 8.51 12.58 15.43 10.48l-4.48-4.48v2.95z"/></svg>
</button>
</div>
<div class="cam-stage" id="cam-stage">
@ -3956,7 +3996,9 @@ async function openCameraModal(orderId) {
* (SCALER_CROP_REGION, gilt auch fuer takePhoto). Beim Zoomen verwirft Chrome den Tippbereich.
* Frontkamera, Webcam am Tablet (X12) usw. melden meist nichts davon → kein Knopf, kein Antippen.
*/
const BLITZ_TEXT = { off: '⚡ Aus', auto: '⚡ Auto', flash: '⚡ An', torch: '🔦 Licht' };
const BLITZ_TEXT = { off: 'Aus', auto: 'Auto', flash: 'An', torch: 'Licht' };
const BLITZ_ICON = { off: 'blitzAus', auto: 'blitzAuto', flash: 'blitzAn', torch: 'licht' };
const blitzZeigen = () => { blitzBtn.innerHTML = icon(BLITZ_ICON[blitz], 22) + '<span>' + BLITZ_TEXT[blitz] + '</span>'; };
const BLITZ_KEY = 'baustelle.kamera.blitz';
let imageCap = null;
let blitzModi = []; // Modi des aktuellen Kamera-Modus (Foto bzw. Video)
@ -4087,7 +4129,7 @@ async function openCameraModal(orderId) {
blitz = blitzModi[(blitzModi.indexOf(blitz) + 1) % blitzModi.length];
}
}
blitzBtn.textContent = BLITZ_TEXT[blitz];
blitzZeigen();
if (modus === 'video') {
blitzVideo = blitz;
} else {
@ -4104,7 +4146,7 @@ async function openCameraModal(orderId) {
blitz = modus === 'video' ? blitzVideo : blitzFoto;
if (!blitzModi.includes(blitz)) blitz = 'off';
blitzBtn.hidden = blitzModi.length < 2;
blitzBtn.textContent = BLITZ_TEXT[blitz];
blitzZeigen();
if ((vorher === 'torch') !== (blitz === 'torch')) lichtSchalten(blitz === 'torch');
}
@ -4437,14 +4479,14 @@ async function openUploadQueueModal() {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="q-close">✕</button>
<div class="fs-title">☁ Upload-Warteschlange</div>
<div class="fs-title">Upload-Warteschlange</div>
<span></span>
</div>
<div class="fs-body" style="flex-direction:column;align-items:stretch;justify-content:flex-start;gap:12px;color:#fff;">
<p style="margin:0;">${pending.length} warten auf Upload${failed.length ? ', <strong>' + failed.length + ' fehlgeschlagen</strong>' : ''}.</p>
<button class="btn" id="q-sync">🔄 Jetzt synchronisieren</button>
<button class="btn" id="q-sync">${icon('sync', 20)}Jetzt synchronisieren</button>
${failed.length ? '<button class="btn btn-secondary" id="q-retry">↻ Fehlgeschlagene erneut senden</button>' : ''}
${failed.length ? '<button class="btn btn-secondary" id="q-share">📤 Fehlgeschlagene sichern / teilen</button>' : ''}
${failed.length ? '<button class="btn btn-secondary" id="q-share">' + icon('teilen', 20) + 'Fehlgeschlagene sichern / teilen</button>' : ''}
<p style="margin:0;font-size:12px;color:#aaa;">Fotos und Videos bleiben gespeichert, bis der Upload bestätigt ist. Schließe die App nicht, solange hier noch Einträge stehen.</p>
</div>
`;
@ -4494,7 +4536,7 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="sk-close">✕</button>
<div class="fs-title">✏️ Skizze</div>
<div class="fs-title">Skizze</div>
<button class="icon-btn" id="sk-save" title="Speichern">✓</button>
</div>
<div class="sketch-toolbar">
@ -4864,7 +4906,7 @@ async function openNotesModal(orderId) {
modal.className = 'fullscreen-modal notes-modal';
modal.innerHTML = `
<div class="modal-header">
<h2>📝 Notizen</h2>
<h2>Notizen</h2>
<button class="close-btn" id="nl-close">&times;</button>
</div>
<div class="modal-body">
@ -4888,7 +4930,7 @@ async function openNotesModal(orderId) {
<div class="note-card" data-file="${escapeHtml(n.file)}">
<div class="note-card-head">
<strong>${escapeHtml(n.subject || '(ohne Betreff)')}</strong>
<button class="icon-btn note-del" title="Löschen">🗑</button>
<button class="icon-btn note-del" title="Löschen" aria-label="Löschen">${icon('loeschen', 20)}</button>
</div>
<div class="note-card-preview">${escapeHtml(n.preview || '')}</div>
<div class="note-card-meta">${escapeHtml(n.modified || n.created || formatShortDate(n.date))}</div>
@ -4944,7 +4986,7 @@ async function openNoteEditor(orderId, file, onSaved) {
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="nt-close">✕</button>
<div class="fs-title">📝 ${file ? 'Notiz bearbeiten' : 'Neue Notiz'}</div>
<div class="fs-title">${file ? 'Notiz bearbeiten' : 'Neue Notiz'}</div>
<button class="icon-btn" id="nt-save" title="Speichern">✓</button>
</div>
<div class="note-edit-body">

View file

@ -42,19 +42,19 @@ header('Expires: 0');
<header id="topbar">
<button id="back-btn" class="back-btn" style="display:none" aria-label="Zurück"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg><span>Zurück</span></button>
<h1 id="page-title">Baustelle</h1>
<button id="install-btn" class="icon-btn" title="Als App installieren" style="display:none">📲</button>
<button id="help-btn" class="icon-btn" title="Hilfe">❓</button>
<button id="install-btn" class="icon-btn" title="Als App installieren" aria-label="Als App installieren" style="display:none"><svg class="ikon ikon-installieren" viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14zm-1-6h-3V8h-2v5H8l4 4 4-4z"/></svg></button>
<button id="help-btn" class="icon-btn" title="Hilfe" aria-label="Hilfe"><svg class="ikon ikon-hilfe" viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg></button>
<span id="status-badge">🟢</span>
</header>
<main id="main"></main>
<nav id="bottom-nav" style="display:none">
<button data-route="today">☀️ Heute</button>
<button data-route="orders" class="active">📋 Aufträge</button>
<button data-route="customers">👥 Kunden</button>
<button data-route="reports">📑 Berichte</button>
<button data-route="settings">⚙️</button>
<button data-route="today"><svg class="ikon ikon-heute" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm9-9.95h-2V3.5h2V.55zm7.45 3.91l-1.41-1.41-1.79 1.79 1.41 1.41 1.79-1.79zm-3.21 13.7l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM20 10.5v2h3v-2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm-1 16.95h2V19.5h-2v2.95zm-7.45-3.91l1.41 1.41 1.79-1.8-1.41-1.41-1.79 1.8z"/></svg><span>Heute</span></button>
<button data-route="orders" class="active"><svg class="ikon ikon-auftraege" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg><span>Aufträge</span></button>
<button data-route="customers"><svg class="ikon ikon-kunden" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg><span>Kunden</span></button>
<button data-route="reports"><svg class="ikon ikon-berichte" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg><span>Berichte</span></button>
<button data-route="settings" aria-label="Einstellungen"><svg class="ikon ikon-einstellungen" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg><span>Einstellungen</span></button>
</nav>
</div>