SIPESAN Pro - Cloud Edition @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap'); :root { --app-primary: #4f46e5; --app-bg: #f7f9fa; --text-main: #1c1c1c; --text-sub: #677489; } * { -webkit-tap-highlight-color: transparent; box-sizing: border-box; } html, body { height: 100%; margin: 0; padding: 0; background-color: #f0f2f5; font-family: 'Plus Jakarta Sans', sans-serif; color: var(--text-main); overflow: hidden; } body { display: flex; justify-content: center; } .desktop-sidebar { width: 280px; background: white; height: 100vh; border-right: 1px solid #edf2f7; padding: 32px 24px; display: none; flex-direction: column; flex-shrink: 0; } @media (min-width: 1024px) { .desktop-sidebar { display: flex; } } .side-link { display: flex; align-items: center; padding: 14px 18px; border-radius: 16px; color: var(--text-sub); font-weight: 600; transition: all 0.3s ease; margin-bottom: 8px; gap: 12px; cursor: pointer; } .side-link.active { background: var(--app-primary); color: white; box-shadow: 0 10px 20px rgba(79, 70, 229, 0.2); } .app-shell { width: 100%; max-width: 480px; height: 100vh; background-color: var(--app-bg); position: relative; display: flex; flex-direction: column; box-shadow: 0 0 100px rgba(0,0,0,0.05); } header { background: white; padding: 24px 24px 16px; border-bottom: 1px solid #f1f4f9; flex-shrink: 0; z-index: 50; } .scroll-area { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 20px 20px 120px; -webkit-overflow-scrolling: touch; } .bottom-nav { flex-shrink: 0; height: 85px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); display: flex; justify-content: space-around; align-items: center; padding-bottom: env(safe-area-inset-bottom); border-top: 1px solid #f1f4f9; z-index: 100; } .nav-btn { display: flex; flex-direction: column; align-items: center; color: #94a3b8; font-size: 10px; font-weight: 700; gap: 6px; cursor: pointer; padding: 10px; flex: 1; } .nav-btn.active { color: var(--app-primary); } .fab-main { width: 56px; height: 56px; background: var(--app-primary); border-radius: 18px; display: flex; align-items: center; justify-content: center; color: white; font-size: 22px; margin-top: -40px; box-shadow: 0 12px 24px rgba(79, 70, 229, 0.3); border: 4px solid var(--app-bg); } .app-card { background: white; border-radius: 24px; padding: 20px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.02); margin-bottom: 16px; } .inner-tabs { display: flex; background: #f1f5f9; padding: 4px; border-radius: 16px; margin-bottom: 20px; } .inner-tab-btn { flex: 1; padding: 10px; text-align: center; font-size: 11px; font-weight: 700; color: #94a3b8; border-radius: 12px; cursor: pointer; } .inner-tab-btn.active { background: white; color: var(--app-primary); box-shadow: 0 2px 6px rgba(0,0,0,0.05); } .form-input { width: 100%; background: #f8fafc; border: 2px solid #f1f5f9; border-radius: 16px; padding: 14px 16px; font-size: 14px; font-weight: 600; outline: none; transition: border-color 0.2s; } .form-input:focus { border-color: var(--app-primary); } .auth-container { position: fixed; inset: 0; background: #f8fafc; z-index: 2000; display: flex; align-items: center; justify-content: center; padding: 20px; } .hidden { display: none !important; } #ui-loading { position: fixed; inset: 0; z-index: 3000; background: white; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: opacity 0.5s ease; } .btn-wa { background: #25D366; color: white; padding: 6px 12px; border-radius: 10px; font-size: 10px; font-weight: 800; display: inline-flex; align-items: center; gap: 6px; } .santri-selector-list { max-height: 180px; overflow-y: auto; border: 2px solid #f1f5f9; border-radius: 16px; padding: 10px; background: #f8fafc; } .santri-item-check { display: flex; align-items: center; gap: 10px; padding: 8px; border-radius: 8px; cursor: pointer; transition: background 0.2s; } .santri-item-check:hover { background: #f1f5f9; } .santri-item-check input { width: 18px; height: 18px; accent-color: var(--app-primary); }

Sinkronisasi Cloud...

import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js"; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js"; import { getFirestore, doc, setDoc, getDoc, collection, onSnapshot, addDoc, updateDoc, deleteDoc, query } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js"; const firebaseConfig = JSON.parse(__firebase_config); const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'sipesan-cloud-v4'; let masterSantri = []; let izinData = []; let gateMode = 'EXIT'; let currentUserRole = null; async function initAuth() { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (err) { console.error("Auth Error:", err); } } onAuthStateChanged(auth, (user) => { document.getElementById('ui-loading').classList.add('hidden'); if (user) { if (!currentUserRole) document.getElementById('view-auth').classList.remove('hidden'); startSync(); } else { initAuth(); } }); function startSync() { const santriRef = collection(db, 'artifacts', appId, 'public', 'data', 'master_santri'); const izinRef = collection(db, 'artifacts', appId, 'public', 'data', 'izin_santri'); onSnapshot(santriRef, (snap) => { masterSantri = snap.docs.map(d => ({ id: d.id, ...d.data() })); renderAll(); updateClassFilters(); }, (err) => console.error(err)); onSnapshot(izinRef, (snap) => { izinData = snap.docs.map(d => ({ id: d.id, ...d.data() })); renderAll(); }, (err) => console.error(err)); } window.showNotif = (txt) => { const n = document.getElementById('notif'); document.getElementById('notif-text').innerText = txt; n.classList.remove('hidden'); setTimeout(() => n.classList.add('hidden'), 2500); }; window.handleLoginSubmit = (role) => { currentUserRole = role; document.getElementById('view-auth').classList.add('hidden'); document.getElementById('main-app').classList.remove('hidden'); const isSatpam = role === 'SATPAM'; document.getElementById('side-data').classList.toggle('hidden', isSatpam); document.getElementById('nav-data').classList.toggle('hidden', isSatpam); document.getElementById('nav-fab').classList.toggle('hidden', isSatpam); document.getElementById('user-role-badge').innerText = role; changeTab('home', 'Dashboard'); }; window.handleLogout = () => { currentUserRole = null; document.getElementById('main-app').classList.add('hidden'); document.getElementById('view-auth').classList.remove('hidden'); }; window.changeTab = (id, title) => { if (currentUserRole === 'SATPAM' && id === 'data') return; document.querySelectorAll('.tab-pane').forEach(p => p.classList.add('hidden')); document.getElementById('view-' + id).classList.remove('hidden'); document.getElementById('tab-title').innerText = title; document.querySelectorAll('.nav-btn, .side-link').forEach(el => el.classList.remove('active')); document.getElementById('nav-' + id)?.classList.add('active'); document.getElementById('side-' + id)?.classList.add('active'); renderAll(); }; window.openAddSantriModal = () => document.getElementById('modal-add-santri').classList.remove('hidden'); window.openBulkUpload = () => document.getElementById('modal-bulk').classList.remove('hidden'); window.closeModal = (id) => document.getElementById(id).classList.add('hidden'); window.processBulkUpload = async () => { const text = document.getElementById('bulk-data').value; const lines = text.split('\n').filter(l => l.trim().includes(',')); if(lines.length === 0) return showNotif("Format salah"); try { for (const line of lines) { const [nama, kelas] = line.split(',').map(s => s.trim()); if(nama && kelas) { await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'master_santri'), { nama, kelas, wa: '628', createdAt: new Date().toISOString() }); } } document.getElementById('bulk-data').value = ''; closeModal('modal-bulk'); showNotif(`${lines.length} data master diproses`); } catch (e) { showNotif("Gagal upload."); } }; window.saveSantri = async () => { const nama = document.getElementById('add-nama').value; const kelas = document.getElementById('add-kelas').value; const wa = document.getElementById('add-wa').value; if(!nama || !kelas) return showNotif("Data wajib diisi"); try { await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'master_santri'), { nama, kelas, wa: wa || '628', createdAt: new Date().toISOString() }); closeModal('modal-add-santri'); showNotif("Santri terdaftar"); } catch (e) { showNotif("Gagal simpan."); } }; window.updateClassFilters = () => { const classes = [...new Set(masterSantri.map(s => s.kelas))].sort(); const filter = document.getElementById('filter-kelas-izin'); const current = filter.value; filter.innerHTML = 'Semua Kelas' + classes.map(c => `${c}`).join(''); }; window.openIzinModal = () => { document.getElementById('filter-kelas-izin').value = ""; updateIzinSantriList(); document.getElementById('modal-izin').classList.remove('hidden'); }; window.updateIzinSantriList = () => { const selectedClass = document.getElementById('filter-kelas-izin').value; const listEl = document.getElementById('form-santri-list'); const santriDiLuarIds = izinData.map(i => i.santriId); let filtered = masterSantri.filter(s => !santriDiLuarIds.includes(s.id)); if (selectedClass) filtered = filtered.filter(s => s.kelas === selectedClass); filtered.sort((a,b) => a.nama.localeCompare(b.nama)); if (filtered.length === 0) { listEl.innerHTML = '

Tidak ada santri tersedia

'; return; } listEl.innerHTML = filtered.map(s => ` `).join(''); }; window.submitIzin = async () => { const checkboxes = document.querySelectorAll('input[name="santri-select"]:checked'); const selectedIds = Array.from(checkboxes).map(cb => cb.value); const tgl = document.getElementById('form-tgl').value; const jam = document.getElementById('form-jam').value; const alasan = document.getElementById('form-alasan').value; if(selectedIds.length === 0 || !tgl || !jam || !alasan) { return showNotif("Lengkapi data dan pilih santri"); } try { for (const sId of selectedIds) { const s = masterSantri.find(x => x.id === sId); if (s) { await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'izin_santri'), { santriId: s.id, nama: s.nama, kelas: s.kelas, wa: s.wa, alasan, deadline: `${tgl} ${jam}`, status: 'WAITING_EXIT', createdAt: new Date().toISOString() }); } } closeModal('modal-izin'); showNotif(`${selectedIds.length} Izin berhasil diproses`); document.getElementById('form-alasan').value = ""; document.getElementById('form-tgl').value = ""; document.getElementById('form-jam').value = ""; } catch (e) { showNotif("Gagal memproses izin."); } }; window.confirmGate = async (id, type) => { const ref = doc(db, 'artifacts', appId, 'public', 'data', 'izin_santri', id); try { if(type === 'OUT') { await updateDoc(ref, { status: 'OUTSIDE', exitAt: new Date().toISOString() }); } else { await deleteDoc(ref); } showNotif("Berhasil diperbarui"); } catch (e) { showNotif("Gagal."); } }; window.setGateMode = (mode) => { gateMode = mode; document.getElementById('gate-tab-exit').classList.toggle('active', mode === 'EXIT'); document.getElementById('gate-tab-entry').classList.toggle('active', mode === 'ENTRY'); renderAll(); }; window.sendWA = (nama, wa) => { const msg = window.encodeURIComponent(`Assalamu'alaikum ayah/bunda... mohon informasi apakah mas ${nama} saat ini masih di rumah? Karena sudah melewati batas jam kembali ke pondok.`); const target = wa && wa.startsWith('62') ? wa : '628'; window.open(`https://wa.me/${target}?text=${msg}`, '_blank'); }; function renderAll() { const search = document.getElementById('search-santri')?.value.toLowerCase() || ''; const now = new Date(); const filteredSantri = masterSantri.filter(s => s.nama.toLowerCase().includes(search)); const outside = izinData.filter(i => i.status === 'OUTSIDE'); const late = outside.filter(i => new Date(i.deadline) { const isLate = new Date(i.deadline) < now; return `

${i.nama}

Deadline: ${i.deadline.split(' ')[1]}

${i.kelas}
${isLate ? `` : ''}
`; }).join('') || '

Tidak ada santri di luar

'; } const santriListEl = document.getElementById('santri-list'); if(santriListEl) { santriListEl.innerHTML = filteredSantri.map(s => `
${s.nama[0]}

${s.nama}

${s.kelas}

`).join('') || '

Data Kosong

'; } const gateEl = document.getElementById('gate-queue'); if(gateEl) { const q = izinData.filter(i => gateMode === 'EXIT' ? i.status === 'WAITING_EXIT' : i.status === 'OUTSIDE'); gateEl.innerHTML = q.map(i => `

${i.nama}

${i.kelas} • ${i.alasan}

${i.deadline.split(' ')[1]}

DEADLINE

`).join('') || `

Antrean Kosong

`; } }
Scroll to Top