PWA: Android-Zurück-Button-Fix + Datei-Teilen via Share-Sheet [deploy]
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 5s
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 5s
- Modal-Stack mit History-API: jedes Modal pusht einen History-Eintrag, Android-Zurück schließt das Modal statt die App zu beenden - Top-Level-Routen: "Nochmal drücken zum Beenden"-Toast (2s Fenster) - shareFile()/shareFiles(): native Android-Share-Sheet für Fotos, PDFs, Dokumente - Multi-Select in Fotogalerie: Mehrere Fotos gleichzeitig auswählen und teilen - PDF-Teilen: auto-.pdf-Extension damit Android-Apps die Datei akzeptieren - openPdfModal: Share-Button neben Download-Button Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8be49c279f
commit
427fdeb0c0
2 changed files with 352 additions and 61 deletions
83
app.css
83
app.css
|
|
@ -307,6 +307,78 @@ body {
|
||||||
opacity: 0.5;
|
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 ----- */
|
||||||
#toast-container {
|
#toast-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -380,11 +452,12 @@ body {
|
||||||
.fs-header {
|
.fs-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
background: rgba(0,0,0,0.7);
|
background: rgba(0,0,0,0.7);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-bottom: 1px solid #222;
|
border-bottom: 1px solid #222;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.fs-header .fs-title {
|
.fs-header .fs-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -609,15 +682,17 @@ body {
|
||||||
.pdf-viewer-modal .fs-header {
|
.pdf-viewer-modal .fs-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.pdf-viewer-modal .fs-title { order: 4; }
|
.pdf-viewer-modal .fs-title { order: 4; }
|
||||||
.pdf-viewer-modal .icon-btn:nth-of-type(1) { order: 1; }
|
.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 .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(2) { order: 3; }
|
||||||
.pdf-viewer-modal .icon-btn:nth-of-type(3) { order: 5; }
|
.pdf-viewer-modal .icon-btn:nth-of-type(3) { order: 5; } /* close */
|
||||||
.pdf-viewer-modal .fs-header a { order: 6; }
|
.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 {
|
.pdf-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
330
app.js
330
app.js
|
|
@ -29,7 +29,117 @@ function setNav(visible, active) {
|
||||||
function setBack(visible, hash) {
|
function setBack(visible, hash) {
|
||||||
const btn = document.getElementById('back-btn');
|
const btn = document.getElementById('back-btn');
|
||||||
btn.style.display = visible ? '' : 'none';
|
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 ----- */
|
/* ----- Auth-Check ----- */
|
||||||
|
|
@ -135,7 +245,7 @@ function promptPin(title) {
|
||||||
current += d;
|
current += d;
|
||||||
render();
|
render();
|
||||||
if (current.length === 4) {
|
if (current.length === 4) {
|
||||||
setTimeout(() => { modal.remove(); resolve(current); }, 150);
|
setTimeout(() => { closeModal(modal); resolve(current); }, 150);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function del() {
|
function del() {
|
||||||
|
|
@ -156,9 +266,12 @@ function promptPin(title) {
|
||||||
const cancel = document.createElement('button');
|
const cancel = document.createElement('button');
|
||||||
cancel.className = 'pin-cancel';
|
cancel.className = 'pin-cancel';
|
||||||
cancel.textContent = 'Abbrechen';
|
cancel.textContent = 'Abbrechen';
|
||||||
cancel.onclick = () => { modal.remove(); resolve(null); };
|
cancel.onclick = () => { closeModal(modal); resolve(null); };
|
||||||
modal.querySelector('.pin-body').appendChild(cancel);
|
modal.querySelector('.pin-body').appendChild(cancel);
|
||||||
document.body.appendChild(modal);
|
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() {
|
async function promptNewPin() {
|
||||||
|
|
@ -186,6 +299,15 @@ window.appBoot = async function appBoot() {
|
||||||
console.warn('[boot] Token-Preload fehlgeschlagen', e);
|
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
|
// FAB: Click öffnet das Schnell-Auftrag-Modal
|
||||||
const fab = document.getElementById('fab-new-order');
|
const fab = document.getElementById('fab-new-order');
|
||||||
if (fab && !fab.dataset.bound) {
|
if (fab && !fab.dataset.bound) {
|
||||||
|
|
@ -411,9 +533,12 @@ router.on('/orders/:id', async (args) => {
|
||||||
<button class="btn btn-secondary" id="btn-new-report">📑 Neuen Bericht anlegen…</button>
|
<button class="btn btn-secondary" id="btn-new-report">📑 Neuen Bericht anlegen…</button>
|
||||||
|
|
||||||
<div class="detail-section" style="margin-top:16px;">
|
<div class="detail-section" style="margin-top:16px;">
|
||||||
<h3>Hochgeladene Fotos (${imagePhotos.length})</h3>
|
<div class="photo-section-head">
|
||||||
|
<h3 style="margin:0;">Hochgeladene Fotos (${imagePhotos.length})</h3>
|
||||||
|
${imagePhotos.length ? '<button class="btn btn-small" id="btn-select-mode" title="Mehrfachauswahl">☑ Auswählen</button>' : ''}
|
||||||
|
</div>
|
||||||
<div class="photo-grid" id="photo-grid">
|
<div class="photo-grid" id="photo-grid">
|
||||||
${imagePhotos.map(p => `<div class="thumb" data-relpath="${escapeHtml(p.relpath)}"><div class="thumb-placeholder">⏳</div></div>`).join('')}
|
${imagePhotos.map(p => `<div class="thumb" data-relpath="${escapeHtml(p.relpath)}"><div class="thumb-placeholder">⏳</div><div class="thumb-check">✓</div></div>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${audioFiles.length ? `
|
${audioFiles.length ? `
|
||||||
|
|
@ -444,10 +569,78 @@ router.on('/orders/:id', async (args) => {
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Foto-Thumbnails: Tap = Vollbild-Modal
|
// Foto-Thumbnails: Tap = Vollbild-Modal (oder Auswahl im Select-Modus)
|
||||||
document.querySelectorAll('#photo-grid .thumb').forEach(t => {
|
const photoGrid = document.getElementById('photo-grid');
|
||||||
t.addEventListener('click', () => openPhotoModal(args.id, t.dataset.relpath));
|
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 = `
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
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)
|
// Weitere Dokumente: Tap = Öffnen (PDF inline, sonst Download)
|
||||||
document.querySelectorAll('.doc-list .doc-item').forEach(el => {
|
document.querySelectorAll('.doc-list .doc-item').forEach(el => {
|
||||||
|
|
@ -544,14 +737,9 @@ router.on('/orders/:id', async (args) => {
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
await uploadPhoto(args.id, f);
|
await uploadPhoto(args.id, f);
|
||||||
}
|
}
|
||||||
// Reload Photo-Liste
|
// Nach Upload einfach die Route neu rendern — so werden Select-Mode,
|
||||||
try {
|
// Click-Handler und Thumbnails sauber neu aufgebaut.
|
||||||
const np = await api.listOrderPhotos(args.id);
|
router.navigate();
|
||||||
const imgs = np.photos.filter(p => (p.mime || '').startsWith('image/'));
|
|
||||||
document.getElementById('photo-grid').innerHTML =
|
|
||||||
imgs.map(p => `<div class="thumb" data-relpath="${escapeHtml(p.relpath)}"><div class="thumb-placeholder">⏳</div></div>`).join('');
|
|
||||||
loadThumbs();
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
}
|
||||||
camInput.addEventListener('change', () => handleFiles(camInput.files));
|
camInput.addEventListener('change', () => handleFiles(camInput.files));
|
||||||
galInput.addEventListener('change', () => handleFiles(galInput.files));
|
galInput.addEventListener('change', () => handleFiles(galInput.files));
|
||||||
|
|
@ -836,7 +1024,8 @@ router.on('/reports/:id', async (args) => {
|
||||||
showToast('PDF wird geladen…');
|
showToast('PDF wird geladen…');
|
||||||
const url = await api.getPdfBlobUrl(args.id);
|
const url = await api.getPdfBlobUrl(args.id);
|
||||||
if (!url) { showToast('PDF konnte nicht geladen werden', 'error'); return; }
|
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
|
// Unterschrift
|
||||||
|
|
@ -881,12 +1070,13 @@ router.on('/reports/:id', async (args) => {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
modal.querySelector('#dr-cancel').onclick = () => modal.remove();
|
pushModal(modal);
|
||||||
modal.querySelector('#dr-no').onclick = () => modal.remove();
|
modal.querySelector('#dr-cancel').onclick = () => closeModal(modal);
|
||||||
|
modal.querySelector('#dr-no').onclick = () => closeModal(modal);
|
||||||
modal.querySelector('#dr-yes').onclick = async () => {
|
modal.querySelector('#dr-yes').onclick = async () => {
|
||||||
try {
|
try {
|
||||||
await api.deleteReport(args.id);
|
await api.deleteReport(args.id);
|
||||||
modal.remove();
|
closeModal(modal);
|
||||||
showToast('Bericht gelöscht');
|
showToast('Bericht gelöscht');
|
||||||
router.go('#/reports');
|
router.go('#/reports');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -1141,8 +1331,9 @@ async function openMaterialModal(elementType, elementId) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
pushModal(modal);
|
||||||
|
|
||||||
modal.querySelector('#mt-close').onclick = () => modal.remove();
|
modal.querySelector('#mt-close').onclick = () => closeModal(modal);
|
||||||
|
|
||||||
async function reload() {
|
async function reload() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1244,6 +1435,7 @@ async function openNewReportModal(orderId) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
pushModal(modal);
|
||||||
|
|
||||||
// ODT-Default vorauswählen
|
// ODT-Default vorauswählen
|
||||||
try {
|
try {
|
||||||
|
|
@ -1254,7 +1446,7 @@ async function openNewReportModal(orderId) {
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
modal.querySelector('#nr-close').onclick = () => modal.remove();
|
modal.querySelector('#nr-close').onclick = () => closeModal(modal);
|
||||||
modal.querySelector('#nr-save').onclick = async () => {
|
modal.querySelector('#nr-save').onclick = async () => {
|
||||||
const titel = modal.querySelector('#nr-titel').value.trim();
|
const titel = modal.querySelector('#nr-titel').value.trim();
|
||||||
const tplSel = modal.querySelector('#nr-template');
|
const tplSel = modal.querySelector('#nr-template');
|
||||||
|
|
@ -1275,7 +1467,7 @@ async function openNewReportModal(orderId) {
|
||||||
template_odt: odt,
|
template_odt: odt,
|
||||||
});
|
});
|
||||||
showToast('✓ Bericht angelegt');
|
showToast('✓ Bericht angelegt');
|
||||||
modal.remove();
|
closeModal(modal);
|
||||||
router.go('#/reports/' + res.bericht_id);
|
router.go('#/reports/' + res.bericht_id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Fehler: ' + e.message, 'error');
|
showToast('Fehler: ' + e.message, 'error');
|
||||||
|
|
@ -1323,6 +1515,7 @@ async function openNewOrderModal() {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
pushModal(modal);
|
||||||
|
|
||||||
const closeBtn = modal.querySelector('#no-close');
|
const closeBtn = modal.querySelector('#no-close');
|
||||||
const saveBtn = modal.querySelector('#no-save');
|
const saveBtn = modal.querySelector('#no-save');
|
||||||
|
|
@ -1339,7 +1532,7 @@ async function openNewOrderModal() {
|
||||||
|
|
||||||
let selectedCustomer = null;
|
let selectedCustomer = null;
|
||||||
|
|
||||||
closeBtn.onclick = () => modal.remove();
|
closeBtn.onclick = () => closeModal(modal);
|
||||||
|
|
||||||
/* ---- Letzte Kunden als Quick-Pick ---- */
|
/* ---- Letzte Kunden als Quick-Pick ---- */
|
||||||
async function renderRecent() {
|
async function renderRecent() {
|
||||||
|
|
@ -1451,7 +1644,7 @@ async function openNewOrderModal() {
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
showToast('✓ Auftrag ' + (res.order.ref || '') + ' angelegt');
|
showToast('✓ Auftrag ' + (res.order.ref || '') + ' angelegt');
|
||||||
modal.remove();
|
closeModal(modal);
|
||||||
router.go('#/orders/' + res.order.id);
|
router.go('#/orders/' + res.order.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Fehler: ' + e.message, 'error');
|
showToast('Fehler: ' + e.message, 'error');
|
||||||
|
|
@ -1483,8 +1676,9 @@ async function openPageActionsModal(berichtId, pageId, relpath, note, title) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
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 () => {
|
modal.querySelector('#pa-save').onclick = async () => {
|
||||||
try {
|
try {
|
||||||
const note = modal.querySelector('#pa-note').value;
|
const note = modal.querySelector('#pa-note').value;
|
||||||
|
|
@ -1494,14 +1688,14 @@ async function openPageActionsModal(berichtId, pageId, relpath, note, title) {
|
||||||
body: JSON.stringify({ note, title }),
|
body: JSON.stringify({ note, title }),
|
||||||
});
|
});
|
||||||
showToast('✓ Gespeichert');
|
showToast('✓ Gespeichert');
|
||||||
modal.remove();
|
closeModal(modal);
|
||||||
router.navigate();
|
router.navigate();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback falls api.request nicht exposed ist
|
// Fallback falls api.request nicht exposed ist
|
||||||
try {
|
try {
|
||||||
await api.updatePageNote(pageId, modal.querySelector('#pa-note').value);
|
await api.updatePageNote(pageId, modal.querySelector('#pa-note').value);
|
||||||
showToast('✓ Notiz gespeichert');
|
showToast('✓ Notiz gespeichert');
|
||||||
modal.remove();
|
closeModal(modal);
|
||||||
router.navigate();
|
router.navigate();
|
||||||
} catch (er) { showToast('Fehler: ' + er.message, 'error'); }
|
} catch (er) { showToast('Fehler: ' + er.message, 'error'); }
|
||||||
}
|
}
|
||||||
|
|
@ -1511,7 +1705,7 @@ async function openPageActionsModal(berichtId, pageId, relpath, note, title) {
|
||||||
try {
|
try {
|
||||||
await api.deletePage(pageId);
|
await api.deletePage(pageId);
|
||||||
showToast('✓ Seite entfernt');
|
showToast('✓ Seite entfernt');
|
||||||
modal.remove();
|
closeModal(modal);
|
||||||
router.navigate();
|
router.navigate();
|
||||||
} catch (e) { showToast('Löschen fehlgeschlagen: ' + e.message, 'error'); }
|
} catch (e) { showToast('Löschen fehlgeschlagen: ' + e.message, 'error'); }
|
||||||
};
|
};
|
||||||
|
|
@ -1520,23 +1714,30 @@ async function openPageActionsModal(berichtId, pageId, relpath, note, title) {
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
* PDF-VORSCHAU MODAL
|
* PDF-VORSCHAU MODAL
|
||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
function openPdfModal(blobUrl) {
|
function openPdfModal(blobUrl, filename) {
|
||||||
|
const fname = filename || 'bericht.pdf';
|
||||||
const modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
modal.className = 'fullscreen-modal';
|
modal.className = 'fullscreen-modal';
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="fs-header">
|
<div class="fs-header">
|
||||||
<button class="icon-btn" id="pdf-close">✕</button>
|
<button class="icon-btn" id="pdf-close">✕</button>
|
||||||
<div class="fs-title">📑 PDF-Vorschau</div>
|
<div class="fs-title">📑 PDF-Vorschau</div>
|
||||||
<a class="icon-btn" id="pdf-download" href="${blobUrl}" download="bericht.pdf" title="Download">⬇</a>
|
<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>
|
</div>
|
||||||
<div class="fs-body" style="padding:0;">
|
<div class="fs-body" style="padding:0;">
|
||||||
<iframe src="${blobUrl}" style="width:100%;height:100%;border:none;background:#444;"></iframe>
|
<iframe src="${blobUrl}" style="width:100%;height:100%;border:none;background:#444;"></iframe>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
modal.querySelector('#pdf-close').onclick = () => {
|
pushModal(modal, () => { try { URL.revokeObjectURL(blobUrl); } catch {} });
|
||||||
URL.revokeObjectURL(blobUrl);
|
modal.querySelector('#pdf-close').onclick = () => closeModal(modal);
|
||||||
modal.remove();
|
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) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
pushModal(modal, () => { try { window.removeEventListener('resize', fitCanvas); } catch {} });
|
||||||
|
|
||||||
const canvas = modal.querySelector('#sig-canvas');
|
const canvas = modal.querySelector('#sig-canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
@ -1610,10 +1812,7 @@ function openSignatureModal(berichtId) {
|
||||||
canvas.addEventListener('touchend', end);
|
canvas.addEventListener('touchend', end);
|
||||||
|
|
||||||
modal.querySelector('#sig-clear').onclick = fitCanvas;
|
modal.querySelector('#sig-clear').onclick = fitCanvas;
|
||||||
modal.querySelector('#sig-close').onclick = () => {
|
modal.querySelector('#sig-close').onclick = () => closeModal(modal);
|
||||||
window.removeEventListener('resize', fitCanvas);
|
|
||||||
modal.remove();
|
|
||||||
};
|
|
||||||
modal.querySelector('#sig-save').onclick = async () => {
|
modal.querySelector('#sig-save').onclick = async () => {
|
||||||
const signer = modal.querySelector('#sig-name').value.trim();
|
const signer = modal.querySelector('#sig-name').value.trim();
|
||||||
if (!signer) {
|
if (!signer) {
|
||||||
|
|
@ -1641,7 +1840,7 @@ function openSignatureModal(berichtId) {
|
||||||
if (gps) { opts.gps_lat = gps.lat; opts.gps_lon = gps.lon; }
|
if (gps) { opts.gps_lat = gps.lat; opts.gps_lon = gps.lon; }
|
||||||
await api.uploadSignature(berichtId, blob, opts);
|
await api.uploadSignature(berichtId, blob, opts);
|
||||||
showToast('✓ Unterschrift hinzugefügt');
|
showToast('✓ Unterschrift hinzugefügt');
|
||||||
modal.remove();
|
closeModal(modal);
|
||||||
router.navigate();
|
router.navigate();
|
||||||
} catch (e) { showToast('Fehler: ' + e.message, 'error'); }
|
} catch (e) { showToast('Fehler: ' + e.message, 'error'); }
|
||||||
}, 'image/png');
|
}, 'image/png');
|
||||||
|
|
@ -1761,7 +1960,8 @@ function openHelpModal() {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
modal.querySelector('#help-close').onclick = () => modal.remove();
|
pushModal(modal);
|
||||||
|
modal.querySelector('#help-close').onclick = () => closeModal(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
|
|
@ -1829,17 +2029,18 @@ function openFileViewer({ url, blob, mime }, filename, relpath) {
|
||||||
<div class="fs-header">
|
<div class="fs-header">
|
||||||
<button class="icon-btn" id="dv-close">✕</button>
|
<button class="icon-btn" id="dv-close">✕</button>
|
||||||
<div class="fs-title">${escapeHtml(filename || '')}</div>
|
<div class="fs-title">${escapeHtml(filename || '')}</div>
|
||||||
|
<button class="icon-btn" id="dv-share" title="Teilen">📤</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="dv-body">
|
<div class="dv-body">
|
||||||
<img src="${url}" style="max-width:100%;max-height:100%;object-fit:contain;">
|
<img src="${url}" style="max-width:100%;max-height:100%;object-fit:contain;">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
const cleanup = () => {
|
pushModal(modal, () => { try { URL.revokeObjectURL(url); } catch {} });
|
||||||
URL.revokeObjectURL(url);
|
modal.querySelector('#dv-close').onclick = () => closeModal(modal);
|
||||||
modal.remove();
|
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) {
|
async function openPdfViewer(blob, filename, relpath) {
|
||||||
|
|
@ -1859,6 +2060,7 @@ async function openPdfViewer(blob, filename, relpath) {
|
||||||
<button class="icon-btn" id="pdf-next" title="Seite vor →">›</button>
|
<button class="icon-btn" id="pdf-next" title="Seite vor →">›</button>
|
||||||
<div class="fs-title">${escapeHtml(filename || '')}</div>
|
<div class="fs-title">${escapeHtml(filename || '')}</div>
|
||||||
<button class="icon-btn" id="pdf-close">✕</button>
|
<button class="icon-btn" id="pdf-close">✕</button>
|
||||||
|
<button class="icon-btn" id="pdf-share" title="Teilen">📤</button>
|
||||||
<button class="icon-btn" id="pdf-download" title="Download">⬇</button>
|
<button class="icon-btn" id="pdf-download" title="Download">⬇</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pdf-body" id="pdf-body">
|
<div class="pdf-body" id="pdf-body">
|
||||||
|
|
@ -1866,6 +2068,7 @@ async function openPdfViewer(blob, filename, relpath) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
pushModal(modal);
|
||||||
|
|
||||||
const canvas = modal.querySelector('#pdf-canvas');
|
const canvas = modal.querySelector('#pdf-canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
@ -1908,6 +2111,9 @@ async function openPdfViewer(blob, filename, relpath) {
|
||||||
const params = new URLSearchParams({ relpath, jwt: t, download: 1 });
|
const params = new URLSearchParams({ relpath, jwt: t, download: 1 });
|
||||||
window.location.href = window.location.origin + '/custom/bericht/api/photo.php?' + params.toString();
|
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-prev').disabled = false;
|
||||||
modal.querySelector('#pdf-next').disabled = pdf.numPages === 1;
|
modal.querySelector('#pdf-next').disabled = pdf.numPages === 1;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -1917,7 +2123,7 @@ async function openPdfViewer(blob, filename, relpath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
modal.querySelector('#pdf-close').onclick = () => {
|
modal.querySelector('#pdf-close').onclick = () => {
|
||||||
modal.remove();
|
closeModal(modal);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1943,6 +2149,7 @@ async function openPhotoModal(orderId, relpath) {
|
||||||
<button class="icon-btn" id="fs-close">✕</button>
|
<button class="icon-btn" id="fs-close">✕</button>
|
||||||
<div class="fs-title" id="pv-title"></div>
|
<div class="fs-title" id="pv-title"></div>
|
||||||
<div class="pv-counter" id="pv-counter"></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-sketch" title="Zeichnen">✏️</button>
|
||||||
<button class="icon-btn" id="fs-delete" title="Löschen">🗑️</button>
|
<button class="icon-btn" id="fs-delete" title="Löschen">🗑️</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1958,6 +2165,7 @@ async function openPhotoModal(orderId, relpath) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
pushModal(modal, () => { try { document.removeEventListener('keydown', keyHandler); } catch {} });
|
||||||
|
|
||||||
const img = modal.querySelector('#pv-image');
|
const img = modal.querySelector('#pv-image');
|
||||||
const body = modal.querySelector('#pv-body');
|
const body = modal.querySelector('#pv-body');
|
||||||
|
|
@ -2002,7 +2210,7 @@ async function openPhotoModal(orderId, relpath) {
|
||||||
resetZoom();
|
resetZoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() { modal.remove(); document.removeEventListener('keydown', keyHandler); }
|
function close() { closeModal(modal); }
|
||||||
function goPrev() { if (currentIndex > 0) loadImage(currentIndex - 1); }
|
function goPrev() { if (currentIndex > 0) loadImage(currentIndex - 1); }
|
||||||
function goNext() { if (currentIndex < allRelpaths.length - 1) loadImage(currentIndex + 1); }
|
function goNext() { if (currentIndex < allRelpaths.length - 1) loadImage(currentIndex + 1); }
|
||||||
|
|
||||||
|
|
@ -2027,6 +2235,15 @@ async function openPhotoModal(orderId, relpath) {
|
||||||
close();
|
close();
|
||||||
openSketchEditor(orderId, currentUrl, allRelpaths[currentIndex]);
|
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
|
// Doppelklick = Zoom Toggle
|
||||||
img.addEventListener('dblclick', (e) => {
|
img.addEventListener('dblclick', (e) => {
|
||||||
|
|
@ -2129,6 +2346,11 @@ async function openVoiceModal(orderId) {
|
||||||
let startTime = 0;
|
let startTime = 0;
|
||||||
let audioBlob = null;
|
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 startBtn = modal.querySelector('#v-start');
|
||||||
const stopBtn = modal.querySelector('#v-stop');
|
const stopBtn = modal.querySelector('#v-stop');
|
||||||
const sendBtn = modal.querySelector('#v-send');
|
const sendBtn = modal.querySelector('#v-send');
|
||||||
|
|
@ -2136,11 +2358,7 @@ async function openVoiceModal(orderId) {
|
||||||
const timeEl = modal.querySelector('#v-time');
|
const timeEl = modal.querySelector('#v-time');
|
||||||
const preview = modal.querySelector('#v-preview');
|
const preview = modal.querySelector('#v-preview');
|
||||||
|
|
||||||
modal.querySelector('#v-close').onclick = () => {
|
modal.querySelector('#v-close').onclick = () => closeModal(modal);
|
||||||
if (mediaRecorder && mediaRecorder.state === 'recording') mediaRecorder.stop();
|
|
||||||
if (timer) clearInterval(timer);
|
|
||||||
modal.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
startBtn.onclick = async () => {
|
startBtn.onclick = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -2182,7 +2400,7 @@ async function openVoiceModal(orderId) {
|
||||||
try {
|
try {
|
||||||
await api.uploadVoiceNote(orderId, audioBlob, 'voice_' + Date.now() + '.webm');
|
await api.uploadVoiceNote(orderId, audioBlob, 'voice_' + Date.now() + '.webm');
|
||||||
showToast('✓ Sprachnotiz hochgeladen');
|
showToast('✓ Sprachnotiz hochgeladen');
|
||||||
modal.remove();
|
closeModal(modal);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Upload fehlgeschlagen: ' + e.message, 'error');
|
showToast('Upload fehlgeschlagen: ' + e.message, 'error');
|
||||||
sendBtn.disabled = false;
|
sendBtn.disabled = false;
|
||||||
|
|
@ -2227,6 +2445,7 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
pushModal(modal, () => { try { window.removeEventListener('resize', fitCanvasToScreen); } catch {} });
|
||||||
|
|
||||||
const canvas = modal.querySelector('#sk-canvas');
|
const canvas = modal.querySelector('#sk-canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
@ -2528,10 +2747,7 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
|
||||||
canvas.addEventListener('touchmove', moveDraw, { passive: false });
|
canvas.addEventListener('touchmove', moveDraw, { passive: false });
|
||||||
canvas.addEventListener('touchend', endDraw);
|
canvas.addEventListener('touchend', endDraw);
|
||||||
|
|
||||||
modal.querySelector('#sk-close').onclick = () => {
|
modal.querySelector('#sk-close').onclick = () => closeModal(modal);
|
||||||
window.removeEventListener('resize', fitCanvasToScreen);
|
|
||||||
modal.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
modal.querySelector('#sk-save').onclick = async () => {
|
modal.querySelector('#sk-save').onclick = async () => {
|
||||||
showToast('Speichere Skizze…');
|
showToast('Speichere Skizze…');
|
||||||
|
|
@ -2539,7 +2755,7 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
|
||||||
try {
|
try {
|
||||||
await api.uploadAnnotatedPhoto(orderId, blob, 'sketch_' + Date.now() + '.jpg');
|
await api.uploadAnnotatedPhoto(orderId, blob, 'sketch_' + Date.now() + '.jpg');
|
||||||
showToast('✓ Skizze gespeichert');
|
showToast('✓ Skizze gespeichert');
|
||||||
modal.remove();
|
closeModal(modal);
|
||||||
router.navigate();
|
router.navigate();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Upload fehlgeschlagen: ' + e.message, 'error');
|
showToast('Upload fehlgeschlagen: ' + e.message, 'error');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue