diff --git a/app.css b/app.css
index a8abce9..c19d428 100644
--- a/app.css
+++ b/app.css
@@ -307,6 +307,78 @@ body {
opacity: 0.5;
}
+/* Foto-Section Kopfzeile mit Auswahl-Toggle */
+.photo-section-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin: 0 0 6px;
+}
+.btn-small {
+ padding: 6px 10px;
+ font-size: 13px;
+ border-radius: 6px;
+ background: #2a2a30;
+ color: #eee;
+ border: 1px solid #444;
+ cursor: pointer;
+}
+.btn-small:hover { background: #33333a; }
+
+/* Auswahl-Mode im Photo-Grid */
+.photo-grid .thumb .thumb-check {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ background: rgba(0,0,0,0.55);
+ color: #fff;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ font-weight: bold;
+ border: 2px solid #fff;
+ pointer-events: none;
+}
+.photo-grid.selecting .thumb-check { display: flex; opacity: 0.35; }
+.photo-grid.selecting .thumb.selected { outline: 3px solid #5ab0ff; outline-offset: -3px; }
+.photo-grid.selecting .thumb.selected .thumb-check {
+ opacity: 1;
+ background: #5ab0ff;
+ border-color: #fff;
+}
+
+/* Sticky Aktions-Bar am unteren Bildschirmrand */
+.select-bar {
+ position: fixed;
+ left: 0; right: 0;
+ bottom: 0;
+ padding: 10px 14px;
+ padding-bottom: calc(10px + env(safe-area-inset-bottom));
+ background: rgba(20,20,25,0.95);
+ border-top: 1px solid #333;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ z-index: 90;
+ backdrop-filter: blur(6px);
+}
+.select-bar .sb-info {
+ flex: 1;
+ color: #eee;
+ font-size: 14px;
+}
+.select-bar .icon-btn {
+ color: #fff;
+ font-size: 20px;
+ padding: 6px 10px;
+}
+.select-bar .icon-btn:disabled { opacity: 0.35; cursor: default; }
+
/* ----- Toast ----- */
#toast-container {
position: fixed;
@@ -380,11 +452,12 @@ body {
.fs-header {
display: flex;
align-items: center;
- gap: 12px;
+ gap: 8px;
padding: 10px 12px;
background: rgba(0,0,0,0.7);
color: #fff;
border-bottom: 1px solid #222;
+ flex-wrap: wrap;
}
.fs-header .fs-title {
flex: 1;
@@ -609,15 +682,17 @@ body {
.pdf-viewer-modal .fs-header {
display: flex;
align-items: center;
- gap: 8px;
+ gap: 6px;
flex-wrap: wrap;
}
.pdf-viewer-modal .fs-title { order: 4; }
.pdf-viewer-modal .icon-btn:nth-of-type(1) { order: 1; }
.pdf-viewer-modal .pdf-page-info { order: 2; font-size: 12px; color: #aaa; }
.pdf-viewer-modal .icon-btn:nth-of-type(2) { order: 3; }
-.pdf-viewer-modal .icon-btn:nth-of-type(3) { order: 5; }
-.pdf-viewer-modal .fs-header a { order: 6; }
+.pdf-viewer-modal .icon-btn:nth-of-type(3) { order: 5; } /* close */
+.pdf-viewer-modal .icon-btn:nth-of-type(4) { order: 6; } /* share */
+.pdf-viewer-modal .icon-btn:nth-of-type(5) { order: 7; } /* download */
+.pdf-viewer-modal .fs-header a { order: 7; }
.pdf-body {
flex: 1;
display: flex;
diff --git a/app.js b/app.js
index 9f717e7..8f71bdb 100644
--- a/app.js
+++ b/app.js
@@ -29,7 +29,117 @@ function setNav(visible, active) {
function setBack(visible, hash) {
const btn = document.getElementById('back-btn');
btn.style.display = visible ? '' : 'none';
- btn.onclick = () => { if (hash) router.go(hash); else history.back(); };
+ // Immer history.back() — offene Modals werden vom popstate-Handler geschlossen,
+ // Hash-Routen per router.go() liegen als History-Einträge vor.
+ btn.onclick = () => history.back();
+}
+
+/* ============================================================
+ * Modal-Stack: Android-Back schließt oberstes Modal statt die App
+ * Jedes Modal pusht beim Öffnen einen eigenen History-Eintrag.
+ * popstate (Android-Back) pop-t den Stack und entfernt das Modal.
+ * Programmatisches Schließen (closeModal) räumt synchron auf und
+ * setzt ein Skip-Flag, damit der eigene history.back() kein
+ * doppeltes Cleanup auslöst.
+ * ============================================================ */
+const modalStack = [];
+let _skipNextPopstate = false;
+let _lastBackAt = 0;
+
+function pushModal(el, cleanup) {
+ modalStack.push({ el, cleanup: cleanup || null });
+ try {
+ history.pushState({ _modal: true, _ts: Date.now() }, '', location.hash);
+ } catch (e) { /* some browsers may block in strict sandboxes */ }
+}
+
+function closeModal(el) {
+ const idx = modalStack.findIndex(m => m.el === el);
+ if (idx === -1) {
+ try { el.remove(); } catch {}
+ return;
+ }
+ const entry = modalStack[idx];
+ modalStack.splice(idx, 1);
+ try { entry.el.remove(); } catch {}
+ try { entry.cleanup && entry.cleanup(); } catch {}
+ // Eigenen History-Eintrag wieder entfernen, ohne popstate-Recursion
+ if (history.state && history.state._modal) {
+ _skipNextPopstate = true;
+ history.back();
+ }
+}
+
+function isTopLevelHash(h) {
+ const hh = (h || '').replace(/^#/, '').replace(/\/$/, '') || '/';
+ return ['/', '/orders', '/today', '/customers', '/reports', '/settings'].includes(hh);
+}
+
+window.addEventListener('popstate', () => {
+ if (_skipNextPopstate) { _skipNextPopstate = false; return; }
+ if (modalStack.length > 0) {
+ const top = modalStack.pop();
+ try { top.el.remove(); } catch {}
+ try { top.cleanup && top.cleanup(); } catch {}
+ return;
+ }
+ if (isTopLevelHash(location.hash)) {
+ const now = Date.now();
+ if (now - _lastBackAt < 2000) return;
+ _lastBackAt = now;
+ try { history.pushState({}, '', location.hash); } catch {}
+ showToast('Nochmal drücken zum Beenden');
+ }
+});
+
+/* ============================================================
+ * Share-Helper: teilt eine oder mehrere Dateien über Web Share API.
+ * shareFile(blob, filename, mime, title) — einzelne Datei
+ * shareFiles([{blob,filename,mime}, …], title) — mehrere Dateien
+ * Fällt auf Toast zurück, wenn das Gerät kein Datei-Sharing unterstützt.
+ * ============================================================ */
+function _buildShareFile(blob, filename, mime) {
+ const fname = filename || 'datei';
+ // Name-Endung am MIME ausrichten, sonst lehnen manche Android-Apps ab
+ let safeName = fname;
+ const m = (mime || blob.type || '').toLowerCase();
+ if (m === 'application/pdf' && !/\.pdf$/i.test(safeName)) safeName += '.pdf';
+ return new File([blob], safeName, { type: mime || blob.type || 'application/octet-stream' });
+}
+
+async function shareFiles(items, titleHint) {
+ const files = items.map(it => _buildShareFile(it.blob, it.filename, it.mime));
+ try {
+ if (!navigator.canShare || !navigator.share) {
+ showToast('Teilen wird vom Browser nicht unterstützt', 'error');
+ return false;
+ }
+ if (!navigator.canShare({ files })) {
+ // Versuche einzeln — manche Geräte lehnen Batches bestimmter MIMEs ab
+ if (files.length > 1) {
+ showToast('Batch-Teilen wird nicht unterstützt — sende einzeln', 'error');
+ return false;
+ }
+ showToast('Dieser Dateityp kann nicht geteilt werden', 'error');
+ return false;
+ }
+ await navigator.share({
+ files,
+ title: titleHint || (files.length === 1 ? files[0].name : files.length + ' Dateien'),
+ text: titleHint || '',
+ });
+ return true;
+ } catch (e) {
+ if (e && e.name !== 'AbortError') {
+ console.warn('[Share]', e);
+ showToast('Teilen fehlgeschlagen: ' + e.message, 'error');
+ }
+ return false;
+ }
+}
+
+async function shareFile(blob, filename, mime, titleHint) {
+ return shareFiles([{ blob, filename, mime }], titleHint);
}
/* ----- Auth-Check ----- */
@@ -135,7 +245,7 @@ function promptPin(title) {
current += d;
render();
if (current.length === 4) {
- setTimeout(() => { modal.remove(); resolve(current); }, 150);
+ setTimeout(() => { closeModal(modal); resolve(current); }, 150);
}
}
function del() {
@@ -156,9 +266,12 @@ function promptPin(title) {
const cancel = document.createElement('button');
cancel.className = 'pin-cancel';
cancel.textContent = 'Abbrechen';
- cancel.onclick = () => { modal.remove(); resolve(null); };
+ cancel.onclick = () => { closeModal(modal); resolve(null); };
modal.querySelector('.pin-body').appendChild(cancel);
document.body.appendChild(modal);
+ // Android-Back schließt Modal → Promise mit null auflösen
+ // (doppeltes resolve ist per JS-Spec no-op, falls PIN bereits eingegeben war)
+ pushModal(modal, () => resolve(null));
});
}
async function promptNewPin() {
@@ -186,6 +299,15 @@ window.appBoot = async function appBoot() {
console.warn('[boot] Token-Preload fehlgeschlagen', e);
}
+ // Bootstrap-Puffer: legt einen zusätzlichen History-Eintrag an, damit beim
+ // ersten Android-Back der popstate-Handler greifen kann (Toast „Nochmal
+ // drücken zum Beenden"), bevor die PWA wirklich verlassen wird.
+ try {
+ if (!history.state || !history.state._bootstrap) {
+ history.pushState({ _bootstrap: true }, '', location.hash || '#/orders');
+ }
+ } catch {}
+
// FAB: Click öffnet das Schnell-Auftrag-Modal
const fab = document.getElementById('fab-new-order');
if (fab && !fab.dataset.bound) {
@@ -411,9 +533,12 @@ router.on('/orders/:id', async (args) => {
-
Hochgeladene Fotos (${imagePhotos.length})
+
+
Hochgeladene Fotos (${imagePhotos.length})
+ ${imagePhotos.length ? '' : ''}
+
- ${imagePhotos.map(p => `
`).join('')}
+ ${imagePhotos.map(p => `
`).join('')}
${audioFiles.length ? `
@@ -444,11 +569,79 @@ router.on('/orders/:id', async (args) => {
` : ''}
`;
- // Foto-Thumbnails: Tap = Vollbild-Modal
- document.querySelectorAll('#photo-grid .thumb').forEach(t => {
- t.addEventListener('click', () => openPhotoModal(args.id, t.dataset.relpath));
+ // Foto-Thumbnails: Tap = Vollbild-Modal (oder Auswahl im Select-Modus)
+ const photoGrid = document.getElementById('photo-grid');
+ photoGrid.querySelectorAll('.thumb').forEach(t => {
+ t.addEventListener('click', () => {
+ if (photoGrid.classList.contains('selecting')) {
+ t.classList.toggle('selected');
+ updateSelectBar();
+ } else {
+ openPhotoModal(args.id, t.dataset.relpath);
+ }
+ });
});
+ // Mehrfachauswahl-Modus
+ const selectBtn = document.getElementById('btn-select-mode');
+ if (selectBtn) selectBtn.onclick = () => startSelectMode();
+
+ function startSelectMode() {
+ photoGrid.classList.add('selecting');
+ renderSelectBar();
+ updateSelectBar();
+ }
+ function endSelectMode() {
+ photoGrid.classList.remove('selecting');
+ photoGrid.querySelectorAll('.thumb.selected').forEach(x => x.classList.remove('selected'));
+ const bar = document.getElementById('select-bar');
+ if (bar) bar.remove();
+ }
+ function renderSelectBar() {
+ if (document.getElementById('select-bar')) return;
+ const bar = document.createElement('div');
+ bar.id = 'select-bar';
+ bar.className = 'select-bar';
+ bar.innerHTML = `
+
+ 0 ausgewählt
+
+
+ `;
+ document.body.appendChild(bar);
+ bar.querySelector('#sb-cancel').onclick = endSelectMode;
+ bar.querySelector('#sb-all').onclick = () => {
+ const thumbs = photoGrid.querySelectorAll('.thumb[data-relpath]');
+ const anyUn = Array.from(thumbs).some(t => !t.classList.contains('selected'));
+ thumbs.forEach(t => t.classList.toggle('selected', anyUn));
+ updateSelectBar();
+ };
+ bar.querySelector('#sb-share').onclick = () => shareSelected();
+ }
+ function updateSelectBar() {
+ const n = photoGrid.querySelectorAll('.thumb.selected').length;
+ const info = document.getElementById('sb-info');
+ const share = document.getElementById('sb-share');
+ if (info) info.textContent = n + ' ausgewählt';
+ if (share) share.disabled = n === 0;
+ }
+ async function shareSelected() {
+ const sel = Array.from(photoGrid.querySelectorAll('.thumb.selected'));
+ if (!sel.length) return;
+ showToast('Lade ' + sel.length + ' Foto' + (sel.length === 1 ? '' : 's') + '…');
+ const items = [];
+ for (const t of sel) {
+ const rp = t.dataset.relpath;
+ try {
+ const f = await api.getFileBlobUrl(rp);
+ if (f) items.push({ blob: f.blob, filename: rp.split('/').pop(), mime: f.mime });
+ } catch (e) { console.warn('Share-Load', rp, e); }
+ }
+ if (!items.length) { showToast('Keine Dateien geladen', 'error'); return; }
+ const ok = await shareFiles(items, 'Fotos vom Auftrag');
+ if (ok) endSelectMode();
+ }
+
// Weitere Dokumente: Tap = Öffnen (PDF inline, sonst Download)
document.querySelectorAll('.doc-list .doc-item').forEach(el => {
el.addEventListener('click', async () => {
@@ -544,14 +737,9 @@ router.on('/orders/:id', async (args) => {
for (const f of files) {
await uploadPhoto(args.id, f);
}
- // Reload Photo-Liste
- try {
- const np = await api.listOrderPhotos(args.id);
- const imgs = np.photos.filter(p => (p.mime || '').startsWith('image/'));
- document.getElementById('photo-grid').innerHTML =
- imgs.map(p => ``).join('');
- loadThumbs();
- } catch (e) {}
+ // Nach Upload einfach die Route neu rendern — so werden Select-Mode,
+ // Click-Handler und Thumbnails sauber neu aufgebaut.
+ router.navigate();
}
camInput.addEventListener('change', () => handleFiles(camInput.files));
galInput.addEventListener('change', () => handleFiles(galInput.files));
@@ -836,7 +1024,8 @@ router.on('/reports/:id', async (args) => {
showToast('PDF wird geladen…');
const url = await api.getPdfBlobUrl(args.id);
if (!url) { showToast('PDF konnte nicht geladen werden', 'error'); return; }
- openPdfModal(url);
+ const fname = (data.report && data.report.ref ? data.report.ref : ('bericht-' + args.id)) + '.pdf';
+ openPdfModal(url, fname);
};
// Unterschrift
@@ -881,12 +1070,13 @@ router.on('/reports/:id', async (args) => {
`;
document.body.appendChild(modal);
- modal.querySelector('#dr-cancel').onclick = () => modal.remove();
- modal.querySelector('#dr-no').onclick = () => modal.remove();
+ pushModal(modal);
+ modal.querySelector('#dr-cancel').onclick = () => closeModal(modal);
+ modal.querySelector('#dr-no').onclick = () => closeModal(modal);
modal.querySelector('#dr-yes').onclick = async () => {
try {
await api.deleteReport(args.id);
- modal.remove();
+ closeModal(modal);
showToast('Bericht gelöscht');
router.go('#/reports');
} catch (e) {
@@ -1141,8 +1331,9 @@ async function openMaterialModal(elementType, elementId) {
`;
document.body.appendChild(modal);
+ pushModal(modal);
- modal.querySelector('#mt-close').onclick = () => modal.remove();
+ modal.querySelector('#mt-close').onclick = () => closeModal(modal);
async function reload() {
try {
@@ -1244,6 +1435,7 @@ async function openNewReportModal(orderId) {
`;
document.body.appendChild(modal);
+ pushModal(modal);
// ODT-Default vorauswählen
try {
@@ -1254,7 +1446,7 @@ async function openNewReportModal(orderId) {
}
} catch (e) {}
- modal.querySelector('#nr-close').onclick = () => modal.remove();
+ modal.querySelector('#nr-close').onclick = () => closeModal(modal);
modal.querySelector('#nr-save').onclick = async () => {
const titel = modal.querySelector('#nr-titel').value.trim();
const tplSel = modal.querySelector('#nr-template');
@@ -1275,7 +1467,7 @@ async function openNewReportModal(orderId) {
template_odt: odt,
});
showToast('✓ Bericht angelegt');
- modal.remove();
+ closeModal(modal);
router.go('#/reports/' + res.bericht_id);
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
@@ -1323,6 +1515,7 @@ async function openNewOrderModal() {
`;
document.body.appendChild(modal);
+ pushModal(modal);
const closeBtn = modal.querySelector('#no-close');
const saveBtn = modal.querySelector('#no-save');
@@ -1339,7 +1532,7 @@ async function openNewOrderModal() {
let selectedCustomer = null;
- closeBtn.onclick = () => modal.remove();
+ closeBtn.onclick = () => closeModal(modal);
/* ---- Letzte Kunden als Quick-Pick ---- */
async function renderRecent() {
@@ -1451,7 +1644,7 @@ async function openNewOrderModal() {
} catch (e) {}
showToast('✓ Auftrag ' + (res.order.ref || '') + ' angelegt');
- modal.remove();
+ closeModal(modal);
router.go('#/orders/' + res.order.id);
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
@@ -1483,8 +1676,9 @@ async function openPageActionsModal(berichtId, pageId, relpath, note, title) {
`;
document.body.appendChild(modal);
+ pushModal(modal);
- modal.querySelector('#pa-close').onclick = () => modal.remove();
+ modal.querySelector('#pa-close').onclick = () => closeModal(modal);
modal.querySelector('#pa-save').onclick = async () => {
try {
const note = modal.querySelector('#pa-note').value;
@@ -1494,14 +1688,14 @@ async function openPageActionsModal(berichtId, pageId, relpath, note, title) {
body: JSON.stringify({ note, title }),
});
showToast('✓ Gespeichert');
- modal.remove();
+ closeModal(modal);
router.navigate();
} catch (e) {
// Fallback falls api.request nicht exposed ist
try {
await api.updatePageNote(pageId, modal.querySelector('#pa-note').value);
showToast('✓ Notiz gespeichert');
- modal.remove();
+ closeModal(modal);
router.navigate();
} catch (er) { showToast('Fehler: ' + er.message, 'error'); }
}
@@ -1511,7 +1705,7 @@ async function openPageActionsModal(berichtId, pageId, relpath, note, title) {
try {
await api.deletePage(pageId);
showToast('✓ Seite entfernt');
- modal.remove();
+ closeModal(modal);
router.navigate();
} catch (e) { showToast('Löschen fehlgeschlagen: ' + e.message, 'error'); }
};
@@ -1520,23 +1714,30 @@ async function openPageActionsModal(berichtId, pageId, relpath, note, title) {
/* ============================================================
* PDF-VORSCHAU MODAL
* ============================================================ */
-function openPdfModal(blobUrl) {
+function openPdfModal(blobUrl, filename) {
+ const fname = filename || 'bericht.pdf';
const modal = document.createElement('div');
modal.className = 'fullscreen-modal';
modal.innerHTML = `
`;
document.body.appendChild(modal);
- modal.querySelector('#pdf-close').onclick = () => {
- URL.revokeObjectURL(blobUrl);
- modal.remove();
+ pushModal(modal, () => { try { URL.revokeObjectURL(blobUrl); } catch {} });
+ modal.querySelector('#pdf-close').onclick = () => closeModal(modal);
+ modal.querySelector('#pdf-share').onclick = async () => {
+ try {
+ const r = await fetch(blobUrl);
+ const blob = await r.blob();
+ await shareFile(blob, fname, 'application/pdf', fname);
+ } catch (e) { showToast('Teilen fehlgeschlagen: ' + e.message, 'error'); }
};
}
@@ -1569,6 +1770,7 @@ function openSignatureModal(berichtId) {
`;
document.body.appendChild(modal);
+ pushModal(modal, () => { try { window.removeEventListener('resize', fitCanvas); } catch {} });
const canvas = modal.querySelector('#sig-canvas');
const ctx = canvas.getContext('2d');
@@ -1610,10 +1812,7 @@ function openSignatureModal(berichtId) {
canvas.addEventListener('touchend', end);
modal.querySelector('#sig-clear').onclick = fitCanvas;
- modal.querySelector('#sig-close').onclick = () => {
- window.removeEventListener('resize', fitCanvas);
- modal.remove();
- };
+ modal.querySelector('#sig-close').onclick = () => closeModal(modal);
modal.querySelector('#sig-save').onclick = async () => {
const signer = modal.querySelector('#sig-name').value.trim();
if (!signer) {
@@ -1641,7 +1840,7 @@ function openSignatureModal(berichtId) {
if (gps) { opts.gps_lat = gps.lat; opts.gps_lon = gps.lon; }
await api.uploadSignature(berichtId, blob, opts);
showToast('✓ Unterschrift hinzugefügt');
- modal.remove();
+ closeModal(modal);
router.navigate();
} catch (e) { showToast('Fehler: ' + e.message, 'error'); }
}, 'image/png');
@@ -1761,7 +1960,8 @@ function openHelpModal() {
`;
document.body.appendChild(modal);
- modal.querySelector('#help-close').onclick = () => modal.remove();
+ pushModal(modal);
+ modal.querySelector('#help-close').onclick = () => closeModal(modal);
}
function escapeHtml(s) {
@@ -1829,17 +2029,18 @@ function openFileViewer({ url, blob, mime }, filename, relpath) {
`;
document.body.appendChild(modal);
- const cleanup = () => {
- URL.revokeObjectURL(url);
- modal.remove();
+ pushModal(modal, () => { try { URL.revokeObjectURL(url); } catch {} });
+ modal.querySelector('#dv-close').onclick = () => closeModal(modal);
+ modal.querySelector('#dv-share').onclick = () => {
+ shareFile(blob, filename || 'datei', mime || blob.type, filename);
};
- modal.querySelector('#dv-close').onclick = cleanup;
}
async function openPdfViewer(blob, filename, relpath) {
@@ -1859,6 +2060,7 @@ async function openPdfViewer(blob, filename, relpath) {
${escapeHtml(filename || '')}
+
@@ -1866,6 +2068,7 @@ async function openPdfViewer(blob, filename, relpath) {
`;
document.body.appendChild(modal);
+ pushModal(modal);
const canvas = modal.querySelector('#pdf-canvas');
const ctx = canvas.getContext('2d');
@@ -1908,6 +2111,9 @@ async function openPdfViewer(blob, filename, relpath) {
const params = new URLSearchParams({ relpath, jwt: t, download: 1 });
window.location.href = window.location.origin + '/custom/bericht/api/photo.php?' + params.toString();
};
+ modal.querySelector('#pdf-share').onclick = () => {
+ shareFile(blob, filename || 'dokument.pdf', 'application/pdf', filename);
+ };
modal.querySelector('#pdf-prev').disabled = false;
modal.querySelector('#pdf-next').disabled = pdf.numPages === 1;
} catch (err) {
@@ -1917,7 +2123,7 @@ async function openPdfViewer(blob, filename, relpath) {
}
modal.querySelector('#pdf-close').onclick = () => {
- modal.remove();
+ closeModal(modal);
};
}
@@ -1943,6 +2149,7 @@ async function openPhotoModal(orderId, relpath) {
+
@@ -1958,6 +2165,7 @@ async function openPhotoModal(orderId, relpath) {
`;
document.body.appendChild(modal);
+ pushModal(modal, () => { try { document.removeEventListener('keydown', keyHandler); } catch {} });
const img = modal.querySelector('#pv-image');
const body = modal.querySelector('#pv-body');
@@ -2002,7 +2210,7 @@ async function openPhotoModal(orderId, relpath) {
resetZoom();
}
- function close() { modal.remove(); document.removeEventListener('keydown', keyHandler); }
+ function close() { closeModal(modal); }
function goPrev() { if (currentIndex > 0) loadImage(currentIndex - 1); }
function goNext() { if (currentIndex < allRelpaths.length - 1) loadImage(currentIndex + 1); }
@@ -2027,6 +2235,15 @@ async function openPhotoModal(orderId, relpath) {
close();
openSketchEditor(orderId, currentUrl, allRelpaths[currentIndex]);
};
+ modal.querySelector('#fs-share').onclick = async () => {
+ const rp = allRelpaths[currentIndex];
+ showToast('Lade Foto…');
+ try {
+ const f = await api.getFileBlobUrl(rp);
+ if (!f) { showToast('Foto konnte nicht geladen werden', 'error'); return; }
+ await shareFile(f.blob, rp.split('/').pop(), f.mime, 'Foto vom Auftrag');
+ } catch (e) { showToast('Fehler: ' + e.message, 'error'); }
+ };
// Doppelklick = Zoom Toggle
img.addEventListener('dblclick', (e) => {
@@ -2129,6 +2346,11 @@ async function openVoiceModal(orderId) {
let startTime = 0;
let audioBlob = null;
+ pushModal(modal, () => {
+ try { if (mediaRecorder && mediaRecorder.state === 'recording') mediaRecorder.stop(); } catch {}
+ try { if (timer) clearInterval(timer); } catch {}
+ });
+
const startBtn = modal.querySelector('#v-start');
const stopBtn = modal.querySelector('#v-stop');
const sendBtn = modal.querySelector('#v-send');
@@ -2136,11 +2358,7 @@ async function openVoiceModal(orderId) {
const timeEl = modal.querySelector('#v-time');
const preview = modal.querySelector('#v-preview');
- modal.querySelector('#v-close').onclick = () => {
- if (mediaRecorder && mediaRecorder.state === 'recording') mediaRecorder.stop();
- if (timer) clearInterval(timer);
- modal.remove();
- };
+ modal.querySelector('#v-close').onclick = () => closeModal(modal);
startBtn.onclick = async () => {
try {
@@ -2182,7 +2400,7 @@ async function openVoiceModal(orderId) {
try {
await api.uploadVoiceNote(orderId, audioBlob, 'voice_' + Date.now() + '.webm');
showToast('✓ Sprachnotiz hochgeladen');
- modal.remove();
+ closeModal(modal);
} catch (e) {
showToast('Upload fehlgeschlagen: ' + e.message, 'error');
sendBtn.disabled = false;
@@ -2227,6 +2445,7 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
`;
document.body.appendChild(modal);
+ pushModal(modal, () => { try { window.removeEventListener('resize', fitCanvasToScreen); } catch {} });
const canvas = modal.querySelector('#sk-canvas');
const ctx = canvas.getContext('2d');
@@ -2528,10 +2747,7 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
canvas.addEventListener('touchmove', moveDraw, { passive: false });
canvas.addEventListener('touchend', endDraw);
- modal.querySelector('#sk-close').onclick = () => {
- window.removeEventListener('resize', fitCanvasToScreen);
- modal.remove();
- };
+ modal.querySelector('#sk-close').onclick = () => closeModal(modal);
modal.querySelector('#sk-save').onclick = async () => {
showToast('Speichere Skizze…');
@@ -2539,7 +2755,7 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
try {
await api.uploadAnnotatedPhoto(orderId, blob, 'sketch_' + Date.now() + '.jpg');
showToast('✓ Skizze gespeichert');
- modal.remove();
+ closeModal(modal);
router.navigate();
} catch (e) {
showToast('Upload fehlgeschlagen: ' + e.message, 'error');