import React, { useState, useEffect, useMemo, useRef } from 'react'; import { Cloud, Settings, Download, Calculator, FileText, Upload, Users, Inbox, TrendingUp, Award, Target, Trash2, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth'; import { getFirestore, collection, addDoc, updateDoc, deleteDoc, doc, onSnapshot, query, orderBy } from 'firebase/firestore'; // --- Firebase Configuration --- const firebaseConfig = JSON.parse(__firebase_config); const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; // --- Helper Functions --- const calculateGrade = (score) => { if (score >= 80) return 'A'; if (score >= 70) return 'B'; if (score >= 60) return 'C'; if (score >= 50) return 'D'; return 'F'; }; const getGradeColor = (grade) => { const colors = { 'A': 'bg-green-100 text-green-800', 'B': 'bg-blue-100 text-blue-800', 'C': 'bg-yellow-100 text-yellow-800', 'D': 'bg-orange-100 text-orange-800', 'F': 'bg-red-100 text-red-800' }; return colors[grade] || 'bg-gray-100 text-gray-800'; }; // --- Main Component --- export default function DashboardPemarkahan() { // State const [user, setUser] = useState(null); const [students, setStudents] = useState([]); const [loading, setLoading] = useState(true); const [configOpen, setConfigOpen] = useState(false); const [importText, setImportText] = useState("AZIZAN BIN MURAD @ IBRAHIM\nEMYLIA BINTI AHMAD\nMOHD FARHAN BIN ABDULLAH"); const [toasts, setToasts] = useState([]); // Weights State const [weights, setWeights] = useState({ caseStudy: 20, kuiz1: 10, kuiz2: 10, presentation: 20, final: 40 }); // Derived State const totalWeight = Object.values(weights).reduce((a, b) => a + Number(b), 0); const weightWarning = Math.abs(totalWeight - 100) > 0.01; // --- Auth & Data Fetching --- useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (error) { console.error("Auth error:", error); showToast("Ralat sambungan authentication", "error"); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, setUser); return () => unsubscribe(); }, []); useEffect(() => { if (!user) return; // Use a specific sub-collection path for this app instance to avoid collisions const studentsRef = collection(db, 'artifacts', appId, 'public', 'data', 'students_v1'); // Simple query, sorting handled in client to avoid complex index requirements const unsubscribe = onSnapshot(studentsRef, (snapshot) => { const fetchedStudents = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); // Sort by name alphabetically fetchedStudents.sort((a, b) => a.name.localeCompare(b.name)); setStudents(fetchedStudents); setLoading(false); }, (error) => { console.error("Firestore error:", error); showToast("Gagal membaca data", "error"); setLoading(false); } ); return () => unsubscribe(); }, [user]); // --- Actions --- const showToast = (message, type = 'success') => { const id = Date.now(); setToasts(prev => [...prev, { id, message, type }]); setTimeout(() => { setToasts(prev => prev.filter(t => t.id !== id)); }, 3000); }; const handleWeightChange = (field, value) => { setWeights(prev => ({ ...prev, [field]: parseFloat(value) || 0 })); }; const calculateTotal = (s) => { // Recalculate based on current weights in state, not just stored total const total = ( ((s.caseStudy || 0) * weights.caseStudy / 100) + ((s.kuiz1 || 0) * weights.kuiz1 / 100) + ((s.kuiz2 || 0) * weights.kuiz2 / 100) + ((s.presentation || 0) * weights.presentation / 100) + ((s.final || 0) * weights.final / 100) ); return Math.round(total * 100) / 100; }; const importStudents = async () => { if (!user) return; const names = importText.split('\n').filter(line => line.trim()); if (names.length === 0) { showToast("Sila masukkan senarai nama", "error"); return; } if (students.length + names.length > 200) { showToast("Had maksimum pelajar dicapai (200)", "error"); return; } setLoading(true); let successCount = 0; const studentsRef = collection(db, 'artifacts', appId, 'public', 'data', 'students_v1'); try { // Process sequentially to avoid rate limits on massive batch for (const name of names) { await addDoc(studentsRef, { name: name.trim(), caseStudy: 0, kuiz1: 0, kuiz2: 0, presentation: 0, final: 0, createdAt: new Date().toISOString() }); successCount++; } showToast(`Berjaya import ${successCount} pelajar!`, "success"); setImportText(""); } catch (error) { console.error("Import error:", error); showToast("Ralat semasa import", "error"); } finally { setLoading(false); } }; const updateMark = async (studentId, field, value) => { if (!user) return; // Optimistic update handled by Firestore listener, but we need to ensure local feel is snappy // For now, we just write to DB. const val = parseFloat(value); if (isNaN(val) || val < 0 || val > 100) return; // Simple validation try { const studentRef = doc(db, 'artifacts', appId, 'public', 'data', 'students_v1', studentId); await updateDoc(studentRef, { [field]: val }); } catch (error) { console.error("Update error:", error); showToast("Gagal mengemaskini markah", "error"); } }; const deleteStudent = async (studentId) => { if (!confirm("Adakah anda pasti mahu memadam pelajar ini?")) return; try { await deleteDoc(doc(db, 'artifacts', appId, 'public', 'data', 'students_v1', studentId)); showToast("Pelajar dipadam", "success"); } catch (error) { showToast("Gagal memadam", "error"); } }; const exportCSV = () => { if (students.length === 0) { showToast("Tiada data untuk diexport", "error"); return; } const headers = ['Nama', `Case Study (${weights.caseStudy}%)`, `Kuiz 1 (${weights.kuiz1}%)`, `Kuiz 2 (${weights.kuiz2}%)`, `Presentation (${weights.presentation}%)`, `Final (${weights.final}%)`, 'Jumlah', 'Gred']; const rows = students.map(s => { const total = calculateTotal(s); const grade = calculateGrade(total); return [ `"${s.name}"`, // Quote name to handle commas s.caseStudy || 0, s.kuiz1 || 0, s.kuiz2 || 0, s.presentation || 0, s.final || 0, total.toFixed(2), grade ].join(','); }); const csvContent = "data:text/csv;charset=utf-8," + [headers.join(','), ...rows].join('\n'); const encodedUri = encodeURI(csvContent); const link = document.createElement("a"); link.setAttribute("href", encodedUri); link.setAttribute("download", `markah_pelajar_${new Date().toISOString().slice(0,10)}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); showToast("CSV berjaya dimuat turun", "success"); }; // --- Statistics --- const stats = useMemo(() => { if (students.length === 0) return { avg: 0, high: 0, low: 0 }; const scores = students.map(s => calculateTotal(s)); const total = scores.reduce((a, b) => a + b, 0); return { avg: (total / students.length).toFixed(2), high: Math.max(...scores).toFixed(2), low: Math.min(...scores).toFixed(2) }; }, [students, weights]); // --- Render --- return (
{/* Status Bar */}
Status: {loading ? 'Memuatkan...' : (user ? 'Bersambung' : 'Menghubungkan...')}
{students.length} pelajar
{/* Logos */}
Logo MARA e.target.style.display='none'} /> Logo IKMKL e.target.style.display='none'} />
{/* Header */}

Dashboard Pemarkahan

Subjek: Strategic Management | Semester: Semasa

Nama Pensyarah: En Muhammad Syukran bin Jamil

{/* Config Panel */} {configOpen && (

Tetapan Pemberat Markah (%)

Markah dimasukkan dalam bentuk /100. Pemberat akan dikira automatik.

{[ { id: 'caseStudy', label: 'Case Study' }, { id: 'kuiz1', label: 'Kuiz 1' }, { id: 'kuiz2', label: 'Kuiz 2' }, { id: 'presentation', label: 'Presentation' }, { id: 'final', label: 'Final Assessment' }, ].map((item) => (
handleWeightChange(item.id, e.target.value)} min="0" max="100" className="w-full p-2 pr-8 rounded border border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm" /> %
))}

Jumlah Pemberat: {totalWeight}%

{weightWarning && ( Mesti sama dengan 100% )}
)} {/* Quick Import */}

Import Senarai Pelajar

Paste nama pelajar (satu nama setiap baris):

{/* Students Table */}

Senarai Pelajar

{students.length === 0 ? ( ) : ( students.map((student) => { const total = calculateTotal(student); const grade = calculateGrade(total); return ( {[ { field: 'caseStudy', val: student.caseStudy }, { field: 'kuiz1', val: student.kuiz1 }, { field: 'kuiz2', val: student.kuiz2 }, { field: 'presentation', val: student.presentation }, { field: 'final', val: student.final }, ].map((cell, idx) => ( ))} ); }) )}
Nama Case Study ({weights.caseStudy}%) Kuiz 1 ({weights.kuiz1}%) Kuiz 2 ({weights.kuiz2}%) Pres. ({weights.presentation}%) Final ({weights.final}%) Total Gred Tindakan

Tiada pelajar. Import senarai pelajar dahulu!

{student.name}
updateMark(student.id, cell.field, e.target.value)} // Use onKeyDown to allow Enter key to blur/submit onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); }} min="0" max="100" className="w-14 p-1 text-center text-sm rounded border border-gray-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none transition-all hover:border-gray-300" /> /100
{total.toFixed(2)} /100 {grade}
{/* Statistics Cards */}
} bgColor="bg-blue-100" label="Jumlah Pelajar" value={students.length} /> } bgColor="bg-green-100" label="Purata Markah" value={stats.avg} /> } bgColor="bg-purple-100" label="Markah Tertinggi" value={stats.high} /> } bgColor="bg-orange-100" label="Markah Terendah" value={stats.low} />
{/* Toast Notification Container */}
{toasts.map((toast) => (
{toast.type === 'success' ? ( ) : ( )} {toast.message}
))}
); } // Simple Subcomponent for Stat Cards to reduce clutter function StatCard({ icon, bgColor, label, value }) { return (
{icon}

{label}

{value}

); }