Membuat Aplikasi Arsip Surat Dengan Google Apps Script

Pada tutorial ini kita akan membuat aplikasi Arsip Surat berbasis Google Apps Script dengan fitur:

  • Dashboard Statistik
  • Surat Masuk
  • Surat Keluar
  • Semua Arsip
  • Ekspor CSV
  • Refresh Data
  • Pengaturan Sistem
  • Upload Dokumen
  • Grafik Tren Arsip
Catatan:
Ganti seluruh kode di bawaan dengan kode berikut.

File Code.gs

Code.gs
/*************************************************
* // ============================================================
//  ARSIP SURAT — Google Apps Script Backend
//  Version 2.0 | 2025
// ============================================================

// ── CONFIG KEYS ─────────────────────────────────────────────
const CONFIG_KEY_SHEET_ID    = 'AS_SHEET_ID';
const CONFIG_KEY_SHEET_NAME  = 'AS_SHEET_NAME';
const CONFIG_KEY_ORG_NAME    = 'AS_ORG_NAME';
const CONFIG_KEY_ORG_LOGO    = 'AS_ORG_LOGO';

const DEFAULT_SHEET_NAME = 'ArsipSurat';

// ── HEADERS ─────────────────────────────────────────────────
const HEADERS = [
  'ID', 'Jenis', 'Nomor Surat', 'Tanggal Surat', 'Perihal',
  'Dari/Kepada', 'Klasifikasi', 'Status', 'Keterangan',
  'Dibuat Oleh', 'Tanggal Input', 'Terakhir Diubah', 'File URL'
];

// ── ENTRY POINT ──────────────────────────────────────────────
function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index')
    .setTitle('Arsip Surat')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1.0');
}

// ── USER PROPS HELPERS ───────────────────────────────────────
function getProps() {
  return PropertiesService.getUserProperties();
}

// ── CONFIG: GET / SAVE ───────────────────────────────────────
function getConfig() {
  const p = getProps();
  return {
    sheetId   : p.getProperty(CONFIG_KEY_SHEET_ID)   || '',
    sheetName : p.getProperty(CONFIG_KEY_SHEET_NAME) || DEFAULT_SHEET_NAME,
    orgName   : p.getProperty(CONFIG_KEY_ORG_NAME)   || 'Instansi / Organisasi',
    orgLogo   : p.getProperty(CONFIG_KEY_ORG_LOGO)   || '',
  };
}

function saveConfig(cfg) {
  const p = getProps();
  if (cfg.sheetId   !== undefined) p.setProperty(CONFIG_KEY_SHEET_ID,   cfg.sheetId);
  if (cfg.sheetName !== undefined) p.setProperty(CONFIG_KEY_SHEET_NAME, cfg.sheetName || DEFAULT_SHEET_NAME);
  if (cfg.orgName   !== undefined) p.setProperty(CONFIG_KEY_ORG_NAME,   cfg.orgName);
  if (cfg.orgLogo   !== undefined) p.setProperty(CONFIG_KEY_ORG_LOGO,   cfg.orgLogo);

  // Auto-init sheet if sheetId provided
  if (cfg.sheetId) {
    try { initSheet(); } catch(e) {}
  }
  return { success: true, message: 'Konfigurasi berhasil disimpan.' };
}

// ── SHEET RESOLVER ───────────────────────────────────────────
function getSheet(forWrite) {
  const cfg  = getConfig();
  let ss;

  if (cfg.sheetId) {
    try { ss = SpreadsheetApp.openById(cfg.sheetId); }
    catch(e) { throw new Error('Gagal membuka Spreadsheet. Pastikan ID benar dan akses sudah diberikan.'); }
  } else {
    ss = SpreadsheetApp.getActiveSpreadsheet();
    if (!ss) throw new Error('Tidak ada Spreadsheet aktif. Silakan konfigurasi terlebih dahulu.');
  }

  let sheet = ss.getSheetByName(cfg.sheetName);
  if (!sheet && forWrite) {
    sheet = ss.insertSheet(cfg.sheetName);
    _writeHeaders(sheet);
  }
  if (!sheet) throw new Error(`Sheet "${cfg.sheetName}" tidak ditemukan.`);
  return sheet;
}

function initSheet() {
  const sheet = getSheet(true);
  if (sheet.getLastRow() === 0) _writeHeaders(sheet);
  else if (sheet.getRange(1,1).getValue() !== 'ID') {
    sheet.insertRowBefore(1);
    _writeHeaders(sheet);
  }
  return { success: true };
}

function _writeHeaders(sheet) {
  const r = sheet.getRange(1, 1, 1, HEADERS.length);
  r.setValues([HEADERS]);
  r.setBackground('#1e293b')
   .setFontColor('#ffffff')
   .setFontWeight('bold')
   .setFontSize(10);
  sheet.setFrozenRows(1);
  sheet.setColumnWidth(1, 80);
  sheet.setColumnWidth(3, 150);
  sheet.setColumnWidth(5, 250);
  sheet.setColumnWidth(6, 180);
}

// ── GENERATE ID ──────────────────────────────────────────────
function generateId() {
  const now  = new Date();
  const year = now.getFullYear();
  const rand = Math.floor(Math.random() * 9000) + 1000;
  return `SRT-${year}-${rand}`;
}

// ── READ ALL ─────────────────────────────────────────────────
function getAllSurat() {
  try {
    const sheet = getSheet(false);
    const last  = sheet.getLastRow();
    if (last < 2) return { success: true, data: [] };

    const values = sheet.getRange(2, 1, last - 1, HEADERS.length).getValues();
    const data = values
      .filter(r => r[0] !== '')
      .map(r => _rowToObj(r));
    return { success: true, data };
  } catch(e) {
    return { success: false, message: e.message };
  }
}

function _rowToObj(r) {
  return {
    id           : r[0],
    jenis        : r[1],
    nomorSurat   : r[2],
    tanggalSurat : r[3] ? Utilities.formatDate(new Date(r[3]), Session.getScriptTimeZone(), 'yyyy-MM-dd') : '',
    perihal      : r[4],
    dariKepada   : r[5],
    klasifikasi  : r[6],
    status       : r[7],
    keterangan   : r[8],
    dibuatOleh   : r[9],
    tanggalInput : r[10] ? Utilities.formatDate(new Date(r[10]), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm') : '',
    terakhirDiubah: r[11] ? Utilities.formatDate(new Date(r[11]), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm') : '',
    fileUrl      : r[12],
  };
}

// ── CREATE ───────────────────────────────────────────────────
function createSurat(data) {
  try {
    const sheet = getSheet(true);
    const id    = generateId();
    const now   = new Date();
    const user  = Session.getActiveUser().getEmail() || 'Sistem';

    const row = [
      id,
      data.jenis        || '',
      data.nomorSurat   || '',
      data.tanggalSurat ? new Date(data.tanggalSurat) : now,
      data.perihal      || '',
      data.dariKepada   || '',
      data.klasifikasi  || 'Biasa',
      data.status       || 'Aktif',
      data.keterangan   || '',
      data.dibuatOleh   || user,
      now,
      now,
      data.fileUrl      || '',
    ];

    sheet.appendRow(row);
    _styleLastRow(sheet);
    return { success: true, id, message: 'Surat berhasil ditambahkan.' };
  } catch(e) {
    return { success: false, message: e.message };
  }
}

function _styleLastRow(sheet) {
  const last = sheet.getLastRow();
  const r    = sheet.getRange(last, 1, 1, HEADERS.length);
  const bg   = last % 2 === 0 ? '#f8fafc' : '#ffffff';
  r.setBackground(bg).setFontSize(10).setVerticalAlignment('middle');
  sheet.setRowHeight(last, 36);
}

// ── UPDATE ───────────────────────────────────────────────────
function updateSurat(data) {
  try {
    const sheet = getSheet(false);
    const last  = sheet.getLastRow();
    if (last < 2) return { success: false, message: 'Data tidak ditemukan.' };

    const ids = sheet.getRange(2, 1, last - 1, 1).getValues().flat();
    const idx = ids.indexOf(data.id);
    if (idx === -1) return { success: false, message: `ID ${data.id} tidak ditemukan.` };

    const rowNum = idx + 2;
    const existing = sheet.getRange(rowNum, 1, 1, HEADERS.length).getValues()[0];

    const updated = [
      data.id,
      data.jenis        || existing[1],
      data.nomorSurat   || existing[2],
      data.tanggalSurat ? new Date(data.tanggalSurat) : existing[3],
      data.perihal      !== undefined ? data.perihal     : existing[4],
      data.dariKepada   !== undefined ? data.dariKepada  : existing[5],
      data.klasifikasi  || existing[6],
      data.status       || existing[7],
      data.keterangan   !== undefined ? data.keterangan  : existing[8],
      existing[9],
      existing[10],
      new Date(),
      data.fileUrl      !== undefined ? data.fileUrl     : existing[12],
    ];

    sheet.getRange(rowNum, 1, 1, HEADERS.length).setValues([updated]);
    return { success: true, message: 'Surat berhasil diperbarui.' };
  } catch(e) {
    return { success: false, message: e.message };
  }
}

// ── DELETE ───────────────────────────────────────────────────
function deleteSurat(id) {
  try {
    const sheet = getSheet(false);
    const last  = sheet.getLastRow();
    if (last < 2) return { success: false, message: 'Data tidak ditemukan.' };

    const ids = sheet.getRange(2, 1, last - 1, 1).getValues().flat();
    const idx = ids.indexOf(id);
    if (idx === -1) return { success: false, message: `ID ${id} tidak ditemukan.` };

    sheet.deleteRow(idx + 2);
    return { success: true, message: 'Surat berhasil dihapus.' };
  } catch(e) {
    return { success: false, message: e.message };
  }
}

// ── SEARCH ───────────────────────────────────────────────────
function searchSurat(query, filters) {
  try {
    const result = getAllSurat();
    if (!result.success) return result;

    let data = result.data;
    const q  = (query || '').toLowerCase().trim();

    if (q) {
      data = data.filter(s =>
        s.nomorSurat.toLowerCase().includes(q)  ||
        s.perihal.toLowerCase().includes(q)      ||
        s.dariKepada.toLowerCase().includes(q)   ||
        s.id.toLowerCase().includes(q)
      );
    }

    if (filters) {
      if (filters.jenis)       data = data.filter(s => s.jenis        === filters.jenis);
      if (filters.status)      data = data.filter(s => s.status       === filters.status);
      if (filters.klasifikasi) data = data.filter(s => s.klasifikasi  === filters.klasifikasi);
      if (filters.dateFrom)    data = data.filter(s => s.tanggalSurat >= filters.dateFrom);
      if (filters.dateTo)      data = data.filter(s => s.tanggalSurat <= filters.dateTo);
    }

    return { success: true, data };
  } catch(e) {
    return { success: false, message: e.message };
  }
}

// ── STATS ────────────────────────────────────────────────────
function getStats() {
  try {
    const result = getAllSurat();
    if (!result.success) return result;

    const data  = result.data;
    const total = data.length;
    const masuk = data.filter(s => s.jenis === 'Masuk').length;
    const keluar= data.filter(s => s.jenis === 'Keluar').length;
    const aktif = data.filter(s => s.status === 'Aktif').length;
    const arsip = data.filter(s => s.status === 'Diarsipkan').length;

    const thisMonth = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM');
    const bulanIni  = data.filter(s => s.tanggalInput.startsWith(thisMonth)).length;

    // Tren 6 bulan terakhir
    const tren = [];
    for (let i = 5; i >= 0; i--) {
      const d   = new Date();
      d.setMonth(d.getMonth() - i);
      const key = Utilities.formatDate(d, Session.getScriptTimeZone(), 'yyyy-MM');
      const lbl = Utilities.formatDate(d, Session.getScriptTimeZone(), 'MMM yy');
      tren.push({ bulan: lbl, total: data.filter(s => s.tanggalInput.startsWith(key)).length });
    }

    return { success: true, stats: { total, masuk, keluar, aktif, arsip, bulanIni, tren } };
  } catch(e) {
    return { success: false, message: e.message };
  }
}

// ── EXPORT CSV ───────────────────────────────────────────────
function exportCsv() {
  try {
    const result = getAllSurat();
    if (!result.success) return result;

    const rows  = [HEADERS.join(',')];
    result.data.forEach(s => {
      rows.push([
        s.id, s.jenis, s.nomorSurat, s.tanggalSurat,
        `"${(s.perihal||'').replace(/"/g,'""')}"`,
        `"${(s.dariKepada||'').replace(/"/g,'""')}"`,
        s.klasifikasi, s.status,
        `"${(s.keterangan||'').replace(/"/g,'""')}"`,
        s.dibuatOleh, s.tanggalInput, s.terakhirDiubah,
        s.fileUrl
      ].join(','));
    });

    return { success: true, csv: rows.join('\n') };
  } catch(e) {
    return { success: false, message: e.message };
  }
}

// ── SPREADSHEET INFO ─────────────────────────────────────────
function getSpreadsheetInfo() {
  try {
    const cfg = getConfig();
    let ss;
    if (cfg.sheetId) {
      ss = SpreadsheetApp.openById(cfg.sheetId);
    } else {
      ss = SpreadsheetApp.getActiveSpreadsheet();
    }
    if (!ss) return { success: false, message: 'Tidak ada spreadsheet terhubung.' };

    const sheets = ss.getSheets().map(s => s.getName());
    return { success: true, name: ss.getName(), url: ss.getUrl(), sheets };
  } catch(e) {
    return { success: false, message: e.message };
  }
}
*************************************************/

function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index');
}

/*
Tambahkan seluruh kode Code.gs Anda
di area ini.
*/

File Index.html

Index.html
<!-- ********************************************<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>Arsip Surat</title>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet"/>

<style>
/* ═══════════════════════════════════
   DESIGN TOKENS
═══════════════════════════════════ */
:root {
  --navy:      #0f172a;
  --navy-2:    #1e293b;
  --navy-3:    #334155;
  --navy-4:    #475569;
  --slate:     #64748b;
  --muted:     #94a3b8;
  --border:    #e2e8f0;
  --surface:   #f8fafc;
  --white:     #ffffff;

  --accent:    #3b82f6;
  --accent-d:  #2563eb;
  --accent-l:  #eff6ff;
  --accent-bg: rgba(59,130,246,.08);

  --green:     #10b981;
  --green-l:   #ecfdf5;
  --red:       #ef4444;
  --red-l:     #fef2f2;
  --amber:     #f59e0b;
  --amber-l:   #fffbeb;
  --purple:    #8b5cf6;
  --purple-l:  #f5f3ff;

  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-xl: 20px;

  --shadow-sm: 0 1px 3px rgba(0,0,0,.07),0 1px 2px rgba(0,0,0,.05);
  --shadow-md: 0 4px 16px rgba(0,0,0,.08),0 2px 6px rgba(0,0,0,.05);
  --shadow-lg: 0 10px 40px rgba(0,0,0,.12),0 4px 12px rgba(0,0,0,.06);
  --shadow-xl: 0 20px 60px rgba(0,0,0,.16);

  --sidebar-w: 260px;
  --topbar-h:  64px;
  --font: 'Inter', system-ui, sans-serif;
  --mono: 'JetBrains Mono', monospace;
}

/* ── RESET ── */
*,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
body { font-family: var(--font); background: var(--surface); color: var(--navy); font-size: 14px; line-height: 1.5; }
button { font-family: inherit; cursor: pointer; border: none; outline: none; }
input, select, textarea { font-family: inherit; outline: none; }

::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: var(--muted); }

/* ═══════════════════════════════════
   LAYOUT
═══════════════════════════════════ */
.app-shell {
  display: flex;
  height: 100vh;
  overflow: hidden;
  position: relative;
}

/* top gradient line */
.app-shell::before {
  content: '';
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: linear-gradient(90deg, #3b82f6, #6366f1, #8b5cf6);
  z-index: 999;
}

/* ── SIDEBAR ── */
.sidebar {
  width: var(--sidebar-w);
  min-width: var(--sidebar-w);
  background: var(--navy);
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow-y: auto;
  overflow-x: hidden;
  transition: width .25s ease, min-width .25s ease;
  position: relative;
  z-index: 50;
  flex-shrink: 0;
}

.sidebar.collapsed { width: 64px; min-width: 64px; }

.sidebar-logo {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 20px 16px 18px;
  border-bottom: 1px solid rgba(255,255,255,.07);
  flex-shrink: 0;
}

.logo-icon {
  width: 38px; height: 38px;
  border-radius: 10px;
  background: linear-gradient(135deg, #3b82f6, #6366f1);
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-size: 16px; flex-shrink: 0;
  box-shadow: 0 4px 12px rgba(59,130,246,.4);
}

.logo-text { overflow: hidden; white-space: nowrap; }
.logo-title { font-size: 14px; font-weight: 700; color: #fff; letter-spacing: -.2px; }
.logo-sub   { font-size: 11px; color: var(--muted); }

.sidebar-nav {
  flex: 1; padding: 10px 10px; display: flex; flex-direction: column; gap: 1px;
}

.nav-label {
  font-size: 10px; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: #475569;
  padding: 12px 8px 4px; white-space: nowrap; overflow: hidden;
}

.nav-item {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 10px; border-radius: var(--r-sm);
  color: #94a3b8; cursor: pointer; transition: all .15s;
  white-space: nowrap; overflow: hidden; position: relative;
  user-select: none;
}

.nav-item i { width: 18px; text-align: center; font-size: 14px; flex-shrink: 0; }
.nav-item span { font-size: 13px; font-weight: 500; overflow: hidden; white-space: nowrap; }
.nav-item:hover { background: rgba(255,255,255,.06); color: #fff; }
.nav-item.active { background: rgba(59,130,246,.15); color: #3b82f6; }
.nav-item.active i { color: #3b82f6; }

.nav-badge {
  margin-left: auto; background: #3b82f6; color: #fff;
  font-size: 10px; font-weight: 700; padding: 2px 7px;
  border-radius: 99px; flex-shrink: 0;
}

.sidebar-footer { padding: 10px; border-top: 1px solid rgba(255,255,255,.07); flex-shrink: 0; }

.sidebar-toggle {
  position: absolute; right: -13px; top: 24px;
  width: 26px; height: 26px; border-radius: 50%;
  background: #334155; border: 2px solid var(--navy);
  color: #94a3b8; display: flex; align-items: center; justify-content: center;
  font-size: 10px; cursor: pointer; z-index: 60; transition: transform .25s, background .15s;
}
.sidebar-toggle:hover { background: #475569; color: #fff; }
.sidebar.collapsed .sidebar-toggle { transform: rotate(180deg); }

.sidebar.collapsed .logo-text,
.sidebar.collapsed .nav-item span,
.sidebar.collapsed .nav-label,
.sidebar.collapsed .nav-badge { opacity: 0; pointer-events: none; width: 0; }

/* ── MAIN ── */
.main-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }

.topbar {
  height: var(--topbar-h); background: var(--white);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; padding: 0 24px; gap: 16px;
  flex-shrink: 0; box-shadow: var(--shadow-sm);
}

.topbar-title { font-size: 16px; font-weight: 700; color: var(--navy); }
.topbar-sub   { font-size: 12px; color: var(--slate); margin-top: 1px; }
.topbar-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; }

/* ── BUTTONS ── */
.btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 16px; border-radius: var(--r-sm);
  font-size: 13px; font-weight: 500; transition: all .15s; white-space: nowrap; cursor: pointer;
}
.btn-primary  { background: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-d); box-shadow: 0 4px 14px rgba(59,130,246,.35); }
.btn-outline  { background: transparent; color: var(--navy-3); border: 1.5px solid var(--border); }
.btn-outline:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-l); }
.btn-ghost    { background: transparent; color: var(--slate); }
.btn-ghost:hover { background: var(--surface); color: var(--navy); }
.btn-danger   { background: var(--red); color: #fff; }
.btn-danger:hover { background: #dc2626; }
.btn-sm       { padding: 6px 12px; font-size: 12px; }

.icon-btn {
  width: 34px; height: 34px; border-radius: var(--r-sm);
  background: var(--surface); border: 1px solid var(--border);
  color: var(--slate); display: flex; align-items: center; justify-content: center;
  cursor: pointer; transition: all .15s; font-size: 13px; flex-shrink: 0;
}
.icon-btn:hover { background: var(--accent-l); color: var(--accent); border-color: var(--accent); }
.icon-btn.danger:hover { background: var(--red-l); color: var(--red); border-color: var(--red); }

/* ── CONTENT ── */
.content-area { flex: 1; overflow-y: auto; padding: 24px; }
.page { display: none; }
.page.active { display: block; animation: fadeIn .2s ease; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ═══════════════════════════════════
   DASHBOARD
═══════════════════════════════════ */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(175px, 1fr));
  gap: 16px; margin-bottom: 24px;
}

.stat-card {
  background: var(--white); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 20px 18px;
  box-shadow: var(--shadow-sm); transition: transform .18s, box-shadow .18s;
}
.stat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }

.stat-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 14px; }

.stat-icon {
  width: 42px; height: 42px; border-radius: var(--r-md);
  display: flex; align-items: center; justify-content: center; font-size: 17px;
}
.si-blue   { background: #eff6ff; color: #3b82f6; }
.si-green  { background: #ecfdf5; color: #10b981; }
.si-purple { background: #f5f3ff; color: #8b5cf6; }
.si-amber  { background: #fffbeb; color: #f59e0b; }
.si-red    { background: #fef2f2; color: #ef4444; }

.stat-val   { font-size: 30px; font-weight: 700; line-height: 1; color: var(--navy); }
.stat-label { font-size: 12px; color: var(--slate); margin-top: 4px; }

.progress-bar {
  height: 5px; background: var(--border);
  border-radius: 99px; overflow: hidden; margin-top: 10px;
}
.progress-fill {
  height: 100%; border-radius: 99px;
  background: linear-gradient(90deg, #3b82f6, #6366f1);
  transition: width .6s ease;
}

/* chart */
.chart-card {
  background: var(--white); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 20px 24px;
  box-shadow: var(--shadow-sm); margin-bottom: 24px;
}
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.card-title  { font-size: 14px; font-weight: 600; color: var(--navy); }
.card-sub    { font-size: 12px; color: var(--slate); margin-top: 2px; }

.bar-chart { display: flex; align-items: flex-end; gap: 10px; height: 130px; padding-top: 24px; }
.bar-col-wrap { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; height: 100%; }
.bar-inner { width: 100%; display: flex; align-items: flex-end; flex: 1; }
.bar {
  width: 100%; border-radius: 6px 6px 0 0;
  background: linear-gradient(180deg, #60a5fa, #3b82f6);
  min-height: 4px; transition: height .5s ease; position: relative; cursor: pointer;
}
.bar:hover { background: linear-gradient(180deg, #93c5fd, #2563eb); }
.bar::before {
  content: attr(data-val);
  position: absolute; top: -22px; left: 50%; transform: translateX(-50%);
  font-size: 11px; font-weight: 700; color: var(--navy-3); white-space: nowrap;
}
.bar-lbl { font-size: 10px; color: var(--slate); white-space: nowrap; text-align: center; }

/* section */
.section-header {
  display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px;
}
.section-title { font-size: 14px; font-weight: 600; color: var(--navy); }

/* ═══════════════════════════════════
   TABLE
═══════════════════════════════════ */
.toolbar {
  background: var(--white); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 14px 18px;
  margin-bottom: 16px; display: flex; flex-wrap: wrap;
  align-items: center; gap: 10px; box-shadow: var(--shadow-sm);
}

.search-box {
  display: flex; align-items: center; gap: 8px;
  background: var(--surface); border: 1.5px solid var(--border);
  border-radius: var(--r-sm); padding: 7px 12px;
  flex: 1; min-width: 200px; transition: border-color .15s;
}
.search-box:focus-within { border-color: var(--accent); background: #fff; }
.search-box i  { color: var(--muted); font-size: 13px; flex-shrink: 0; }
.search-box input { background: none; border: none; flex: 1; font-size: 13px; color: var(--navy); }
.search-box input::placeholder { color: var(--muted); }

.filter-sel {
  background: var(--surface); border: 1.5px solid var(--border);
  border-radius: var(--r-sm); padding: 7px 12px; font-size: 13px;
  color: var(--navy-3); cursor: pointer; transition: border-color .15s;
}
.filter-sel:focus { border-color: var(--accent); }

.count-chip {
  font-size: 12px; color: var(--slate); background: var(--surface);
  border: 1px solid var(--border); border-radius: 99px; padding: 4px 12px;
  white-space: nowrap; flex-shrink: 0;
}

/* table wrap */
.table-card {
  background: var(--white); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-sm); overflow: hidden;
}
.table-scroll { overflow-x: auto; }

table { width: 100%; border-collapse: collapse; min-width: 860px; }

thead { background: var(--navy); }
thead th {
  padding: 12px 14px; text-align: left; font-size: 10.5px;
  font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: #64748b; white-space: nowrap;
}
thead th.sort { cursor: pointer; user-select: none; }
thead th.sort:hover { color: #fff; }
thead th.asc::after  { content: ' ↑'; color: #60a5fa; }
thead th.desc::after { content: ' ↓'; color: #60a5fa; }

tbody tr { border-bottom: 1px solid var(--border); transition: background .1s; }
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: #f0f7ff; }

tbody td { padding: 11px 14px; font-size: 13px; color: var(--navy-3); vertical-align: middle; }

.td-id    { font-family: var(--mono); font-size: 11px; color: var(--muted); white-space: nowrap; }
.td-main  { font-weight: 500; color: var(--navy); max-width: 220px; }
.td-main small { display: block; font-size: 11px; font-weight: 400; color: var(--slate); margin-top: 1px; }
.td-date  { font-size: 12px; white-space: nowrap; color: var(--slate); }
.td-act   { white-space: nowrap; }

/* badges */
.badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 9px; border-radius: 99px; font-size: 11px; font-weight: 600; white-space: nowrap;
}
.bm  { background: #eff6ff; color: #3b82f6; }
.bk  { background: #f5f3ff; color: #7c3aed; }
.ba  { background: #ecfdf5; color: #059669; }
.bar2{ background: #fffbeb; color: #b45309; }
.bh  { background: #fef2f2; color: #dc2626; }
.br  { background: #fdf2f8; color: #9d174d; }
.bp  { background: #fff7ed; color: #c2410c; }
.bb  { background: var(--surface); color: var(--slate); border: 1px solid var(--border); }

.action-row { display: flex; gap: 5px; }

/* pagination */
.pag-wrap {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 18px; border-top: 1px solid var(--border); flex-wrap: wrap; gap: 10px;
}
.pag-info { font-size: 12px; color: var(--slate); }
.pag-btns { display: flex; gap: 4px; }
.pag-btn {
  width: 32px; height: 32px; border-radius: var(--r-sm);
  background: var(--surface); border: 1px solid var(--border);
  color: var(--navy-3); font-size: 12px; display: flex; align-items: center;
  justify-content: center; cursor: pointer; transition: all .12s;
}
.pag-btn:hover { background: var(--accent-l); color: var(--accent); border-color: var(--accent); }
.pag-btn.on { background: var(--accent); color: #fff; border-color: var(--accent); }
.pag-btn:disabled { opacity: .35; pointer-events: none; }

/* ═══════════════════════════════════
   MODAL
═══════════════════════════════════ */
.overlay {
  position: fixed; inset: 0; background: rgba(15,23,42,.6);
  backdrop-filter: blur(4px); z-index: 200;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none; transition: opacity .2s; padding: 16px;
}
.overlay.open { opacity: 1; pointer-events: all; }

.modal {
  background: var(--white); border-radius: var(--r-xl);
  box-shadow: var(--shadow-xl); width: 100%; max-width: 600px;
  max-height: 92vh; overflow-y: auto;
  transform: translateY(16px) scale(.97); transition: transform .22s ease;
}
.overlay.open .modal { transform: translateY(0) scale(1); }

.modal-lg { max-width: 680px; }
.modal-sm { max-width: 420px; }

.modal-hd {
  display: flex; align-items: center; justify-content: space-between;
  padding: 20px 24px 16px; border-bottom: 1px solid var(--border);
  position: sticky; top: 0; background: var(--white); z-index: 2;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
}
.modal-title { font-size: 16px; font-weight: 700; color: var(--navy); }
.modal-sub   { font-size: 12px; color: var(--slate); }
.modal-x {
  width: 32px; height: 32px; border-radius: 8px;
  background: var(--surface); border: 1px solid var(--border);
  color: var(--slate); display: flex; align-items: center;
  justify-content: center; cursor: pointer; font-size: 13px; transition: all .15s;
}
.modal-x:hover { background: var(--red-l); color: var(--red); border-color: var(--red); }

.modal-bd { padding: 22px 24px; }

.modal-ft {
  display: flex; justify-content: flex-end; gap: 10px;
  padding: 14px 24px 20px; border-top: 1px solid var(--border);
}

/* form */
.fgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.fgrid .full { grid-column: 1 / -1; }
.ffield { display: flex; flex-direction: column; gap: 5px; }
.flabel {
  font-size: 12px; font-weight: 600; color: var(--navy-3);
  display: flex; align-items: center; gap: 3px;
}
.flabel .req { color: var(--red); }
.finput, .fsel, .ftarea {
  background: var(--surface); border: 1.5px solid var(--border);
  border-radius: var(--r-sm); padding: 9px 12px; font-size: 13px;
  color: var(--navy); transition: border-color .15s, background .15s; width: 100%;
}
.finput:focus, .fsel:focus, .ftarea:focus {
  border-color: var(--accent); background: #fff;
  box-shadow: 0 0 0 3px rgba(59,130,246,.1);
}
.ftarea { min-height: 80px; resize: vertical; }
.fhint  { font-size: 11px; color: var(--muted); }

/* ═══════════════════════════════════
   DETAIL PANEL
═══════════════════════════════════ */
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.detail-full { grid-column: 1 / -1; }
.dl { font-size: 10.5px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.dv { font-size: 13px; color: var(--navy); margin-top: 4px; font-weight: 500; }
.dv.mono { font-family: var(--mono); font-size: 12px; }
.dv a    { color: var(--accent); }

/* ═══════════════════════════════════
   SETTINGS
═══════════════════════════════════ */
.settings-wrap { display: grid; grid-template-columns: 220px 1fr; gap: 20px; }

.settings-sidenav {
  background: var(--white); border: 1px solid var(--border);
  border-radius: var(--r-lg); overflow: hidden;
  box-shadow: var(--shadow-sm); height: fit-content;
}
.snav-item {
  display: flex; align-items: center; gap: 10px; padding: 12px 16px;
  font-size: 13px; font-weight: 500; color: var(--navy-3);
  cursor: pointer; transition: all .15s; border-bottom: 1px solid var(--border);
}
.snav-item:last-child { border-bottom: none; }
.snav-item:hover  { background: var(--surface); color: var(--navy); }
.snav-item.active { background: var(--accent-l); color: var(--accent); }
.snav-item i { width: 16px; text-align: center; font-size: 13px; }

.spanel {
  background: var(--white); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 26px; box-shadow: var(--shadow-sm);
}
.spanel-title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
.spanel-sub   { font-size: 12px; color: var(--slate); margin-bottom: 22px; }
.sdivider     { height: 1px; background: var(--border); margin: 20px 0; }

.info-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); padding: 16px; margin-top: 16px;
}
.info-row {
  display: flex; align-items: flex-start; gap: 10px;
  font-size: 12px; color: var(--navy-3); margin-bottom: 8px; line-height: 1.5;
}
.info-row:last-child { margin-bottom: 0; }
.info-row i { color: var(--muted); width: 14px; margin-top: 2px; flex-shrink: 0; }
.info-row a { color: var(--accent); }

/* ═══════════════════════════════════
   EMPTY / LOADING / TOAST
═══════════════════════════════════ */
.empty-state {
  text-align: center; padding: 56px 20px; color: var(--muted);
}
.empty-state .ei { font-size: 52px; margin-bottom: 14px; opacity: .25; display: block; }
.empty-state h3  { font-size: 15px; font-weight: 600; color: var(--slate); margin-bottom: 6px; }
.empty-state p   { font-size: 13px; }
.empty-state button { margin-top: 16px; }

.spinner-row td { text-align: center; padding: 48px 20px; }
.spinner {
  width: 30px; height: 30px; border: 3px solid var(--border);
  border-top-color: var(--accent); border-radius: 50%;
  animation: spin .65s linear infinite; margin: 0 auto 10px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-txt { font-size: 13px; color: var(--muted); }

.toast-wrap {
  position: fixed; bottom: 24px; right: 24px; z-index: 500;
  display: flex; flex-direction: column; gap: 8px;
}
.toast {
  display: flex; align-items: center; gap: 11px; padding: 12px 16px;
  border-radius: var(--r-md); background: var(--navy-2); color: #fff;
  font-size: 13px; font-weight: 500; box-shadow: var(--shadow-lg);
  min-width: 240px; max-width: 360px; animation: toastIn .22s ease;
}
@keyframes toastIn { from { transform: translateX(110%); opacity: 0; } to { transform: none; opacity: 1; } }
.toast i    { font-size: 15px; flex-shrink: 0; }
.toast span { flex: 1; }
.toast .tx  { opacity: .55; cursor: pointer; font-size: 12px; flex-shrink: 0; }
.toast .tx:hover { opacity: 1; }
.t-success i { color: #10b981; }
.t-error   i { color: #ef4444; }
.t-warning i { color: #f59e0b; }
.t-info    i { color: #60a5fa; }

/* ── Confirm modal ── */
.confirm-body { text-align: center; padding: 32px 24px; }
.confirm-icon { font-size: 40px; margin-bottom: 12px; }
.confirm-body h3  { font-size: 16px; font-weight: 700; margin-bottom: 8px; }
.confirm-body p   { font-size: 13px; color: var(--slate); }
.confirm-actions  { margin-top: 24px; display: flex; justify-content: center; gap: 10px; }

/* ── responsive ── */
@media (max-width: 768px) {
  :root { --sidebar-w: 260px; }
  .sidebar { position: fixed; transform: translateX(-100%); }
  .sidebar.mob-open { transform: none; }
  .settings-wrap { grid-template-columns: 1fr; }
  .fgrid { grid-template-columns: 1fr; }
  .fgrid .full { grid-column: 1; }
  .stat-grid { grid-template-columns: 1fr 1fr; }
}
</style>
</head>

<body>
<div class="app-shell">

<!-- ══════════ SIDEBAR ══════════ -->
<aside class="sidebar" id="sidebar">
  <button class="sidebar-toggle" onclick="toggleSidebar()"><i class="fa fa-chevron-left"></i></button>

  <div class="sidebar-logo">
    <div class="logo-icon"><i class="fa fa-folder-open"></i></div>
    <div class="logo-text">
      <div class="logo-title" id="sbOrgName">Arsip Surat</div>
      <div class="logo-sub">Manajemen Dokumen</div>
    </div>
  </div>

  <nav class="sidebar-nav">
    <div class="nav-label">Utama</div>
    <div class="nav-item active" data-page="dashboard" onclick="showPage('dashboard')">
      <i class="fa fa-chart-pie"></i><span>Dashboard</span>
    </div>
    <div class="nav-item" data-page="surat-masuk" onclick="showPage('surat-masuk')">
      <i class="fa fa-inbox"></i><span>Surat Masuk</span>
      <span class="nav-badge" id="bdgMasuk">0</span>
    </div>
    <div class="nav-item" data-page="surat-keluar" onclick="showPage('surat-keluar')">
      <i class="fa fa-paper-plane"></i><span>Surat Keluar</span>
      <span class="nav-badge" id="bdgKeluar">0</span>
    </div>
    <div class="nav-item" data-page="semua-surat" onclick="showPage('semua-surat')">
      <i class="fa fa-list-ul"></i><span>Semua Arsip</span>
    </div>

    <div class="nav-label">Alat</div>
    <div class="nav-item" onclick="doExportCsv()">
      <i class="fa fa-file-csv"></i><span>Ekspor CSV</span>
    </div>
    <div class="nav-item" onclick="refreshAll()">
      <i class="fa fa-rotate-right"></i><span>Refresh Data</span>
    </div>

    <div class="nav-label">Sistem</div>
    <div class="nav-item" data-page="settings" onclick="showPage('settings')">
      <i class="fa fa-gear"></i><span>Pengaturan</span>
    </div>
  </nav>

  <div class="sidebar-footer">
    <div style="font-size:11px;color:#475569;padding:6px 8px;white-space:nowrap;overflow:hidden;">
      <i class="fa fa-shield-halved" style="margin-right:6px;"></i><span>v2.0 · Google Apps Script</span>
    </div>
  </div>
</aside>

<!-- ══════════ MAIN ══════════ -->
<div class="main-area">

  <!-- TOPBAR -->
  <header class="topbar">
    <button class="icon-btn" id="mobMenuBtn" onclick="toggleMobile()" style="display:none;">
      <i class="fa fa-bars"></i>
    </button>
    <div>
      <div class="topbar-title" id="tbTitle">Dashboard</div>
      <div class="topbar-sub"  id="tbSub">Ringkasan arsip surat</div>
    </div>
    <div class="topbar-actions">
      <button class="icon-btn" onclick="refreshAll()" title="Refresh data"><i class="fa fa-rotate-right"></i></button>
      <button class="btn btn-primary" onclick="openAdd()">
        <i class="fa fa-plus"></i><span>Tambah Surat</span>
      </button>
    </div>
  </header>

  <!-- PAGES -->
  <div class="content-area">

    <!-- ── DASHBOARD ── -->
    <div class="page active" id="page-dashboard">
      <div class="stat-grid">
        <div class="stat-card">
          <div class="stat-top">
            <div><div class="stat-val" id="stTotal">—</div><div class="stat-label">Total Arsip</div></div>
            <div class="stat-icon si-blue"><i class="fa fa-folder-open"></i></div>
          </div>
          <div class="progress-bar"><div class="progress-fill" id="pfTotal" style="width:0%"></div></div>
        </div>
        <div class="stat-card">
          <div class="stat-top">
            <div><div class="stat-val" id="stMasuk">—</div><div class="stat-label">Surat Masuk</div></div>
            <div class="stat-icon si-green"><i class="fa fa-inbox"></i></div>
          </div>
          <div class="progress-bar"><div class="progress-fill" id="pfMasuk" style="width:0%;background:linear-gradient(90deg,#10b981,#059669)"></div></div>
        </div>
        <div class="stat-card">
          <div class="stat-top">
            <div><div class="stat-val" id="stKeluar">—</div><div class="stat-label">Surat Keluar</div></div>
            <div class="stat-icon si-purple"><i class="fa fa-paper-plane"></i></div>
          </div>
          <div class="progress-bar"><div class="progress-fill" id="pfKeluar" style="width:0%;background:linear-gradient(90deg,#8b5cf6,#7c3aed)"></div></div>
        </div>
        <div class="stat-card">
          <div class="stat-top">
            <div><div class="stat-val" id="stBulan">—</div><div class="stat-label">Bulan Ini</div></div>
            <div class="stat-icon si-amber"><i class="fa fa-calendar-day"></i></div>
          </div>
        </div>
        <div class="stat-card">
          <div class="stat-top">
            <div><div class="stat-val" id="stAktif">—</div><div class="stat-label">Aktif</div></div>
            <div class="stat-icon si-green"><i class="fa fa-circle-check"></i></div>
          </div>
        </div>
        <div class="stat-card">
          <div class="stat-top">
            <div><div class="stat-val" id="stArsip">—</div><div class="stat-label">Diarsipkan</div></div>
            <div class="stat-icon si-amber"><i class="fa fa-archive"></i></div>
          </div>
        </div>
      </div>

      <div class="chart-card">
        <div class="card-header">
          <div>
            <div class="card-title">Tren Arsip 6 Bulan Terakhir</div>
            <div class="card-sub">Jumlah surat diinput per bulan</div>
          </div>
        </div>
        <div class="bar-chart" id="barChart">
          <div style="color:var(--muted);font-size:13px;margin:auto;">Memuat data…</div>
        </div>
      </div>

      <div class="section-header">
        <div class="section-title"><i class="fa fa-clock-rotate-left" style="margin-right:7px;color:var(--accent);"></i>Arsip Terbaru</div>
        <button class="btn btn-outline btn-sm" onclick="showPage('semua-surat')">Lihat Semua →</button>
      </div>
      <div class="table-card">
        <div class="table-scroll">
          <table>
            <thead><tr>
              <th>ID</th><th>Jenis</th><th>Nomor Surat</th>
              <th>Perihal</th><th>Dari/Kepada</th><th>Tanggal</th><th>Status</th><th>Aksi</th>
            </tr></thead>
            <tbody id="tbRecent">
              <tr class="spinner-row"><td colspan="8"><div class="spinner"></div><div class="loading-txt">Memuat…</div></td></tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>

    <!-- ── SURAT MASUK ── -->
    <div class="page" id="page-surat-masuk">
      <div class="toolbar">
        <div class="search-box">
          <i class="fa fa-magnifying-glass"></i>
          <input id="q-masuk" type="text" placeholder="Cari nomor, perihal, pengirim…" oninput="doSearch('masuk')"/>
        </div>
        <select class="filter-sel" id="fs-masuk" onchange="doSearch('masuk')">
          <option value="">Semua Status</option>
          <option value="Aktif">Aktif</option>
          <option value="Diarsipkan">Diarsipkan</option>
          <option value="Dihapus">Dihapus</option>
        </select>
        <select class="filter-sel" id="fk-masuk" onchange="doSearch('masuk')">
          <option value="">Semua Klasifikasi</option>
          <option value="Biasa">Biasa</option>
          <option value="Penting">Penting</option>
          <option value="Rahasia">Rahasia</option>
          <option value="Sangat Rahasia">Sangat Rahasia</option>
        </select>
        <span class="count-chip" id="cnt-masuk">0 arsip</span>
        <button class="btn btn-primary btn-sm" onclick="openAdd('Masuk')"><i class="fa fa-plus"></i> Tambah</button>
      </div>
      <div class="table-card">
        <div class="table-scroll">
          <table>
            <thead><tr>
              <th class="sort" onclick="sortTable('masuk',0)">ID</th>
              <th>Nomor Surat</th>
              <th class="sort" onclick="sortTable('masuk',1)">Perihal</th>
              <th>Dari</th>
              <th class="sort" onclick="sortTable('masuk',2)">Tanggal</th>
              <th>Klasifikasi</th><th>Status</th><th>Aksi</th>
            </tr></thead>
            <tbody id="tb-masuk">
              <tr class="spinner-row"><td colspan="8"><div class="spinner"></div><div class="loading-txt">Memuat…</div></td></tr>
            </tbody>
          </table>
        </div>
        <div class="pag-wrap" id="pg-masuk"></div>
      </div>
    </div>

    <!-- ── SURAT KELUAR ── -->
    <div class="page" id="page-surat-keluar">
      <div class="toolbar">
        <div class="search-box">
          <i class="fa fa-magnifying-glass"></i>
          <input id="q-keluar" type="text" placeholder="Cari nomor, perihal, tujuan…" oninput="doSearch('keluar')"/>
        </div>
        <select class="filter-sel" id="fs-keluar" onchange="doSearch('keluar')">
          <option value="">Semua Status</option>
          <option value="Aktif">Aktif</option>
          <option value="Diarsipkan">Diarsipkan</option>
          <option value="Dihapus">Dihapus</option>
        </select>
        <select class="filter-sel" id="fk-keluar" onchange="doSearch('keluar')">
          <option value="">Semua Klasifikasi</option>
          <option value="Biasa">Biasa</option>
          <option value="Penting">Penting</option>
          <option value="Rahasia">Rahasia</option>
          <option value="Sangat Rahasia">Sangat Rahasia</option>
        </select>
        <span class="count-chip" id="cnt-keluar">0 arsip</span>
        <button class="btn btn-primary btn-sm" onclick="openAdd('Keluar')"><i class="fa fa-plus"></i> Tambah</button>
      </div>
      <div class="table-card">
        <div class="table-scroll">
          <table>
            <thead><tr>
              <th class="sort" onclick="sortTable('keluar',0)">ID</th>
              <th>Nomor Surat</th>
              <th class="sort" onclick="sortTable('keluar',1)">Perihal</th>
              <th>Kepada</th>
              <th class="sort" onclick="sortTable('keluar',2)">Tanggal</th>
              <th>Klasifikasi</th><th>Status</th><th>Aksi</th>
            </tr></thead>
            <tbody id="tb-keluar">
              <tr class="spinner-row"><td colspan="8"><div class="spinner"></div><div class="loading-txt">Memuat…</div></td></tr>
            </tbody>
          </table>
        </div>
        <div class="pag-wrap" id="pg-keluar"></div>
      </div>
    </div>

    <!-- ── SEMUA ARSIP ── -->
    <div class="page" id="page-semua-surat">
      <div class="toolbar">
        <div class="search-box">
          <i class="fa fa-magnifying-glass"></i>
          <input id="q-semua" type="text" placeholder="Cari di seluruh arsip…" oninput="doSearch('semua')"/>
        </div>
        <select class="filter-sel" id="fj-semua" onchange="doSearch('semua')">
          <option value="">Semua Jenis</option>
          <option value="Masuk">Masuk</option>
          <option value="Keluar">Keluar</option>
        </select>
        <select class="filter-sel" id="fs-semua" onchange="doSearch('semua')">
          <option value="">Semua Status</option>
          <option value="Aktif">Aktif</option>
          <option value="Diarsipkan">Diarsipkan</option>
          <option value="Dihapus">Dihapus</option>
        </select>
        <select class="filter-sel" id="fk-semua" onchange="doSearch('semua')">
          <option value="">Semua Klasifikasi</option>
          <option value="Biasa">Biasa</option>
          <option value="Penting">Penting</option>
          <option value="Rahasia">Rahasia</option>
          <option value="Sangat Rahasia">Sangat Rahasia</option>
        </select>
        <span class="count-chip" id="cnt-semua">0 arsip</span>
      </div>
      <div class="table-card">
        <div class="table-scroll">
          <table>
            <thead><tr>
              <th>ID</th><th>Jenis</th><th>Nomor Surat</th>
              <th>Perihal</th><th>Dari/Kepada</th>
              <th class="sort" onclick="sortTable('semua',0)">Tanggal</th>
              <th>Klasifikasi</th><th>Status</th><th>Aksi</th>
            </tr></thead>
            <tbody id="tb-semua">
              <tr class="spinner-row"><td colspan="9"><div class="spinner"></div><div class="loading-txt">Memuat…</div></td></tr>
            </tbody>
          </table>
        </div>
        <div class="pag-wrap" id="pg-semua"></div>
      </div>
    </div>

    <!-- ── SETTINGS ── -->
    <div class="page" id="page-settings">
      <div class="settings-wrap">
        <div class="settings-sidenav">
          <div class="snav-item active" onclick="showSpanel('sheet')"><i class="fa fa-table-cells"></i> Google Sheet</div>
          <div class="snav-item" onclick="showSpanel('org')"><i class="fa fa-building"></i> Organisasi</div>
          <div class="snav-item" onclick="showSpanel('about')"><i class="fa fa-circle-info"></i> Tentang</div>
        </div>

        <div>
          <!-- Sheet panel -->
          <div class="spanel" id="sp-sheet">
            <div class="spanel-title">Konfigurasi Google Sheet</div>
            <div class="spanel-sub">Hubungkan aplikasi ke Spreadsheet Google Anda. Data akan otomatis tersimpan di sana.</div>

            <div class="ffield" style="margin-bottom:14px;">
              <label class="flabel">ID Spreadsheet</label>
              <input class="finput" id="cfgId" placeholder="Tempel Spreadsheet ID…"/>
              <div class="fhint">Ambil dari URL: …/spreadsheets/d/<strong>ID_INI</strong>/edit</div>
            </div>

            <div class="ffield" style="margin-bottom:14px;">
              <label class="flabel">Nama Sheet / Tab</label>
              <input class="finput" id="cfgName" placeholder="ArsipSurat"/>
              <div class="fhint">Nama tab di dalam spreadsheet. Dibuat otomatis jika belum ada. Default: ArsipSurat</div>
            </div>

            <div class="info-card" id="sheetInfoBox" style="display:none;">
              <div class="info-row"><i class="fa fa-circle-check" style="color:var(--green);"></i><strong>Terhubung ke:</strong></div>
              <div class="info-row"><i class="fa fa-table"></i><span id="siName">—</span></div>
              <div class="info-row"><i class="fa fa-link"></i><a id="siUrl" href="#" target="_blank">Buka Spreadsheet ↗</a></div>
              <div class="info-row"><i class="fa fa-layer-group"></i>Sheet: <span id="siSheets">—</span></div>
            </div>

            <div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:20px;">
              <button class="btn btn-primary" onclick="saveSettings()"><i class="fa fa-floppy-disk"></i> Simpan</button>
              <button class="btn btn-outline" onclick="testConn()"><i class="fa fa-plug"></i> Test Koneksi</button>
              <button class="btn btn-outline" onclick="initSht()"><i class="fa fa-table"></i> Inisialisasi Sheet</button>
            </div>
          </div>

          <!-- Org panel -->
          <div class="spanel" id="sp-org" style="display:none;">
            <div class="spanel-title">Identitas Organisasi</div>
            <div class="spanel-sub">Nama yang ditampilkan di sidebar dan header aplikasi.</div>
            <div class="ffield" style="margin-bottom:14px;">
              <label class="flabel">Nama Instansi / Organisasi</label>
              <input class="finput" id="cfgOrg" placeholder="Contoh: Dinas Kesehatan Kota Manado"/>
            </div>
            <div class="ffield" style="margin-bottom:14px;">
              <label class="flabel">URL Logo <span style="color:var(--muted);font-weight:400;">(opsional)</span></label>
              <input class="finput" id="cfgLogo" placeholder="https://…"/>
            </div>
            <button class="btn btn-primary" style="margin-top:8px;" onclick="saveSettings()"><i class="fa fa-floppy-disk"></i> Simpan</button>
          </div>

          <!-- About panel -->
          <div class="spanel" id="sp-about" style="display:none;">
            <div class="spanel-title">Tentang Aplikasi</div>
            <div class="spanel-sub">Informasi sistem dan versi.</div>
            <div style="display:flex;flex-direction:column;gap:14px;">
              <div><div class="dl">Nama Aplikasi</div><div class="dv">Arsip Surat</div></div>
              <div><div class="dl">Versi</div><div class="dv">2.0.0</div></div>
              <div><div class="dl">Platform</div><div class="dv">Google Apps Script + Google Sheets</div></div>
              <div><div class="dl">Dibuat Dengan</div><div class="dv">HTML · CSS · JavaScript · GAS</div></div>
              <div class="sdivider"></div>
              <div><div class="dl">Fitur</div>
                <div class="dv" style="font-weight:400;font-size:13px;color:var(--slate);line-height:1.8;">
                  ✓ Surat Masuk &amp; Keluar<br>
                  ✓ Pencarian &amp; Filter Multi-dimensi<br>
                  ✓ Klasifikasi &amp; Status Surat<br>
                  ✓ Lampiran via Google Drive<br>
                  ✓ Ekspor CSV<br>
                  ✓ Tren Statistik 6 Bulan<br>
                  ✓ Konfigurasi Spreadsheet Langsung dari App<br>
                  ✓ Pagination &amp; Sorting
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

  </div><!-- /content-area -->
</div><!-- /main-area -->
</div><!-- /app-shell -->

<!-- ══════════ MODAL: TAMBAH / EDIT ══════════ -->
<div class="overlay" id="ovForm">
  <div class="modal modal-lg">
    <div class="modal-hd">
      <div>
        <div class="modal-title" id="fmTitle">Tambah Surat Baru</div>
        <div class="modal-sub" id="fmSub">Isi semua kolom yang bertanda *</div>
      </div>
      <button class="modal-x" onclick="closeOv('ovForm')"><i class="fa fa-xmark"></i></button>
    </div>
    <div class="modal-bd">
      <div class="fgrid">
        <!-- row 1 -->
        <div class="ffield">
          <label class="flabel">Jenis Surat <span class="req">*</span></label>
          <select class="fsel" id="fJenis" onchange="updateLabel()">
            <option value="">— Pilih Jenis —</option>
            <option value="Masuk">📥 Surat Masuk</option>
            <option value="Keluar">📤 Surat Keluar</option>
          </select>
        </div>
        <div class="ffield">
          <label class="flabel">Tanggal Surat <span class="req">*</span></label>
          <input type="date" class="finput" id="fTanggal"/>
        </div>
        <!-- row 2 -->
        <div class="ffield full">
          <label class="flabel">Nomor Surat <span class="req">*</span></label>
          <input class="finput" id="fNomor" placeholder="Contoh: 045/SK-VII/2025"/>
        </div>
        <!-- row 3 -->
        <div class="ffield full">
          <label class="flabel">Perihal <span class="req">*</span></label>
          <input class="finput" id="fPerihal" placeholder="Ringkasan isi surat…"/>
        </div>
        <!-- row 4 -->
        <div class="ffield full">
          <label class="flabel" id="fDKLabel">Dari / Kepada <span class="req">*</span></label>
          <input class="finput" id="fDariKepada" placeholder="Nama instansi / perorangan…"/>
        </div>
        <!-- row 5 -->
        <div class="ffield">
          <label class="flabel">Klasifikasi</label>
          <select class="fsel" id="fKlasif">
            <option value="Biasa">Biasa</option>
            <option value="Penting">⚠️ Penting</option>
            <option value="Rahasia">🔒 Rahasia</option>
            <option value="Sangat Rahasia">🔐 Sangat Rahasia</option>
          </select>
        </div>
        <div class="ffield">
          <label class="flabel">Status</label>
          <select class="fsel" id="fStatus">
            <option value="Aktif">Aktif</option>
            <option value="Diarsipkan">Diarsipkan</option>
            <option value="Dihapus">Dihapus</option>
          </select>
        </div>
        <!-- row 6 -->
        <div class="ffield full">
          <label class="flabel">Dibuat Oleh</label>
          <input class="finput" id="fDibuat" placeholder="Nama pegawai / petugas (opsional)"/>
          <div class="fhint">Kosongkan untuk menggunakan email akun Google Anda secara otomatis.</div>
        </div>
        <!-- row 7 -->
        <div class="ffield full">
          <label class="flabel">URL File di Google Drive <span style="color:var(--muted);font-weight:400;">(opsional)</span></label>
          <input class="finput" id="fFileUrl" placeholder="https://drive.google.com/file/d/…"/>
        </div>
        <!-- row 8 -->
        <div class="ffield full">
          <label class="flabel">Keterangan Tambahan</label>
          <textarea class="ftarea" id="fKet" placeholder="Catatan, disposisi, atau keterangan lainnya…"></textarea>
        </div>
      </div>
    </div>
    <div class="modal-ft">
      <button class="btn btn-ghost" onclick="closeOv('ovForm')">Batal</button>
      <button class="btn btn-primary" id="btnSimpan" onclick="simpan()">
        <i class="fa fa-floppy-disk"></i> Simpan
      </button>
    </div>
  </div>
</div>

<!-- ══════════ MODAL: DETAIL ══════════ -->
<div class="overlay" id="ovDetail">
  <div class="modal modal-lg">
    <div class="modal-hd">
      <div>
        <div class="modal-title">Detail Surat</div>
        <div class="modal-sub" id="dtId">—</div>
      </div>
      <button class="modal-x" onclick="closeOv('ovDetail')"><i class="fa fa-xmark"></i></button>
    </div>
    <div class="modal-bd">
      <div class="detail-grid" id="dtContent"></div>
    </div>
    <div class="modal-ft">
      <button class="btn btn-ghost" onclick="closeOv('ovDetail')">Tutup</button>
      <button class="btn btn-outline" onclick="editFromDetail()"><i class="fa fa-pen"></i> Edit</button>
    </div>
  </div>
</div>

<!-- ══════════ MODAL: KONFIRMASI HAPUS ══════════ -->
<div class="overlay" id="ovDel">
  <div class="modal modal-sm">
    <div class="confirm-body">
      <div class="confirm-icon">🗑️</div>
      <h3>Hapus Surat?</h3>
      <p>Data akan dihapus permanen dari spreadsheet dan tidak dapat dipulihkan.</p>
      <div class="confirm-actions">
        <button class="btn btn-outline" onclick="closeOv('ovDel')">Batal</button>
        <button class="btn btn-danger" id="btnDel" onclick="doDelete()"><i class="fa fa-trash"></i> Ya, Hapus</button>
      </div>
    </div>
  </div>
</div>

<!-- ══════════ TOAST ══════════ -->
<div class="toast-wrap" id="toastWrap"></div>

<script>
// ═══════════════════════════════════════════════
//  STATE
// ═══════════════════════════════════════════════
const S = {
  all      : [],           // all data from sheet
  filtered : { masuk:[], keluar:[], semua:[] },
  page     : { masuk:1,   keluar:1,  semua:1  },
  sort     : { masuk:{col:-1,dir:1}, keluar:{col:-1,dir:1}, semua:{col:-1,dir:1} },
  editId   : null,
  deleteId : null,
  detailId : null,
};

const PAGE_SIZE = 15;

const PAGE_META = {
  'dashboard'   : ['Dashboard',     'Ringkasan arsip surat'],
  'surat-masuk' : ['Surat Masuk',   'Daftar surat yang diterima'],
  'surat-keluar': ['Surat Keluar',  'Daftar surat yang dikirim'],
  'semua-surat' : ['Semua Arsip',   'Seluruh data arsip surat'],
  'settings'    : ['Pengaturan',    'Konfigurasi aplikasi & Google Sheet'],
};

// ═══════════════════════════════════════════════
//  NAVIGATION
// ═══════════════════════════════════════════════
function showPage(id) {
  document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  document.getElementById('page-' + id).classList.add('active');

  document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
  const ni = document.querySelector(`[data-page="${id}"]`);
  if (ni) ni.classList.add('active');

  const m = PAGE_META[id] || [id, ''];
  document.getElementById('tbTitle').textContent = m[0];
  document.getElementById('tbSub').textContent   = m[1];

  if (id === 'surat-masuk')  { S.filtered.masuk  = S.all.filter(x => x.jenis==='Masuk');  renderTbl('masuk');  }
  if (id === 'surat-keluar') { S.filtered.keluar = S.all.filter(x => x.jenis==='Keluar'); renderTbl('keluar'); }
  if (id === 'semua-surat')  { S.filtered.semua  = [...S.all];                             renderTbl('semua');  }
  if (id === 'settings')     { loadSettings(); }
}

function toggleSidebar() {
  document.getElementById('sidebar').classList.toggle('collapsed');
}
function toggleMobile() {
  document.getElementById('sidebar').classList.toggle('mob-open');
}

// ═══════════════════════════════════════════════
//  DATA
// ═══════════════════════════════════════════════
function refreshAll() {
  loadStats();
  loadData();
}

function loadStats() {
  google.script.run
    .withSuccessHandler(r => {
      if (!r.success) return toast(r.message, 'error');
      const s = r.stats;
      set('stTotal',  s.total);
      set('stMasuk',  s.masuk);
      set('stKeluar', s.keluar);
      set('stBulan',  s.bulanIni);
      set('stAktif',  s.aktif);
      set('stArsip',  s.arsip);
      set('bdgMasuk',  s.masuk);
      set('bdgKeluar', s.keluar);

      const mx = Math.max(...s.tren.map(t => t.total), 1);
      el('pfTotal').style.width  = '100%';
      el('pfMasuk').style.width  = s.total ? (s.masuk /s.total*100)+'%' : '0%';
      el('pfKeluar').style.width = s.total ? (s.keluar/s.total*100)+'%' : '0%';
      renderChart(s.tren, mx);
    })
    .withFailureHandler(e => toast(e.message,'error'))
    .getStats();
}

function loadData() {
  google.script.run
    .withSuccessHandler(r => {
      if (!r.success) return toast(r.message, 'error');
      S.all = r.data;
      renderRecent();

      const cur = document.querySelector('.page.active');
      if (!cur) return;
      const pid = cur.id.replace('page-','');
      if (pid==='surat-masuk')  { S.filtered.masuk  = S.all.filter(x=>x.jenis==='Masuk');  renderTbl('masuk');  }
      if (pid==='surat-keluar') { S.filtered.keluar = S.all.filter(x=>x.jenis==='Keluar'); renderTbl('keluar'); }
      if (pid==='semua-surat')  { S.filtered.semua  = [...S.all];                           renderTbl('semua');  }
    })
    .withFailureHandler(e => toast(e.message,'error'))
    .getAllSurat();
}

// ═══════════════════════════════════════════════
//  CHART
// ═══════════════════════════════════════════════
function renderChart(tren, mx) {
  const c = el('barChart');
  c.innerHTML = '';
  tren.forEach(t => {
    const pct = mx > 0 ? Math.max((t.total/mx)*100, 3) : 3;
    c.innerHTML += `
      <div class="bar-col-wrap">
        <div class="bar-inner">
          <div class="bar" style="height:${pct}%" data-val="${t.total}"></div>
        </div>
        <div class="bar-lbl">${t.bulan}</div>
      </div>`;
  });
}

// ═══════════════════════════════════════════════
//  RECENT TABLE
// ═══════════════════════════════════════════════
function renderRecent() {
  const tb = el('tbRecent');
  const d  = S.all.slice(-8).reverse();
  if (!d.length) {
    tb.innerHTML = emptyRow(8, 'Belum ada arsip', 'Klik Tambah Surat untuk memulai');
    return;
  }
  tb.innerHTML = d.map(s => `<tr>
    <td class="td-id">${s.id}</td>
    <td>${bJenis(s.jenis)}</td>
    <td style="font-weight:500">${x(s.nomorSurat)}</td>
    <td class="td-main">${x(s.perihal)}</td>
    <td>${x(s.dariKepada)}</td>
    <td class="td-date">${s.tanggalSurat}</td>
    <td>${bStatus(s.status)}</td>
    <td class="td-act"><div class="action-row">
      ${s.fileUrl ? `<button class="icon-btn" title="Buka File" onclick="window.open('${x(s.fileUrl)}','_blank')"><i class="fa fa-file-arrow-up"></i></button>` : ''}
      <button class="icon-btn" title="Detail"  onclick="openDetail('${s.id}')"><i class="fa fa-eye"></i></button>
      <button class="icon-btn" title="Edit"    onclick="openEdit('${s.id}')"><i class="fa fa-pen"></i></button>
      <button class="icon-btn danger" title="Hapus" onclick="openDel('${s.id}')"><i class="fa fa-trash"></i></button>
    </div></td>
  </tr>`).join('');
}

// ═══════════════════════════════════════════════
//  MAIN TABLE
// ═══════════════════════════════════════════════
function renderTbl(type, pg) {
  const isSemua = type === 'semua';
  const tbody   = el('tb-' + type);
  const pagDiv  = el('pg-' + type);
  const cntEl   = el('cnt-' + type);
  if (!tbody) return;

  const data = S.filtered[type];
  pg = pg || S.page[type] || 1;
  S.page[type] = pg;

  if (cntEl) cntEl.textContent = data.length + ' arsip';

  if (!data.length) {
    tbody.innerHTML = emptyRow(isSemua ? 9 : 8, 'Tidak ada data', 'Coba ubah filter atau tambah surat baru.');
    if (pagDiv) pagDiv.innerHTML = '';
    return;
  }

  const total  = data.length;
  const pages  = Math.ceil(total / PAGE_SIZE);
  const start  = (pg - 1) * PAGE_SIZE;
  const chunk  = data.slice(start, start + PAGE_SIZE);

  tbody.innerHTML = chunk.map(s => isSemua ? rowSemua(s) : rowJenis(type, s)).join('');

  // pagination
  if (pagDiv) {
    const info = `<div class="pag-info">Menampilkan ${start+1}–${Math.min(start+PAGE_SIZE,total)} dari ${total} arsip</div>`;
    let btns = `<div class="pag-btns">`;
    btns += pagBtn('‹', pg > 1, `goPage('${type}',${pg-1})`);
    const s2 = Math.max(1, Math.min(pg-2, pages-4));
    const e2 = Math.min(pages, s2+4);
    for (let i=s2;i<=e2;i++) btns += pagBtn(i, true, `goPage('${type}',${i})`, i===pg);
    btns += pagBtn('›', pg < pages, `goPage('${type}',${pg+1})`);
    btns += `</div>`;
    pagDiv.innerHTML = info + btns;
  }
}

function goPage(type, pg) { S.page[type] = pg; renderTbl(type, pg); }

function pagBtn(lbl, enabled, fn, active=false) {
  return `<button class="pag-btn${active?' on':''}" ${enabled?`onclick="${fn}"`:'disabled'}>${lbl}</button>`;
}

function rowJenis(type, s) {
  return `<tr>
    <td class="td-id">${s.id}</td>
    <td style="font-weight:500">${x(s.nomorSurat)}</td>
    <td class="td-main">${x(s.perihal)}</td>
    <td>${x(s.dariKepada)}</td>
    <td class="td-date">${s.tanggalSurat}</td>
    <td>${bKlasif(s.klasifikasi)}</td>
    <td>${bStatus(s.status)}</td>
    <td class="td-act">${actBtns(s)}</td>
  </tr>`;
}

function rowSemua(s) {
  return `<tr>
    <td class="td-id">${s.id}</td>
    <td>${bJenis(s.jenis)}</td>
    <td style="font-weight:500">${x(s.nomorSurat)}</td>
    <td class="td-main">${x(s.perihal)}</td>
    <td>${x(s.dariKepada)}</td>
    <td class="td-date">${s.tanggalSurat}</td>
    <td>${bKlasif(s.klasifikasi)}</td>
    <td>${bStatus(s.status)}</td>
    <td class="td-act">${actBtns(s)}</td>
  </tr>`;
}

function actBtns(s) {
  return `<div class="action-row">
    ${s.fileUrl ? `<button class="icon-btn" title="Buka File" onclick="window.open('${x(s.fileUrl)}','_blank')"><i class="fa fa-file-arrow-up"></i></button>` : ''}
    <button class="icon-btn" title="Detail"  onclick="openDetail('${s.id}')"><i class="fa fa-eye"></i></button>
    <button class="icon-btn" title="Edit"    onclick="openEdit('${s.id}')"><i class="fa fa-pen"></i></button>
    <button class="icon-btn danger" title="Hapus" onclick="openDel('${s.id}')"><i class="fa fa-trash"></i></button>
  </div>`;
}

// ═══════════════════════════════════════════════
//  SEARCH & FILTER
// ═══════════════════════════════════════════════
function doSearch(type) {
  const q  = (val('q-'  + type)).toLowerCase();
  const fs = val('fs-' + type);
  const fk = val('fk-' + type);
  const fj = type==='semua' ? val('fj-semua') : (type==='masuk'?'Masuk':'Keluar');

  let src = type === 'semua' ? [...S.all] : S.all.filter(s => s.jenis === (type==='masuk'?'Masuk':'Keluar'));

  if (type === 'semua' && fj) src = src.filter(s => s.jenis === fj);
  if (q)  src = src.filter(s => [s.nomorSurat,s.perihal,s.dariKepada,s.id].join(' ').toLowerCase().includes(q));
  if (fs) src = src.filter(s => s.status === fs);
  if (fk) src = src.filter(s => s.klasifikasi === fk);

  S.filtered[type] = src;
  S.page[type] = 1;
  renderTbl(type);
}

// ═══════════════════════════════════════════════
//  SORT
// ═══════════════════════════════════════════════
const SORT_KEYS = { masuk:['id','perihal','tanggalSurat'], keluar:['id','perihal','tanggalSurat'], semua:['tanggalSurat'] };

function sortTable(type, col) {
  const ss = S.sort[type];
  if (ss.col === col) ss.dir *= -1;
  else { ss.col = col; ss.dir = 1; }

  const key = SORT_KEYS[type][col];
  S.filtered[type].sort((a,b) => {
    const av = (a[key]||'').toString(), bv = (b[key]||'').toString();
    return av.localeCompare(bv) * ss.dir;
  });

  // update header classes
  const ths = document.querySelectorAll(`#page-${type==='semua'?'semua-surat':'surat-'+type} thead th.sort`);
  ths.forEach((th,i) => { th.classList.remove('asc','desc'); if(i===col) th.classList.add(ss.dir===1?'asc':'desc'); });

  renderTbl(type);
}

// ═══════════════════════════════════════════════
//  FORM MODAL
// ═══════════════════════════════════════════════
function openAdd(jenis) {
  S.editId = null;
  el('fmTitle').textContent = 'Tambah Surat Baru';
  el('fmSub').textContent   = 'Isi semua kolom yang bertanda *';
  clearForm();
  el('fTanggal').value = today();
  if (jenis) el('fJenis').value = jenis;
  updateLabel();
  openOv('ovForm');
}

function openEdit(id) {
  const s = S.all.find(x => x.id === id);
  if (!s) return toast('Data tidak ditemukan','error');
  S.editId = id;
  el('fmTitle').textContent = 'Edit Surat';
  el('fmSub').textContent   = id;
  el('fJenis').value       = s.jenis;
  el('fTanggal').value     = s.tanggalSurat;
  el('fNomor').value       = s.nomorSurat;
  el('fPerihal').value     = s.perihal;
  el('fDariKepada').value  = s.dariKepada;
  el('fKlasif').value      = s.klasifikasi;
  el('fStatus').value      = s.status;
  el('fDibuat').value      = s.dibuatOleh || '';
  el('fFileUrl').value     = s.fileUrl    || '';
  el('fKet').value         = s.keterangan || '';
  updateLabel();
  openOv('ovForm');
}

function clearForm() {
  ['fJenis','fNomor','fPerihal','fDariKepada','fFileUrl','fKet','fDibuat'].forEach(id => { const e=el(id); if(e) e.value=''; });
  el('fKlasif').value = 'Biasa';
  el('fStatus').value = 'Aktif';
}

function updateLabel() {
  const j = el('fJenis').value;
  el('fDKLabel').innerHTML = (j==='Keluar' ? 'Kepada' : 'Dari') + ' <span class="req">*</span>';
  el('fDariKepada').placeholder = j==='Keluar' ? 'Nama / instansi penerima…' : 'Nama / instansi pengirim…';
}

function simpan() {
  const data = {
    jenis      : val('fJenis'),
    tanggalSurat:val('fTanggal'),
    nomorSurat : val('fNomor'),
    perihal    : val('fPerihal'),
    dariKepada : val('fDariKepada'),
    klasifikasi: val('fKlasif'),
    status     : val('fStatus'),
    dibuatOleh : val('fDibuat'),
    fileUrl    : val('fFileUrl'),
    keterangan : val('fKet'),
  };

  if (!data.jenis)       return toast('Pilih jenis surat dahulu','warning');
  if (!data.nomorSurat)  return toast('Nomor surat wajib diisi','warning');
  if (!data.perihal)     return toast('Perihal wajib diisi','warning');
  if (!data.dariKepada)  return toast('Dari/Kepada wajib diisi','warning');
  if (!data.tanggalSurat)return toast('Tanggal surat wajib diisi','warning');

  const btn = el('btnSimpan');
  btn.disabled = true;
  btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Menyimpan…';

  const cb = r => {
    btn.disabled = false;
    btn.innerHTML = '<i class="fa fa-floppy-disk"></i> Simpan';
    if (r.success) { toast(r.message,'success'); closeOv('ovForm'); refreshAll(); }
    else toast(r.message, 'error');
  };
  const eb = e => { btn.disabled=false; btn.innerHTML='<i class="fa fa-floppy-disk"></i> Simpan'; toast(e.message,'error'); };

  if (S.editId) {
    data.id = S.editId;
    google.script.run.withSuccessHandler(cb).withFailureHandler(eb).updateSurat(data);
  } else {
    google.script.run.withSuccessHandler(cb).withFailureHandler(eb).createSurat(data);
  }
}

// ═══════════════════════════════════════════════
//  DETAIL MODAL
// ═══════════════════════════════════════════════
function openDetail(id) {
  const s = S.all.find(x => x.id === id);
  if (!s) return;
  S.detailId = id;
  el('dtId').textContent = id;

  const rows = [
    ['Jenis Surat',   bJenis(s.jenis)],
    ['Status',        bStatus(s.status)],
    ['Nomor Surat',   `<span class="mono">${x(s.nomorSurat)}</span>`],
    ['Tanggal Surat', s.tanggalSurat],
    ['Dari / Kepada', x(s.dariKepada)],
    ['Klasifikasi',   bKlasif(s.klasifikasi)],
    ['Perihal',       x(s.perihal), true],
    ['Keterangan',    x(s.keterangan)||'—', true],
    ['Dibuat Oleh',   x(s.dibuatOleh)||'—'],
    ['Tanggal Input', s.tanggalInput],
    ['Terakhir Diubah',s.terakhirDiubah],
    ['File Lampiran', s.fileUrl ? `<a href="${x(s.fileUrl)}" target="_blank">Buka File ↗</a>` : '—', true],
  ];

  el('dtContent').innerHTML = rows.map(([l,v,full]) =>
    `<div class="${full?'detail-full':''}"><div class="dl">${l}</div><div class="dv">${v}</div></div>`
  ).join('');

  openOv('ovDetail');
}

function editFromDetail() {
  closeOv('ovDetail');
  setTimeout(() => openEdit(S.detailId), 220);
}

// ═══════════════════════════════════════════════
//  DELETE
// ═══════════════════════════════════════════════
function openDel(id) { S.deleteId = id; openOv('ovDel'); }

function doDelete() {
  if (!S.deleteId) return;
  const btn = el('btnDel');
  btn.disabled = true;
  btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Menghapus…';

  google.script.run
    .withSuccessHandler(r => {
      btn.disabled = false; btn.innerHTML = '<i class="fa fa-trash"></i> Ya, Hapus';
      if (r.success) { toast(r.message,'success'); closeOv('ovDel'); refreshAll(); }
      else toast(r.message,'error');
    })
    .withFailureHandler(e => { btn.disabled=false; btn.innerHTML='<i class="fa fa-trash"></i> Ya, Hapus'; toast(e.message,'error'); })
    .deleteSurat(S.deleteId);
}

// ═══════════════════════════════════════════════
//  SETTINGS
// ═══════════════════════════════════════════════
function loadSettings() {
  google.script.run
    .withSuccessHandler(cfg => {
      el('cfgId').value   = cfg.sheetId   || '';
      el('cfgName').value = cfg.sheetName || '';
      el('cfgOrg').value  = cfg.orgName   || '';
      el('cfgLogo').value = cfg.orgLogo   || '';
      if (cfg.orgName) el('sbOrgName').textContent = cfg.orgName;
      if (cfg.sheetId) testConn(true);
    })
    .getConfig();
}

function saveSettings() {
  const cfg = {
    sheetId  : val('cfgId'),
    sheetName: val('cfgName'),
    orgName  : val('cfgOrg'),
    orgLogo  : val('cfgLogo'),
  };
  google.script.run
    .withSuccessHandler(r => {
      if (r.success) {
        toast(r.message,'success');
        if (cfg.orgName) el('sbOrgName').textContent = cfg.orgName;
        loadSettings();
      } else toast(r.message,'error');
    })
    .withFailureHandler(e => toast(e.message,'error'))
    .saveConfig(cfg);
}

function testConn(silent) {
  if (!silent) toast('Menguji koneksi…','info');
  google.script.run
    .withSuccessHandler(r => {
      const box = el('sheetInfoBox');
      if (r.success) {
        box.style.display = 'block';
        el('siName').textContent   = r.name;
        el('siUrl').href           = r.url;
        el('siSheets').textContent = r.sheets.join(', ');
        if (!silent) toast('Terhubung: ' + r.name,'success');
      } else {
        box.style.display = 'none';
        if (!silent) toast(r.message,'error');
      }
    })
    .withFailureHandler(e => { if(!silent) toast(e.message,'error'); })
    .getSpreadsheetInfo();
}

function initSht() {
  google.script.run
    .withSuccessHandler(r => { if(r.success) toast('Sheet berhasil diinisialisasi','success'); else toast(r.message,'error'); })
    .withFailureHandler(e => toast(e.message,'error'))
    .initSheet();
}

function showSpanel(name) {
  ['sheet','org','about'].forEach(p => el('sp-'+p).style.display = p===name?'block':'none');
  document.querySelectorAll('.snav-item').forEach((el,i)=>{
    el.classList.toggle('active', ['sheet','org','about'][i]===name);
  });
}

// ═══════════════════════════════════════════════
//  EXPORT
// ═══════════════════════════════════════════════
function doExportCsv() {
  toast('Mempersiapkan ekspor CSV…','info');
  google.script.run
    .withSuccessHandler(r => {
      if (!r.success) return toast(r.message,'error');
      const b = new Blob([r.csv], { type:'text/csv;charset=utf-8;' });
      const u = URL.createObjectURL(b);
      const a = document.createElement('a');
      a.href = u; a.download = 'arsip-surat-' + today() + '.csv'; a.click();
      URL.revokeObjectURL(u);
      toast('Ekspor berhasil (' + S.all.length + ' baris)','success');
    })
    .withFailureHandler(e => toast(e.message,'error'))
    .exportCsv();
}

// ═══════════════════════════════════════════════
//  OVERLAY / MODAL HELPERS
// ═══════════════════════════════════════════════
function openOv(id)  { el(id).classList.add('open'); }
function closeOv(id) { el(id).classList.remove('open'); }

document.querySelectorAll('.overlay').forEach(o => {
  o.addEventListener('click', e => { if (e.target === o) closeOv(o.id); });
});

document.addEventListener('keydown', e => {
  if (e.key === 'Escape') document.querySelectorAll('.overlay.open').forEach(o => closeOv(o.id));
});

// ═══════════════════════════════════════════════
//  TOAST
// ═══════════════════════════════════════════════
function toast(msg, type='info') {
  const icons = { success:'circle-check', error:'circle-xmark', warning:'triangle-exclamation', info:'circle-info' };
  const tw  = el('toastWrap');
  const div = document.createElement('div');
  div.className = `toast t-${type}`;
  div.innerHTML = `<i class="fa fa-${icons[type]||'circle-info'}"></i><span>${msg}</span><i class="fa fa-xmark tx" onclick="this.parentElement.remove()"></i>`;
  tw.appendChild(div);
  setTimeout(()=>{ div.style.transition='opacity .3s'; div.style.opacity='0'; }, 3500);
  setTimeout(()=>div.remove(), 3900);
}

// ═══════════════════════════════════════════════
//  BADGE HELPERS
// ═══════════════════════════════════════════════
function bJenis(j) {
  if (j==='Masuk')  return `<span class="badge bm"><i class="fa fa-inbox fa-xs"></i>Masuk</span>`;
  if (j==='Keluar') return `<span class="badge bk"><i class="fa fa-paper-plane fa-xs"></i>Keluar</span>`;
  return `<span class="badge bb">${j||'—'}</span>`;
}
function bStatus(s) {
  if (s==='Aktif')      return `<span class="badge ba">Aktif</span>`;
  if (s==='Diarsipkan') return `<span class="badge bar2">Diarsipkan</span>`;
  if (s==='Dihapus')    return `<span class="badge bh">Dihapus</span>`;
  return `<span class="badge bb">${s||'—'}</span>`;
}
function bKlasif(k) {
  if (k==='Rahasia'||k==='Sangat Rahasia') return `<span class="badge br">🔒${k}</span>`;
  if (k==='Penting') return `<span class="badge bp">⚠️${k}</span>`;
  return `<span class="badge bb">${k||'Biasa'}</span>`;
}

// ═══════════════════════════════════════════════
//  UTILS
// ═══════════════════════════════════════════════
function el(id)   { return document.getElementById(id); }
function set(id,v){ const e=el(id); if(e) e.textContent=v; }
function val(id)  { return (el(id)?.value||'').trim(); }
function x(s)     { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function today()  { return new Date().toISOString().slice(0,10); }
function emptyRow(cols, title, sub) {
  return `<tr><td colspan="${cols}">
    <div class="empty-state">
      <i class="fa fa-folder-open ei"></i>
      <h3>${title}</h3><p>${sub}</p>
    </div></td></tr>`;
}

// ═══════════════════════════════════════════════
//  RESPONSIVE: detect mobile
// ═══════════════════════════════════════════════
function checkMobile() {
  const mob = window.innerWidth < 769;
  el('mobMenuBtn').style.display = mob ? 'flex' : 'none';
}
window.addEventListener('resize', checkMobile);

// ═══════════════════════════════════════════════
//  INIT
// ═══════════════════════════════════════════════
document.addEventListener('DOMContentLoaded', () => {
  checkMobile();
  refreshAll();
});
</script>
</body>
</html>

********************************************* -->

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
</head>
<body>

  <h2>Aplikasi Arsip Surat</h2>

</body>
</html>

Deployment

Setelah seluruh file selesai dibuat:

  1. Klik Deploy
  2. Pilih New Deployment
  3. Pilih Web App
  4. Execute As: Me
  5. Who Has Access: Anyone
  6. Klik Deploy

Sekarang aplikasi Arsip Surat Anda siap digunakan.

Posting Komentar untuk "Membuat Aplikasi Arsip Surat Dengan Google Apps Script"