import React, { useState, useMemo, useEffect } from 'react'; import { JournalEntry, ClassJournalEntry, Teacher, SchoolSettings, AuthUser } from '../types'; import { Printer, FileText, Search, Download, Image as ImageIcon, ExternalLink, Loader2, User, Shield, Edit2, Trash2, X, Save, AlertTriangle, Calendar, Filter, RefreshCw, ChevronDown, CheckCircle2, BarChart3, TrendingUp, Users, Info } from 'lucide-react'; import { updateJournalEntry, deleteJournalEntry } from '../services/apiService'; import jsPDF from 'jspdf'; import 'jspdf-autotable'; interface RecapViewProps { entries: JournalEntry[]; classEntries: ClassJournalEntry[]; teachers: Teacher[]; settings: SchoolSettings; currentUser: AuthUser; onRefresh?: () => void; } // Helper to get today's date in YYYY-MM-DD format (local timezone) const getTodayDate = () => { const today = new Date(); const year = today.getFullYear(); const month = String(today.getMonth() + 1).padStart(2, '0'); const day = String(today.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; // Helper to format Date to YYYY-MM-DD in local timezone const formatDateToLocalString = (d: Date) => { 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}`; }; // Helper to convert Google Drive links to embeddable direct links const formatGoogleDriveImageUrl = (url: string) => { if (!url) return ''; // If it's already a base64 string, return as is if (url.startsWith('data:image')) return url; try { // Extract ID from typical Google Drive URLs const idMatch = url.match(/(?:id=|\/d\/)([\w-]+)/); if (idMatch && idMatch[1]) { // Using lh3.googleusercontent.com is often more reliable for embedding than drive.google.com/uc return `https://lh3.googleusercontent.com/d/${idMatch[1]}`; } return url; } catch (e) { return url; } }; // Helper to fetch image as Base64 for PDF embedding const getBase64ImageFromURL = (url: string): Promise => { return new Promise((resolve, reject) => { const img = new Image(); img.setAttribute('crossOrigin', 'anonymous'); img.onload = () => { const canvas = document.createElement("canvas"); // Resize to reasonable thumbnail size to save PDF size // If it is KOP (usually wide), allow larger width const isWide = img.width > img.height; const maxDim = isWide ? 1200 : 800; const scale = Math.min(1, maxDim / Math.max(img.width, img.height)); canvas.width = img.width * scale; canvas.height = img.height * scale; const ctx = canvas.getContext("2d"); if (ctx) { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const dataURL = canvas.toDataURL("image/jpeg", 0.85); resolve(dataURL); } else { reject(new Error("Canvas context failed")); } }; img.onerror = (error) => { reject(error); }; img.src = url; }); }; const RecapView: React.FC = ({ entries, classEntries, teachers, settings, currentUser, onRefresh }) => { // Initialize state based on User Role directly to ensure instant rendering const [activeTab, setActiveTab] = useState<'GURU' | 'KELAS'>(() => { if (currentUser.role === 'SEKRETARIS') return 'KELAS'; return 'GURU'; // Default for GURU and ADMIN }); const [selectedFilter, setSelectedFilter] = useState(() => { if (currentUser.role === 'GURU') return currentUser.name; if (currentUser.role === 'SEKRETARIS') return currentUser.className || ''; return 'ALL'; // Admin sees all by default }); // Date filter states for Admin const [dateFrom, setDateFrom] = useState(() => { if (currentUser.role === 'ADMIN') return getTodayDate(); return ''; }); const [dateTo, setDateTo] = useState(() => { if (currentUser.role === 'ADMIN') return getTodayDate(); return ''; }); // Search query for Admin const [searchQuery, setSearchQuery] = useState(''); // Show filter panel for Admin const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); // Admin View Mode: 'journal' for journal list, 'statistics' for statistics view, 'attendance' for teacher attendance report const [adminViewMode, setAdminViewMode] = useState<'journal' | 'statistics' | 'attendance'>('journal'); // Statistics period filter const [statsPeriod, setStatsPeriod] = useState<'week' | 'month'>('week'); // Teacher Attendance Report filters const [attendancePeriod, setAttendancePeriod] = useState<'daily' | 'weekly' | 'monthly'>('daily'); const [attendanceDate, setAttendanceDate] = useState(getTodayDate()); const [isGeneratingPdf, setIsGeneratingPdf] = useState(false); // Admin action states const [editingEntry, setEditingEntry] = useState(null); const [deletingEntry, setDeletingEntry] = useState(null); const [isProcessing, setIsProcessing] = useState(false); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); // Multi-selection state const [selectedEntryIds, setSelectedEntryIds] = useState>(new Set()); // Reset selection and page when tab or filters change useEffect(() => { setSelectedEntryIds(new Set()); setCurrentPage(1); }, [activeTab, selectedFilter, dateFrom, dateTo, searchQuery]); // When switching to attendance view, sync the attendance date with current filter useEffect(() => { if (adminViewMode === 'attendance' && dateFrom) { setAttendanceDate(dateFrom); setAttendancePeriod('daily'); } }, [adminViewMode, dateFrom]); const toggleSelectAll = () => { if (selectedEntryIds.size === filteredEntries.length) { setSelectedEntryIds(new Set()); } else { setSelectedEntryIds(new Set(filteredEntries.map(e => e.id))); } }; const toggleSelectEntry = (id: string) => { const newSelection = new Set(selectedEntryIds); if (newSelection.has(id)) { newSelection.delete(id); } else { newSelection.add(id); } setSelectedEntryIds(newSelection); }; const getTargetEntries = () => { if (selectedEntryIds.size === 0) return filteredEntries; return filteredEntries.filter(e => selectedEntryIds.has(e.id)); }; // Keep sync in case props change, but initial state handles the "automatic" requirement useEffect(() => { if (currentUser.role === 'GURU') { setActiveTab('GURU'); setSelectedFilter(currentUser.name); } else if (currentUser.role === 'SEKRETARIS' && currentUser.className) { setActiveTab('KELAS'); setSelectedFilter(currentUser.className); } else if (currentUser.role === 'ADMIN') { // Admin: set to today's date on mount const today = getTodayDate(); setDateFrom(today); setDateTo(today); setSelectedFilter('ALL'); } }, [currentUser]); // Sync attendanceDate with dateFrom when panel filter changes (for consistency) useEffect(() => { if (dateFrom && attendancePeriod === 'daily') { setAttendanceDate(dateFrom); } }, [dateFrom]); // Sync dateFrom/dateTo with attendanceDate when attendance date picker changes (for consistency) useEffect(() => { if (attendancePeriod === 'daily' && attendanceDate && adminViewMode === 'attendance') { setDateFrom(attendanceDate); setDateTo(attendanceDate); } }, [attendanceDate, adminViewMode]); // Get unique classes from BOTH sources (Teacher Jurnal & Class Jurnal) const classes = useMemo(() => { if (activeTab === 'GURU') { const c = new Set(entries.map(e => e.className).filter(Boolean)); return Array.from(c).sort(); } else { const c = new Set(classEntries.map(e => e.className).filter(Boolean)); return Array.from(c).sort(); } }, [entries, classEntries, activeTab]); // Count journals for today (for Admin dashboard info) const todayStats = useMemo(() => { const today = getTodayDate(); const guruToday = entries.filter(e => e.date === today).length; const kelasToday = classEntries.filter(e => e.date === today).length; return { guruToday, kelasToday, total: guruToday + kelasToday }; }, [entries, classEntries]); // ========== TEACHER STATISTICS CALCULATIONS ========== // Helper to get start of week (Monday) const getStartOfWeek = (date: Date) => { const d = new Date(date); const day = d.getDay(); const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Adjust when day is Sunday d.setDate(diff); d.setHours(0, 0, 0, 0); return d; }; // Helper to get week number const getWeekNumber = (date: Date) => { const startOfYear = new Date(date.getFullYear(), 0, 1); const pastDaysOfYear = (date.getTime() - startOfYear.getTime()) / 86400000; return Math.ceil((pastDaysOfYear + startOfYear.getDay() + 1) / 7); }; // Weekly Statistics - Last 4 weeks const weeklyStats = useMemo(() => { const today = new Date(); const weeks: { weekLabel: string; startDate: Date; endDate: Date; teachers: { name: string; count: number; dates: string[] }[] }[] = []; for (let i = 0; i < 4; i++) { const weekStart = getStartOfWeek(new Date(today.getTime() - i * 7 * 24 * 60 * 60 * 1000)); const weekEnd = new Date(weekStart.getTime() + 6 * 24 * 60 * 60 * 1000); weekEnd.setHours(23, 59, 59, 999); const weekStartStr = formatDateToLocalString(weekStart); const weekEndStr = formatDateToLocalString(weekEnd); // Filter entries for this week const weekEntries = entries.filter(e => e.date >= weekStartStr && e.date <= weekEndStr); // Group by teacher const teacherMap = new Map }>(); weekEntries.forEach(e => { if (!teacherMap.has(e.teacherName)) { teacherMap.set(e.teacherName, { count: 0, dates: new Set() }); } const teacher = teacherMap.get(e.teacherName)!; teacher.count++; teacher.dates.add(e.date); }); const teacherStats = Array.from(teacherMap.entries()) .map(([name, data]) => ({ name, count: data.count, dates: Array.from(data.dates).sort() })) .sort((a, b) => b.count - a.count); const weekLabel = i === 0 ? 'Minggu Ini' : i === 1 ? 'Minggu Lalu' : `${i} Minggu Lalu`; weeks.push({ weekLabel, startDate: weekStart, endDate: weekEnd, teachers: teacherStats }); } return weeks; }, [entries]); // Monthly Statistics - Last 3 months const monthlyStats = useMemo(() => { const today = new Date(); const months: { monthLabel: string; year: number; month: number; teachers: { name: string; count: number; uniqueDays: number }[] }[] = []; for (let i = 0; i < 3; i++) { const targetDate = new Date(today.getFullYear(), today.getMonth() - i, 1); const year = targetDate.getFullYear(); const month = targetDate.getMonth(); const monthStart = new Date(year, month, 1); const monthEnd = new Date(year, month + 1, 0); const monthStartStr = formatDateToLocalString(monthStart); const monthEndStr = formatDateToLocalString(monthEnd); // Filter entries for this month const monthEntries = entries.filter(e => e.date >= monthStartStr && e.date <= monthEndStr); // Group by teacher const teacherMap = new Map }>(); monthEntries.forEach(e => { if (!teacherMap.has(e.teacherName)) { teacherMap.set(e.teacherName, { count: 0, dates: new Set() }); } const teacher = teacherMap.get(e.teacherName)!; teacher.count++; teacher.dates.add(e.date); }); const teacherStats = Array.from(teacherMap.entries()) .map(([name, data]) => ({ name, count: data.count, uniqueDays: data.dates.size })) .sort((a, b) => b.count - a.count); const monthLabel = i === 0 ? 'Bulan Ini' : targetDate.toLocaleString('id-ID', { month: 'long', year: 'numeric' }); months.push({ monthLabel, year, month, teachers: teacherStats }); } return months; }, [entries]); // Get all teachers who haven't submitted journals this week/month const teachersWithoutJournals = useMemo(() => { const currentPeriodTeachers = statsPeriod === 'week' ? new Set(weeklyStats[0]?.teachers.map(t => t.name) || []) : new Set(monthlyStats[0]?.teachers.map(t => t.name) || []); return teachers.filter(t => !currentPeriodTeachers.has(t.name)); }, [teachers, weeklyStats, monthlyStats, statsPeriod]); // ========== TEACHER ATTENDANCE STATISTICS (FROM CLASS JOURNAL) ========== // Helper to get week range const getWeekRange = (date: Date) => { const start = getStartOfWeek(date); const end = new Date(start.getTime() + 6 * 24 * 60 * 60 * 1000); return { start, end }; }; // Helper to get month range const getMonthRange = (date: Date) => { const start = new Date(date.getFullYear(), date.getMonth(), 1); const end = new Date(date.getFullYear(), date.getMonth() + 1, 0); return { start, end }; }; // Helper to format date (moved here to be available for useMemo below) const formatDate = (dateString: string) => { if (!dateString) return '-'; try { const options: Intl.DateTimeFormatOptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; return new Date(dateString).toLocaleDateString('id-ID', options); } catch (e) { return dateString; } }; // Teacher Attendance Statistics based on classEntries const teacherAttendanceStats = useMemo(() => { let dateStart: Date; let dateEnd: Date; let periodLabel: string; const baseDate = new Date(attendanceDate); if (attendancePeriod === 'daily') { dateStart = new Date(attendanceDate); dateStart.setHours(0, 0, 0, 0); dateEnd = new Date(attendanceDate); dateEnd.setHours(23, 59, 59, 999); periodLabel = formatDate(attendanceDate); } else if (attendancePeriod === 'weekly') { const range = getWeekRange(baseDate); dateStart = range.start; dateEnd = range.end; periodLabel = `${dateStart.toLocaleDateString('id-ID', { day: 'numeric', month: 'short' })} - ${dateEnd.toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' })}`; } else { const range = getMonthRange(baseDate); dateStart = range.start; dateEnd = range.end; periodLabel = baseDate.toLocaleDateString('id-ID', { month: 'long', year: 'numeric' }); } // Use local date formatting to avoid timezone issues const startStr = formatDateToLocalString(dateStart); const endStr = formatDateToLocalString(dateEnd); // Filter class entries for the selected period // Normalize the entry date to YYYY-MM-DD format for consistent comparison let periodEntries = classEntries.filter(e => { const entryDate = e.date ? e.date.split('T')[0] : ''; return entryDate >= startStr && entryDate <= endStr; }); console.log(`[Kehadiran Guru] Period: ${startStr} to ${endStr}, Entries found: ${periodEntries.length}`); // Deduplicate entries by ID to prevent double counting if there are duplicates in the data source const uniqueEntriesMap = new Map(); periodEntries.forEach(e => { if (e.id) uniqueEntriesMap.set(e.id, e); else uniqueEntriesMap.set(Math.random().toString(), e); // Fallback for items without ID }); const finalEntries = Array.from(uniqueEntriesMap.values()); // Group by teacher and count presence status const teacherMap = new Map(); finalEntries.forEach(entry => { const teacherName = entry.teacherName; if (!teacherMap.has(teacherName)) { teacherMap.set(teacherName, { hadir: 0, tugas: 0, tidakHadir: 0, total: 0, details: [] }); } const teacher = teacherMap.get(teacherName)!; teacher.total++; const status = entry.teacherPresence || 'Hadir'; if (status === 'Hadir') teacher.hadir++; else if (status === 'Tugas') teacher.tugas++; else teacher.tidakHadir++; teacher.details.push({ date: entry.date, className: entry.className, subject: entry.subject, status: status, jam: `${entry.startTime}-${entry.endTime}` }); }); // Convert to array and sort by total entries const teacherStats = Array.from(teacherMap.entries()) .map(([name, data]) => ({ name, ...data, hadirPercentage: data.total > 0 ? Math.round((data.hadir / data.total) * 100) : 0 })) .sort((a, b) => b.total - a.total); // Calculate summary stats - COUNT UNIQUE TEACHERS, not entries // Priority: If teacher has at least 1 "Hadir" → count as Hadir // If teacher has at least 1 "Tugas" (but no Hadir) → count as Tugas // If ALL entries are "Tidak Hadir" → count as Tidak Hadir let uniqueHadir = 0; let uniqueTugas = 0; let uniqueTidakHadir = 0; teacherStats.forEach(teacher => { if (teacher.hadir > 0) { uniqueHadir++; } else if (teacher.tugas > 0) { uniqueTugas++; } else if (teacher.tidakHadir > 0) { uniqueTidakHadir++; } }); const summary = { totalEntries: finalEntries.length, totalTeachers: teacherStats.length, totalHadir: uniqueHadir, totalTugas: uniqueTugas, totalTidakHadir: uniqueTidakHadir }; return { periodLabel, startDate: startStr, endDate: endStr, teachers: teacherStats, summary }; }, [classEntries, attendancePeriod, attendanceDate]); // Filter Logic - Unified for all roles const filteredEntries = useMemo(() => { let source: any[] = activeTab === 'GURU' ? [...entries] : [...classEntries]; // Apply role-based initial filter if (currentUser.role === 'GURU') { source = source.filter(e => e.teacherName === currentUser.name); } else if (currentUser.role === 'SEKRETARIS') { source = source.filter(e => e.className === currentUser.className); } else if (currentUser.role === 'ADMIN' && selectedFilter !== 'ALL') { if (activeTab === 'GURU') { source = source.filter(e => e.teacherName === selectedFilter); } else { source = source.filter(e => e.className === selectedFilter); } } // Apply date filter (now for everyone) if (dateFrom) { source = source.filter(e => e.date >= dateFrom); } if (dateTo) { source = source.filter(e => e.date <= dateTo); } // Apply search query if (searchQuery.trim()) { const query = searchQuery.toLowerCase().trim(); source = source.filter(e => { const searchableText = [ e.teacherName, e.className, e.topic || '', e.subject || '', e.notes || '', ].join(' ').toLowerCase(); return searchableText.includes(query); }); } // Sort by date descending return source.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); }, [activeTab, selectedFilter, entries, classEntries, dateFrom, dateTo, searchQuery, currentUser]); // Pagination Logic const totalPages = Math.ceil(filteredEntries.length / itemsPerPage); const paginatedEntries = useMemo(() => { const start = (currentPage - 1) * itemsPerPage; return filteredEntries.slice(start, start + itemsPerPage); }, [filteredEntries, currentPage]); // Quick date filter presets for Admin const setDatePreset = (preset: 'today' | 'week' | 'month' | 'all') => { const today = new Date(); switch (preset) { case 'today': const todayStr = getTodayDate(); setDateFrom(todayStr); setDateTo(todayStr); break; case 'week': const weekAgo = new Date(today); weekAgo.setDate(weekAgo.getDate() - 7); setDateFrom(formatDateToLocalString(weekAgo)); setDateTo(getTodayDate()); break; case 'month': const monthAgo = new Date(today); monthAgo.setMonth(monthAgo.getMonth() - 1); setDateFrom(formatDateToLocalString(monthAgo)); setDateTo(getTodayDate()); break; case 'all': setDateFrom(''); setDateTo(''); break; } }; // Helper to format absent students string const getAbsentString = (entry: any) => { if (!entry.attendanceDetails || entry.attendanceDetails.length === 0) return '-'; // If string "NIHIL" if (entry.attendanceDetails === "NIHIL") return "-"; const absents = Array.isArray(entry.attendanceDetails) ? entry.attendanceDetails.filter((d: any) => d.status !== 'H') : []; if (absents.length === 0) return '-'; return absents.map((d: any) => `${d.studentName} (${d.status})`).join(', '); }; // --- ADMIN ACTION HANDLERS --- const handleEditSubmit = async () => { if (!editingEntry) return; setIsProcessing(true); const type = activeTab === 'GURU' ? 'teacher' : 'class'; const success = await updateJournalEntry(editingEntry.id, editingEntry, type); setIsProcessing(false); if (success) { alert('✅ Jurnal berhasil diperbarui!'); setEditingEntry(null); onRefresh?.(); } else { alert('❌ Gagal menyimpan perubahan. Silakan coba lagi.'); } }; const handleDelete = async () => { if (!deletingEntry) return; setIsProcessing(true); const type = activeTab === 'GURU' ? 'teacher' : 'class'; const success = await deleteJournalEntry(deletingEntry.id, type); setIsProcessing(false); if (success) { alert('✅ Jurnal berhasil dihapus!'); setDeletingEntry(null); onRefresh?.(); } else { alert('❌ Gagal menghapus jurnal. Silakan coba lagi.'); } }; const handleEditFieldChange = (field: string, value: any) => { setEditingEntry((prev: any) => ({ ...prev, [field]: value })); }; // --- PRINT STATISTICS --- const handlePrintStatistics = () => { const printWindow = window.open('', '', 'height=600,width=1024'); if (!printWindow) return; const today = new Date(); const dateStr = `Badung, ${today.getDate()} ${today.toLocaleString('id-ID', { month: 'long' })} ${today.getFullYear()}`; // Prepare Header (Kop) const kopUrl = formatGoogleDriveImageUrl(settings.kopUrl); const headerContent = kopUrl ? `` : `

PEMERINTAH PROVINSI BALI

SMA NEGERI 1 ABIANSEMAL

Alamat: Jalan Raya Abiansemal, Badung, Bali

`; let tableContent = ''; if (statsPeriod === 'week') { // Weekly Statistics weeklyStats.forEach((week, weekIdx) => { const startStr = week.startDate.toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' }); const endStr = week.endDate.toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' }); tableContent += `

${week.weekLabel} (${startStr} - ${endStr})

${week.teachers.length > 0 ? week.teachers.map((t, idx) => ` `).join('') : ` `}
No. Nama Guru Jumlah Jurnal Hari Mengisi
${idx + 1} ${t.name} ${t.count} ${t.dates.length} hari
Belum ada data jurnal

Total: ${week.teachers.length} guru telah mengisi jurnal

`; }); } else { // Monthly Statistics monthlyStats.forEach((month, monthIdx) => { const monthName = new Date(month.year, month.month).toLocaleString('id-ID', { month: 'long', year: 'numeric' }); tableContent += `

${month.monthLabel} (${monthName})

${month.teachers.length > 0 ? month.teachers.map((t, idx) => ` `).join('') : ` `}
No. Nama Guru Jumlah Jurnal Hari Aktif Mengisi
${idx + 1} ${t.name} ${t.count} ${t.uniqueDays} hari
Belum ada data jurnal

Total: ${month.teachers.length} guru telah mengisi jurnal

`; }); } // Teachers without journals section const noJournalSection = teachersWithoutJournals.length > 0 ? `

⚠️ Guru yang Belum Mengisi Jurnal (${statsPeriod === 'week' ? 'Minggu Ini' : 'Bulan Ini'})

${teachersWithoutJournals.map(t => t.name).join(', ')}

` : ''; printWindow.document.write(` Statistik Kehadiran Guru - ${statsPeriod === 'week' ? 'Mingguan' : 'Bulanan'} ${headerContent}
STATISTIK PENGISIAN JURNAL MENGAJAR GURU
${statsPeriod === 'week' ? 'LAPORAN MINGGUAN' : 'LAPORAN BULANAN'}
SEMESTER ${settings.semester.toUpperCase()} - TAHUN PELAJARAN ${settings.academicYear}
${tableContent} ${noJournalSection} `); printWindow.document.close(); }; // --- PRINT HTML GENERATION LOGIC --- const handlePrint = () => { const targetEntries = getTargetEntries(); if (!targetEntries.length) return; const printWindow = window.open('', '', 'height=600,width=1024'); if (!printWindow) return; const title = activeTab === 'GURU' ? `JURNAL MENGAJAR GURU` : `JURNAL KELAS ${selectedFilter}`; const today = new Date(); const dateStr = `Badung, ${today.getDate()} ${today.toLocaleString('id-ID', { month: 'long' })} ${today.getFullYear()}`; // Prepare Header (Kop) const kopUrl = formatGoogleDriveImageUrl(settings.kopUrl); const headerContent = kopUrl ? `` : `

PEMERINTAH PROVINSI BALI

SMA NEGERI 1 ABIANSEMAL

Alamat: Jalan Raya Abiansemal, Badung, Bali

`; // Identity Info let identityContent = ''; if (activeTab === 'GURU') { identityContent = `
Nama Guru: ${selectedFilter}
Mata Pelajaran: ${filteredEntries[0]?.subject || '-'}
`; } else { const dateRange = filteredEntries.length > 0 ? `${formatDate(filteredEntries[0].date)} s/d ${formatDate(filteredEntries[filteredEntries.length - 1].date)}` : '-'; identityContent = `
Kelas: ${selectedFilter}
Hari / Tanggal: ${dateRange}
`; } // Table Content let tableHead = ''; if (activeTab === 'GURU') { tableHead = ` No. Hari/ Tanggal Kelas Jam Materi Siswa Tidak Hadir Foto Mengajar `; } else { tableHead = ` No. Hari/ Tanggal Nama Guru Jam Ke Kehadiran Siswa Tidak Hadir Keterangan `; } const tableBody = targetEntries.map((entry, idx) => { const absentStr = getAbsentString(entry); if (activeTab === 'GURU') { // IMPORTANT: Convert to embeddable URL const displayUrl = formatGoogleDriveImageUrl(entry.photoUrl || ''); const imgHtml = displayUrl ? `Foto` : '-'; return ` ${idx + 1} ${formatDate(entry.date)} ${entry.className} ${entry.startTime} - ${entry.endTime} ${entry.topic} ${absentStr} ${imgHtml} `; } else { // Class Journal Row return ` ${idx + 1} ${formatDate(entry.date)} ${entry.teacherName} ${entry.startTime} - ${entry.endTime} ${entry.teacherPresence || '-'} ${absentStr} ${entry.notes || '-'} `; } }).join(''); // Document HTML printWindow.document.write(` Cetak Laporan - ${selectedFilter} ${headerContent}
${title}
SEMESTER ${settings.semester.toUpperCase()}
TAHUN PELAJARAN ${settings.academicYear}
${identityContent} ${tableHead}${tableBody}
`); printWindow.document.close(); }; // --- PDF GENERATION LOGIC --- const generatePDF = async () => { const targetEntries = getTargetEntries(); if (!targetEntries.length) return; setIsGeneratingPdf(true); try { const targetEntries = getTargetEntries(); const doc = new jsPDF('l', 'mm', 'a4'); // Landscape, mm, A4 // --- 1. Page Setup & Margins --- const docWidth = doc.internal.pageSize.getWidth(); // 297mm // const docHeight = doc.internal.pageSize.getHeight(); // 210mm const margin = 20; // 2cm margin all sides // Initial Y Position starts at margin let yPos = margin; // --- 2. HEADER (KOP) --- // Requested Size: 25.78 cm x 5.27 cm -> 257.8 mm x 52.7 mm const kopWidth = 257.8; const kopHeight = 52.7; // Center the Kop horizontally relative to the page width const xKop = (docWidth - kopWidth) / 2; if (settings.kopUrl) { try { const formattedKopUrl = formatGoogleDriveImageUrl(settings.kopUrl); const kopBase64 = await getBase64ImageFromURL(formattedKopUrl); doc.addImage(kopBase64, 'JPEG', xKop, yPos, kopWidth, kopHeight); yPos += kopHeight + 5; // Add padding below Kop } catch (e) { console.error("Error adding KOP image", e); // Fallback Text doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text("PEMERINTAH PROVINSI BALI", docWidth / 2, yPos + 10, { align: 'center' }); doc.text("SMA NEGERI 1 ABIANSEMAL", docWidth / 2, yPos + 20, { align: 'center' }); yPos += 30; } } else { // Default Text Header doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text("PEMERINTAH PROVINSI BALI", docWidth / 2, yPos + 10, { align: 'center' }); doc.text("SMA NEGERI 1 ABIANSEMAL", docWidth / 2, yPos + 20, { align: 'center' }); yPos += 30; } // --- 3. TITLES --- doc.setFontSize(11); doc.setFont('helvetica', 'bold'); if (activeTab === 'GURU') { doc.text("JURNAL MENGAJAR GURU", docWidth / 2, yPos, { align: 'center' }); } else { doc.text(`JURNAL KELAS ${selectedFilter}`, docWidth / 2, yPos, { align: 'center' }); } yPos += 5; doc.setFont('helvetica', 'normal'); doc.setFontSize(10); doc.text(`SEMESTER ${settings.semester.toUpperCase()}`, docWidth / 2, yPos, { align: 'center' }); yPos += 5; doc.text(`TAHUN PELAJARAN ${settings.academicYear}`, docWidth / 2, yPos, { align: 'center' }); yPos += 10; // --- 4. IDENTITY SECTION --- doc.setFontSize(10); doc.setFont('helvetica', 'normal'); const identityX = margin + 5; // Slight offset from margin if (activeTab === 'GURU') { doc.text(`Nama Guru : ${selectedFilter}`, identityX, yPos); yPos += 5; const mapel = targetEntries[0]?.subject || '-'; doc.text(`Mata Pelajaran : ${mapel}`, identityX, yPos); } else { const dateText = targetEntries.length > 0 ? `${formatDate(targetEntries[0].date)} s/d ${formatDate(targetEntries[targetEntries.length - 1].date)}` : '-'; doc.text(`Hari/ Tanggal : ${dateText}`, identityX, yPos); } yPos += 8; // --- 5. PREPARE DATA --- let tableHeaders = []; let tableData = []; if (activeTab === 'GURU') { tableHeaders = [['No.', 'Hari/ Tanggal', 'Kelas', 'Jam', 'Materi', 'Siswa Tidak Hadir', 'Foto Mengajar']]; // Pre-fetch images for PDF tableData = await Promise.all(targetEntries.map(async (entry, index) => { let photoContent: any = '-'; if (entry.photoUrl) { const url = formatGoogleDriveImageUrl(entry.photoUrl); try { const base64 = await getBase64ImageFromURL(url); photoContent = base64; } catch (error) { photoContent = 'Foto Error'; } } return [ index + 1, formatDate(entry.date), entry.className, `${entry.startTime}-${entry.endTime}`, entry.topic, getAbsentString(entry), photoContent ]; })); } else { tableHeaders = [['No.', 'Hari/ Tanggal', 'Nama Guru', 'Jam', 'Kehadiran', 'Siswa Tidak Hadir', 'Keterangan']]; tableData = targetEntries.map((entry, index) => [ index + 1, formatDate(entry.date), entry.teacherName, `${entry.startTime}-${entry.endTime}`, entry.teacherPresence || '-', getAbsentString(entry), entry.notes || '-' ]); } // --- 6. DRAW TABLE --- (doc as any).autoTable({ startY: yPos, head: tableHeaders, body: tableData, theme: 'grid', // Set explicit margins: 2cm all around. This ensures Page 2+ respects the margin. margin: { top: margin, right: margin, bottom: margin, left: margin }, headStyles: { fillColor: [240, 240, 240], textColor: [0, 0, 0], lineWidth: 0.1, lineColor: [0, 0, 0], halign: 'center', valign: 'middle', fontStyle: 'bold' }, styles: { fontSize: 9, cellPadding: 2, lineColor: [0, 0, 0], lineWidth: 0.1, textColor: [0, 0, 0], valign: 'middle', overflow: 'linebreak' }, // Define Column Widths - Ensure they sum up to roughly usable width (257mm) columnStyles: activeTab === 'GURU' ? { 0: { cellWidth: 10, halign: 'center' }, 1: { cellWidth: 28 }, // Reduced to make space 2: { cellWidth: 18, halign: 'center' }, 3: { cellWidth: 18, halign: 'center' }, 4: { cellWidth: 'auto' }, // Materi takes remaining space 5: { cellWidth: 40, textColor: [220, 38, 38] }, // Absen red // Width 42mm to accommodate 40mm (4cm) photo with padding 6: { cellWidth: 42, halign: 'center' } } : { 0: { cellWidth: 10, halign: 'center' }, 1: { cellWidth: 35 }, 2: { cellWidth: 55 }, 3: { cellWidth: 22, halign: 'center' }, 4: { cellWidth: 25, halign: 'center' }, 5: { cellWidth: 55, textColor: [220, 38, 38] }, 6: { cellWidth: 'auto' } // Keterangan fills the rest ensuring full margin width }, // --- 7. IMAGE RENDERING (Fixed Size 4cm x 3cm) --- didDrawCell: (data: any) => { if (activeTab === 'GURU' && data.column.index === 6 && data.cell.section === 'body') { const content = data.cell.raw; if (typeof content === 'string' && content.startsWith('data:image')) { // Specific Dimensions as requested: Width 4cm, Height 3cm const imgWidth = 40; // mm const imgHeight = 30; // mm // Center the image in the cell const xPos = data.cell.x + (data.cell.width - imgWidth) / 2; const yPos = data.cell.y + (data.cell.height - imgHeight) / 2; doc.addImage(content, 'JPEG', xPos, yPos, imgWidth, imgHeight); } } }, // Force row height to be sufficient for the 3cm image didParseCell: (data: any) => { if (activeTab === 'GURU' && data.column.index === 6 && data.section === 'body') { const rawContent = data.cell.raw; if (typeof rawContent === 'string' && rawContent.startsWith('data:image')) { // Set height slightly larger than 30mm to avoid clipping data.row.height = 32; // CRITICAL FIX: Clear the text content so the base64 string isn't printed data.cell.text = ['']; // Backup: make text invisible just in case data.cell.styles.textColor = [255, 255, 255]; } } } }); // --- 8. FOOTER (SIGNATURE) --- let finalY = (doc as any).lastAutoTable.finalY + 10; // Check if footer fits on current page, considering bottom margin const pageHeight = doc.internal.pageSize.getHeight(); const footerHeight = 40; // Approx height of signature block if (finalY + footerHeight > pageHeight - margin) { doc.addPage(); finalY = margin + 10; } const today = new Date(); const dateStrFooter = `Badung, ${today.getDate()} ${today.toLocaleString('id-ID', { month: 'long' })} ${today.getFullYear()}`; // Align right side (margin right: 20mm) // Assuming width around 60-70mm for signature block const footerX = docWidth - margin - 65; doc.text(dateStrFooter, footerX, finalY); doc.text("Kepala SMA Negeri 1 Abiansemal,", footerX, finalY + 5); doc.text(`(${settings.headmasterName || '.........................'})`, footerX, finalY + 25); doc.text(`NIP. ${settings.headmasterNip || '.........................'}`, footerX, finalY + 30); doc.save(`Jurnal_${activeTab}_${selectedFilter}.pdf`); } catch (error) { console.error("PDF Generation Error:", error); alert("Gagal membuat PDF. Pastikan gambar dapat diakses (tidak diblokir CORS)."); } finally { setIsGeneratingPdf(false); } }; return (

Rekapitulasi Jurnal

Filter, Cetak, dan Export data jurnal mengajar

{/* Admin View Mode Toggle */} {currentUser.role === 'ADMIN' && (
)}
{/* Filter Controls */} {currentUser.role === 'ADMIN' && adminViewMode === 'journal' ? ( /* ========== ADMIN ENHANCED FILTERS ========== */
{/* Top Row: Stats Cards */}

Jurnal Guru Hari Ini

{todayStats.guruToday}

Jurnal Kelas Hari Ini

{todayStats.kelasToday}

Total Hari Ini

{todayStats.total}

{/* Tab Switch */}
{/* Quick Date Presets */}
Filter Cepat:
{/* Advanced Filters Panel */} {showAdvancedFilters && (
{/* Date From */}
setDateFrom(e.target.value)} className="w-full pl-10 pr-3 py-2 rounded-lg border border-slate-200 focus:border-school-800 focus:ring-2 focus:ring-school-100 outline-none text-sm" />
{/* Date To */}
setDateTo(e.target.value)} className="w-full pl-10 pr-3 py-2 rounded-lg border border-slate-200 focus:border-school-800 focus:ring-2 focus:ring-school-100 outline-none text-sm" />
{/* Teacher/Class Selection */}
{/* Search */}
setSearchQuery(e.target.value)} placeholder="Cari materi, guru, kelas..." className="w-full pl-10 pr-3 py-2 rounded-lg border border-slate-200 focus:border-school-800 focus:ring-2 focus:ring-school-100 outline-none text-sm" />
{/* Reset Filters Button */}
)}
) : currentUser.role === 'ADMIN' && adminViewMode === 'statistics' ? ( /* ========== ADMIN STATISTICS VIEW ========== */
{/* Statistics Header */}

Statistik Kehadiran Guru

Rekap guru yang mengisi jurnal {statsPeriod === 'week' ? 'mingguan' : 'bulanan'}

{/* Period Toggle */}
{/* Print Button */}
{/* Statistics Cards Grid */}
{statsPeriod === 'week' ? ( /* Weekly Statistics */ weeklyStats.map((week, weekIdx) => (

{week.weekLabel}

{week.startDate.toLocaleDateString('id-ID', { day: 'numeric', month: 'short' })} - {week.endDate.toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' })}

{week.teachers.length}

Guru Aktif

{week.teachers.length > 0 ? ( {week.teachers.map((teacher, tIdx) => ( ))}
Nama Guru Jurnal Hari
{teacher.name} {teacher.count} {teacher.dates.length}
) : (

Belum ada jurnal

)}
)) ) : ( /* Monthly Statistics */ monthlyStats.map((month, monthIdx) => (

{month.monthLabel}

{new Date(month.year, month.month).toLocaleString('id-ID', { month: 'long', year: 'numeric' })}

{month.teachers.length}

Guru Aktif

{month.teachers.length > 0 ? ( {month.teachers.map((teacher, tIdx) => ( ))}
Nama Guru Jurnal Hari Aktif
{teacher.name} {teacher.count} {teacher.uniqueDays}
) : (

Belum ada jurnal

)}
)) )}
{/* Teachers Without Journals Warning */} {teachersWithoutJournals.length > 0 && (

⚠️ Guru Belum Mengisi Jurnal ({statsPeriod === 'week' ? 'Minggu Ini' : 'Bulan Ini'})

{teachersWithoutJournals.length} dari {teachers.length} guru belum mengisi jurnal mengajar

{teachersWithoutJournals.slice(0, 10).map((teacher, idx) => ( {teacher.name} ))} {teachersWithoutJournals.length > 10 && ( +{teachersWithoutJournals.length - 10} lainnya )}
)} {/* Summary Stats */}

{teachers.length}

Total Guru

{statsPeriod === 'week' ? weeklyStats[0]?.teachers.length || 0 : monthlyStats[0]?.teachers.length || 0}

Guru Aktif

{teachersWithoutJournals.length}

Belum Mengisi

{statsPeriod === 'week' ? weeklyStats[0]?.teachers.reduce((sum, t) => sum + t.count, 0) || 0 : monthlyStats[0]?.teachers.reduce((sum, t) => sum + t.count, 0) || 0 }

Total Jurnal

) : currentUser.role === 'ADMIN' && adminViewMode === 'attendance' ? ( /* ========== ADMIN TEACHER ATTENDANCE VIEW ========== */
{/* Attendance Header */}

Laporan Kehadiran Guru

Berdasarkan Jumlah Baris Data Jurnal Kelas

{/* Print Button */}
{/* Period Filter */}
setAttendanceDate(e.target.value)} className="px-3 py-2 rounded-lg border border-slate-200 text-sm outline-none focus:border-teal-500 focus:ring-2 focus:ring-teal-100" /> → {teacherAttendanceStats.periodLabel}
{/* Summary Cards */}

{teacherAttendanceStats.summary.totalHadir}

Hadir

{teacherAttendanceStats.summary.totalTugas}

Tugas

{teacherAttendanceStats.summary.totalTidakHadir}

Tidak Hadir

{teacherAttendanceStats.summary.totalEntries}

Total Entri

{teacherAttendanceStats.summary.totalTeachers}

Guru Tercatat

{/* Teachers Attendance Table */}

Detail Kehadiran per Guru

Periode: {teacherAttendanceStats.periodLabel}

{teacherAttendanceStats.teachers.length > 0 ? (
{teacherAttendanceStats.teachers.map((teacher, idx) => ( ))}
No Nama Guru Hadir Tugas Tidak Hadir Total % Kehadiran
{idx + 1} {teacher.name} {teacher.hadir} {teacher.tugas} {teacher.tidakHadir} {teacher.total}
= 80 ? 'bg-emerald-500' : teacher.hadirPercentage >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`} style={{ width: `${teacher.hadirPercentage}%` }} />
= 80 ? 'text-emerald-600' : teacher.hadirPercentage >= 50 ? 'text-yellow-600' : 'text-red-600'}`}> {teacher.hadirPercentage}%
) : (

Tidak ada data kehadiran

Belum ada entri jurnal kelas untuk periode ini

)}
{/* Info Note */}

Tentang Laporan Kehadiran Guru

Data kehadiran guru diambil dari field "Kehadiran Guru" yang diisi oleh Sekretaris Kelas saat mengisi Jurnal Kelas. Status kehadiran terdiri dari: Hadir, Tugas (memberikan tugas tanpa hadir langsung), dan Tidak Hadir.

) : ( /* ========== NON-ADMIN FILTERS (Enhanced with Week/Month/Date) ========== */
{activeTab === 'GURU' ? : } {activeTab === 'GURU' ? 'LAPORAN GURU' : 'LAPORAN KELAS'}
{selectedFilter || currentUser.name}
setDateFrom(e.target.value)} className="w-full px-3 py-2 rounded-lg border border-slate-200 text-sm outline-none focus:border-school-800" />
s/d
setDateTo(e.target.value)} className="w-full px-3 py-2 rounded-lg border border-slate-200 text-sm outline-none focus:border-school-800" />
setSearchQuery(e.target.value)} className="w-full pl-10 pr-3 py-2 rounded-lg border border-slate-200 text-sm outline-none focus:border-school-800" />
)} {/* Results & Action - Only show for Journal view */} {((currentUser.role === 'ADMIN' && adminViewMode === 'journal') || (currentUser.role !== 'ADMIN' && selectedFilter)) && (

{filteredEntries.length} Data Ditemukan

Siap untuk dicetak atau diexport

{/* Preview Table */}
{activeTab === 'GURU' ? ( <> {currentUser.role === 'ADMIN' && selectedFilter === 'ALL' && ( )} ) : ( <> {currentUser.role === 'ADMIN' && selectedFilter === 'ALL' && ( )} )} {(currentUser.role === 'ADMIN' || currentUser.role === 'SEKRETARIS') && ( )} {paginatedEntries.map((entry, idx) => ( {activeTab === 'GURU' ? ( <> {currentUser.role === 'ADMIN' && selectedFilter === 'ALL' && ( )} ) : ( <> {currentUser.role === 'ADMIN' && selectedFilter === 'ALL' && ( )} )} {(currentUser.role === 'ADMIN' || currentUser.role === 'SEKRETARIS') && ( )} ))}
0 && selectedEntryIds.size === filteredEntries.length} onChange={toggleSelectAll} /> No TanggalGuruKelas Jam Ke Materi Siswa Absen Foto MengajarGuruKelasJam Ke Kehadiran Guru Siswa Absen KeteranganAksi
toggleSelectEntry(entry.id)} /> {(currentPage - 1) * itemsPerPage + idx + 1} {formatDate(entry.date)}{entry.teacherName}{entry.className} {entry.startTime}-{entry.endTime} {entry.topic} {getAbsentString(entry)} {entry.photoUrl ? ( ) : ( - )} {entry.teacherName}{entry.className}{entry.startTime}-{entry.endTime} {entry.teacherPresence || '-'} {getAbsentString(entry)} {entry.notes || '-'}
{/* Pagination Controls - Always show for better UX */}

Menampilkan {filteredEntries.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} - {Math.min(currentPage * itemsPerPage, filteredEntries.length)} dari {filteredEntries.length} data

{/* Items per page selector */}
Tampilkan: per halaman
{totalPages > 1 && (
{[...Array(totalPages)].map((_, i) => { // Show only limited page numbers if there are too many if (totalPages > 5 && Math.abs(currentPage - (i + 1)) > 2) return null; return ( ); })}
)}
)}
{/* Edit Modal */} {editingEntry && (
!isProcessing && setEditingEntry(null)}>
e.stopPropagation()}>

Edit Jurnal

{activeTab === 'GURU' ? ( <>
handleEditFieldChange('topic', e.target.value)} className="w-full px-4 py-2 rounded-lg border border-slate-200 focus:border-school-800 outline-none" />
handleEditFieldChange('startTime', e.target.value)} className="w-full px-4 py-2 rounded-lg border border-slate-200 focus:border-school-800 outline-none" />
handleEditFieldChange('endTime', e.target.value)} className="w-full px-4 py-2 rounded-lg border border-slate-200 focus:border-school-800 outline-none" />