feat: Block C — Seiten-Titel + api.request exposed
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 1s

- openPageActionsModal bekommt Titel-Feld (wird im PDF groß oben auf
  der Seite gedruckt, wenn gesetzt) zusätzlich zur Notiz
- Save sendet beide Felder zusammen via api.request POST
- Seiten-Thumbs zeigen Titel als Badge unten (ellipsis bei langem
  Text)
- lib/api.js exposed request() als Low-Level-Funktion für
  Spezialfälle wie dieses Update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
This commit is contained in:
Eduard Wisch 2026-04-09 09:10:57 +02:00
parent 31a690454c
commit 54eea0fb22
3 changed files with 41 additions and 11 deletions

12
app.css
View file

@ -568,6 +568,18 @@ body {
border-radius: 10px; border-radius: 10px;
font-weight: 600; font-weight: 600;
} }
.report-page-thumb .page-title-badge {
position: absolute;
bottom: 0; left: 0; right: 0;
background: rgba(0,0,0,0.75);
color: #fff;
font-size: 10px;
padding: 3px 6px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Unterschrift-Modal */ /* Unterschrift-Modal */
.signature-modal .signature-meta { .signature-modal .signature-meta {

38
app.js
View file

@ -630,9 +630,10 @@ router.on('/reports/:id', async (args) => {
${hasPages ? ` ${hasPages ? `
<div class="photo-grid" id="report-pages"> <div class="photo-grid" id="report-pages">
${data.pages.map((p, i) => ` ${data.pages.map((p, i) => `
<div class="thumb report-page-thumb" data-relpath="${escapeHtml(p.source_path)}" data-page-id="${p.id}" data-note="${escapeHtml(p.note || '')}"> <div class="thumb report-page-thumb" data-relpath="${escapeHtml(p.source_path)}" data-page-id="${p.id}" data-note="${escapeHtml(p.note || '')}" data-title="${escapeHtml(p.title || '')}">
<div class="thumb-placeholder"></div> <div class="thumb-placeholder"></div>
<div class="page-num">${i + 1}</div> <div class="page-num">${i + 1}</div>
${p.title ? `<div class="page-title-badge">${escapeHtml(p.title)}</div>` : ''}
</div> </div>
`).join('')} `).join('')}
</div>` : '<div class="empty-state"><div class="icon">📭</div>Dieser Bericht hat noch keine Seiten. Fotos aufnehmen, um Seiten hinzuzufügen.</div>'} </div>` : '<div class="empty-state"><div class="icon">📭</div>Dieser Bericht hat noch keine Seiten. Fotos aufnehmen, um Seiten hinzuzufügen.</div>'}
@ -785,7 +786,7 @@ function bindReportPageInteractions(reportId) {
// Click/Tap → Aktion-Modal (nur wenn nicht gedraggt wurde) // Click/Tap → Aktion-Modal (nur wenn nicht gedraggt wurde)
t.addEventListener('click', (e) => { t.addEventListener('click', (e) => {
if (dragging) { e.preventDefault(); return; } if (dragging) { e.preventDefault(); return; }
openPageActionsModal(reportId, t.dataset.pageId, t.dataset.relpath, t.dataset.note || ''); openPageActionsModal(reportId, t.dataset.pageId, t.dataset.relpath, t.dataset.note || '', t.dataset.title || '');
}); });
// Long-Press → Drag-Modus // Long-Press → Drag-Modus
@ -939,7 +940,7 @@ async function openNewReportModal(orderId) {
/* ============================================================ /* ============================================================
* SEITEN-AKTIONEN MODAL (Notiz, Löschen, Vollbild) * SEITEN-AKTIONEN MODAL (Notiz, Löschen, Vollbild)
* ============================================================ */ * ============================================================ */
async function openPageActionsModal(berichtId, pageId, relpath, note) { async function openPageActionsModal(berichtId, pageId, relpath, note, title) {
const url = await api.getPhotoBlobUrl(relpath); const url = await api.getPhotoBlobUrl(relpath);
const modal = document.createElement('div'); const modal = document.createElement('div');
modal.className = 'fullscreen-modal'; modal.className = 'fullscreen-modal';
@ -950,22 +951,37 @@ async function openPageActionsModal(berichtId, pageId, relpath, note) {
<button class="icon-btn" id="pa-delete" title="Seite löschen">🗑</button> <button class="icon-btn" id="pa-delete" title="Seite löschen">🗑</button>
</div> </div>
<div class="fs-body" style="flex-direction:column;gap:12px;padding:16px;"> <div class="fs-body" style="flex-direction:column;gap:12px;padding:16px;">
${url ? `<img src="${url}" style="max-height:50vh;">` : '<div class="thumb-placeholder">⚠</div>'} ${url ? `<img src="${url}" style="max-height:40vh;">` : '<div class="thumb-placeholder">⚠</div>'}
<label class="label" style="align-self:flex-start;">Notiz zur Seite (wird im PDF gedruckt):</label> <label class="label" style="align-self:flex-start;">Titel / Zwischentitel (groß oben auf der Seite):</label>
<textarea id="pa-note" rows="4" style="width:100%;background:#2a2a30;color:#fff;border:1px solid #444;border-radius:6px;padding:10px;font-size:14px;">${escapeHtml(note)}</textarea> <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;">
<button class="btn" id="pa-save-note">💾 Notiz speichern</button> <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>
</div> </div>
`; `;
document.body.appendChild(modal); document.body.appendChild(modal);
modal.querySelector('#pa-close').onclick = () => modal.remove(); modal.querySelector('#pa-close').onclick = () => modal.remove();
modal.querySelector('#pa-save-note').onclick = async () => { modal.querySelector('#pa-save').onclick = async () => {
try { try {
await api.updatePageNote(pageId, modal.querySelector('#pa-note').value); const note = modal.querySelector('#pa-note').value;
showToast('✓ Notiz gespeichert'); const title = modal.querySelector('#pa-title').value;
await api.request('/pages.php?id=' + pageId, {
method: 'POST',
body: JSON.stringify({ note, title }),
});
showToast('✓ Gespeichert');
modal.remove(); modal.remove();
router.navigate(); router.navigate();
} catch (e) { showToast('Fehler: ' + e.message, 'error'); } } catch (e) {
// Fallback falls api.request nicht exposed ist
try {
await api.updatePageNote(pageId, modal.querySelector('#pa-note').value);
showToast('✓ Notiz gespeichert');
modal.remove();
router.navigate();
} catch (er) { showToast('Fehler: ' + er.message, 'error'); }
}
}; };
modal.querySelector('#pa-delete').onclick = async () => { modal.querySelector('#pa-delete').onclick = async () => {
if (!confirm('Diese Seite aus dem Bericht entfernen?')) return; if (!confirm('Diese Seite aus dem Bericht entfernen?')) return;

View file

@ -221,7 +221,9 @@
blobUrlCache.clear(); blobUrlCache.clear();
} }
// Low-level request-Funktion auch exposen für Spezialfälle
window.api = { window.api = {
request,
getToken, setToken, clearToken, getToken, setToken, clearToken,
login, logout, login, logout,
listOrders, getOrder, listOrderPhotos, uploadOrderPhoto, listOrders, getOrder, listOrderPhotos, uploadOrderPhoto,