|
|
/**
|
|
|
* Monitor-Check – Google Apps Script Backend
|
|
|
* ------------------------------------------
|
|
|
* Stellt eine JSON-API fuer das Web-Tool bereit.
|
|
|
* Deployment: Bereitstellen > Neue Bereitstellung > Web-App
|
|
|
* Ausfuehren als: Ich
|
|
|
* Zugriff: Jeder
|
|
|
*
|
|
|
* WICHTIG: API_TOKEN unten aendern und identisch im Frontend eintragen.
|
|
|
*/
|
|
|
|
|
|
var CONFIG = {
|
|
|
SHEET_NAME: 'Monitore',
|
|
|
LOG_SHEET_NAME: 'Protokoll',
|
|
|
API_TOKEN: 'BITTE-AENDERN-langes-zufaelliges-token',
|
|
|
ID_PREFIX: 'MON',
|
|
|
ID_PAD: 3
|
|
|
};
|
|
|
|
|
|
var HEADERS = [
|
|
|
'ID',
|
|
|
'Bezeichnung',
|
|
|
'Standort',
|
|
|
'Bild-URL',
|
|
|
'Audio',
|
|
|
'Bild',
|
|
|
'Sprache',
|
|
|
'Status',
|
|
|
'Notiz',
|
|
|
'Erfasst am',
|
|
|
'Geprueft am',
|
|
|
'Geprueft von'
|
|
|
];
|
|
|
|
|
|
var COL = {
|
|
|
ID: 0, BEZEICHNUNG: 1, STANDORT: 2, BILD_URL: 3,
|
|
|
AUDIO: 4, BILD: 5, SPRACHE: 6, STATUS: 7,
|
|
|
NOTIZ: 8, ERFASST: 9, GEPRUEFT: 10, PRUEFER: 11
|
|
|
};
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
/* Status-Logik */
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
var STATUS = {
|
|
|
OK: 'Funktionsfähig',
|
|
|
TEIL: 'Teilweise funktionsfähig',
|
|
|
FEHLER: 'Fehlerhaft',
|
|
|
AUS: 'Deaktiviert',
|
|
|
OFFEN: 'Nicht geprüft'
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Regeln (audio/bild/sprache jeweils 'JA' | 'NEIN' | 'n.A.' | '')
|
|
|
* - Felder mit 'n.A.' werden aus der Bewertung ausgeschlossen.
|
|
|
* - Alle drei 'n.A.' -> Deaktiviert
|
|
|
* - Kein Feld gesetzt -> Nicht geprueft
|
|
|
* - Alle bewerteten Felder JA -> Funktionsfaehig
|
|
|
* - Alle bewerteten Felder NEIN -> Fehlerhaft
|
|
|
* - Gemischt -> Teilweise funktionsfaehig
|
|
|
*/
|
|
|
function berechneStatus(audio, bild, sprache) {
|
|
|
var werte = [norm_(audio), norm_(bild), norm_(sprache)];
|
|
|
|
|
|
var gesetzt = werte.filter(function (w) { return w !== ''; });
|
|
|
if (gesetzt.length === 0) return STATUS.OFFEN;
|
|
|
|
|
|
var na = werte.filter(function (w) { return w === 'n.A.'; }).length;
|
|
|
if (na === 3) return STATUS.AUS;
|
|
|
|
|
|
var bewertet = werte.filter(function (w) { return w === 'JA' || w === 'NEIN'; });
|
|
|
if (bewertet.length === 0) return STATUS.AUS;
|
|
|
|
|
|
var ja = bewertet.filter(function (w) { return w === 'JA'; }).length;
|
|
|
var nein = bewertet.filter(function (w) { return w === 'NEIN'; }).length;
|
|
|
|
|
|
if (nein === 0) return STATUS.OK;
|
|
|
if (ja === 0) return STATUS.FEHLER;
|
|
|
return STATUS.TEIL;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Vereinheitlicht Statustexte. Aeltere Zeilen wurden ohne Umlaute geschrieben
|
|
|
* ("Funktionsfaehig"); beide Schreibweisen ergeben denselben Status.
|
|
|
*/
|
|
|
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 STATUS.OK;
|
|
|
if (k === 'teilweise funktionsfaehig') return STATUS.TEIL;
|
|
|
if (k === 'fehlerhaft') return STATUS.FEHLER;
|
|
|
if (k === 'deaktiviert') return STATUS.AUS;
|
|
|
return STATUS.OFFEN;
|
|
|
}
|
|
|
|
|
|
function norm_(v) {
|
|
|
if (v === null || v === undefined) return '';
|
|
|
var s = String(v).trim().toUpperCase();
|
|
|
if (s === '') return '';
|
|
|
if (s === 'JA' || s === 'J' || s === 'YES' || s === 'TRUE') return 'JA';
|
|
|
if (s === 'NEIN' || s === 'N' || s === 'NO' || s === 'FALSE') return 'NEIN';
|
|
|
if (s === 'N.A.' || s === 'NA' || s === 'N/A' || s === 'N.A') return 'n.A.';
|
|
|
return '';
|
|
|
}
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
/* HTTP-Einstieg */
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
function doGet(e) {
|
|
|
return handle_(e, (e && e.parameter) ? e.parameter : {});
|
|
|
}
|
|
|
|
|
|
function doPost(e) {
|
|
|
var payload = {};
|
|
|
try {
|
|
|
if (e && e.postData && e.postData.contents) {
|
|
|
payload = JSON.parse(e.postData.contents);
|
|
|
}
|
|
|
} catch (err) {
|
|
|
return json_({ ok: false, error: 'Ungueltiges JSON' });
|
|
|
}
|
|
|
return handle_(e, payload);
|
|
|
}
|
|
|
|
|
|
function handle_(e, p) {
|
|
|
try {
|
|
|
if (String(p.token || '') !== CONFIG.API_TOKEN) {
|
|
|
return json_({ ok: false, error: 'Nicht autorisiert' });
|
|
|
}
|
|
|
switch (String(p.action || '')) {
|
|
|
case 'ping': return json_({ ok: true, version: 1, time: new Date().toISOString() });
|
|
|
case 'list': return json_({ ok: true, items: listeEintraege_() });
|
|
|
case 'create': return json_(erstelleEintrag_(p));
|
|
|
case 'status': return json_(setzeStatus_(p));
|
|
|
case 'update': return json_(aktualisiereEintrag_(p));
|
|
|
case 'archive': return json_(archiviereEintrag_(p));
|
|
|
case 'delete': return json_(loescheEintrag_(p));
|
|
|
default: return json_({ ok: false, error: 'Unbekannte Aktion' });
|
|
|
}
|
|
|
} catch (err) {
|
|
|
return json_({ ok: false, error: String(err && err.message ? err.message : err) });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function json_(obj) {
|
|
|
return ContentService
|
|
|
.createTextOutput(JSON.stringify(obj))
|
|
|
.setMimeType(ContentService.MimeType.JSON);
|
|
|
}
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
/* Sheet-Zugriff */
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
function sheet_() {
|
|
|
var ss = SpreadsheetApp.getActiveSpreadsheet();
|
|
|
var sh = ss.getSheetByName(CONFIG.SHEET_NAME);
|
|
|
if (!sh) {
|
|
|
sh = ss.insertSheet(CONFIG.SHEET_NAME);
|
|
|
}
|
|
|
if (sh.getLastRow() === 0 || String(sh.getRange(1, 1).getValue()).trim() === '') {
|
|
|
sh.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]);
|
|
|
sh.getRange(1, 1, 1, HEADERS.length).setFontWeight('bold');
|
|
|
sh.setFrozenRows(1);
|
|
|
}
|
|
|
return sh;
|
|
|
}
|
|
|
|
|
|
function daten_() {
|
|
|
var sh = sheet_();
|
|
|
var last = sh.getLastRow();
|
|
|
if (last < 2) return [];
|
|
|
return sh.getRange(2, 1, last - 1, HEADERS.length).getValues();
|
|
|
}
|
|
|
|
|
|
function zeileFuerId_(id) {
|
|
|
var rows = daten_();
|
|
|
for (var i = 0; i < rows.length; i++) {
|
|
|
if (String(rows[i][COL.ID]).trim() === String(id).trim()) {
|
|
|
return { row: i + 2, values: rows[i] };
|
|
|
}
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
function listeEintraege_() {
|
|
|
return daten_()
|
|
|
.filter(function (r) { return String(r[COL.ID]).trim() !== ''; })
|
|
|
.map(function (r) {
|
|
|
return {
|
|
|
id: String(r[COL.ID]),
|
|
|
bezeichnung: String(r[COL.BEZEICHNUNG] || ''),
|
|
|
standort: String(r[COL.STANDORT] || ''),
|
|
|
bildUrl: String(r[COL.BILD_URL] || ''),
|
|
|
audio: norm_(r[COL.AUDIO]),
|
|
|
bild: norm_(r[COL.BILD]),
|
|
|
sprache: norm_(r[COL.SPRACHE]),
|
|
|
status: statusKanon_(r[COL.STATUS]),
|
|
|
notiz: String(r[COL.NOTIZ] || ''),
|
|
|
erfasstAm: iso_(r[COL.ERFASST]),
|
|
|
geprueftAm: iso_(r[COL.GEPRUEFT]),
|
|
|
pruefer: String(r[COL.PRUEFER] || '')
|
|
|
};
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function iso_(v) {
|
|
|
if (!v) return '';
|
|
|
if (v instanceof Date) return v.toISOString();
|
|
|
return String(v);
|
|
|
}
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
/* Aktionen */
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
function erstelleEintrag_(p) {
|
|
|
var bez = String(p.bezeichnung || '').trim();
|
|
|
if (!bez) return { ok: false, error: 'Bezeichnung fehlt' };
|
|
|
|
|
|
var lock = LockService.getScriptLock();
|
|
|
lock.waitLock(20000);
|
|
|
try {
|
|
|
var sh = sheet_();
|
|
|
var id = naechsteId_(sh);
|
|
|
var jetzt = new Date();
|
|
|
|
|
|
var audio = norm_(p.audio);
|
|
|
var bild = norm_(p.bild);
|
|
|
var sprache = norm_(p.sprache);
|
|
|
var status = berechneStatus(audio, bild, sprache);
|
|
|
var geprueft = status === STATUS.OFFEN ? '' : jetzt;
|
|
|
|
|
|
var zeile = [];
|
|
|
zeile[COL.ID] = id;
|
|
|
zeile[COL.BEZEICHNUNG] = bez;
|
|
|
zeile[COL.STANDORT] = String(p.standort || '').trim();
|
|
|
zeile[COL.BILD_URL] = String(p.bildUrl || '').trim();
|
|
|
zeile[COL.AUDIO] = audio;
|
|
|
zeile[COL.BILD] = bild;
|
|
|
zeile[COL.SPRACHE] = sprache;
|
|
|
zeile[COL.STATUS] = status;
|
|
|
zeile[COL.NOTIZ] = String(p.notiz || '').trim();
|
|
|
zeile[COL.ERFASST] = jetzt;
|
|
|
zeile[COL.GEPRUEFT] = geprueft;
|
|
|
zeile[COL.PRUEFER] = String(p.pruefer || '').trim();
|
|
|
|
|
|
sh.appendRow(zeile);
|
|
|
faerbeZeile_(sh, sh.getLastRow(), status);
|
|
|
protokolliere_(id, 'angelegt', status, p.pruefer);
|
|
|
|
|
|
return { ok: true, id: id, status: status };
|
|
|
} finally {
|
|
|
lock.releaseLock();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Naechste freie ID. Der hoechste je vergebene Zaehler wird zusaetzlich in den
|
|
|
* Skript-Eigenschaften gemerkt, damit IDs geloeschter Eintraege nie erneut
|
|
|
* vergeben werden.
|
|
|
*/
|
|
|
function naechsteId_(sh) {
|
|
|
var props = PropertiesService.getScriptProperties();
|
|
|
var max = parseInt(props.getProperty('lastId') || '0', 10);
|
|
|
if (isNaN(max)) max = 0;
|
|
|
|
|
|
var rows = daten_();
|
|
|
var re = new RegExp('^' + CONFIG.ID_PREFIX + '-(\\d+)$');
|
|
|
for (var i = 0; i < rows.length; i++) {
|
|
|
var m = re.exec(String(rows[i][COL.ID]).trim());
|
|
|
if (m) {
|
|
|
var n = parseInt(m[1], 10);
|
|
|
if (n > max) max = n;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
var nummer = max + 1;
|
|
|
props.setProperty('lastId', String(nummer));
|
|
|
|
|
|
var next = String(nummer);
|
|
|
while (next.length < CONFIG.ID_PAD) next = '0' + next;
|
|
|
return CONFIG.ID_PREFIX + '-' + next;
|
|
|
}
|
|
|
|
|
|
function setzeStatus_(p) {
|
|
|
var treffer = zeileFuerId_(p.id);
|
|
|
if (!treffer) return { ok: false, error: 'ID nicht gefunden: ' + p.id };
|
|
|
|
|
|
var sh = sheet_();
|
|
|
var audio = norm_(p.audio);
|
|
|
var bild = norm_(p.bild);
|
|
|
var sprache = norm_(p.sprache);
|
|
|
var status = berechneStatus(audio, bild, sprache);
|
|
|
var jetzt = new Date();
|
|
|
|
|
|
sh.getRange(treffer.row, COL.AUDIO + 1, 1, 4)
|
|
|
.setValues([[audio, bild, sprache, status]]);
|
|
|
if (p.notiz !== undefined) {
|
|
|
sh.getRange(treffer.row, COL.NOTIZ + 1).setValue(String(p.notiz || ''));
|
|
|
}
|
|
|
sh.getRange(treffer.row, COL.GEPRUEFT + 1, 1, 2)
|
|
|
.setValues([[jetzt, String(p.pruefer || '')]]);
|
|
|
|
|
|
faerbeZeile_(sh, treffer.row, status);
|
|
|
protokolliere_(p.id, 'Status gesetzt', status, p.pruefer);
|
|
|
|
|
|
return { ok: true, id: String(p.id), status: status, geprueftAm: jetzt.toISOString() };
|
|
|
}
|
|
|
|
|
|
function aktualisiereEintrag_(p) {
|
|
|
var treffer = zeileFuerId_(p.id);
|
|
|
if (!treffer) return { ok: false, error: 'ID nicht gefunden: ' + p.id };
|
|
|
var sh = sheet_();
|
|
|
|
|
|
if (p.bezeichnung !== undefined) sh.getRange(treffer.row, COL.BEZEICHNUNG + 1).setValue(String(p.bezeichnung));
|
|
|
if (p.standort !== undefined) sh.getRange(treffer.row, COL.STANDORT + 1).setValue(String(p.standort));
|
|
|
if (p.bildUrl !== undefined) sh.getRange(treffer.row, COL.BILD_URL + 1).setValue(String(p.bildUrl));
|
|
|
if (p.notiz !== undefined) sh.getRange(treffer.row, COL.NOTIZ + 1).setValue(String(p.notiz));
|
|
|
|
|
|
protokolliere_(p.id, 'bearbeitet', '', p.pruefer);
|
|
|
return { ok: true, id: String(p.id) };
|
|
|
}
|
|
|
|
|
|
function archiviereEintrag_(p) {
|
|
|
var treffer = zeileFuerId_(p.id);
|
|
|
if (!treffer) return { ok: false, error: 'ID nicht gefunden: ' + p.id };
|
|
|
var sh = sheet_();
|
|
|
sh.getRange(treffer.row, COL.AUDIO + 1, 1, 4).setValues([['n.A.', 'n.A.', 'n.A.', STATUS.AUS]]);
|
|
|
faerbeZeile_(sh, treffer.row, STATUS.AUS);
|
|
|
protokolliere_(p.id, 'deaktiviert', STATUS.AUS, p.pruefer);
|
|
|
return { ok: true, id: String(p.id), status: STATUS.AUS };
|
|
|
}
|
|
|
|
|
|
function loescheEintrag_(p) {
|
|
|
var treffer = zeileFuerId_(p.id);
|
|
|
if (!treffer) return { ok: false, error: 'ID nicht gefunden: ' + p.id };
|
|
|
|
|
|
var lock = LockService.getScriptLock();
|
|
|
lock.waitLock(20000);
|
|
|
try {
|
|
|
// Zeile koennte sich zwischenzeitlich verschoben haben – erneut suchen.
|
|
|
treffer = zeileFuerId_(p.id);
|
|
|
if (!treffer) return { ok: false, error: 'ID nicht gefunden: ' + p.id };
|
|
|
|
|
|
var bez = String(treffer.values[COL.BEZEICHNUNG] || '');
|
|
|
var bild = String(treffer.values[COL.BILD_URL] || '');
|
|
|
sheet_().deleteRow(treffer.row);
|
|
|
protokolliere_(p.id, 'geloescht (' + bez + ')', bild, p.pruefer);
|
|
|
return { ok: true, id: String(p.id) };
|
|
|
} finally {
|
|
|
lock.releaseLock();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
/* Darstellung + Protokoll */
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
function faerbeZeile_(sh, row, status) {
|
|
|
var farben = {};
|
|
|
farben[STATUS.OK] = '#e6f4ea';
|
|
|
farben[STATUS.TEIL] = '#fef7e0';
|
|
|
farben[STATUS.FEHLER] = '#fce8e6';
|
|
|
farben[STATUS.AUS] = '#f1f3f4';
|
|
|
farben[STATUS.OFFEN] = '#ffffff';
|
|
|
sh.getRange(row, 1, 1, HEADERS.length).setBackground(farben[status] || '#ffffff');
|
|
|
}
|
|
|
|
|
|
function protokolliere_(id, aktion, status, pruefer) {
|
|
|
try {
|
|
|
var ss = SpreadsheetApp.getActiveSpreadsheet();
|
|
|
var log = ss.getSheetByName(CONFIG.LOG_SHEET_NAME);
|
|
|
if (!log) {
|
|
|
log = ss.insertSheet(CONFIG.LOG_SHEET_NAME);
|
|
|
log.appendRow(['Zeitpunkt', 'ID', 'Aktion', 'Status', 'Person']);
|
|
|
log.getRange(1, 1, 1, 5).setFontWeight('bold');
|
|
|
log.setFrozenRows(1);
|
|
|
}
|
|
|
log.appendRow([new Date(), String(id || ''), String(aktion || ''), String(status || ''), String(pruefer || '')]);
|
|
|
} catch (err) {
|
|
|
// Protokoll ist optional – Fehler hier duerfen die Aktion nicht abbrechen.
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
/* Einmalige Einrichtung / Menue */
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
function onOpen() {
|
|
|
SpreadsheetApp.getUi()
|
|
|
.createMenu('Monitor-Check')
|
|
|
.addItem('Tabelle einrichten', 'einrichten')
|
|
|
.addItem('Alle Status neu berechnen', 'statusNeuBerechnen')
|
|
|
.addToUi();
|
|
|
}
|
|
|
|
|
|
function einrichten() {
|
|
|
var sh = sheet_();
|
|
|
sh.setColumnWidth(COL.ID + 1, 90);
|
|
|
sh.setColumnWidth(COL.BEZEICHNUNG + 1, 200);
|
|
|
sh.setColumnWidth(COL.STANDORT + 1, 160);
|
|
|
sh.setColumnWidth(COL.BILD_URL + 1, 260);
|
|
|
sh.setColumnWidth(COL.STATUS + 1, 190);
|
|
|
sh.setColumnWidth(COL.NOTIZ + 1, 260);
|
|
|
|
|
|
var jn = SpreadsheetApp.newDataValidation()
|
|
|
.requireValueInList(['JA', 'NEIN', 'n.A.'], true)
|
|
|
.setAllowInvalid(false)
|
|
|
.build();
|
|
|
sh.getRange(2, COL.AUDIO + 1, 1000, 3).setDataValidation(jn);
|
|
|
|
|
|
protokolliere_('', 'Tabelle eingerichtet', '', Session.getActiveUser().getEmail());
|
|
|
SpreadsheetApp.getUi().alert('Tabelle "' + CONFIG.SHEET_NAME + '" ist eingerichtet.');
|
|
|
}
|
|
|
|
|
|
function statusNeuBerechnen() {
|
|
|
var sh = sheet_();
|
|
|
var rows = daten_();
|
|
|
for (var i = 0; i < rows.length; i++) {
|
|
|
if (String(rows[i][COL.ID]).trim() === '') continue;
|
|
|
var s = berechneStatus(rows[i][COL.AUDIO], rows[i][COL.BILD], rows[i][COL.SPRACHE]);
|
|
|
sh.getRange(i + 2, COL.STATUS + 1).setValue(s);
|
|
|
faerbeZeile_(sh, i + 2, s);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
/* Selbsttest (im Editor ausfuehrbar) */
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
function testStatusLogik() {
|
|
|
var faelle = [
|
|
|
[['JA', 'JA', 'JA'], STATUS.OK],
|
|
|
[['NEIN', 'NEIN', 'NEIN'], STATUS.FEHLER],
|
|
|
[['JA', 'NEIN', 'JA'], STATUS.TEIL],
|
|
|
[['JA', 'NEIN', 'NEIN'], STATUS.TEIL],
|
|
|
[['n.A.', 'n.A.', 'n.A.'], STATUS.AUS],
|
|
|
[['n.A.', 'JA', 'JA'], STATUS.OK],
|
|
|
[['n.A.', 'NEIN', 'NEIN'], STATUS.FEHLER],
|
|
|
[['n.A.', 'JA', 'NEIN'], STATUS.TEIL],
|
|
|
[['', '', ''], STATUS.OFFEN]
|
|
|
];
|
|
|
var fehler = [];
|
|
|
faelle.forEach(function (f) {
|
|
|
var ist = berechneStatus(f[0][0], f[0][1], f[0][2]);
|
|
|
if (ist !== f[1]) fehler.push(f[0].join('/') + ' -> ' + ist + ' (erwartet ' + f[1] + ')');
|
|
|
});
|
|
|
if (fehler.length) throw new Error('Fehlgeschlagen:\n' + fehler.join('\n'));
|
|
|
Logger.log('Alle ' + faelle.length + ' Testfaelle bestanden.');
|
|
|
}
|