|
|
|
@ -0,0 +1,885 @@
|
|
|
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
|
|
<html lang="de">
|
|
|
|
|
|
|
|
<head>
|
|
|
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
|
|
|
|
|
|
<meta name="theme-color" content="#111418">
|
|
|
|
|
|
|
|
<title>Monitor-Check</title>
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
|
|
:root{
|
|
|
|
|
|
|
|
--bg:#f5f6f7;
|
|
|
|
|
|
|
|
--surface:#ffffff;
|
|
|
|
|
|
|
|
--line:#d9dce0;
|
|
|
|
|
|
|
|
--line-strong:#b7bcc3;
|
|
|
|
|
|
|
|
--ink:#111418;
|
|
|
|
|
|
|
|
--ink-2:#5a616b;
|
|
|
|
|
|
|
|
--ink-3:#8b929b;
|
|
|
|
|
|
|
|
--accent:#1b57d6;
|
|
|
|
|
|
|
|
--ok:#1a7f45; --ok-bg:#e4f3ea;
|
|
|
|
|
|
|
|
--teil:#9a6400; --teil-bg:#fdf1d8;
|
|
|
|
|
|
|
|
--fehler:#b3261e; --fehler-bg:#fbe5e3;
|
|
|
|
|
|
|
|
--aus:#5a616b; --aus-bg:#eceef0;
|
|
|
|
|
|
|
|
--offen:#5a616b; --offen-bg:#ffffff;
|
|
|
|
|
|
|
|
--r:10px;
|
|
|
|
|
|
|
|
--tap:48px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
|
|
|
|
|
|
|
/* muss vor den Layoutregeln stehen: display:block wuerde [hidden] sonst aushebeln */
|
|
|
|
|
|
|
|
[hidden]{display:none !important}
|
|
|
|
|
|
|
|
html,body{margin:0;padding:0}
|
|
|
|
|
|
|
|
body{
|
|
|
|
|
|
|
|
background:var(--bg);color:var(--ink);
|
|
|
|
|
|
|
|
font:16px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
|
|
|
|
|
|
padding-bottom:calc(72px + env(safe-area-inset-bottom));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-variant-numeric:tabular-nums}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
header{
|
|
|
|
|
|
|
|
position:sticky;top:0;z-index:30;background:var(--surface);
|
|
|
|
|
|
|
|
border-bottom:1px solid var(--line);
|
|
|
|
|
|
|
|
padding:calc(10px + env(safe-area-inset-top)) 14px 10px;
|
|
|
|
|
|
|
|
display:flex;align-items:center;gap:10px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
header h1{font-size:16px;font-weight:650;margin:0;letter-spacing:-.01em;flex:1}
|
|
|
|
|
|
|
|
.netstate{font-size:11px;padding:3px 8px;border-radius:999px;border:1px solid var(--line);color:var(--ink-2);white-space:nowrap}
|
|
|
|
|
|
|
|
.netstate.off{background:var(--fehler-bg);border-color:#f0c4c0;color:var(--fehler)}
|
|
|
|
|
|
|
|
.netstate.queue{background:var(--teil-bg);border-color:#eddaa8;color:var(--teil)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main{padding:14px;max-width:720px;margin:0 auto}
|
|
|
|
|
|
|
|
.view{display:none}
|
|
|
|
|
|
|
|
.view.on{display:block}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nav{
|
|
|
|
|
|
|
|
position:fixed;left:0;right:0;bottom:0;z-index:30;
|
|
|
|
|
|
|
|
background:var(--surface);border-top:1px solid var(--line);
|
|
|
|
|
|
|
|
display:grid;grid-template-columns:repeat(3,1fr);
|
|
|
|
|
|
|
|
padding-bottom:env(safe-area-inset-bottom);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
nav button{
|
|
|
|
|
|
|
|
appearance:none;border:0;background:none;color:var(--ink-3);
|
|
|
|
|
|
|
|
padding:12px 4px;font:inherit;font-size:12px;font-weight:550;
|
|
|
|
|
|
|
|
display:flex;flex-direction:column;align-items:center;gap:4px;cursor:pointer;
|
|
|
|
|
|
|
|
min-height:var(--tap);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
nav button svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
|
|
|
|
|
|
|
nav button.on{color:var(--accent)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);padding:14px;margin-bottom:12px}
|
|
|
|
|
|
|
|
.card h2{font-size:13px;font-weight:650;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-2);margin:0 0 12px}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
label{display:block;font-size:13px;font-weight:600;color:var(--ink-2);margin:0 0 6px}
|
|
|
|
|
|
|
|
input[type=text],input[type=url],input[type=password],textarea,select{
|
|
|
|
|
|
|
|
width:100%;padding:12px;font:inherit;color:var(--ink);
|
|
|
|
|
|
|
|
background:#fff;border:1px solid var(--line-strong);border-radius:8px;min-height:var(--tap);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
input:focus,textarea:focus,select:focus{outline:2px solid var(--accent);outline-offset:-1px;border-color:var(--accent)}
|
|
|
|
|
|
|
|
textarea{min-height:76px;resize:vertical}
|
|
|
|
|
|
|
|
.field{margin-bottom:14px}
|
|
|
|
|
|
|
|
.hint{font-size:12px;color:var(--ink-3);margin-top:5px}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
button.btn{
|
|
|
|
|
|
|
|
appearance:none;font:inherit;font-weight:600;cursor:pointer;
|
|
|
|
|
|
|
|
border:1px solid var(--line-strong);background:#fff;color:var(--ink);
|
|
|
|
|
|
|
|
border-radius:8px;padding:13px 16px;min-height:var(--tap);width:100%;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
button.btn.primary{background:var(--ink);border-color:var(--ink);color:#fff}
|
|
|
|
|
|
|
|
button.btn.ghost{background:transparent}
|
|
|
|
|
|
|
|
button.btn.danger{color:var(--fehler);border-color:#f0c4c0}
|
|
|
|
|
|
|
|
button.btn.danger:active{background:var(--fehler-bg)}
|
|
|
|
|
|
|
|
button.btn:disabled{opacity:.5;cursor:default}
|
|
|
|
|
|
|
|
.btnrow{display:flex;gap:8px}
|
|
|
|
|
|
|
|
.btnrow .btn{width:auto;flex:1}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Status-Schalter */
|
|
|
|
|
|
|
|
.triple{display:flex;flex-direction:column;gap:10px}
|
|
|
|
|
|
|
|
.trow{display:grid;grid-template-columns:78px 1fr;align-items:center;gap:10px}
|
|
|
|
|
|
|
|
.trow > span{font-size:14px;font-weight:600}
|
|
|
|
|
|
|
|
.seg{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--line-strong);border-radius:8px;overflow:hidden}
|
|
|
|
|
|
|
|
.seg button{
|
|
|
|
|
|
|
|
appearance:none;border:0;border-right:1px solid var(--line-strong);background:#fff;
|
|
|
|
|
|
|
|
font:inherit;font-size:14px;font-weight:600;color:var(--ink-2);
|
|
|
|
|
|
|
|
padding:12px 4px;min-height:var(--tap);cursor:pointer;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.seg button:last-child{border-right:0}
|
|
|
|
|
|
|
|
.seg button[aria-pressed=true][data-v=JA]{background:var(--ok-bg);color:var(--ok);box-shadow:inset 0 0 0 2px var(--ok)}
|
|
|
|
|
|
|
|
.seg button[aria-pressed=true][data-v=NEIN]{background:var(--fehler-bg);color:var(--fehler);box-shadow:inset 0 0 0 2px var(--fehler)}
|
|
|
|
|
|
|
|
.seg button[aria-pressed=true][data-v="n.A."]{background:var(--aus-bg);color:var(--aus);box-shadow:inset 0 0 0 2px var(--aus)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.chip{
|
|
|
|
|
|
|
|
display:inline-flex;align-items:center;gap:6px;
|
|
|
|
|
|
|
|
font-size:12px;font-weight:650;padding:4px 10px;border-radius:999px;border:1px solid transparent;white-space:nowrap;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.chip::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}
|
|
|
|
|
|
|
|
.s-ok{background:var(--ok-bg);color:var(--ok)}
|
|
|
|
|
|
|
|
.s-teil{background:var(--teil-bg);color:var(--teil)}
|
|
|
|
|
|
|
|
.s-fehler{background:var(--fehler-bg);color:var(--fehler)}
|
|
|
|
|
|
|
|
.s-aus{background:var(--aus-bg);color:var(--aus)}
|
|
|
|
|
|
|
|
.s-offen{background:#fff;color:var(--offen);border-color:var(--line-strong)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.preview{
|
|
|
|
|
|
|
|
display:flex;align-items:center;justify-content:space-between;gap:10px;
|
|
|
|
|
|
|
|
padding:12px;border:1px dashed var(--line-strong);border-radius:8px;margin-top:12px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.preview small{color:var(--ink-3);font-size:12px}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Foto */
|
|
|
|
|
|
|
|
.photo{position:relative;border:1px dashed var(--line-strong);border-radius:8px;overflow:hidden;background:#fff}
|
|
|
|
|
|
|
|
.photo img{display:block;width:100%;max-height:260px;object-fit:cover}
|
|
|
|
|
|
|
|
.photo .ph{padding:26px 12px;text-align:center;color:var(--ink-3);font-size:14px}
|
|
|
|
|
|
|
|
.photo .rm{position:absolute;top:8px;right:8px;width:34px;height:34px;border-radius:50%;border:0;background:rgba(17,20,24,.78);color:#fff;font-size:18px;cursor:pointer;line-height:1}
|
|
|
|
|
|
|
|
input[type=file]{display:none}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Liste */
|
|
|
|
|
|
|
|
.toolbar{display:flex;gap:8px;margin-bottom:10px}
|
|
|
|
|
|
|
|
.toolbar input{flex:1}
|
|
|
|
|
|
|
|
.filters{display:flex;gap:6px;overflow-x:auto;padding-bottom:10px;margin-bottom:4px;-webkit-overflow-scrolling:touch}
|
|
|
|
|
|
|
|
.filters button{
|
|
|
|
|
|
|
|
appearance:none;font:inherit;font-size:13px;font-weight:600;white-space:nowrap;cursor:pointer;
|
|
|
|
|
|
|
|
border:1px solid var(--line-strong);background:#fff;color:var(--ink-2);border-radius:999px;padding:8px 13px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.filters button.on{background:var(--ink);border-color:var(--ink);color:#fff}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.item{
|
|
|
|
|
|
|
|
display:flex;gap:12px;align-items:center;width:100%;text-align:left;cursor:pointer;
|
|
|
|
|
|
|
|
background:var(--surface);border:1px solid var(--line);border-radius:var(--r);
|
|
|
|
|
|
|
|
padding:10px;margin-bottom:8px;font:inherit;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.item .thumb{width:56px;height:56px;border-radius:6px;background:var(--aus-bg);flex:0 0 auto;object-fit:cover}
|
|
|
|
|
|
|
|
.item .thumb.empty{display:flex;align-items:center;justify-content:center;color:var(--ink-3);font-size:11px}
|
|
|
|
|
|
|
|
.item .meta{flex:1;min-width:0}
|
|
|
|
|
|
|
|
.item .idline{font-size:11px;color:var(--ink-3);letter-spacing:.04em}
|
|
|
|
|
|
|
|
.item .name{font-weight:600;font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
|
|
|
|
|
|
.item .sub{font-size:12px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stats{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-bottom:12px}
|
|
|
|
|
|
|
|
.stat{background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:8px 4px;text-align:center}
|
|
|
|
|
|
|
|
.stat b{display:block;font-size:18px;font-variant-numeric:tabular-nums}
|
|
|
|
|
|
|
|
.stat span{font-size:10px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.04em}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Detail-Sheet */
|
|
|
|
|
|
|
|
.sheet{position:fixed;inset:0;z-index:50;display:none}
|
|
|
|
|
|
|
|
.sheet.on{display:block}
|
|
|
|
|
|
|
|
.sheet .bd{position:absolute;inset:0;background:rgba(17,20,24,.45)}
|
|
|
|
|
|
|
|
.sheet .pn{
|
|
|
|
|
|
|
|
position:absolute;left:0;right:0;bottom:0;max-height:92vh;overflow:auto;
|
|
|
|
|
|
|
|
background:var(--bg);border-radius:16px 16px 0 0;padding:16px 14px calc(20px + env(safe-area-inset-bottom));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.sheet .grip{width:38px;height:4px;border-radius:2px;background:var(--line-strong);margin:0 auto 14px}
|
|
|
|
|
|
|
|
@media(min-width:720px){.sheet .pn{max-width:640px;margin:0 auto;border-radius:16px;bottom:auto;top:50%;transform:translateY(-50%)}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.toast{
|
|
|
|
|
|
|
|
position:fixed;left:50%;transform:translateX(-50%);bottom:calc(84px + env(safe-area-inset-bottom));
|
|
|
|
|
|
|
|
z-index:60;background:var(--ink);color:#fff;padding:11px 16px;border-radius:8px;
|
|
|
|
|
|
|
|
font-size:14px;font-weight:550;max-width:88vw;text-align:center;opacity:0;pointer-events:none;transition:opacity .18s;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.toast.on{opacity:1}
|
|
|
|
|
|
|
|
.toast.err{background:var(--fehler)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.empty{text-align:center;color:var(--ink-3);padding:40px 20px;font-size:14px}
|
|
|
|
|
|
|
|
.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
|
|
|
|
|
|
|
dl.kv{margin:0;font-size:13px}
|
|
|
|
|
|
|
|
dl.kv div{display:flex;justify-content:space-between;gap:12px;padding:6px 0;border-bottom:1px solid var(--line)}
|
|
|
|
|
|
|
|
dl.kv dt{color:var(--ink-3)}
|
|
|
|
|
|
|
|
dl.kv dd{margin:0;text-align:right}
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
</head>
|
|
|
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<header>
|
|
|
|
|
|
|
|
<h1>Monitor-Check</h1>
|
|
|
|
|
|
|
|
<span class="netstate" id="netstate">bereit</span>
|
|
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<main>
|
|
|
|
|
|
|
|
<!-- ============ ERFASSEN ============ -->
|
|
|
|
|
|
|
|
<section class="view on" id="v-erfassen">
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
|
|
<h2>Neuer Eintrag</h2>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
|
|
<label for="f-bez">Bezeichnung *</label>
|
|
|
|
|
|
|
|
<input type="text" id="f-bez" placeholder="z. B. Monitor Foyer Nord" autocomplete="off">
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
|
|
<label for="f-ort">Standort</label>
|
|
|
|
|
|
|
|
<input type="text" id="f-ort" placeholder="z. B. Halle 2, Reihe A" autocomplete="off">
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
|
|
<label>Foto</label>
|
|
|
|
|
|
|
|
<div class="photo" id="f-photo">
|
|
|
|
|
|
|
|
<div class="ph" id="f-photo-ph">Tippen zum Fotografieren</div>
|
|
|
|
|
|
|
|
<img id="f-photo-img" alt="" hidden>
|
|
|
|
|
|
|
|
<button class="rm" id="f-photo-rm" hidden aria-label="Foto entfernen">×</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<input type="file" id="f-file" accept="image/*" capture="environment">
|
|
|
|
|
|
|
|
<p class="hint" id="f-photo-hint">Wird auf deinen Server geladen, das Sheet speichert die URL.</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
|
|
<label>Status sofort erfassen (optional)</label>
|
|
|
|
|
|
|
|
<div class="triple" id="f-triple"></div>
|
|
|
|
|
|
|
|
<div class="preview">
|
|
|
|
|
|
|
|
<span class="chip s-offen" id="f-status">Nicht geprüft</span>
|
|
|
|
|
|
|
|
<small>wird automatisch berechnet</small>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
|
|
<label for="f-notiz">Notiz</label>
|
|
|
|
|
|
|
|
<textarea id="f-notiz" placeholder="Auffälligkeiten, Defekte, Zuständigkeit …"></textarea>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<button class="btn primary" id="f-save">Eintrag anlegen</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ============ ÜBERSICHT ============ -->
|
|
|
|
|
|
|
|
<section class="view" id="v-liste">
|
|
|
|
|
|
|
|
<div class="stats" id="stats"></div>
|
|
|
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
|
|
|
<input type="text" id="q" placeholder="ID, Bezeichnung, Standort …" autocomplete="off">
|
|
|
|
|
|
|
|
<button class="btn" id="reload" style="width:auto;padding:0 16px" aria-label="Neu laden">↻</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="filters" id="filters"></div>
|
|
|
|
|
|
|
|
<div id="list"></div>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ============ EINSTELLUNGEN ============ -->
|
|
|
|
|
|
|
|
<section class="view" id="v-setup">
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
|
|
<h2>Verbindung</h2>
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
|
|
<label for="s-api">Apps-Script Web-App URL</label>
|
|
|
|
|
|
|
|
<input type="url" id="s-api" placeholder="https://script.google.com/macros/s/…/exec" autocomplete="off">
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
|
|
<label for="s-token">API-Token</label>
|
|
|
|
|
|
|
|
<input type="password" id="s-token" placeholder="Token aus Code.gs" autocomplete="off">
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
|
|
<label for="s-upload">Upload-Endpoint</label>
|
|
|
|
|
|
|
|
<input type="url" id="s-upload" placeholder="https://dein-server/api/upload" autocomplete="off">
|
|
|
|
|
|
|
|
<p class="hint">Leer lassen, wenn ohne Foto gearbeitet wird.</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
|
|
<label for="s-user">Dein Name</label>
|
|
|
|
|
|
|
|
<input type="text" id="s-user" placeholder="wird als Prüfer gespeichert" autocomplete="off">
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="btnrow">
|
|
|
|
|
|
|
|
<button class="btn primary" id="s-save">Speichern</button>
|
|
|
|
|
|
|
|
<button class="btn" id="s-test">Verbindung testen</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
|
|
<h2>Statusregeln</h2>
|
|
|
|
|
|
|
|
<dl class="kv">
|
|
|
|
|
|
|
|
<div><dt>Audio / Bild / Sprache alle JA</dt><dd><span class="chip s-ok">Funktionsfähig</span></dd></div>
|
|
|
|
|
|
|
|
<div><dt>Teilweise NEIN</dt><dd><span class="chip s-teil">Teilweise</span></dd></div>
|
|
|
|
|
|
|
|
<div><dt>Alle bewerteten NEIN</dt><dd><span class="chip s-fehler">Fehlerhaft</span></dd></div>
|
|
|
|
|
|
|
|
<div><dt>Alle drei n.A.</dt><dd><span class="chip s-aus">Deaktiviert</span></dd></div>
|
|
|
|
|
|
|
|
<div style="border:0"><dt>Nichts erfasst</dt><dd><span class="chip s-offen">Nicht geprüft</span></dd></div>
|
|
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
|
|
<p class="hint">Einzelne Felder mit „n.A.“ werden aus der Bewertung ausgeschlossen und zählen nicht als Fehler.</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
|
|
<h2>Offline-Warteschlange</h2>
|
|
|
|
|
|
|
|
<p class="hint" id="q-info" style="margin-top:0">–</p>
|
|
|
|
|
|
|
|
<div class="btnrow">
|
|
|
|
|
|
|
|
<button class="btn" id="q-flush">Jetzt senden</button>
|
|
|
|
|
|
|
|
<button class="btn ghost" id="q-clear">Verwerfen</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<nav>
|
|
|
|
|
|
|
|
<button class="on" data-view="erfassen">
|
|
|
|
|
|
|
|
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>Erfassen
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button data-view="liste">
|
|
|
|
|
|
|
|
<svg viewBox="0 0 24 24"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>Übersicht
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button data-view="setup">
|
|
|
|
|
|
|
|
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-2.9 1.2v.1a2 2 0 1 1-4 0v-.2a1.7 1.7 0 0 0-3-1.1l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0-1.1-2.9H3a2 2 0 1 1 0-4h.2A1.7 1.7 0 0 0 4.3 6l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 2.9-1.1V2a2 2 0 1 1 4 0v.2a1.7 1.7 0 0 0 2.9 1.1l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0 1.1 2.9h.2a2 2 0 1 1 0 4h-.2a1.7 1.7 0 0 0-1.4 1z"/></svg>Setup
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Detail -->
|
|
|
|
|
|
|
|
<div class="sheet" id="detail">
|
|
|
|
|
|
|
|
<div class="bd" data-close></div>
|
|
|
|
|
|
|
|
<div class="pn" id="detail-pn"></div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= Konfiguration ================= */
|
|
|
|
|
|
|
|
/* Optional hier fest eintragen – sonst über den Setup-Tab pflegen. */
|
|
|
|
|
|
|
|
var DEFAULTS = {
|
|
|
|
|
|
|
|
api: "",
|
|
|
|
|
|
|
|
token: "",
|
|
|
|
|
|
|
|
upload: "",
|
|
|
|
|
|
|
|
user: ""
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var LS = "monitorcheck.v1";
|
|
|
|
|
|
|
|
var cfg = Object.assign({}, DEFAULTS, load(LS + ".cfg", {}));
|
|
|
|
|
|
|
|
var queue = load(LS + ".queue", []);
|
|
|
|
|
|
|
|
var items = load(LS + ".cache", []);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function load(k, fb){ try{ var v = localStorage.getItem(k); return v ? JSON.parse(v) : fb; }catch(e){ return fb; } }
|
|
|
|
|
|
|
|
function save(k, v){ try{ localStorage.setItem(k, JSON.stringify(v)); }catch(e){} }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= Statuslogik (identisch zu Code.gs) ================= */
|
|
|
|
|
|
|
|
var S = {
|
|
|
|
|
|
|
|
OK:"Funktionsfähig", TEIL:"Teilweise funktionsfähig",
|
|
|
|
|
|
|
|
FEHLER:"Fehlerhaft", AUS:"Deaktiviert", OFFEN:"Nicht geprüft"
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
var CLS = {};
|
|
|
|
|
|
|
|
CLS[S.OK]="s-ok"; CLS[S.TEIL]="s-teil"; CLS[S.FEHLER]="s-fehler";
|
|
|
|
|
|
|
|
CLS[S.AUS]="s-aus"; CLS[S.OFFEN]="s-offen";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function norm(v){
|
|
|
|
|
|
|
|
var s = String(v == null ? "" : v).trim().toUpperCase();
|
|
|
|
|
|
|
|
if(!s) return "";
|
|
|
|
|
|
|
|
if(s==="JA"||s==="J"||s==="TRUE") return "JA";
|
|
|
|
|
|
|
|
if(s==="NEIN"||s==="N"||s==="FALSE") return "NEIN";
|
|
|
|
|
|
|
|
if(s==="N.A."||s==="NA"||s==="N/A"||s==="N.A") return "n.A.";
|
|
|
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function berechneStatus(a,b,c){
|
|
|
|
|
|
|
|
var w = [norm(a), norm(b), norm(c)];
|
|
|
|
|
|
|
|
var gesetzt = w.filter(function(x){ return x !== ""; });
|
|
|
|
|
|
|
|
if(!gesetzt.length) return S.OFFEN;
|
|
|
|
|
|
|
|
if(w.filter(function(x){ return x==="n.A."; }).length === 3) return S.AUS;
|
|
|
|
|
|
|
|
var bew = w.filter(function(x){ return x==="JA" || x==="NEIN"; });
|
|
|
|
|
|
|
|
if(!bew.length) return S.AUS;
|
|
|
|
|
|
|
|
var nein = bew.filter(function(x){ return x==="NEIN"; }).length;
|
|
|
|
|
|
|
|
if(nein === 0) return S.OK;
|
|
|
|
|
|
|
|
if(nein === bew.length) return S.FEHLER;
|
|
|
|
|
|
|
|
return S.TEIL;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Vereinheitlicht Statustexte aus der Tabelle – aeltere Zeilen stehen dort
|
|
|
|
|
|
|
|
ohne Umlaute ("Funktionsfaehig"). Ohne das greifen Filter und Zaehler nicht. */
|
|
|
|
|
|
|
|
function statusKanon(v){
|
|
|
|
|
|
|
|
var k = String(v == null ? "" : v).toLowerCase().trim()
|
|
|
|
|
|
|
|
.replace(/ä/g,"ae").replace(/ö/g,"oe").replace(/ü/g,"ue").replace(/ß/g,"ss");
|
|
|
|
|
|
|
|
if(k === "funktionsfaehig") return S.OK;
|
|
|
|
|
|
|
|
if(k === "teilweise funktionsfaehig") return S.TEIL;
|
|
|
|
|
|
|
|
if(k === "fehlerhaft") return S.FEHLER;
|
|
|
|
|
|
|
|
if(k === "deaktiviert") return S.AUS;
|
|
|
|
|
|
|
|
return S.OFFEN;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Kurzform für die Statistik-Kacheln */
|
|
|
|
|
|
|
|
function shortStatus(s){
|
|
|
|
|
|
|
|
if(s===S.OK) return "OK";
|
|
|
|
|
|
|
|
if(s===S.TEIL) return "Teil";
|
|
|
|
|
|
|
|
if(s===S.FEHLER) return "Fehler";
|
|
|
|
|
|
|
|
if(s===S.AUS) return "Aus";
|
|
|
|
|
|
|
|
return "Offen";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= Hilfen ================= */
|
|
|
|
|
|
|
|
var $ = function(sel, root){ return (root||document).querySelector(sel); };
|
|
|
|
|
|
|
|
var $$ = function(sel, root){ return Array.prototype.slice.call((root||document).querySelectorAll(sel)); };
|
|
|
|
|
|
|
|
function esc(s){
|
|
|
|
|
|
|
|
return String(s == null ? "" : s).replace(/[&<>"']/g, function(c){
|
|
|
|
|
|
|
|
return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c];
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
var toastTimer;
|
|
|
|
|
|
|
|
function toast(msg, isErr){
|
|
|
|
|
|
|
|
var t = $("#toast");
|
|
|
|
|
|
|
|
t.textContent = msg;
|
|
|
|
|
|
|
|
t.className = "toast on" + (isErr ? " err" : "");
|
|
|
|
|
|
|
|
clearTimeout(toastTimer);
|
|
|
|
|
|
|
|
toastTimer = setTimeout(function(){ t.className = "toast"; }, 2800);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function chip(status){
|
|
|
|
|
|
|
|
return '<span class="chip ' + (CLS[status]||"s-offen") + '">' + esc(status) + "</span>";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function datum(iso){
|
|
|
|
|
|
|
|
if(!iso) return "–";
|
|
|
|
|
|
|
|
var d = new Date(iso);
|
|
|
|
|
|
|
|
if(isNaN(d)) return String(iso);
|
|
|
|
|
|
|
|
return d.toLocaleString("de-DE",{day:"2-digit",month:"2-digit",year:"2-digit",hour:"2-digit",minute:"2-digit"});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= API ================= */
|
|
|
|
|
|
|
|
/* text/plain vermeidet den CORS-Preflight, den Apps Script nicht beantwortet. */
|
|
|
|
|
|
|
|
function api(action, payload){
|
|
|
|
|
|
|
|
if(!cfg.api) return Promise.reject(new Error("Keine Apps-Script-URL hinterlegt (Setup)."));
|
|
|
|
|
|
|
|
var body = Object.assign({ action: action, token: cfg.token }, payload || {});
|
|
|
|
|
|
|
|
return fetch(cfg.api, {
|
|
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
|
|
redirect: "follow",
|
|
|
|
|
|
|
|
headers: { "Content-Type": "text/plain;charset=utf-8" },
|
|
|
|
|
|
|
|
body: JSON.stringify(body)
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.then(function(r){ return r.text(); })
|
|
|
|
|
|
|
|
.then(function(txt){
|
|
|
|
|
|
|
|
var data;
|
|
|
|
|
|
|
|
try{ data = JSON.parse(txt); }
|
|
|
|
|
|
|
|
catch(e){ throw new Error("Unerwartete Antwort – Web-App-Zugriff auf „Jeder“ gesetzt?"); }
|
|
|
|
|
|
|
|
if(!data.ok) throw new Error(data.error || "Fehler");
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function uploadFoto(file){
|
|
|
|
|
|
|
|
if(!cfg.upload) return Promise.reject(new Error("Kein Upload-Endpoint hinterlegt (Setup)."));
|
|
|
|
|
|
|
|
var fd = new FormData();
|
|
|
|
|
|
|
|
fd.append("datei", file, file.name || "foto.jpg");
|
|
|
|
|
|
|
|
return fetch(cfg.upload, {
|
|
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
|
|
headers: cfg.token ? { "X-Token": cfg.token } : {},
|
|
|
|
|
|
|
|
body: fd
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.then(function(r){ return r.json().catch(function(){ throw new Error("Upload fehlgeschlagen (" + r.status + ")"); }); })
|
|
|
|
|
|
|
|
.then(function(d){
|
|
|
|
|
|
|
|
if(!d.ok) throw new Error(d.error || "Upload fehlgeschlagen");
|
|
|
|
|
|
|
|
return d.url;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= Warteschlange ================= */
|
|
|
|
|
|
|
|
function enqueue(action, payload){
|
|
|
|
|
|
|
|
queue.push({ action: action, payload: payload, ts: Date.now() });
|
|
|
|
|
|
|
|
save(LS + ".queue", queue);
|
|
|
|
|
|
|
|
renderNet();
|
|
|
|
|
|
|
|
window.dispatchEvent(new Event("mc:queue"));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function flushQueue(silent){
|
|
|
|
|
|
|
|
if(!queue.length){ if(!silent) toast("Warteschlange ist leer."); return Promise.resolve(); }
|
|
|
|
|
|
|
|
if(!navigator.onLine){ if(!silent) toast("Offline – später erneut.", true); return Promise.resolve(); }
|
|
|
|
|
|
|
|
var pending = queue.slice();
|
|
|
|
|
|
|
|
var ok = 0;
|
|
|
|
|
|
|
|
return pending.reduce(function(p, job){
|
|
|
|
|
|
|
|
return p.then(function(){
|
|
|
|
|
|
|
|
return api(job.action, job.payload).then(function(){
|
|
|
|
|
|
|
|
ok++;
|
|
|
|
|
|
|
|
queue = queue.filter(function(q){ return q !== job; });
|
|
|
|
|
|
|
|
save(LS + ".queue", queue);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}, Promise.resolve())
|
|
|
|
|
|
|
|
.then(function(){
|
|
|
|
|
|
|
|
if(ok) toast(ok + " Eintrag/Einträge nachgesendet.");
|
|
|
|
|
|
|
|
renderNet();
|
|
|
|
|
|
|
|
window.dispatchEvent(new Event("mc:queue"));
|
|
|
|
|
|
|
|
return ladeListe(true);
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.catch(function(err){
|
|
|
|
|
|
|
|
renderNet();
|
|
|
|
|
|
|
|
if(!silent) toast(err.message, true);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderNet(){
|
|
|
|
|
|
|
|
var el = $("#netstate");
|
|
|
|
|
|
|
|
if(!navigator.onLine){ el.className = "netstate off"; el.textContent = "offline"; return; }
|
|
|
|
|
|
|
|
if(queue.length){ el.className = "netstate queue"; el.textContent = queue.length + " wartet"; return; }
|
|
|
|
|
|
|
|
el.className = "netstate"; el.textContent = "verbunden";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
$("#q-info") && (function(){
|
|
|
|
|
|
|
|
var upd = function(){ $("#q-info").textContent = queue.length ? queue.length + " Vorgang/Vorgänge warten auf Übertragung." : "Keine offenen Vorgänge."; };
|
|
|
|
|
|
|
|
upd();
|
|
|
|
|
|
|
|
window.addEventListener("mc:queue", upd);
|
|
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= Navigation ================= */
|
|
|
|
|
|
|
|
$$("nav button").forEach(function(b){
|
|
|
|
|
|
|
|
b.addEventListener("click", function(){
|
|
|
|
|
|
|
|
$$("nav button").forEach(function(x){ x.classList.toggle("on", x === b); });
|
|
|
|
|
|
|
|
$$(".view").forEach(function(v){ v.classList.toggle("on", v.id === "v-" + b.dataset.view); });
|
|
|
|
|
|
|
|
window.scrollTo(0,0);
|
|
|
|
|
|
|
|
if(b.dataset.view === "liste") ladeListe(true);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= Status-Schalter ================= */
|
|
|
|
|
|
|
|
function buildTriple(host, werte, onChange){
|
|
|
|
|
|
|
|
host.__werte = werte;
|
|
|
|
|
|
|
|
host.__onChange = onChange;
|
|
|
|
|
|
|
|
var felder = [["audio","Audio"],["bild","Bild"],["sprache","Sprache"]];
|
|
|
|
|
|
|
|
host.innerHTML = felder.map(function(f){
|
|
|
|
|
|
|
|
return '<div class="trow"><span>' + f[1] + '</span><div class="seg" data-k="' + f[0] + '">' +
|
|
|
|
|
|
|
|
["JA","NEIN","n.A."].map(function(v){
|
|
|
|
|
|
|
|
var on = norm(werte[f[0]]) === v;
|
|
|
|
|
|
|
|
return '<button type="button" data-v="' + v + '" aria-pressed="' + on + '">' + v + "</button>";
|
|
|
|
|
|
|
|
}).join("") + "</div></div>";
|
|
|
|
|
|
|
|
}).join("");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Listener nur einmal pro Host binden – buildTriple wird beim Zuruecksetzen erneut aufgerufen.
|
|
|
|
|
|
|
|
if(host.dataset.bound === "1") return;
|
|
|
|
|
|
|
|
host.dataset.bound = "1";
|
|
|
|
|
|
|
|
host.addEventListener("click", function(ev){
|
|
|
|
|
|
|
|
var btn = ev.target.closest("button[data-v]");
|
|
|
|
|
|
|
|
if(!btn) return;
|
|
|
|
|
|
|
|
var seg = btn.parentNode, k = seg.dataset.k;
|
|
|
|
|
|
|
|
var war = btn.getAttribute("aria-pressed") === "true";
|
|
|
|
|
|
|
|
$$("button", seg).forEach(function(x){ x.setAttribute("aria-pressed", "false"); });
|
|
|
|
|
|
|
|
if(!war){ btn.setAttribute("aria-pressed","true"); host.__werte[k] = btn.dataset.v; }
|
|
|
|
|
|
|
|
else { host.__werte[k] = ""; } // erneutes Tippen hebt die Auswahl auf
|
|
|
|
|
|
|
|
host.__onChange(host.__werte);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= Erfassen ================= */
|
|
|
|
|
|
|
|
var neu = { audio:"", bild:"", sprache:"" };
|
|
|
|
|
|
|
|
var neuFile = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
buildTriple($("#f-triple"), neu, function(w){
|
|
|
|
|
|
|
|
var st = berechneStatus(w.audio, w.bild, w.sprache);
|
|
|
|
|
|
|
|
var el = $("#f-status");
|
|
|
|
|
|
|
|
el.className = "chip " + (CLS[st] || "s-offen");
|
|
|
|
|
|
|
|
el.textContent = st;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$("#f-photo").addEventListener("click", function(ev){
|
|
|
|
|
|
|
|
if(ev.target.id === "f-photo-rm") return;
|
|
|
|
|
|
|
|
$("#f-file").click();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
var vorschauUrl = null;
|
|
|
|
|
|
|
|
$("#f-file").addEventListener("change", function(){
|
|
|
|
|
|
|
|
var f = this.files && this.files[0];
|
|
|
|
|
|
|
|
if(!f) return;
|
|
|
|
|
|
|
|
neuFile = f;
|
|
|
|
|
|
|
|
if(vorschauUrl) URL.revokeObjectURL(vorschauUrl);
|
|
|
|
|
|
|
|
vorschauUrl = URL.createObjectURL(f);
|
|
|
|
|
|
|
|
var img = $("#f-photo-img");
|
|
|
|
|
|
|
|
img.src = vorschauUrl;
|
|
|
|
|
|
|
|
img.hidden = false;
|
|
|
|
|
|
|
|
$("#f-photo-ph").hidden = true;
|
|
|
|
|
|
|
|
$("#f-photo-rm").hidden = false;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
function fotoEntfernen(){
|
|
|
|
|
|
|
|
neuFile = null;
|
|
|
|
|
|
|
|
$("#f-file").value = "";
|
|
|
|
|
|
|
|
if(vorschauUrl){ URL.revokeObjectURL(vorschauUrl); vorschauUrl = null; }
|
|
|
|
|
|
|
|
var img = $("#f-photo-img");
|
|
|
|
|
|
|
|
img.removeAttribute("src");
|
|
|
|
|
|
|
|
img.hidden = true;
|
|
|
|
|
|
|
|
$("#f-photo-ph").hidden = false;
|
|
|
|
|
|
|
|
$("#f-photo-rm").hidden = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
$("#f-photo-rm").addEventListener("click", function(ev){
|
|
|
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
fotoEntfernen();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$("#f-save").addEventListener("click", function(){
|
|
|
|
|
|
|
|
var btn = this;
|
|
|
|
|
|
|
|
var bez = $("#f-bez").value.trim();
|
|
|
|
|
|
|
|
if(!bez){ toast("Bezeichnung fehlt.", true); $("#f-bez").focus(); return; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
|
|
btn.textContent = "Wird gespeichert …";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var schritt = neuFile
|
|
|
|
|
|
|
|
? uploadFoto(neuFile).catch(function(err){
|
|
|
|
|
|
|
|
toast("Foto: " + err.message, true);
|
|
|
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
: Promise.resolve("");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
schritt.then(function(url){
|
|
|
|
|
|
|
|
var payload = {
|
|
|
|
|
|
|
|
bezeichnung: bez,
|
|
|
|
|
|
|
|
standort: $("#f-ort").value.trim(),
|
|
|
|
|
|
|
|
bildUrl: url,
|
|
|
|
|
|
|
|
audio: neu.audio, bild: neu.bild, sprache: neu.sprache,
|
|
|
|
|
|
|
|
notiz: $("#f-notiz").value.trim(),
|
|
|
|
|
|
|
|
pruefer: cfg.user || ""
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
return api("create", payload).then(function(res){
|
|
|
|
|
|
|
|
toast("Angelegt: " + res.id);
|
|
|
|
|
|
|
|
resetForm();
|
|
|
|
|
|
|
|
ladeListe(true);
|
|
|
|
|
|
|
|
}).catch(function(err){
|
|
|
|
|
|
|
|
if(!navigator.onLine || /Failed to fetch|NetworkError/i.test(err.message)){
|
|
|
|
|
|
|
|
enqueue("create", payload);
|
|
|
|
|
|
|
|
toast("Offline gespeichert – wird nachgesendet.");
|
|
|
|
|
|
|
|
resetForm();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
toast(err.message, true);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}).then(function(){
|
|
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
|
|
btn.textContent = "Eintrag anlegen";
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function resetForm(){
|
|
|
|
|
|
|
|
$("#f-bez").value = ""; $("#f-ort").value = ""; $("#f-notiz").value = "";
|
|
|
|
|
|
|
|
neu.audio = neu.bild = neu.sprache = "";
|
|
|
|
|
|
|
|
buildTriple($("#f-triple"), neu, function(w){
|
|
|
|
|
|
|
|
var st = berechneStatus(w.audio, w.bild, w.sprache);
|
|
|
|
|
|
|
|
var el = $("#f-status");
|
|
|
|
|
|
|
|
el.className = "chip " + (CLS[st] || "s-offen");
|
|
|
|
|
|
|
|
el.textContent = st;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
$("#f-status").className = "chip s-offen";
|
|
|
|
|
|
|
|
$("#f-status").textContent = S.OFFEN;
|
|
|
|
|
|
|
|
fotoEntfernen();
|
|
|
|
|
|
|
|
window.scrollTo(0, 0);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= Übersicht ================= */
|
|
|
|
|
|
|
|
var filter = "alle";
|
|
|
|
|
|
|
|
var suche = "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var FILTERS = [["alle","Alle"],[S.OFFEN,"Nicht geprüft"],[S.FEHLER,"Fehlerhaft"],[S.TEIL,"Teilweise"],[S.OK,"Funktionsfähig"],[S.AUS,"Deaktiviert"]];
|
|
|
|
|
|
|
|
$("#filters").innerHTML = FILTERS.map(function(f){
|
|
|
|
|
|
|
|
return '<button data-f="' + esc(f[0]) + '"' + (f[0]==="alle"?' class="on"':"") + ">" + esc(f[1]) + "</button>";
|
|
|
|
|
|
|
|
}).join("");
|
|
|
|
|
|
|
|
$("#filters").addEventListener("click", function(ev){
|
|
|
|
|
|
|
|
var b = ev.target.closest("button[data-f]");
|
|
|
|
|
|
|
|
if(!b) return;
|
|
|
|
|
|
|
|
filter = b.dataset.f;
|
|
|
|
|
|
|
|
$$("#filters button").forEach(function(x){ x.classList.toggle("on", x === b); });
|
|
|
|
|
|
|
|
renderListe();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
$("#q").addEventListener("input", function(){ suche = this.value.trim().toLowerCase(); renderListe(); });
|
|
|
|
|
|
|
|
$("#reload").addEventListener("click", function(){ ladeListe(false); });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function setzeItems(arr){
|
|
|
|
|
|
|
|
items = (arr || []).map(function(i){
|
|
|
|
|
|
|
|
i.status = statusKanon(i.status);
|
|
|
|
|
|
|
|
return i;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
save(LS + ".cache", items);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function ladeListe(silent){
|
|
|
|
|
|
|
|
if(!cfg.api){ if(!silent) toast("Bitte zuerst die Verbindung einrichten.", true); renderListe(); return Promise.resolve(); }
|
|
|
|
|
|
|
|
return api("list").then(function(res){
|
|
|
|
|
|
|
|
setzeItems(res.items || []);
|
|
|
|
|
|
|
|
renderListe();
|
|
|
|
|
|
|
|
if(!silent) toast(items.length + " Einträge geladen.");
|
|
|
|
|
|
|
|
}).catch(function(err){
|
|
|
|
|
|
|
|
renderListe();
|
|
|
|
|
|
|
|
if(!silent) toast(err.message, true);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderListe(){
|
|
|
|
|
|
|
|
var zahl = {};
|
|
|
|
|
|
|
|
zahl[S.OK]=0; zahl[S.TEIL]=0; zahl[S.FEHLER]=0; zahl[S.AUS]=0; zahl[S.OFFEN]=0;
|
|
|
|
|
|
|
|
items.forEach(function(i){ if(zahl[i.status] !== undefined) zahl[i.status]++; else zahl[S.OFFEN]++; });
|
|
|
|
|
|
|
|
$("#stats").innerHTML = [S.OK,S.TEIL,S.FEHLER,S.AUS,S.OFFEN].map(function(s){
|
|
|
|
|
|
|
|
return '<div class="stat"><b>' + zahl[s] + "</b><span>" + esc(shortStatus(s)) + "</span></div>";
|
|
|
|
|
|
|
|
}).join("");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var gefiltert = items.filter(function(i){
|
|
|
|
|
|
|
|
if(filter !== "alle" && i.status !== filter) return false;
|
|
|
|
|
|
|
|
if(!suche) return true;
|
|
|
|
|
|
|
|
return (i.id + " " + i.bezeichnung + " " + i.standort + " " + i.notiz).toLowerCase().indexOf(suche) >= 0;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(!gefiltert.length){
|
|
|
|
|
|
|
|
$("#list").innerHTML = '<div class="empty">' +
|
|
|
|
|
|
|
|
(items.length ? "Keine Treffer." : "Noch keine Einträge – oben rechts neu laden oder im Tab „Erfassen“ anlegen.") +
|
|
|
|
|
|
|
|
"</div>";
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$("#list").innerHTML = gefiltert.map(function(i){
|
|
|
|
|
|
|
|
var thumb = i.bildUrl
|
|
|
|
|
|
|
|
? '<img class="thumb" src="' + esc(i.bildUrl) + '" alt="" loading="lazy">'
|
|
|
|
|
|
|
|
: '<div class="thumb empty">kein<br>Bild</div>';
|
|
|
|
|
|
|
|
return '<button class="item" data-id="' + esc(i.id) + '">' + thumb +
|
|
|
|
|
|
|
|
'<div class="meta">' +
|
|
|
|
|
|
|
|
'<div class="idline mono">' + esc(i.id) + "</div>" +
|
|
|
|
|
|
|
|
'<div class="name">' + esc(i.bezeichnung) + "</div>" +
|
|
|
|
|
|
|
|
'<div class="sub">' + esc(i.standort || "–") + "</div>" +
|
|
|
|
|
|
|
|
"</div>" + chip(i.status) + "</button>";
|
|
|
|
|
|
|
|
}).join("");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$("#list").addEventListener("click", function(ev){
|
|
|
|
|
|
|
|
var b = ev.target.closest(".item");
|
|
|
|
|
|
|
|
if(b) oeffneDetail(b.dataset.id);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= Detail / Statusmeldung ================= */
|
|
|
|
|
|
|
|
var aktiv = null;
|
|
|
|
|
|
|
|
var aktivWerte = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function oeffneDetail(id){
|
|
|
|
|
|
|
|
var it = items.filter(function(x){ return x.id === id; })[0];
|
|
|
|
|
|
|
|
if(!it) return;
|
|
|
|
|
|
|
|
aktiv = it;
|
|
|
|
|
|
|
|
aktivWerte = { audio: it.audio || "", bild: it.bild || "", sprache: it.sprache || "" };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$("#detail-pn").innerHTML =
|
|
|
|
|
|
|
|
'<div class="grip"></div>' +
|
|
|
|
|
|
|
|
'<div class="card">' +
|
|
|
|
|
|
|
|
'<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px">' +
|
|
|
|
|
|
|
|
"<div><div class='idline mono' style='font-size:12px;color:var(--ink-3)'>" + esc(it.id) + "</div>" +
|
|
|
|
|
|
|
|
"<div style='font-size:18px;font-weight:650'>" + esc(it.bezeichnung) + "</div>" +
|
|
|
|
|
|
|
|
"<div style='font-size:13px;color:var(--ink-3)'>" + esc(it.standort || "–") + "</div></div>" +
|
|
|
|
|
|
|
|
chip(it.status) +
|
|
|
|
|
|
|
|
"</div>" +
|
|
|
|
|
|
|
|
(it.bildUrl ? '<img src="' + esc(it.bildUrl) + '" alt="" style="width:100%;border-radius:8px;margin-bottom:12px">' : "") +
|
|
|
|
|
|
|
|
'<div class="triple" id="d-triple"></div>' +
|
|
|
|
|
|
|
|
'<div class="preview"><span class="chip ' + (CLS[it.status]||"s-offen") + '" id="d-preview">' + esc(it.status) + "</span><small>neuer Status</small></div>" +
|
|
|
|
|
|
|
|
'<div class="field" style="margin-top:14px"><label for="d-notiz">Notiz</label><textarea id="d-notiz">' + esc(it.notiz) + "</textarea></div>" +
|
|
|
|
|
|
|
|
'<div class="btnrow"><button class="btn primary" id="d-save">Status melden</button>' +
|
|
|
|
|
|
|
|
'<button class="btn ghost" data-close>Schließen</button></div>' +
|
|
|
|
|
|
|
|
'<button class="btn ghost danger" id="d-del" style="margin-top:8px">Eintrag löschen</button>' +
|
|
|
|
|
|
|
|
"</div>" +
|
|
|
|
|
|
|
|
'<div class="card"><h2>Verlauf</h2><dl class="kv">' +
|
|
|
|
|
|
|
|
"<div><dt>Erfasst</dt><dd>" + esc(datum(it.erfasstAm)) + "</dd></div>" +
|
|
|
|
|
|
|
|
"<div><dt>Zuletzt geprüft</dt><dd>" + esc(datum(it.geprueftAm)) + "</dd></div>" +
|
|
|
|
|
|
|
|
"<div style='border:0'><dt>Prüfer</dt><dd>" + esc(it.pruefer || "–") + "</dd></div>" +
|
|
|
|
|
|
|
|
"</dl></div>";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
buildTriple($("#d-triple"), aktivWerte, function(w){
|
|
|
|
|
|
|
|
var st = berechneStatus(w.audio, w.bild, w.sprache);
|
|
|
|
|
|
|
|
var el = $("#d-preview");
|
|
|
|
|
|
|
|
el.className = "chip " + (CLS[st] || "s-offen");
|
|
|
|
|
|
|
|
el.textContent = st;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$("#d-save").addEventListener("click", speichereStatus);
|
|
|
|
|
|
|
|
$("#d-del").addEventListener("click", loescheEintrag);
|
|
|
|
|
|
|
|
$("#detail").classList.add("on");
|
|
|
|
|
|
|
|
document.body.style.overflow = "hidden";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function schliesseDetail(){
|
|
|
|
|
|
|
|
$("#detail").classList.remove("on");
|
|
|
|
|
|
|
|
document.body.style.overflow = "";
|
|
|
|
|
|
|
|
aktiv = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
document.addEventListener("click", function(ev){
|
|
|
|
|
|
|
|
if(ev.target.matches("[data-close]")) schliesseDetail();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
document.addEventListener("keydown", function(ev){
|
|
|
|
|
|
|
|
if(ev.key === "Escape" && $("#detail").classList.contains("on")) schliesseDetail();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function speichereStatus(){
|
|
|
|
|
|
|
|
if(!aktiv) return;
|
|
|
|
|
|
|
|
var btn = $("#d-save");
|
|
|
|
|
|
|
|
var payload = {
|
|
|
|
|
|
|
|
id: aktiv.id,
|
|
|
|
|
|
|
|
audio: aktivWerte.audio, bild: aktivWerte.bild, sprache: aktivWerte.sprache,
|
|
|
|
|
|
|
|
notiz: $("#d-notiz").value.trim(),
|
|
|
|
|
|
|
|
pruefer: cfg.user || ""
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
var neuStatus = berechneStatus(payload.audio, payload.bild, payload.sprache);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
|
|
btn.textContent = "Wird gemeldet …";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api("status", payload).then(function(res){
|
|
|
|
|
|
|
|
lokalAktualisieren(payload, res.status || neuStatus, res.geprueftAm);
|
|
|
|
|
|
|
|
toast(aktiv.id + ": " + (res.status || neuStatus));
|
|
|
|
|
|
|
|
schliesseDetail();
|
|
|
|
|
|
|
|
}).catch(function(err){
|
|
|
|
|
|
|
|
if(!navigator.onLine || /Failed to fetch|NetworkError/i.test(err.message)){
|
|
|
|
|
|
|
|
enqueue("status", payload);
|
|
|
|
|
|
|
|
lokalAktualisieren(payload, neuStatus, new Date().toISOString());
|
|
|
|
|
|
|
|
toast("Offline gespeichert – wird nachgesendet.");
|
|
|
|
|
|
|
|
schliesseDetail();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
toast(err.message, true);
|
|
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
|
|
btn.textContent = "Status melden";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function loescheEintrag(){
|
|
|
|
|
|
|
|
if(!aktiv) return;
|
|
|
|
|
|
|
|
var it = aktiv;
|
|
|
|
|
|
|
|
if(!confirm("„" + it.id + " – " + it.bezeichnung + "“ wirklich löschen?\n\nDie Zeile wird aus der Tabelle entfernt. Das lässt sich nicht rückgängig machen.")) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var btn = $("#d-del");
|
|
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
|
|
btn.textContent = "Wird gelöscht …";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api("delete", { id: it.id, pruefer: cfg.user || "" }).then(function(){
|
|
|
|
|
|
|
|
items = items.filter(function(x){ return x.id !== it.id; });
|
|
|
|
|
|
|
|
save(LS + ".cache", items);
|
|
|
|
|
|
|
|
renderListe();
|
|
|
|
|
|
|
|
toast(it.id + " gelöscht.");
|
|
|
|
|
|
|
|
schliesseDetail();
|
|
|
|
|
|
|
|
}).catch(function(err){
|
|
|
|
|
|
|
|
toast(err.message, true);
|
|
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
|
|
btn.textContent = "Eintrag löschen";
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function lokalAktualisieren(p, status, zeit){
|
|
|
|
|
|
|
|
items = items.map(function(i){
|
|
|
|
|
|
|
|
if(i.id !== p.id) return i;
|
|
|
|
|
|
|
|
return Object.assign({}, i, {
|
|
|
|
|
|
|
|
audio: norm(p.audio), bild: norm(p.bild), sprache: norm(p.sprache),
|
|
|
|
|
|
|
|
status: statusKanon(status), notiz: p.notiz,
|
|
|
|
|
|
|
|
geprueftAm: zeit || i.geprueftAm, pruefer: p.pruefer || i.pruefer
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
save(LS + ".cache", items);
|
|
|
|
|
|
|
|
renderListe();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= Setup ================= */
|
|
|
|
|
|
|
|
function fuelleSetup(){
|
|
|
|
|
|
|
|
$("#s-api").value = cfg.api || "";
|
|
|
|
|
|
|
|
$("#s-token").value = cfg.token || "";
|
|
|
|
|
|
|
|
$("#s-upload").value = cfg.upload || "";
|
|
|
|
|
|
|
|
$("#s-user").value = cfg.user || "";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
fuelleSetup();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$("#s-save").addEventListener("click", function(){
|
|
|
|
|
|
|
|
cfg.api = $("#s-api").value.trim();
|
|
|
|
|
|
|
|
cfg.token = $("#s-token").value.trim();
|
|
|
|
|
|
|
|
cfg.upload = $("#s-upload").value.trim();
|
|
|
|
|
|
|
|
cfg.user = $("#s-user").value.trim();
|
|
|
|
|
|
|
|
save(LS + ".cfg", cfg);
|
|
|
|
|
|
|
|
toast("Einstellungen gespeichert.");
|
|
|
|
|
|
|
|
ladeListe(true);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
$("#s-test").addEventListener("click", function(){
|
|
|
|
|
|
|
|
var btn = this;
|
|
|
|
|
|
|
|
cfg.api = $("#s-api").value.trim();
|
|
|
|
|
|
|
|
cfg.token = $("#s-token").value.trim();
|
|
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
|
|
api("ping").then(function(r){ toast("Verbindung ok (" + datum(r.time) + ")"); })
|
|
|
|
|
|
|
|
.catch(function(e){ toast(e.message, true); })
|
|
|
|
|
|
|
|
.then(function(){ btn.disabled = false; });
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
$("#q-flush").addEventListener("click", function(){ flushQueue(false); });
|
|
|
|
|
|
|
|
$("#q-clear").addEventListener("click", function(){
|
|
|
|
|
|
|
|
if(!queue.length){ toast("Warteschlange ist leer."); return; }
|
|
|
|
|
|
|
|
if(confirm("Wirklich " + queue.length + " nicht gesendete Vorgänge verwerfen?")){
|
|
|
|
|
|
|
|
queue = []; save(LS + ".queue", queue); renderNet();
|
|
|
|
|
|
|
|
window.dispatchEvent(new Event("mc:queue"));
|
|
|
|
|
|
|
|
toast("Warteschlange geleert.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ================= Start ================= */
|
|
|
|
|
|
|
|
window.addEventListener("online", function(){ renderNet(); flushQueue(true); });
|
|
|
|
|
|
|
|
window.addEventListener("offline", renderNet);
|
|
|
|
|
|
|
|
renderNet();
|
|
|
|
|
|
|
|
setzeItems(items); // Zwischenspeicher aus aelteren Versionen vereinheitlichen
|
|
|
|
|
|
|
|
renderListe();
|
|
|
|
|
|
|
|
if(cfg.api){ flushQueue(true).then(function(){ ladeListe(true); }); }
|
|
|
|
|
|
|
|
else { $$("nav button")[2].click(); }
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
</body>
|
|
|
|
|
|
|
|
</html>
|