/** * © 2026 shortfused.dev. Wszelkie prawa zastrzeżone. Licencja: /licencja * Dziennik ciśnienia — skrypt do Twojego Arkusza Google. * * Instalacja: * 1. W arkuszu: Rozszerzenia → Apps Script, wklej ten plik w miejsce Code.gs, zapisz. * 2. Odśwież arkusz, wybierz menu „Dziennik ciśnienia → Konfiguracja”, zatwierdź uprawnienia, skopiuj token. * 3. Wdróż → Nowe wdrożenie → Aplikacja internetowa. Wykonaj jako: Ja. Kto ma dostęp: Każdy. * 4. Adres /exec i token wklej w aplikacji (Ustawienia). * * Bez ważnego tokenu skrypt niczego nie zapisze ani nie odczyta. * Po zmianie kodu: Wdróż → Zarządzaj wdrożeniami → edytuj → Nowa wersja (adres się nie zmienia). */ const SHEET_NAME = 'Pomiary'; const HEADERS = ['ID', 'Data', 'Godzina', 'Skurczowe (mmHg)', 'Rozkurczowe (mmHg)', 'Tętno (bpm)', 'Ręka', 'Notatka', 'Zapisano']; const TEXT_COLUMNS = [1, 2, 3, 8, 9]; // ID, Data, Godzina, Notatka, Zapisano — jako zwykły tekst, żeby Arkusze nie zamieniały ich na daty const COL = { id: 0, date: 1, time: 2, sys: 3, dia: 4, pulse: 5, arm: 6, note: 7, createdAt: 8 }; /* ---------- Menu w arkuszu ---------- */ function onOpen() { SpreadsheetApp.getUi() .createMenu('Dziennik ciśnienia') .addItem('Konfiguracja (pokaż token)', 'showSetup') .addItem('Wygeneruj nowy token', 'resetToken') .addToUi(); } function showSetup() { ensureSheet_(); const token = getToken_(); let url = ''; try { url = ScriptApp.getService().getUrl() || ''; } catch (e) { /* brak wdrożenia */ } SpreadsheetApp.getUi().alert( 'Dziennik ciśnienia — konfiguracja', 'Token (wklej w aplikacji):\n\n' + token + '\n\n' + (url && /\/exec$/.test(url) ? 'Adres aplikacji internetowej:\n\n' + url : 'Adres: wykonaj Wdróż → Nowe wdrożenie → Aplikacja internetowa (Wykonaj jako: Ja, Dostęp: Każdy) i skopiuj adres kończący się na /exec.'), SpreadsheetApp.getUi().ButtonSet.OK ); } function resetToken() { const ui = SpreadsheetApp.getUi(); const ok = ui.alert('Nowy token', 'Stary token przestanie działać i trzeba będzie wkleić nowy w aplikacji. Kontynuować?', ui.ButtonSet.YES_NO); if (ok !== ui.Button.YES) return; PropertiesService.getScriptProperties().deleteProperty('TOKEN'); showSetup(); } /* ---------- Aplikacja internetowa ---------- */ function doGet() { return ContentService.createTextOutput('Dziennik ciśnienia: skrypt działa. Wklej ten adres w aplikacji.'); } function doPost(e) { try { const body = JSON.parse((e && e.postData && e.postData.contents) || '{}'); checkToken_(body.token); const lock = LockService.getScriptLock(); lock.waitLock(15000); try { const sheet = ensureSheet_(); switch (body.action) { case 'ping': return json_({ ok: true, sheet: SpreadsheetApp.getActive().getName() }); case 'sync': return json_(Object.assign({ ok: true }, applySync_(sheet, body.upsert || [], body.delete || []))); case 'list': return json_({ ok: true, entries: listEntries_(sheet) }); default: return json_({ ok: false, error: 'Nieznana akcja' }); } } finally { lock.releaseLock(); } } catch (err) { return json_({ ok: false, error: String((err && err.message) || err) }); } } /* ---------- Logika ---------- */ function applySync_(sheet, upsert, del) { const idRow = indexIds_(sheet); // id -> numer wiersza let updated = 0; const toAppend = []; upsert.forEach(function (raw) { const row = toRow_(raw); if (!row) return; const r = idRow[row[COL.id]]; if (r > 0) { sheet.getRange(r, 1, 1, HEADERS.length).setValues([row]); updated++; } else if (r !== -1) { toAppend.push(row); idRow[row[COL.id]] = -1; } // -1 = już dopisany w tej paczce }); if (toAppend.length) { const start = sheet.getLastRow() + 1; TEXT_COLUMNS.forEach(function (c) { sheet.getRange(start, c, toAppend.length, 1).setNumberFormat('@'); }); sheet.getRange(start, 1, toAppend.length, HEADERS.length).setValues(toAppend); } // Usuwanie od dołu, żeby numery wierszy się nie przesuwały. const fresh = indexIds_(sheet); const rows = del.map(function (id) { return fresh[String(id)]; }).filter(Boolean).sort(function (a, b) { return b - a; }); rows.forEach(function (r) { sheet.deleteRow(r); }); return { added: toAppend.length, updated: updated, deleted: rows.length }; } function toRow_(e) { if (!e || !e.id) return null; const sys = Number(e.sys), dia = Number(e.dia), pulse = Number(e.pulse); if (!isFinite(sys) || !isFinite(dia) || !isFinite(pulse)) return null; if (!/^\d{4}-\d{2}-\d{2}$/.test(String(e.date)) || !/^\d{2}:\d{2}$/.test(String(e.time))) return null; return [ String(e.id).slice(0, 64), String(e.date), String(e.time), sys, dia, pulse, String(e.arm || '').slice(0, 10), String(e.note || '').slice(0, 200), String(e.createdAt || new Date().toISOString()), ]; } function listEntries_(sheet) { const last = sheet.getLastRow(); if (last < 2) return []; const range = sheet.getRange(2, 1, last - 1, HEADERS.length); const values = range.getValues(); const tz = Session.getScriptTimeZone(); const out = []; let filledIds = false; values.forEach(function (v, i) { if (v[COL.sys] === '' || v[COL.dia] === '') return; // pusty wiersz if (!v[COL.id]) { // wiersz dopisany ręcznie w arkuszu — nadaj mu ID v[COL.id] = Utilities.getUuid(); sheet.getRange(i + 2, 1).setNumberFormat('@').setValue(v[COL.id]); filledIds = true; } const date = v[COL.date] instanceof Date ? Utilities.formatDate(v[COL.date], tz, 'yyyy-MM-dd') : String(v[COL.date]); const time = v[COL.time] instanceof Date ? Utilities.formatDate(v[COL.time], tz, 'HH:mm') : String(v[COL.time]).slice(0, 5); out.push({ id: String(v[COL.id]), date: date, time: time, sys: Number(v[COL.sys]), dia: Number(v[COL.dia]), pulse: Number(v[COL.pulse]), arm: String(v[COL.arm] || ''), note: String(v[COL.note] || ''), createdAt: String(v[COL.createdAt] || ''), }); }); if (filledIds) SpreadsheetApp.flush(); return out; } function indexIds_(sheet) { const map = {}; const last = sheet.getLastRow(); if (last < 2) return map; sheet.getRange(2, 1, last - 1, 1).getValues().forEach(function (v, i) { if (v[0]) map[String(v[0])] = i + 2; }); return map; } function ensureSheet_() { const ss = SpreadsheetApp.getActive(); let sheet = ss.getSheetByName(SHEET_NAME); if (!sheet) sheet = ss.insertSheet(SHEET_NAME, 0); if (sheet.getLastRow() === 0) { sheet.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]).setFontWeight('bold'); sheet.setFrozenRows(1); TEXT_COLUMNS.forEach(function (c) { sheet.getRange(2, c, sheet.getMaxRows() - 1, 1).setNumberFormat('@'); }); sheet.setColumnWidth(1, 90); sheet.hideColumns(1); // ID jest techniczne } return sheet; } function getToken_() { const props = PropertiesService.getScriptProperties(); let t = props.getProperty('TOKEN'); if (!t) { t = Utilities.getUuid().replace(/-/g, '') + Utilities.getUuid().replace(/-/g, '').slice(0, 8); props.setProperty('TOKEN', t); } return t; } function checkToken_(token) { const expected = PropertiesService.getScriptProperties().getProperty('TOKEN'); if (!expected) throw new Error('Skrypt nie jest skonfigurowany. Otwórz arkusz i wybierz Dziennik ciśnienia → Konfiguracja.'); if (typeof token !== 'string' || token !== expected) throw new Error('Nieprawidłowy token.'); } function json_(obj) { return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON); }