/* Baustelle PWA Hauptlogik. Alle Routen + Views in einer Datei. */
const main = () => document.getElementById('main');
const title = (s) => document.getElementById('page-title').textContent = s;
window.showToast = function (msg, kind) {
const t = document.createElement('div');
t.className = 'toast' + (kind ? ' ' + kind : '');
t.textContent = msg;
document.getElementById('toast-container').appendChild(t);
setTimeout(() => t.remove(), 2800);
};
function showLoader(text) {
main().innerHTML = '
' + (text || 'Lade…') + '
';
}
function setNav(visible, active) {
const nav = document.getElementById('bottom-nav');
nav.style.display = visible ? '' : 'none';
nav.querySelectorAll('button').forEach(b => {
b.classList.toggle('active', b.dataset.route === active);
});
}
function setBack(visible, hash) {
const btn = document.getElementById('back-btn');
btn.style.display = visible ? '' : 'none';
btn.onclick = () => { if (hash) router.go(hash); else history.back(); };
}
/* ----- Auth-Check ----- */
async function ensureAuth() {
const t = await api.getToken();
if (!t) {
router.go('#/login');
return false;
}
return true;
}
/* ====== ROUTES ====== */
router.on('/login', async () => {
title('Anmelden');
setNav(false);
setBack(false);
main().innerHTML = `
`;
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
try {
await api.login(fd.get('login'), fd.get('password'));
showToast('Erfolgreich angemeldet');
router.go('#/orders');
} catch (err) {
showToast(err.message, 'error');
}
});
});
router.on('/orders', async () => {
if (!(await ensureAuth())) return;
title('Aufträge');
setNav(true, 'orders');
setBack(false);
showLoader('Lade Aufträge…');
try {
const data = await api.listOrders({ open: 1 });
if (!data.orders.length) {
main().innerHTML = '';
return;
}
const html = `
${renderOrderList(data.orders)}
`;
main().innerHTML = html;
document.querySelectorAll('.order-card').forEach(c => {
c.addEventListener('click', () => router.go('#/orders/' + c.dataset.id));
});
document.getElementById('order-search').addEventListener('input', async (e) => {
const q = e.target.value;
const d = await api.listOrders({ q, open: 1 });
document.getElementById('order-list').innerHTML = renderOrderList(d.orders);
document.querySelectorAll('.order-card').forEach(c => {
c.addEventListener('click', () => router.go('#/orders/' + c.dataset.id));
});
});
} catch (e) {
main().innerHTML = '';
}
});
function renderOrderList(orders) {
return orders.map(o => `
${escapeHtml(o.ref)}
${escapeHtml(o.customer.name || '')}
${escapeHtml((o.customer.zip || '') + ' ' + (o.customer.town || ''))}
${o.bericht_count > 0 ? `📑 ${o.bericht_count}` : ''}
`).join('');
}
router.on('/orders/:id', async (args) => {
if (!(await ensureAuth())) return;
setNav(true, 'orders');
setBack(true, '#/orders');
showLoader('Lade Auftrag…');
try {
const data = await api.getOrder(args.id);
const photos = await api.listOrderPhotos(args.id).catch(() => ({ photos: [] }));
title(data.order.ref);
main().innerHTML = `
Kunde
${escapeHtml(data.customer.name)}
${escapeHtml(data.customer.address || '')}
${escapeHtml((data.customer.zip || '') + ' ' + (data.customer.town || ''))}
${data.customer.phone ? `
📞 ${escapeHtml(data.customer.phone)}
` : ''}
${data.order.auftragsbeschreibung ? `
Beschreibung
${escapeHtml(data.order.auftragsbeschreibung)}
` : ''}
Hochgeladene Fotos (${photos.photos.length})
${photos.photos.map(p => `
`).join('')}
`;
loadThumbs();
const camInput = document.getElementById('camera-input');
const galInput = document.getElementById('gallery-input');
document.getElementById('btn-take-photo').onclick = () => camInput.click();
document.getElementById('btn-pick-photo').onclick = () => galInput.click();
async function handleFiles(files) {
for (const f of files) {
await uploadPhoto(args.id, f);
}
// Reload Photo-Liste
try {
const np = await api.listOrderPhotos(args.id);
document.getElementById('photo-grid').innerHTML =
np.photos.map(p => ``).join('');
loadThumbs();
} catch (e) {}
}
camInput.addEventListener('change', () => handleFiles(camInput.files));
galInput.addEventListener('change', () => handleFiles(galInput.files));
} catch (e) {
main().innerHTML = '';
}
});
async function uploadPhoto(orderId, file) {
showToast('Optimiere & sende ' + file.name);
const blob = await resizeImage(file, 2000);
if (!navigator.onLine) {
await offline.enqueuePhoto(orderId, blob, file.name);
showToast('Offline — Foto in Queue', 'warn');
return;
}
try {
await api.uploadOrderPhoto(orderId, blob, file.name);
showToast('✓ ' + file.name + ' hochgeladen');
} catch (e) {
await offline.enqueuePhoto(orderId, blob, file.name);
showToast('Upload fehlgeschlagen — in Queue', 'error');
}
}
async function resizeImage(file, maxSide) {
return new Promise((resolve) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
const scale = Math.min(1, maxSide / Math.max(img.width, img.height));
if (scale === 1) { resolve(file); return; }
const c = document.createElement('canvas');
c.width = Math.round(img.width * scale);
c.height = Math.round(img.height * scale);
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
c.toBlob(b => resolve(b || file), 'image/jpeg', 0.85);
};
img.onerror = () => { URL.revokeObjectURL(url); resolve(file); };
img.src = url;
});
}
/**
* Lädt alle sichtbaren Thumbnails im aktuellen Photo-Grid.
* Nutzt /api/photo.php mit JWT (Header kann
nicht schicken,
* deshalb Blob-URLs).
*/
async function loadThumbs() {
const thumbs = document.querySelectorAll('.photo-grid .thumb[data-relpath]');
for (const t of thumbs) {
const rel = t.dataset.relpath;
try {
// Erst Thumbnail versuchen (_small), bei Misserfolg das Original
let url = await api.getPhotoBlobUrl(rel, 'small');
if (!url) url = await api.getPhotoBlobUrl(rel);
if (url) {
t.innerHTML = '
';
} else {
t.innerHTML = '❌
';
}
} catch (e) {
t.innerHTML = '❌
';
}
}
}
router.on('/reports', async () => {
if (!(await ensureAuth())) return;
title('Berichte');
setNav(true, 'reports');
setBack(false);
main().innerHTML = '';
});
router.on('/settings', async () => {
if (!(await ensureAuth())) return;
title('Einstellungen');
setNav(true, 'settings');
setBack(false);
const user = await idb.get('user') || {};
main().innerHTML = `
Konto
${escapeHtml(user.name || user.login || 'Unbekannt')}
${escapeHtml(user.login || '')}
Baustelle PWA v1.0
`;
document.getElementById('btn-sync').onclick = async () => {
await offline.syncQueue();
showToast('Sync ausgelöst');
};
document.getElementById('btn-logout').onclick = async () => {
await api.logout();
router.go('#/login');
};
});
/* Bottom nav */
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('#bottom-nav button').forEach(b => {
b.addEventListener('click', () => router.go('#/' + b.dataset.route));
});
});
function escapeHtml(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}