const { useState, useEffect, useRef } = React; const Icon = ({ name, size = 24, className = "", strokeWidth = 2 }) => { const spanRef = useRef(null); useEffect(() => { if (spanRef.current && window.lucide) { spanRef.current.innerHTML = ``; window.lucide.createIcons({ nameAttr: 'data-lucide', attrs: { class: className, width: size, height: size, 'stroke-width': strokeWidth }, root: spanRef.current }); } }, [name, size, className, strokeWidth]); return ; }; const createIcon = (name) => (props) => ; const Sun = createIcon("sun"); const Heart = createIcon("heart"); const Zap = createIcon("zap"); const ShoppingCart = createIcon("shopping-cart"); const BookOpen = createIcon("book-open"); const Users = createIcon("users"); const Moon = createIcon("moon"); const CloudUpload = createIcon("cloud-upload"); const Calendar = createIcon("calendar"); const ChevronLeft = createIcon("chevron-left"); const User = createIcon("user"); const Clock = createIcon("clock"); const CheckCircle = createIcon("check-circle"); const X = createIcon("x"); const Camera = createIcon("camera"); const Plus = createIcon("plus"); const Trash2 = createIcon("trash-2"); const ImageIcon = createIcon("image"); const Award = createIcon("award"); const Shield = createIcon("shield"); const UserCheck = createIcon("user-check"); const SquarePen = createIcon("square-pen"); const LogOut = createIcon("log-out"); const Lock = createIcon("lock"); const ArrowLeft = createIcon("arrow-left"); const LayoutGrid = createIcon("layout-grid"); const GraduationCap = createIcon("graduation-cap"); const ActivityIcon = createIcon("activity"); const RefreshCw = createIcon("refresh-cw"); const Search = createIcon("search"); const Edit = createIcon("edit"); const Eye = createIcon("eye"); const EyeOff = createIcon("eye-off"); const HistoryIcon = createIcon("history"); const AlertTriangle = createIcon("alert-triangle"); const Save = createIcon("save"); const Info = createIcon("info"); const Download = createIcon("download"); const Sparkles = createIcon("sparkles"); const Printer = createIcon("printer"); const FileText = createIcon("file-text"); const KeyRound = createIcon("key-round"); const Loader2 = ({ className="", ...props }) => ; const INITIAL_SISWA_DATABASE = [{ nisn: "001", nama: "Ahmad Fauzi", kelas: "VII-A", nipGuru: "19800101" }]; const INITIAL_GURU_DATABASE = [{ nip: "19800101", nama: "Risky, S.Pd." }]; const getTodayDate = () => { const witaTime = new Date().toLocaleString("en-US", { timeZone: "Asia/Makassar" }); const d = new Date(witaTime); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; // Ubah "2026-08-01" menjadi "Sabtu, 1 Agustus 2026" (format tanggal Indonesia) const formatTanggalIndonesia = (tanggalStr) => { if (!tanggalStr) return ''; const HARI = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu']; const BULAN = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember']; const [y, m, d] = tanggalStr.split('-').map(Number); const dateObj = new Date(y, m - 1, d); return `${HARI[dateObj.getDay()]}, ${d} ${BULAN[m - 1]} ${y}`; }; const safeGet = (key, fallback = null) => { try { return localStorage.getItem(key) ?? fallback; } catch (e) { return fallback; } }; const safeParse = (key, fallback) => { try { const item = safeGet(key); if (item === "undefined" || item === "null" || !item) return fallback; return JSON.parse(item) || fallback; } catch (e) { return fallback; } }; // Penyimpanan lokal bersifat cache saja. Quota penuh atau mode private // tidak boleh menghentikan React saat menyimpan draf. const safeSet = (key, value) => { try { localStorage.setItem(key, value); } catch (e) { console.warn('Penyimpanan lokal tidak tersedia:', key); } }; // KOMPONEN: Latar Belakang Khusus Halaman Login const LoginBackground = () => (
{/* Soft Colorful Glow Blobs (Gradasi Lembut) */}
{/* Playful Dotted Pattern */}
{/* Floating Icons (Mewakili 7 Kebiasaan) */} {/* 1. Bangun Pagi */}
{/* 2. Beribadah */}
{/* 3. Olahraga */}
{/* 4. Gemar Belajar */}
{/* 5. Bermasyarakat */}
{/* 6. Tidur Cepat */}
{/* Decorative Geometric Elements */}
); // KOMPONEN: Latar Belakang Dalam Aplikasi (Lebih Halus) const MagicalBackground = () => (
); // ========================================== // KOMPONEN KARTU MAINAN (TOY BLOCK) // ========================================== const HabitCard = ({ num, title, icon: IconComp, bgClass, borderClass, textDark = false, isEvaluated, onClick, badgeText }) => { const textColor = textDark ? 'text-slate-800' : 'text-white'; let badgeStyle = isEvaluated ? 'bg-white text-slate-800' : (textDark ? 'bg-white/80 text-slate-800' : 'bg-white/30 text-white'); if (badgeText?.includes('Perlu Dinilai')) badgeStyle = 'bg-rose-500 text-white animate-pulse shadow-md border-2 border-white'; if (badgeText?.includes('Menunggu Guru')) badgeStyle = 'bg-white/90 text-slate-700 shadow-sm'; return (

Kebiasaan {num}

{title}

{badgeText?.includes('Membudaya') && '🌟 '} {badgeText?.includes('Mulai Terbentuk') && '👍 '} {badgeText?.includes('Belum Terlihat') && '⏳ '} {badgeText || (isEvaluated ? 'Selesai' : 'Belum')}
); }; const PlayfulHeader = ({ title, subtitle, badgeText, onRefresh, isLoading, isLocked, role = 'student' }) => (
{role === 'student' ? : }

{title}

{subtitle} {badgeText && {badgeText}}
{onRefresh && ( )} {isLocked && role === 'student' && (

Jurnal Terkirim (Hanya Baca)

)}
); const Modal = ({ isOpen, onClose, title, icon: IconComp, colorClass, children }) => { if (!isOpen) return null; return (
{IconComp && }

{title}

{children}
); }; const FormGroup = ({ label, children }) => (
{children}
); const AppCredit = ({ light = false }) => (

SMP Negeri 34 Samarinda

); function App() { const [tanggalHariIni, setTanggalHariIni] = useState(getTodayDate()); const [siswaAktif, setSiswaAktif] = useState(() => safeParse('siswaAktif', null)); const [guruAktif, setGuruAktif] = useState(() => safeParse('guruAktif', null)); const [adminAktif, setAdminAktif] = useState(null); const [dataSiswaLokal, setDataSiswaLokal] = useState(() => safeParse('dataSiswaLokal', INITIAL_SISWA_DATABASE)); const [dataGuruLokal, setDataGuruLokal] = useState(() => safeParse('dataGuruLokal', INITIAL_GURU_DATABASE)); useEffect(() => { safeSet('siswaAktif', JSON.stringify(siswaAktif)); }, [siswaAktif]); useEffect(() => { safeSet('guruAktif', JSON.stringify(guruAktif)); }, [guruAktif]); useEffect(() => { try { localStorage.removeItem('adminAktif'); } catch (e) {} }, []); useEffect(() => { safeSet('dataSiswaLokal', JSON.stringify(dataSiswaLokal || [])); }, [dataSiswaLokal]); useEffect(() => { safeSet('dataGuruLokal', JSON.stringify(dataGuruLokal || [])); }, [dataGuruLokal]); // --- DRAF TANGGAPAN GURU (anti hilang saat refresh) --- const DRAFT_TANGGAPAN_PREFIX = 'draftTanggapanGuru_'; const getDraftTanggapanKey = (nisn, tanggal) => `${DRAFT_TANGGAPAN_PREFIX}${nisn}_${tanggal}`; const isEvaluasiTempKosong = (data) => !data || Object.values(data).every(v => !v?.apresiasi && !v?.catatan); const saveDraftTanggapan = (nisn, tanggal, data) => { if (!nisn || !tanggal) return; safeSet(getDraftTanggapanKey(nisn, tanggal), JSON.stringify(data)); }; const loadDraftTanggapan = (nisn, tanggal) => { try { const raw = localStorage.getItem(getDraftTanggapanKey(nisn, tanggal)); return raw ? JSON.parse(raw) : null; } catch (e) { return null; } }; const clearDraftTanggapan = (nisn, tanggal) => { try { localStorage.removeItem(getDraftTanggapanKey(nisn, tanggal)); } catch (e) {} }; const [loginTab, setLoginTab] = useState('siswa'); const [showAdminPassword, setShowAdminPassword] = useState(false); const [showLoginPassword, setShowLoginPassword] = useState(false); const [isAdminMode, setIsAdminMode] = useState(false); const [secretTapCount, setSecretTapCount] = useState(0); const secretTapTimerRef = useRef(null); const handleSecretLogoTap = () => { if (isAdminMode) return; setSecretTapCount(prev => { const next = prev + 1; if (secretTapTimerRef.current) clearTimeout(secretTapTimerRef.current); if (next >= 5) { setIsAdminMode(true); showNotification("Mode Admin diaktifkan", "success"); return 0; } secretTapTimerRef.current = setTimeout(() => setSecretTapCount(0), 1500); return next; }); }; const [isVerifying, setIsVerifying] = useState(false); const [isLoading, setIsLoading] = useState(false); const [toast, setToast] = useState({ isOpen: false, text: '', type: 'info' }); const [inputNisn, setInputNisn] = useState(''); const [inputSiswaPassword, setInputSiswaPassword] = useState(''); const [guruNip, setGuruNip] = useState(''); const [guruPassword, setGuruPassword] = useState(''); const [adminUsername, setAdminUsername] = useState(''); const [adminPassword, setAdminPassword] = useState(''); const [showGantiPasswordModal, setShowGantiPasswordModal] = useState(false); const [gantiPwLama, setGantiPwLama] = useState(''); const [gantiPwBaru, setGantiPwBaru] = useState(''); const [gantiPwKonfirmasi, setGantiPwKonfirmasi] = useState(''); const [showGantiPwFields, setShowGantiPwFields] = useState(false); const [isGantiPwLoading, setIsGantiPwLoading] = useState(false); const [penilaianData, setPenilaianData] = useState({ 1: null, 2: [], 3: null, 4: null, 5: null, 6: null, 7: null }); const [isJurnalTerkirim, setIsJurnalTerkirim] = useState(false); const [showSiswaRiwayat, setShowSiswaRiwayat] = useState(false); const [activeModal, setActiveModal] = useState(null); const [resetNotif, setResetNotif] = useState(false); const [formTemp, setFormTemp] = useState({}); // State Beribadah const [ibadahInfo, setIbadahInfo] = useState({ agama: 'Islam', rutinitas: [], haid: false }); const [ibadahTemp, setIbadahTemp] = useState({ tanggal: tanggalHariIni, jenisIbadah: 'Subuh', jam: '', fotoPreview: null }); // State Makan Sehat (Sarapan / Makan Siang / Makan Malam) — mengikuti pola yang sama // seperti Beribadah: siswa menambah tiap waktu makan satu per satu, tersimpan sbg draf // di perangkat (localStorage) sebelum jurnal harian dikirim. const waktuMakanUrutan = ['Sarapan', 'Makan Siang', 'Makan Malam']; const [makananInfo, setMakananInfo] = useState({ rutinitas: [] }); const [makananTemp, setMakananTemp] = useState({ tanggal: tanggalHariIni, waktuMakan: 'Sarapan', namaMakanan: '', jam: '' }); const [siswaBimbingan, setSiswaBimbingan] = useState([]); const [selectedSiswaNisn, setSelectedSiswaNisn] = useState(() => safeParse('guruSelectedSiswaNisn', '')); // Status pengisian jurnal hari ini per siswa bimbingan (untuk badge & notifikasi guru wali) const [statusJurnalHariIni, setStatusJurnalHariIni] = useState({}); // { nisn: {sudahMengisi, waktuIsi} } const [guruViewMode, setGuruViewMode] = useState(() => safeParse('guruViewModeSaved', 'selection')); const [activeGuruModal, setActiveGuruModal] = useState(null); const [studentHabitData, setStudentHabitData] = useState(null); const [guruSemuaJurnal, setGuruSemuaJurnal] = useState({}); const [guruSelectedDate, setGuruSelectedDate] = useState(''); const [isSubmittingTanggapan, setIsSubmittingTanggapan] = useState(false); const [evaluasiTemp, setEvaluasiTemp] = useState({ 1: { apresiasi: '', catatan: '' }, 2: { apresiasi: '', catatan: '' }, 3: { apresiasi: '', catatan: '' }, 4: { apresiasi: '', catatan: '' }, 5: { apresiasi: '', catatan: '' }, 6: { apresiasi: '', catatan: '' }, 7: { apresiasi: '', catatan: '' } }); // State untuk fitur Tanggapan Massal (guru wali memberi tanggapan ke SEMUA siswa // yang sudah mengirim jurnal hari ini dalam satu layar, TAPI tanggapan tiap siswa // bisa berbeda-beda / disesuaikan sendiri satu per satu di dalam layar tsb) const [showTanggapanMassalModal, setShowTanggapanMassalModal] = useState(false); const [massalTanggapanMap, setMassalTanggapanMap] = useState({}); // { nisn: {apresiasi, catatan} } const [isSubmittingMassal, setIsSubmittingMassal] = useState(false); const [massalTanggal, setMassalTanggal] = useState(tanggalHariIni); // tanggal yang sedang dipilih di modal massal const [statusJurnalMassal, setStatusJurnalMassal] = useState({}); // status siswa untuk massalTanggal (terpisah dari status "hari ini") const [isLoadingStatusMassal, setIsLoadingStatusMassal] = useState(false); // Ingat posisi terakhir guru wali (siswa & mode tampilan) supaya tidak balik ke halaman pilih siswa saat refresh useEffect(() => { safeSet('guruSelectedSiswaNisn', JSON.stringify(selectedSiswaNisn)); }, [selectedSiswaNisn]); useEffect(() => { safeSet('guruViewModeSaved', JSON.stringify(guruViewMode)); }, [guruViewMode]); // Simpan draf otomatis tiap kali guru wali mengisi/mengubah apresiasi atau catatan useEffect(() => { if (guruViewMode !== 'dashboard' || !selectedSiswaNisn) return; if (isEvaluasiTempKosong(evaluasiTemp)) return; const tglAktif = studentHabitData?._tanggal || guruSelectedDate || tanggalHariIni; saveDraftTanggapan(selectedSiswaNisn, tglAktif, evaluasiTemp); }, [evaluasiTemp, guruViewMode, selectedSiswaNisn, studentHabitData, guruSelectedDate]); const [activeAdminTab, setActiveAdminTab] = useState('dashboard'); const [viewPhotoUrl, setViewPhotoUrl] = useState(null); // Data Kepala Sekolah (sumber utama: database, localStorage hanya cache offline) const [dataKepalaSekolah, setDataKepalaSekolah] = useState(() => safeParse('dataKepalaSekolah', { nama: '', nip: '' })); const [formKepala, setFormKepala] = useState(() => safeParse('dataKepalaSekolah', { nama: '', nip: '' })); const [isSavingKepala, setIsSavingKepala] = useState(false); useEffect(() => { safeSet('dataKepalaSekolah', JSON.stringify(dataKepalaSekolah)); }, [dataKepalaSekolah]); const simpanDataKepala = async () => { if (!formKepala.nama.trim()) { showNotification('Nama kepala sekolah tidak boleh kosong.', 'error'); return; } setIsSavingKepala(true); try { const result = await panggilBackend({ action: "simpanDataKepala", nama: formKepala.nama.trim(), nip: formKepala.nip.trim() }); if (result.status === "success") { const dataBaru = result.data || { nama: formKepala.nama.trim(), nip: formKepala.nip.trim() }; setDataKepalaSekolah(dataBaru); setFormKepala(dataBaru); showNotification('Data Kepala Sekolah berhasil disimpan ke database!', 'success'); } else { showNotification(result.message || 'Gagal menyimpan data Kepala Sekolah.', 'error'); } } catch (e) { showNotification('Gagal terhubung ke server. Periksa koneksi Anda.', 'error'); } finally { setIsSavingKepala(false); } }; const [formSiswa, setFormSiswa] = useState({ nisn: '', nama: '', kelas: '', nipGuru: '', password: '' }); const [isEditingSiswa, setIsEditingSiswa] = useState(false); const [formGuru, setFormGuru] = useState({ nip: '', nama: '', password: '' }); const [isEditingGuru, setIsEditingGuru] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState({ isOpen: false, type: '', id: '', nama: '' }); // Pencarian & Paginasi untuk tabel Data Siswa & Data Guru (Panel Admin) const [siswaTableSearch, setSiswaTableSearch] = useState(''); const [siswaTablePage, setSiswaTablePage] = useState(1); const [guruTableSearch, setGuruTableSearch] = useState(''); const [guruTablePage, setGuruTablePage] = useState(1); const ITEMS_PER_PAGE = 10; const [adminSiswaJurnal, setAdminSiswaJurnal] = useState({}); const [adminSelectedDate, setAdminSelectedDate] = useState(''); const [adminViewSiswa, setAdminViewSiswa] = useState(null); const [adminSearchQuery, setAdminSearchQuery] = useState(''); const [adminClassFilter, setAdminClassFilter] = useState(''); const [activeAdminModal, setActiveAdminModal] = useState(null); // ========================================== // FUNGSI HELPER CETAK LAPORAN // ========================================== const bukaHalamanCetak = (params) => { const fullParams = { ...params, asalUrl: window.location.href, kepalaSekolah: dataKepalaSekolah }; sessionStorage.setItem('laporanParams', JSON.stringify(fullParams)); window.open('cetak_laporan.html', '_blank'); }; const showNotification = (text, type = 'info') => { setToast({ isOpen: true, text, type }); setTimeout(() => setToast(prev => ({ ...prev, isOpen: false })), 4000); }; const getModalConfig = (num) => { const configs = { 1: { title: 'Bangun Pagi', icon: Sun, color: 'bg-[#ffca28]' }, 2: { title: 'Beribadah', icon: Heart, color: 'bg-[#66bb6a]' }, 3: { title: 'Berolahraga', icon: Zap, color: 'bg-[#29b6f6]' }, 4: { title: 'Makan Sehat', icon: ShoppingCart, color: 'bg-[#ef5350]' }, 5: { title: 'Gemar Belajar', icon: BookOpen, color: 'bg-[#ab47bc]' }, 6: { title: 'Bermasyarakat', icon: Users, color: 'bg-[#ffa726]' }, 7: { title: 'Tidur Cepat', icon: Moon, color: 'bg-[#5c6bc0]' } }; return num ? configs[num] : {}; }; const fetchDatabaseOnline = async () => { setIsLoading(true); try { const resultS = await panggilBackend({ action: "getAllSiswa" }); if (resultS.status === "success") setDataSiswaLokal(resultS.data || []); const resultG = await panggilBackend({ action: "getAllGuru" }); if (resultG.status === "success") setDataGuruLokal(resultG.data || []); const resultK = await panggilBackend({ action: "getKepalaSekolah" }); if (resultK.status === "success" && resultK.data) { setDataKepalaSekolah(resultK.data); setFormKepala(resultK.data); } } catch (e) { console.warn("Offline/Error. Mode lokal."); } finally { setIsLoading(false); } }; useEffect(() => { fetchDatabaseOnline(); }, []); useEffect(() => { let masihTerpasang = true; const pulihkanSesiAdmin = async () => { try { const result = await panggilBackend({ action: "adminSession" }); if (masihTerpasang && result.status === "success" && result.authenticated && result.admin) { setAdminAktif(result.admin); } } catch (e) { console.warn("Sesi admin tidak dapat dipulihkan."); } }; pulihkanSesiAdmin(); return () => { masihTerpasang = false; }; }, []); useEffect(() => { const intervalId = setInterval(() => { const aktualDate = getTodayDate(); if (aktualDate !== tanggalHariIni) { setTanggalHariIni(aktualDate); setResetNotif(true); setTimeout(() => setResetNotif(false), 5000); } }, 10000); return () => clearInterval(intervalId); }, [tanggalHariIni]); useEffect(() => { if (siswaAktif) { const statusTerkirim = safeGet(`jurnalTerkirim_${siswaAktif.nisn}_${tanggalHariIni}`) === 'true'; setIsJurnalTerkirim(statusTerkirim); const key = `penilaianData_${siswaAktif.nisn}_${tanggalHariIni}`; const localDraft = safeGet(key); if (localDraft && localDraft !== "undefined") { try { setPenilaianData(JSON.parse(localDraft)); } catch (e) { setPenilaianData({ 1: null, 2: [], 3: null, 4: null, 5: null, 6: null, 7: null }); } } else { setPenilaianData({ 1: null, 2: [], 3: null, 4: null, 5: null, 6: null, 7: null }); } } else { setPenilaianData({ 1: null, 2: [], 3: null, 4: null, 5: null, 6: null, 7: null }); setIsJurnalTerkirim(false); } }, [siswaAktif, tanggalHariIni]); useEffect(() => { if (guruAktif) { const safeDataSiswa = dataSiswaLokal || []; const filtered = safeDataSiswa.filter(s => String(s.nipGuru).trim() === String(guruAktif.nip).trim()); setSiswaBimbingan(filtered); if (filtered.length > 0 && !selectedSiswaNisn) setSelectedSiswaNisn(filtered[0].nisn); } }, [guruAktif, dataSiswaLokal, selectedSiswaNisn]); const handleSiswaLogin = async () => { if (!inputNisn || !inputSiswaPassword) return showNotification("NISN dan Password wajib diisi!", "error"); setIsVerifying(true); const cleanedInput = String(inputNisn).trim(); try { const result = await panggilBackend({ action: "getStudent", nisn: cleanedInput, password: inputSiswaPassword }); if (result.status === "success") { setSiswaAktif(result.data); showNotification("Login Berhasil!", "success"); await handleSiswaRefresh(cleanedInput); } else showNotification(result.message || "Gagal masuk.", "error"); } catch (e) { showNotification("Koneksi gagal.", "error"); } finally { setIsVerifying(false); } }; const handleGuruLogin = async () => { if (!guruNip || !guruPassword) return showNotification("NIP dan Password wajib diisi!", "error"); setIsVerifying(true); const cleanedNip = String(guruNip).trim(); try { const result = await panggilBackend({ action: "getGuru", nip: cleanedNip, password: guruPassword }); if (result.status === "success") { setGuruAktif(result.guru); showNotification("Selamat datang, Guru!", "success"); } else showNotification(result.message || "Gagal masuk.", "error"); } catch (e) { showNotification("Koneksi gagal.", "error"); } finally { setIsVerifying(false); } }; const handleGantiPasswordGuru = async () => { if (!gantiPwLama || !gantiPwBaru || !gantiPwKonfirmasi) return showNotification("Semua kolom wajib diisi!", "error"); if (gantiPwBaru.length < 4) return showNotification("Password baru minimal 4 karakter!", "error"); if (gantiPwBaru !== gantiPwKonfirmasi) return showNotification("Konfirmasi password tidak sama!", "error"); setIsGantiPwLoading(true); try { const result = await panggilBackend({ action: "gantiPasswordGuru", nip: String(guruAktif.nip).trim(), passwordLama: gantiPwLama, passwordBaru: gantiPwBaru }); if (result.status === "success") { showNotification("Password berhasil diubah!", "success"); setShowGantiPasswordModal(false); setGantiPwLama(''); setGantiPwBaru(''); setGantiPwKonfirmasi(''); } else { showNotification(result.message || "Gagal mengubah password.", "error"); } } catch (e) { showNotification("Koneksi gagal.", "error"); } finally { setIsGantiPwLoading(false); } }; const handleAdminLogin = async () => { if (!adminUsername || !adminPassword) return showNotification("Lengkapi Data Login!", "error"); setIsVerifying(true); try { const result = await panggilBackend({ action: "adminLogin", username: adminUsername.trim(), password: adminPassword }); if (result.status === "success" && result.admin) { setAdminAktif(result.admin); setAdminPassword(''); await fetchDatabaseOnline(); showNotification("Akses Admin Diberikan!", "success"); } else { showNotification(result.message || "Username/Password salah!", "error"); } } catch (e) { showNotification("Gagal login.", "error"); } finally { setIsVerifying(false); } }; const handleLogoutAll = async () => { if (adminAktif) { try { await panggilBackend({ action: "adminLogout" }); } catch (e) { console.warn("Sesi admin server tidak dapat dihapus."); } } setSiswaAktif(null); setGuruAktif(null); setAdminAktif(null); setInputNisn(''); setInputSiswaPassword(''); setGuruNip(''); setGuruPassword(''); setSelectedSiswaNisn(''); setGuruViewMode('selection'); setStudentHabitData(null); // Bersihkan state tampilan sementara (foto, modal, riwayat) supaya tidak nyangkut ke sesi/peran berikutnya setViewPhotoUrl(null); setActiveModal(null); setActiveGuruModal(null); setActiveAdminModal(null); setShowSiswaRiwayat(false); setAdminSiswaJurnal({}); setAdminSelectedDate(''); showNotification("Berhasil Keluar."); }; const isFilled = (num, source = studentHabitData) => { const val = source?.[num]; if (num === 2 || num === 4) { if (Array.isArray(val)) return val.length > 0; if (val && typeof val === 'object') return (val.rutinitas || []).length > 0; return false; } return !!val; }; const handleSiswaRefresh = async (nisnOverride) => { const targetNisn = nisnOverride || siswaAktif?.nisn; if (!targetNisn) return; setIsLoading(true); try { const resJData = await panggilBackend({ action: "getJurnalSiswa", nisn: String(targetNisn).trim() }); if (resJData.status === "success" && resJData.data) { if (resJData.data[tanggalHariIni]) { setPenilaianData(resJData.data[tanggalHariIni]); setIsJurnalTerkirim(true); safeSet(`jurnalTerkirim_${targetNisn}_${tanggalHariIni}`, 'true'); safeSet(`penilaianData_${targetNisn}_${tanggalHariIni}`, JSON.stringify(resJData.data[tanggalHariIni])); } else { setIsJurnalTerkirim(false); safeSet(`jurnalTerkirim_${targetNisn}_${tanggalHariIni}`, 'false'); const localDraft = safeGet(`penilaianData_${targetNisn}_${tanggalHariIni}`); if (localDraft && localDraft !== "undefined") { try { setPenilaianData(JSON.parse(localDraft)); } catch (e) { setPenilaianData({ 1: null, 2: [], 3: null, 4: null, 5: null, 6: null, 7: null }); } } else { setPenilaianData({ 1: null, 2: [], 3: null, 4: null, 5: null, 6: null, 7: null }); } } if (!nisnOverride) showNotification("Data berhasil disegarkan!", "success"); } } catch (e) { if (!nisnOverride) showNotification("Gagal menyegarkan data.", "error"); } finally { setIsLoading(false); } }; const openModal = (num) => { if (num === 2) { const loadedData = penilaianData[2]; let parsedData = { agama: 'Islam', rutinitas: [], haid: false }; if (Array.isArray(loadedData)) { parsedData.rutinitas = loadedData.map(item => ({...item, jenisIbadah: item.jenisIbadah || item.sholat})); } else if (loadedData && typeof loadedData === 'object' && loadedData.rutinitas) { parsedData = { agama: 'Islam', rutinitas: [], haid: false, ...loadedData }; } setIbadahInfo(parsedData); // Cari sholat pertama yang tersedia jika agamanya Islam const added = parsedData.rutinitas.map(i => i.jenisIbadah || i.sholat); const firstAvail = ['Subuh', 'Dzuhur', 'Ashar', 'Magrib', 'Isya'].find(s => !added.includes(s)) || ''; setIbadahTemp({ tanggal: tanggalHariIni, jenisIbadah: parsedData.agama === 'Islam' ? firstAvail : '', jam: '', fotoPreview: null }); } else if (num === 4) { const loadedData = penilaianData[4]; let parsedData = { rutinitas: [] }; if (Array.isArray(loadedData)) { parsedData.rutinitas = loadedData; } else if (loadedData && typeof loadedData === 'object') { if (loadedData.rutinitas) { parsedData = { rutinitas: loadedData.rutinitas }; } else if (loadedData.namaMakanan) { // Kompatibilitas data lama (format tunggal sebelum ada Sarapan/Siang/Malam) parsedData.rutinitas = [{ tanggal: loadedData.tanggal || tanggalHariIni, waktuMakan: 'Sarapan', namaMakanan: loadedData.namaMakanan, jam: loadedData.jam || '' }]; } } setMakananInfo(parsedData); const addedWaktu = parsedData.rutinitas.map(i => i.waktuMakan); const firstAvailWaktu = waktuMakanUrutan.find(w => !addedWaktu.includes(w)) || ''; setMakananTemp({ tanggal: tanggalHariIni, waktuMakan: firstAvailWaktu, namaMakanan: '', jam: '' }); } else { setFormTemp(penilaianData[num] || { tanggal: tanggalHariIni }); } setActiveModal(num); }; const addMakananEntry = () => { if (!makananTemp.waktuMakan) return showNotification("Pilih waktu makan terlebih dahulu!", "error"); if (!makananTemp.namaMakanan) return showNotification("Menu makan wajib diisi!", "error"); if (!makananTemp.jam) return showNotification("Jam makan wajib diisi!", "error"); const newRutinitas = [...makananInfo.rutinitas, makananTemp]; setMakananInfo({ ...makananInfo, rutinitas: newRutinitas }); const addedNow = newRutinitas.map(i => i.waktuMakan); const availableNext = waktuMakanUrutan.find(w => !addedNow.includes(w)) || ''; setMakananTemp({ tanggal: tanggalHariIni, waktuMakan: availableNext, namaMakanan: '', jam: '' }); }; const saveMakanan = () => { setPenilaianData(prev => { const updated = { ...prev, 4: makananInfo }; if (siswaAktif) safeSet(`penilaianData_${siswaAktif.nisn}_${tanggalHariIni}`, JSON.stringify(updated)); return updated; }); setActiveModal(null); showNotification("Draf makan disimpan.", "info"); }; const saveForm = (num) => { if (!formTemp.tanggal) return showNotification("Tanggal wajib diisi!", "error"); if (num === 1 || num === 7) { if (!formTemp.jam) return showNotification("Jam wajib diisi!", "error"); if (!formTemp.catatan || formTemp.catatan.trim() === "") return showNotification("Catatan Orang Tua wajib diisi!", "error"); } else if (num === 3) { if (!formTemp.namaOlahraga) return showNotification("Nama Olahraga wajib diisi!", "error"); if (!formTemp.jamMulai || !formTemp.jamSelesai) return showNotification("Jam wajib diisi!", "error"); if (!formTemp.catatan || formTemp.catatan.trim() === "") return showNotification("Catatan Orang Tua wajib diisi!", "error"); } else if (num === 5) { if (!formTemp.jenisBuku) return showNotification("Jenis Bacaan wajib dipilih!", "error"); if (!formTemp.jamMulai || !formTemp.jamSelesai) return showNotification("Jam wajib diisi!", "error"); if (!formTemp.catatan || formTemp.catatan.trim() === "") return showNotification("Catatan Orang Tua wajib diisi!", "error"); } else if (num === 6) { if (!formTemp.refleksi || formTemp.refleksi.trim() === "") return showNotification("Refleksi wajib diisi!", "error"); if (!formTemp.perasaan) return showNotification("Pilih perasaanmu hari ini!", "error"); } setPenilaianData(prev => { const updated = { ...prev, [num]: formTemp }; if (siswaAktif) safeSet(`penilaianData_${siswaAktif.nisn}_${tanggalHariIni}`, JSON.stringify(updated)); return updated; }); setActiveModal(null); showNotification("Tersimpan di perangkat.", "info"); }; const saveIbadah = () => { setPenilaianData(prev => { const updated = { ...prev, 2: ibadahInfo }; if (siswaAktif) safeSet(`penilaianData_${siswaAktif.nisn}_${tanggalHariIni}`, JSON.stringify(updated)); return updated; }); setActiveModal(null); showNotification("Draf ibadah disimpan.", "info"); }; const handleFotoUpload = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => { const img = new Image(); img.src = reader.result; img.onload = () => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const MAX = 800; let { width, height } = img; if (width > height && width > MAX) { height *= MAX / width; width = MAX; } else if (height > MAX) { width *= MAX / height; height = MAX; } canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); setIbadahTemp({ ...ibadahTemp, fotoPreview: canvas.toDataURL('image/jpeg', 0.6) }); }; }; reader.readAsDataURL(file); } }; const addIbadahEntry = () => { if(!ibadahTemp.jam) return showNotification("Jam ibadah wajib diisi!", "error"); if(ibadahInfo.agama !== 'Islam' && !ibadahTemp.jenisIbadah) return showNotification("Nama kegiatan ibadah wajib diisi!", "error"); if(ibadahInfo.agama === 'Islam' && !ibadahTemp.jenisIbadah) return showNotification("Silakan pilih waktu sholat terlebih dahulu!", "error"); const newRutinitas = [...ibadahInfo.rutinitas, ibadahTemp]; setIbadahInfo({...ibadahInfo, rutinitas: newRutinitas}); if (ibadahInfo.agama === 'Islam') { const sholatUrutan = ['Subuh', 'Dzuhur', 'Jumat', 'Ashar', 'Magrib', 'Isya']; const addedNow = newRutinitas.map(i => i.jenisIbadah || i.sholat); const availableNext = sholatUrutan.find(s => !addedNow.includes(s)) || ''; setIbadahTemp({ tanggal: ibadahTemp.tanggal, jenisIbadah: availableNext, jam: '', fotoPreview: null }); } else { setIbadahTemp({ tanggal: ibadahTemp.tanggal, jenisIbadah: '', jam: '', fotoPreview: null }); } }; const kirimJurnal = async () => { setIsLoading(true); try { const result = await panggilBackend({ action: "simpanJurnal", nisn: String(siswaAktif.nisn), namasiswa: siswaAktif.nama, kelas: siswaAktif.kelas, tanggal: tanggalHariIni, data: penilaianData }); if (result.status !== "success") throw new Error(result.message); setIsJurnalTerkirim(true); safeSet(`jurnalTerkirim_${siswaAktif.nisn}_${tanggalHariIni}`, 'true'); showNotification("Jurnal berhasil dikirim!", "success"); } catch (error) { showNotification("Gagal mengirim: " + error.message, "error"); } finally { setIsLoading(false); } }; // --- LOGIKA GURU WALI --- // Cek status pengisian jurnal hari ini untuk semua siswa bimbingan guru aktif. // Dipanggil saat guru login/masuk dashboard, lalu diulang tiap 30 detik (polling) // supaya guru tahu begitu ada siswa yang baru saja mengisi jurnal. const cekStatusJurnalHariIni = async (silent = true) => { if (!guruAktif) return; try { const result = await panggilBackend({ action: "getStatusJurnalHariIni", nipGuru: String(guruAktif.nip).trim(), tanggal: tanggalHariIni }); if (result.status === "success" && Array.isArray(result.data)) { setStatusJurnalHariIni(prev => { const updated = {}; result.data.forEach(row => { updated[row.nisn] = { sudahMengisi: !!row.sudahMengisi, waktuIsi: row.waktuIsi, nama: row.nama, ringkasan: row.ringkasan || '', fotoIbadah: row.fotoIbadah || [] }; // Bandingkan dengan status sebelumnya: kalau siswa baru saja berubah dari // "belum" menjadi "sudah mengisi", beri notifikasi ke guru. const prevRow = prev[row.nisn]; const baruMengisi = row.sudahMengisi && (!prevRow || !prevRow.sudahMengisi); if (baruMengisi && Object.keys(prev).length > 0) { showNotification(`${row.nama} baru saja mengisi jurnal hari ini!`, "success"); if (typeof Notification !== 'undefined' && Notification.permission === 'granted') { try { new Notification("Jurnal Baru Masuk", { body: `${row.nama} (${row.kelas}) sudah mengisi jurnal hari ini.`, icon: './assets/images/icon-192.png' }); } catch (e) { /* abaikan kalau browser tidak mendukung */ } } } }); return updated; }); } } catch (e) { if (!silent) showNotification("Gagal memeriksa status jurnal.", "error"); } }; // Minta izin notifikasi browser saat guru login, lalu mulai polling status jurnal useEffect(() => { if (!guruAktif) { setStatusJurnalHariIni({}); return; } if (typeof Notification !== 'undefined' && Notification.permission === 'default') { Notification.requestPermission(); } cekStatusJurnalHariIni(false); // cek pertama kali langsung saat login const pollId = setInterval(() => cekStatusJurnalHariIni(true), 30000); // lalu tiap 30 detik return () => clearInterval(pollId); }, [guruAktif]); // Ambil status jurnal siswa untuk TANGGAL TERTENTU (dipakai modal Tanggapan Massal // supaya guru bisa menanggapi jurnal di hari lain, bukan cuma hari ini) const cekStatusJurnalUntukTanggal = async (tanggal) => { if (!guruAktif) return; setIsLoadingStatusMassal(true); try { const result = await panggilBackend({ action: "getStatusJurnalHariIni", nipGuru: String(guruAktif.nip).trim(), tanggal }); if (result.status === "success" && Array.isArray(result.data)) { const updated = {}; result.data.forEach(row => { updated[row.nisn] = { sudahMengisi: !!row.sudahMengisi, waktuIsi: row.waktuIsi, nama: row.nama, ringkasan: row.ringkasan || '', fotoIbadah: row.fotoIbadah || [], sudahDitanggapi: !!row.sudahDitanggapi }; }); setStatusJurnalMassal(updated); } } catch (e) { showNotification("Gagal memuat status jurnal untuk tanggal ini.", "error"); } finally { setIsLoadingStatusMassal(false); } }; // Kirim tanggapan ke SEMUA siswa yang sudah mengisi jurnal pada tanggal yang DIPILIH. // Setiap siswa memakai apresiasi & catatan MASING-MASING dari massalTanggapanMap // (bisa berbeda-beda per siswa). Tidak menghapus/mengganggu fitur isi satu-per-satu. const jumlahSudahMengisiHariIni = siswaBimbingan.filter(s => statusJurnalHariIni[s.nisn]?.sudahMengisi).length; const daftarSiswaSudahMengisi = siswaBimbingan.filter(s => statusJurnalMassal[s.nisn]?.sudahMengisi && !statusJurnalMassal[s.nisn]?.sudahDitanggapi); const jumlahSudahDitanggapi = siswaBimbingan.filter(s => statusJurnalMassal[s.nisn]?.sudahMengisi && statusJurnalMassal[s.nisn]?.sudahDitanggapi).length; // Buka modal massal: default ke tanggal hari ini, siapkan baris kosong untuk tiap siswa const bukaModalTanggapanMassal = () => { setMassalTanggal(tanggalHariIni); setMassalTanggapanMap({}); cekStatusJurnalUntukTanggal(tanggalHariIni); setShowTanggapanMassalModal(true); }; // Guru ganti tanggal di dalam modal → ambil ulang status siswa untuk tanggal baru const gantiTanggalMassal = (tanggalBaru) => { setMassalTanggal(tanggalBaru); setMassalTanggapanMap({}); cekStatusJurnalUntukTanggal(tanggalBaru); }; const setMassalApresiasiSiswa = (nisn, apresiasi) => { setMassalTanggapanMap(prev => ({ ...prev, [nisn]: { ...(prev[nisn] || { catatan: '' }), apresiasi } })); }; const setMassalCatatanSiswa = (nisn, catatan) => { setMassalTanggapanMap(prev => ({ ...prev, [nisn]: { ...(prev[nisn] || { apresiasi: '' }), catatan } })); }; // Tombol cepat: samakan apresiasi & catatan untuk SEMUA siswa di daftar sekaligus. // Guru tetap bisa mengubah lagi satu-satu sesudahnya kalau ada yang mau dibedakan. const terapkanKeSemuaSiswa = (apresiasi) => { setMassalTanggapanMap(prev => { const next = { ...prev }; daftarSiswaSudahMengisi.forEach(s => { next[s.nisn] = { apresiasi, catatan: next[s.nisn]?.catatan || '' }; }); return next; }); }; const jumlahSiapDikirimMassal = daftarSiswaSudahMengisi.filter(s => !!massalTanggapanMap[s.nisn]?.apresiasi).length; const handleKirimTanggapanMassal = async () => { const items = daftarSiswaSudahMengisi .filter(s => !!massalTanggapanMap[s.nisn]?.apresiasi) .map(s => ({ nisn: s.nisn, apresiasi: massalTanggapanMap[s.nisn].apresiasi, catatan: massalTanggapanMap[s.nisn].catatan || '' })); if (items.length === 0) return showNotification("Pilih apresiasi untuk minimal satu siswa!", "error"); setIsSubmittingMassal(true); try { const res = await panggilBackend({ action: "simpanTanggapanMassal", nipGuru: String(guruAktif.nip).trim(), tanggal: massalTanggal, items }); if (res.status === "success") { showNotification(res.message || "Tanggapan terkirim!", "success"); setShowTanggapanMassalModal(false); setMassalTanggapanMap({}); // Jika guru sedang membuka jurnal salah satu siswa, refresh agar tanggapan terbaru terlihat if (guruViewMode === 'dashboard' && selectedSiswaNisn) await handleLihatTanggapanSiswa(); } else { showNotification(res.message || "Gagal mengirim tanggapan.", "error"); } } catch (e) { showNotification("Gagal mengirim: " + e.message, "error"); } finally { setIsSubmittingMassal(false); } }; const handleCetakSiswa = async (siswa, modeCetak) => { setIsLoading(true); try { let allJurnal = {}; const result = await panggilBackend({ action: "getJurnalSiswa", nisn: String(siswa.nisn).trim() }); if (result.status === "success" && result.data) allJurnal = result.data; bukaHalamanCetak({ mode:'guru-satu-siswa', modeCetak, tanggalDipilih: tanggalHariIni, siswa, guru: guruAktif, allJurnal }); } catch (e) { showNotification("Gagal memuat data jurnal.", "error"); } finally { setIsLoading(false); } }; const handleLihatTanggapanSiswa = async (nisnParam) => { const targetNisn = nisnParam ? String(nisnParam).trim() : (selectedSiswaNisn ? String(selectedSiswaNisn).trim() : null); if (!targetNisn) return showNotification("Pilih siswa terlebih dahulu!", "error"); if (nisnParam) setSelectedSiswaNisn(nisnParam); setIsLoading(true); try { const result = await panggilBackend({ action: "getJurnalSiswa", nisn: targetNisn }); if (result.status === "success" && result.data) { setGuruSemuaJurnal(result.data); const dates = Object.keys(result.data).sort().reverse(); const targetDate = dates.length > 0 ? dates[0] : tanggalHariIni; setGuruSelectedDate(targetDate); const latestJurnal = dates.length > 0 ? result.data[targetDate] : {1:null,2:[],3:null,4:null,5:null,6:null,7:null}; latestJurnal._tanggal = targetDate; const evaluasiServer = latestJurnal.evaluasiGuru || { 1:{apresiasi:'',catatan:''}, 2:{apresiasi:'',catatan:''}, 3:{apresiasi:'',catatan:''}, 4:{apresiasi:'',catatan:''}, 5:{apresiasi:'',catatan:''}, 6:{apresiasi:'',catatan:''}, 7:{apresiasi:'',catatan:''} }; const draftTersimpan = loadDraftTanggapan(targetNisn, targetDate); if (draftTersimpan && !isEvaluasiTempKosong(draftTersimpan)) { setEvaluasiTemp(draftTersimpan); showNotification("Draf tanggapan yang belum terkirim berhasil dipulihkan.", "success"); } else { setEvaluasiTemp(evaluasiServer); } setStudentHabitData(latestJurnal); } setGuruViewMode('dashboard'); } catch (e) { showNotification("Gagal memuat jurnal.", "error"); } finally { setIsLoading(false); } }; // Kalau sebelum refresh guru sedang di dashboard menilai siswa tertentu, // begitu halaman dibuka lagi, otomatis masuk lagi ke siswa yang sama // (sekaligus draft tanggapannya ikut dipulihkan oleh handleLihatTanggapanSiswa di atas) const sudahAutoRestoreGuru = useRef(false); useEffect(() => { if (sudahAutoRestoreGuru.current) return; if (guruAktif && guruViewMode === 'dashboard' && selectedSiswaNisn && !studentHabitData) { sudahAutoRestoreGuru.current = true; handleLihatTanggapanSiswa(); } }, [guruAktif, guruViewMode, selectedSiswaNisn, studentHabitData]); const handleGuruDateChange = (date) => { setGuruSelectedDate(date); const targetJurnal = guruSemuaJurnal[date] || {1:null,2:[],3:null,4:null,5:null,6:null,7:null}; targetJurnal._tanggal = date; if (targetJurnal.evaluasiGuru) setEvaluasiTemp(targetJurnal.evaluasiGuru); else setEvaluasiTemp({ 1:{apresiasi:'',catatan:''}, 2:{apresiasi:'',catatan:''}, 3:{apresiasi:'',catatan:''}, 4:{apresiasi:'',catatan:''}, 5:{apresiasi:'',catatan:''}, 6:{apresiasi:'',catatan:''}, 7:{apresiasi:'',catatan:''} }); setStudentHabitData(targetJurnal); }; // --- LOGIKA ADMIN --- const handleAdminRefresh = async () => { await fetchDatabaseOnline(); if (activeAdminTab === 'kegiatan' && adminViewSiswa) await fetchSiswaJurnalForAdmin(adminViewSiswa.nisn); showNotification("Data tersinkronisasi!", "success"); }; const simpanDataSiswa = async () => { if (!formSiswa.nisn || !formSiswa.nama || !formSiswa.kelas || !formSiswa.nipGuru) return showNotification("Semua field (kecuali password) wajib diisi!", "error"); setIsLoading(true); const updatedSiswa = { ...formSiswa, nisn: String(formSiswa.nisn).trim(), nipGuru: String(formSiswa.nipGuru).trim() }; try { const result = await panggilBackend({ action: "simpanDataSiswa", data: updatedSiswa, isEdit: isEditingSiswa }); if (result.status !== "success") throw new Error(result.message); if (isEditingSiswa) { setDataSiswaLokal(prev => (prev || []).map(s => String(s.nisn).trim() === updatedSiswa.nisn ? updatedSiswa : s)); setIsEditingSiswa(false); } else setDataSiswaLokal(prev => [...(prev || []), updatedSiswa]); setFormSiswa({ nisn: '', nama: '', kelas: '', nipGuru: '', password: '' }); showNotification("Data Siswa disimpan", "success"); } catch (err) { showNotification("Gagal menyimpan.", "error"); } finally { setIsLoading(false); } }; const klikEditSiswa = (s) => { setFormSiswa({...s, password: ''}); setIsEditingSiswa(true); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const simpanDataGuru = async () => { if (!formGuru.nip || !formGuru.nama) return showNotification("NIP & Nama wajib diisi!", "error"); setIsLoading(true); const updatedGuru = { ...formGuru, nip: String(formGuru.nip).trim() }; try { const result = await panggilBackend({ action: "simpanDataGuru", data: updatedGuru, isEdit: isEditingGuru }); if (result.status !== "success") throw new Error(result.message); if (isEditingGuru) { setDataGuruLokal(prev => (prev || []).map(g => String(g.nip).trim() === updatedGuru.nip ? updatedGuru : g)); setIsEditingGuru(false); } else setDataGuruLokal(prev => [...(prev || []), updatedGuru]); setFormGuru({ nip: '', nama: '', password: '' }); showNotification("Data Guru disimpan", "success"); } catch (err) { showNotification("Gagal menyimpan.", "error"); } finally { setIsLoading(false); } }; const klikEditGuru = (g) => { setFormGuru({...g, password: ''}); setIsEditingGuru(true); window.scrollTo({ top: 0, behavior: 'smooth' }); }; const downloadFormatSiswa = () => { if (!window.XLSX) return showNotification("Excel belum termuat.", "error"); const ws = window.XLSX.utils.json_to_sheet([{ nisn: "001", nama: "Budi", kelas: "VII-A", nipGuru: "1980", password: "123" }]); const wb = window.XLSX.utils.book_new(); window.XLSX.utils.book_append_sheet(wb, ws, "Siswa"); window.XLSX.writeFile(wb, "Template_Siswa.xlsx"); }; const downloadFormatGuru = () => { if (!window.XLSX) return showNotification("Excel belum termuat.", "error"); const ws = window.XLSX.utils.json_to_sheet([{ nip: "1980", nama: "Guru A", password: "123" }]); const wb = window.XLSX.utils.book_new(); window.XLSX.utils.book_append_sheet(wb, ws, "Guru"); window.XLSX.writeFile(wb, "Template_Guru.xlsx"); }; const handleImportSiswa = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (evt) => { try { const data = evt.target.result; const wb = window.XLSX.read(data, { type: 'binary' }); const rows = window.XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval: "" }); if (rows.length === 0) return showNotification("File kosong!", "error"); setIsLoading(true); let sukses = 0; let gagal = 0; for (let i = 0; i < rows.length; i++) { const row = rows[i]; const keys = Object.keys(row); let nisn = "", nama = "", kelas = "", nipGuru = "", pwd = ""; keys.forEach(k => { const keyL = k.toLowerCase().trim(); if (keyL === 'nisn') nisn = String(row[k]).trim(); else if (keyL === 'nama') nama = String(row[k]).trim(); else if (keyL === 'kelas') kelas = String(row[k]).trim(); else if (keyL.includes('nip')) nipGuru = String(row[k]).trim(); else if (keyL.includes('pass') || keyL.includes('sandi')) pwd = String(row[k]).trim(); }); if (!nisn || !nama || !kelas || !nipGuru) { gagal++; continue; } const password = pwd || '123456'; const dataBaru = { nisn, nama, kelas, nipGuru, password }; try { const res = await panggilBackend({ action: "simpanDataSiswa", data: dataBaru, isEdit: false }); if (res.status === "success") sukses++; else gagal++; } catch (err) { gagal++; } } await fetchDatabaseOnline(); showNotification(`Import Selesai! Sukses: ${sukses}, Gagal: ${gagal}`, sukses > 0 ? "success" : "error"); } catch (e) { showNotification("Gagal membaca file Excel.", "error"); } finally { setIsLoading(false); e.target.value = ""; } }; reader.readAsBinaryString(file); }; const handleImportGuru = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (evt) => { try { const data = evt.target.result; const wb = window.XLSX.read(data, { type: 'binary' }); const rows = window.XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval: "" }); if (rows.length === 0) return showNotification("File kosong!", "error"); setIsLoading(true); let sukses = 0; let gagal = 0; for (let i = 0; i < rows.length; i++) { const row = rows[i]; const keys = Object.keys(row); let nip = "", nama = "", pwd = ""; keys.forEach(k => { const keyL = k.toLowerCase().trim(); if (keyL === 'nip') nip = String(row[k]).trim(); else if (keyL === 'nama') nama = String(row[k]).trim(); else if (keyL.includes('pass') || keyL.includes('sandi')) pwd = String(row[k]).trim(); }); if (!nip || !nama) { gagal++; continue; } const password = pwd || '123456'; const dataBaru = { nip, nama, password }; try { const res = await panggilBackend({ action: "simpanDataGuru", data: dataBaru, isEdit: false }); if (res.status === "success") sukses++; else gagal++; } catch (err) { gagal++; } } await fetchDatabaseOnline(); showNotification(`Import Selesai! Sukses: ${sukses}, Gagal: ${gagal}`, sukses > 0 ? "success" : "error"); } catch (e) { showNotification("Gagal membaca Excel.", "error"); } finally { setIsLoading(false); e.target.value = ""; } }; reader.readAsBinaryString(file); }; const jalankanHapusData = async () => { setIsLoading(true); try { const actionName = deleteConfirm.type === 'siswa' ? "hapusDataSiswa" : "hapusDataGuru"; const payload = deleteConfirm.type === 'siswa' ? { action: actionName, nisn: deleteConfirm.id } : { action: actionName, nip: deleteConfirm.id }; const result = await panggilBackend(payload); if (result.status !== "success") throw new Error(result.message); if (deleteConfirm.type === 'siswa') setDataSiswaLokal(prev => (prev || []).filter(s => s.nisn !== deleteConfirm.id)); else setDataGuruLokal(prev => (prev || []).filter(g => g.nip !== deleteConfirm.id)); showNotification("Data terhapus.", "success"); } catch (err) { showNotification("Gagal menghapus.", "error"); } finally { setIsLoading(false); setDeleteConfirm({ isOpen: false, type: '', id: '', nama: '' }); } }; const fetchSiswaJurnalForAdmin = async (nisn) => { setIsLoading(true); try { const res = await panggilBackend({ action: "getJurnalSiswa", nisn: String(nisn).trim() }); if (res.status === "success" && res.data) { setAdminSiswaJurnal(res.data); const dates = Object.keys(res.data).sort().reverse(); setAdminSelectedDate(dates.length > 0 ? dates[0] : ''); } else { setAdminSiswaJurnal({}); setAdminSelectedDate(''); } } catch (e) { showNotification("Gagal memuat jurnal.", "error"); } finally { setIsLoading(false); } }; // Riwayat jurnal milik siswa sendiri (pakai mekanisme baca read-only yang sama dengan Admin) const bukaRiwayatSiswa = async () => { setActiveAdminModal(null); await fetchSiswaJurnalForAdmin(siswaAktif.nisn); setShowSiswaRiwayat(true); }; const renderFormContent = () => { const commonInputClass = "w-full bg-white border-2 border-slate-200 p-3 rounded-[16px] focus:outline-none focus:border-[#f9a03f] transition-all font-bold text-slate-700 text-[16px] md:text-sm disabled:bg-slate-100 disabled:text-slate-400 disabled:cursor-not-allowed disabled:border-slate-100 disabled:opacity-80"; const evaluasiDariGuru = penilaianData.evaluasiGuru?.[activeModal]; // LOGIKA UNTUK MENONAKTIFKAN WAKTU SHOLAT YANG SUDAH DIPILIH const sholatUrutan = ['Subuh', 'Dzuhur', 'Jumat', 'Ashar', 'Magrib', 'Isya']; const addedSholat = ibadahInfo.rutinitas.map(item => item.jenisIbadah || item.sholat); return (
{isJurnalTerkirim && (
Jurnal terkirim. Hanya bisa dibaca.
)}

TANGGAPAN GURU

{evaluasiDariGuru && evaluasiDariGuru.apresiasi ? (

Status: {evaluasiDariGuru.apresiasi}

{evaluasiDariGuru.catatan &&

"{evaluasiDariGuru.catatan}"

}
) : (
Belum ada tanggapan.
)}
{activeModal === 1 || activeModal === 7 ? ( <> setFormTemp({...formTemp, tanggal: e.target.value})} /> setFormTemp({...formTemp, jam: e.target.value})} /> {!isJurnalTerkirim && } ) : activeModal === 2 ? ( <>
{ibadahInfo.haid ? (

🌸 Sedang Haid / Halangan

Jurnal ibadah hari ini otomatis tercatat sebagai "Haid/Halangan". Tekan Simpan Semua untuk menyimpan.

) : !isJurnalTerkirim && ibadahInfo.rutinitas.length < 6 ? (

Tambah Kegiatan Ibadah

setIbadahTemp({...ibadahTemp, tanggal: e.target.value})} /> {ibadahInfo.agama === 'Islam' ? ( ) : ( setIbadahTemp({...ibadahTemp, jenisIbadah: e.target.value})} /> )}
setIbadahTemp({...ibadahTemp, jam: e.target.value})} />
) : (!isJurnalTerkirim ?
✨ Daftar Ibadah Penuh!
: null)} {!ibadahInfo.haid &&
{ibadahInfo.rutinitas.map((item, idx) => (
{item.fotoPreview ? :
}

{item.jenisIbadah || item.sholat}

{item.tanggal} • {item.jam}

{!isJurnalTerkirim && }
))} {ibadahInfo.rutinitas.length === 0 &&

Belum ada kegiatan ibadah yang ditambahkan.

}
} {!isJurnalTerkirim && } ) : activeModal === 4 ? ( <> {!isJurnalTerkirim && makananInfo.rutinitas.length < 3 ? (

Tambah Waktu Makan

setMakananTemp({...makananTemp, tanggal: e.target.value})} />
setMakananTemp({...makananTemp, namaMakanan: e.target.value})} /> setMakananTemp({...makananTemp, jam: e.target.value})} />
) : (!isJurnalTerkirim ?
✨ Daftar Makan Hari Ini Lengkap!
: null)}
{makananInfo.rutinitas.map((item, idx) => (

{item.waktuMakan}

{item.namaMakanan}

{item.jam}

{!isJurnalTerkirim && }
))} {makananInfo.rutinitas.length === 0 &&

Belum ada waktu makan yang ditambahkan.

}
{!isJurnalTerkirim && } ) : activeModal === 3 || activeModal === 5 ? ( <> setFormTemp({...formTemp, tanggal: e.target.value})} /> {activeModal === 3 && setFormTemp({...formTemp, namaOlahraga: e.target.value})} />} {activeModal === 5 && }
setFormTemp({...formTemp, jamMulai: e.target.value})} /> setFormTemp({...formTemp, jamSelesai: e.target.value})} />
{!isJurnalTerkirim && } ) : activeModal === 6 ? ( <> setFormTemp({...formTemp, tanggal: e.target.value})} />
{[{ id:'senang', emoji:'😄', label:'Senang', col:'bg-[#ffde59]', bor:'border-[#f5c618]' }, { id:'baik', emoji:'🙂', label:'Baik', col:'bg-[#7ed957]', bor:'border-[#5cb835]' }, { id:'kurang', emoji:'😐', label:'Biasa', col:'bg-slate-200', bor:'border-slate-300' }, { id:'sedih', emoji:'😢', label:'Sedih', col:'bg-[#38b6ff]', bor:'border-[#1a9dec]' }, { id:'marah', emoji:'😡', label:'Marah', col:'bg-[#ff5757]', bor:'border-[#e03a3a]' }].map(em => ( ))}
{!isJurnalTerkirim && } ) : null}
); }; const renderGuruModalContent = () => { if (!activeGuruModal) return null; const hab = studentHabitData?.[activeGuruModal]; const hasDataSiswa = isFilled(activeGuruModal); const currentAp = evaluasiTemp[activeGuruModal]?.apresiasi || ''; const currentCat = evaluasiTemp[activeGuruModal]?.catatan || ''; // Penanganan format baru & lama khusus Habit 2 const rutinitas = Array.isArray(hab) ? hab : (hab?.rutinitas || []); const agamaUser = Array.isArray(hab) ? 'Islam' : (hab?.agama || 'Islam'); return (

LAPORAN SISWA:

{!hasDataSiswa ?

Belum ada isian.

: activeGuruModal === 2 ? (
Agama/Kepercayaan: {agamaUser}
{rutinitas.map((sh, sidx) => (
sh.fotoPreview && setViewPhotoUrl(sh.fotoPreview)}>
{sh.fotoPreview ? :
}

{sh.jenisIbadah || sh.sholat}

{sh.jam}

{sh.fotoPreview &&
}
))}
) : activeGuruModal === 4 ? (
{rutinitas.map((mk, midx) => (

{mk.waktuMakan || 'Makan'}

{mk.namaMakanan}

{mk.jam}

))}
) : (
{hab.tanggal &&
Tanggal {hab.tanggal}
} {hab.jam &&
Pukul {hab.jam}
} {hab.namaOlahraga &&
Olahraga {hab.namaOlahraga}
} {hab.jenisBuku &&
Buku {hab.jenisBuku}
} {hab.jamMulai &&
Durasi {hab.jamMulai} - {hab.jamSelesai}
} {hab.refleksi &&
Cerita Anak: "{hab.refleksi}"
} {hab.catatan &&
Catatan Ortu: "{hab.catatan}"
}
)}

Pilih Apresiasi

{['Membudaya', 'Mulai Terbentuk', 'Belum Terlihat'].map(ap => { let apColor = ap === 'Membudaya' ? 'text-[#f9a03f] bg-[#fffcf5] border-[#f9a03f]' : ap === 'Mulai Terbentuk' ? 'text-[#66bb6a] bg-[#f1f8e9] border-[#66bb6a]' : 'text-[#ef5350] bg-[#ffebee] border-[#ef5350]'; return ( ); })}

Pesan Guru (Opsional)