<!doctype html>
<html lang="id" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sistem Manajemen TPP</title>
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<script src="https://cdn.jsdelivr.net/npm/lucide@0.263.0/dist/umd/lucide.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.min.js"></script>
<script src="/_sdk/element_sdk.js"></script>
<script src="/_sdk/data_sdk.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* { font-family: 'Plus Jakarta Sans', sans-serif; }
html, body { height: 100%; margin: 0; }
.app-root { height: 100%; overflow: auto; }
.sidebar-link { transition: all 0.2s; }
.sidebar-link:hover, .sidebar-link.active { background: rgba(255,255,255,0.15); }
.card-hover { transition: transform 0.2s, box-shadow 0.2s; }
.card-hover:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.1); }
.bubble-float { animation: floatBubble 3s ease-in-out infinite; }
@keyframes floatBubble { 0%,100%{ transform: translateY(0); } 50%{ transform: translateY(-8px); } }
.fade-in { animation: fadeIn 0.3s ease; }
@keyframes fadeIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }
.toast-show { animation: toastIn 0.3s ease; }
@keyframes toastIn { from { opacity:0; transform:translateX(100px); } to { opacity:1; transform:translateX(0); } }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
.chat-bubble-left { border-radius: 4px 16px 16px 16px; }
.chat-bubble-right { border-radius: 16px 4px 16px 16px; }
</style>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: { 50:'#eff6ff', 100:'#dbeafe', 200:'#bfdbfe', 300:'#93c5fd', 400:'#60a5fa', 500:'#2563eb', 600:'#1d4ed8', 700:'#1e3a5f', 800:'#172554', 900:'#0f172a' },
accent: { 400:'#f59e0b', 500:'#d97706' },
surface: '#f8fafc'
}
}
}
}
</script>
<style>body { box-sizing: border-box; }</style>
</head>
<body class="h-full bg-surface text-brand-900">
<div id="app" class="app-root"></div>
<script>
// ==================== STATE ====================
let allData = [];
let currentUser = null; // { username, role, kecamatan, __backendId }
let currentPage = 'login';
let adminPage = 'dashboard';
let userPage = 'belajar';
let toastMsg = '';
let toastTimer = null;
let chatDraft = '';
let onlineUsers = {}; // Track which users are currently online
let lastActivityTime = {}; // Track last activity per user
const defaultConfig = {
app_title: 'BERDAYA TPP PASURUAN',
background_color: '#0f172a',
surface_color: '#f8fafc',
text_color: '#1e293b',
primary_color: '#2563eb',
secondary_color: '#f59e0b'
};
// ==================== HELPERS ====================
function getByType(type) { return allData.filter(d => d.type === type); }
function showToast(msg) {
toastMsg = msg;
render();
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { toastMsg = ''; render(); }, 3000);
}
function isUserOnline(username) {
return onlineUsers[username] === true;
}
function updateUserOnlineStatus(username, isOnline) {
onlineUsers[username] = isOnline;
if (isOnline) {
lastActivityTime[username] = Date.now();
}
render();
}
function formatDate(iso) {
if (!iso) return '-';
const d = new Date(iso);
return d.toLocaleDateString('id-ID', { day:'2-digit', month:'short', year:'numeric', hour:'2-digit', minute:'2-digit' });
}
// ==================== DATA SDK ====================
const dataHandler = {
onDataChanged(data) {
allData = data;
render();
}
};
async function initApp() {
const r = await window.dataSdk.init(dataHandler);
if (!r.isOk) console.error('Data SDK init failed');
render();
}
async function createRecord(obj) {
if (allData.length >= 999) { showToast('Batas data tercapai (999)'); return false; }
const r = await window.dataSdk.create(obj);
if (!r.isOk) { showToast('Gagal menyimpan data'); return false; }
return true;
}
async function updateRecord(obj) {
const r = await window.dataSdk.update(obj);
if (!r.isOk) showToast('Gagal update');
return r.isOk;
}
async function deleteRecord(obj) {
const r = await window.dataSdk.delete(obj);
if (!r.isOk) showToast('Gagal hapus');
return r.isOk;
}
// ==================== ELEMENT SDK ====================
window.elementSdk.init({
defaultConfig,
onConfigChange: async (config) => {
document.documentElement.style.setProperty('--bg', config.background_color || defaultConfig.background_color);
document.documentElement.style.setProperty('--surface', config.surface_color || defaultConfig.surface_color);
document.documentElement.style.setProperty('--text', config.text_color || defaultConfig.text_color);
document.documentElement.style.setProperty('--primary', config.primary_color || defaultConfig.primary_color);
document.documentElement.style.setProperty('--secondary', config.secondary_color || defaultConfig.secondary_color);
render();
},
mapToCapabilities: (config) => ({
recolorables: [
{ get: () => config.background_color || defaultConfig.background_color, set: v => { config.background_color = v; window.elementSdk.setConfig({ background_color: v }); } },
{ get: () => config.surface_color || defaultConfig.surface_color, set: v => { config.surface_color = v; window.elementSdk.setConfig({ surface_color: v }); } },
{ get: () => config.text_color || defaultConfig.text_color, set: v => { config.text_color = v; window.elementSdk.setConfig({ text_color: v }); } },
{ get: () => config.primary_color || defaultConfig.primary_color, set: v => { config.primary_color = v; window.elementSdk.setConfig({ primary_color: v }); } },
{ get: () => config.secondary_color || defaultConfig.secondary_color, set: v => { config.secondary_color = v; window.elementSdk.setConfig({ secondary_color: v }); } }
],
borderables: [],
fontEditable: undefined,
fontSizeable: undefined
}),
mapToEditPanelValues: (config) => new Map([
['app_title', config.app_title || defaultConfig.app_title]
])
});
// ==================== RENDER ====================
function render() {
const app = document.getElementById('app');
if (!app) return;
const cfg = window.elementSdk.config || defaultConfig;
const title = cfg.app_title || defaultConfig.app_title;
if (currentPage === 'login') {
app.innerHTML = renderLogin(title);
} else if (currentPage === 'admin') {
app.innerHTML = renderAdmin(title);
} else if (currentPage === 'user') {
app.innerHTML = renderUser(title);
}
lucide.createIcons();
bindEvents();
}
// ==================== LOGIN PAGE ====================
function renderLogin(title) {
return `
<div class="h-full flex items-center justify-center" style="background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 50%, #2563eb 100%);">
<div class="bg-white rounded-2xl shadow-2xl p-8 w-full max-w-md mx-4 fade-in">
<div class="text-center mb-8">
<div class="w-16 h-16 bg-brand-500 rounded-2xl flex items-center justify-center mx-auto mb-4">
<i data-lucide="database" class="text-white" style="width:32px;height:32px;"></i>
</div>
<h1 class="text-2xl font-bold text-brand-900">${title}</h1>
<p class="text-sm text-gray-500 mt-1">Masuk ke sistem untuk melanjutkan</p>
</div>
<div id="login-error" class="hidden bg-red-50 text-red-600 text-sm p-3 rounded-lg mb-4"></div>
<form id="login-form" class="space-y-4">
<div>
<label for="login-user" class="block text-sm font-medium text-gray-700 mb-1">Username</label>
<input id="login-user" type="text" class="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-brand-500 focus:border-transparent outline-none" placeholder="Masukkan username">
</div>
<div>
<label for="login-pass" class="block text-sm font-medium text-gray-700 mb-1">Password</label>
<input id="login-pass" type="password" class="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-brand-500 focus:border-transparent outline-none" placeholder="Masukkan password">
</div>
<button type="submit" class="w-full py-3 bg-brand-500 hover:bg-brand-600 text-white font-semibold rounded-xl transition">Masuk</button>
</form>
</div>
</div>
${renderToast()}`;
}
// ==================== ADMIN PAGES ====================
function renderAdmin(title) {
const pages = {
dashboard: renderAdminDashboard,
belajar: renderAdminBelajar,
berdata: renderAdminBerdata,
pengguna: renderAdminPengguna,
aktifitas: renderAdminAktifitas,
info: renderAdminInfo,
chat: renderAdminChat
};
const content = (pages[adminPage] || renderAdminDashboard)();
const menuItems = [
{ id:'dashboard', icon:'layout-dashboard', label:'Dashboard' },
{ id:'belajar', icon:'book-open', label:'Pusat Belajar' },
{ id:'berdata', icon:'file-spreadsheet', label:'Berdata' },
{ id:'pengguna', icon:'users', label:'Data Pengguna' },
{ id:'aktifitas', icon:'activity', label:'Aktifitas' },
{ id:'info', icon:'megaphone', label:'Info Hot' },
{ id:'chat', icon:'message-circle', label:'Chat' }
];
return `
<div class="h-full flex">
<!-- Sidebar -->
<aside class="w-64 flex-shrink-0 text-white flex flex-col" style="background: linear-gradient(180deg, #0f172a 0%, #1e3a5f 100%);">
<div class="p-5 border-b border-white/10">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-brand-500 rounded-xl flex items-center justify-center">
<i data-lucide="database" class="text-white" style="width:20px;height:20px;"></i>
</div>
<div>
<div class="font-bold text-sm">${title}</div>
<div class="text-xs text-blue-300">Admin Panel</div>
</div>
</div>
</div>
<nav class="flex-1 p-3 space-y-1 overflow-auto">
${menuItems.map(m => `
<button data-admin-nav="${m.id}" class="sidebar-link w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm ${adminPage===m.id?'active bg-white/15 font-semibold':'text-blue-200 hover:text-white'}">
<i data-lucide="${m.icon}" style="width:18px;height:18px;"></i> ${m.label}
</button>
`).join('')}
</nav>
<div class="p-3 border-t border-white/10">
<button id="admin-logout" class="sidebar-link w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm text-red-300 hover:text-red-200">
<i data-lucide="log-out" style="width:18px;height:18px;"></i> Keluar
</button>
</div>
</aside>
<!-- Main -->
<main class="flex-1 overflow-auto p-6 fade-in">${content}</main>
</div>
${renderToast()}`;
}
function renderAdminDashboard() {
const users = getByType('user');
const tagihan = getByType('tagihan');
const activities = getByType('activity');
const loggedKec = [...new Set(activities.filter(a=>a.action==='login').map(a=>a.kecamatan))];
return `
<h2 class="text-2xl font-bold mb-6">Dashboard</h2>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
${statCard('users','Total Pengguna', users.length, 'bg-blue-500')}
${statCard('file-spreadsheet','Total Tagihan', tagihan.length, 'bg-amber-500')}
${statCard('map-pin','Kecamatan Login', loggedKec.length, 'bg-emerald-500')}
${statCard('activity','Total Aktifitas', activities.length, 'bg-purple-500')}
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white rounded-2xl p-6 shadow-sm">
<h3 class="font-bold text-lg mb-4">Status Kecamatan</h3>
<div class="space-y-2 max-h-64 overflow-auto">
${users.length === 0 ? '<p class="text-gray-400 text-sm">Belum ada pengguna terdaftar</p>' :
users.map(u => {
const logged = loggedKec.includes(u.kecamatan);
return `<div class="flex items-center justify-between p-3 rounded-xl ${logged?'bg-emerald-50':'bg-gray-50'}">
<div>
<div class="font-medium text-sm">${u.kecamatan || u.username}</div>
<div class="text-xs text-gray-500">${u.username}</div>
</div>
<span class="text-xs px-3 py-1 rounded-full font-medium ${logged?'bg-emerald-100 text-emerald-700':'bg-gray-200 text-gray-500'}">${logged?'Sudah Login':'Belum Login'}</span>
</div>`;
}).join('')}
</div>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm">
<h3 class="font-bold text-lg mb-4">Aktifitas Terbaru</h3>
<div class="space-y-2 max-h-64 overflow-auto">
${activities.length === 0 ? '<p class="text-gray-400 text-sm">Belum ada aktifitas</p>' :
activities.slice(-10).reverse().map(a => `
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-xl">
<div class="w-8 h-8 bg-brand-100 rounded-lg flex items-center justify-center"><i data-lucide="user" style="width:14px;height:14px;" class="text-brand-500"></i></div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium truncate">${a.username} - ${a.action}</div>
<div class="text-xs text-gray-400">${formatDate(a.timestamp)}</div>
</div>
</div>
`).join('')}
</div>
</div>
</div>`;
}
function statCard(icon, label, value, bg) {
return `<div class="card-hover bg-white rounded-2xl p-5 shadow-sm">
<div class="flex items-center gap-4">
<div class="w-12 h-12 ${bg} rounded-xl flex items-center justify-center"><i data-lucide="${icon}" class="text-white" style="width:22px;height:22px;"></i></div>
<div><div class="text-2xl font-bold">${value}</div><div class="text-xs text-gray-500">${label}</div></div>
</div>
</div>`;
}
function renderAdminBelajar() {
const materials = getByType('materi');
return `
<h2 class="text-2xl font-bold mb-6">Pusat Belajar</h2>
<div class="bg-white rounded-2xl p-6 shadow-sm mb-6">
<h3 class="font-semibold mb-4">Tambah Materi</h3>
<form id="form-materi" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<input id="mat-title" placeholder="Judul Materi" class="px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500" required>
<select id="mat-type" class="px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500">
<option value="pdf">PDF</option><option value="ppt">PPT</option><option value="video">Video/YouTube</option>
</select>
<input id="mat-category" placeholder="Kategori" class="px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500">
<input id="mat-link" placeholder="Link Materi (URL)" class="px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500">
<textarea id="mat-summary" placeholder="Ringkasan Materi" rows="3" class="md:col-span-2 px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500"></textarea>
<div class="md:col-span-2"><button type="submit" class="px-6 py-3 bg-brand-500 hover:bg-brand-600 text-white rounded-xl font-semibold transition">Simpan Materi</button></div>
</form>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm">
<h3 class="font-semibold mb-4">Daftar Materi (${materials.length})</h3>
${materials.length===0?'<p class="text-gray-400 text-sm">Belum ada materi</p>':`
<div class="overflow-auto">
<table class="w-full text-sm">
<thead><tr class="border-b text-left text-gray-500"><th class="p-3">No</th><th class="p-3">Judul</th><th class="p-3">Tipe</th><th class="p-3">Kategori</th><th class="p-3">Aksi</th></tr></thead>
<tbody>${materials.map((m,i)=>`
<tr class="border-b hover:bg-gray-50">
<td class="p-3">${i+1}</td>
<td class="p-3 font-medium">${m.title}</td>
<td class="p-3"><span class="px-2 py-1 rounded-full text-xs font-medium ${m.file_type==='video'?'bg-red-100 text-red-600':m.file_type==='ppt'?'bg-orange-100 text-orange-600':'bg-blue-100 text-blue-600'}">${(m.file_type||'').toUpperCase()}</span></td>
<td class="p-3">${m.category||'-'}</td>
<td class="p-3"><button data-del-materi="${m.__backendId}" class="text-red-500 hover:text-red-700 text-xs font-medium">Hapus</button></td>
</tr>
`).join('')}</tbody>
</table>
</div>`}
</div>`;
}
function renderAdminBerdata() {
const tagihan = getByType('tagihan');
return `
<h2 class="text-2xl font-bold mb-6">Berdata - Tagihan</h2>
<div class="bg-white rounded-2xl p-6 shadow-sm mb-6">
<h3 class="font-semibold mb-4">Tambah Tagihan Data</h3>
<form id="form-tagihan" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<input id="tag-title" placeholder="Judul Tagihan" class="px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500" required>
<input id="tag-link" placeholder="Link Google Sheet" class="px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500" required>
<input id="tag-deadline" type="date" class="px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500">
<input id="tag-status" placeholder="Status (aktif/selesai)" value="aktif" class="px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500">
<textarea id="tag-desc" placeholder="Penjelasan pengisian data" rows="2" class="md:col-span-2 px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500"></textarea>
<div class="md:col-span-2"><button type="submit" class="px-6 py-3 bg-brand-500 hover:bg-brand-600 text-white rounded-xl font-semibold transition">Simpan Tagihan</button></div>
</form>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm">
<h3 class="font-semibold mb-4">Daftar Tagihan (${tagihan.length})</h3>
${tagihan.length===0?'<p class="text-gray-400 text-sm">Belum ada tagihan</p>':`
<div class="space-y-3">
${tagihan.map(t=>`
<div class="flex items-center justify-between p-4 border rounded-xl hover:bg-gray-50">
<div class="flex-1 min-w-0">
<div class="font-medium">${t.title}</div>
<div class="text-xs text-gray-500 mt-1">Deadline: ${t.deadline||'-'} | Status: ${t.status||'aktif'}</div>
<div class="text-xs text-gray-400 truncate mt-1">${t.link||''}</div>
</div>
<button data-del-tagihan="${t.__backendId}" class="ml-3 text-red-500 hover:text-red-700"><i data-lucide="trash-2" style="width:16px;height:16px;"></i></button>
</div>
`).join('')}
</div>`}
</div>`;
}
function renderAdminPengguna() {
const users = getByType('user');
return `
<h2 class="text-2xl font-bold mb-6">Data Pengguna</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<!-- Form Manual -->
<div class="bg-white rounded-2xl p-6 shadow-sm">
<h3 class="font-semibold mb-4">Tambah Manual</h3>
<form id="form-user" class="space-y-4">
<input id="usr-name" placeholder="Username" class="w-full px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500" required>
<input id="usr-pass" placeholder="Password" class="w-full px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500" required>
<input id="usr-kec" placeholder="Kecamatan" class="w-full px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500" required>
<button type="submit" class="w-full px-6 py-3 bg-brand-500 hover:bg-brand-600 text-white rounded-xl font-semibold transition">Tambah</button>
</form>
</div>
<!-- Import Excel -->
<div class="bg-white rounded-2xl p-6 shadow-sm">
<h3 class="font-semibold mb-4">Import Excel</h3>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Pilih File</label>
<input id="import-excel" type="file" accept=".xlsx,.xls" class="w-full px-4 py-3 border rounded-xl text-sm cursor-pointer">
</div>
<button id="btn-import" type="button" class="w-full px-6 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl font-semibold transition flex items-center justify-center gap-2">
<i data-lucide="upload" style="width:16px;height:16px;"></i> Import
</button>
<div id="import-status" class="hidden text-sm p-3 rounded-lg"></div>
</div>
</div>
<!-- Paste Data -->
<div class="bg-white rounded-2xl p-6 shadow-sm">
<h3 class="font-semibold mb-4">Paste Data 📋</h3>
<div class="space-y-3">
<p class="text-xs text-gray-600 mb-2">Paste dari spreadsheet (3 kolom: Username | Password | Kecamatan)</p>
<textarea id="paste-data" placeholder="Paste data di sini user1 pass123 Pasuruan user2 pass456 Pandaan" rows="4" class="w-full px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500 text-sm font-mono"></textarea>
<button id="btn-paste" type="button" class="w-full px-6 py-3 bg-violet-500 hover:bg-violet-600 text-white rounded-xl font-semibold transition flex items-center justify-center gap-2">
<i data-lucide="clipboard-paste" style="width:16px;height:16px;"></i> Paste
</button>
<div id="paste-status" class="hidden text-sm p-3 rounded-lg"></div>
</div>
</div>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm">
<h3 class="font-semibold mb-4">Daftar Pengguna (${users.length})</h3>
${users.length===0?'<p class="text-gray-400 text-sm">Belum ada pengguna</p>':`
<div class="overflow-auto">
<table class="w-full text-sm">
<thead><tr class="border-b text-left text-gray-500"><th class="p-3">No</th><th class="p-3">Username</th><th class="p-3">Kecamatan</th><th class="p-3">Aksi</th></tr></thead>
<tbody>${users.map((u,i)=>`
<tr class="border-b hover:bg-gray-50">
<td class="p-3">${i+1}</td>
<td class="p-3 font-medium">${u.username}</td>
<td class="p-3">${u.kecamatan||'-'}</td>
<td class="p-3"><button data-del-user="${u.__backendId}" class="text-red-500 hover:text-red-700 text-xs font-medium">Hapus</button></td>
</tr>
`).join('')}</tbody>
</table>
</div>`}
</div>`;
}
function renderAdminAktifitas() {
const activities = getByType('activity').slice().reverse();
return `
<h2 class="text-2xl font-bold mb-6">Aktifitas Pengguna</h2>
<div class="bg-white rounded-2xl p-6 shadow-sm">
${activities.length===0?'<p class="text-gray-400 text-sm">Belum ada aktifitas</p>':`
<div class="overflow-auto">
<table class="w-full text-sm">
<thead><tr class="border-b text-left text-gray-500"><th class="p-3">Waktu</th><th class="p-3">Username</th><th class="p-3">Kecamatan</th><th class="p-3">Aktifitas</th><th class="p-3">Detail</th></tr></thead>
<tbody>${activities.map(a=>`
<tr class="border-b hover:bg-gray-50">
<td class="p-3 text-xs">${formatDate(a.timestamp)}</td>
<td class="p-3 font-medium">${a.username}</td>
<td class="p-3">${a.kecamatan||'-'}</td>
<td class="p-3"><span class="px-2 py-1 rounded-full text-xs font-medium ${a.action==='login'?'bg-green-100 text-green-700':'bg-blue-100 text-blue-700'}">${a.action}</span></td>
<td class="p-3 text-xs text-gray-500">${a.description||'-'}</td>
</tr>
`).join('')}</tbody>
</table>
</div>`}
</div>`;
}
function renderAdminInfo() {
const infos = getByType('info');
return `
<h2 class="text-2xl font-bold mb-6">Info Hot 🔥</h2>
<div class="bg-white rounded-2xl p-6 shadow-sm mb-6">
<h3 class="font-semibold mb-4">Tambah Info</h3>
<form id="form-info" class="space-y-4">
<input id="info-title" placeholder="Judul Info" class="w-full px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500" required>
<textarea id="info-content" placeholder="Isi Info" rows="3" class="w-full px-4 py-3 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500" required></textarea>
<button type="submit" class="px-6 py-3 bg-amber-500 hover:bg-amber-600 text-white rounded-xl font-semibold transition">Tambah Info Hot</button>
</form>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm">
<h3 class="font-semibold mb-4">Daftar Info (${infos.length})</h3>
<div class="space-y-3">
${infos.length===0?'<p class="text-gray-400 text-sm">Belum ada info</p>':
infos.map(info=>`
<div class="p-4 border-l-4 border-amber-400 bg-amber-50 rounded-xl flex justify-between items-start">
<div><div class="font-semibold text-sm">${info.title}</div><div class="text-xs text-gray-600 mt-1">${info.content}</div></div>
<button data-del-info="${info.__backendId}" class="text-red-500 hover:text-red-700 ml-3"><i data-lucide="trash-2" style="width:16px;height:16px;"></i></button>
</div>
`).join('')}
</div>
</div>`;
}
function renderAdminChat() {
const users = getByType('user');
const chats = getByType('chat');
// Get unread chat count per user (messages from user to admin not yet marked as read)
const getUnreadCount = (username) => {
return chats.filter(c =>
c.sender === username && c.receiver === 'admin' && !c.read
).length;
};
return `
<h2 class="text-2xl font-bold mb-6">Chat</h2>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="bg-white rounded-2xl p-4 shadow-sm">
<h3 class="font-semibold mb-3 text-sm">Daftar Pengguna</h3>
<div class="space-y-2">
${users.length===0?'<p class="text-gray-400 text-xs">Belum ada pengguna terdaftar</p>':
users.map(u=>{
const unreadCount = getUnreadCount(u.username);
const online = isUserOnline(u.username);
return `<button data-chat-select="${u.username}" class="w-full text-left p-3 rounded-xl hover:bg-brand-50 text-sm font-medium flex items-center gap-2 transition border border-gray-200 relative">
<div class="relative">
<i data-lucide="message-circle" style="width:14px;height:14px;" class="text-brand-500"></i>
<div class="absolute -top-1 -right-1 w-2.5 h-2.5 rounded-full ${online ? 'bg-green-500 animate-pulse' : 'bg-gray-300'}"></div>
</div>
<div class="flex-1">
<div class="flex items-center gap-1">
${u.username}
${online ? '<span class="text-xs px-2 py-0.5 bg-green-100 text-green-700 rounded-full font-semibold">Online</span>' : ''}
</div>
<div class="text-xs text-gray-400">${u.kecamatan||'N/A'}</div>
</div>
${unreadCount > 0 ? `<span class="absolute top-1 right-1 w-6 h-6 bg-red-500 text-white text-xs flex items-center justify-center rounded-full font-bold animate-pulse">${unreadCount}</span>` : ''}
</button>`;
}).join('')}
</div>
</div>
<div class="lg:col-span-2 bg-white rounded-2xl p-4 shadow-sm flex flex-col" id="chat-area" style="height: 600px;">
<p class="text-gray-400 text-sm text-center py-12">Pilih pengguna untuk memulai chat</p>
</div>
</div>`;
}
function renderChatWith(partner) {
const chats = getByType('chat').filter(c =>
(c.sender===partner && c.receiver==='admin') || (c.sender==='admin' && c.receiver===partner)
).sort((a,b) => new Date(a.timestamp) - new Date(b.timestamp));
// Mark messages from partner as read
(async () => {
for (let c of chats) {
if (c.sender === partner && c.receiver === 'admin' && !c.read) {
const updated = { ...c, read: true };
await updateRecord(updated);
}
}
})();
const area = document.getElementById('chat-area');
if (!area) return;
area.innerHTML = `
<h3 class="font-semibold mb-3 text-sm border-b pb-3 flex-shrink-0">Chat dengan ${partner}</h3>
<div class="flex-1 overflow-auto space-y-3 mb-3 p-2" id="chat-messages">
${chats.map(c => c.sender==='admin' ?
`<div class="flex justify-end"><div class="chat-bubble-right bg-brand-500 text-white px-4 py-2 text-sm max-w-xs rounded-lg">${c.message}<div class="text-xs opacity-70 mt-1">${formatDate(c.timestamp)}</div></div></div>` :
`<div class="flex justify-start"><div class="chat-bubble-left bg-gray-100 px-4 py-2 text-sm max-w-xs rounded-lg">${c.message}<div class="text-xs text-gray-400 mt-1">${formatDate(c.timestamp)}</div></div></div>`
).join('')}
</div>
<form id="admin-chat-form" data-partner="${partner}" class="flex gap-2 flex-shrink-0">
<input id="admin-chat-input" placeholder="Ketik pesan..." class="flex-1 px-4 py-2 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500 text-sm" required>
<button type="submit" class="px-4 py-2 bg-brand-500 text-white rounded-xl hover:bg-brand-600 transition font-medium"><i data-lucide="send" style="width:16px;height:16px;"></i></button>
</form>`;
lucide.createIcons();
const msgs = document.getElementById('chat-messages');
if (msgs) msgs.scrollTop = msgs.scrollHeight;
// Rebind admin chat form
const acf = document.getElementById('admin-chat-form');
if (acf) acf.onsubmit = async (e) => {
e.preventDefault();
const input = document.getElementById('admin-chat-input');
const msg = input.value.trim();
if (!msg) return;
input.value = '';
const btn = acf.querySelector('button[type=submit]');
btn.disabled = true;
btn.innerHTML = '...';
const created = await createRecord({
type: 'chat', sender: 'admin', receiver: partner, message: msg, timestamp: new Date().toISOString()
});
if (created) {
setTimeout(() => {
renderChatWith(partner);
btn.disabled = false;
btn.innerHTML = '<i data-lucide="send" style="width:16px;height:16px;"></i>';
lucide.createIcons();
}, 300);
} else {
btn.disabled = false;
btn.innerHTML = '<i data-lucide="send" style="width:16px;height:16px;"></i>';
}
};
}
// ==================== USER PAGES ====================
function renderUser(title) {
const pages = {
belajar: renderUserBelajar,
berdata: renderUserBerdata,
info: renderUserInfo,
masukan: renderUserMasukan
};
const content = (pages[userPage] || renderUserBelajar)();
const infos = getByType('info');
const menuItems = [
{ id:'belajar', icon:'book-open', label:'Pusat Belajar' },
{ id:'berdata', icon:'file-spreadsheet', label:'Berdata' },
{ id:'info', icon:'megaphone', label:'Info' },
{ id:'masukan', icon:'message-circle', label:'Masukan' }
];
return `
<div class="h-full flex">
<aside class="w-60 flex-shrink-0 text-white flex flex-col" style="background: linear-gradient(180deg, #1e3a5f 0%, #2563eb 100%);">
<div class="p-5 border-b border-white/10">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
<i data-lucide="user" class="text-white" style="width:20px;height:20px;"></i>
</div>
<div>
<div class="font-bold text-sm">${currentUser?.username||''}</div>
<div class="text-xs text-blue-200">${currentUser?.kecamatan||''}</div>
</div>
</div>
</div>
<nav class="flex-1 p-3 space-y-1 overflow-auto">
${menuItems.map(m => {
let badge = '';
if (m.id === 'masukan') {
const unreadChats = getByType('chat').filter(c => c.sender === 'admin' && c.receiver === currentUser?.username && !c.read).length;
if (unreadChats > 0) {
badge = `<span class="absolute top-2 right-2 w-5 h-5 bg-red-500 text-white text-xs flex items-center justify-center rounded-full font-bold animate-pulse">${unreadChats}</span>`;
}
}
return `
<button data-user-nav="${m.id}" class="sidebar-link w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm ${userPage===m.id?'active bg-white/15 font-semibold':'text-blue-200 hover:text-white'} relative">
<i data-lucide="${m.icon}" style="width:18px;height:18px;"></i> ${m.label}
${badge}
</button>`;
}).join('')}
</nav>
<div class="p-3 border-t border-white/10">
<button id="user-logout" class="sidebar-link w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm text-red-300 hover:text-red-200">
<i data-lucide="log-out" style="width:18px;height:18px;"></i> Keluar
</button>
</div>
</aside>
<main class="flex-1 overflow-auto p-6 fade-in relative">
${content}
${infos.length > 0 ? `
<div class="fixed bottom-6 right-6 bubble-float cursor-pointer z-50" id="info-bubble">
<div class="w-14 h-14 bg-amber-500 rounded-full flex items-center justify-center shadow-lg relative">
<i data-lucide="bell" class="text-white" style="width:24px;height:24px;"></i>
<span class="absolute -top-1 -right-1 w-5 h-5 bg-red-500 rounded-full text-white text-xs flex items-center justify-center font-bold">${infos.length}</span>
</div>
</div>
<div id="info-popup" class="hidden fixed bottom-24 right-6 w-80 bg-white rounded-2xl shadow-2xl border p-4 z-50 max-h-72 overflow-auto">
<h4 class="font-bold text-sm mb-3 flex items-center gap-2">🔥 Info Terbaru</h4>
<div class="space-y-2">
${infos.map(info=>`<div class="p-3 bg-amber-50 border-l-4 border-amber-400 rounded-lg"><div class="font-semibold text-xs">${info.title}</div><div class="text-xs text-gray-600 mt-1">${info.content}</div></div>`).join('')}
</div>
</div>` : ''}
</main>
</div>
${renderToast()}`;
}
function renderUserBelajar() {
const materials = getByType('materi');
return `
<h2 class="text-2xl font-bold mb-6">Pusat Belajar 📚</h2>
<div class="bg-white rounded-2xl p-6 shadow-sm">
${materials.length===0?'<p class="text-gray-400 text-sm">Belum ada materi tersedia</p>':`
<div class="overflow-auto">
<table class="w-full text-sm">
<thead><tr class="border-b text-left text-gray-500"><th class="p-3">No</th><th class="p-3">Materi</th><th class="p-3">Tipe</th><th class="p-3">Ringkasan</th><th class="p-3">Aksi</th></tr></thead>
<tbody>${materials.map((m,i)=>`
<tr class="border-b hover:bg-gray-50">
<td class="p-3">${i+1}</td>
<td class="p-3 font-medium">${m.title}</td>
<td class="p-3"><span class="px-2 py-1 rounded-full text-xs font-medium ${m.file_type==='video'?'bg-red-100 text-red-600':m.file_type==='ppt'?'bg-orange-100 text-orange-600':'bg-blue-100 text-blue-600'}">${(m.file_type||'').toUpperCase()}</span></td>
<td class="p-3 text-xs text-gray-600 max-w-xs truncate">${m.summary||'-'}</td>
<td class="p-3">${m.link?`<a href="${m.link}" target="_blank" rel="noopener noreferrer" class="text-brand-500 hover:underline text-xs font-medium" data-log-belajar="${m.title}">Buka</a>`:'-'}</td>
</tr>
`).join('')}</tbody>
</table>
</div>`}
</div>`;
}
function renderUserBerdata() {
const tagihan = getByType('tagihan');
const kec = currentUser?.kecamatan || '';
return `
<h2 class="text-2xl font-bold mb-2">Berdata 📊</h2>
<p class="text-sm text-gray-500 mb-6">Kecamatan: <strong>${kec}</strong></p>
<div class="space-y-4">
${tagihan.length===0?'<div class="bg-white rounded-2xl p-8 shadow-sm text-center text-gray-400">Belum ada tagihan data</div>':
tagihan.map(t=>{
const isExpired = t.deadline && new Date(t.deadline) < new Date();
return `
<div class="card-hover bg-white rounded-2xl p-6 shadow-sm border-l-4 ${isExpired?'border-red-400':'border-brand-500'}">
<div class="flex items-start justify-between">
<div class="flex-1">
<h3 class="font-bold text-lg">${t.title}</h3>
<p class="text-sm text-gray-500 mt-1">${t.description||'Tidak ada deskripsi'}</p>
<div class="flex items-center gap-4 mt-3">
<span class="text-xs px-3 py-1 rounded-full ${isExpired?'bg-red-100 text-red-600':'bg-green-100 text-green-600'} font-medium">
Deadline: ${t.deadline||'Tidak ditentukan'}
</span>
<span class="text-xs px-3 py-1 rounded-full bg-blue-100 text-blue-600 font-medium">${t.status||'aktif'}</span>
</div>
</div>
${t.link?`<a href="${t.link}" target="_blank" rel="noopener noreferrer" class="px-5 py-2 bg-brand-500 hover:bg-brand-600 text-white rounded-xl text-sm font-semibold transition flex-shrink-0" data-log-berdata="${t.title}">Kerjakan</a>`:''}
</div>
<p class="text-xs text-gray-400 mt-3">💡 Data akan terfilter otomatis untuk kecamatan <strong>${kec}</strong></p>
</div>`;
}).join('')}
</div>`;
}
function renderUserInfo() {
const infos = getByType('info');
return `
<h2 class="text-2xl font-bold mb-6">Info 📢</h2>
<div class="space-y-4">
${infos.length===0?'<div class="bg-white rounded-2xl p-8 shadow-sm text-center text-gray-400">Belum ada info</div>':
infos.map(info=>`
<div class="card-hover bg-white rounded-2xl p-6 shadow-sm border-l-4 border-amber-400">
<div class="flex items-start gap-3">
<div class="text-2xl">🔥</div>
<div>
<h3 class="font-bold">${info.title}</h3>
<p class="text-sm text-gray-600 mt-2">${info.content}</p>
<p class="text-xs text-gray-400 mt-2">${formatDate(info.timestamp)}</p>
</div>
</div>
</div>
`).join('')}
</div>`;
}
function renderUserMasukan() {
const chats = getByType('chat').filter(c =>
(c.sender===currentUser?.username && c.receiver==='admin') || (c.sender==='admin' && c.receiver===currentUser?.username)
).sort((a,b) => new Date(a.timestamp) - new Date(b.timestamp));
// Count unread messages from admin
const unreadCount = chats.filter(c => c.sender === 'admin' && c.receiver === currentUser?.username && !c.read).length;
// Mark messages from admin as read
(async () => {
for (let c of chats) {
if (c.sender === 'admin' && c.receiver === currentUser?.username && !c.read) {
const updated = { ...c, read: true };
await updateRecord(updated);
}
}
})();
return `
<h2 class="text-2xl font-bold mb-6 flex items-center gap-2">Masukan & Diskusi 💬 ${unreadCount > 0 ? `<span class="inline-flex items-center justify-center w-6 h-6 bg-red-500 text-white text-xs font-bold rounded-full">${unreadCount}</span>` : ''}</h2>
<div class="bg-white rounded-2xl shadow-sm overflow-hidden">
<div class="p-4 border-b bg-brand-50">
<div class="font-semibold text-sm flex items-center gap-2">
<div class="relative">
<i data-lucide="message-circle" style="width:16px;height:16px;" class="text-brand-500"></i>
<div class="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full ${isUserOnline('admin') ? 'bg-green-500 animate-pulse' : 'bg-gray-300'}"></div>
</div>
Chat dengan Admin ${isUserOnline('admin') ? '<span class="text-xs ml-auto px-2 py-0.5 bg-green-100 text-green-700 rounded-full font-semibold">Online</span>' : '<span class="text-xs ml-auto px-2 py-0.5 bg-gray-100 text-gray-600 rounded-full">Offline</span>'}
</div>
</div>
<div class="h-72 overflow-auto p-4 space-y-2" id="user-chat-messages">
${chats.length===0?'<p class="text-gray-400 text-sm text-center py-8">Belum ada pesan. Mulai diskusi!</p>':
chats.map(c => c.sender===currentUser?.username ?
`<div class="flex justify-end"><div class="chat-bubble-right bg-brand-500 text-white px-4 py-2 text-sm max-w-xs">${c.message}<div class="text-xs opacity-70 mt-1">${formatDate(c.timestamp)}</div></div></div>` :
`<div class="flex justify-start"><div class="chat-bubble-left bg-gray-100 px-4 py-2 text-sm max-w-xs">${c.message}<div class="text-xs text-gray-400 mt-1">${formatDate(c.timestamp)}</div></div></div>`
).join('')}
</div>
<form id="user-chat-form" class="p-4 border-t flex gap-2">
<input id="user-chat-input" placeholder="Ketik pesan..." class="flex-1 px-4 py-2 border rounded-xl outline-none focus:ring-2 focus:ring-brand-500 text-sm">
<button type="submit" class="px-4 py-2 bg-brand-500 text-white rounded-xl hover:bg-brand-600 transition"><i data-lucide="send" style="width:16px;height:16px;"></i></button>
</form>
</div>`;
}
function renderToast() {
if (!toastMsg) return '';
return `<div class="fixed top-4 right-4 z-50 toast-show bg-brand-900 text-white px-5 py-3 rounded-xl shadow-lg text-sm font-medium">${toastMsg}</div>`;
}
// ==================== EVENTS ====================
function bindEvents() {
// Login
const loginForm = document.getElementById('login-form');
if (loginForm) loginForm.onsubmit = async (e) => {
e.preventDefault();
const u = document.getElementById('login-user').value.trim();
const p = document.getElementById('login-pass').value.trim();
if (!u || !p) return;
// Check admin
if (u === 'admin' && p === 'admin123') {
currentUser = { username: 'admin', role: 'admin', kecamatan: 'ALL' };
currentPage = 'admin';
updateUserOnlineStatus('admin', true);
await createRecord({ type:'activity', username:'admin', kecamatan:'ALL', action:'login', description:'Admin login', timestamp: new Date().toISOString() });
render();
return;
}
// Check users
const users = getByType('user');
const found = users.find(x => x.username === u && x.password === p);
if (found) {
currentUser = found;
currentPage = 'user';
updateUserOnlineStatus(found.username, true);
await createRecord({ type:'activity', username:found.username, kecamatan:found.kecamatan||'', action:'login', description:'User login', timestamp: new Date().toISOString() });
render();
} else {
const err = document.getElementById('login-error');
if (err) { err.textContent = 'Username atau password salah!'; err.classList.remove('hidden'); }
}
};
// Admin nav
document.querySelectorAll('[data-admin-nav]').forEach(btn => {
btn.onclick = () => { adminPage = btn.dataset.adminNav; render(); };
});
// User nav
document.querySelectorAll('[data-user-nav]').forEach(btn => {
btn.onclick = () => { userPage = btn.dataset.userNav; render(); };
});
// Logouts
const al = document.getElementById('admin-logout');
if (al) al.onclick = () => {
updateUserOnlineStatus(currentUser?.username, false);
currentUser = null;
currentPage = 'login';
render();
};
const ul = document.getElementById('user-logout');
if (ul) ul.onclick = () => {
updateUserOnlineStatus(currentUser?.username, false);
currentUser = null;
currentPage = 'login';
render();
};
// Admin forms
const matForm = document.getElementById('form-materi');
if (matForm) matForm.onsubmit = async (e) => {
e.preventDefault();
const btn = matForm.querySelector('button[type=submit]');
btn.disabled = true; btn.textContent = 'Menyimpan...';
await createRecord({
type: 'materi',
title: document.getElementById('mat-title').value,
file_type: document.getElementById('mat-type').value,
category: document.getElementById('mat-category').value,
link: document.getElementById('mat-link').value,
summary: document.getElementById('mat-summary').value,
timestamp: new Date().toISOString()
});
showToast('Materi berhasil disimpan');
};
const tagForm = document.getElementById('form-tagihan');
if (tagForm) tagForm.onsubmit = async (e) => {
e.preventDefault();
const btn = tagForm.querySelector('button[type=submit]');
btn.disabled = true; btn.textContent = 'Menyimpan...';
await createRecord({
type: 'tagihan',
title: document.getElementById('tag-title').value,
link: document.getElementById('tag-link').value,
deadline: document.getElementById('tag-deadline').value,
status: document.getElementById('tag-status').value,
description: document.getElementById('tag-desc').value,
timestamp: new Date().toISOString()
});
showToast('Tagihan berhasil disimpan');
};
const usrForm = document.getElementById('form-user');
if (usrForm) usrForm.onsubmit = async (e) => {
e.preventDefault();
const btn = usrForm.querySelector('button[type=submit]');
btn.disabled = true; btn.textContent = 'Menyimpan...';
await createRecord({
type: 'user',
username: document.getElementById('usr-name').value,
password: document.getElementById('usr-pass').value,
kecamatan: document.getElementById('usr-kec').value,
role: 'user',
timestamp: new Date().toISOString()
});
showToast('Pengguna berhasil ditambahkan');
};
const infoForm = document.getElementById('form-info');
if (infoForm) infoForm.onsubmit = async (e) => {
e.preventDefault();
const btn = infoForm.querySelector('button[type=submit]');
btn.disabled = true; btn.textContent = 'Menyimpan...';
await createRecord({
type: 'info',
title: document.getElementById('info-title').value,
content: document.getElementById('info-content').value,
is_hot: true,
timestamp: new Date().toISOString()
});
showToast('Info berhasil ditambahkan');
};
// Delete buttons
document.querySelectorAll('[data-del-materi]').forEach(btn => {
btn.onclick = async () => {
const rec = allData.find(d => d.__backendId === btn.dataset.delMateri);
if (rec) { btn.textContent = '...'; await deleteRecord(rec); showToast('Materi dihapus'); }
};
});
document.querySelectorAll('[data-del-tagihan]').forEach(btn => {
btn.onclick = async () => {
const rec = allData.find(d => d.__backendId === btn.dataset.delTagihan);
if (rec) { await deleteRecord(rec); showToast('Tagihan dihapus'); }
};
});
document.querySelectorAll('[data-del-user]').forEach(btn => {
btn.onclick = async () => {
const rec = allData.find(d => d.__backendId === btn.dataset.delUser);
if (rec) { await deleteRecord(rec); showToast('Pengguna dihapus'); }
};
});
document.querySelectorAll('[data-del-info]').forEach(btn => {
btn.onclick = async () => {
const rec = allData.find(d => d.__backendId === btn.dataset.delInfo);
if (rec) { await deleteRecord(rec); showToast('Info dihapus'); }
};
});
// Template download - REMOVED
// Import Excel
const btnImport = document.getElementById('btn-import');
if (btnImport) {
btnImport.onclick = () => importExcel();
}
// Paste Data
const btnPaste = document.getElementById('btn-paste');
if (btnPaste) {
btnPaste.onclick = () => pasteUserData();
}
// Chat select (admin)
document.querySelectorAll('[data-chat-select]').forEach(btn => {
btn.onclick = () => renderChatWith(btn.dataset.chatSelect);
});
// User chat send
const ucf = document.getElementById('user-chat-form');
if (ucf) ucf.onsubmit = async (e) => {
e.preventDefault();
const input = document.getElementById('user-chat-input');
const msg = input.value.trim();
if (!msg) return;
const btn = ucf.querySelector('button[type=submit]');
btn.disabled = true;
await createRecord({
type: 'chat', sender: currentUser.username, receiver: 'admin', message: msg, timestamp: new Date().toISOString()
});
input.value = '';
btn.disabled = false;
};
// Info bubble toggle
const bubble = document.getElementById('info-bubble');
const popup = document.getElementById('info-popup');
if (bubble && popup) {
bubble.onclick = () => popup.classList.toggle('hidden');
}
// Activity logging for user actions
document.querySelectorAll('[data-log-belajar]').forEach(a => {
a.onclick = () => {
createRecord({ type:'activity', username:currentUser?.username||'', kecamatan:currentUser?.kecamatan||'', action:'belajar', description:'Membuka materi: '+a.dataset.logBelajar, timestamp: new Date().toISOString() });
};
});
document.querySelectorAll('[data-log-berdata]').forEach(a => {
a.onclick = () => {
createRecord({ type:'activity', username:currentUser?.username||'', kecamatan:currentUser?.kecamatan||'', action:'berdata', description:'Mengerjakan tagihan: '+a.dataset.logBerdata, timestamp: new Date().toISOString() });
};
});
// Scroll chat to bottom
const ucm = document.getElementById('user-chat-messages');
if (ucm) ucm.scrollTop = ucm.scrollHeight;
}
// ==================== EXCEL FUNCTIONS ====================
function importExcel() {
const fileInput = document.getElementById('import-excel');
const file = fileInput.files[0];
if (!file) {
showToast('Pilih file Excel terlebih dahulu');
return;
}
const reader = new FileReader();
reader.onload = async (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json(sheet, { defval: '' });
if (rows.length === 0) {
showToast('File Excel kosong');
return;
}
// Normalize header names (handle case variations)
const normalizedRows = rows.map(row => {
const normalized = {};
for (const [key, value] of Object.entries(row)) {
const normalizedKey = key.toLowerCase().trim();
if (normalizedKey.includes('username')) normalized.username = value;
if (normalizedKey.includes('password')) normalized.password = value;
if (normalizedKey.includes('kecamatan')) normalized.kecamatan = value;
}
return normalized;
});
const status = document.getElementById('import-status');
status.classList.remove('hidden', 'bg-red-50', 'text-red-700', 'border-red-200');
status.classList.add('bg-blue-50', 'text-blue-700', 'border', 'border-blue-200');
status.innerHTML = 'Mengimpor ' + rows.length + ' pengguna...';
let success = 0, failed = 0;
const btnImport = document.getElementById('btn-import');
btnImport.disabled = true;
for (let row of normalizedRows) {
const username = String(row.username || '').trim();
const password = String(row.password || '').trim();
const kecamatan = String(row.kecamatan || '').trim();
if (!username || !password || !kecamatan) {
failed++;
continue;
}
const created = await createRecord({
type: 'user',
username: username,
password: password,
kecamatan: kecamatan,
role: 'user',
timestamp: new Date().toISOString()
});
if (created) success++;
else failed++;
}
btnImport.disabled = false;
status.classList.remove('bg-blue-50', 'text-blue-700', 'border-blue-200');
status.classList.add('bg-emerald-50', 'text-emerald-700', 'border-emerald-200');
status.innerHTML = `✓ Berhasil: ${success} | ✗ Gagal: ${failed}`;
fileInput.value = '';
setTimeout(() => { status.classList.add('hidden'); }, 4000);
} catch (error) {
const status = document.getElementById('import-status');
status.classList.remove('hidden', 'bg-blue-50', 'text-blue-700', 'border-blue-200');
status.classList.add('bg-red-50', 'text-red-700', 'border', 'border-red-200');
status.innerHTML = 'Error: Format file tidak valid. Pastikan kolom: Username, Password, Kecamatan';
document.getElementById('btn-import').disabled = false;
}
};
reader.readAsArrayBuffer(file);
}
function pasteUserData() {
const textarea = document.getElementById('paste-data');
const text = textarea.value.trim();
if (!text) {
showToast('Paste data terlebih dahulu');
return;
}
const lines = text.split('\n').filter(l => l.trim());
const rows = lines.map(line => {
const parts = line.split('\t');
return {
username: (parts[0] || '').trim(),
password: (parts[1] || '').trim(),
kecamatan: (parts[2] || '').trim()
};
}).filter(r => r.username && r.password && r.kecamatan);
if (rows.length === 0) {
showToast('Format data tidak valid. Gunakan: username\\tpassword\\tkecamatan');
return;
}
const status = document.getElementById('paste-status');
status.classList.remove('hidden', 'bg-red-50', 'text-red-700', 'border-red-200');
status.classList.add('bg-blue-50', 'text-blue-700', 'border', 'border-blue-200');
status.innerHTML = 'Memproses ' + rows.length + ' baris...';
const btnPaste = document.getElementById('btn-paste');
btnPaste.disabled = true;
(async () => {
let success = 0, failed = 0;
for (let row of rows) {
const created = await createRecord({
type: 'user',
username: row.username,
password: row.password,
kecamatan: row.kecamatan,
role: 'user',
timestamp: new Date().toISOString()
});
if (created) success++;
else failed++;
}
btnPaste.disabled = false;
status.classList.remove('bg-blue-50', 'text-blue-700', 'border-blue-200');
status.classList.add('bg-emerald-50', 'text-emerald-700', 'border-emerald-200');
status.innerHTML = `✓ Berhasil: ${success} | ✗ Gagal: ${failed}`;
textarea.value = '';
setTimeout(() => { status.classList.add('hidden'); }, 4000);
})();
}
// Init
initApp();
</script>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'9e6d646cc2244536',t:'MTc3NTI3NTA4Mi4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>