Farbige Symbole statt Emojis auf allen Knöpfen [deploy]
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 13s
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:
parent
73a481fa8f
commit
5a54a64b7e
5 changed files with 202 additions and 73 deletions
|
|
@ -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)
|
||||
|
|
|
|||
20
ROADMAP.md
20
ROADMAP.md
|
|
@ -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
75
app.css
|
|
@ -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
164
app.js
|
|
@ -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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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">×</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">
|
||||
|
|
|
|||
14
index.php
14
index.php
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue