Files
smanab/App-Jurnal/components/RecapView.tsx

2373 lines
138 KiB
TypeScript
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string> => {
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<RecapViewProps> = ({ 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<string>(() => {
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<string>(() => {
if (currentUser.role === 'ADMIN') return getTodayDate();
return '';
});
const [dateTo, setDateTo] = useState<string>(() => {
if (currentUser.role === 'ADMIN') return getTodayDate();
return '';
});
// Search query for Admin
const [searchQuery, setSearchQuery] = useState<string>('');
// Show filter panel for Admin
const [showAdvancedFilters, setShowAdvancedFilters] = useState<boolean>(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<string>(getTodayDate());
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
// Admin action states
const [editingEntry, setEditingEntry] = useState<any>(null);
const [deletingEntry, setDeletingEntry] = useState<any>(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<Set<string>>(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<string, { count: number; dates: Set<string> }>();
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<string, { count: number; dates: Set<string> }>();
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<string, {
hadir: number;
tugas: number;
tidakHadir: number;
total: number;
details: { date: string; className: string; subject: string; status: string; jam: string }[]
}>();
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
? `<img src="${kopUrl}" style="width: 100%; max-height: 5cm; object-fit: contain; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto;" />`
: `<div style="text-align: center; font-weight: bold; margin-bottom: 20px; border-bottom: 3px double black; padding-bottom: 10px;">
<h3 style="margin:0">PEMERINTAH PROVINSI BALI</h3>
<h2 style="margin:5px 0">SMA NEGERI 1 ABIANSEMAL</h2>
<p style="margin:0; font-weight: normal; font-size: 12px;">Alamat: Jalan Raya Abiansemal, Badung, Bali</p>
</div>`;
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 += `
<div style="margin-bottom: 30px; page-break-inside: avoid;">
<h4 style="margin: 10px 0; color: #1e3a5f; border-bottom: 2px solid #1e3a5f; padding-bottom: 5px;">
${week.weekLabel} (${startStr} - ${endStr})
</h4>
<table class="data">
<thead>
<tr>
<th style="width: 8%">No.</th>
<th style="width: 45%">Nama Guru</th>
<th style="width: 20%">Jumlah Jurnal</th>
<th style="width: 27%">Hari Mengisi</th>
</tr>
</thead>
<tbody>
${week.teachers.length > 0 ? week.teachers.map((t, idx) => `
<tr>
<td style="text-align: center">${idx + 1}</td>
<td>${t.name}</td>
<td style="text-align: center; font-weight: bold; color: #16a34a">${t.count}</td>
<td style="font-size: 10px">${t.dates.length} hari</td>
</tr>
`).join('') : `
<tr>
<td colspan="4" style="text-align: center; color: #888; font-style: italic">Belum ada data jurnal</td>
</tr>
`}
</tbody>
</table>
<p style="font-size: 11px; color: #666; margin-top: 5px;">Total: ${week.teachers.length} guru telah mengisi jurnal</p>
</div>
`;
});
} else {
// Monthly Statistics
monthlyStats.forEach((month, monthIdx) => {
const monthName = new Date(month.year, month.month).toLocaleString('id-ID', { month: 'long', year: 'numeric' });
tableContent += `
<div style="margin-bottom: 30px; page-break-inside: avoid;">
<h4 style="margin: 10px 0; color: #1e3a5f; border-bottom: 2px solid #1e3a5f; padding-bottom: 5px;">
${month.monthLabel} (${monthName})
</h4>
<table class="data">
<thead>
<tr>
<th style="width: 8%">No.</th>
<th style="width: 40%">Nama Guru</th>
<th style="width: 25%">Jumlah Jurnal</th>
<th style="width: 27%">Hari Aktif Mengisi</th>
</tr>
</thead>
<tbody>
${month.teachers.length > 0 ? month.teachers.map((t, idx) => `
<tr>
<td style="text-align: center">${idx + 1}</td>
<td>${t.name}</td>
<td style="text-align: center; font-weight: bold; color: #16a34a">${t.count}</td>
<td style="text-align: center">${t.uniqueDays} hari</td>
</tr>
`).join('') : `
<tr>
<td colspan="4" style="text-align: center; color: #888; font-style: italic">Belum ada data jurnal</td>
</tr>
`}
</tbody>
</table>
<p style="font-size: 11px; color: #666; margin-top: 5px;">Total: ${month.teachers.length} guru telah mengisi jurnal</p>
</div>
`;
});
}
// Teachers without journals section
const noJournalSection = teachersWithoutJournals.length > 0 ? `
<div style="margin-top: 30px; padding: 15px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px;">
<h4 style="margin: 0 0 10px 0; color: #dc2626;">⚠️ Guru yang Belum Mengisi Jurnal (${statsPeriod === 'week' ? 'Minggu Ini' : 'Bulan Ini'})</h4>
<p style="font-size: 12px; color: #666; margin: 0;">
${teachersWithoutJournals.map(t => t.name).join(', ')}
</p>
</div>
` : '';
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Statistik Kehadiran Guru - ${statsPeriod === 'week' ? 'Mingguan' : 'Bulanan'}</title>
<style>
body { font-family: Arial, sans-serif; font-size: 12px; color: #000; -webkit-print-color-adjust: exact; box-sizing: border-box; }
table.data { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
table.data th, table.data td { border: 1px solid #000; padding: 8px 10px; vertical-align: middle; }
table.data th { background-color: #f0f0f0; font-weight: bold; text-align: center; }
.header-title { text-align: center; margin-bottom: 25px; font-weight: bold; font-size: 14px; margin-top: 15px; }
.footer { margin-top: 30px; float: right; width: 250px; text-align: left; page-break-inside: avoid; }
@media print {
@page { size: A4 portrait; margin: 20mm; }
body { padding: 0; margin: 0; }
}
@media screen {
body { padding: 20mm; max-width: 210mm; margin: auto; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
}
</style>
</head>
<body>
${headerContent}
<div class="header-title">
STATISTIK PENGISIAN JURNAL MENGAJAR GURU<br>
${statsPeriod === 'week' ? 'LAPORAN MINGGUAN' : 'LAPORAN BULANAN'}<br>
SEMESTER ${settings.semester.toUpperCase()} - TAHUN PELAJARAN ${settings.academicYear}
</div>
${tableContent}
${noJournalSection}
<div class="footer">
<p>${dateStr}</p>
<p>Kepala SMA Negeri 1 Abiansemal,</p>
<br><br><br>
<p style="font-weight: bold; text-decoration: underline;">${settings.headmasterName || '(.........................)'}</p>
<p>NIP. ${settings.headmasterNip || '-'}</p>
</div>
<script>
window.onload = function() {
setTimeout(function() {
window.print();
}, 1000);
}
</script>
</body>
</html>
`);
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
? `<img src="${kopUrl}" style="width: 100%; max-height: 5cm; object-fit: contain; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto;" />`
: `<div style="text-align: center; font-weight: bold; margin-bottom: 20px; border-bottom: 3px double black; padding-bottom: 10px;">
<h3 style="margin:0">PEMERINTAH PROVINSI BALI</h3>
<h2 style="margin:5px 0">SMA NEGERI 1 ABIANSEMAL</h2>
<p style="margin:0; font-weight: normal; font-size: 12px;">Alamat: Jalan Raya Abiansemal, Badung, Bali</p>
</div>`;
// Identity Info
let identityContent = '';
if (activeTab === 'GURU') {
identityContent = `
<table style="width: 100%; margin-bottom: 15px; font-size: 12px; font-family: Arial;">
<tr><td style="width: 150px; font-weight: bold;">Nama Guru</td><td>: ${selectedFilter}</td></tr>
<tr><td style="font-weight: bold;">Mata Pelajaran</td><td>: ${filteredEntries[0]?.subject || '-'}</td></tr>
</table>
`;
} else {
const dateRange = filteredEntries.length > 0
? `${formatDate(filteredEntries[0].date)} s/d ${formatDate(filteredEntries[filteredEntries.length - 1].date)}`
: '-';
identityContent = `
<table style="width: 100%; margin-bottom: 15px; font-size: 12px; font-family: Arial;">
<tr><td style="width: 150px; font-weight: bold;">Kelas</td><td>: ${selectedFilter}</td></tr>
<tr><td style="font-weight: bold;">Hari / Tanggal</td><td>: ${dateRange}</td></tr>
</table>
`;
}
// Table Content
let tableHead = '';
if (activeTab === 'GURU') {
tableHead = `<tr>
<th style="width: 5%;">No.</th>
<th style="width: 12%;">Hari/ Tanggal</th>
<th style="width: 8%;">Kelas</th>
<th style="width: 8%;">Jam</th>
<th style="width: 20%;">Materi</th>
<th style="width: 20%;">Siswa Tidak Hadir</th>
<th style="width: 15%;">Foto Mengajar</th>
</tr>`;
} else {
tableHead = `<tr>
<th style="width: 5%;">No.</th>
<th style="width: 15%;">Hari/ Tanggal</th>
<th style="width: 20%;">Nama Guru</th>
<th style="width: 10%;">Jam Ke</th>
<th style="width: 10%;">Kehadiran</th>
<th style="width: 25%;">Siswa Tidak Hadir</th>
<th style="width: 15%;">Keterangan</th>
</tr>`;
}
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
? `<img src="${displayUrl}" alt="Foto" style="width: 4cm; height: 3cm; object-fit: cover; border-radius: 4px;" loading="lazy" />`
: '<span style="color:#ccc">-</span>';
return `
<tr>
<td style="text-align: center;">${idx + 1}</td>
<td>${formatDate(entry.date)}</td>
<td style="text-align: center; font-weight: bold;">${entry.className}</td>
<td style="text-align: center;">${entry.startTime} - ${entry.endTime}</td>
<td>${entry.topic}</td>
<td style="color: #dc2626; font-size: 11px;">${absentStr}</td>
<td style="text-align: center; padding: 5px;">${imgHtml}</td>
</tr>
`;
} else {
// Class Journal Row
return `
<tr>
<td style="text-align: center;">${idx + 1}</td>
<td>${formatDate(entry.date)}</td>
<td>${entry.teacherName}</td>
<td style="text-align: center;">${entry.startTime} - ${entry.endTime}</td>
<td style="text-align: center; font-weight: bold;">${entry.teacherPresence || '-'}</td>
<td style="color: #dc2626; font-size: 11px;">${absentStr}</td>
<td>${entry.notes || '-'}</td>
</tr>
`;
}
}).join('');
// Document HTML
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Cetak Laporan - ${selectedFilter}</title>
<style>
/* Default Styles for Screen and Print */
body { font-family: Arial, sans-serif; font-size: 12px; color: #000; -webkit-print-color-adjust: exact; box-sizing: border-box; }
table.data { width: 100%; border-collapse: collapse; margin-bottom: 20px; table-layout: fixed; }
table.data th, table.data td { border: 1px solid #000; padding: 6px 8px; vertical-align: middle; word-wrap: break-word; }
table.data th { background-color: #f0f0f0; font-weight: bold; text-align: center; text-transform: none; }
.header-title { text-align: center; margin-bottom: 20px; font-weight: bold; font-size: 14px; margin-top: 10px; }
.footer { margin-top: 20px; float: right; width: 250px; text-align: left; page-break-inside: avoid; }
/* Print Specific Styles - Ensures margins on ALL pages */
@media print {
@page {
size: A4 landscape;
margin: 20mm; /* This enforces physical margin on every page */
}
body {
padding: 0;
margin: 0;
}
thead { display: table-header-group; } /* Repeat headers */
tr { page-break-inside: avoid; }
}
/* Screen Preview Style */
@media screen {
body { padding: 20mm; max-width: 297mm; margin: auto; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
}
</style>
</head>
<body>
${headerContent}
<div class="header-title">
${title}<br>
SEMESTER ${settings.semester.toUpperCase()}<br>
TAHUN PELAJARAN ${settings.academicYear}
</div>
${identityContent}
<table class="data">
<thead>${tableHead}</thead>
<tbody>${tableBody}</tbody>
</table>
<div class="footer">
<p>${dateStr}</p>
<p>Kepala SMA Negeri 1 Abiansemal,</p>
<br><br><br>
<p style="font-weight: bold; text-decoration: underline;">${settings.headmasterName || '(.........................)'}</p>
<p>NIP. ${settings.headmasterNip || '-'}</p>
</div>
<script>
// Wait for images to load before printing
window.onload = function() {
setTimeout(function() {
window.print();
}, 1500); // Delay to ensure KOP and Photos load
}
</script>
</body>
</html>
`);
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 (
<div className="space-y-6 animate-fade-in pb-20">
<div className="bg-white rounded-2xl shadow-lg p-8 border border-slate-100">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-8">
<div className="flex items-center gap-4">
<div className="p-3 bg-school-900 text-white rounded-xl">
<Printer size={24} />
</div>
<div>
<h2 className="font-heading text-2xl font-bold text-school-900">Rekapitulasi Jurnal</h2>
<p className="text-slate-500">Filter, Cetak, dan Export data jurnal mengajar</p>
</div>
</div>
{/* Admin View Mode Toggle */}
{currentUser.role === 'ADMIN' && (
<div className="bg-slate-100 p-1 rounded-xl flex flex-wrap">
<button
onClick={() => setAdminViewMode('journal')}
className={`py-2.5 px-5 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${adminViewMode === 'journal' ? 'bg-white text-school-900 shadow-sm' : 'text-slate-500 hover:text-school-700'}`}
>
<FileText size={16} />
Data Jurnal
</button>
<button
onClick={() => setAdminViewMode('statistics')}
className={`py-2.5 px-5 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${adminViewMode === 'statistics' ? 'bg-white text-school-900 shadow-sm' : 'text-slate-500 hover:text-school-700'}`}
>
<BarChart3 size={16} />
Statistik Guru
</button>
<button
onClick={() => setAdminViewMode('attendance')}
className={`py-2.5 px-5 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${adminViewMode === 'attendance' ? 'bg-white text-school-900 shadow-sm' : 'text-slate-500 hover:text-school-700'}`}
>
<Users size={16} />
Kehadiran Guru
</button>
</div>
)}
</div>
{/* Filter Controls */}
{currentUser.role === 'ADMIN' && adminViewMode === 'journal' ? (
/* ========== ADMIN ENHANCED FILTERS ========== */
<div className="space-y-6 mb-8">
{/* Top Row: Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="bg-gradient-to-br from-emerald-50 to-emerald-100 border border-emerald-200 rounded-xl p-4 flex items-center gap-4">
<div className="bg-emerald-500 text-white p-3 rounded-lg">
<CheckCircle2 size={24} />
</div>
<div>
<p className="text-xs font-medium text-emerald-600 uppercase tracking-wide">Jurnal Guru Hari Ini</p>
<p className="text-2xl font-bold text-emerald-800">{todayStats.guruToday}</p>
</div>
</div>
<div className="bg-gradient-to-br from-blue-50 to-blue-100 border border-blue-200 rounded-xl p-4 flex items-center gap-4">
<div className="bg-blue-500 text-white p-3 rounded-lg">
<FileText size={24} />
</div>
<div>
<p className="text-xs font-medium text-blue-600 uppercase tracking-wide">Jurnal Kelas Hari Ini</p>
<p className="text-2xl font-bold text-blue-800">{todayStats.kelasToday}</p>
</div>
</div>
<div className="bg-gradient-to-br from-purple-50 to-purple-100 border border-purple-200 rounded-xl p-4 flex items-center gap-4">
<div className="bg-purple-500 text-white p-3 rounded-lg">
<Calendar size={24} />
</div>
<div>
<p className="text-xs font-medium text-purple-600 uppercase tracking-wide">Total Hari Ini</p>
<p className="text-2xl font-bold text-purple-800">{todayStats.total}</p>
</div>
</div>
</div>
{/* Tab Switch */}
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div className="bg-slate-100 p-1 rounded-xl flex">
<button
onClick={() => { setActiveTab('GURU'); setSelectedFilter('ALL'); }}
className={`py-2.5 px-6 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${activeTab === 'GURU' ? 'bg-white text-school-900 shadow-sm' : 'text-slate-500 hover:text-school-700'
}`}
>
<User size={16} />
Jurnal Guru
</button>
<button
onClick={() => { setActiveTab('KELAS'); setSelectedFilter('ALL'); }}
className={`py-2.5 px-6 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${activeTab === 'KELAS' ? 'bg-white text-school-900 shadow-sm' : 'text-slate-500 hover:text-school-700'
}`}
>
<FileText size={16} />
Jurnal Kelas
</button>
</div>
{/* Quick Date Presets */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-slate-500 font-medium">Filter Cepat:</span>
<button
onClick={() => setDatePreset('today')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all border ${dateFrom === getTodayDate() && dateTo === getTodayDate() ? 'bg-school-900 text-white border-school-900' : 'bg-white text-slate-600 border-slate-200 hover:border-school-800'}`}
>
Hari Ini
</button>
<button
onClick={() => setDatePreset('week')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all border bg-white text-slate-600 border-slate-200 hover:border-school-800`}
>
7 Hari
</button>
<button
onClick={() => setDatePreset('month')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all border bg-white text-slate-600 border-slate-200 hover:border-school-800`}
>
30 Hari
</button>
<button
onClick={() => setDatePreset('all')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all border ${!dateFrom && !dateTo ? 'bg-school-900 text-white border-school-900' : 'bg-white text-slate-600 border-slate-200 hover:border-school-800'}`}
>
Semua
</button>
<button
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
className="px-3 py-1.5 rounded-lg text-xs font-bold transition-all border bg-slate-100 text-slate-700 border-slate-200 hover:bg-slate-200 flex items-center gap-1"
>
<Filter size={12} />
{showAdvancedFilters ? 'Sembunyikan' : 'Filter Lanjutan'}
<ChevronDown size={12} className={`transition-transform ${showAdvancedFilters ? 'rotate-180' : ''}`} />
</button>
</div>
</div>
{/* Advanced Filters Panel */}
{showAdvancedFilters && (
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4 space-y-4 animate-fade-in">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Date From */}
<div>
<label className="block text-xs font-semibold text-slate-600 mb-1.5">Dari Tanggal</label>
<div className="relative">
<Calendar className="absolute left-3 top-2.5 text-slate-400" size={16} />
<input
type="date"
value={dateFrom}
onChange={(e) => 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"
/>
</div>
</div>
{/* Date To */}
<div>
<label className="block text-xs font-semibold text-slate-600 mb-1.5">Sampai Tanggal</label>
<div className="relative">
<Calendar className="absolute left-3 top-2.5 text-slate-400" size={16} />
<input
type="date"
value={dateTo}
onChange={(e) => 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"
/>
</div>
</div>
{/* Teacher/Class Selection */}
<div>
<label className="block text-xs font-semibold text-slate-600 mb-1.5">
{activeTab === 'GURU' ? 'Guru' : 'Kelas'}
</label>
<select
value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-slate-200 focus:border-school-800 focus:ring-2 focus:ring-school-100 outline-none text-sm appearance-none bg-white"
>
<option value="ALL">-- Semua {activeTab === 'GURU' ? 'Guru' : 'Kelas'} --</option>
{activeTab === 'GURU'
? teachers.map((t, i) => <option key={i} value={t.name}>{t.name}</option>)
: classes.map((c, i) => <option key={i} value={c}>{c}</option>)
}
</select>
</div>
{/* Search */}
<div>
<label className="block text-xs font-semibold text-slate-600 mb-1.5">Cari</label>
<div className="relative">
<Search className="absolute left-3 top-2.5 text-slate-400" size={16} />
<input
type="text"
value={searchQuery}
onChange={(e) => 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"
/>
</div>
</div>
</div>
{/* Reset Filters Button */}
<div className="flex justify-end">
<button
onClick={() => {
setDateFrom(getTodayDate());
setDateTo(getTodayDate());
setSelectedFilter('ALL');
setSearchQuery('');
}}
className="px-4 py-2 text-sm font-medium text-slate-600 hover:text-school-900 flex items-center gap-2 transition"
>
<RefreshCw size={14} />
Reset Filter
</button>
</div>
</div>
)}
</div>
) : currentUser.role === 'ADMIN' && adminViewMode === 'statistics' ? (
/* ========== ADMIN STATISTICS VIEW ========== */
<div className="space-y-6 mb-8">
{/* Statistics Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="p-3 bg-gradient-to-br from-violet-500 to-purple-600 text-white rounded-xl">
<TrendingUp size={24} />
</div>
<div>
<h3 className="font-bold text-xl text-school-900">Statistik Kehadiran Guru</h3>
<p className="text-sm text-slate-500">Rekap guru yang mengisi jurnal {statsPeriod === 'week' ? 'mingguan' : 'bulanan'}</p>
</div>
</div>
<div className="flex items-center gap-3">
{/* Period Toggle */}
<div className="bg-slate-100 p-1 rounded-lg flex">
<button
onClick={() => setStatsPeriod('week')}
className={`py-2 px-4 rounded-md text-sm font-bold transition-all ${statsPeriod === 'week' ? 'bg-white text-school-900 shadow-sm' : 'text-slate-500 hover:text-school-700'}`}
>
Mingguan
</button>
<button
onClick={() => setStatsPeriod('month')}
className={`py-2 px-4 rounded-md text-sm font-bold transition-all ${statsPeriod === 'month' ? 'bg-white text-school-900 shadow-sm' : 'text-slate-500 hover:text-school-700'}`}
>
Bulanan
</button>
</div>
{/* Print Button */}
<button
onClick={handlePrintStatistics}
className="bg-school-900 hover:bg-school-800 text-white px-4 py-2 rounded-lg font-bold text-sm transition shadow-lg flex items-center gap-2"
>
<Printer size={16} />
Cetak Statistik
</button>
</div>
</div>
{/* Statistics Cards Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{statsPeriod === 'week' ? (
/* Weekly Statistics */
weeklyStats.map((week, weekIdx) => (
<div key={weekIdx} className={`bg-white border rounded-xl overflow-hidden shadow-sm ${weekIdx === 0 ? 'border-emerald-300 ring-2 ring-emerald-100' : 'border-slate-200'}`}>
<div className={`px-4 py-3 ${weekIdx === 0 ? 'bg-gradient-to-r from-emerald-500 to-green-500 text-white' : 'bg-slate-100'}`}>
<div className="flex items-center justify-between">
<div>
<h4 className={`font-bold ${weekIdx === 0 ? 'text-white' : 'text-slate-700'}`}>{week.weekLabel}</h4>
<p className={`text-xs ${weekIdx === 0 ? 'text-emerald-100' : 'text-slate-500'}`}>
{week.startDate.toLocaleDateString('id-ID', { day: 'numeric', month: 'short' })} - {week.endDate.toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' })}
</p>
</div>
<div className={`text-right ${weekIdx === 0 ? 'text-white' : 'text-slate-700'}`}>
<p className="text-2xl font-bold">{week.teachers.length}</p>
<p className="text-xs">Guru Aktif</p>
</div>
</div>
</div>
<div className="p-4 max-h-64 overflow-y-auto">
{week.teachers.length > 0 ? (
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-slate-500 uppercase border-b">
<th className="pb-2">Nama Guru</th>
<th className="pb-2 text-center">Jurnal</th>
<th className="pb-2 text-center">Hari</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{week.teachers.map((teacher, tIdx) => (
<tr key={tIdx} className="hover:bg-slate-50">
<td className="py-2 font-medium text-slate-700">{teacher.name}</td>
<td className="py-2 text-center">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-emerald-100 text-emerald-700 font-bold text-sm">
{teacher.count}
</span>
</td>
<td className="py-2 text-center text-slate-500">{teacher.dates.length}</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="text-center py-8 text-slate-400">
<FileText size={32} className="mx-auto mb-2 opacity-50" />
<p className="text-sm">Belum ada jurnal</p>
</div>
)}
</div>
</div>
))
) : (
/* Monthly Statistics */
monthlyStats.map((month, monthIdx) => (
<div key={monthIdx} className={`bg-white border rounded-xl overflow-hidden shadow-sm ${monthIdx === 0 ? 'border-blue-300 ring-2 ring-blue-100' : 'border-slate-200'}`}>
<div className={`px-4 py-3 ${monthIdx === 0 ? 'bg-gradient-to-r from-blue-500 to-indigo-500 text-white' : 'bg-slate-100'}`}>
<div className="flex items-center justify-between">
<div>
<h4 className={`font-bold ${monthIdx === 0 ? 'text-white' : 'text-slate-700'}`}>{month.monthLabel}</h4>
<p className={`text-xs ${monthIdx === 0 ? 'text-blue-100' : 'text-slate-500'}`}>
{new Date(month.year, month.month).toLocaleString('id-ID', { month: 'long', year: 'numeric' })}
</p>
</div>
<div className={`text-right ${monthIdx === 0 ? 'text-white' : 'text-slate-700'}`}>
<p className="text-2xl font-bold">{month.teachers.length}</p>
<p className="text-xs">Guru Aktif</p>
</div>
</div>
</div>
<div className="p-4 max-h-64 overflow-y-auto">
{month.teachers.length > 0 ? (
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-slate-500 uppercase border-b">
<th className="pb-2">Nama Guru</th>
<th className="pb-2 text-center">Jurnal</th>
<th className="pb-2 text-center">Hari Aktif</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{month.teachers.map((teacher, tIdx) => (
<tr key={tIdx} className="hover:bg-slate-50">
<td className="py-2 font-medium text-slate-700">{teacher.name}</td>
<td className="py-2 text-center">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 text-blue-700 font-bold text-sm">
{teacher.count}
</span>
</td>
<td className="py-2 text-center text-slate-500">{teacher.uniqueDays}</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="text-center py-8 text-slate-400">
<FileText size={32} className="mx-auto mb-2 opacity-50" />
<p className="text-sm">Belum ada jurnal</p>
</div>
)}
</div>
</div>
))
)}
</div>
{/* Teachers Without Journals Warning */}
{teachersWithoutJournals.length > 0 && (
<div className="bg-gradient-to-br from-red-50 to-orange-50 border border-red-200 rounded-xl p-5">
<div className="flex items-start gap-3">
<div className="p-2 bg-red-100 rounded-lg">
<AlertTriangle size={20} className="text-red-600" />
</div>
<div className="flex-1">
<h4 className="font-bold text-red-800 mb-1">
Guru Belum Mengisi Jurnal ({statsPeriod === 'week' ? 'Minggu Ini' : 'Bulan Ini'})
</h4>
<p className="text-sm text-red-600 mb-3">
{teachersWithoutJournals.length} dari {teachers.length} guru belum mengisi jurnal mengajar
</p>
<div className="flex flex-wrap gap-2">
{teachersWithoutJournals.slice(0, 10).map((teacher, idx) => (
<span key={idx} className="inline-flex items-center gap-1 px-3 py-1 bg-white border border-red-200 rounded-full text-xs font-medium text-red-700">
<User size={12} />
{teacher.name}
</span>
))}
{teachersWithoutJournals.length > 10 && (
<span className="inline-flex items-center px-3 py-1 bg-red-100 rounded-full text-xs font-bold text-red-700">
+{teachersWithoutJournals.length - 10} lainnya
</span>
)}
</div>
</div>
</div>
</div>
)}
{/* Summary Stats */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="bg-slate-50 rounded-xl p-4 text-center">
<p className="text-3xl font-bold text-school-900">{teachers.length}</p>
<p className="text-xs text-slate-500 uppercase tracking-wide">Total Guru</p>
</div>
<div className="bg-emerald-50 rounded-xl p-4 text-center">
<p className="text-3xl font-bold text-emerald-700">
{statsPeriod === 'week' ? weeklyStats[0]?.teachers.length || 0 : monthlyStats[0]?.teachers.length || 0}
</p>
<p className="text-xs text-emerald-600 uppercase tracking-wide">Guru Aktif</p>
</div>
<div className="bg-red-50 rounded-xl p-4 text-center">
<p className="text-3xl font-bold text-red-700">{teachersWithoutJournals.length}</p>
<p className="text-xs text-red-600 uppercase tracking-wide">Belum Mengisi</p>
</div>
<div className="bg-blue-50 rounded-xl p-4 text-center">
<p className="text-3xl font-bold text-blue-700">
{statsPeriod === 'week'
? weeklyStats[0]?.teachers.reduce((sum, t) => sum + t.count, 0) || 0
: monthlyStats[0]?.teachers.reduce((sum, t) => sum + t.count, 0) || 0
}
</p>
<p className="text-xs text-blue-600 uppercase tracking-wide">Total Jurnal</p>
</div>
</div>
</div>
) : currentUser.role === 'ADMIN' && adminViewMode === 'attendance' ? (
/* ========== ADMIN TEACHER ATTENDANCE VIEW ========== */
<div className="space-y-6 mb-8">
{/* Attendance Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="p-3 bg-gradient-to-br from-teal-500 to-cyan-600 text-white rounded-xl">
<Users size={24} />
</div>
<div>
<h3 className="font-bold text-xl text-school-900">Laporan Kehadiran Guru</h3>
<p className="text-sm text-slate-500 font-medium text-emerald-600">Berdasarkan Jumlah Baris Data Jurnal Kelas</p>
</div>
</div>
{/* Print Button */}
<button
onClick={() => {
const printWindow = window.open('', '', 'height=600,width=1024');
if (!printWindow) return;
const kopUrl = settings.kopUrl ? formatGoogleDriveImageUrl(settings.kopUrl) : '';
const headerContent = kopUrl
? `<img src="${kopUrl}" style="width: 100%; max-height: 5cm; object-fit: contain; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto;" />`
: `<div style="text-align: center; font-weight: bold; margin-bottom: 20px; border-bottom: 3px double black; padding-bottom: 10px;">
<h3 style="margin:0">PEMERINTAH PROVINSI BALI</h3>
<h2 style="margin:5px 0">SMA NEGERI 1 ABIANSEMAL</h2>
<p style="margin:0; font-weight: normal; font-size: 12px;">Alamat: Jalan Raya Abiansemal, Badung, Bali</p>
</div>`;
const today = new Date();
const dateStr = `Badung, ${today.getDate()} ${today.toLocaleString('id-ID', { month: 'long' })} ${today.getFullYear()}`;
const tableRows = teacherAttendanceStats.teachers.map((t, idx) => `
<tr>
<td style="text-align: center">${idx + 1}</td>
<td>${t.name}</td>
<td style="text-align: center; background: #d1fae5; color: #065f46; font-weight: bold">${t.hadir}</td>
<td style="text-align: center; background: #dbeafe; color: #1e40af; font-weight: bold">${t.tugas}</td>
<td style="text-align: center; background: #fee2e2; color: #991b1b; font-weight: bold">${t.tidakHadir}</td>
<td style="text-align: center; font-weight: bold">${t.total}</td>
<td style="text-align: center">${t.hadirPercentage}%</td>
</tr>
`).join('');
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Laporan Kehadiran Guru - ${teacherAttendanceStats.periodLabel}</title>
<style>
body { font-family: Arial, sans-serif; font-size: 12px; color: #000; -webkit-print-color-adjust: exact; }
table.data { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
table.data th, table.data td { border: 1px solid #000; padding: 8px; vertical-align: middle; }
table.data th { background-color: #f0f0f0; font-weight: bold; text-align: center; }
.header-title { text-align: center; margin-bottom: 25px; font-weight: bold; font-size: 14px; margin-top: 15px; }
.footer { margin-top: 30px; float: right; width: 250px; text-align: left; }
.summary-box { display: flex; justify-content: space-around; margin-bottom: 20px; padding: 10px; background: #f8f8f8; border-radius: 8px; }
.summary-item { text-align: center; }
.summary-value { font-size: 20px; font-weight: bold; }
@media print { @page { size: A4 portrait; margin: 20mm; } body { padding: 0; margin: 0; } }
@media screen { body { padding: 20mm; max-width: 210mm; margin: auto; box-shadow: 0 0 10px rgba(0,0,0,0.1); } }
</style>
</head>
<body>
${headerContent}
<div class="header-title">
LAPORAN KEHADIRAN GURU<br>
BERDASARKAN JURNAL KELAS<br>
PERIODE: ${teacherAttendanceStats.periodLabel.toUpperCase()}<br>
SEMESTER ${settings.semester.toUpperCase()} - TAHUN PELAJARAN ${settings.academicYear}
</div>
<div class="summary-box">
<div class="summary-item">
<div class="summary-value" style="color: #059669">${teacherAttendanceStats.summary.totalHadir}</div>
<div>Hadir</div>
</div>
<div class="summary-item">
<div class="summary-value" style="color: #2563eb">${teacherAttendanceStats.summary.totalTugas}</div>
<div>Tugas</div>
</div>
<div class="summary-item">
<div class="summary-value" style="color: #dc2626">${teacherAttendanceStats.summary.totalTidakHadir}</div>
<div>Tidak Hadir</div>
</div>
<div class="summary-item">
<div class="summary-value">${teacherAttendanceStats.summary.totalEntries}</div>
<div>Total Entri</div>
</div>
</div>
<table class="data">
<thead>
<tr>
<th style="width: 5%">No.</th>
<th style="width: 35%">Nama Guru</th>
<th style="width: 12%">Hadir</th>
<th style="width: 12%">Tugas</th>
<th style="width: 12%">Tidak Hadir</th>
<th style="width: 12%">Total</th>
<th style="width: 12%">% Hadir</th>
</tr>
</thead>
<tbody>
${tableRows || '<tr><td colspan="7" style="text-align: center; color: #888">Tidak ada data</td></tr>'}
</tbody>
</table>
<p style="font-size: 11px; color: #666">Catatan: Data kehadiran berdasarkan pencatatan Jurnal Kelas oleh Sekretaris. Total ${teacherAttendanceStats.summary.totalTeachers} guru tercatat.</p>
<div class="footer">
<p>${dateStr}</p>
<p>Kepala SMA Negeri 1 Abiansemal,</p>
<br><br><br>
<p style="font-weight: bold; text-decoration: underline;">${settings.headmasterName || '(.........................)'}</p>
<p>NIP. ${settings.headmasterNip || '-'}</p>
</div>
<script>window.onload = function() { setTimeout(function() { window.print(); }, 1000); }</script>
</body>
</html>
`);
printWindow.document.close();
}}
className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-teal-500 to-cyan-600 text-white font-bold rounded-xl hover:from-teal-600 hover:to-cyan-700 transition-all shadow-lg shadow-teal-500/25"
>
<Printer size={18} />
Cetak Laporan
</button>
</div>
{/* Period Filter */}
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
<div className="bg-slate-100 p-1 rounded-xl flex">
<button
onClick={() => setAttendancePeriod('daily')}
className={`py-2 px-4 rounded-lg text-sm font-bold transition-all ${attendancePeriod === 'daily' ? 'bg-white text-school-900 shadow-sm' : 'text-slate-500 hover:text-school-700'}`}
>
Harian
</button>
<button
onClick={() => setAttendancePeriod('weekly')}
className={`py-2 px-4 rounded-lg text-sm font-bold transition-all ${attendancePeriod === 'weekly' ? 'bg-white text-school-900 shadow-sm' : 'text-slate-500 hover:text-school-700'}`}
>
Mingguan
</button>
<button
onClick={() => setAttendancePeriod('monthly')}
className={`py-2 px-4 rounded-lg text-sm font-bold transition-all ${attendancePeriod === 'monthly' ? 'bg-white text-school-900 shadow-sm' : 'text-slate-500 hover:text-school-700'}`}
>
Bulanan
</button>
</div>
<div className="flex items-center gap-2">
<Calendar size={18} className="text-slate-400" />
<input
type="date"
value={attendanceDate}
onChange={(e) => 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"
/>
<span className="text-sm text-slate-500 hidden sm:inline">
{teacherAttendanceStats.periodLabel}
</span>
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-2 sm:grid-cols-5 gap-4">
<div className="bg-gradient-to-br from-emerald-50 to-green-100 border border-emerald-200 rounded-xl p-4 text-center">
<p className="text-3xl font-bold text-emerald-700">{teacherAttendanceStats.summary.totalHadir}</p>
<p className="text-xs text-emerald-600 uppercase tracking-wide font-medium">Hadir</p>
</div>
<div className="bg-gradient-to-br from-blue-50 to-indigo-100 border border-blue-200 rounded-xl p-4 text-center">
<p className="text-3xl font-bold text-blue-700">{teacherAttendanceStats.summary.totalTugas}</p>
<p className="text-xs text-blue-600 uppercase tracking-wide font-medium">Tugas</p>
</div>
<div className="bg-gradient-to-br from-red-50 to-rose-100 border border-red-200 rounded-xl p-4 text-center">
<p className="text-3xl font-bold text-red-700">{teacherAttendanceStats.summary.totalTidakHadir}</p>
<p className="text-xs text-red-600 uppercase tracking-wide font-medium">Tidak Hadir</p>
</div>
<div className="bg-gradient-to-br from-slate-50 to-gray-100 border border-slate-200 rounded-xl p-4 text-center">
<p className="text-3xl font-bold text-slate-700">{teacherAttendanceStats.summary.totalEntries}</p>
<p className="text-xs text-slate-600 uppercase tracking-wide font-medium">Total Entri</p>
</div>
<div className="bg-gradient-to-br from-violet-50 to-purple-100 border border-violet-200 rounded-xl p-4 text-center">
<p className="text-3xl font-bold text-violet-700">{teacherAttendanceStats.summary.totalTeachers}</p>
<p className="text-xs text-violet-600 uppercase tracking-wide font-medium">Guru Tercatat</p>
</div>
</div>
{/* Teachers Attendance Table */}
<div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
<div className="px-5 py-4 bg-gradient-to-r from-slate-50 to-slate-100 border-b border-slate-200">
<h4 className="font-bold text-slate-800">Detail Kehadiran per Guru</h4>
<p className="text-xs text-slate-500">Periode: {teacherAttendanceStats.periodLabel}</p>
</div>
{teacherAttendanceStats.teachers.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-slate-600 uppercase border-b bg-slate-50">
<th className="px-4 py-3 text-center w-12">No</th>
<th className="px-4 py-3">Nama Guru</th>
<th className="px-4 py-3 text-center bg-emerald-50">Hadir</th>
<th className="px-4 py-3 text-center bg-blue-50">Tugas</th>
<th className="px-4 py-3 text-center bg-red-50">Tidak Hadir</th>
<th className="px-4 py-3 text-center">Total</th>
<th className="px-4 py-3 text-center">% Kehadiran</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{teacherAttendanceStats.teachers.map((teacher, idx) => (
<tr key={idx} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-3 text-center text-slate-500">{idx + 1}</td>
<td className="px-4 py-3 font-medium text-slate-800">{teacher.name}</td>
<td className="px-4 py-3 text-center bg-emerald-50/50">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-emerald-100 text-emerald-700 font-bold text-sm">
{teacher.hadir}
</span>
</td>
<td className="px-4 py-3 text-center bg-blue-50/50">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 text-blue-700 font-bold text-sm">
{teacher.tugas}
</span>
</td>
<td className="px-4 py-3 text-center bg-red-50/50">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-red-100 text-red-700 font-bold text-sm">
{teacher.tidakHadir}
</span>
</td>
<td className="px-4 py-3 text-center font-bold text-slate-700">{teacher.total}</td>
<td className="px-4 py-3 text-center">
<div className="flex items-center justify-center gap-2">
<div className="w-16 h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${teacher.hadirPercentage >= 80 ? 'bg-emerald-500' : teacher.hadirPercentage >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
style={{ width: `${teacher.hadirPercentage}%` }}
/>
</div>
<span className={`font-bold text-xs ${teacher.hadirPercentage >= 80 ? 'text-emerald-600' : teacher.hadirPercentage >= 50 ? 'text-yellow-600' : 'text-red-600'}`}>
{teacher.hadirPercentage}%
</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center py-12 text-slate-400">
<Users size={48} className="mx-auto mb-3 opacity-50" />
<p className="text-lg font-medium">Tidak ada data kehadiran</p>
<p className="text-sm">Belum ada entri jurnal kelas untuk periode ini</p>
</div>
)}
</div>
{/* Info Note */}
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4 flex items-start gap-3">
<Info size={20} className="text-blue-500 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-700">
<p className="font-medium">Tentang Laporan Kehadiran Guru</p>
<p className="text-blue-600">Data kehadiran guru diambil dari field "Kehadiran Guru" yang diisi oleh Sekretaris Kelas saat mengisi Jurnal Kelas. Status kehadiran terdiri dari: <strong>Hadir</strong>, <strong>Tugas</strong> (memberikan tugas tanpa hadir langsung), dan <strong>Tidak Hadir</strong>.</p>
</div>
</div>
</div>
) : (
/* ========== NON-ADMIN FILTERS (Enhanced with Week/Month/Date) ========== */
<div className="space-y-4 mb-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 flex items-center gap-2 font-bold text-school-900">
{activeTab === 'GURU' ? <Shield size={18} className="text-accent-green" /> : <Shield size={18} className="text-accent-yellow" />}
<span className="text-xs uppercase">{activeTab === 'GURU' ? 'LAPORAN GURU' : 'LAPORAN KELAS'}</span>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 flex items-center gap-3 text-slate-600">
<User size={18} className="text-school-900" />
<span className="text-xs font-bold truncate">
{selectedFilter || currentUser.name}
</span>
</div>
<div className="lg:col-span-2 flex items-center gap-2">
<div className="flex-1">
<input
type="date"
value={dateFrom}
onChange={(e) => 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"
/>
</div>
<span className="text-slate-400">s/d</span>
<div className="flex-1">
<input
type="date"
value={dateTo}
onChange={(e) => 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"
/>
</div>
</div>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setDatePreset('today')}
className={`px-4 py-2 rounded-lg text-xs font-bold border transition ${dateFrom === getTodayDate() && dateTo === getTodayDate() ? 'bg-school-900 text-white border-school-900' : 'bg-white text-slate-600 border-slate-200 hover:border-school-800'}`}
>
Hari Ini
</button>
<button
onClick={() => setDatePreset('week')}
className={`px-4 py-2 rounded-lg text-xs font-bold border transition bg-white text-slate-600 border-slate-200 hover:border-school-800`}
>
Minggu Ini (7 Hari)
</button>
<button
onClick={() => setDatePreset('month')}
className={`px-4 py-2 rounded-lg text-xs font-bold border transition bg-white text-slate-600 border-slate-200 hover:border-school-800`}
>
Bulan Ini (30 Hari)
</button>
<button
onClick={() => setDatePreset('all')}
className={`px-4 py-2 rounded-lg text-xs font-bold border transition bg-white text-slate-600 border-slate-200 hover:border-school-800`}
>
Semua Waktu
</button>
<div className="flex-1 min-w-[200px] relative">
<Search className="absolute left-3 top-2.5 text-slate-400" size={16} />
<input
type="text"
placeholder="Cari materi..."
value={searchQuery}
onChange={(e) => 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"
/>
</div>
</div>
</div>
)}
{/* Results & Action - Only show for Journal view */}
{((currentUser.role === 'ADMIN' && adminViewMode === 'journal') || (currentUser.role !== 'ADMIN' && selectedFilter)) && (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between bg-blue-50 p-4 rounded-xl border border-blue-100 gap-4">
<div className="flex items-center gap-3">
<FileText className="text-school-900" />
<div>
<p className="text-sm font-bold text-school-900">
{filteredEntries.length} Data Ditemukan
</p>
<p className="text-xs text-blue-600">
Siap untuk dicetak atau diexport
</p>
</div>
</div>
<div className="flex gap-3">
<button
onClick={handlePrint}
disabled={filteredEntries.length === 0}
className={`${selectedEntryIds.size > 0 ? 'bg-amber-50 border-amber-500 text-amber-700' : 'bg-white border-school-900 text-school-900'} border-2 hover:bg-school-50 px-4 py-2 rounded-lg font-bold text-sm transition shadow-sm flex items-center gap-2`}
>
<Printer size={16} /> {selectedEntryIds.size > 0 ? `Cetak Terpilih (${selectedEntryIds.size})` : 'Cetak Laporan'}
</button>
<button
onClick={generatePDF}
disabled={filteredEntries.length === 0 || isGeneratingPdf}
className={`${selectedEntryIds.size > 0 ? 'bg-emerald-600 hover:bg-emerald-700' : 'bg-school-900 hover:bg-school-800'} text-white px-4 py-2 rounded-lg font-bold text-sm transition shadow-lg flex items-center gap-2 ${isGeneratingPdf ? 'opacity-75 cursor-wait' : ''}`}
>
{isGeneratingPdf ? <Loader2 size={16} className="animate-spin" /> : <Download size={16} />}
{isGeneratingPdf ? 'Memproses...' : (selectedEntryIds.size > 0 ? `Export Terpilih (${selectedEntryIds.size})` : 'Export PDF')}
</button>
</div>
</div>
{/* Preview Table */}
<div className="overflow-x-auto border border-slate-200 rounded-xl">
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 text-slate-600 font-bold uppercase text-xs">
<tr>
<th className="p-3 text-center">
<input
type="checkbox"
className="w-4 h-4 rounded text-school-900 focus:ring-school-800"
checked={filteredEntries.length > 0 && selectedEntryIds.size === filteredEntries.length}
onChange={toggleSelectAll}
/>
</th>
<th className="p-3 text-center">No</th>
<th className="p-3">Tanggal</th>
{activeTab === 'GURU' ? (
<>
{currentUser.role === 'ADMIN' && selectedFilter === 'ALL' && (
<th className="p-3">Guru</th>
)}
<th className="p-3 text-center">Kelas</th>
<th className="p-3 text-center">Jam Ke</th>
<th className="p-3">Materi</th>
<th className="p-3">Siswa Absen</th>
<th className="p-3 text-center">Foto Mengajar</th>
</>
) : (
<>
<th className="p-3">Guru</th>
{currentUser.role === 'ADMIN' && selectedFilter === 'ALL' && (
<th className="p-3 text-center">Kelas</th>
)}
<th className="p-3 text-center">Jam Ke</th>
<th className="p-3 text-center">Kehadiran Guru</th>
<th className="p-3">Siswa Absen</th>
<th className="p-3">Keterangan</th>
</>
)}
{(currentUser.role === 'ADMIN' || currentUser.role === 'SEKRETARIS') && (
<th className="p-3 text-center">Aksi</th>
)}
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paginatedEntries.map((entry, idx) => (
<tr key={idx} className={`${selectedEntryIds.has(entry.id) ? 'bg-blue-50' : 'hover:bg-slate-50'} transition-colors`}>
<td className="p-3 text-center">
<input
type="checkbox"
className="w-4 h-4 rounded text-school-900 focus:ring-school-800"
checked={selectedEntryIds.has(entry.id)}
onChange={() => toggleSelectEntry(entry.id)}
/>
</td>
<td className="p-3 text-center">{(currentPage - 1) * itemsPerPage + idx + 1}</td>
<td className="p-3 whitespace-nowrap">{formatDate(entry.date)}</td>
{activeTab === 'GURU' ? (
<>
{currentUser.role === 'ADMIN' && selectedFilter === 'ALL' && (
<td className="p-3 font-bold text-slate-700">{entry.teacherName}</td>
)}
<td className="p-3 font-bold text-blue-700 text-center">{entry.className}</td>
<td className="p-3 text-center">{entry.startTime}-{entry.endTime}</td>
<td className="p-3">{entry.topic}</td>
<td className="p-3 text-xs text-red-600 font-medium">{getAbsentString(entry)}</td>
<td className="p-3 text-center">
{entry.photoUrl ? (
<div className="flex flex-col items-center gap-1">
<a href={formatGoogleDriveImageUrl(entry.photoUrl)} target="_blank" rel="noreferrer" className="relative block h-10 w-10 rounded overflow-hidden border border-slate-200 hover:border-blue-400 transition">
<img
src={formatGoogleDriveImageUrl(entry.photoUrl)}
alt="Bukti"
className="h-full w-full object-cover"
onError={(e) => (e.currentTarget.style.display = 'none')}
/>
</a>
</div>
) : (
<span className="text-xs text-slate-300">-</span>
)}
</td>
</>
) : (
<>
<td className="p-3 font-bold text-slate-700">{entry.teacherName}</td>
{currentUser.role === 'ADMIN' && selectedFilter === 'ALL' && (
<td className="p-3 font-bold text-blue-700 text-center">{entry.className}</td>
)}
<td className="p-3 text-center">{entry.startTime}-{entry.endTime}</td>
<td className="p-3 text-center font-bold">{entry.teacherPresence || '-'}</td>
<td className="p-3 text-xs text-red-600 font-medium">{getAbsentString(entry)}</td>
<td className="p-3">{entry.notes || '-'}</td>
</>
)}
{(currentUser.role === 'ADMIN' || currentUser.role === 'SEKRETARIS') && (
<td className="p-3 text-center">
<div className="flex items-center justify-center gap-2">
<button
onClick={() => setEditingEntry({ ...entry })}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition"
title="Edit Jurnal"
>
<Edit2 size={16} />
</button>
<button
onClick={() => setDeletingEntry(entry)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition"
title="Hapus Jurnal"
>
<Trash2 size={16} />
</button>
</div>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination Controls - Always show for better UX */}
<div className="flex flex-col sm:flex-row items-center justify-between border-t border-slate-100 pt-6 gap-4">
<div className="flex items-center gap-4">
<p className="text-sm text-slate-500">
Menampilkan <span className="font-bold text-slate-700">{filteredEntries.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0}</span> - <span className="font-bold text-slate-700">{Math.min(currentPage * itemsPerPage, filteredEntries.length)}</span> dari <span className="font-bold text-slate-700">{filteredEntries.length}</span> data
</p>
{/* Items per page selector */}
<div className="flex items-center gap-2">
<span className="text-sm text-slate-500">Tampilkan:</span>
<select
value={itemsPerPage}
onChange={(e) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1); // Reset to first page when changing items per page
}}
className="px-3 py-1.5 border border-slate-200 rounded-lg text-sm font-bold text-slate-700 bg-white focus:border-school-800 focus:ring-2 focus:ring-school-100 outline-none cursor-pointer"
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={15}>15</option>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={9999}>Semua</option>
</select>
<span className="text-sm text-slate-500">per halaman</span>
</div>
</div>
{totalPages > 1 && (
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="px-4 py-2 border border-slate-200 rounded-xl text-sm font-bold text-slate-600 hover:bg-slate-50 disabled:opacity-50 transition shadow-sm"
>
Sebelumnya
</button>
<div className="flex gap-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 (
<button
key={i}
onClick={() => setCurrentPage(i + 1)}
className={`w-10 h-10 rounded-xl text-sm font-bold transition ${currentPage === i + 1 ? 'bg-school-900 text-white shadow-md' : 'hover:bg-slate-100 text-slate-600'}`}
>
{i + 1}
</button>
);
})}
</div>
<button
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className="px-4 py-2 border border-slate-200 rounded-xl text-sm font-bold text-slate-600 hover:bg-slate-50 disabled:opacity-50 transition shadow-sm"
>
Selanjutnya
</button>
</div>
)}
</div>
</div>
)}
</div>
{/* Edit Modal */}
{editingEntry && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" onClick={() => !isProcessing && setEditingEntry(null)}>
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="bg-school-900 text-white p-4 rounded-t-2xl flex items-center justify-between">
<h3 className="font-bold flex items-center gap-2"><Edit2 size={18} /> Edit Jurnal</h3>
<button onClick={() => !isProcessing && setEditingEntry(null)} className="p-1 hover:bg-white/20 rounded">
<X size={20} />
</button>
</div>
<div className="p-6 space-y-4">
{activeTab === 'GURU' ? (
<>
<div>
<label className="block text-sm font-semibold text-slate-600 mb-1">Materi</label>
<input
type="text"
value={editingEntry.topic || ''}
onChange={(e) => handleEditFieldChange('topic', e.target.value)}
className="w-full px-4 py-2 rounded-lg border border-slate-200 focus:border-school-800 outline-none"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-slate-600 mb-1">Jam Mulai</label>
<input
type="text"
value={editingEntry.startTime || ''}
onChange={(e) => handleEditFieldChange('startTime', e.target.value)}
className="w-full px-4 py-2 rounded-lg border border-slate-200 focus:border-school-800 outline-none"
/>
</div>
<div>
<label className="block text-sm font-semibold text-slate-600 mb-1">Jam Selesai</label>
<input
type="text"
value={editingEntry.endTime || ''}
onChange={(e) => handleEditFieldChange('endTime', e.target.value)}
className="w-full px-4 py-2 rounded-lg border border-slate-200 focus:border-school-800 outline-none"
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-slate-600 mb-1">Keterangan</label>
<textarea
value={editingEntry.notes || ''}
onChange={(e) => handleEditFieldChange('notes', e.target.value)}
rows={2}
className="w-full px-4 py-2 rounded-lg border border-slate-200 focus:border-school-800 outline-none"
/>
</div>
</>
) : (
<>
<div>
<label className="block text-sm font-semibold text-slate-600 mb-1">Kehadiran Guru</label>
<select
value={editingEntry.teacherPresence || 'Hadir'}
onChange={(e) => handleEditFieldChange('teacherPresence', e.target.value)}
className="w-full px-4 py-2 rounded-lg border border-slate-200 focus:border-school-800 outline-none"
>
<option value="Hadir">Hadir</option>
<option value="Tugas">Tugas</option>
<option value="Tidak Hadir">Tidak Hadir</option>
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-slate-600 mb-1">Jam Mulai</label>
<input
type="text"
value={editingEntry.startTime || ''}
onChange={(e) => handleEditFieldChange('startTime', e.target.value)}
className="w-full px-4 py-2 rounded-lg border border-slate-200 focus:border-school-800 outline-none"
/>
</div>
<div>
<label className="block text-sm font-semibold text-slate-600 mb-1">Jam Selesai</label>
<input
type="text"
value={editingEntry.endTime || ''}
onChange={(e) => handleEditFieldChange('endTime', e.target.value)}
className="w-full px-4 py-2 rounded-lg border border-slate-200 focus:border-school-800 outline-none"
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-slate-600 mb-1">Keterangan</label>
<textarea
value={editingEntry.notes || ''}
onChange={(e) => handleEditFieldChange('notes', e.target.value)}
rows={2}
className="w-full px-4 py-2 rounded-lg border border-slate-200 focus:border-school-800 outline-none"
/>
</div>
</>
)}
<div className="flex gap-3 pt-4 border-t">
<button
onClick={() => setEditingEntry(null)}
disabled={isProcessing}
className="flex-1 py-2 px-4 rounded-lg border border-slate-300 text-slate-600 font-bold hover:bg-slate-50 transition"
>
Batal
</button>
<button
onClick={handleEditSubmit}
disabled={isProcessing}
className="flex-1 py-2 px-4 rounded-lg bg-school-900 text-white font-bold hover:bg-school-800 transition flex items-center justify-center gap-2"
>
{isProcessing ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
{isProcessing ? 'Menyimpan...' : 'Simpan'}
</button>
</div>
</div>
</div>
</div>
)}
{/* Delete Confirmation Dialog */}
{deletingEntry && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" onClick={() => !isProcessing && setDeletingEntry(null)}>
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md" onClick={e => e.stopPropagation()}>
<div className="p-6 text-center">
<div className="mx-auto h-16 w-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
<AlertTriangle size={32} className="text-red-600" />
</div>
<h3 className="text-xl font-bold text-slate-800 mb-2">Hapus Jurnal?</h3>
<p className="text-slate-500 mb-6">
Apakah Anda yakin ingin menghapus jurnal <span className="font-bold text-slate-700">{formatDate(deletingEntry.date)}</span>?
<br /><span className="text-red-500 text-sm">Tindakan ini tidak dapat dibatalkan.</span>
</p>
<div className="flex gap-3">
<button
onClick={() => setDeletingEntry(null)}
disabled={isProcessing}
className="flex-1 py-3 px-4 rounded-xl border border-slate-300 text-slate-600 font-bold hover:bg-slate-50 transition"
>
Batal
</button>
<button
onClick={handleDelete}
disabled={isProcessing}
className="flex-1 py-3 px-4 rounded-xl bg-red-600 text-white font-bold hover:bg-red-700 transition flex items-center justify-center gap-2"
>
{isProcessing ? <Loader2 size={16} className="animate-spin" /> : <Trash2 size={16} />}
{isProcessing ? 'Menghapus...' : 'Hapus'}
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default RecapView;