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 && (
{isLoading ? "Memuat..." : "Refresh Data"}
)}
{isLocked && role === 'student' && (
Jurnal Terkirim (Hanya Baca)
)}
);
const Modal = ({ isOpen, onClose, title, icon: IconComp, colorClass, children }) => {
if (!isOpen) return null;
return (
);
};
const FormGroup = ({ label, children }) => (
{label}
{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 &&
saveForm(activeModal)} className="w-full bg-[#f9a03f] border-b-[4px] border-[#d98020] text-white py-3.5 rounded-[20px] font-fredoka text-lg mt-2 shadow-md hover:bg-[#ffb05c] transition-colors active-scale">Simpan Cerita }
>
) : activeModal === 2 ? (
<>
{
const newAgama = e.target.value;
setIbadahInfo({...ibadahInfo, agama: newAgama});
// Cari waktu yang masih kosong saat ganti ke Islam
const currentAdded = ibadahInfo.rutinitas.map(i => i.jenisIbadah || i.sholat);
const firstAvail = sholatUrutan.find(s => !currentAdded.includes(s)) || '';
setIbadahTemp({...ibadahTemp, jenisIbadah: newAgama === 'Islam' ? firstAvail : ''});
}}>
Islam
Kristen Protestan
Katolik
Hindu
Buddha
Konghucu
Lainnya
{
const checked = e.target.checked;
if (checked) {
setIbadahInfo({
...ibadahInfo,
haid: true,
rutinitas: [{ tanggal: tanggalHariIni, jenisIbadah: 'Haid/Halangan', jam: '-', fotoPreview: null }]
});
} else {
setIbadahInfo({ ...ibadahInfo, haid: false, rutinitas: [] });
const firstAvail = sholatUrutan[0];
setIbadahTemp({ tanggal: tanggalHariIni, jenisIbadah: ibadahInfo.agama === 'Islam' ? firstAvail : '', jam: '', fotoPreview: null });
}
}}
/>
Sedang Haid / Halangan (Khusus Siswi — centang jika sedang tidak melaksanakan sholat karena halangan)
{ibadahInfo.haid ? (
🌸 Sedang Haid / Halangan
Jurnal ibadah hari ini otomatis tercatat sebagai "Haid/Halangan". Tekan Simpan Semua untuk menyimpan.
) : !isJurnalTerkirim && ibadahInfo.rutinitas.length < 6 ? (
) : (!isJurnalTerkirim ?
✨ Daftar Ibadah Penuh!
: null)}
{!ibadahInfo.haid &&
{ibadahInfo.rutinitas.map((item, idx) => (
{item.fotoPreview ?
:
}
{item.jenisIbadah || item.sholat}
{item.tanggal} • {item.jam}
{!isJurnalTerkirim &&
{
const newRutinitas = ibadahInfo.rutinitas.filter((_, i) => i !== idx);
setIbadahInfo({...ibadahInfo, rutinitas: newRutinitas});
// Jika yg dihapus membuat jenisIbadah Temp kosong, isi otomatis dgn yg tersedia
if (ibadahInfo.agama === 'Islam') {
const newAdded = newRutinitas.map(i => i.jenisIbadah || i.sholat);
const availableNext = sholatUrutan.find(s => !newAdded.includes(s)) || '';
if(!ibadahTemp.jenisIbadah || newAdded.includes(ibadahTemp.jenisIbadah)) {
setIbadahTemp(prev => ({...prev, jenisIbadah: availableNext}));
}
}
}} className="text-rose-400 bg-rose-50 p-2.5 rounded-full hover:bg-rose-100"> }
))}
{ibadahInfo.rutinitas.length === 0 &&
Belum ada kegiatan ibadah yang ditambahkan.
}
}
{!isJurnalTerkirim &&
Simpan Semua }
>
) : activeModal === 4 ? (
<>
{!isJurnalTerkirim && makananInfo.rutinitas.length < 3 ? (
) : (!isJurnalTerkirim ?
✨ Daftar Makan Hari Ini Lengkap!
: null)}
{makananInfo.rutinitas.map((item, idx) => (
{item.waktuMakan}
{item.namaMakanan}
{item.jam}
{!isJurnalTerkirim &&
{
const newRutinitas = makananInfo.rutinitas.filter((_, i) => i !== idx);
setMakananInfo({...makananInfo, rutinitas: newRutinitas});
const newAdded = newRutinitas.map(i => i.waktuMakan);
const availableNext = waktuMakanUrutan.find(w => !newAdded.includes(w)) || '';
if (!makananTemp.waktuMakan || newAdded.includes(makananTemp.waktuMakan)) {
setMakananTemp(prev => ({...prev, waktuMakan: availableNext}));
}
}} className="text-rose-400 bg-rose-50 p-2.5 rounded-full hover:bg-rose-100"> }
))}
{makananInfo.rutinitas.length === 0 &&
Belum ada waktu makan yang ditambahkan.
}
{!isJurnalTerkirim &&
Simpan Semua }
>
) : activeModal === 3 || activeModal === 5 ? (
<>
setFormTemp({...formTemp, tanggal: e.target.value})} />
{activeModal === 3 &&
setFormTemp({...formTemp, namaOlahraga: e.target.value})} />}
{activeModal === 5 &&
setFormTemp({...formTemp, jenisBuku: e.target.value})}>Pilih Buku... Buku Pelajaran Buku Cerita / Dongeng Bacaan Edukasi Digital }
setFormTemp({...formTemp, jamMulai: e.target.value})} /> setFormTemp({...formTemp, jamSelesai: e.target.value})} />
{!isJurnalTerkirim &&
saveForm(activeModal)} className="w-full bg-[#f9a03f] border-b-[4px] border-[#d98020] text-white py-3.5 rounded-[20px] font-fredoka text-lg mt-2 shadow-md active-scale">Simpan Cerita }
>
) : 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 => (
setFormTemp({...formTemp, perasaan: em.label})} className={`flex flex-col items-center justify-center py-3 rounded-[16px] border-2 transition-all active-scale ${formTemp.perasaan === em.label ? `${em.col} ${em.bor} scale-110 shadow-md` : 'border-slate-100 bg-white'} disabled:opacity-50 disabled:cursor-not-allowed`}>{em.emoji} {em.label}
))}
{!isJurnalTerkirim &&
saveForm(6)} className="w-full bg-[#f9a03f] border-b-[4px] border-[#d98020] text-white py-3.5 rounded-[20px] font-fredoka text-lg mt-4 shadow-md active-scale">Simpan Cerita }
>
) : 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 ?
: 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 (
setEvaluasiTemp(prev => ({...prev, [activeGuruModal]: { ...prev[activeGuruModal], apresiasi: ap }}))} className={`py-3 sm:py-4 border-2 rounded-[20px] text-[10px] sm:text-xs font-black transition-all active-scale flex flex-col items-center gap-1.5 ${currentAp === ap ? `${apColor} shadow-md scale-105` : 'bg-white border-slate-200 text-slate-400'}`}>
{ap === 'Membudaya' ? '🌟' : ap === 'Mulai Terbentuk' ? '👍' : '⏳'} {ap}
);
})}
Pesan Guru (Opsional)
);
};
const renderAdminModalContent = () => {
if (!activeAdminModal || !adminSelectedDate || !adminSiswaJurnal[adminSelectedDate]) return null;
const hab = adminSiswaJurnal[adminSelectedDate]?.[activeAdminModal];
const hasData = isFilled(activeAdminModal, adminSiswaJurnal[adminSelectedDate]);
const evalGuru = adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[activeAdminModal];
// Penanganan format baru & lama khusus Habit 2 (Admin)
const rutinitasAdmin = Array.isArray(hab) ? hab : (hab?.rutinitas || []);
const agamaAdmin = Array.isArray(hab) ? 'Islam' : (hab?.agama || 'Islam');
return (
ISIAN SISWA
{!hasData ?
: activeAdminModal === 2 ? (
Agama: {agamaAdmin}
{rutinitasAdmin.map((sh, sidx) => (
sh.fotoPreview && setViewPhotoUrl(sh.fotoPreview)}>
{sh.fotoPreview ?
:
}
{sh.jenisIbadah || sh.sholat}
{sh.jam}
{sh.fotoPreview &&
}
))}
) : activeAdminModal === 4 ? (
{rutinitasAdmin.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: "{hab.refleksi}"
}
{hab.catatan &&
Ortu: "{hab.catatan}"
}
)}
EVALUASI GURU
{evalGuru && evalGuru.apresiasi ? (
Status: {evalGuru.apresiasi}
{evalGuru.catatan &&
"{evalGuru.catatan}"
}
) :
Belum dinilai
}
);
};
const ToastNotification = () => {
if (!toast.isOpen) return null;
const colorClass = toast.type === 'success' ? 'bg-[#7ed957] text-slate-800 border-[#5cb835]' : toast.type === 'error' ? 'bg-[#ff5757] text-white border-[#e03a3a]' : 'bg-[#2b3990] text-white border-[#1a235c]';
return (
{toast.type === 'success' ?
: toast.type === 'error' ?
:
}
{toast.text}
);
};
const ConfirmDeleteModal = () => {
if (!deleteConfirm.isOpen) return null;
return (
Hapus Data?
Hapus {deleteConfirm.nama} secara permanen?
setDeleteConfirm({ isOpen: false })} className="flex-1 bg-white border-2 border-slate-200 py-3.5 rounded-[20px] font-black text-xs text-slate-600 active-scale hover:bg-slate-50">Batal
Hapus
);
};
const PhotoViewer = () => {
if (!viewPhotoUrl) return null;
return (
setViewPhotoUrl(null)} className="absolute top-4 right-4 text-white bg-white/20 p-2 rounded-full active-scale">
);
};
// === TAMPILAN LOGIN ===
if (!siswaAktif && !guruAktif && !adminAktif) {
return (
{/* Memanggil Latar Belakang Custom CSS & Ikon tanpa Gambar Eksternal */}
{isAdminMode ? (
<>
>
) : (
<>
Jurnal Anak Hebat
SMP Negeri 34 Samarinda
setLoginTab('siswa')} className={`flex-1 py-3 rounded-[18px] font-black text-xs transition-all ${loginTab === 'siswa' ? 'bg-[#2b3990] text-white shadow-md' : 'text-slate-400 hover:text-slate-600'}`}>Siswa
setLoginTab('guru')} className={`flex-1 py-3 rounded-[18px] font-black text-xs transition-all ${loginTab === 'guru' ? 'bg-[#2b3990] text-white shadow-md' : 'text-slate-400 hover:text-slate-600'}`}>Guru Wali
>
)}
{isAdminMode && (
setIsAdminMode(false)} className="text-[11px] font-black text-slate-400 hover:text-[#2b3990] flex items-center gap-1.5 active-scale bg-slate-50 px-4 py-2 rounded-full transition-colors"> Kembali ke Awal
)}
);
}
// === TAMPILAN ADMIN ===
if (adminAktif) {
return (
setActiveAdminModal(null)} title={activeAdminModal ? getModalConfig(activeAdminModal)?.title : ""} icon={CheckCircle} colorClass={activeAdminModal ? getModalConfig(activeAdminModal)?.color : 'bg-[#2b3990]'}>
{renderAdminModalContent()}
{/* Sidebar Desktop Admin */}
{/* Konten Admin */}
{activeAdminTab !== 'kegiatan' || !adminViewSiswa ? (
) : null}
{activeAdminTab === 'dashboard' && (
{dataSiswaLokal?.length || 0} Total Siswa
{dataGuruLokal?.length || 0} Guru Wali
)}
{activeAdminTab === 'siswa' && (
{isEditingSiswa ? 'Ubah Data Siswa' : 'Tambah Siswa Baru'}
{isEditingSiswa && { setIsEditingSiswa(false); setFormSiswa({nisn:'',nama:'',kelas:'',nipGuru:'', password:''}); }} className="bg-slate-100 border-2 border-slate-200 text-slate-600 px-6 py-3 rounded-[20px] font-black text-xs hover:bg-slate-200 active-scale">Batal Edit }
Simpan Data
Unggah Massal (Excel)
Download Template
NISN Nama Lengkap Kelas Guru Wali Tindakan
{(() => {
const q = siswaTableSearch.trim().toLowerCase();
const filtered = (dataSiswaLokal || []).filter(s => !q || String(s.nama).toLowerCase().includes(q) || String(s.nisn).toLowerCase().includes(q) || String(s.kelas).toLowerCase().includes(q));
const totalPages = Math.max(1, Math.ceil(filtered.length / ITEMS_PER_PAGE));
const currentPage = Math.min(siswaTablePage, totalPages);
const startIdx = (currentPage - 1) * ITEMS_PER_PAGE;
const paged = filtered.slice(startIdx, startIdx + ITEMS_PER_PAGE);
return paged.length > 0 ? paged.map((s, i) => {
const namaGuruWali = (dataGuruLokal || []).find(g => String(g.nip) === String(s.nipGuru))?.nama || '-';
return ({s.nisn} {s.nama} {s.kelas} {namaGuruWali} klikEditSiswa(s)} className="text-[#d98020] bg-[#fffcf5] border border-[#ffe0b2] p-2.5 rounded-[12px] hover:bg-[#ffe0b2] transition-colors"> setDeleteConfirm({isOpen:true, type:'siswa', id:s.nisn, nama:s.nama})} className="text-[#ef5350] bg-[#ffebee] border border-[#ffcdd2] p-2.5 rounded-[12px] hover:bg-[#ffcdd2] transition-colors"> );
}) : (Tidak ada data ditemukan. );
})()}
{(() => {
const q = siswaTableSearch.trim().toLowerCase();
const filtered = (dataSiswaLokal || []).filter(s => !q || String(s.nama).toLowerCase().includes(q) || String(s.nisn).toLowerCase().includes(q) || String(s.kelas).toLowerCase().includes(q));
const totalPages = Math.max(1, Math.ceil(filtered.length / ITEMS_PER_PAGE));
const currentPage = Math.min(siswaTablePage, totalPages);
if (filtered.length === 0) return null;
return (
Halaman {currentPage} dari {totalPages} · {filtered.length} data
setSiswaTablePage(p => Math.max(1, p - 1))} className="px-4 py-2 rounded-[14px] font-black text-xs border-2 border-slate-200 text-slate-600 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-50 active-scale">Sebelumnya
= totalPages} onClick={() => setSiswaTablePage(p => Math.min(totalPages, p + 1))} className="px-4 py-2 rounded-[14px] font-black text-xs border-2 border-slate-200 text-slate-600 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-50 active-scale">Berikutnya
);
})()}
)}
{activeAdminTab === 'guru' && (
{isEditingGuru ? 'Ubah Data Guru Wali' : 'Tambah Guru Wali Baru'}
{isEditingGuru && { setIsEditingGuru(false); setFormGuru({nip:'',nama:'', password:''}); }} className="bg-slate-100 border-2 border-slate-200 text-slate-600 px-6 py-3 rounded-[20px] font-black text-xs hover:bg-slate-200 active-scale">Batal Edit }
Simpan Data
Unggah Massal (Excel)
Download Template
NIP Guru Nama Lengkap Tindakan
{(() => {
const q = guruTableSearch.trim().toLowerCase();
const filtered = (dataGuruLokal || []).filter(g => !q || String(g.nama).toLowerCase().includes(q) || String(g.nip).toLowerCase().includes(q));
const totalPages = Math.max(1, Math.ceil(filtered.length / ITEMS_PER_PAGE));
const currentPage = Math.min(guruTablePage, totalPages);
const startIdx = (currentPage - 1) * ITEMS_PER_PAGE;
const paged = filtered.slice(startIdx, startIdx + ITEMS_PER_PAGE);
return paged.length > 0 ? paged.map((g, i) => ({g.nip} {g.nama} klikEditGuru(g)} className="text-[#d98020] bg-[#fffcf5] border border-[#ffe0b2] p-2.5 rounded-[12px] hover:bg-[#ffe0b2] transition-colors"> setDeleteConfirm({isOpen:true, type:'guru', id:g.nip, nama:g.nama})} className="text-[#ef5350] bg-[#ffebee] border border-[#ffcdd2] p-2.5 rounded-[12px] hover:bg-[#ffcdd2] transition-colors"> )) : (Tidak ada data ditemukan. );
})()}
{(() => {
const q = guruTableSearch.trim().toLowerCase();
const filtered = (dataGuruLokal || []).filter(g => !q || String(g.nama).toLowerCase().includes(q) || String(g.nip).toLowerCase().includes(q));
const totalPages = Math.max(1, Math.ceil(filtered.length / ITEMS_PER_PAGE));
const currentPage = Math.min(guruTablePage, totalPages);
if (filtered.length === 0) return null;
return (
Halaman {currentPage} dari {totalPages} · {filtered.length} data
setGuruTablePage(p => Math.max(1, p - 1))} className="px-4 py-2 rounded-[14px] font-black text-xs border-2 border-slate-200 text-slate-600 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-50 active-scale">Sebelumnya
= totalPages} onClick={() => setGuruTablePage(p => Math.min(totalPages, p + 1))} className="px-4 py-2 rounded-[14px] font-black text-xs border-2 border-slate-200 text-slate-600 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-50 active-scale">Berikutnya
);
})()}
)}
{activeAdminTab === 'kegiatan' && (
{!adminViewSiswa ? (
Identitas Siswa Kelas Guru Wali Pantau
{(dataSiswaLokal || []).map(s => ({ ...s, namaGuruWaliPantau: (dataGuruLokal || []).find(g => String(g.nip) === String(s.nipGuru))?.nama || '-' })).filter(s => (s.nama.toLowerCase().includes(adminSearchQuery.toLowerCase()) || s.nisn.includes(adminSearchQuery) || s.namaGuruWaliPantau.toLowerCase().includes(adminSearchQuery.toLowerCase())) && (adminClassFilter === '' || s.kelas === adminClassFilter)).map((s, i) => (
{s.nama} {s.nisn}
{s.kelas}
{s.namaGuruWaliPantau}
{ setAdminViewSiswa(s); fetchSiswaJurnalForAdmin(s.nisn); }} className="bg-[#f9a03f] hover:bg-[#e08b30] border-b-2 border-[#c77220] text-white px-4 py-2.5 rounded-[16px] font-black text-[11px] inline-flex items-center gap-1.5 active-scale transition-all shadow-sm"> Buka Jurnal
))}
) : (
setAdminViewSiswa(null)} className="absolute top-6 left-6 bg-white/20 hover:bg-white/30 p-2.5 rounded-full backdrop-blur-md active-scale transition-colors">
{adminViewSiswa.nama}
NISN: {adminViewSiswa.nisn} Kelas {adminViewSiswa.kelas}
setAdminSelectedDate(e.target.value)}>
{Object.keys(adminSiswaJurnal).sort().reverse().map(dt => Tanggal: {dt} )}
{Object.keys(adminSiswaJurnal).length === 0 && Data Kosong }
{/* Tombol Cetak Admin – Satu Siswa */}
{Object.keys(adminSiswaJurnal).length > 0 && (
bukaHalamanCetak({ mode:'admin-satu-siswa', modeCetak:'perhari', tanggalDipilih:adminSelectedDate, siswa:adminViewSiswa, allJurnal:adminSiswaJurnal })} className="bg-white/20 backdrop-blur-sm border-2 border-white/30 text-white py-2 px-4 rounded-[14px] font-black text-[10px] flex items-center gap-1.5 hover:bg-white/30 active-scale transition-colors">
Cetak Harian Ini
bukaHalamanCetak({ mode:'admin-satu-siswa', modeCetak:'rekap', tanggalDipilih:adminSelectedDate, siswa:adminViewSiswa, allJurnal:adminSiswaJurnal })} className="bg-white/20 backdrop-blur-sm border-2 border-white/30 text-white py-2 px-4 rounded-[14px] font-black text-[10px] flex items-center gap-1.5 hover:bg-white/30 active-scale transition-colors">
Rekap Semua Tanggal
)}
{isLoading ?
Mengambil Data...
: Object.keys(adminSiswaJurnal).length > 0 && adminSelectedDate ? (
setActiveAdminModal(1)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[1]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[1].apresiasi}` : (isFilled(1, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu' : 'Kosong')} />
setActiveAdminModal(2)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[2]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[2].apresiasi}` : (isFilled(2, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu' : 'Kosong')} />
setActiveAdminModal(3)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[3]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[3].apresiasi}` : (isFilled(3, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu' : 'Kosong')} />
setActiveAdminModal(4)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[4]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[4].apresiasi}` : (isFilled(4, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu' : 'Kosong')} />
setActiveAdminModal(5)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[5]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[5].apresiasi}` : (isFilled(5, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu' : 'Kosong')} />
setActiveAdminModal(6)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[6]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[6].apresiasi}` : (isFilled(6, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu' : 'Kosong')} />
setActiveAdminModal(7)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[7]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[7].apresiasi}` : (isFilled(7, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu' : 'Kosong')} />
) : (
Belum ada jurnal untuk tanggal ini.
)}
)}
)}
{activeAdminTab === 'pengaturan' && (
{/* Kartu Pengaturan Kepala Sekolah */}
Data Kepala Sekolah
Nama dan NIP akan muncul di kolom tanda tangan pada setiap laporan yang dicetak.
{isSavingKepala ? 'Menyimpan...' : 'Simpan Data'}
{dataKepalaSekolah.nama && (
Preview Tanda Tangan Laporan
Mengetahui, Kepala Sekolah
{dataKepalaSekolah.nama}
{dataKepalaSekolah.nip &&
NIP: {dataKepalaSekolah.nip}
}
)}
)}
{/* Navigasi Bawah Khusus HP (Admin) */}
{[{id:'dashboard', label:'Dasbor', icon:LayoutGrid}, {id:'siswa', label:'Siswa', icon:GraduationCap}, {id:'guru', label:'Guru', icon:Users}, {id:'kegiatan', label:'Jurnal', icon:ActivityIcon}, {id:'pengaturan', label:'Setting', icon:Save}].map(menu => (
{ setActiveAdminTab(menu.id); if(menu.id==='kegiatan') setAdminViewSiswa(null); }} className={`flex flex-col items-center flex-1 py-2 px-1 rounded-[20px] transition-all ${activeAdminTab === menu.id ? 'text-[#2b3990] bg-[#e3f2fd]' : 'text-slate-400'}`}>
{menu.label}
))}
);
}
// === TAMPILAN WALI KELAS ===
if (guruAktif) {
if (guruViewMode === 'selection') {
return (
{ setShowGantiPasswordModal(false); setGantiPwLama(''); setGantiPwBaru(''); setGantiPwKonfirmasi(''); }} title="Ganti Password" icon={KeyRound} colorClass="bg-[#2b3990]">
{/* MODAL TANGGAPAN MASSAL */}
setShowTanggapanMassalModal(false)} title="Tanggapan Massal" icon={Sparkles} colorClass="bg-[#f9a03f]">
gantiTanggalMassal(e.target.value)} />
Beri apresiasi & catatan untuk tiap siswa di bawah ini — boleh berbeda-beda per siswa , dan otomatis hanya diterapkan ke kebiasaan yang mereka isi pada tanggal ini (kebiasaan yang tidak diisi tidak ikut dinilai). Hanya siswa yang sudah mengisi jurnal dan belum ditanggapi yang ditampilkan.
{jumlahSudahDitanggapi > 0 && ` ${jumlahSudahDitanggapi} siswa lain sudah ditanggapi sebelumnya dan disembunyikan dari daftar ini.`}
{isLoadingStatusMassal ? (
Memuat status jurnal...
) : daftarSiswaSudahMengisi.length > 0 ? (
Terapkan ke Semua Sekaligus
{['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 (
terapkanKeSemuaSiswa(ap)} className={`py-2.5 border-2 rounded-[16px] text-[10px] font-black transition-all active-scale flex flex-col items-center gap-1 bg-white ${apColor} opacity-80 hover:opacity-100`}>
{ap === 'Membudaya' ? '🌟' : ap === 'Mulai Terbentuk' ? '👍' : '⏳'} {ap}
);
})}
Tanggapan per Siswa ({daftarSiswaSudahMengisi.length})
{daftarSiswaSudahMengisi.map(s => {
const currentAp = massalTanggapanMap[s.nisn]?.apresiasi || '';
const currentCat = massalTanggapanMap[s.nisn]?.catatan || '';
return (
{s.nama}
{statusJurnalMassal[s.nisn]?.ringkasan || 'Sudah mengisi jurnal'}
{statusJurnalMassal[s.nisn]?.fotoIbadah?.length > 0 && (
{statusJurnalMassal[s.nisn].fotoIbadah.map((url, fi) => (
setViewPhotoUrl(url)} className="w-12 h-12 rounded-[10px] object-cover border-2 border-[#c8e6c9] flex-shrink-0 cursor-pointer hover:opacity-80 transition-opacity" alt="Foto ibadah"/>
))}
)}
{['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 (
setMassalApresiasiSiswa(s.nisn, ap)} className={`py-2 border-2 rounded-[12px] text-[9px] font-black transition-all active-scale flex flex-col items-center gap-0.5 ${currentAp === ap ? `${apColor} shadow-sm` : 'bg-slate-50 border-slate-200 text-slate-400'}`}>
{ap === 'Membudaya' ? '🌟' : ap === 'Mulai Terbentuk' ? '👍' : '⏳'} {ap}
);
})}
);
})}
) : (
{jumlahSudahDitanggapi > 0 ? "Semua siswa yang mengisi jurnal di tanggal ini sudah ditanggapi. 🎉" : "Belum ada siswa yang mengisi jurnal pada tanggal ini."}
)}
{isSubmittingMassal ? : } Kirim ke {jumlahSiapDikirimMassal} Siswa
{/* HEADER DASHBOARD */}
Selamat Datang, Guru Wali
{guruAktif.nama}
SMP Negeri 34 Samarinda
{/* DAFTAR SISWA */}
Daftar Siswa Bimbingan
{siswaBimbingan.length} Siswa
{/* RINGKASAN STATUS JURNAL HARI INI */}
{siswaBimbingan.length > 0 && (
Sudah Mengisi Hari Ini
{jumlahSudahMengisiHariIni} / {siswaBimbingan.length}
)}
{/* TOMBOL TANGGAPAN MASSAL: kirim 1 tanggapan ke SEMUA siswa yang sudah mengisi jurnal.
Fitur isi satu-per-satu (tombol "Lihat Jurnal" di tiap siswa) tetap tersedia seperti biasa.
Tombol ini SELALU AKTIF (tidak menunggu ada siswa yang mengisi jurnal hari ini),
karena di dalam modal guru wali bisa memilih tanggal lain (hari-hari sebelumnya)
untuk memberi tanggapan/penilaian susulan. */}
{siswaBimbingan.length > 0 && (
Beri Tanggapan Massal ({jumlahSudahMengisiHariIni} Siswa Hari Ini)
)}
{siswaBimbingan.length === 0 ? (
Belum ada siswa yang terdaftar di kelas Anda.
) : (
{siswaBimbingan.map((s, i) => (
{/* Info Siswa */}
{i + 1}
{s.nama}
NISN: {s.nisn} • Kelas {s.kelas}
{statusJurnalHariIni[s.nisn]?.sudahMengisi ? (
Sudah
) : (
Belum
)}
{/* Action Buttons */}
handleLihatTanggapanSiswa(s.nisn)}
disabled={isLoading}
className="flex-1 bg-[#2b3990] border-b-[3px] border-[#1a235c] text-white py-2.5 rounded-[14px] font-black text-[11px] flex items-center justify-center gap-1.5 active-scale hover:bg-[#3d4ea3] transition-all disabled:opacity-50"
>
{isLoading && selectedSiswaNisn === s.nisn ? : } Lihat Jurnal
handleCetakSiswa(s, 'perhari')}
disabled={isLoading}
className="bg-[#e3f2fd] border-2 border-[#bbdefb] text-[#1565c0] py-2.5 px-3 rounded-[14px] font-black text-[11px] flex items-center justify-center gap-1 active-scale hover:bg-[#bbdefb] transition-colors disabled:opacity-50"
>
Harian
handleCetakSiswa(s, 'rekap')}
disabled={isLoading}
className="bg-[#e8f5e9] border-2 border-[#c8e6c9] text-[#2e7d32] py-2.5 px-3 rounded-[14px] font-black text-[11px] flex items-center justify-center gap-1 active-scale hover:bg-[#c8e6c9] transition-colors disabled:opacity-50"
>
Rekap
))}
)}
{/* CETAK SEMUA SISWA */}
{siswaBimbingan.length > 0 && (
Cetak Laporan Semua Siswa
bukaHalamanCetak({ mode:'guru-semua-siswa', modeCetak:'perhari', tanggalDipilih: tanggalHariIni, guru:guruAktif, listSiswa:siswaBimbingan })} className="flex-1 bg-[#fffde7] border-2 border-[#fff59d] text-[#f57f17] py-2.5 px-3 rounded-[14px] font-black text-[10px] flex items-center justify-center gap-1 hover:bg-[#fff59d] active-scale transition-colors">
Semua – Harian
bukaHalamanCetak({ mode:'guru-semua-siswa', modeCetak:'rekap', tanggalDipilih: tanggalHariIni, guru:guruAktif, listSiswa:siswaBimbingan })} className="flex-1 bg-[#f3e5f5] border-2 border-[#e1bee7] text-[#6a1b9a] py-2.5 px-3 rounded-[14px] font-black text-[10px] flex items-center justify-center gap-1 hover:bg-[#e1bee7] active-scale transition-colors">
Semua – Rekap
)}
{/* GANTI PASSWORD & LOGOUT */}
setShowGantiPasswordModal(true)} className="text-xs font-black text-[#2b3990] hover:text-[#1a235c] bg-[#e8eaf6] px-6 py-3 rounded-full flex items-center justify-center gap-1.5 active-scale transition-colors border-2 border-[#c5cae9]"> Ganti Password
Keluar Aplikasi
);
}
const selectedStudent = siswaBimbingan.find(s => s.nisn === selectedSiswaNisn);
return (
setActiveGuruModal(null)} title={getModalConfig(activeGuruModal)?.title} icon={SquarePen} colorClass={activeGuruModal ? getModalConfig(activeGuruModal)?.color : 'bg-[#2b3990]'}>{renderGuruModalContent()}
handleGuruDateChange(e.target.value)}>
{Object.keys(guruSemuaJurnal).length > 0 ? Object.keys(guruSemuaJurnal).sort().reverse().map(dt => Tanggal: {dt} ) : {tanggalHariIni} (Kosong) }
{/* GRID 2 KOLOM DI HP UNTUK TAMPILAN BALOK MAINAN */}
setActiveGuruModal(1)} badgeText={evaluasiTemp[1]?.apresiasi ? `Status: ${evaluasiTemp[1].apresiasi}` : (isFilled(1) ? '📝 Nilai' : 'Kosong')} />
setActiveGuruModal(2)} badgeText={evaluasiTemp[2]?.apresiasi ? `Status: ${evaluasiTemp[2].apresiasi}` : (isFilled(2) ? '📝 Nilai' : 'Kosong')} />
setActiveGuruModal(3)} badgeText={evaluasiTemp[3]?.apresiasi ? `Status: ${evaluasiTemp[3].apresiasi}` : (isFilled(3) ? '📝 Nilai' : 'Kosong')} />
setActiveGuruModal(4)} badgeText={evaluasiTemp[4]?.apresiasi ? `Status: ${evaluasiTemp[4].apresiasi}` : (isFilled(4) ? '📝 Nilai' : 'Kosong')} />
setActiveGuruModal(5)} badgeText={evaluasiTemp[5]?.apresiasi ? `Status: ${evaluasiTemp[5].apresiasi}` : (isFilled(5) ? '📝 Nilai' : 'Kosong')} />
setActiveGuruModal(6)} badgeText={evaluasiTemp[6]?.apresiasi ? `Status: ${evaluasiTemp[6].apresiasi}` : (isFilled(6) ? '📝 Nilai' : 'Kosong')} />
setActiveGuruModal(7)} badgeText={evaluasiTemp[7]?.apresiasi ? `Status: ${evaluasiTemp[7].apresiasi}` : (isFilled(7) ? '📝 Nilai' : 'Kosong')} />
{
const total = Object.keys(evaluasiTemp).filter(k => !!evaluasiTemp[k]?.apresiasi).length;
if (total === 0) return showNotification("Belum ada tanggapan untuk disimpan!", "error");
setIsSubmittingTanggapan(true);
try {
const res = await panggilBackend({ action: "simpanTanggapanGuru", nisn: selectedSiswaNisn, tanggal: (studentHabitData?._tanggal || tanggalHariIni), evaluasi: evaluasiTemp });
if (res.status === "success") { clearDraftTanggapan(selectedSiswaNisn, studentHabitData?._tanggal || tanggalHariIni); showNotification("Terkirim!", "success"); await handleLihatTanggapanSiswa(); setGuruViewMode('selection'); window.scrollTo(0,0); }
} catch(e) { showNotification("Gagal mengirim.", "error"); } finally { setIsSubmittingTanggapan(false); }
}} disabled={isSubmittingTanggapan} className="w-full bg-[#f9a03f] border-b-[4px] border-[#d98020] text-white py-4 rounded-[24px] font-fredoka text-lg flex justify-center items-center gap-2 active-scale shadow-[0_10px_25px_rgba(249,160,63,0.4)] hover:bg-[#ffb05c] transition-all"> Kirim Penilaian ({Object.keys(evaluasiTemp).filter(k => !!evaluasiTemp[k]?.apresiasi).length}/7)
{setGuruViewMode('selection'); window.scrollTo(0,0);}} className="flex-1 text-[#2b3990] font-black text-[11px] uppercase tracking-wide bg-white border-2 border-slate-200 py-3.5 rounded-[20px] flex justify-center items-center gap-1.5 active-scale hover:bg-slate-50"> Ganti Siswa
Keluar
);
}
// === TAMPILAN SISWA (DASHBOARD) ===
return (
<>
setActiveModal(null)} title={getModalConfig(activeModal)?.title} icon={getModalConfig(activeModal)?.icon} colorClass={getModalConfig(activeModal)?.color}>{renderFormContent()}
setActiveAdminModal(null)} title={activeAdminModal ? getModalConfig(activeAdminModal)?.title : ""} icon={CheckCircle} colorClass={activeAdminModal ? getModalConfig(activeAdminModal)?.color : 'bg-[#2b3990]'}>{renderAdminModalContent()}
{resetNotif && Halaman Beralih ke {tanggalHariIni}
}
handleSiswaRefresh()} isLoading={isLoading} isLocked={isJurnalTerkirim} role="student" />
{!showSiswaRiwayat && (
Jurnal Hari Ini: {formatTanggalIndonesia(tanggalHariIni)}
)}
{!showSiswaRiwayat ? (
Riwayat Jurnal Saya
) : (
{ setShowSiswaRiwayat(false); setActiveAdminModal(null); setViewPhotoUrl(null); }} className="bg-[#2b3990] text-white font-black text-[12px] uppercase tracking-wide py-3 px-6 rounded-full flex items-center gap-2 active-scale shadow-md hover:bg-[#3d4ea3] transition-colors"> Kembali ke Jurnal Hari Ini
)}
{!showSiswaRiwayat ? (
<>
{/* GRID 2 KOLOM DI HP UNTUK TAMPILAN BALOK MAINAN */}
openModal(1)} badgeText={penilaianData.evaluasiGuru?.[1]?.apresiasi ? `Status: ${penilaianData.evaluasiGuru[1].apresiasi}` : (isFilled(1, penilaianData) ? 'Sudah Diisi' : 'Belum')} />
openModal(2)} badgeText={penilaianData.evaluasiGuru?.[2]?.apresiasi ? `Status: ${penilaianData.evaluasiGuru[2].apresiasi}` : (isFilled(2, penilaianData) ? 'Sudah Diisi' : 'Belum')} />
openModal(3)} badgeText={penilaianData.evaluasiGuru?.[3]?.apresiasi ? `Status: ${penilaianData.evaluasiGuru[3].apresiasi}` : (isFilled(3, penilaianData) ? 'Sudah Diisi' : 'Belum')} />
openModal(4)} badgeText={penilaianData.evaluasiGuru?.[4]?.apresiasi ? `Status: ${penilaianData.evaluasiGuru[4].apresiasi}` : (isFilled(4, penilaianData) ? 'Sudah Diisi' : 'Belum')} />
openModal(5)} badgeText={penilaianData.evaluasiGuru?.[5]?.apresiasi ? `Status: ${penilaianData.evaluasiGuru[5].apresiasi}` : (isFilled(5, penilaianData) ? 'Sudah Diisi' : 'Belum')} />
openModal(6)} badgeText={penilaianData.evaluasiGuru?.[6]?.apresiasi ? `Status: ${penilaianData.evaluasiGuru[6].apresiasi}` : (isFilled(6, penilaianData) ? 'Sudah Diisi' : 'Belum')} />
openModal(7)} badgeText={penilaianData.evaluasiGuru?.[7]?.apresiasi ? `Status: ${penilaianData.evaluasiGuru[7].apresiasi}` : (isFilled(7, penilaianData) ? 'Sudah Diisi' : 'Belum')} />
{isLoading ? 'Menyiapkan keajaiban...' : (isJurnalTerkirim ? 'Jurnal Hari Ini Terkirim! ✅' : 'Kirim Semua Jurnal Hari Ini!')}
>
) : (
Pilih Tanggal
setAdminSelectedDate(e.target.value)}>
{Object.keys(adminSiswaJurnal).sort().reverse().map(dt => Tanggal: {dt} )}
{Object.keys(adminSiswaJurnal).length === 0 && Belum Ada Riwayat }
{isLoading ? (
Mengambil Data...
) : Object.keys(adminSiswaJurnal).length > 0 && adminSelectedDate ? (
<>
Riwayat hanya bisa dibaca, tidak bisa diubah.
setActiveAdminModal(1)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[1]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[1].apresiasi}` : (isFilled(1, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu Tanggapan' : 'Kosong')} />
setActiveAdminModal(2)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[2]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[2].apresiasi}` : (isFilled(2, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu Tanggapan' : 'Kosong')} />
setActiveAdminModal(3)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[3]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[3].apresiasi}` : (isFilled(3, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu Tanggapan' : 'Kosong')} />
setActiveAdminModal(4)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[4]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[4].apresiasi}` : (isFilled(4, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu Tanggapan' : 'Kosong')} />
setActiveAdminModal(5)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[5]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[5].apresiasi}` : (isFilled(5, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu Tanggapan' : 'Kosong')} />
setActiveAdminModal(6)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[6]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[6].apresiasi}` : (isFilled(6, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu Tanggapan' : 'Kosong')} />
setActiveAdminModal(7)} badgeText={adminSiswaJurnal[adminSelectedDate]?.evaluasiGuru?.[7]?.apresiasi ? `Status: ${adminSiswaJurnal[adminSelectedDate].evaluasiGuru[7].apresiasi}` : (isFilled(7, adminSiswaJurnal[adminSelectedDate]) ? 'Menunggu Tanggapan' : 'Kosong')} />
>
) : (
Belum ada riwayat jurnal.
)}
)}
Jurnal direset otomatis tiap jam 00:00 WITA
Keluar Aplikasi
>
);
}
class AppErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, info) {
console.error('[APP RENDER ERROR]', error, info);
}
render() {
if (this.state.error) {
return (
Aplikasi mengalami kendala
Halaman tidak dapat menampilkan data. Silakan muat ulang aplikasi.
window.location.reload()}>Muat Ulang
);
}
return this.props.children;
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( );
window.__appStarted = true;