fix: Finalisieren-Button immer sichtbar + Sprachnotizen eigene Sektion
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 1s

- Bericht-Detail: Button ist jetzt immer da, bei Final heißt er
  'PDF neu erzeugen'. Bei 0 Seiten disabled statt versteckt, mit
  Hinweis-Empty-State darüber.
- Auftrags-Detail: Audio-Files (webm/mp3/ogg/m4a/wav) werden aus
  'Weitere Dokumente' rausgefiltert und in eine eigene Sektion
  '🎙 Sprachnotizen' mit Play-Button je Eintrag gelegt. Tap auf ▶
  lädt die Datei als Blob (mit JWT) und setzt einen <audio>-Player
  inline ein.
- Service Worker v3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
This commit is contained in:
Eduard Wisch 2026-04-09 00:43:27 +02:00
parent a1589a7ae2
commit bd9580bd46
3 changed files with 98 additions and 9 deletions

33
app.css
View file

@ -405,3 +405,36 @@ body {
width: 100px;
flex-shrink: 0;
}
/* Audio-Items in Auftrags-Detail */
.audio-list { display: flex; flex-direction: column; gap: 8px; }
.audio-item {
background: #2a2a30;
border-radius: 6px;
padding: 10px 12px;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.audio-item .audio-name {
flex: 1;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.audio-item .audio-play {
background: #337ab7;
color: #fff;
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
font-size: 14px;
cursor: pointer;
flex-shrink: 0;
}
.audio-item audio { width: 100%; flex-basis: 100%; }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }

72
app.js
View file

@ -124,9 +124,13 @@ router.on('/orders/:id', async (args) => {
const photos = await api.listOrderPhotos(args.id).catch(() => ({ photos: [] }));
title(data.order.ref);
// Nur Bilder in der Foto-Grid zeigen; alles andere (PDFs etc.) unten auflisten
// Nach MIME-Type aufteilen
const imagePhotos = photos.photos.filter(p => (p.mime || '').startsWith('image/'));
const otherDocs = photos.photos.filter(p => !(p.mime || '').startsWith('image/'));
const audioFiles = photos.photos.filter(p => (p.mime || '').startsWith('audio/') || /\.(webm|mp3|ogg|m4a|wav)$/i.test(p.filename));
const otherDocs = photos.photos.filter(p =>
!(p.mime || '').startsWith('image/') &&
!(p.mime || '').startsWith('audio/') &&
!/\.(webm|mp3|ogg|m4a|wav)$/i.test(p.filename));
main().innerHTML = `
<div class="detail-section">
@ -155,6 +159,14 @@ router.on('/orders/:id', async (args) => {
${imagePhotos.map(p => `<div class="thumb" data-relpath="${escapeHtml(p.relpath)}"><div class="thumb-placeholder">⏳</div></div>`).join('')}
</div>
</div>
${audioFiles.length ? `
<div class="detail-section">
<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><button class="audio-play" title="Abspielen">▶</button></div>`).join('')}
</div>
</div>` : ''}
${otherDocs.length ? `
<div class="detail-section">
<h3>Weitere Dokumente (${otherDocs.length})</h3>
@ -170,6 +182,44 @@ router.on('/orders/:id', async (args) => {
// Sprachnotiz
document.getElementById('btn-voice').onclick = () => openVoiceModal(args.id);
// Audio-Files abspielen
document.querySelectorAll('.audio-item .audio-play').forEach(btn => {
btn.addEventListener('click', async (e) => {
const item = e.target.closest('.audio-item');
const rel = item.dataset.relpath;
const mime = item.dataset.mime || 'audio/webm';
// Existierenden Player toggeln
let player = item.querySelector('audio');
if (player) {
if (player.paused) player.play();
else player.pause();
return;
}
btn.textContent = '⏳';
try {
const t = await api.getToken();
const params = new URLSearchParams({ relpath: rel, jwt: t });
const r = await fetch(window.location.origin + '/custom/bericht/api/photo.php?' + params.toString());
if (!r.ok) throw new Error('Load failed');
const blob = await r.blob();
const url = URL.createObjectURL(new Blob([blob], { type: mime }));
player = document.createElement('audio');
player.controls = true;
player.src = url;
player.style.width = '100%';
player.style.marginTop = '8px';
item.appendChild(player);
player.play();
btn.textContent = '⏸';
player.onplay = () => btn.textContent = '⏸';
player.onpause = () => btn.textContent = '▶';
} catch (err) {
showToast('Audio laden fehlgeschlagen: ' + err.message, 'error');
btn.textContent = '▶';
}
});
});
loadThumbs();
const camInput = document.getElementById('camera-input');
@ -302,7 +352,8 @@ router.on('/reports/:id', async (args) => {
title(data.report.ref);
const statusLabel = data.report.status === 1 ? 'Final' : 'Entwurf';
const canFinalize = data.report.status !== 1 && data.pages.length > 0;
const hasPages = data.pages.length > 0;
const finalizeLabel = data.report.status === 1 ? '🔄 PDF neu erzeugen' : '📑 Bericht finalisieren (PDF)';
main().innerHTML = `
<div class="detail-section">
@ -314,19 +365,24 @@ router.on('/reports/:id', async (args) => {
<p class="label">Status: <strong>${statusLabel}</strong></p>
</div>
${hasPages ? `
<div class="photo-grid" id="report-pages">
${data.pages.map(p => `<div class="thumb" data-relpath="${escapeHtml(p.source_path)}" data-page-id="${p.id}"><div class="thumb-placeholder">⏳</div></div>`).join('')}
</div>
</div>` : '<div class="empty-state"><div class="icon">📭</div>Dieser Bericht hat noch keine Seiten. Fotos aufnehmen, um Seiten hinzuzufügen.</div>'}
${canFinalize ? `<button class="btn btn-large" id="btn-finalize">📑 Bericht finalisieren (PDF)</button>` : ''}
<button class="btn btn-large" id="btn-finalize" ${hasPages ? '' : 'disabled'}>${finalizeLabel}</button>
<button class="btn btn-secondary" id="btn-open-editor"> Im Desktop-Editor öffnen</button>
`;
loadThumbs();
if (canFinalize) {
document.getElementById('btn-finalize').onclick = async () => {
if (!confirm('Bericht jetzt finalisieren und PDF erzeugen?')) return;
const finalizeBtn = document.getElementById('btn-finalize');
if (finalizeBtn) {
finalizeBtn.onclick = async () => {
if (!hasPages) { showToast('Bericht hat keine Seiten', 'warn'); return; }
if (!confirm(data.report.status === 1
? 'PDF neu erzeugen und unter den verknüpften Dokumenten ablegen?'
: 'Bericht jetzt finalisieren und PDF erzeugen?')) return;
showToast('PDF wird erzeugt…');
try {
const r = await api.finalizeReport(args.id);

2
sw.js
View file

@ -4,7 +4,7 @@
* - API-Calls: network-first, kein offline-cache (da auth-pflichtig)
*/
const CACHE = 'baustelle-v2';
const CACHE = 'baustelle-v3';
const SHELL = [
'./',
'./index.html',