fix: Finalisieren-Button immer sichtbar + Sprachnotizen eigene Sektion
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 1s
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:
parent
a1589a7ae2
commit
bd9580bd46
3 changed files with 98 additions and 9 deletions
33
app.css
33
app.css
|
|
@ -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
72
app.js
|
|
@ -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
2
sw.js
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue