diff --git a/app.css b/app.css
index 97a8196..52222e4 100644
--- a/app.css
+++ b/app.css
@@ -436,6 +436,29 @@ body {
flex-shrink: 0;
}
.audio-item audio { width: 100%; flex-basis: 100%; }
+.audio-item .audio-transcribe {
+ background: #2a2a30;
+ color: #7aa2f7;
+ border: 1px solid #444;
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ font-size: 14px;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+.audio-transcript {
+ flex-basis: 100%;
+ background: #1a1a1f;
+ padding: 10px 12px;
+ margin-top: 6px;
+ border-radius: 4px;
+ border-left: 3px solid #7aa2f7;
+ font-size: 13px;
+ line-height: 1.5;
+ color: #e0e0e0;
+ white-space: pre-wrap;
+}
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
diff --git a/app.js b/app.js
index ffdebfc..59b7322 100644
--- a/app.js
+++ b/app.js
@@ -253,6 +253,7 @@ router.on('/orders/:id', async (args) => {
+
Hochgeladene Fotos (${imagePhotos.length})
@@ -264,7 +265,11 @@ router.on('/orders/:id', async (args) => {
🎙 Sprachnotizen (${audioFiles.length})
- ${audioFiles.map(a => `
${escapeHtml(a.filename)}
`).join('')}
+ ${audioFiles.map(a => `
+ ${escapeHtml(a.filename)}
+
+
+
`).join('')}
` : ''}
@@ -283,6 +288,33 @@ router.on('/orders/:id', async (args) => {
// Sprachnotiz
document.getElementById('btn-voice').onclick = () => openVoiceModal(args.id);
+ // Neuen Bericht anlegen
+ document.getElementById('btn-new-report').onclick = () => openNewReportModal(args.id);
+
+ // Transkribieren
+ document.querySelectorAll('.audio-item .audio-transcribe').forEach(btn => {
+ btn.addEventListener('click', async (e) => {
+ const item = e.target.closest('.audio-item');
+ const rel = item.dataset.relpath;
+ btn.textContent = '⏳';
+ try {
+ const r = await api.transcribeAudio(rel);
+ const text = r.text || '(leer)';
+ const existing = item.querySelector('.audio-transcript');
+ if (existing) existing.remove();
+ const box = document.createElement('div');
+ box.className = 'audio-transcript';
+ box.textContent = text;
+ item.appendChild(box);
+ btn.textContent = '📝';
+ showToast('✓ Transkribiert');
+ } catch (err) {
+ btn.textContent = '📝';
+ showToast('Whisper-Fehler: ' + err.message, 'error');
+ }
+ });
+ });
+
// Audio-Files abspielen
document.querySelectorAll('.audio-item .audio-play').forEach(btn => {
btn.addEventListener('click', async (e) => {
@@ -814,6 +846,96 @@ function bindReportPageInteractions(reportId) {
});
}
+/* ============================================================
+ * NEW REPORT MODAL (Schnell-Bericht mit Meta + Vorlage + ODT)
+ * ============================================================ */
+async function openNewReportModal(orderId) {
+ // Templates parallel laden
+ let tpls = [], odts = [];
+ try { tpls = (await api.listTemplates()).templates || []; } catch (e) {}
+ try { odts = (await api.listOdtTemplates()).templates || []; } catch (e) {}
+
+ const modal = document.createElement('div');
+ modal.className = 'fullscreen-modal';
+ modal.innerHTML = `
+
+
+
+
+
+ ${tpls.length ? `
+
+
` : ''}
+
+
+
+
+
+
+
+ ${odts.length ? `
+
+
` : ''}
+
+ `;
+ document.body.appendChild(modal);
+
+ // ODT-Default vorauswählen
+ try {
+ const odtData = await api.listOdtTemplates();
+ if (odtData.default) {
+ const sel = modal.querySelector('#nr-odt');
+ if (sel) sel.value = odtData.default;
+ }
+ } catch (e) {}
+
+ modal.querySelector('#nr-close').onclick = () => modal.remove();
+ modal.querySelector('#nr-save').onclick = async () => {
+ const titel = modal.querySelector('#nr-titel').value.trim();
+ const tplSel = modal.querySelector('#nr-template');
+ const tpl = tplSel ? parseInt(tplSel.value, 10) : 0;
+ const format = modal.querySelector('#nr-format').value;
+ const orient = modal.querySelector('#nr-orient').value;
+ const odtSel = modal.querySelector('#nr-odt');
+ const odt = odtSel ? odtSel.value : '';
+ showToast('Lege Bericht an…');
+ try {
+ const res = await api.createReport({
+ element_type: 'order',
+ element_id: parseInt(orderId, 10),
+ titel: titel,
+ template_id: tpl,
+ page_format: format,
+ page_orientation: orient,
+ template_odt: odt,
+ });
+ showToast('✓ Bericht angelegt');
+ modal.remove();
+ router.go('#/reports/' + res.bericht_id);
+ } catch (e) {
+ showToast('Fehler: ' + e.message, 'error');
+ }
+ };
+}
+
/* ============================================================
* SEITEN-AKTIONEN MODAL (Notiz, Löschen, Vollbild)
* ============================================================ */
diff --git a/lib/api.js b/lib/api.js
index 795c97d..3521a8b 100644
--- a/lib/api.js
+++ b/lib/api.js
@@ -99,6 +99,21 @@
return request('/reports.php');
}
+ async function createReport(opts) {
+ return request('/reports.php?action=create', {
+ method: 'POST',
+ body: JSON.stringify(opts),
+ });
+ }
+
+ async function listTemplates() {
+ return request('/templates.php');
+ }
+
+ async function listOdtTemplates() {
+ return request('/odt_templates.php');
+ }
+
async function finalizeReport(id) {
return request('/reports.php?id=' + id + '&action=finalize', { method: 'POST' });
}
@@ -116,6 +131,13 @@
return request('/voice.php?order_id=' + orderId, { method: 'POST', body: fd });
}
+ async function transcribeAudio(relpath) {
+ return request('/transcribe.php', {
+ method: 'POST',
+ body: JSON.stringify({ relpath }),
+ });
+ }
+
async function uploadAnnotatedPhoto(orderId, fileBlob, filename) {
// Wie uploadOrderPhoto — Skizze ist schon ins Bild eingebrannt
return uploadOrderPhoto(orderId, fileBlob, filename);
@@ -204,8 +226,8 @@
login, logout,
listOrders, getOrder, listOrderPhotos, uploadOrderPhoto,
listCustomers, getCustomer,
- getReport, listReports, finalizeReport,
- deletePhoto, uploadVoiceNote, uploadAnnotatedPhoto,
+ getReport, listReports, createReport, listTemplates, listOdtTemplates, finalizeReport,
+ deletePhoto, uploadVoiceNote, transcribeAudio, uploadAnnotatedPhoto,
getPhotoBlobUrl, clearPhotoCache,
deletePage, updatePageNote, uploadSignature, getPdfBlobUrl, reorderPages,
};