@@ -291,6 +390,9 @@ router.on('/orders/:id', async (args) => {
// Neuen Bericht anlegen
document.getElementById('btn-new-report').onclick = () => openNewReportModal(args.id);
+ // Materialliste
+ document.getElementById('btn-material').onclick = () => openMaterialModal('order', args.id);
+
// Transkribieren
document.querySelectorAll('.audio-item .audio-transcribe').forEach(btn => {
btn.addEventListener('click', async (e) => {
@@ -694,6 +796,8 @@ router.on('/settings', async () => {
const user = await idb.get('user') || {};
const pinEnabled = !!(await idb.get('pin_hash'));
+ const ntfyTopic = await idb.get('ntfy_topic') || '';
+ const ntfyServer = await idb.get('ntfy_server') || 'https://notify.data-it-solution.de';
main().innerHTML = `
Sicherheit
PIN-Schutz beim App-Start — nützlich falls das Handy verloren geht.
@@ -726,6 +840,42 @@ router.on('/settings', async () => {
await idb.del('pin_salt');
router.go('#/login');
};
+ // ntfy-Settings
+ document.getElementById('btn-ntfy-save').onclick = async () => {
+ const srv = document.getElementById('ntfy-server').value.trim().replace(/\/$/, '');
+ const topic = document.getElementById('ntfy-topic').value.trim();
+ await idb.set('ntfy_server', srv);
+ await idb.set('ntfy_topic', topic);
+ if (topic && 'Notification' in window) {
+ if (Notification.permission !== 'granted') {
+ const perm = await Notification.requestPermission();
+ if (perm !== 'granted') {
+ document.getElementById('ntfy-status').textContent = 'Benachrichtigungen abgelehnt — keine Notifications';
+ return;
+ }
+ }
+ startNtfySubscription(srv, topic);
+ document.getElementById('ntfy-status').textContent = '✓ Abonniert: ' + topic;
+ showToast('✓ Benachrichtigungen aktiviert');
+ } else {
+ stopNtfySubscription();
+ document.getElementById('ntfy-status').textContent = 'Kein Topic gesetzt';
+ }
+ };
+ document.getElementById('btn-ntfy-test').onclick = async () => {
+ const srv = document.getElementById('ntfy-server').value.trim().replace(/\/$/, '');
+ const topic = document.getElementById('ntfy-topic').value.trim();
+ if (!srv || !topic) { showToast('Server + Topic eingeben', 'error'); return; }
+ try {
+ await fetch(srv + '/' + topic, {
+ method: 'POST',
+ headers: { 'Title': 'Baustelle', 'Tags': 'construction_worker' },
+ body: 'Test-Nachricht ' + new Date().toLocaleTimeString('de-DE'),
+ });
+ showToast('✓ Gesendet');
+ } catch (e) { showToast('Fehler: ' + e.message, 'error'); }
+ };
+
document.getElementById('pin-toggle').addEventListener('change', async (e) => {
if (e.target.checked) {
// PIN setzen
@@ -847,6 +997,91 @@ function bindReportPageInteractions(reportId) {
});
}
+/* ============================================================
+ * MATERIALLISTE MODAL
+ * ============================================================ */
+async function openMaterialModal(elementType, elementId) {
+ const modal = document.createElement('div');
+ modal.className = 'fullscreen-modal';
+ modal.innerHTML = `
+
+
+ `;
+ document.body.appendChild(modal);
+
+ modal.querySelector('#mt-close').onclick = () => modal.remove();
+
+ async function reload() {
+ try {
+ const d = await api.listMaterials(elementType, elementId);
+ const listEl = modal.querySelector('#mt-list');
+ if (!d.materials.length) {
+ listEl.innerHTML = '
Noch keine Einträge
';
+ return;
+ }
+ listEl.innerHTML = d.materials.map(m => `
+
+
+
${escapeHtml(m.label)}
+
${m.qty} ${escapeHtml(m.unit)}${m.note ? ' · ' + escapeHtml(m.note) : ''}
+
+
+
+ `).join('');
+ listEl.querySelectorAll('.mt-del').forEach(btn => {
+ btn.addEventListener('click', async (e) => {
+ const card = e.target.closest('.mini-card');
+ const id = card.dataset.id;
+ if (!confirm('Eintrag löschen?')) return;
+ await api.deleteMaterial(id);
+ reload();
+ });
+ });
+ } catch (e) {
+ modal.querySelector('#mt-list').innerHTML = '
Fehler: ' + escapeHtml(e.message) + '
';
+ }
+ }
+ reload();
+
+ modal.querySelector('#mt-add').onclick = async () => {
+ const label = modal.querySelector('#mt-label').value.trim();
+ if (!label) { showToast('Bezeichnung eingeben', 'error'); return; }
+ const qty = parseFloat(modal.querySelector('#mt-qty').value) || 1;
+ const unit = modal.querySelector('#mt-unit').value;
+ const note = modal.querySelector('#mt-note').value.trim();
+ try {
+ await api.addMaterial(elementType, elementId, { label, qty, unit, note });
+ modal.querySelector('#mt-label').value = '';
+ modal.querySelector('#mt-qty').value = '1';
+ modal.querySelector('#mt-note').value = '';
+ showToast('✓ Hinzugefügt');
+ reload();
+ } catch (e) { showToast('Fehler: ' + e.message, 'error'); }
+ };
+}
+
/* ============================================================
* NEW REPORT MODAL (Schnell-Bericht mit Meta + Vorlage + ODT)
* ============================================================ */
@@ -1393,6 +1628,9 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
+
+
+
@@ -1440,6 +1678,9 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
let drawing = false;
let startX = 0, startY = 0;
const history = [canvas.toDataURL()];
+ // Mess-Skala: pixelsPerUnit + unit (z. B. 32.5 px/cm)
+ let calibration = null; // { pxPerUnit: number, unit: 'cm'|'m'|'mm' }
+ let calibStep = 0; // 0 = nicht im Prozess, 1 = warte auf zweiten Punkt
function pushHistory() {
history.push(canvas.toDataURL());
@@ -1504,6 +1745,18 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
return;
}
+ // Calibrate: Drag-Line zwischen zwei Punkten, dann Länge eingeben
+ if (tool === 'calibrate' || tool === 'measure') {
+ drawing = true;
+ snapshotBeforeShape = canvas.toDataURL();
+ ctx.strokeStyle = (tool === 'calibrate') ? '#f0ad4e' : '#5cb85c';
+ ctx.fillStyle = ctx.strokeStyle;
+ ctx.lineWidth = Math.max(3, lineWidth);
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+ return;
+ }
+
drawing = true;
ctx.strokeStyle = color;
ctx.fillStyle = color;
@@ -1522,6 +1775,36 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
if (!drawing) return;
e.preventDefault();
const p = getPos(e);
+ lastMoveX = p.x; lastMoveY = p.y;
+
+ if (tool === 'calibrate' || tool === 'measure') {
+ restoreSnapshot(snapshotBeforeShape).then(() => {
+ const col = (tool === 'calibrate') ? '#f0ad4e' : '#5cb85c';
+ ctx.strokeStyle = col;
+ ctx.fillStyle = col;
+ ctx.lineWidth = Math.max(3, lineWidth);
+ ctx.lineCap = 'round';
+ // Mess-Linie
+ ctx.beginPath();
+ ctx.moveTo(startX, startY);
+ ctx.lineTo(p.x, p.y);
+ ctx.stroke();
+ // End-Punkte als kleine Kreuze
+ drawCross(ctx, startX, startY, 10);
+ drawCross(ctx, p.x, p.y, 10);
+ // Live-Distanz bei measure
+ if (tool === 'measure' && calibration) {
+ const dx = p.x - startX, dy = p.y - startY;
+ const dist_px = Math.sqrt(dx * dx + dy * dy);
+ const real = (dist_px / calibration.pxPerUnit);
+ const mid_x = (startX + p.x) / 2;
+ const mid_y = (startY + p.y) / 2 - 15;
+ drawDistanceLabel(ctx, mid_x, mid_y, real, calibration.unit);
+ }
+ });
+ return;
+ }
+
if (tool === 'pen') {
ctx.lineTo(p.x, p.y);
ctx.stroke();
@@ -1562,13 +1845,96 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {
}
}
- function endDraw() {
+ async function endDraw(e) {
if (!drawing) return;
drawing = false;
+ const tool_now = tool;
+
+ if (tool_now === 'calibrate') {
+ // Länge vom User abfragen
+ const p = e && (e.changedTouches ? { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY } : { x: e.clientX, y: e.clientY });
+ // fallback: aktuelle endposition haben wir nicht mehr exakt — wir nutzen die letzte gezeichnete position
+ const dx_px = lastMoveX - startX, dy_px = lastMoveY - startY;
+ const dist_px = Math.sqrt(dx_px * dx_px + dy_px * dy_px);
+ if (dist_px < 5) { // zu kurz
+ await restoreSnapshot(snapshotBeforeShape);
+ snapshotBeforeShape = null;
+ showToast('Zu kurz — 2 Punkte mit Abstand wählen', 'error');
+ return;
+ }
+ const input = prompt('Reale Länge dieser Strecke:\nFormat: Zahl + Einheit (z. B. "50 cm", "1.5 m", "250 mm")');
+ if (!input) {
+ await restoreSnapshot(snapshotBeforeShape);
+ snapshotBeforeShape = null;
+ return;
+ }
+ const m = input.match(/^\s*([0-9]+(?:[.,][0-9]+)?)\s*(mm|cm|m)?\s*$/i);
+ if (!m) {
+ await restoreSnapshot(snapshotBeforeShape);
+ snapshotBeforeShape = null;
+ alert('Ungültiges Format. Beispiel: 1.5 m');
+ return;
+ }
+ const val = parseFloat(m[1].replace(',', '.'));
+ const unit = (m[2] || 'cm').toLowerCase();
+ // Umrechnung auf mm intern? Nein: pxPerUnit in der eingegebenen Einheit halten
+ calibration = { pxPerUnit: dist_px / val, unit };
+ showToast('✓ Kalibriert: ' + val + ' ' + unit + ' = ' + dist_px.toFixed(0) + ' px');
+ // Kalibrierungslinie entfernen (zeichnen war nur Helper)
+ await restoreSnapshot(snapshotBeforeShape);
+ snapshotBeforeShape = null;
+ // Automatisch auf Measure-Tool wechseln
+ const mt = document.querySelector('.sk-tool[data-tool="measure"]');
+ if (mt) mt.click();
+ return;
+ }
+
+ if (tool_now === 'measure') {
+ if (!calibration) {
+ await restoreSnapshot(snapshotBeforeShape);
+ snapshotBeforeShape = null;
+ alert('Zuerst kalibrieren! Tap auf 📏⚙, zwei Punkte mit bekannter Länge wählen.');
+ return;
+ }
+ // Die letzte Preview-Zeichnung ist jetzt schon auf dem Canvas — das ist unser finales Ergebnis
+ snapshotBeforeShape = null;
+ pushHistory();
+ return;
+ }
+
snapshotBeforeShape = null;
pushHistory();
}
+ // Track der letzten Move-Position für calibrate-endDraw
+ let lastMoveX = 0, lastMoveY = 0;
+
+ function drawCross(ctx, x, y, size) {
+ ctx.save();
+ ctx.beginPath();
+ ctx.moveTo(x - size / 2, y);
+ ctx.lineTo(x + size / 2, y);
+ ctx.moveTo(x, y - size / 2);
+ ctx.lineTo(x, y + size / 2);
+ ctx.stroke();
+ ctx.restore();
+ }
+
+ function drawDistanceLabel(ctx, x, y, value, unit) {
+ const text = value.toFixed(2) + ' ' + unit;
+ ctx.save();
+ ctx.font = 'bold 28px -apple-system, sans-serif';
+ const w = ctx.measureText(text).width + 16;
+ const h = 36;
+ ctx.fillStyle = 'rgba(0,0,0,0.75)';
+ ctx.fillRect(x - w / 2, y - h / 2, w, h);
+ ctx.fillStyle = '#5cb85c';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText(text, x, y);
+ ctx.restore();
+ }
+
canvas.addEventListener('mousedown', startDraw);
canvas.addEventListener('mousemove', moveDraw);
canvas.addEventListener('mouseup', endDraw);
diff --git a/index.php b/index.php
index 81c28dd..19ce34d 100644
--- a/index.php
+++ b/index.php
@@ -50,6 +50,7 @@ header('Expires: 0');