38691924
0.55 38
4.35 69
7.28 19
11.06 24
Code.gs
// ===================================================================
// 1. KONFIGURASI APLIKASI
// ===================================================================
// Ganti dengan ID Spreadsheet Anda
const SHEET_ID = 'UBAH DENGAN SPREADSHEET ID';
const SHEET_NAME = 'Pengaduan';
const SHEET_CONFIG = 'Kategori'; // Sheet untuk simpan Kategori & Password Admin
// Ganti dengan ID Folder Google Drive (Pastikan folder ini "Anyone with link" -> Viewer)
const FOLDER_ID = 'UBAH DENGAN FOLDER ID';
// Ganti dengan Email Admin Sekolah (Untuk menerima notifikasi laporan masuk)
const ADMIN_EMAIL = 'UBAH DENGAN EMAIL';
// ===================================================================
// 2. FUNGSI UTAMA (DO GET)
// ===================================================================
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('Sistem Pengaduan Masyarakat Sekolah')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag("viewport", "width=device-width, initial-scale=1.0");
}
// ===================================================================
// 3. FUNGSI DATABASE & HELPER
// ===================================================================
function setupInitialData() {
const ss = SpreadsheetApp.openById(SHEET_ID);
let sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
sheet = ss.insertSheet(SHEET_NAME);
// Header Kolom (Pastikan urutan ini jangan diubah)
sheet.appendRow([
'ID', 'Tanggal', 'Nama', 'Nomor HP', 'Kategori',
'Isi Pengaduan', 'Anonim', 'Status', 'Foto Bukti',
'Tanggapan Admin', 'Email Pelapor'
]);
sheet.getRange(1, 1, 1, 11).setFontWeight('bold');
sheet.setFrozenRows(1);
}
return sheet;
}
function getConfigSheet() {
const ss = SpreadsheetApp.openById(SHEET_ID);
let sheet = ss.getSheetByName(SHEET_CONFIG);
if (!sheet) {
sheet = ss.insertSheet(SHEET_CONFIG);
// Setup Default Config
sheet.getRange('A1').setValue('Daftar Kategori').setFontWeight('bold');
sheet.getRange('B1').setValue('Password Admin').setFontWeight('bold');
// Default Data
const defaultKategori = [['Sarana Prasarana'], ['Akademik'], ['Kesiswaan'], ['Layanan Publik'], ['Bullying/Kekerasan']];
sheet.getRange(2, 1, defaultKategori.length, 1).setValues(defaultKategori);
sheet.getRange('B2').setValue('admin123'); // Password Default
}
return sheet;
}
function generateId() {
// Format: ADU-TIMESTAMP-ACAK (Contoh: ADU-1736-XYZ)
const randomStr = Math.random().toString(36).substring(2, 5).toUpperCase();
const timestamp = Date.now().toString().substring(6, 10);
return 'ADU-' + timestamp + randomStr;
}
function formatTanggal(dateObj) {
if (!dateObj) return '-';
// Format: DD/MM/YYYY HH:mm
return Utilities.formatDate(new Date(dateObj), "Asia/Jakarta", "dd/MM/yyyy HH:mm");
}
function kirimEmail(to, subject, htmlBody) {
try {
MailApp.sendEmail({
to: to,
subject: subject,
htmlBody: htmlBody,
name: "Sistem Pengaduan Sekolah"
});
} catch (e) {
console.log("Gagal kirim email: " + e.toString());
}
}
// ===================================================================
// 4. FUNGSI PUBLIK (FORM & LIST)
// ===================================================================
function getDaftarKategori() {
const sheet = getConfigSheet();
const lastRow = sheet.getLastRow();
if (lastRow < 2) return [];
const data = sheet.getRange(2, 1, lastRow - 1, 1).getValues().flat();
return data.filter(String);
}
function simpanPengaduan(data) {
try {
// REVISI: Menggunakan setupInitialData() menggantikan getSheet()
const sheet = setupInitialData();
const id = generateId(); // Membuat ID Unik
const tanggal = new Date();
let fotoUrl = '-';
// ==========================================
// 1. PROSES UPLOAD FOTO KE GOOGLE DRIVE
// ==========================================
if (data.fileData && data.fileName && data.mimeType) {
try {
// Decode data gambar dari Base64
const decoded = Utilities.base64Decode(data.fileData);
const blob = Utilities.newBlob(decoded, data.mimeType, "Foto_" + id);
// Simpan ke Folder Drive
const folder = DriveApp.getFolderById(FOLDER_ID);
const file = folder.createFile(blob);
// Atur izin file agar bisa dilihat publik (Anyone with link -> Viewer)
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
// Menggunakan domain lh3.googleusercontent.com agar gambar bisa tampil di tag <img>
fotoUrl = "https://lh3.googleusercontent.com/d/" + file.getId();
} catch (e) {
return { success: false, message: 'Gagal upload gambar (Cek ID Folder): ' + e.toString() };
}
}
// ==========================================
// 2. SIMPAN DATA KE GOOGLE SHEET
// ==========================================
sheet.appendRow([
id, // Kolom A: ID
tanggal, // Kolom B: Tanggal
data.anonim === 'true' ? 'Anonim' : data.nama,// Kolom C: Nama
data.anonim === 'true' ? '-' : data.nomorHp, // Kolom D: No HP
data.kategori, // Kolom E: Kategori
data.isiPengaduan, // Kolom F: Isi
data.anonim, // Kolom G: Status Anonim
'Belum Diproses', // Kolom H: Status Awal
fotoUrl, // Kolom I: Link Foto
'', // Kolom J: Tanggapan (Kosong dulu)
data.email || '-' // Kolom K: Email Pelapor
]);
// ==========================================
// 3. KIRIM NOTIFIKASI EMAIL
// ==========================================
try {
// A. Kirim Notifikasi ke Admin Sekolah
const adminSubject = `[Laporan Baru] ${data.kategori} - ${id}`;
const adminBody = `
<h3>Laporan Baru Masuk</h3>
<p><b>ID Tiket:</b> ${id}</p>
<p><b>Kategori:</b> ${data.kategori}</p>
<p><b>Pelapor:</b> ${data.anonim === 'true' ? 'Anonim' : data.nama}</p>
<p><b>Isi Laporan:</b><br/>${data.isiPengaduan}</p>
<p><a href="${fotoUrl}" target="_blank">Lihat Bukti Foto</a></p>
<hr/>
<p><i>Silakan login ke dashboard admin untuk menindaklanjuti.</i></p>
`;
kirimEmail(ADMIN_EMAIL, adminSubject, adminBody);
// B. Kirim Auto-Reply ke Pelapor (Jika email diisi valid)
if (data.email && data.email.includes('@')) {
const pelaporSubject = `[Tanda Terima] Laporan Pengaduan ID: ${id}`;
const pelaporBody = `
<div style="font-family: Arial, sans-serif; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px; max-width: 600px;">
<h2 style="color: #1e40af; margin-top: 0;">Laporan Diterima</h2>
<p>Halo, laporan pengaduan Anda telah berhasil masuk ke sistem kami.</p>
<div style="background-color: #f3f4f6; padding: 15px; border-radius: 6px; margin: 20px 0; border-left: 4px solid #3b82f6;">
<p style="margin: 0; font-size: 0.9em; color: #4b5563;">ID Tiket Anda:</p>
<h1 style="margin: 5px 0; color: #111827; letter-spacing: 1px;">${id}</h1>
</div>
<table style="width: 100%; border-collapse: collapse; font-size: 0.9em; margin-bottom: 20px;">
<tr><td style="padding: 5px 0; color: #6b7280; width: 100px;">Tanggal</td><td>: ${formatTanggal(tanggal)}</td></tr>
<tr><td style="padding: 5px 0; color: #6b7280;">Kategori</td><td>: ${data.kategori}</td></tr>
</table>
<p style="font-size: 0.9em; color: #b45309; background-color: #fffbeb; padding: 10px; border-radius: 4px;">
<strong>PENTING:</strong> Simpan ID Tiket di atas. Anda dapat menggunakannya untuk melacak status/respon laporan melalui menu <b>Tracking</b> di website.
</p>
<hr style="border: 0; border-top: 1px solid #eee; margin: 20px 0;">
<p style="font-size: 0.8em; color: #9ca3af; text-align: center;">Email ini dikirim otomatis oleh Sistem Pengaduan Sekolah.</p>
</div>
`;
kirimEmail(data.email, pelaporSubject, pelaporBody);
}
} catch (emailError) {
console.log("Gagal kirim email: " + emailError.toString());
}
// ==========================================
// 4. KEMBALIKAN RESPONS SUKSES + ID
// ==========================================
return {
success: true,
message: 'Pengaduan berhasil disimpan!',
id: id
};
} catch (error) {
return { success: false, message: 'Error Server: ' + error.toString() };
}
}
function getPengaduanPublik() {
try {
// REVISI: Menggunakan setupInitialData() menggantikan getSheet()
const sheet = setupInitialData();
if (sheet.getLastRow() <= 1) return [];
const data = sheet.getDataRange().getValues();
const result = [];
// Loop dari baris 2 (Data)
for (let i = 1; i < data.length; i++) {
const row = data[i];
// Ambil hanya data yang valid
if(row[0] && row[5]) {
result.push({
id: row[0], // Untuk key
tanggal: formatTanggal(row[1]),
kategori: row[4],
ringkasan: row[5].length > 100 ? row[5].substring(0, 100) + '...' : row[5],
status: row[7],
fotoUrl: row[8],
tanggapan: row[9]
});
}
}
return result.reverse(); // Tampilkan yang terbaru di atas
} catch (e) {
return [];
}
}
// ===================================================================
// 5. FUNGSI TRACKING (Cek Status)
// ===================================================================
function cariStatusPengaduan(idTiket) {
try {
// REVISI: Menggunakan setupInitialData() menggantikan getSheet()
const sheet = setupInitialData();
const data = sheet.getDataRange().getValues();
const searchId = idTiket.toString().trim().toUpperCase();
for (let i = 1; i < data.length; i++) {
// Kolom 0 adalah ID
if (data[i][0].toString().trim().toUpperCase() === searchId) {
const row = data[i];
return {
success: true,
data: {
id: row[0],
tanggal: formatTanggal(row[1]),
nama: row[6] === 'true' ? 'Anonim' : row[2],
kategori: row[4],
isi: row[5],
status: row[7],
foto: row[8],
tanggapan: row[9] || 'Belum ada tanggapan.'
}
};
}
}
return { success: false, message: 'ID Tiket tidak ditemukan.' };
} catch (error) {
return { success: false, message: 'Error: ' + error.toString() };
}
}
// ===================================================================
// 6. FUNGSI ADMIN
// ===================================================================
function loginAdmin(password) {
const sheet = getConfigSheet();
const storedPass = sheet.getRange('B2').getValue().toString();
if (password === storedPass) {
return { success: true };
} else {
return { success: false, message: 'Password salah!' };
}
}
function getPengaduanAdmin() {
// REVISI: Menggunakan setupInitialData() menggantikan getSheet()
const sheet = setupInitialData();
if (sheet.getLastRow() <= 1) return [];
const data = sheet.getDataRange().getValues();
const result = [];
for (let i = 1; i < data.length; i++) {
const row = data[i];
result.push({
id: row[0],
tanggal: formatTanggal(row[1]),
nama: row[2],
nomorHp: row[3],
kategori: row[4],
isiPengaduan: row[5],
anonim: row[6],
status: row[7],
fotoUrl: row[8],
tanggapan: row[9],
email: row[10]
});
}
return result.reverse();
}
function updateStatus(id) {
// REVISI: Menggunakan setupInitialData() menggantikan getSheet()
const sheet = setupInitialData();
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === id) {
sheet.getRange(i + 1, 8).setValue('Sudah Diproses');
// Kirim Notifikasi Selesai ke Email Pelapor
const emailPelapor = data[i][10];
if (emailPelapor && emailPelapor.includes('@')) {
const body = `
<h3>Laporan Selesai</h3>
<p>Laporan Anda dengan ID <b>${id}</b> telah ditandai <b>SELESAI</b>.</p>
<p>Terima kasih telah membantu sekolah menjadi lebih baik.</p>
`;
kirimEmail(emailPelapor, `[Status Update] Tiket ${id} Selesai`, body);
}
return { success: true };
}
}
return { success: false, message: 'Data tidak ditemukan' };
}
function hapusPengaduan(id) {
// REVISI: Menggunakan setupInitialData() menggantikan getSheet()
const sheet = setupInitialData();
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === id) {
sheet.deleteRow(i + 1);
return { success: true };
}
}
return { success: false, message: 'Data tidak ditemukan' };
}
function simpanTanggapanAdmin(id, isiTanggapan) {
try {
// REVISI: Menggunakan setupInitialData() menggantikan getSheet()
const sheet = setupInitialData();
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === id) {
// Simpan Tanggapan di Kolom J (Index 9)
sheet.getRange(i + 1, 10).setValue(isiTanggapan);
// Otomatis ubah status jadi Sudah Diproses
sheet.getRange(i + 1, 8).setValue('Sudah Diproses');
// Kirim Email Balasan ke Pelapor
const emailPelapor = data[i][10];
if (emailPelapor && emailPelapor.includes('@')) {
const body = `
<h3>Tanggapan Admin Sekolah</h3>
<p>Laporan ID: <b>${id}</b></p>
<div style="background-color: #f0fdf4; padding: 15px; border-left: 5px solid #16a34a;">
<b>Respon:</b><br/>
"${isiTanggapan}"
</div>
`;
kirimEmail(emailPelapor, `[Tanggapan] Tiket ${id}`, body);
}
return { success: true };
}
}
return { success: false, message: 'Data tidak ditemukan' };
} catch (error) {
return { success: false, message: error.toString() };
}
}
// ===================================================================
// FITUR EKSPOR EXCEL
// ===================================================================
function generateExcelAdmin() {
// Kita set default type 'laporan_pengaduan'
return generateExcel('laporan_pengaduan', {});
}
function generateExcel(type, filters) {
try {
// 1. Membuat Spreadsheet baru sementara
var timestamp = Utilities.formatDate(new Date(), 'Asia/Jakarta', 'yyyyMMdd_HHmmss');
var fileName = "Export_" + type + "_" + timestamp;
var ss = SpreadsheetApp.create(fileName);
var sheet = ss.getActiveSheet();
// 2. Mengambil data
var data = getExportData(type, filters);
var headers = [];
// 3. Definisi Header Laporan Pengaduan
if (type === 'laporan_pengaduan') {
headers = [
"No",
"ID Tiket",
"Tanggal Masuk",
"Nama Pelapor",
"Nomor HP",
"Email",
"Kategori",
"Isi Pengaduan",
"Status",
"Tanggapan Sekolah"
];
}
// 4. Menulis Header
sheet.appendRow(headers);
// Styling Header (Kuning, Bold, Border)
var headerRange = sheet.getRange(1, 1, 1, headers.length);
headerRange.setFontWeight('bold')
.setBackground('#FFFF00')
.setBorder(true, true, true, true, true, true)
.setHorizontalAlignment('center')
.setVerticalAlignment('middle');
// 5. Menulis Data
if (data && data.length > 0) {
// Mulai tulis dari baris ke-2
sheet.getRange(2, 1, data.length, data[0].length).setValues(data);
// Auto Resize Kolom
sheet.autoResizeColumns(1, headers.length);
}
// 6. Atur Izin File (Agar bisa didownload script)
var fileId = ss.getId();
var file = DriveApp.getFileById(fileId);
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
// 7. Generate Link Download .xlsx
var downloadUrl = "https://docs.google.com/spreadsheets/d/" + fileId + "/export?format=xlsx";
return {
success: true,
url: downloadUrl,
filename: fileName + ".xlsx"
};
} catch (e) {
return {
success: false,
message: 'Gagal ekspor: ' + e.toString()
};
}
}
function getExportData(type, filters) {
var result = [];
if (type === 'laporan_pengaduan') {
// REVISI: Menggunakan setupInitialData() menggantikan getSheet()
var sheet = setupInitialData();
var lastRow = sheet.getLastRow();
if (lastRow > 1) {
var data = sheet.getRange(2, 1, lastRow - 1, 11).getValues();
// Loop data untuk diformat
for (var i = 0; i < data.length; i++) {
var row = data[i];
var no = i + 1;
var id = row[0];
var tanggal = formatTanggal(row[1]);
var nama = row[6] === 'true' ? 'Anonim' : row[2];
var hp = row[6] === 'true' ? '-' : row[3];
var email = row[10] || '-';
var kategori = row[4];
var isi = row[5];
var status = row[7];
var tanggapan = row[9] || '-';
// Push ke array hasil (Urutan harus sama dengan headers di generateExcel)
result.push([
no,
id,
tanggal,
nama,
"'" + hp, // Tambah kutip satu agar HP tidak jadi angka ilmiah di Excel
email,
kategori,
isi,
status,
tanggapan
]);
}
}
}
return result;
}
// ===================================================================
// FUNGSI KHUSUS: MEMPERBAIKI LINK FOTO YANG RUSAK (EXPIRED)
// ===================================================================
function perbaikiLinkLama() {
// REVISI: Menggunakan setupInitialData() menggantikan getSheet()
const sheet = setupInitialData();
const data = sheet.getDataRange().getValues();
// Loop dari baris ke-2 (Data)
for (let i = 1; i < data.length; i++) {
const urlLama = data[i][8]; // Kolom I (Index 8) adalah Foto Bukti
// Cek apakah url berisi format lama "uc?export=view"
if (urlLama && urlLama.includes("drive.google.com/uc?export=view")) {
try {
const idFile = urlLama.split('id=')[1];
if (idFile) {
const urlBaru = "https://lh3.googleusercontent.com/d/" + idFile;
sheet.getRange(i + 1, 9).setValue(urlBaru);
console.log(`Baris ${i + 1} diperbarui: ${urlBaru}`);
}
} catch (e) {
console.log(`Gagal update baris ${i + 1}: ${e.toString()}`);
}
}
}
}
Index.html
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sistem Pengaduan Masyarakat Sekolah</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
/* Menggunakan style yang sama dengan sebelumnya */
* { font-family: 'Inter', sans-serif; box-sizing: border-box; }
body { background-color: #f3f4f6; color: #1f2937; }
#landingPage {
background: linear-gradient(rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0.85)),
url('https://alazharasysyarifsumut.sch.id/wp-content/uploads/2022/09/Gedung-Kelas-Ikhwan-scaled.jpg');
background-size: cover; background-position: center; background-attachment: fixed; min-height: 100vh;
}
.pro-card { background-color: #ffffff; border: 1px solid #cbd5e1; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); }
.input-wrapper { position: relative; width: 100%; }
.input-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); width: 20px; height: 20px; color: #9ca3af; pointer-events: none; }
.input-wrapper.textarea-wrapper .input-icon { top: 20px; transform: none; }
.input-with-icon { padding-left: 46px !important; }
.input-wrapper:focus-within .input-icon { color: #2563eb; }
/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #a8a8a8; }
/* Animations */
.transition-all { transition: all 0.3s ease; }
@keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
.notification { animation: slideInRight 0.3s ease; }
/* Loading */
.loading { border: 3px solid #e5e7eb; border-top: 3px solid #2563eb; border-radius: 50%; width: 24px; height: 24px; animation: spin 1s linear infinite; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* Badges & Buttons */
.badge-pending { background-color: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
.badge-done { background-color: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
.btn-primary { background-color: #2563eb; color: white; }
.btn-primary:hover { background-color: #1d4ed8; }
.btn-success { background-color: #059669; color: white; }
.btn-success:hover { background-color: #047857; }
.btn-danger { background-color: #dc2626; color: white; }
.btn-danger:hover { background-color: #b91c1c; }
.sidebar-scroll::-webkit-scrollbar { display: none; }
.sidebar-scroll { -ms-overflow-style: none; scrollbar-width: none; }
</style>
</head>
<body class="bg-gray-50 text-gray-800 font-sans antialiased selection:bg-blue-100 selection:text-blue-900">
<div id="landingPage">
<nav class="shadow-md fixed top-0 left-0 right-0 z-50 transition-all" style="background-color: rgb(26, 25, 103);">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center">
<div class="flex-shrink-0 flex items-center gap-3">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Logo_of_Ministry_of_Education_and_Culture_of_Republic_of_Indonesia.svg/2048px-Logo_of_Ministry_of_Education_and_Culture_of_Republic_of_Indonesia.svg.png"
alt="Logo Sekolah"
class="h-10 w-10 object-contain bg-white rounded-full p-1 shadow-sm">
<div class="flex flex-col">
<h1 class="text-white text-lg md:text-xl font-bold tracking-wide leading-tight">SMPN 1001 Bengkulu</h1>
<span class="text-blue-200 text-xs font-medium tracking-wide">Sistem Pengaduan Masyarakat</span>
</div>
</div>
</div>
<div>
<button onclick="showLoginModal()" class="text-white hover:bg-white hover:bg-opacity-20 px-4 py-2 rounded-lg transition-all font-medium border border-transparent hover:border-white/30 text-sm md:text-base">
🔐 Login Admin
</button>
</div>
</div>
</div>
</nav>
<div class="pt-24 pb-16 px-4">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-10">
<h2 class="text-4xl md:text-5xl font-bold text-gray-900 mb-4">Sistem Pengaduan Masyarakat</h2>
<p class="text-gray-600 text-lg max-w-2xl mx-auto font-medium">
Sampaikan aspirasi dan keluhan Anda untuk kemajuan bersama.
</p>
<div class="mt-8 max-w-xl mx-auto px-4">
<div class="bg-white p-2 rounded-full shadow-lg border border-blue-100 flex items-center hover:shadow-xl transition-shadow">
<div class="pl-4 text-gray-400">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</div>
<input type="text" id="inputTracking"
class="w-full px-4 py-3 bg-transparent border-none focus:outline-none focus:ring-0 text-gray-700 placeholder-gray-400"
placeholder="Masukkan ID Pengaduan (Contoh: ADU-1736...)"
onkeypress="handleEnterTracking(event)">
<button onclick="cekStatus()" id="btnTracking"
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-full font-bold transition-all shadow-md flex items-center gap-2 flex-shrink-0">
<span>Lacak</span>
<div id="loadingTracking" class="hidden w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
</button>
</div>
<p class="text-xs text-gray-500 mt-2">Simpan ID Pengaduan Anda setelah mengirim laporan untuk mengecek status.</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="pro-card rounded-xl p-6 md:p-8 bg-white/95 backdrop-blur-sm border-t-4 shadow-xl" style="border-top-color: rgb(26, 25, 103);">
<h3 class="text-2xl font-bold text-gray-800 mb-6 border-b pb-4">📝 Form Pengaduan</h3>
<form id="formPengaduan" onsubmit="submitPengaduan(event)">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-semibold mb-2">Nama Lengkap</label>
<div class="input-wrapper group">
<svg class="input-icon group-hover:text-blue-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
<input type="text" id="nama" required class="input-with-icon w-full px-4 py-3 rounded-lg bg-white text-gray-900 border border-gray-300 focus:ring-2 focus:ring-blue-500 transition-all" placeholder="Masukkan nama lengkap">
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-semibold mb-2">Nomor HP (WhatsApp)</label>
<div class="input-wrapper group">
<svg class="input-icon group-hover:text-blue-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path></svg>
<input type="tel" id="nomorHp" required class="input-with-icon w-full px-4 py-3 rounded-lg bg-white text-gray-900 border border-gray-300 focus:ring-2 focus:ring-blue-500 transition-all" placeholder="081234567890">
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-semibold mb-2">Email (Opsional)</label>
<div class="input-wrapper group">
<svg class="input-icon group-hover:text-blue-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
<input type="email" id="emailPelapor" class="input-with-icon w-full px-4 py-3 rounded-lg bg-white text-gray-900 border border-gray-300 focus:ring-2 focus:ring-blue-500 transition-all" placeholder="email@contoh.com (Untuk notifikasi balasan)">
</div>
<p class="text-xs text-gray-500 mt-1 ml-1 flex items-center gap-1">
<svg class="w-3 h-3 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Isi email agar mendapat salinan ID Tiket & notifikasi balasan.
</p>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-semibold mb-2">Kategori</label>
<div class="input-wrapper group">
<svg class="input-icon group-hover:text-blue-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path></svg>
<select id="kategori" required class="input-with-icon w-full px-4 py-3 rounded-lg bg-white text-gray-900 border border-gray-300 focus:ring-2 focus:ring-blue-500 transition-all">
<option value="" class="text-gray-500">Pilih Kategori</option>
</select>
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-semibold mb-2">Isi Pengaduan</label>
<div class="input-wrapper textarea-wrapper group">
<svg class="input-icon group-hover:text-blue-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path></svg>
<textarea id="isiPengaduan" required rows="5" class="input-with-icon w-full px-4 py-3 rounded-lg bg-white text-gray-900 border border-gray-300 focus:ring-2 focus:ring-blue-500 transition-all" placeholder="Jelaskan detail pengaduan Anda..."></textarea>
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-semibold mb-2">Foto Bukti (Opsional)</label>
<div class="input-wrapper group">
<svg class="input-icon group-hover:text-blue-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
<input type="file" id="fotoBukti" accept="image/*" class="input-with-icon w-full px-4 py-3 rounded-lg bg-white border border-gray-300 text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 transition-all">
</div>
</div>
<div class="mb-6">
<label class="flex items-center cursor-pointer select-none">
<input type="checkbox" id="anonim" class="w-5 h-5 rounded text-blue-600 border-gray-300 focus:ring-blue-500">
<span class="ml-3 text-gray-600 text-sm">Kirim sebagai Anonim (Identitas dirahasiakan di publik)</span>
</label>
</div>
<button type="submit" class="w-full btn-primary font-bold py-3.5 px-6 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all flex justify-center items-center">
<span id="btnSubmitText">Kirim Pengaduan</span>
<span id="btnSubmitLoading" class="hidden"><div class="loading inline-block" style="border-top-color: white; border-right-color: rgba(255,255,255,0.2);"></div></span>
</button>
</form>
</div>
<div class="pro-card rounded-xl p-6 md:p-8 flex flex-col h-full bg-white/95 backdrop-blur-sm border-t-4 shadow-xl" style="border-top-color: rgb(26, 25, 103);">
<div class="flex flex-wrap items-center justify-between mb-6 border-b pb-4 gap-2">
<h3 class="text-2xl font-bold text-gray-800">📊 Pengaduan Terkini</h3>
<div class="flex items-center gap-2">
<select id="publicLimit" onchange="renderPublicList()" class="bg-gray-50 border border-gray-300 text-gray-700 text-xs rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="all">Semua</option>
</select>
<button onclick="loadPengaduanPublik()" class="text-blue-600 hover:bg-blue-50 p-2 rounded-lg transition-all" title="Refresh Data">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
</button>
</div>
</div>
<div id="listPengaduanPublik" class="space-y-4 max-h-[700px] overflow-y-auto sidebar-scroll pr-2">
</div>
</div>
</div>
</div>
</div>
</div>
<div id="loginModal" class="hidden fixed inset-0 bg-gray-900 bg-opacity-60 z-50 flex items-center justify-center p-4 backdrop-blur-sm transition-opacity">
<div class="bg-white rounded-xl shadow-2xl p-8 max-w-md w-full border border-gray-200 transform scale-100 transition-transform">
<h3 class="text-2xl font-bold text-gray-900 mb-6 text-center">🔐 Login Admin</h3>
<form onsubmit="loginAdmin(event)">
<div class="mb-6">
<label class="block text-gray-700 text-sm font-semibold mb-2">Password</label>
<div class="input-wrapper group">
<svg class="input-icon group-hover:text-blue-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
<input type="password" id="adminPassword" required class="input-with-icon w-full px-4 py-3 rounded-lg bg-gray-50 text-gray-900 border border-gray-300 focus:ring-2 focus:ring-blue-500 transition-all" placeholder="Masukkan password admin">
</div>
</div>
<div class="flex gap-3">
<button type="submit" id="btnLoginAdmin" class="flex-1 btn-primary font-bold py-3 px-6 rounded-lg flex justify-center items-center gap-2 transition-all disabled:opacity-70 disabled:cursor-not-allowed">
<span id="btnLoginText">Login</span>
<span id="btnLoginLoading" class="hidden">
<div class="loading inline-block" style="width: 20px; height: 20px; border-width: 2px; border-top-color: white; border-right-color: rgba(255,255,255,0.2);"></div>
</span>
</button>
<button type="button" onclick="hideLoginModal()" class="flex-1 bg-gray-100 text-gray-700 font-bold py-3 px-6 rounded-lg hover:bg-gray-200 transition-colors">Batal</button>
</div>
</form>
</div>
</div>
<div id="adminPage" class="hidden bg-gray-100 min-h-screen font-sans">
<div id="sidebar" class="bg-[rgb(26,25,103)] text-gray-300 fixed top-0 bottom-0 left-0 w-64 z-50 transition-all duration-300 flex flex-col font-inter shadow-2xl">
<div class="h-16 flex items-center px-6 bg-black/20 border-b border-white/10 shrink-0">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Logo_of_Ministry_of_Education_and_Culture_of_Republic_of_Indonesia.svg/2048px-Logo_of_Ministry_of_Education_and_Culture_of_Republic_of_Indonesia.svg.png"
alt="Logo Sekolah"
class="h-8 w-8 mr-3 object-contain bg-white rounded-full p-0.5">
<span class="text-white font-bold text-lg tracking-wider">SEKOLAH</span>
</div>
<div class="p-6 border-b border-white/10 shrink-0">
<div class="flex items-center gap-4">
<div class="relative">
<img src="https://ui-avatars.com/api/?name=Admin&background=3b82f6&color=fff&size=128"
alt="Admin"
class="w-12 h-12 rounded-full border-2 border-gray-400 p-0.5">
<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-[rgb(26,25,103)] rounded-full"></div>
</div>
<div>
<h4 class="text-white font-bold text-sm">Administrator</h4>
<div class="flex items-center mt-1">
<div class="w-2 h-2 bg-green-500 rounded-full mr-1.5 animate-pulse"></div>
<span class="text-xs text-blue-200 font-medium">Online</span>
</div>
</div>
</div>
</div>
<div class="flex-1 overflow-y-auto py-6 px-3 space-y-1 sidebar-scroll">
<p class="px-3 text-xs font-bold text-blue-300/70 uppercase tracking-wider mb-2">Menu Utama</p>
<button onclick="showDashboard()" id="menuDashboard" class="w-full flex items-center gap-3 px-3 py-3 rounded-lg text-gray-300 hover:bg-white/10 hover:text-white transition-all group">
<span class="p-1.5 bg-white/10 rounded-md group-hover:bg-blue-500 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
</span>
<span class="font-medium text-sm">Dashboard</span>
</button>
<button onclick="showDataPengaduan()" id="menuPengaduan" class="w-full flex items-center gap-3 px-3 py-3 rounded-lg text-gray-300 hover:bg-white/10 hover:text-white transition-all group">
<span class="p-1.5 bg-white/10 rounded-md group-hover:bg-blue-500 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path></svg>
</span>
<span class="font-medium text-sm">Data Pengaduan</span>
</button>
</div>
<div class="p-4 bg-black/20 border-t border-white/10 shrink-0">
<button onclick="logout()" class="w-full flex items-center justify-center gap-2 bg-red-600 hover:bg-red-700 text-white py-2.5 rounded-lg transition-all shadow-lg shadow-red-900/20 group">
<svg class="w-5 h-5 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
<span class="font-bold text-sm">Logout System</span>
</button>
</div>
</div>
<div id="adminContent" class="ml-64 transition-all duration-300 min-h-screen flex flex-col">
<nav class="bg-white h-16 shadow-sm border-b border-gray-200 flex items-center justify-between px-6 sticky top-0 z-40">
<div class="flex items-center gap-4">
<button onclick="toggleSidebar()" class="text-gray-500 hover:text-blue-600 hover:bg-gray-100 p-2 rounded-lg transition-all focus:outline-none">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7"/>
</svg>
</button>
<h1 class="text-gray-800 text-lg font-bold tracking-wide">ADMIN PANEL</h1>
</div>
<div class="hidden md:flex items-center gap-3">
<span class="text-sm text-gray-500 font-medium">Sistem Pengaduan v2.0</span>
<div class="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center text-gray-500 ring-2 ring-gray-100">
🔔
</div>
</div>
</nav>
<div class="p-6">
<div id="dashboardSection">
<div class="w-full">
<div class="mb-8 flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold text-gray-800">Dashboard Ringkasan</h2>
<p class="text-gray-500 text-sm mt-1">Pantau statistik pengaduan masuk secara realtime.</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800 p-6 text-white shadow-lg transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl group">
<div class="absolute -top-10 -right-10 h-32 w-32 rounded-full bg-white opacity-10 blur-2xl transition-all group-hover:opacity-20"></div>
<div class="relative z-10 flex items-start justify-between">
<div>
<p class="mb-1 text-xs font-bold uppercase tracking-widest text-blue-200">Total Pengaduan</p>
<h3 class="text-5xl font-extrabold tracking-tight text-white" id="totalPengaduan">0</h3>
<div class="mt-4 flex items-center gap-2">
<span class="rounded-full bg-white/20 px-2 py-0.5 text-[10px] font-medium text-white backdrop-blur-sm">Laporan Masuk</span>
</div>
</div>
<div class="rounded-xl bg-white/10 p-3 text-white backdrop-blur-sm shadow-inner ring-1 ring-white/20">
<span class="text-3xl">📝</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-orange-400 via-orange-500 to-red-500 p-6 text-white shadow-lg transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl group">
<div class="absolute -top-10 -right-10 h-32 w-32 rounded-full bg-white opacity-10 blur-2xl transition-all group-hover:opacity-20"></div>
<div class="relative z-10 flex items-start justify-between">
<div>
<p class="mb-1 text-xs font-bold uppercase tracking-widest text-orange-100">Belum Diproses</p>
<h3 class="text-5xl font-extrabold tracking-tight text-white" id="belumDiproses">0</h3>
<div class="mt-4 flex items-center gap-2">
<span class="rounded-full bg-white/20 px-2 py-0.5 text-[10px] font-medium text-white backdrop-blur-sm">Perlu Tindakan</span>
</div>
</div>
<div class="rounded-xl bg-white/10 p-3 text-white backdrop-blur-sm shadow-inner ring-1 ring-white/20">
<span class="text-3xl">⏳</span>
</div>
</div>
</div>
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-emerald-400 via-emerald-600 to-teal-700 p-6 text-white shadow-lg transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl group">
<div class="absolute -top-10 -right-10 h-32 w-32 rounded-full bg-white opacity-10 blur-2xl transition-all group-hover:opacity-20"></div>
<div class="relative z-10 flex items-start justify-between">
<div>
<p class="mb-1 text-xs font-bold uppercase tracking-widest text-emerald-100">Sudah Selesai</p>
<h3 class="text-5xl font-extrabold tracking-tight text-white" id="sudahDiproses">0</h3>
<div class="mt-4 flex items-center gap-2">
<span class="rounded-full bg-white/20 px-2 py-0.5 text-[10px] font-medium text-white backdrop-blur-sm">Terselesaikan</span>
</div>
</div>
<div class="rounded-xl bg-white/10 p-3 text-white backdrop-blur-sm shadow-inner ring-1 ring-white/20">
<span class="text-3xl">✅</span>
</div>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-100 mb-8">
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="text-lg font-bold text-gray-800">Statistik Berdasarkan Kategori</h3>
<p class="text-sm text-gray-500">Jumlah pengaduan yang masuk per kategori.</p>
</div>
</div>
<div class="relative h-80">
<canvas id="categoryChart"></canvas>
</div>
</div>
</div>
</div>
<div id="dataPengaduanSection" class="hidden">
<div class="w-full">
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="p-6 border-b border-gray-200">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
<div>
<h2 class="text-xl font-bold text-gray-800">Data Pengaduan Masuk</h2>
<p class="text-gray-500 text-xs">Kelola dan pantau semua laporan dari sini.</p>
</div>
<div class="flex gap-2">
<button onclick="downloadExcel()" id="btnExportExcel" class="bg-green-600 text-white hover:bg-green-700 px-4 py-2 rounded-lg transition-all text-sm font-bold flex items-center gap-2 shadow-sm">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
<span>Ekspor Excel</span>
<div id="loadingExcel" class="hidden w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
</button>
<button onclick="loadPengaduanAdmin()" class="bg-blue-50 text-blue-600 hover:bg-blue-100 px-4 py-2 rounded-lg transition-all text-sm font-bold flex items-center gap-2">
<span>🔄</span> Refresh Data
</button>
</div>
</div>
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
<div class="flex items-center gap-2 text-sm text-gray-600 w-full md:w-auto">
<span>Tampilkan</span>
<select id="itemsPerPage" onchange="changeItemsPerPage()" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="all">Semua</option>
</select>
<span>data</span>
</div>
<div class="flex flex-col md:flex-row gap-3 w-full md:w-auto items-center">
<select id="filterStatus" onchange="handleSearch()" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full md:w-48 p-2.5">
<option value="">Semua Status</option>
<option value="Belum Diproses">⏳ Belum Diproses</option>
<option value="Sudah Diproses">✅ Sudah Diproses</option>
</select>
<div class="relative w-full md:w-64">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/></svg>
</div>
<input type="text" id="searchInput" onkeyup="handleSearch()" class="block w-full p-2.5 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500" placeholder="Cari data...">
</div>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full" id="tablePengaduan">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Tanggal</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Nama</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Nomor HP</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Kategori</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Isi Pengaduan</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Foto</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Tanggapan</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Aksi</th>
</tr>
</thead>
<tbody id="tbodyPengaduan" class="text-gray-700 divide-y divide-gray-200 bg-white"></tbody>
</table>
</div>
<div class="bg-gray-50 px-6 py-4 border-t border-gray-200 flex flex-col md:flex-row items-center justify-between gap-4">
<span class="text-sm text-gray-700" id="paginationInfo">
Menampilkan <span class="font-semibold text-gray-900" id="startRange">0</span> - <span class="font-semibold text-gray-900" id="endRange">0</span> dari <span class="font-semibold text-gray-900" id="totalData">0</span> data
</span>
<div class="inline-flex mt-2 xs:mt-0">
<button onclick="prevPage()" id="btnPrev" class="flex items-center justify-center px-4 h-10 text-base font-medium text-white bg-blue-600 rounded-l hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed">Prev</button>
<button onclick="nextPage()" id="btnNext" class="flex items-center justify-center px-4 h-10 text-base font-medium text-white bg-blue-600 border-0 border-l border-blue-700 rounded-r hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed">Next</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="imageModal" class="hidden fixed inset-0 z-[60] bg-black bg-opacity-90 flex items-center justify-center p-4 backdrop-blur-sm transition-opacity duration-300" onclick="closeImageModal()">
<button class="absolute top-4 right-4 text-white hover:text-gray-300 transition-colors bg-black bg-opacity-50 rounded-full p-2">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
<div class="relative w-full max-w-5xl h-full flex items-center justify-center pointer-events-none">
<img id="modalImageFull" src="" alt="Preview" class="max-w-full max-h-[90vh] object-contain rounded-lg shadow-2xl pointer-events-auto transition-transform duration-300 transform scale-95 opacity-0">
</div>
</div>
<div id="modalTracking" class="hidden fixed inset-0 bg-gray-900 bg-opacity-60 z-50 flex items-center justify-center p-4 backdrop-blur-sm transition-opacity">
<div class="bg-white rounded-2xl shadow-2xl max-w-lg w-full overflow-hidden border border-gray-200 transform scale-100 transition-transform">
<div class="bg-[rgb(26,25,103)] px-6 py-4 flex items-center justify-between">
<h3 class="text-white font-bold text-lg flex items-center gap-2">
<span>🔍</span> Detail Pengaduan
</h3>
<button onclick="closeTrackingModal()" class="text-white/70 hover:text-white transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<div class="p-6 overflow-y-auto max-h-[70vh]">
<div class="flex justify-center mb-6">
<span id="trackStatusBadge" class="px-6 py-2 rounded-full text-sm font-extrabold uppercase tracking-widest shadow-sm border">
STATUS
</span>
</div>
<div class="space-y-4">
<div class="flex justify-between border-b border-gray-100 pb-3">
<div>
<p class="text-xs text-gray-500 uppercase font-bold">ID Tiket</p>
<p class="text-gray-800 font-mono font-bold" id="trackId">-</p>
</div>
<div class="text-right">
<p class="text-xs text-gray-500 uppercase font-bold">Tanggal</p>
<p class="text-gray-800 font-medium text-sm" id="trackTanggal">-</p>
</div>
</div>
<div>
<p class="text-xs text-gray-500 uppercase font-bold mb-1">Pengirim</p>
<p class="text-gray-800 font-medium" id="trackNama">-</p>
</div>
<div>
<p class="text-xs text-gray-500 uppercase font-bold mb-1">Isi Laporan</p>
<div class="bg-gray-50 p-3 rounded-lg border border-gray-100 text-gray-700 text-sm leading-relaxed" id="trackIsi">
-
</div>
</div>
<div class="bg-blue-50 rounded-xl p-4 border border-blue-100">
<div class="flex items-center gap-2 mb-2">
<span class="text-lg">💬</span>
<p class="text-blue-900 font-bold text-sm">Tanggapan Sekolah</p>
</div>
<p class="text-gray-700 text-sm italic" id="trackTanggapan">
Belum ada tanggapan.
</p>
</div>
<div id="trackFotoContainer" class="hidden pt-2">
<button onclick="lihatFotoTracking()" class="w-full py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-sm font-bold transition-colors flex items-center justify-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
Lihat Bukti Foto
</button>
<input type="hidden" id="trackFotoUrl">
</div>
</div>
</div>
<div class="bg-gray-50 px-6 py-4 text-center">
<button onclick="closeTrackingModal()" class="text-blue-600 font-bold text-sm hover:underline">Tutup Jendela</button>
</div>
</div>
</div>
<div id="notificationContainer" class="fixed top-24 right-4 z-50 space-y-2"></div>
<script>
// ==========================================
// 1. STATE & KONFIGURASI GLOBAL
// ==========================================
let sidebarOpen = true;
let isLoggedIn = false;
// Variabel untuk Admin Table (Filter & Pagination)
let allPengaduanData = []; // Master data dari server
let filteredData = []; // Data setelah difilter
let currentPage = 1; // Halaman aktif
let itemsPerPage = 10; // Baris per halaman
// Variabel untuk Publik & Chart
let allPublicData = [];
let categoryChartInstance = null;
// ==========================================
// 2. INISIALISASI SAAT HALAMAN DIMUAT
// ==========================================
document.addEventListener('DOMContentLoaded', function() {
// 1. Load Opsi Kategori untuk Form
loadKategoriOptions();
// 2. Cek Sesi Login di LocalStorage
const session = localStorage.getItem('adminSession');
if (session === 'active') {
// Jika sesi aktif, masuk mode Admin
isLoggedIn = true;
document.getElementById('landingPage').classList.add('hidden');
document.getElementById('adminPage').classList.remove('hidden');
showDashboard();
} else {
// Jika tidak, tampilkan halaman Publik
loadPengaduanPublik();
}
// 3. Listener Resize Layar
handleResize();
});
// ==========================================
// 3. LOGIKA FORMULIR PUBLIK (KIRIM LAPORAN)
// ==========================================
function loadKategoriOptions() {
const select = document.getElementById('kategori');
const loadingOpt = document.createElement('option');
loadingOpt.text = "Memuat kategori...";
select.add(loadingOpt);
google.script.run
.withSuccessHandler(function(data) {
select.innerHTML = '<option value="" class="text-gray-500">Pilih Kategori</option>';
if (!data || data.length === 0) {
const opt = document.createElement('option');
opt.text = "Umum"; opt.value = "Umum";
select.appendChild(opt);
} else {
data.forEach(item => {
const opt = document.createElement('option');
opt.value = item; opt.textContent = item;
select.appendChild(opt);
});
}
})
.withFailureHandler(function(error) {
console.error("Gagal kategori:", error);
select.innerHTML = '<option value="">Gagal memuat data</option>';
})
.getDaftarKategori();
}
function submitPengaduan(event) {
event.preventDefault();
// UI Loading
document.getElementById('btnSubmitText').classList.add('hidden');
document.getElementById('btnSubmitLoading').classList.remove('hidden');
const fileInput = document.getElementById('fotoBukti');
const file = fileInput.files[0];
// Ambil Data Form (Termasuk Email)
const data = {
nama: document.getElementById('nama').value,
nomorHp: "'" + document.getElementById('nomorHp').value,
email: document.getElementById('emailPelapor').value, // [PENTING] Ambil Email
kategori: document.getElementById('kategori').value,
isiPengaduan: document.getElementById('isiPengaduan').value,
anonim: document.getElementById('anonim').checked.toString()
};
if (file) {
// Validasi File (Maks 5MB)
if (file.size > 5 * 1024 * 1024) {
showNotification('Ukuran file terlalu besar (Maks 5MB)', 'error');
resetBtnLoading();
return;
}
const reader = new FileReader();
reader.onload = function(e) {
data.fileData = e.target.result.split(',')[1];
data.mimeType = file.type;
data.fileName = file.name;
kirimKeServer(data);
};
reader.readAsDataURL(file);
} else {
kirimKeServer(data);
}
}
function kirimKeServer(data) {
google.script.run
.withSuccessHandler(function(result) {
resetBtnLoading();
if (result.success) {
// [PENTING] Tampilkan ID Tiket ke User
Swal.fire({
title: 'Laporan Diterima!',
html: `
<p>Terima kasih, laporan Anda telah kami terima.</p>
<div style="margin: 20px 0; padding: 15px; background: #f3f4f6; border: 2px dashed #3b82f6; border-radius: 10px;">
<p style="font-size: 0.9em; color: #6b7280; margin-bottom: 5px;">ID Tiket Anda:</p>
<h2 style="font-size: 1.8em; font-weight: 800; color: #1e40af; letter-spacing: 2px; margin: 0;">${result.id}</h2>
</div>
<p style="font-size: 0.9em; color: #ef4444;">*Harap catat ID ini untuk mengecek status laporan.</p>
<p style="font-size: 0.8em; color: #9ca3af; margin-top: 10px;">(Salinan bukti juga dikirim ke email jika Anda mengisinya)</p>
`,
icon: 'success',
confirmButtonText: 'Siap, Sudah Dicatat!',
confirmButtonColor: '#2563eb'
});
// Reset Form
document.getElementById('formPengaduan').reset();
// Refresh Data Publik
if(!isLoggedIn) loadPengaduanPublik();
} else {
showNotification(result.message, 'error');
}
})
.withFailureHandler(function(error) {
resetBtnLoading();
showNotification('Gagal mengirim: ' + error.message, 'error');
})
.simpanPengaduan(data);
}
function resetBtnLoading() {
document.getElementById('btnSubmitText').classList.remove('hidden');
document.getElementById('btnSubmitLoading').classList.add('hidden');
}
// ==========================================
// 4. LOGIKA TRACKING (USER CEK STATUS)
// ==========================================
function handleEnterTracking(event) {
if (event.key === "Enter") {
cekStatus();
}
}
function cekStatus() {
const input = document.getElementById('inputTracking');
const idTiket = input.value.trim();
const btn = document.getElementById('btnTracking');
const loading = document.getElementById('loadingTracking');
if (!idTiket) {
Swal.fire('Input Kosong', 'Mohon masukkan ID Pengaduan Anda.', 'warning');
return;
}
// UI Loading State
btn.disabled = true;
btn.querySelector('span').textContent = 'Mencari...';
loading.classList.remove('hidden');
google.script.run
.withSuccessHandler(function(result) {
// Reset UI
btn.disabled = false;
btn.querySelector('span').textContent = 'Lacak';
loading.classList.add('hidden');
if (result.success) {
tampilkanModalTracking(result.data);
} else {
Swal.fire('Tidak Ditemukan', result.message, 'error');
}
})
.withFailureHandler(function(error) {
btn.disabled = false;
btn.querySelector('span').textContent = 'Lacak';
loading.classList.add('hidden');
Swal.fire('Error', 'Gagal koneksi ke server: ' + error.message, 'error');
})
.cariStatusPengaduan(idTiket);
}
function tampilkanModalTracking(data) {
// Isi Data ke Modal
document.getElementById('trackId').textContent = data.id;
document.getElementById('trackTanggal').textContent = data.tanggal;
document.getElementById('trackNama').textContent = data.nama;
document.getElementById('trackIsi').textContent = data.isi;
document.getElementById('trackTanggapan').textContent = data.tanggapan;
// Atur Badge Status
const badge = document.getElementById('trackStatusBadge');
if (data.status === 'Sudah Diproses') {
badge.className = 'px-6 py-2 rounded-full text-sm font-extrabold uppercase tracking-widest shadow-sm border bg-green-100 text-green-700 border-green-200';
badge.textContent = '✅ SELESAI';
} else {
badge.className = 'px-6 py-2 rounded-full text-sm font-extrabold uppercase tracking-widest shadow-sm border bg-yellow-100 text-yellow-700 border-yellow-200';
badge.textContent = '⏳ DIPROSES';
}
// Atur Foto
const btnFoto = document.getElementById('trackFotoContainer');
const urlFoto = document.getElementById('trackFotoUrl');
if (data.foto && data.foto !== '-' && data.foto !== '') {
btnFoto.classList.remove('hidden');
urlFoto.value = data.foto;
} else {
btnFoto.classList.add('hidden');
urlFoto.value = '';
}
document.getElementById('modalTracking').classList.remove('hidden');
}
function closeTrackingModal() {
document.getElementById('modalTracking').classList.add('hidden');
}
function lihatFotoTracking() {
const url = document.getElementById('trackFotoUrl').value;
if(url) showImageModal(url);
}
// ==========================================
// 5. LIST PENGADUAN PUBLIK
// ==========================================
function loadPengaduanPublik() {
const container = document.getElementById('listPengaduanPublik');
container.innerHTML = '<div class="text-center py-8"><div class="loading mx-auto" style="border-top-color: #2563eb;"></div></div>';
google.script.run
.withSuccessHandler(function(data) {
allPublicData = data;
renderPublicList();
})
.withFailureHandler(function(error) {
container.innerHTML = `<div class="text-center text-red-500 py-8">Gagal memuat data: ${error.message}</div>`;
})
.getPengaduanPublik();
}
function renderPublicList() {
const container = document.getElementById('listPengaduanPublik');
const limitElement = document.getElementById('publicLimit');
const limitVal = limitElement ? limitElement.value : '10';
if (!allPublicData || allPublicData.length === 0) {
container.innerHTML = `
<div class="text-center py-8 bg-gray-50 rounded-lg border border-dashed border-gray-300">
<p class="text-gray-500">Belum ada pengaduan publik.</p>
</div>`;
return;
}
let displayData = [];
if (limitVal === 'all') {
displayData = allPublicData;
} else {
displayData = allPublicData.slice(0, parseInt(limitVal));
}
let html = '';
displayData.forEach(item => {
const statusClass = item.status === 'Sudah Diproses' ? 'badge-done' : 'badge-pending';
let imgHtml = '';
if (item.fotoUrl && item.fotoUrl !== '-') {
imgHtml = `
<a href="javascript:void(0)" onclick="showImageModal('${item.fotoUrl}')" class="ml-4 flex-shrink-0 relative group block cursor-pointer">
<img src="${item.fotoUrl}" alt="Bukti" class="w-24 h-24 object-cover rounded-lg border border-gray-200 shadow-sm transition-transform group-hover:scale-105">
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-10 transition-all rounded-lg flex items-center justify-center">
<span class="text-white opacity-0 group-hover:opacity-100 text-xs font-bold">🔍</span>
</div>
</a>`;
}
let tanggapanHtml = '';
if (item.tanggapan && item.tanggapan !== '') {
const safeTanggapan = item.tanggapan.replace(/"/g, '"');
tanggapanHtml = `
<div class="mt-3 bg-blue-50/80 border-l-4 border-blue-600 p-3 rounded-r-lg animate-fade-in">
<div class="flex items-center gap-2 mb-1">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9c/Logo_of_Ministry_of_Education_and_Culture_of_Republic_of_Indonesia.svg/2048px-Logo_of_Ministry_of_Education_and_Culture_of_Republic_of_Indonesia.svg.png" class="w-4 h-4 object-contain">
<span class="text-xs font-bold text-blue-800">Tanggapan Sekolah:</span>
</div>
<p class="text-xs text-gray-700 italic leading-relaxed">"${safeTanggapan}"</p>
</div>
`;
}
html += `
<div class="bg-white rounded-lg p-4 border border-gray-200 shadow-sm hover:shadow-md transition-all mb-4">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0 pr-2">
<div class="flex items-center gap-2 mb-2">
<span class="text-xs text-gray-500 font-medium">${item.tanggal}</span>
<span class="${statusClass} text-[10px] px-2 py-0.5 rounded-full font-bold uppercase tracking-wide">
${item.status}
</span>
</div>
<h4 class="text-gray-900 font-bold text-sm mb-1 truncate">${item.kategori}</h4>
<p class="text-gray-600 text-sm leading-relaxed line-clamp-3 mb-2">${item.ringkasan}</p>
${tanggapanHtml}
</div>
${imgHtml}
</div>
</div>`;
});
container.innerHTML = html;
}
// ==========================================
// 6. LOGIN & ADMIN SYSTEM
// ==========================================
function showLoginModal() {
document.getElementById('loginModal').classList.remove('hidden');
}
function hideLoginModal() {
document.getElementById('loginModal').classList.add('hidden');
document.getElementById('adminPassword').value = '';
}
function loginAdmin(event) {
event.preventDefault();
const btn = document.getElementById('btnLoginAdmin');
const textSpan = document.getElementById('btnLoginText');
const loadingSpan = document.getElementById('btnLoginLoading');
const passwordInput = document.getElementById('adminPassword');
textSpan.classList.add('hidden');
loadingSpan.classList.remove('hidden');
btn.disabled = true;
const password = passwordInput.value;
google.script.run
.withSuccessHandler(function(result) {
textSpan.classList.remove('hidden');
loadingSpan.classList.add('hidden');
btn.disabled = false;
if (result.success) {
localStorage.setItem('adminSession', 'active');
isLoggedIn = true;
hideLoginModal();
document.getElementById('landingPage').classList.add('hidden');
document.getElementById('adminPage').classList.remove('hidden');
showNotification('Login berhasil!', 'success');
showDashboard();
} else {
showNotification(result.message, 'error');
passwordInput.value = '';
passwordInput.focus();
}
})
.withFailureHandler(function(error) {
textSpan.classList.remove('hidden');
loadingSpan.classList.add('hidden');
btn.disabled = false;
showNotification('Gagal login: ' + error.message, 'error');
})
.loginAdmin(password);
}
function logout() {
Swal.fire({
title: 'Konfirmasi Logout',
text: "Apakah Anda yakin ingin keluar dari sesi Admin?",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ya, Keluar!',
cancelButtonText: 'Batal',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
localStorage.removeItem('adminSession');
isLoggedIn = false;
document.getElementById('adminPage').classList.add('hidden');
document.getElementById('landingPage').classList.remove('hidden');
Swal.fire({
title: 'Berhasil!',
text: 'Anda telah keluar dari sistem.',
icon: 'success',
timer: 1500,
showConfirmButton: false
});
loadPengaduanPublik();
}
});
}
function toggleSidebar() {
sidebarOpen = !sidebarOpen;
const sidebar = document.getElementById('sidebar');
const content = document.getElementById('adminContent');
if (sidebarOpen) {
sidebar.style.marginLeft = '0';
content.style.marginLeft = '256px';
} else {
sidebar.style.marginLeft = '-256px';
content.style.marginLeft = '0';
}
}
function showDashboard() {
document.getElementById('dashboardSection').classList.remove('hidden');
document.getElementById('dataPengaduanSection').classList.add('hidden');
document.getElementById('menuDashboard').classList.add('bg-[#2a2a3c]', 'text-white');
document.getElementById('menuPengaduan').classList.remove('bg-[#2a2a3c]', 'text-white');
loadPengaduanAdmin();
}
function showDataPengaduan() {
document.getElementById('dashboardSection').classList.add('hidden');
document.getElementById('dataPengaduanSection').classList.remove('hidden');
document.getElementById('menuDashboard').classList.remove('bg-[#2a2a3c]', 'text-white');
document.getElementById('menuPengaduan').classList.add('bg-[#2a2a3c]', 'text-white');
loadPengaduanAdmin();
}
// ==========================================
// 7. MANAJEMEN DATA ADMIN (TABLE, CHART)
// ==========================================
function loadPengaduanAdmin() {
const tbody = document.getElementById('tbodyPengaduan');
if(tbody) tbody.innerHTML = `<tr><td colspan="9" class="text-center py-8"><div class="loading mx-auto" style="border-top-color: #2563eb;"></div></td></tr>`;
google.script.run
.withSuccessHandler(function(data) {
allPengaduanData = data;
filteredData = data;
// Update Statistik Ringkasan
const total = data.length;
const belum = data.filter(item => item.status === 'Belum Diproses').length;
const sudah = data.filter(item => item.status === 'Sudah Diproses').length;
if(document.getElementById('totalPengaduan')) {
document.getElementById('totalPengaduan').textContent = total;
document.getElementById('belumDiproses').textContent = belum;
document.getElementById('sudahDiproses').textContent = sudah;
}
// Render Chart (Jika di Dashboard)
if(!document.getElementById('dashboardSection').classList.contains('hidden')) {
renderChart();
}
// Render Table (Jika di Halaman Data)
if(!document.getElementById('dataPengaduanSection').classList.contains('hidden')) {
handleSearch();
}
})
.withFailureHandler(function(error) {
showNotification('Gagal memuat data: ' + error.message, 'error');
})
.getPengaduanAdmin();
}
function renderChart() {
const counts = {};
allPengaduanData.forEach(item => {
const categoryName = item.kategori ? item.kategori.trim() : "Tanpa Kategori";
counts[categoryName] = (counts[categoryName] || 0) + 1;
});
const chartLabels = Object.keys(counts);
const chartDataPoints = Object.values(counts);
if (categoryChartInstance) {
categoryChartInstance.destroy();
}
const ctx = document.getElementById('categoryChart').getContext('2d');
const gradientBg = ctx.createLinearGradient(0, 0, 0, 400);
gradientBg.addColorStop(0, 'rgba(59, 130, 246, 0.9)');
gradientBg.addColorStop(1, 'rgba(59, 130, 246, 0.1)');
categoryChartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: chartLabels,
datasets: [{
label: 'Jumlah Pengaduan',
data: chartDataPoints,
backgroundColor: gradientBg,
borderColor: 'rgba(59, 130, 246, 1)',
borderWidth: 1,
borderRadius: 8,
barThickness: 'flex',
maxBarThickness: 40,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#1e1e2d',
padding: 12,
cornerRadius: 8,
}
},
scales: {
y: {
beginAtZero: true,
grid: { borderDash: [5, 5] },
ticks: { stepSize: 1 }
},
x: {
grid: { display: false }
}
}
}
});
}
function renderTable() {
const tbody = document.getElementById('tbodyPengaduan');
tbody.innerHTML = '';
const totalItems = filteredData.length;
const totalPages = itemsPerPage === 'all' ? 1 : Math.ceil(totalItems / itemsPerPage);
if (currentPage > totalPages) currentPage = totalPages || 1;
if (currentPage < 1) currentPage = 1;
let displayData = [];
let startRange = 0;
let endRange = 0;
if (totalItems > 0) {
if (itemsPerPage === 'all') {
displayData = filteredData;
startRange = 1;
endRange = totalItems;
} else {
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + parseInt(itemsPerPage);
displayData = filteredData.slice(startIndex, endIndex);
startRange = startIndex + 1;
endRange = Math.min(endIndex, totalItems);
}
}
document.getElementById('startRange').textContent = startRange;
document.getElementById('endRange').textContent = endRange;
document.getElementById('totalData').textContent = totalItems;
document.getElementById('btnPrev').disabled = currentPage === 1;
document.getElementById('btnNext').disabled = currentPage === totalPages || totalPages === 0;
if (displayData.length === 0) {
tbody.innerHTML = `<tr><td colspan="9" class="px-6 py-8 text-center text-gray-500 bg-gray-50">Tidak ada data ditemukan.</td></tr>`;
return;
}
let html = '';
displayData.forEach(item => {
const isDone = item.status === 'Sudah Diproses';
const disableBtn = isDone ? 'opacity-50 cursor-not-allowed' : '';
let fotoLink = '<span class="text-xs text-gray-400 italic">Tidak ada</span>';
if (item.fotoUrl && item.fotoUrl !== '-') {
fotoLink = `<a href="javascript:void(0)" onclick="showImageModal('${item.fotoUrl}')" class="text-blue-600 hover:text-blue-800 underline text-xs font-medium flex items-center gap-1"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg> Lihat</a>`;
}
const rawTanggapan = item.tanggapan || '';
const safeTanggapan = rawTanggapan.replace(/'/g, "\\'").replace(/"/g, '"');
const displayTanggapan = rawTanggapan
? `<span class="text-gray-700 italic">"${rawTanggapan}"</span>`
: `<span class="text-gray-400 text-xs">Belum ditanggapi</span>`;
html += `
<tr class="hover:bg-blue-50/50 transition-colors border-b border-gray-100 last:border-0 align-top">
<td class="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">${item.tanggal}</td>
<td class="px-6 py-4 text-sm font-bold text-gray-800">${item.nama}</td>
<td class="px-6 py-4 text-sm text-gray-600 whitespace-nowrap">
<span class="font-mono bg-gray-100 px-2 py-1 rounded text-xs tracking-wide">${item.nomorHp || '-'}</span>
</td>
<td class="px-6 py-4 text-sm text-gray-600">
<span class="bg-gray-100 text-gray-600 px-2 py-1 rounded text-xs font-medium border border-gray-200">${item.kategori}</span>
</td>
<td class="px-6 py-4 text-sm text-gray-600 max-w-xs">
<div class="line-clamp-2" title="${item.isiPengaduan}">${item.isiPengaduan}</div>
</td>
<td class="px-6 py-4 text-sm whitespace-nowrap">${fotoLink}</td>
<td class="px-6 py-4 text-sm max-w-xs">
<div class="line-clamp-2 text-xs border-l-2 border-blue-300 pl-2">
${displayTanggapan}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="${isDone ? 'badge-done' : 'badge-pending'} text-xs px-3 py-1 rounded-full font-bold uppercase tracking-wide flex w-fit items-center gap-1">
${isDone ? '✓' : '⏳'} ${item.status}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex gap-2">
<button onclick="bukaModalTanggapan('${item.id}', '${safeTanggapan}')"
class="bg-blue-600 hover:bg-blue-700 text-white text-xs px-3 py-1.5 rounded shadow-sm hover:shadow-md transition-all flex items-center gap-1"
title="Beri Tanggapan">
💬 Balas
</button>
<button onclick="updateStatusPengaduan('${item.id}')"
class="btn-success text-white text-xs px-3 py-1.5 rounded shadow-sm hover:shadow-md transition-all ${disableBtn}"
title="Tandai Selesai" ${isDone ? 'disabled' : ''}>
✓
</button>
<button onclick="hapusPengaduanConfirm('${item.id}')"
class="bg-white border border-red-200 text-red-600 hover:bg-red-50 text-xs px-3 py-1.5 rounded shadow-sm hover:shadow-md transition-all"
title="Hapus Data">
🗑️
</button>
</div>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
function handleSearch() {
const keyword = document.getElementById('searchInput').value.toLowerCase();
const statusFilter = document.getElementById('filterStatus').value;
filteredData = allPengaduanData.filter(item => {
const matchesKeyword = (
item.nama.toLowerCase().includes(keyword) ||
item.kategori.toLowerCase().includes(keyword) ||
item.isiPengaduan.toLowerCase().includes(keyword)
);
const matchesStatus = statusFilter === "" || item.status === statusFilter;
return matchesKeyword && matchesStatus;
});
currentPage = 1;
renderTable();
}
function changeItemsPerPage() {
const value = document.getElementById('itemsPerPage').value;
itemsPerPage = value === 'all' ? 'all' : parseInt(value);
currentPage = 1;
renderTable();
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderTable();
}
}
function nextPage() {
const totalItems = filteredData.length;
const totalPages = itemsPerPage === 'all' ? 1 : Math.ceil(totalItems / itemsPerPage);
if (currentPage < totalPages) {
currentPage++;
renderTable();
}
}
function updateStatusPengaduan(id) {
Swal.fire({
title: 'Konfirmasi Proses',
text: "Apakah Anda yakin ingin menandai pengaduan ini sebagai Selesai?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#10b981',
cancelButtonColor: '#6b7280',
confirmButtonText: 'Ya, Selesaikan!',
cancelButtonText: 'Batal',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
Swal.fire({
title: 'Memproses...', text: 'Sedang memperbarui status data.',
allowOutsideClick: false, showConfirmButton: false,
didOpen: () => { Swal.showLoading(); }
});
google.script.run
.withSuccessHandler(function(result) {
if (result.success) {
Swal.fire({ title: 'Berhasil!', text: 'Status diperbarui menjadi Selesai.', icon: 'success', timer: 1500, showConfirmButton: false });
loadPengaduanAdmin();
} else {
Swal.fire('Gagal!', result.message, 'error');
}
})
.withFailureHandler(function(error) {
Swal.fire('Error!', 'Gagal update status: ' + error.message, 'error');
})
.updateStatus(id);
}
});
}
function hapusPengaduanConfirm(id) {
Swal.fire({
title: 'Hapus Data?',
text: "Apakah Anda yakin ingin menghapus data ini secara PERMANEN?",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ya, Hapus!',
cancelButtonText: 'Batal',
reverseButtons: true
}).then((result) => {
if (result.isConfirmed) {
Swal.fire({
title: 'Memproses...', text: 'Sedang menghapus data.',
allowOutsideClick: false, didOpen: () => { Swal.showLoading(); }
});
google.script.run
.withSuccessHandler(function(result) {
if (result.success) {
Swal.fire({ title: 'Terhapus!', text: 'Data pengaduan berhasil dihapus.', icon: 'success', timer: 1500, showConfirmButton: false });
loadPengaduanAdmin();
} else {
Swal.fire('Gagal!', result.message, 'error');
}
})
.withFailureHandler(function(error) {
Swal.fire('Error!', 'Gagal menghapus: ' + error.message, 'error');
})
.hapusPengaduan(id);
}
});
}
function bukaModalTanggapan(id, currentText) {
const textVal = currentText === 'undefined' ? '' : currentText;
Swal.fire({
title: 'Tanggapan Sekolah',
input: 'textarea',
inputLabel: 'Tulis tanggapan atau tindak lanjut untuk laporan ini:',
inputValue: textVal,
inputPlaceholder: 'Contoh: Laporan diterima, tim sarpras akan segera memperbaiki...',
showCancelButton: true,
confirmButtonText: 'Kirim Tanggapan',
cancelButtonText: 'Batal',
confirmButtonColor: '#2563eb',
showLoaderOnConfirm: true,
preConfirm: (text) => {
if (!text) {
Swal.showValidationMessage('Tanggapan tidak boleh kosong!');
} else {
return new Promise((resolve) => {
google.script.run
.withSuccessHandler((result) => {
if(result.success) {
resolve();
loadPengaduanAdmin();
Swal.fire({ title: 'Terkirim!', text: 'Tanggapan berhasil disimpan.', icon: 'success', timer: 1500, showConfirmButton: false });
} else {
Swal.showValidationMessage(result.message);
}
})
.withFailureHandler((error) => {
Swal.showValidationMessage(`Request failed: ${error}`);
})
.simpanTanggapanAdmin(id, text);
});
}
}
});
}
// ==========================================
// 8. UTILITIES (NOTIFIKASI & GAMBAR)
// ==========================================
// ==========================================
// EKSPOR EXCEL FUNCTION
// ==========================================
function downloadExcel() {
const btn = document.getElementById('btnExportExcel');
const loading = document.getElementById('loadingExcel');
const text = btn.querySelector('span'); // Span teks "Ekspor Excel"
// UI Loading State
btn.disabled = true;
btn.classList.add('opacity-75', 'cursor-not-allowed');
loading.classList.remove('hidden');
text.textContent = 'Memproses...';
// Panggil Server
google.script.run
.withSuccessHandler(function(result) {
// Reset UI
btn.disabled = false;
btn.classList.remove('opacity-75', 'cursor-not-allowed');
loading.classList.add('hidden');
text.textContent = 'Ekspor Excel';
if (result.success) {
// Tampilkan Notif Sukses & Buka Link
Swal.fire({
title: 'Siap Mengunduh!',
text: 'File Excel berhasil dibuat. Unduhan akan segera dimulai.',
icon: 'success',
timer: 2000,
showConfirmButton: false
});
// Buka link download di tab baru
window.open(result.url, '_blank');
} else {
Swal.fire('Gagal!', result.message, 'error');
}
})
.withFailureHandler(function(error) {
// Reset UI Error
btn.disabled = false;
btn.classList.remove('opacity-75', 'cursor-not-allowed');
loading.classList.add('hidden');
text.textContent = 'Ekspor Excel';
Swal.fire('Error Server', error.message, 'error');
})
.generateExcelAdmin(); // Memanggil fungsi wrapper di server
}
function showImageModal(url) {
const modal = document.getElementById('imageModal');
const img = document.getElementById('modalImageFull');
img.src = url;
modal.classList.remove('hidden');
setTimeout(() => {
img.classList.remove('scale-95', 'opacity-0');
img.classList.add('scale-100', 'opacity-100');
}, 50);
}
function closeImageModal() {
const modal = document.getElementById('imageModal');
const img = document.getElementById('modalImageFull');
img.classList.remove('scale-100', 'opacity-100');
img.classList.add('scale-95', 'opacity-0');
setTimeout(() => {
modal.classList.add('hidden');
img.src = '';
}, 300);
}
function showNotification(message, type = 'success') {
const container = document.getElementById('notificationContainer');
const bgClass = type === 'success' ? 'bg-green-600' : 'bg-red-600';
const notification = document.createElement('div');
notification.className = `notification ${bgClass} text-white px-6 py-4 rounded-lg shadow-lg max-w-md flex items-center justify-between gap-4`;
notification.innerHTML = `
<span class="font-medium text-sm">${message}</span>
<button onclick="this.parentElement.remove()" class="text-white hover:text-gray-200 font-bold">✕</button>
`;
container.appendChild(notification);
setTimeout(() => {
if(notification.parentElement) notification.remove();
}, 5000);
}
function handleResize() {
const width = window.innerWidth;
const sidebar = document.getElementById('sidebar');
const content = document.getElementById('adminContent');
if (!document.getElementById('adminPage').classList.contains('hidden')) {
if (width < 768) {
sidebar.style.marginLeft = '-256px';
content.style.marginLeft = '0';
sidebarOpen = false;
} else {
sidebar.style.marginLeft = '0';
content.style.marginLeft = '256px';
sidebarOpen = true;
}
}
}
window.addEventListener('resize', handleResize);
</script>
</body>
</html>
Tidak ada komentar:
Posting Komentar