Auftragsbeschreibung in der App bearbeiten (Merkzettel zum Auftrag)

Beschreibung am Auftrag mit Knopf Bearbeiten, mehrzeilig angezeigt, Cursor am
Ende. Hat das Büro inzwischen geändert: Büro-Stand + eigene Ergänzung, neuer
Stand oder eigener Text. Hinweis auf den Stundenzettel. Knöpfe im
Abschnittskopf nur so breit wie ihr Inhalt. Braucht Bericht 1.9.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-10-21 19:57:16 +02:00
parent 47cd91193e
commit ee28b6938f
5 changed files with 134 additions and 7 deletions

View file

@ -44,6 +44,15 @@ Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und
- ✅ Foto-Viewer mit Zoom + Swipe
- ✅ Foto-Skizze: Annotationen mit Pfeilen, Kreisen, Rechtecken, Text
**Auftragsbeschreibung als Merkzettel** (21.10.2026, Server: Bericht ≥ 1.9.0)
- ✅ Am Auftrag „Beschreibung" mit Knopf **Bearbeiten**: Vollbild-Textfeld, Cursor steht am Ende (meist wird etwas
dazugeschrieben), mehrzeilig angezeigt. Gespeichert im Zusatzfeld `auftragsbeschreibung` — dasselbe wie im Büro an der
Auftragskarte. Eddy: „Sachen, die ich mir merken will, lieber in der Auftragsbeschreibung festhalten."
- ✅ Hinweis im Dialog: **steht auch auf dem Stundenzettel für den Kunden**
- ✅ Kein stilles Überschreiben: hat das Büro inzwischen geändert, zeigt der Dialog den neuen Stand —
„Büro-Stand + meine Ergänzung" (wenn nur angehängt wurde), „Neuen Stand übernehmen" oder „Meinen Text speichern"
- ✅ Nur mit Netz wie die Notizen: ohne Netz bleibt der Dialog offen, nichts geht verloren
**Notizen**
- ✅ Getippte Notiz zum Auftrag: Betreff + Text, Liste mit Vorschau und Zeitstempel, jederzeit wieder les- und änderbar
- ✅ Ablage als `notiz_<betreff>_<datum>.txt` im Auftragsordner — damit auch im Dolibarr-Auftrag und in der Anhänge-Spalte des Bericht-Editors sichtbar

View file

@ -1,6 +1,6 @@
# ROADMAP — Baustelle PWA
Stand: 2026-10-19 · Gegenstück: `data/bericht` (Modul-Version 1.8.0)
Stand: 2026-10-21 · Gegenstück: `data/bericht` (Modul-Version 1.9.0)
Die PWA hat kein eigenes Backend — alles läuft über `custom/bericht/api/`. Punkte, die eine
API-Änderung brauchen, sind mit **[API]** markiert und stehen zusätzlich in der ROADMAP des
@ -10,6 +10,9 @@ Bericht-Moduls.
## Offen
- [x] ✅ 21.10.2026 **[API]** Auftragsbeschreibung in der App bearbeiten (Eddy: Merkzettel lieber in der Beschreibung als
als extra Notiz-Datei). Bericht 1.9.0 `set_beschreibung` mit Konfliktschutz (409). Steht auch auf dem
Stundenzettel-PDF — Eddy hat das so gewählt. Erst Bericht ausliefern, dann die App.
- [x] ✅ 19.10.2026 (ohne Schema-Änderung): `idb.queueAll()` lud **alle** Foto-Daten in den Speicher, weil sie
als ArrayBuffer gesichert waren. Neue Einträge tragen den Blob selbst — Chrome legt ihn als Datei ab und liest
ihn erst beim Hochladen; `getAll()` liefert nur den Verweis. Alte ArrayBuffer-Einträge laufen wie bisher ab.

View file

@ -354,6 +354,12 @@ body {
color: #7aa2f7; /* fett in normaler Schreibweise — keine Versalien (Eddy) */
}
.detail-section p { margin: 4px 0; font-size: 14px; }
/* Auftragsbeschreibung: Zeilenumbrüche behalten (mehrzeilig gemerkt) */
.auftrag-beschreibung { white-space: pre-wrap; word-break: break-word; }
.auftrag-beschreibung-leer { opacity: 0.6; font-size: 13px; }
.beschreibung-konflikt { background: #3a2a10; border: 1px solid #ca8a04; border-radius: 8px; padding: 10px; margin-bottom: 10px; font-size: 14px; }
.beschreibung-konflikt-text { white-space: pre-wrap; word-break: break-word; margin: 6px 0; opacity: 0.9; }
.beschreibung-konflikt-knoepfe { display: flex; gap: 8px; flex-wrap: wrap; }
.detail-section .label { opacity: 0.6; font-size: 12px; }
.photo-grid, .video-grid {
@ -442,6 +448,9 @@ body {
gap: 8px;
margin: 0 0 6px;
}
/* Knopf im Abschnittskopf (Bearbeiten, Auswählen) so breit wie sein Inhalt — .btn hat sonst width:100%
und zog sich über die ganze Zeile */
.photo-section-head > .btn { width: auto; flex: 0 0 auto; }
.btn-small {
padding: 6px 10px;
font-size: 13px;

106
app.js
View file

@ -842,11 +842,15 @@ router.on('/orders/:id', async (args) => {
${data.customer.phone ? `<p><a class="tel-link" href="tel:${escapeHtml(data.customer.phone)}">${icon('telefon', 18)}${escapeHtml(data.customer.phone)}</a></p>` : ''}
</div>
${data.order.auftragsbeschreibung ? `
<div class="detail-section">
<h3>Beschreibung</h3>
<p>${escapeHtml(data.order.auftragsbeschreibung)}</p>
</div>` : ''}
<div class="photo-section-head">
<h3 style="margin:0;">Beschreibung</h3>
<button class="btn btn-small" id="btn-edit-desc">${icon('zeichnen', 18)}Bearbeiten</button>
</div>
${data.order.auftragsbeschreibung
? `<p class="auftrag-beschreibung">${escapeHtml(data.order.auftragsbeschreibung)}</p>`
: '<p class="auftrag-beschreibung-leer">Noch nichts festgehalten — mit „Bearbeiten" notieren, was du dir für den Auftrag merken willst.</p>'}
</div>
<button class="btn btn-large" id="btn-take-photo">${icon('kamera', 20)}Foto aufnehmen</button>
<input type="file" id="camera-input" class="hidden-input" accept="image/*" capture="environment" multiple>
@ -1040,6 +1044,7 @@ router.on('/orders/:id', async (args) => {
// Sprachnotiz
document.getElementById('btn-voice').onclick = () => openVoiceModal(args.id);
document.getElementById('btn-notes').onclick = () => openNotesModal(args.id);
document.getElementById('btn-edit-desc').onclick = () => openDescriptionEditor(args.id, data.order.auftragsbeschreibung || '');
// Neuen Bericht anlegen
document.getElementById('btn-new-report').onclick = () => openNewReportModal(args.id);
@ -4972,6 +4977,99 @@ async function openNotesModal(orderId) {
refresh();
}
/* ============================================================
* AUFTRAGSBESCHREIBUNG BEARBEITEN (Bericht ≥ 1.9.0)
*
* Eddy: „Notizen ist gut, aber als extra Datei brauche ich das selten — Sachen, die ich mir
* merken will, lieber in der Auftragsbeschreibung festhalten." Gespeichert wird im Zusatzfeld
* auftragsbeschreibung am Auftrag — dasselbe, das im Büro an der Auftragskarte steht und auf
* dem Stundenzettel-PDF für den Kunden gedruckt wird (Eddy wollte es so, Hinweis im Dialog).
*
* Nur mit Netz, wie die Notizen: ohne Netz bleibt der Dialog offen, nichts geht verloren.
* Der angezeigte Stand reist als `vorher` mit — hat das Büro inzwischen geändert, zeigt der
* Dialog den neuen Stand und fragt, statt ihn zu überschreiben.
* ============================================================ */
async function openDescriptionEditor(orderId, aktuell) {
let vorher = aktuell || '';
const modal = document.createElement('div');
modal.className = 'fullscreen-modal note-modal';
modal.innerHTML = `
<div class="fs-header">
<button class="icon-btn" id="ds-close">✕</button>
<div class="fs-title">Auftragsbeschreibung</div>
<button class="icon-btn" id="ds-save" title="Speichern">✓</button>
</div>
<div class="note-edit-body">
<div class="beschreibung-konflikt" id="ds-konflikt" hidden></div>
<textarea id="ds-text" placeholder="Was du dir für den Auftrag merken willst …">${escapeHtml(vorher)}</textarea>
<div class="note-edit-meta">Steht auch auf dem Stundenzettel für den Kunden.</div>
</div>
`;
document.body.appendChild(modal);
pushModal(modal);
const textEl = modal.querySelector('#ds-text');
const saveBtn = modal.querySelector('#ds-save');
const konfliktEl = modal.querySelector('#ds-konflikt');
modal.querySelector('#ds-close').onclick = async () => {
if (textEl.value.replace(/\s+$/, '') !== vorher.replace(/\s+$/, '') && !await confirmDialog({
title: 'Änderungen verwerfen',
text: 'Die Beschreibung wurde noch nicht gespeichert.',
confirmLabel: 'Verwerfen',
danger: true,
})) return;
closeModal(modal);
};
async function speichern() {
saveBtn.disabled = true;
try {
const r = await api.setOrderDescription(orderId, textEl.value, vorher);
showToast(r.unveraendert ? 'Keine Änderung' : '✓ Beschreibung gespeichert');
closeModal(modal);
router.navigate();
} catch (e) {
saveBtn.disabled = false;
if (e && e.status === 409 && e.data) {
// Im Büro geändert: neuen Stand zeigen, Eddy entscheidet
const neu = e.data.auftragsbeschreibung || '';
// Meist wurde unten nur etwas angehängt — dann bleibt die eigene Ergänzung beim Übernehmen erhalten
const ergaenzung = textEl.value.startsWith(vorher) ? textEl.value.slice(vorher.length) : '';
konfliktEl.hidden = false;
konfliktEl.innerHTML = `
<strong>Inzwischen im Büro geändert:</strong>
<div class="beschreibung-konflikt-text">${escapeHtml(neu) || '<em>(leer)</em>'}</div>
<div class="beschreibung-konflikt-knoepfe">
<button class="btn btn-small" id="ds-neu-laden">${ergaenzung.trim() ? 'Büro-Stand + meine Ergänzung' : 'Neuen Stand übernehmen'}</button>
<button class="btn btn-small" id="ds-ueberschreiben">Meinen Text speichern</button>
</div>`;
konfliktEl.querySelector('#ds-neu-laden').onclick = () => {
textEl.value = neu + (ergaenzung.trim() ? ergaenzung : '');
vorher = neu; konfliktEl.hidden = true;
textEl.focus();
const n = textEl.value.length;
try { textEl.setSelectionRange(n, n); } catch (_) {}
};
konfliktEl.querySelector('#ds-ueberschreiben').onclick = () => {
vorher = neu; konfliktEl.hidden = true; speichern();
};
return;
}
showToast((e && e.offline) ? 'Kein Netz — noch nicht gespeichert. Später erneut ✓ tippen.' : ('Speichern fehlgeschlagen: ' + e.message), 'error');
}
}
saveBtn.onclick = speichern;
// Cursor ans Ende: meist wird etwas dazugeschrieben
setTimeout(() => {
textEl.focus();
const n = textEl.value.length;
try { textEl.setSelectionRange(n, n); } catch (_) {}
textEl.scrollTop = textEl.scrollHeight;
}, 80);
}
/* Eine Notiz schreiben oder ändern. file = null → neue Notiz. */
async function openNoteEditor(orderId, file, onSaved) {
let existing = null;

View file

@ -86,7 +86,8 @@
e.status = r.status; e.nonJson = true;
throw e;
}
if (!r.ok) { const e = new Error(data.error || 'API-Fehler'); e.status = r.status; throw e; }
// e.data: die Antwort selbst — z. B. 409 bei der Auftragsbeschreibung trägt den aktuellen Stand
if (!r.ok) { const e = new Error(data.error || 'API-Fehler'); e.status = r.status; e.data = data; throw e; }
return data;
}
@ -268,6 +269,13 @@
return request(qs, { method: 'POST', body: JSON.stringify(data), timeoutMs: 45000 });
}
/* Auftragsbeschreibung ändern (Bericht ≥ 1.9.0). vorher = angezeigter Stand: hat das Büro
* inzwischen geändert, antwortet der Server mit 409 + aktuellem Text statt zu überschreiben. */
async function setOrderDescription(orderId, text, vorher) {
return request('/orders.php?id=' + orderId + '&action=set_beschreibung',
{ method: 'POST', body: JSON.stringify({ text, vorher }), timeoutMs: 30000 });
}
async function deleteNote(orderId, file) {
return request('/note.php?order_id=' + orderId + '&file=' + encodeURIComponent(file) + '&delete=1',
{ method: 'POST' });
@ -425,7 +433,7 @@
listCustomers, getCustomer,
getReport, listReports, createReport, listTemplates, listOdtTemplates, finalizeReport, deleteReport,
deletePhoto, uploadVoiceNote, transcribeAudio, uploadAnnotatedPhoto,
listNotes, getNote, saveNote, deleteNote,
listNotes, getNote, saveNote, deleteNote, setOrderDescription,
listMaterials, addMaterial, deleteMaterial,
getPhotoBlobUrl, clearPhotoCache, getFileBlobUrl, photoThumbUrl, photoUrl,
deletePage, updatePageNote, uploadSignature, getPdfBlobUrl, reorderPages,