All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 13s
Eddy: „so dass es gut aussieht", zu rein weißen Symbolen dann „viel zu schlicht". Emojis erscheinen je nach Handy als Kästchen. - icon() in app.js (Material-Symbole), Farbe je Symbol in app.css (.ikon-<name>): große Knöpfe = weißes Symbol im farbigen runden Feld (Kamera blau, Galerie grün, Sprachnotiz rot, Notizen gelb, Material orange, Bericht lila, Lieferungen türkis …), kleine Knöpfe = farbiges Symbol (Teilen blau, Löschen rot, Stift/Blitz/ Hilfe gelb, Download/Telefon grün). - Untere Leiste: farbiges Symbol über dem Wort, Heute als Sonne, Zahnrad heißt „Einstellungen". - Titel und Überschriften ohne Emoji; Telefonnummer lesbar. - .icon-btn[hidden] bleibt versteckt (inline-flex hätte hidden ausgehebelt). Im Android-Emulator angesehen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
185 lines
12 KiB
PHP
185 lines
12 KiB
PHP
<?php
|
|
/* Baustelle PWA — Einstiegsseite.
|
|
* Cache-Versionierung via filemtime() nach Pattern claude-db #201.
|
|
* Bei jedem Deploy ändern sich die mtimes → neue URLs → Browser holt frische Files.
|
|
*/
|
|
$cssVersion = @filemtime(__DIR__.'/app.css') ?: time();
|
|
$jsVersion = max(
|
|
@filemtime(__DIR__.'/app.js') ?: time(),
|
|
@filemtime(__DIR__.'/lib/idb.js') ?: time(),
|
|
@filemtime(__DIR__.'/lib/api.js') ?: time(),
|
|
@filemtime(__DIR__.'/lib/offline.js')?: time(),
|
|
@filemtime(__DIR__.'/lib/router.js') ?: time()
|
|
);
|
|
$manifestVersion = @filemtime(__DIR__.'/manifest.webmanifest') ?: time();
|
|
$swVersion = max($cssVersion, $jsVersion, $manifestVersion);
|
|
|
|
// Einstiegsseite NIE cachen
|
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
|
header('Pragma: no-cache');
|
|
header('Expires: 0');
|
|
?>
|
|
<!doctype html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
|
<meta name="theme-color" content="#1a1a1f">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<title>Baustelle</title>
|
|
<link rel="manifest" href="manifest.webmanifest?v=<?php echo $manifestVersion; ?>">
|
|
<link rel="icon" type="image/svg+xml" href="icons/icon.svg">
|
|
<link rel="icon" type="image/png" sizes="192x192" href="icons/icon-192.png">
|
|
<link rel="icon" type="image/png" sizes="512x512" href="icons/icon-512.png">
|
|
<link rel="apple-touch-icon" href="icons/icon-192.png">
|
|
<link rel="stylesheet" href="app.css?v=<?php echo $cssVersion; ?>">
|
|
</head>
|
|
<body>
|
|
|
|
<div id="app">
|
|
<header id="topbar">
|
|
<button id="back-btn" class="back-btn" style="display:none" aria-label="Zurück"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg><span>Zurück</span></button>
|
|
<h1 id="page-title">Baustelle</h1>
|
|
<button id="install-btn" class="icon-btn" title="Als App installieren" aria-label="Als App installieren" style="display:none"><svg class="ikon ikon-installieren" viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14zm-1-6h-3V8h-2v5H8l4 4 4-4z"/></svg></button>
|
|
<button id="help-btn" class="icon-btn" title="Hilfe" aria-label="Hilfe"><svg class="ikon ikon-hilfe" viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg></button>
|
|
<span id="status-badge">🟢</span>
|
|
</header>
|
|
|
|
<main id="main"></main>
|
|
|
|
<nav id="bottom-nav" style="display:none">
|
|
<button data-route="today"><svg class="ikon ikon-heute" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm9-9.95h-2V3.5h2V.55zm7.45 3.91l-1.41-1.41-1.79 1.79 1.41 1.41 1.79-1.79zm-3.21 13.7l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM20 10.5v2h3v-2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm-1 16.95h2V19.5h-2v2.95zm-7.45-3.91l1.41 1.41 1.79-1.8-1.41-1.41-1.79 1.8z"/></svg><span>Heute</span></button>
|
|
<button data-route="orders" class="active"><svg class="ikon ikon-auftraege" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg><span>Aufträge</span></button>
|
|
<button data-route="customers"><svg class="ikon ikon-kunden" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg><span>Kunden</span></button>
|
|
<button data-route="reports"><svg class="ikon ikon-berichte" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg><span>Berichte</span></button>
|
|
<button data-route="settings" aria-label="Einstellungen"><svg class="ikon ikon-einstellungen" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg><span>Einstellungen</span></button>
|
|
</nav>
|
|
</div>
|
|
|
|
<button id="fab-new-order" class="fab" title="Neuer Auftrag" aria-label="Neuer Auftrag" hidden>+</button>
|
|
|
|
<div id="toast-container"></div>
|
|
|
|
<script>
|
|
console.log('[Init] PDF.js wird geladen von: /custom/bericht/js/lib/pdf.min.js');
|
|
</script>
|
|
<script src="/custom/bericht/js/lib/pdf.min.js?v=<?php echo $jsVersion; ?>"></script>
|
|
<script>
|
|
// PDF.js wird als pdfjsLib exportiert, nicht als pdfjs
|
|
const pdfjs = window.pdfjsLib;
|
|
console.log('[PDF.js] Nach Script-Load: typeof pdfjs =', typeof pdfjs);
|
|
// PDF.js Worker setzen NACHDEM pdf.min.js geladen wurde
|
|
if (typeof pdfjs !== 'undefined') {
|
|
pdfjs.GlobalWorkerOptions.workerSrc = '/custom/bericht/js/lib/pdf.worker.min.js?v=<?php echo $jsVersion; ?>';
|
|
console.log('[PDF.js] ✓ Geladen! Worker:', pdfjs.GlobalWorkerOptions.workerSrc);
|
|
} else {
|
|
console.error('[PDF.js] ❌ NICHT GELADEN!');
|
|
}
|
|
</script>
|
|
<script src="lib/idb.js?v=<?php echo $jsVersion; ?>"></script>
|
|
<script src="lib/api.js?v=<?php echo $jsVersion; ?>"></script>
|
|
<script src="lib/offline.js?v=<?php echo $jsVersion; ?>"></script>
|
|
<script src="lib/router.js?v=<?php echo $jsVersion; ?>"></script>
|
|
<script src="app.js?v=<?php echo $jsVersion; ?>"></script>
|
|
<!-- awlauth WebAuthn/Passkey (zentrales Modul, same-origin unter /custom/awlauth) -->
|
|
<script>window.AWLAUTH_BASE = '/custom/awlauth';</script>
|
|
<script src="/custom/awlauth/js/awlauth-webauthn.js?v=<?php echo @filemtime($_SERVER['DOCUMENT_ROOT'].'/custom/awlauth/js/awlauth-webauthn.js') ?: $jsVersion; ?>"></script>
|
|
<script>
|
|
/* PWA Install Prompt abfangen */
|
|
let deferredInstallPrompt = null;
|
|
window.addEventListener('beforeinstallprompt', function (e) {
|
|
e.preventDefault();
|
|
deferredInstallPrompt = e;
|
|
var btn = document.getElementById('install-btn');
|
|
if (btn) btn.style.display = '';
|
|
console.log('PWA: Install-Prompt ist verfügbar');
|
|
});
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var btn = document.getElementById('install-btn');
|
|
if (btn) btn.addEventListener('click', async function () {
|
|
if (!deferredInstallPrompt) {
|
|
alert('Installation nicht verfügbar. Falls du die App bereits installiert hast, öffne sie über das Home-Screen-Icon.\n\nAuf iPhone: Safari → Teilen → "Zum Home-Bildschirm".');
|
|
return;
|
|
}
|
|
deferredInstallPrompt.prompt();
|
|
var choice = await deferredInstallPrompt.userChoice;
|
|
if (choice.outcome === 'accepted') {
|
|
btn.style.display = 'none';
|
|
}
|
|
deferredInstallPrompt = null;
|
|
});
|
|
});
|
|
window.addEventListener('appinstalled', function () {
|
|
var btn = document.getElementById('install-btn');
|
|
if (btn) btn.style.display = 'none';
|
|
console.log('PWA: Installation abgeschlossen');
|
|
});
|
|
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', function () {
|
|
navigator.serviceWorker.register('sw.js?v=<?php echo $swVersion; ?>').then(function (reg) {
|
|
// Periodisch updates prüfen
|
|
setInterval(function () { reg.update(); }, 60000);
|
|
// Bei Tab-Wechsel zurück: sofort prüfen
|
|
document.addEventListener('visibilitychange', function () {
|
|
if (document.visibilityState === 'visible') reg.update();
|
|
});
|
|
// Neuer SW installed → sofort aktivieren
|
|
reg.addEventListener('updatefound', function () {
|
|
var nw = reg.installing;
|
|
if (!nw) return;
|
|
nw.addEventListener('statechange', function () {
|
|
if (nw.state === 'installed' && navigator.serviceWorker.controller) {
|
|
nw.postMessage({ type: 'SKIP_WAITING' });
|
|
}
|
|
});
|
|
});
|
|
}).catch(function (e) { console.warn('SW reg failed', e); });
|
|
|
|
/* Dem Service Worker melden, was diese Seite tatsaechlich geladen hat, damit er
|
|
es nachcachen kann. Noetig, weil der Cache nach jedem Deploy einen neuen Namen
|
|
traegt und leer startet — und weil die Asset-URLs ein ?v=<mtime> tragen, laesst
|
|
sich im Worker keine feste Dateiliste hinterlegen (KB #1041). Ohne das steht,
|
|
wer die neue Fassung laedt und dann ins Funkloch faehrt, vor einer toten App. */
|
|
navigator.serviceWorker.ready.then(function (reg) {
|
|
var ziel = reg.active || navigator.serviceWorker.controller;
|
|
if (!ziel) return;
|
|
var urls = performance.getEntriesByType('resource')
|
|
.map(function (r) { return r.name; })
|
|
.filter(function (u) {
|
|
return u.indexOf(location.origin) === 0
|
|
&& /\.(css|js|woff2?|png|svg|ico|webmanifest)(\?|$)/i.test(u);
|
|
});
|
|
urls.push(location.origin + location.pathname);
|
|
ziel.postMessage({ type: 'PRECACHE', urls: urls });
|
|
}).catch(function () {});
|
|
|
|
/* Controller-Change → einmal neu laden — aber nicht mitten in der Arbeit.
|
|
Frueher kam der Reload sofort: offene Kamera zu, getippte Notiz weg, halbe
|
|
Unterschrift weg (und lagen noch Fotos in der Warteschlange, zeigte der Browser
|
|
dazu seinen eigenen "Seite verlassen?"-Dialog). Jetzt fragt die Seite app.js
|
|
(window.appBusy) und holt den Reload nach, sobald nichts mehr offen ist —
|
|
spaetestens, wenn die App in den Hintergrund geht und dabei nichts offen ist. */
|
|
var reloadWanted = false, reloaded = false;
|
|
function tryReload() {
|
|
if (!reloadWanted || reloaded) return;
|
|
var busy = false;
|
|
try { busy = (typeof window.appBusy === 'function') && window.appBusy(); } catch (_) {}
|
|
if (busy) return;
|
|
reloaded = true;
|
|
window.__selfReload = true; // offline.js: eigener Reload, keine beforeunload-Warnung
|
|
window.location.reload();
|
|
}
|
|
navigator.serviceWorker.addEventListener('controllerchange', function () {
|
|
reloadWanted = true;
|
|
tryReload();
|
|
});
|
|
setInterval(tryReload, 3000);
|
|
document.addEventListener('visibilitychange', tryReload);
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|