Membuat Absensi Online Pakai GPS + Foto dengan Google Script

absensi online gratis,absen online gps dan foto wajah

Membuat Absensi Online Pakai GPS + Foto dengan Google Script

Banyak HRD dan admin mencari solusi absensi online Google Script yang mudah digunakan, tanpa perlu aplikasi tambahan. Dengan memanfaatkan absensi pakai GPS dan foto, karyawan bisa melakukan check in dan check out hanya lewat smartphone, dan data otomatis masuk ke Google Spreadsheet. Panduan ini akan membahas cara membuat absensi dengan Google Apps Script mulai dari form input NIK, pencatatan lokasi GPS, hingga upload foto kamera sebagai bukti kehadiran. Dengan sistem ini, absensi karyawan otomatis spreadsheet bisa diakses kapan saja oleh HRD tanpa ribet.

Kenapa Memakai Google Script untuk Absensi?

Google Apps Script adalah platform ringan berbasis JavaScript yang bisa dipakai untuk mengotomatisasi Google Spreadsheet, Google Drive, hingga Google Forms. Kelebihannya adalah gratis, mudah diintegrasikan, dan bisa diakses lewat web browser. Dengan sedikit coding, perusahaan bisa membuat sistem absensi sederhana namun efektif, tanpa biaya tambahan untuk aplikasi pihak ketiga.

Fitur Absensi Online

  • Check-in dan check-out karyawan berbasis NIK
  • Pencatatan GPS (latitude, longitude) secara otomatis
  • Upload foto langsung dari kamera smartphone
  • Data tersimpan otomatis di Google Spreadsheet
  • Admin HRD bisa melihat laporan harian, mingguan, atau bulanan

Struktur Spreadsheet

Dalam contoh ini, kita memakai dua sheet utama pada Google Sheet:

  1. Employees: menyimpan data karyawan (employee_id, nama, NIK, departemen, posisi)
  2. Absensi: mencatat absensi (id, employee_id, nama, timestamp, tipe, latitude, longitude, foto)

Langkah Membuat Absensi Online

Persiapan Spreadsheet

  • Buka Google Spreadsheet baru.
  • Buat 2 sheet:
    • Employees → menyimpan data karyawan (employee_id, nama, NIK, departemen, posisi).
    • Attendance → menyimpan data absen (id, employee_id, nama, timestamp, tipe, latitude, longitude, foto).

Membuat Google Apps Script

  • Masuk ke script.google.com.
  • Buat proyek baru, beri nama AbsensiGPSFoto.
  • Buat file Code.gs dengan isi sesuai kebutuhan sistem absensi untuk codenya sebagai berikut.
  • Silahkan ganti SPREADSHEET_ID dengan ID Google Sheet Anda.
// Code.gs
const SPREADSHEET_ID = '1kcNbOdnzMFH7n_ALaLVeB2QuN2KxTYSyMaNYxNfEQ0A';
const PHOTO_FOLDER_NAME = 'AttendancePhotos';

/**
 * ========================
 * Konfigurasi Spreadsheet
 * ========================
 */
function getSS() {
  // Ganti dengan ID Spreadsheet kamu
  return SpreadsheetApp.openById(SPREADSHEET_ID);
}

function getPhotoFolder() {
  const folders = DriveApp.getFoldersByName(PHOTO_FOLDER_NAME);
  return folders.hasNext() ? folders.next() : DriveApp.createFolder(PHOTO_FOLDER_NAME);
}

/**
 * ========================
 * Employees
 * ========================
 */
function getEmployeeByNIK(nik) {
  console.log("getEmployeeByNIK called with NIK:", nik);

  const ss = getSS();
  if(!ss){
    console.log('File tidak ada!');
  }

  const sh = ss.getSheetByName("Employees");
  if(!ss){
    console.log('File tidak ada!');
  }

  const data = sh.getDataRange().getValues();
  data.shift();

  let emp = data.find(r => {
      let sheetNIK = r[2] !== undefined ? r[2].toString().trim() : "";
      let inputNIK = nik.toString().trim();

      console.log(`Comparing: '${sheetNIK}' === '${inputNIK}'`);
      return sheetNIK === inputNIK;
    });
  if (!emp) return null;

  return {
    employee_id: emp[0],
    name: emp[1],
    nik: emp[2],
    department: emp[3],
    position: emp[4]
  };
}

/**
 * ========================
 * Attendance
 * ========================
 */
function submitAttendance(payload) {
  const emp = getEmployeeByNIK(payload.nik);

  if (!emp) return { success: false, message: "ERROR NIK TIDAK DITEMUKAN" };

  // simpan foto ke Drive
  const folder = getPhotoFolder();
  const photoBlob = Utilities.newBlob(Utilities.base64Decode(payload.photo.split(',')[1]), 'image/png', emp.nik + "_" + new Date().getTime() + ".png");
  const photoUrl = folder.createFile(photoBlob);

  const ss = getSS();
  const sh = ss.getSheetByName("Absensi");
  const id = sh.getLastRow();
  const ts = new Date();

  sh.appendRow([
    id,
    emp.employee_id,
    emp.name,
    ts,
    payload.type,
    payload.lat,
    payload.lon,
    payload.acc,
    photoUrl.getUrl()
  ]);

  return { success: true, message: "Absensi berhasil dicatat", employee: emp };
}

function getAttendanceData() {
  const ss = getSS();
  const sh = ss.getSheetByName("Absensi");
  if (!sh) {
    console.log("Sheet Attendance tidak ditemukan");
    return [];
  }

  const data = sh.getDataRange().getValues();
  if (data.length <= 1) {
    console.log("Data kosong");
    return [];
  }

  data.shift(); // buang header

  // mapping baris ke objek
  const result = data.map(r => ({
    id: r[0],
    employee_id: r[1],
    name: r[2],
    timestamp: (r[3] instanceof Date)
      ? Utilities.formatDate(r[3], Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss")
      : String(r[3] || ""),
    type: r[4],
    latitude: r[5],
    longitude: r[6],
    accuracy: r[7],
    photoUrl: r[8]
  }));

  console.log("Jumlah data attendance:", result.length);
  return result;
}

function getAttendanceDataByDate(dateStr) {
  const ss = getSS();
  const sh = ss.getSheetByName("Absensi");
  if (!sh) {
    return [];
  }

  const data = sh.getDataRange().getValues();
  data.shift(); // buang header

  if (data.length === 0) {
    return [];
  }

  return data
    .map((r, i) => {
      let ts = (r[3] instanceof Date)
        ? Utilities.formatDate(r[3], Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss")
        : String(r[3] || "");

      return {
        id: r[0],
        employee_id: r[1],
        name: r[2],
        timestamp: ts,
        dateOnly: ts.substring(0, 10),
        type: r[4],
        latitude: r[5],
        longitude: r[6],
        accuracy: r[7],
        photoUrl: r[8]
      }
    })
    .filter(r => r.dateOnly === dateStr);
}

/**
 * ========================
 * Admin / HRD Panel
 * ========================
 */
function loginAdmin(nik) {
  const emp = getEmployeeByNIK(nik);
  if (!emp) return { success: false, message: "NIK tidak ditemukan" };

  let role = "operator"; // default
  if (emp.position.toLowerCase().includes("hrd")) {
    role = "hrd";
  }

  return {
    success: true,
    message: "Login berhasil",
    name: emp.name,
    role: role
  };
}

/**
 * ========================
 * Export
 * ========================
 */
function exportCSV() {
  const data = getAttendanceData();
  let csv = "ID,EmployeeID,Name,Timestamp,Type,Latitude,Longitude,Accuracy,PhotoURL\n";
  data.forEach(r => {
    csv += `${r.id},${r.employee_id},${r.name},${r.timestamp},${r.type},${r.latitude},${r.longitude},${r.accuracy},${r.photoUrl}\n`;
  });
  return csv;
}

/**
 * ========================
 * Routing Halaman
 * ========================
 */
function doGet(e) {
  let page = e.parameter.page || "Absensi";
  return HtmlService.createHtmlOutputFromFile(page).setTitle("Sistem Absensi Online");
}

// function doGet() {
//   return HtmlService.createHtmlOutputFromFile("Admin")
//     .setTitle("Admin HRD Panel")
//     .setSandboxMode(HtmlService.SandboxMode.IFRAME);
// }

/**
 * ========================
 * Capture kamera
 * ========================
 */
function uploadPhoto(base64, filename) {
  try {
    // Hapus prefix base64
    const data = base64.replace(/^data:image\/\w+;base64,/, "");
    const blob = Utilities.newBlob(Utilities.base64Decode(data), "image/png", filename);

    // Simpan ke folder Google Drive (buat folder "AbsensiPhoto")
    const folder = DriveApp.getFoldersByName("AbsensiPhoto").hasNext()
      ? DriveApp.getFoldersByName("AbsensiPhoto").next()
      : DriveApp.createFolder("AbsensiPhoto");

    const file = folder.createFile(blob);
    return file.getUrl();
  } catch (err) {
    return "ERROR_UPLOAD: " + err.message;
  }
}

function getAttendanceById(id) {
  const ss = getSS();
  const sheet = ss.getSheetByName('Attendance');
  const rows = sheet.getDataRange().getValues();
  rows.shift();
  for (let r of rows) {
    if (r[0] === id) {
      return {
        id: r[0],
        nip: r[1],
        name: r[2],
        clockType: r[3],
        timestamp: r[4],
        lat: r[5],
        lng: r[6],
        photoUrl: r[7],
        note: r[8]
      };
    }
  }
  return null;
}

// SERVER: Employees CRUD dasar (digunakan HRD)
function getEmployees() {
  const ss = getSS();
  const s = ss.getSheetByName('Employees');
  if (!s) throw new Error('Sheet "Employees" tidak ditemukan.');
  const rows = s.getDataRange().getValues();
  const header = rows.shift();
  return rows.map(r => ({ email: r[0], name: r[1], role: r[2] }));
}

function addEmployee(emp) {
  // emp = { nip, name, role }
  const me = Session.getActiveUser().getEmail();
  if (!isHRD(me)) return { status: 'error', message: 'Hanya HRD yang boleh menambah karyawan.' };
  const ss = getSS();
  const s = ss.getSheetByName('Employees');
  s.appendRow([emp.nip, emp.name, emp.department, emp.role]);
  return { status: 'success' };
}

Membuat File HTML

  • Buat file HTML seperti Admin.html, Absensi.html, Laporan.html, LaporanDetail.html,dan Export.html.
  • Untuk isi kodenya sebagai berikut:
#Admin.html

<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="UTF-8">
  <title>Admin HRD Panel</title>
  <!-- Bootstrap 5 CSS -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">

<div class="container py-4">
  <div class="card shadow-sm">
    <div class="card-header bg-primary text-white">
      <h3 class="mb-0">Admin HRD Panel</h3>
    </div>
    <div class="card-body">
      <!-- Panel -->
      <div id="panel">
        <h5 id="welcome" class="mb-3"></h5>

        <!-- Filter Tanggal -->
        <div class="row mb-3">
          <div class="col-md-4">
            <label class="form-label">Pilih Tanggal</label>
            <input type="date" id="filterDate" class="form-control">
          </div>
          <div class="col-md-2 d-flex align-items-end">
            <button class="btn btn-primary w-100" onclick="filterByDate()">Filter</button>
          </div>
          <div class="col-md-2 d-flex align-items-end">
            <button class="btn btn-secondary w-100" onclick="loadAttendance()">Reset</button>
          </div>
        </div>

        <div class="table-responsive">
          <table class="table table-striped table-hover align-middle">
            <thead class="table-dark">
              <tr>
                <th>ID</th>
                <th>Nama</th>
                <th>Waktu</th>
                <th>Type</th>
                <th>Lokasi</th>
                <th>Foto</th>
              </tr>
            </thead>
            <tbody id="attendanceTable"></tbody>
          </table>
        </div>
      </div>

    </div>
  </div>
</div>

<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

<script>
  function loadAttendance() {
    google.script.run
      .withSuccessHandler(function(rows) {
        console.log("Rows dari server:", rows); // cek lagi
        if (!rows || rows.length === 0) {
          document.getElementById("attendanceTable").innerHTML = "<tr><td colspan='6'>Tidak ada data</td></tr>";
          return;
        }
        renderTable(rows);
      })
      .withFailureHandler(function(err) {
        console.error("Error getAttendanceData:", err);
      })
      .getAttendanceData();
  }

  function filterByDate() {
    let date = document.getElementById("filterDate").value;
    if (!date) {
      alert("Silakan pilih tanggal terlebih dahulu!");
      return;
    }
    google.script.run
      .withSuccessHandler(function(rows) {
        console.log("Rows dari server:", rows); // cek lagi
        if (!rows || rows.length === 0) {
          document.getElementById("attendanceTable").innerHTML = "<tr><td colspan='6'>Tidak ada data</td></tr>";
          return;
        }
        renderTable(rows);
      })
      .withFailureHandler(function(err) {
        console.error("Error getAttendanceData:", err);
      })
    .getAttendanceDataByDate(date);
  }

  function renderTable(rows) {
    console.log(rows);

    let tbody = document.getElementById("attendanceTable");
    if (!rows || rows.length === 0) {
      tbody.innerHTML = `<tr><td colspan="6" class="text-center text-muted">Tidak ada data</td></tr>`;
      return;
    }

    tbody.innerHTML = "";
    rows.forEach(r => {
      let tr = `
        <tr>
          <td>${r.id}</td>
          <td>${r.name}</td>
          <td>${new Date(r.timestamp).toLocaleString()}</td>
          <td>
            <span class="badge ${r.type === 'checkin' ? 'bg-success' : 'bg-danger'}">
              ${r.type}
            </span>
          </td>
          <td>
            <a href="https://www.openstreetmap.org/?mlat=${r.latitude}&mlon=${r.longitude}&zoom=18"
               target="_blank" class="btn btn-sm btn-outline-primary">Map</a>
          </td>
          <td>
            <a href="${r.photoUrl}" target="_blank" class="btn btn-sm btn-outline-secondary">Foto</a>
          </td>
        </tr>
      `;
      tbody.innerHTML += tr;
    });
  }

  document.addEventListener("DOMContentLoaded", function() {
    loadAttendance();
  });
</script>
</body>
</html>

#Absensi.html
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    video, canvas {
      width: 100%;
      max-width: 430px;
      border: 2px solid #ddd;
      border-radius: 10px;
    }
    #canvas {
      display: none;
      margin-top: 10px;
    }
  </style>
</head>
<body class="container-fluid py-4">
  <div class="row d-flex justify-content-center">
    <div class="col-sm-8 col-lg-4" style="border:1px solid red;">
      <div class="row d-flex justify-content-center">
        <div class="col-md-12">
          <h3 class="mb-3 text-center">Form Absensi Karyawan</h3>
        </div>
        <div class="col-md-12">
          <form id="absensiForm" class="mb-3">
            <div class="row d-flex justify-content-center">
              <div class="col-md-12">
                <div class="mb-3">
                  <label class="form-label">NIK</label>
                  <input type="text" id="nik" class="form-control" required>
                </div>
                <div class="mb-3">
                  <label class="form-label">Tipe Absensi</label>
                  <select id="type" class="form-select">
                    <option value="checkin">Check In</option>
                    <option value="checkout">Check Out</option>
                  </select>
                </div>
              </div>
            </div>

            <!-- Kamera Preview -->
            <div class="mb-2">
              <label>Camera Preview:</label><br>
              <video id="video" autoplay playsinline></video>
              <canvas id="canvas"></canvas>
              <div class="row d-flex justify-content-center">
                <div class="col-md-12 d-grid gap-2">
                  <button type="button" id="captureBtn" class="btn btn-primary mt-2">Ambil Foto</button>
                  <button type="button" id="resetBtn" class="btn btn-danger mt-2" style="display:none;">Ambil Ulang</button>
                </div>
              </div>
            </div>

            <input type="hidden" id="photoData">
            <div class="row d-flex justify-content-center">
              <div class="col-md-12">
                <div class="d-grid gap-2">
                  <button class="btn btn-success">Submit Absensi</button>
                </div>
              </div>
            </div>
          </form>
        </div>
      </div>
   
      <div class="row d-flex justify-content-center">
        <div class="col-md-12 text-center">
          <a href="?page=Admin">Admin HRD</a>, Supported by <a href="https://www.mampirklik.com">www.mampirklik.com</a> dan <a href="https://www.cmsgue.id">cmsgue.id</a><br>

          <div id="status" class="mt-3 text-danger"></div>
        </div>
      </div>
    </div>
  </div>

  <script>
    const video = document.getElementById('video');
    const canvas = document.getElementById('canvas');
    const captureBtn = document.getElementById('captureBtn');
    const resetBtn = document.getElementById('resetBtn');
    const photoData = document.getElementById('photoData');
    let stream;

    // Aktifkan Kamera
    navigator.mediaDevices.getUserMedia({ video: true })
      .then(s => {
        stream = s;
        video.srcObject = stream;
      })
      .catch(err => {
        alert("Kamera tidak bisa diakses: " + err);
      });

    // Capture Foto
    captureBtn.addEventListener('click', () => {
      const ctx = canvas.getContext('2d');
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

      // Simpan base64
      photoData.value = canvas.toDataURL("image/png");

      // Tampilkan hasil, sembunyikan kamera
      video.style.display = "none";
      canvas.style.display = "block";
      captureBtn.style.display = "none";
      resetBtn.style.display = "inline-block";
    });

    // Reset Foto
    resetBtn.addEventListener('click', () => {
      photoData.value = "";
      canvas.style.display = "none";
      video.style.display = "block";
      captureBtn.style.display = "inline-block";
      resetBtn.style.display = "none";
    });

    // Submit Form
    document.getElementById("absensiForm").addEventListener("submit", e => {
      e.preventDefault();
      if (!photoData.value) {
        alert("Harap ambil foto dulu!");
        return;
      }

      navigator.geolocation.getCurrentPosition(function(pos) {
        let lat = pos.coords.latitude;
        let lon = pos.coords.longitude;
        let acc = pos.coords.accuracy;
        let photo = photoData.value;

        const payload = {
          nik: document.getElementById("nik").value,
          type: document.getElementById("type").value,
          lat,
          lon,
          acc,
          photo
        };
        google.script.run.withSuccessHandler(msg => {
          if (msg.success) {
            document.getElementById("status").innerHTML = "<span class='text-success'>" + msg.message + "</span>";
          } else {
            document.getElementById("status").innerHTML = msg.message;
          }

          document.getElementById("absensiForm").reset();
          // reset tampilan ke kamera lagi
          resetBtn.click();

        }).submitAttendance(payload);
      }, function() {
        alert("Aktifkan lokasi GPS terlebih dahulu");
      });
    });
  </script>
</body>
</html>

#Laporan.html
<!DOCTYPE html>
<html>
<head>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container py-4">
  <h3>Laporan Absensi</h3>
  <div class="table-responsive">
    <table class="table table-bordered" id="laporanTable">
      <thead class="table-dark">
        <tr>
          <th>ID</th><th>Nama</th><th>Waktu</th><th>Type</th><th>Detail</th>
        </tr>
      </thead>
      <tbody></tbody>
    </table>
  </div>

  <script>
    google.script.run.withSuccessHandler(function(rows){
      let tbody = document.querySelector("#laporanTable tbody");
      rows.forEach(r=>{
        tbody.innerHTML += `
          <tr>
            <td>${r.id}</td>
            <td>${r.name}</td>
            <td>${new Date(r.timestamp).toLocaleString()}</td>
            <td>${r.type}</td>
            <td><a href="?page=LaporanDetail&id=${r.id}" class="btn btn-sm btn-primary">Detail</a></td>
          </tr>`;
      });
    }).getAttendanceData();
  </script>
</body>
</html>

#LaporanDetail.html
<!DOCTYPE html>
<html>
<head>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
  <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
</head>
<body class="container py-4">
  <h3>Detail Absensi</h3>
  <div id="detail"></div>
  <div id="map" style="height:400px;" class="mt-3"></div>

  <script>
    const params = new URLSearchParams(window.location.search);
    const id = params.get("id");

    google.script.run.withSuccessHandler(function(rows){
      let data = rows.find(r=> r.id == id);
      if(!data) {
        document.getElementById("detail").innerHTML = "Data tidak ditemukan";
        return;
      }

      document.getElementById("detail").innerHTML = `
        <p><b>Nama:</b> ${data.name}</p>
        <p><b>Waktu:</b> ${new Date(data.timestamp).toLocaleString()}</p>
        <p><b>Type:</b> ${data.type}</p>
        <p><b>Foto:</b> <a href="${data.photoUrl}" target="_blank">Lihat</a></p>
      `;

      let map = L.map("map").setView([data.latitude, data.longitude], 18);
      L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        attribution: "&copy; OpenStreetMap contributors"
      }).addTo(map);

      L.marker([data.latitude, data.longitude]).addTo(map)
        .bindPopup(`${data.name} - ${data.type}`).openPopup();
    }).getAttendanceData();
  </script>
</body>
</html>

#Export.html
<!DOCTYPE html>
<html>
<head>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container py-4">
  <h3>Export Data Absensi</h3>
  <button class="btn btn-success" onclick="downloadCSV()">Download CSV</button>

  <script>
    function downloadCSV() {
      google.script.run.withSuccessHandler(function(csv){
        let blob = new Blob([csv], {type:"text/csv"});
        let url = URL.createObjectURL(blob);
        let a = document.createElement("a");
        a.href = url;
        a.download = "attendance.csv";
        a.click();
        URL.revokeObjectURL(url);
      }).exportCSV();
    }
  </script>
</body>
</html>

Deploy

  • Klik menu Deploy kemudian pilih New Deployment.
  • Pilih Web App sebagai tipe deployment.
  • Atur konfigurasi:
    • Execute as: Me
    • Who has access: Siapa saja yang memiliki akun gmail
  • Salin URL Web App untuk digunakan sebagai akses frontend.

Menampilkan Laporan Absensi

Admin HRD bisa membuka halaman admin yang menampilkan data absensi dalam bentuk tabel. Data dapat difilter berdasarkan tanggal, dilengkapi tombol untuk melihat peta lokasi dengan Leaflet atau OpenStreetMap, dan foto bisa ditampilkan langsung sebagai thumbnail. Dengan begitu, HRD bisa memastikan kehadiran karyawan lebih transparan.

Membuat absensi pakai GPS + foto dengan Google Script adalah solusi praktis untuk perusahaan kecil hingga menengah. Selain gratis, sistem ini fleksibel, mudah dikembangkan, dan bisa langsung dipakai tanpa install aplikasi. Dengan sedikit modifikasi, sistem ini juga dapat diintegrasikan ke payroll atau laporan HRD bulanan. Jika ingin absensi lebih akurat, pastikan karyawan mengaktifkan GPS dan izin kamera saat melakukan absen.

Posting Komentar

Lebih baru Lebih lama

نموذج الاتصال