Menjadikan hidup lebih bermakna

This is default featured slide 1 title

Go to Blogger edit html and find these sentences.Now replace these sentences with your own descriptions.This theme is Bloggerized by Lasantha Bandara - Premiumbloggertemplates.com.

This is default featured slide 2 title

Go to Blogger edit html and find these sentences.Now replace these sentences with your own descriptions.This theme is Bloggerized by Lasantha Bandara - Premiumbloggertemplates.com.

This is default featured slide 3 title

Go to Blogger edit html and find these sentences.Now replace these sentences with your own descriptions.This theme is Bloggerized by Lasantha Bandara - Premiumbloggertemplates.com.

This is default featured slide 4 title

Go to Blogger edit html and find these sentences.Now replace these sentences with your own descriptions.This theme is Bloggerized by Lasantha Bandara - Premiumbloggertemplates.com.

This is default featured slide 5 title

Go to Blogger edit html and find these sentences.Now replace these sentences with your own descriptions.This theme is Bloggerized by Lasantha Bandara - Premiumbloggertemplates.com.

Monday, October 26, 2026

/** * Sistem Absensi Guru & Kepala Sekolah — Google Apps Script * Pasang Code.gs dan Index.html dalam satu proyek Apps Script. * Jalankan setupSpreadsheet() sekali dari editor, lalu deploy sebagai Web App. */ const SPREADSHEET_ID = '1ErzL6V_jA-JKwFd5B1M--Nx_oBCO3DQyVSdJbjU6s-g'; // Isi ID spreadsheet bila proyek bukan terikat (bound) ke Google Sheets. const SHEET_STAFF = 'Guru'; const SHEET_ATTENDANCE = 'AbsensiGuru'; const SHEET_SETTINGS = 'Settings'; const SHEET_HEADERS = { Guru: ['NIP', 'Nama', 'Jabatan', 'MataPelajaranUnit', 'Status', 'FaceDescriptor', 'FotoReferensi', 'DibuatPada'], AbsensiGuru: ['ID', 'NIP', 'Nama', 'Jabatan', 'Tanggal', 'JamMasuk', 'StatusMasuk', 'FotoMasuk', 'SkorWajahMasuk', 'LatitudeMasuk', 'LongitudeMasuk', 'AlamatMasuk', 'JamPulang', 'StatusPulang', 'FotoPulang', 'SkorWajahPulang', 'LatitudePulang', 'LongitudePulang', 'AlamatPulang', 'Keterangan'], Settings: ['Kunci', 'Nilai'] }; function doGet(e) { const page = (e && e.parameter && e.parameter.page === 'admin') ? 'admin' : 'absen'; return HtmlService.createTemplateFromFile('Index').evaluate() .setTitle(page === 'admin' ? 'Admin Absensi Guru' : 'Absensi Guru & Kepala Sekolah') .addMetaTag('viewport', 'width=device-width, initial-scale=1'); } function include(filename) { return HtmlService.createHtmlOutputFromFile(filename).getContent(); } function getSpreadsheet_() { if (SPREADSHEET_ID && SPREADSHEET_ID.trim()) return SpreadsheetApp.openById(SPREADSHEET_ID.trim()); const ss = SpreadsheetApp.getActiveSpreadsheet(); if (!ss) throw new Error('Spreadsheet belum ditentukan. Isi SPREADSHEET_ID pada Code.gs atau buka Apps Script dari Google Sheets.'); return ss; } function getSheet_(name) { const ss = getSpreadsheet_(); let sh = ss.getSheetByName(name); if (!sh) sh = ss.insertSheet(name); return sh; } function setupSpreadsheet() { const ss = getSpreadsheet_(); Object.keys(SHEET_HEADERS).forEach(name => { const sh = getSheet_(name); if (sh.getLastRow() === 0) sh.appendRow(SHEET_HEADERS[name]); else { const first = sh.getRange(1, 1, 1, Math.max(sh.getLastColumn(), SHEET_HEADERS[name].length)).getDisplayValues()[0]; if (first.join('').trim() === '') sh.getRange(1, 1, 1, SHEET_HEADERS[name].length).setValues([SHEET_HEADERS[name]]); } sh.setFrozenRows(1); sh.getRange(1, 1, 1, SHEET_HEADERS[name].length).setFontWeight('bold').setBackground('#dbeafe'); }); const settings = getSheet_(SHEET_SETTINGS); const existing = settings.getLastRow() > 1 ? settings.getRange(2, 1, settings.getLastRow() - 1, 2).getDisplayValues() : []; const keys = existing.map(r => r[0]); const defaults = [ ['JamMasuk', '07:15'], ['JamPulang', '14:00'], ['AmbangWajah', '0.50'], ['AdminPIN', 'GANTI_PIN_ADMIN'], ['FolderFotoID', ''] ]; defaults.forEach(r => { if (!keys.includes(r[0])) settings.appendRow(r); }); // Buat folder khusus foto absensi jika belum ada. const folderId = getSetting_('FolderFotoID'); if (!folderId) { const folder = DriveApp.createFolder('Foto Absensi Guru dan Kepala Sekolah'); setSetting_('FolderFotoID', folder.getId()); } return 'Setup selesai. Sheet Guru, AbsensiGuru, dan Settings siap digunakan. Segera ganti AdminPIN di sheet Settings.'; } function getSetting_(key) { const sh = getSheet_(SHEET_SETTINGS); if (sh.getLastRow() < 2) return ''; const rows = sh.getRange(2, 1, sh.getLastRow() - 1, 2).getDisplayValues(); for (let i = 0; i < rows.length; i++) if (rows[i][0] === key) return rows[i][1]; return ''; } function setSetting_(key, value) { const sh = getSheet_(SHEET_SETTINGS); const rows = sh.getLastRow() > 1 ? sh.getRange(2, 1, sh.getLastRow() - 1, 2).getDisplayValues() : []; for (let i = 0; i < rows.length; i++) { if (rows[i][0] === key) { sh.getRange(i + 2, 2).setValue(value); return; } } sh.appendRow([key, value]); } function requireAdmin_(pin) { const configured = getSetting_('AdminPIN'); if (!configured || configured === 'GANTI_PIN_ADMIN') throw new Error('AdminPIN belum diganti. Ubah nilai AdminPIN di sheet Settings terlebih dahulu.'); if (String(pin || '') !== String(configured)) throw new Error('PIN admin salah.'); } function adminLogin(pin) { requireAdmin_(pin); return { success: true, message: 'Login admin berhasil.' }; } function saveSettings(pin, data) { requireAdmin_(pin); if (data.jamMasuk) setSetting_('JamMasuk', data.jamMasuk); if (data.jamPulang) setSetting_('JamPulang', data.jamPulang); if (data.ambangWajah !== undefined && data.ambangWajah !== '') setSetting_('AmbangWajah', String(data.ambangWajah)); return { success: true }; } function getPublicSettings() { return { jamMasuk: getSetting_('JamMasuk') || '07:15', jamPulang: getSetting_('JamPulang') || '14:00', ambangWajah: Number(getSetting_('AmbangWajah') || 0.5) }; } function addStaff(pin, data) { requireAdmin_(pin); const nip = String(data.nip || '').trim(); const nama = String(data.nama || '').trim(); const jabatan = String(data.jabatan || '').trim(); if (!nip || !nama || !['Guru', 'Kepala Sekolah'].includes(jabatan)) throw new Error('NIP, nama, dan jabatan wajib diisi.'); const sh = getSheet_(SHEET_STAFF); const rows = sh.getDataRange().getDisplayValues(); for (let i = 1; i < rows.length; i++) if (String(rows[i][0]).trim() === nip) throw new Error('NIP sudah terdaftar.'); sh.appendRow([nip, nama, jabatan, String(data.unit || '').trim(), 'Aktif', '', '', new Date()]); return { success: true }; } function updateStaffStatus(pin, nip, status) { requireAdmin_(pin); const sh = getSheet_(SHEET_STAFF), rows = sh.getDataRange().getDisplayValues(); for (let i = 1; i < rows.length; i++) if (String(rows[i][0]).trim() === String(nip).trim()) { sh.getRange(i + 1, 5).setValue(status === 'Nonaktif' ? 'Nonaktif' : 'Aktif'); return { success: true }; } throw new Error('Data pegawai tidak ditemukan.'); } function getStaffByNip(nip) { const target = String(nip || '').trim(); if (!target) throw new Error('Masukkan NIP.'); const sh = getSheet_(SHEET_STAFF), rows = sh.getDataRange().getValues(); for (let i = 1; i < rows.length; i++) { if (String(rows[i][0]).trim() === target && String(rows[i][4]) === 'Aktif') { return { success: true, staff: { nip: String(rows[i][0]), nama: String(rows[i][1]), jabatan: String(rows[i][2]), unit: String(rows[i][3] || ''), faceDescriptor: rows[i][5] ? JSON.parse(rows[i][5]) : null, fotoReferensi: String(rows[i][6] || '') } }; } } return { success: false, message: 'NIP tidak ditemukan atau status pegawai nonaktif.' }; } function registerStaffFace(pin, data) { requireAdmin_(pin); const nip = String(data.nip || '').trim(); if (!nip || !Array.isArray(data.faceDescriptor) || data.faceDescriptor.length < 64) throw new Error('Data wajah tidak valid. Ambil foto dengan wajah terlihat jelas.'); const sh = getSheet_(SHEET_STAFF), rows = sh.getDataRange().getValues(); let rowNumber = 0; for (let i = 1; i < rows.length; i++) if (String(rows[i][0]).trim() === nip) { rowNumber = i + 1; break; } if (!rowNumber) throw new Error('NIP tidak ditemukan. Tambahkan pegawai terlebih dahulu.'); let photoUrl = ''; if (data.photoData) photoUrl = savePhoto_(data.photoData, 'Referensi_' + nip + '_' + Date.now() + '.jpg'); sh.getRange(rowNumber, 6).setValue(JSON.stringify(data.faceDescriptor)); if (photoUrl) sh.getRange(rowNumber, 7).setValue(photoUrl); return { success: true }; } function savePhoto_(dataUrl, filename) { const match = String(dataUrl || '').match(/^data:image\/(jpeg|jpg|png);base64,(.+)$/); if (!match) return ''; const bytes = Utilities.base64Decode(match[2]); const blob = Utilities.newBlob(bytes, match[1] === 'png' ? 'image/png' : 'image/jpeg', filename); let folderId = getSetting_('FolderFotoID'), folder; try { folder = folderId ? DriveApp.getFolderById(folderId) : DriveApp.createFolder('Foto Absensi Guru dan Kepala Sekolah'); } catch (e) { folder = DriveApp.createFolder('Foto Absensi Guru dan Kepala Sekolah'); setSetting_('FolderFotoID', folder.getId()); } return folder.createFile(blob).getUrl(); } function submitStaffAttendance(data) { const nip = String(data.nip || '').trim(); const kind = String(data.kind || ''); if (!nip || !['Masuk', 'Pulang'].includes(kind)) throw new Error('Data absensi tidak valid.'); const staffResult = getStaffByNip(nip); if (!staffResult.success) throw new Error(staffResult.message); const staff = staffResult.staff; if (!staff.faceDescriptor) throw new Error('Wajah belum didaftarkan oleh admin.'); const score = Number(data.faceScore || 0), threshold = Number(getSetting_('AmbangWajah') || 0.5); if (!isFinite(score) || score < threshold) throw new Error('Verifikasi wajah tidak cocok. Silakan coba lagi atau hubungi admin.'); const now = new Date(), date = Utilities.formatDate(now, 'Asia/Jakarta', 'yyyy-MM-dd'); const time = Utilities.formatDate(now, 'Asia/Jakarta', 'HH:mm:ss'); const sh = getSheet_(SHEET_ATTENDANCE), rows = sh.getDataRange().getValues(); let rowNumber = 0; for (let i = 1; i < rows.length; i++) if (String(rows[i][1]) === nip && String(rows[i][4]) === date) { rowNumber = i + 1; break; } if (!rowNumber) { if (kind === 'Pulang') throw new Error('Absensi masuk belum tercatat untuk hari ini.'); const lateAt = getSetting_('JamMasuk') || '07:15'; const status = time.substring(0, 5) > lateAt ? 'Terlambat' : 'Tepat Waktu'; const photoUrl = data.photoData ? savePhoto_(data.photoData, 'Masuk_' + nip + '_' + date + '.jpg') : ''; sh.appendRow(['ATG' + Date.now(), nip, staff.nama, staff.jabatan, date, time, status, photoUrl, score, data.latitude || '', data.longitude || '', data.alamat || '', '', 'Belum Absen', '', '', '', '', '', '']); return { success: true, kind: kind, time: time, status: status, message: 'Absensi masuk berhasil disimpan.' }; } if (kind === 'Masuk') { if (rows[rowNumber - 1][5]) throw new Error('Absensi masuk hari ini sudah tercatat pada ' + rows[rowNumber - 1][5] + '.'); throw new Error('Data absensi masuk hari ini sudah ada. Hubungi admin jika ada kesalahan.'); } if (rows[rowNumber - 1][12]) throw new Error('Absensi pulang sudah tercatat pada ' + rows[rowNumber - 1][12] + '.'); const photoUrl = data.photoData ? savePhoto_(data.photoData, 'Pulang_' + nip + '_' + date + '.jpg') : ''; const planned = getSetting_('JamPulang') || '14:00'; const statusOut = time.substring(0, 5) < planned ? 'Pulang Lebih Awal' : 'Sesuai Jadwal'; sh.getRange(rowNumber, 13, 1, 8).setValues([[time, statusOut, photoUrl, score, data.latitude || '', data.longitude || '', data.alamat || '', String(data.keterangan || '')]]); return { success: true, kind: kind, time: time, status: statusOut, message: 'Absensi pulang berhasil disimpan.' }; } function getAdminSummary(pin) { requireAdmin_(pin); const today = Utilities.formatDate(new Date(), 'Asia/Jakarta', 'yyyy-MM-dd'); const staffRows = getSheet_(SHEET_STAFF).getDataRange().getDisplayValues(); const attRows = getSheet_(SHEET_ATTENDANCE).getDataRange().getDisplayValues(); const staff = []; for (let i = 1; i < staffRows.length; i++) if (staffRows[i][0]) staff.push({ nip: staffRows[i][0], nama: staffRows[i][1], jabatan: staffRows[i][2], unit: staffRows[i][3], status: staffRows[i][4], faceRegistered: !!staffRows[i][5] }); const attendance = []; for (let i = 1; i < attRows.length; i++) if (attRows[i][0]) attendance.push({ id: attRows[i][0], nip: attRows[i][1], nama: attRows[i][2], jabatan: attRows[i][3], tanggal: attRows[i][4], jamMasuk: attRows[i][5], statusMasuk: attRows[i][6], jamPulang: attRows[i][12], statusPulang: attRows[i][13], fotoMasuk: attRows[i][7], fotoPulang: attRows[i][14] }); const todayRows = attendance.filter(a => a.tanggal === today); return { success: true, staff: staff, attendance: attendance.reverse(), stats: { totalStaff: staff.filter(s => s.status === 'Aktif').length, hadir: todayRows.filter(a => a.jamMasuk).length, terlambat: todayRows.filter(a => a.statusMasuk === 'Terlambat').length, belumPulang: todayRows.filter(a => a.jamMasuk && !a.jamPulang).length }, settings: getPublicSettings() }; }
Share:

Friday, October 16, 2026

ABSEN KEHADIRAN

ABSEN SD LPM PICUAN SATU

ABSEN SD LPM PICUAN SATU

Masuk untuk memulai pekerjaan

Notifikasi...
/** * APLIKASI PRESENSI PEGAWAI - BACKEND CORE (LENGKAP) * Menangani Database Google Sheets, GPS Validation, Auth, CRUD, dan Google Drive. */ // Gunakan function agar selalu dapat spreadsheet yang benar function getSS() { var ss = SpreadsheetApp.getActiveSpreadsheet(); if (!ss) { // Fallback: pakai ID spreadsheet (ganti dengan ID spreadsheet Anda) // ss = SpreadsheetApp.openById('YOUR_SPREADSHEET_ID'); throw new Error('Spreadsheet tidak ditemukan. Pastikan script terhubung ke spreadsheet.'); } return ss; } function doGet() { return HtmlService.createTemplateFromFile('Index') .evaluate() .setTitle('Presensi Mobile Pro v3') .addMetaTag('viewport', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no') .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); } /** * SISTEM LOGIN * Kolom Users: [0]ID, [1]Nama, [2]Jabatan, [3]Phone, [4]Role, [5]Password */ function checkLogin(phone, password) { try { const sheet = getSS().getSheetByName('Users'); const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (data[i][3].toString() == phone && data[i][5].toString() == password) { return { success: true, user: { id: data[i][0], nama: data[i][1], jabatan: data[i][2], phone: data[i][3], role: data[i][4] } }; } } return { success: false, message: "Nomor HP atau Password salah!" }; } catch (e) { return { success: false, message: e.toString() }; } } /** * PROSES ABSENSI (MASUK & KELUAR) * Wajib Selfie dan Validasi GPS */ function submitAttendance(type, lat, lng, userId, selfieData) { try { const settings = getSettings(); const dist = calculateDistance(lat, lng, settings.office_lat, settings.office_lng); if (dist > settings.radius) { return { success: false, message: "Gagal! Anda berada di luar radius kantor (" + Math.round(dist) + "m)." }; } if (!selfieData) return { success: false, message: "Wajib mengambil foto selfie!" }; // Simpan Selfie ke Drive const photoUrl = saveFileToDrive(selfieData, `Selfie_${userId}_${Date.now()}.jpg`); const sheet = getSS().getSheetByName('Attendance'); const data = sheet.getDataRange().getValues(); const nowDate = new Date(); const tz = getSS().getSpreadsheetTimeZone(); const today = Utilities.formatDate(nowDate, tz, "yyyy-MM-dd"); const now = Utilities.formatDate(nowDate, tz, "HH:mm:ss"); if (type === 'IN') { // Cek double absen masuk for (let i = 1; i < data.length; i++) { let recordDate = data[i][2]; if (recordDate instanceof Date) { recordDate = Utilities.formatDate(recordDate, tz, "yyyy-MM-dd"); } if (data[i][1] == userId && recordDate == today) { return { success: false, message: "Anda sudah absen masuk hari ini!" }; } } // [0]id, [1]userId, [2]tgl, [3]jamIn, [4]jamOut, [5]lat, [6]lng, [7]latOut, [8]lngOut, [9]status, [10]photo sheet.appendRow([Utilities.getUuid(), userId, today, now, "", lat, lng, "", "", "Hadir", photoUrl]); return { success: true, message: "Absen Masuk Berhasil!", serverTime: now, serverDate: today, serverTs: nowDate.getTime() }; } else { // Cari data masuk hari ini untuk diupdate for (let i = data.length - 1; i >= 1; i--) { let recordDate = data[i][2]; if (recordDate instanceof Date) { recordDate = Utilities.formatDate(recordDate, tz, "yyyy-MM-dd"); } if (data[i][1] == userId && recordDate == today) { if (data[i][4] !== "") return { success: false, message: "Anda sudah absen keluar hari ini!" }; sheet.getRange(i + 1, 5).setValue(now); // Jam Keluar sheet.getRange(i + 1, 8).setValue(lat); // Lat Out sheet.getRange(i + 1, 9).setValue(lng); // Lng Out sheet.getRange(i + 1, 11).setValue(photoUrl); // Foto Keluar return { success: true, message: "Absen Keluar Berhasil!", serverTime: now, serverDate: today, serverTs: nowDate.getTime() }; } } return { success: false, message: "Anda belum melakukan absen masuk!" }; } } catch (e) { return { success: false, message: e.toString() }; } } /** * PENGAJUAN IZIN & UPLOAD LAMPIRAN */ function submitPermit(permitData) { try { let fileUrl = ""; if (permitData.fileData && permitData.fileName) { fileUrl = saveFileToDrive(permitData.fileData, permitData.fileName); } const sheet = getSS().getSheetByName('Permits'); sheet.appendRow([ Utilities.getUuid(), permitData.userId, permitData.date, permitData.type, permitData.reason, "Menunggu", fileUrl ]); return { success: true, message: "Izin berhasil diajukan!" }; } catch (e) { return { success: false, message: e.toString() }; } } function updatePermitStatus(id, status) { try { const sheet = getSS().getSheetByName('Permits'); const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (data[i][0] == id) { sheet.getRange(i + 1, 6).setValue(status); return { success: true }; } } return { success: false, message: "ID Izin tidak ditemukan." }; } catch (e) { return { success: false, message: e.toString() }; } } /** * MANAJEMEN PEGAWAI (CRUD) */ function addUser(userData) { try { const sheet = getSS().getSheetByName('Users'); sheet.appendRow([ Utilities.getUuid(), userData.nama, userData.jabatan, userData.phone, userData.role, userData.password ]); return { success: true, message: "Pegawai berhasil ditambahkan!" }; } catch (e) { return { success: false, message: e.toString() }; } } function editUser(userData) { try { const sheet = getSS().getSheetByName('Users'); const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (data[i][0] == userData.id) { sheet.getRange(i + 1, 2, 1, 5).setValues([[ userData.nama, userData.jabatan, userData.phone, userData.role, userData.password ]]); return { success: true, message: "Data pegawai diperbarui!" }; } } return { success: false }; } catch (e) { return { success: false, message: e.toString() }; } } function deleteUser(id) { try { const sheet = getSS().getSheetByName('Users'); const data = sheet.getDataRange().getValues(); for (let i = 1; i < data.length; i++) { if (data[i][0] == id) { sheet.deleteRow(i + 1); return { success: true, message: "Pegawai dihapus!" }; } } return { success: false }; } catch (e) { return { success: false, message: e.toString() }; } } /** * DATA AGGREGATION & ADMIN DASHBOARD */ function getDashboardData(userId, role) { try { const att = getSheetData('Attendance'); const permits = getSheetData('Permits'); const users = getSheetData('Users'); const settings = getSettings(); const tz = getSS().getSpreadsheetTimeZone(); const today = Utilities.formatDate(new Date(), tz, "yyyy-MM-dd"); const res = { stats: { totalPegawai: users.length - 1, absenHariIni: att.filter(r => r[2] == today).length, izinPending: permits.filter(r => r[5] == "Menunggu").length }, history: att.filter(r => r[1] == userId).slice(-10).reverse(), userPermits: permits.filter(r => r[1] == userId).slice(-10).reverse(), allPermits: permits.slice(1).reverse(), allAttendance: att.slice(1).reverse().map(r => { var found = users.find(u => u[0] == r[1]); var userName = found ? found[1] : "Anonim"; var mapsUrl = 'https://www.google.com/maps?q=' + r[5] + ',' + r[6]; return r.concat([mapsUrl, userName]); }), allUsers: users.slice(1), settings: settings }; return res; } catch (e) { return { error: e.toString() }; } } /** * PENGATURAN SISTEM */ function updateOfficeSettings(lat, lng, radius) { try { const s = getSS().getSheetByName('Settings'); const data = s.getDataRange().getValues(); // Cari baris berdasarkan key name for (var i = 0; i < data.length; i++) { var key = String(data[i][0]).trim().toLowerCase(); if (key === 'office_lat') s.getRange(i + 1, 2).setValue(lat); if (key === 'office_lng') s.getRange(i + 1, 2).setValue(lng); if (key === 'radius') s.getRange(i + 1, 2).setValue(radius); } return { success: true, message: "Lokasi kantor diperbarui!" }; } catch (e) { return { success: false, message: e.toString() }; } } /** * HELPER FUNCTIONS */ function saveFileToDrive(base64Data, fileName) { const folderName = "Presensi_App_Data"; let folders = DriveApp.getFoldersByName(folderName); let folder = folders.hasNext() ? folders.next() : DriveApp.createFolder(folderName); const contentType = base64Data.substring(5, base64Data.indexOf(';')); const bytes = Utilities.base64Decode(base64Data.split(',')[1]); const blob = Utilities.newBlob(bytes, contentType, fileName); const file = folder.createFile(blob); file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); return file.getUrl(); } function getSettings() { const d = getSS().getSheetByName('Settings').getDataRange().getValues(); const s = {}; d.forEach(r => s[r[0]] = r[1]); return s; } /** Fungsi terpisah untuk ambil koordinat kantor - dipanggil langsung dari frontend */ function getOfficeLocation() { try { var s = getSettings(); return { lat: parseFloat(s.office_lat) || 0, lng: parseFloat(s.office_lng) || 0, rad: parseInt(s.radius) || 100 }; } catch(e) { return { lat: 0, lng: 0, rad: 100, error: e.toString() }; } } /** Ambil semua data pegawai */ function getUsers() { try { var data = getSheetData('Users'); return data.slice(1); // Tanpa header } catch(e) { return []; } } /** Ambil log absensi */ function getAttendanceLog() { try { var att = getSheetData('Attendance'); var users = getSheetData('Users'); return att.slice(1).reverse().map(function(r) { var found = users.find(function(u) { return u[0] == r[1]; }); var userName = found ? found[1] : 'Anonim'; var mapsUrl = 'https://www.google.com/maps?q=' + r[5] + ',' + r[6]; return r.concat([mapsUrl, userName]); }); } catch(e) { return []; } } function getSheetData(n) { const sheet = getSS().getSheetByName(n); if (!sheet) return []; var data = sheet.getDataRange().getValues(); var tz = sheet.getParent().getSpreadsheetTimeZone(); // Automatically convert ALL dates to strings globally // This prevents google.script.run from silently crashing! return data.map(function(row) { return row.map(function(cell) { if (cell instanceof Date) { if (cell.getFullYear() === 1899) { return Utilities.formatDate(cell, tz, "HH:mm:ss"); } return Utilities.formatDate(cell, tz, "yyyy-MM-dd"); } return cell; }); }); } function calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371e3; // Meter const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; } /** * Kembalikan waktu server (Apps Script) untuk sinkronisasi jam client */ function getServerTime() { try { var tz = getSS().getSpreadsheetTimeZone(); var now = new Date(); return { now: now.getTime(), formattedDate: Utilities.formatDate(now, tz, 'yyyy-MM-dd'), formattedTime: Utilities.formatDate(now, tz, 'HH:mm:ss'), timeZone: tz }; } catch (e) { return { error: e.toString() }; } }
Share:

Saturday, October 10, 2026

ABSEN GURU DAN PEGAWAI

Presensi Digital SD LPM Picuan Satu

SD LPM PICUAN SATU

Sistem Presensi Online Guru & Kepala Sekolah

--:--:-- WITA

Kamera belum aktif. Klik tombol di bawah untuk menyalakan kamera.

Koordinat Anda:
Mendapatkan lokasi...
Mengakses GPS perangkat...
Peta akan tampil setelah lokasi didapatkan.
Pesan pemberitahuan

© 2026 SD LPM Picuan Satu. Seluruh hak cipta dilindungi.

Share: