Memeriksa Koneksi...
Dokumentasi Klinis Apotek & Integrasi WhatsApp
Akun Admin Awal:
Username: admin | Password: 123
Dibuat otomatis saat setupDatabase() dijalankan.
Pratinjau & sesuaikan pesan sebelum dikirim
Format otomatis dikonversi ke format internasional (+62).
Solusi koneksi terputus: Pastikan Web App diset ke "Execute as: Me" dan "Who has access: Anyone".
var SHEET_HEADERS = {
Pengkajian: [
'id','noResep','tanggal','pasien','namaPasien','noHp','umur','bbTb',
'dokter','namaDokter','diagnosa','riwayatAlergi','kehamilan',
'penyakitPenyerta','checklist','kesimpulan','statusPengkajian','catatan'
],
PIO: [
'id','tanggal','pasien','namaPasien','penanya','namaPenanya','noHp',
'jenisPelayanan','kategoriPenanya','materi','catatan'
],
Konseling: [
'id','tanggal','pasien','namaPasien','noHp','diagnosis','obat','teachBack','catatan'
],
CekKesehatan: [
'id','tanggal','pasien','namaPasien','noHp','jenisKelamin','sistol','diastol','gulaDarah',
'jenisGula','kolesterol','asamUrat','statusTekananDarah','statusGulaDarah',
'statusKolesterol','statusAsamUrat','statusKeseluruhan','catatan'
],
Pasien: [
'id','nama','noHp','umur','bbTb','riwayatAlergi'
],
Users: [
'id','username','passwordHash','passwordSalt','nama','role','status','createdAt','updatedAt'
]
};
var SESSION_SECONDS = 21600; // 6 jam
/**
* JALANKAN SEKALI SECARA MANUAL dari editor Apps Script.
* Membuat seluruh sheet + header dan akun admin awal: admin / 123.
*/
function setupDatabase() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
if (!ss) {
throw new Error('Spreadsheet aktif tidak ditemukan. Buka Apps Script dari menu Ekstensi di Google Sheet yang akan dipakai.');
}
PropertiesService.getScriptProperties().setProperty('APOCARE_SPREADSHEET_ID', ss.getId());
ensureDatabaseStructure(ss);
ensureDefaultAdmin(ss);
SpreadsheetApp.flush();
return 'Database ApoCare Pro siap. Akun admin awal: admin / 123';
}
function getDatabase() {
var spreadsheetId = PropertiesService.getScriptProperties().getProperty('APOCARE_SPREADSHEET_ID');
if (spreadsheetId) {
return SpreadsheetApp.openById(spreadsheetId);
}
var active = SpreadsheetApp.getActiveSpreadsheet();
if (active) {
PropertiesService.getScriptProperties().setProperty('APOCARE_SPREADSHEET_ID', active.getId());
return active;
}
throw new Error('Database belum diinisialisasi. Jalankan fungsi setupDatabase() satu kali dari editor Apps Script.');
}
function ensureDatabaseStructure(ss) {
for (var sheetName in SHEET_HEADERS) {
if (SHEET_HEADERS.hasOwnProperty(sheetName)) {
ensureSheetHeaders(ss, sheetName, SHEET_HEADERS[sheetName]);
}
}
}
function ensureSheetHeaders(ss, sheetName, requiredHeaders) {
var sheet = ss.getSheetByName(sheetName);
if (!sheet) sheet = ss.insertSheet(sheetName);
var existingHeaders = [];
var lastColumn = sheet.getLastColumn();
var lastRow = sheet.getLastRow();
if (lastRow >= 1 && lastColumn >= 1) {
var rawHeaders = sheet.getRange(1, 1, 1, lastColumn).getValues()[0];
for (var i = 0; i < rawHeaders.length; i++) {
var value = String(rawHeaders[i] || '').trim();
if (value) existingHeaders.push(value);
}
}
if (existingHeaders.length === 0) {
sheet.clearContents();
sheet.getRange(1, 1, 1, requiredHeaders.length).setValues([requiredHeaders]);
} else {
for (var h = 0; h < requiredHeaders.length; h++) {
var header = requiredHeaders[h];
if (existingHeaders.indexOf(header) === -1) {
existingHeaders.push(header);
sheet.getRange(1, existingHeaders.length).setValue(header);
}
}
}
var finalLastColumn = sheet.getLastColumn();
if (finalLastColumn > 0) {
sheet.getRange(1, 1, 1, finalLastColumn).setFontWeight('bold');
sheet.setFrozenRows(1);
}
return sheet;
}
function doGet() {
try {
var ss = getDatabase();
ensureDatabaseStructure(ss);
ensureDefaultAdmin(ss);
return ContentService.createTextOutput('ApoCare Pro Backend Active - Database Ready').setMimeType(ContentService.MimeType.TEXT);
} catch (err) {
return ContentService.createTextOutput('ApoCare Pro Backend Error: ' + String(err)).setMimeType(ContentService.MimeType.TEXT);
}
}
function doPost(e) {
try {
if (!e || !e.postData || !e.postData.contents) {
return responseJSON({ status: 'error', message: 'Request body kosong' });
}
var contents = JSON.parse(e.postData.contents);
var action = contents.action || '';
var data = contents.data || {};
var token = contents.token || '';
var ss = getDatabase();
ensureDatabaseStructure(ss);
ensureDefaultAdmin(ss);
if (action === 'login') return handleUserLogin(ss, data);
if (action === 'validateSession') return handleValidateSession(token);
if (action === 'logout') return handleLogoutSession(token);
var session = requireSession(token);
if (action === 'getAllData') {
return responseJSON({
status: 'success',
data: {
pengkajian: getSheetData(ss, 'Pengkajian'),
pio: getSheetData(ss, 'PIO'),
konseling: getSheetData(ss, 'Konseling'),
cekKesehatan: getSheetData(ss, 'CekKesehatan'),
pasien: getSheetData(ss, 'Pasien')
}
});
}
if (action === 'listUsers') {
requireAdmin(session);
return responseJSON({ status: 'success', data: listUsersSafe(ss) });
}
if (action === 'saveUser') {
requireAdmin(session);
return responseJSON({ status: 'success', data: saveManagedUser(ss, data) });
}
if (action === 'deleteUser') {
requireAdmin(session);
deleteManagedUser(ss, data.id, session);
return responseJSON({ status: 'success' });
}
if (action === 'savePengkajian') { saveToSheet(ss, 'Pengkajian', data); return responseJSON({ status: 'success' }); }
if (action === 'deletePengkajian') { deleteFromSheet(ss, 'Pengkajian', data.id); return responseJSON({ status: 'success' }); }
if (action === 'savePIO') { saveToSheet(ss, 'PIO', data); return responseJSON({ status: 'success' }); }
if (action === 'deletePIO') { deleteFromSheet(ss, 'PIO', data.id); return responseJSON({ status: 'success' }); }
if (action === 'saveKonseling') { saveToSheet(ss, 'Konseling', data); return responseJSON({ status: 'success' }); }
if (action === 'deleteKonseling') { deleteFromSheet(ss, 'Konseling', data.id); return responseJSON({ status: 'success' }); }
if (action === 'saveCekKesehatan') { saveToSheet(ss, 'CekKesehatan', data); return responseJSON({ status: 'success' }); }
if (action === 'deleteCekKesehatan') { deleteFromSheet(ss, 'CekKesehatan', data.id); return responseJSON({ status: 'success' }); }
if (action === 'savePasien') { saveToSheet(ss, 'Pasien', data); return responseJSON({ status: 'success' }); }
return responseJSON({ status: 'error', message: 'Action tidak dikenal: ' + action });
} catch (err) {
return responseJSON({ status: 'error', message: String(err && err.message ? err.message : err) });
}
}
function responseJSON(obj) {
return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON);
}
// ==================== AUTH / USER MANAGEMENT ====================
function ensureDefaultAdmin(ss) {
var users = getSheetData(ss, 'Users');
for (var i = 0; i < users.length; i++) {
if (String(users[i].username || '').toLowerCase() === 'admin') return;
}
var salt = Utilities.getUuid();
var now = new Date().toISOString();
saveToSheet(ss, 'Users', {
id: 'U-ADMIN',
username: 'admin',
passwordHash: hashPassword('123', salt),
passwordSalt: salt,
nama: 'Apoteker Admin',
role: 'Admin',
status: 'Aktif',
createdAt: now,
updatedAt: now
});
}
function hashPassword(password, salt) {
var input = String(salt || '') + '|' + String(password || '');
var bytes = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, input, Utilities.Charset.UTF_8);
var hex = '';
for (var i = 0; i < bytes.length; i++) {
var v = bytes[i];
if (v < 0) v += 256;
var h = v.toString(16);
if (h.length === 1) h = '0' + h;
hex += h;
}
return hex;
}
function handleUserLogin(ss, data) {
var username = String(data.username || '').trim().toLowerCase();
var password = String(data.password || '');
if (!username || !password) return responseJSON({ status: 'error', message: 'Username dan password wajib diisi.' });
var users = getSheetData(ss, 'Users');
var found = null;
for (var i = 0; i < users.length; i++) {
if (String(users[i].username || '').trim().toLowerCase() === username) {
found = users[i];
break;
}
}
if (!found || String(found.status || '').toLowerCase() !== 'aktif') {
return responseJSON({ status: 'error', message: 'Username/password salah atau akun tidak aktif.' });
}
var expected = String(found.passwordHash || '');
var actual = hashPassword(password, String(found.passwordSalt || ''));
if (!expected || expected !== actual) {
return responseJSON({ status: 'error', message: 'Username/password salah atau akun tidak aktif.' });
}
var user = sanitizeUser(found);
var token = Utilities.getUuid() + Utilities.getUuid();
CacheService.getScriptCache().put('session_' + token, JSON.stringify(user), SESSION_SECONDS);
return responseJSON({ status: 'success', token: token, user: user });
}
function handleValidateSession(token) {
try {
var session = requireSession(token);
return responseJSON({ status: 'success', user: session });
} catch (err) {
return responseJSON({ status: 'error', message: 'Sesi login berakhir. Silakan login kembali.' });
}
}
function handleLogoutSession(token) {
if (token) CacheService.getScriptCache().remove('session_' + token);
return responseJSON({ status: 'success' });
}
function requireSession(token) {
if (!token) throw new Error('Sesi login tidak tersedia. Silakan login kembali.');
var raw = CacheService.getScriptCache().get('session_' + token);
if (!raw) throw new Error('Sesi login berakhir. Silakan login kembali.');
var user = JSON.parse(raw);
CacheService.getScriptCache().put('session_' + token, raw, SESSION_SECONDS);
return user;
}
function requireAdmin(session) {
if (!session || String(session.role || '').toLowerCase() !== 'admin') {
throw new Error('Akses ditolak. Menu ini hanya untuk Admin.');
}
}
function sanitizeUser(user) {
return {
id: String(user.id || ''),
username: String(user.username || ''),
nama: String(user.nama || user.username || ''),
role: String(user.role || 'Petugas'),
status: String(user.status || 'Aktif'),
createdAt: String(user.createdAt || ''),
updatedAt: String(user.updatedAt || '')
};
}
function listUsersSafe(ss) {
var users = getSheetData(ss, 'Users');
var result = [];
for (var i = 0; i < users.length; i++) result.push(sanitizeUser(users[i]));
return result;
}
function saveManagedUser(ss, data) {
var id = String(data.id || '').trim();
var username = String(data.username || '').trim();
var usernameLower = username.toLowerCase();
var nama = String(data.nama || '').trim();
var role = String(data.role || 'Petugas') === 'Admin' ? 'Admin' : 'Petugas';
var status = String(data.status || 'Aktif') === 'Nonaktif' ? 'Nonaktif' : 'Aktif';
var password = String(data.password || '');
if (!username || !nama) throw new Error('Username dan nama wajib diisi.');
if (!/^[A-Za-z0-9._-]{3,30}$/.test(username)) throw new Error('Username 3-30 karakter dan hanya boleh huruf, angka, titik, garis bawah, atau tanda minus.');
var users = getSheetData(ss, 'Users');
var existing = null;
for (var i = 0; i < users.length; i++) {
var candidate = users[i];
if (String(candidate.username || '').trim().toLowerCase() === usernameLower && String(candidate.id || '') !== id) {
throw new Error('Username sudah digunakan.');
}
if (id && String(candidate.id || '') === id) existing = candidate;
}
var now = new Date().toISOString();
var item;
if (existing) {
if (String(existing.username || '').toLowerCase() === 'admin') {
role = 'Admin';
status = 'Aktif';
username = 'admin';
}
item = {
id: String(existing.id),
username: username,
passwordHash: String(existing.passwordHash || ''),
passwordSalt: String(existing.passwordSalt || ''),
nama: nama,
role: role,
status: status,
createdAt: String(existing.createdAt || now),
updatedAt: now
};
if (password) {
if (password.length < 6) throw new Error('Password baru minimal 6 karakter.');
item.passwordSalt = Utilities.getUuid();
item.passwordHash = hashPassword(password, item.passwordSalt);
}
} else {
if (password.length < 6) throw new Error('Password minimal 6 karakter untuk user baru.');
var salt = Utilities.getUuid();
item = {
id: 'U-' + Utilities.getUuid().slice(0, 8).toUpperCase(),
username: username,
passwordHash: hashPassword(password, salt),
passwordSalt: salt,
nama: nama,
role: role,
status: status,
createdAt: now,
updatedAt: now
};
}
saveToSheet(ss, 'Users', item);
return sanitizeUser(item);
}
function deleteManagedUser(ss, userId, session) {
userId = String(userId || '');
if (!userId) throw new Error('ID user tidak tersedia.');
if (String(session.id || '') === userId) throw new Error('Akun yang sedang digunakan tidak dapat dihapus.');
var users = getSheetData(ss, 'Users');
for (var i = 0; i < users.length; i++) {
if (String(users[i].id || '') === userId) {
if (String(users[i].username || '').toLowerCase() === 'admin') throw new Error('Akun admin utama tidak dapat dihapus.');
break;
}
}
deleteFromSheet(ss, 'Users', userId);
}
// ==================== GENERIC SHEET HELPERS ====================
function getSheetData(ss, sheetName) {
var requiredHeaders = SHEET_HEADERS[sheetName] || [];
var sheet = ensureSheetHeaders(ss, sheetName, requiredHeaders);
if (sheet.getLastRow() < 2) return [];
var lastColumn = sheet.getLastColumn();
var data = sheet.getRange(1, 1, sheet.getLastRow(), lastColumn).getValues();
var headers = data[0];
var result = [];
for (var i = 1; i < data.length; i++) {
var row = data[i];
var obj = {};
var hasValue = false;
for (var j = 0; j < headers.length; j++) {
var header = String(headers[j] || '').trim();
if (!header) continue;
var val = row[j];
if (val !== '' && val !== null) hasValue = true;
if (header === 'checklist' || header === 'materi') {
if (typeof val === 'string' && val !== '') {
try { val = JSON.parse(val); } catch (parseErr) {}
}
}
obj[header] = val;
}
if (hasValue) result.push(obj);
}
return result;
}
function saveToSheet(ss, sheetName, item) {
if (!item || typeof item !== 'object') throw new Error('Data yang akan disimpan tidak valid');
var itemKeys = Object.keys(item);
if (itemKeys.length === 0) throw new Error('Data kosong');
var baseHeaders = SHEET_HEADERS[sheetName] || [];
var requiredHeaders = baseHeaders.slice();
for (var k = 0; k < itemKeys.length; k++) {
if (requiredHeaders.indexOf(itemKeys[k]) === -1) requiredHeaders.push(itemKeys[k]);
}
var sheet = ensureSheetHeaders(ss, sheetName, requiredHeaders);
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
for (var h = 0; h < headers.length; h++) headers[h] = String(headers[h] || '').trim();
var existingRowIndex = -1;
var idColIndex = headers.indexOf('id');
if (idColIndex !== -1 && sheet.getLastRow() > 1 && item.id !== undefined) {
var idValues = sheet.getRange(2, idColIndex + 1, sheet.getLastRow() - 1, 1).getValues();
for (var r = 0; r < idValues.length; r++) {
if (String(idValues[r][0]) === String(item.id)) { existingRowIndex = r + 2; break; }
}
}
var rowData = [];
for (var c = 0; c < headers.length; c++) {
var headerName = headers[c];
var value = item[headerName] !== undefined ? item[headerName] : '';
if (value !== null && typeof value === 'object') value = JSON.stringify(value);
rowData.push(value);
}
if (existingRowIndex !== -1) sheet.getRange(existingRowIndex, 1, 1, rowData.length).setValues([rowData]);
else sheet.appendRow(rowData);
}
function deleteFromSheet(ss, sheetName, itemId) {
var requiredHeaders = SHEET_HEADERS[sheetName] || [];
var sheet = ensureSheetHeaders(ss, sheetName, requiredHeaders);
if (sheet.getLastRow() < 2) return;
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var idColIndex = -1;
for (var i = 0; i < headers.length; i++) {
if (String(headers[i]).trim() === 'id') { idColIndex = i; break; }
}
if (idColIndex === -1) return;
var idValues = sheet.getRange(2, idColIndex + 1, sheet.getLastRow() - 1, 1).getValues();
for (var r = 0; r < idValues.length; r++) {
if (String(idValues[r][0]) === String(itemId)) {
sheet.deleteRow(r + 2);
return;
}
}
}
Apakah Anda yakin ingin menghapus data ini dari database?
Arahkan kamera ke barcode/QR resep digital