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

398 lines
19 KiB
TypeScript
Executable File

import React, { useState, useMemo } from 'react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell, Legend } from 'recharts';
import { DashboardStats, JournalEntry, ClassJournalEntry } from '../types';
import { Calendar as CalendarIcon, Users, Book, TrendingUp, ChevronLeft, ChevronRight, GraduationCap, Briefcase, BarChart2, Filter, X, Clock } from 'lucide-react';
interface DashboardProps {
stats: DashboardStats;
teacherEntries: JournalEntry[];
classEntries: ClassJournalEntry[];
}
const Dashboard: React.FC<DashboardProps> = ({ stats, teacherEntries, classEntries }) => {
// Use "today" as default selected date if it has entries, otherwise null or today
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date()); // Default to today
const months = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];
const getCalendarDays = () => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const days = [];
for (let i = 0; i < firstDay; i++) days.push(null);
for (let i = 1; i <= daysInMonth; i++) days.push(i);
return days;
};
const changeMonth = (offset: number) => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + offset, 1));
};
const hasEntryOnDate = (day: number) => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth() + 1;
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
return teacherEntries.some(e => e.date === dateStr) || classEntries.some(e => e.date === dateStr);
};
const isSelectedDay = (day: number) => {
if (!selectedDate) return false;
return (
selectedDate.getDate() === day &&
selectedDate.getMonth() === currentDate.getMonth() &&
selectedDate.getFullYear() === currentDate.getFullYear()
);
};
const handleDayClick = (day: number) => {
if (!day) return;
const newDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), day);
setSelectedDate(newDate);
};
// 1. "Kehadiran Guru per hari" Dashboard Logic
const dailyStats = useMemo(() => {
const targetDate = selectedDate || new Date();
const dateStr = targetDate.toISOString().split('T')[0];
// Count teachers present in class entries for this date
const presentTeachers = new Set(
classEntries
.filter(ce => ce.date === dateStr && (ce.teacherPresence === 'Hadir' || ce.teacherPresence === 'Tugas'))
.map(ce => ce.teacherName)
).size;
// Latest activities for this day (from both teacher and class journals)
const dailyActivities = [
...teacherEntries.filter(te => te.date === dateStr).map(te => ({ ...te, type: 'teacher' })),
...classEntries.filter(ce => ce.date === dateStr).map(ce => ({ ...ce, type: 'class' }))
].sort((a, b) => new Date(b.id.split('-')[1]).getTime() - new Date(a.id.split('-')[1]).getTime());
// Class performance for this specific day
const classPerfMap: Record<string, { present: number, absent: number }> = {};
classEntries.filter(ce => ce.date === dateStr).forEach(ce => {
if (!classPerfMap[ce.className]) classPerfMap[ce.className] = { present: 0, absent: 0 };
classPerfMap[ce.className].present += Number(ce.studentsPresent) || 0;
classPerfMap[ce.className].absent += Number(ce.studentsAbsent) || 0;
});
const dailyClassPerformance = Object.keys(classPerfMap).map(name => ({
name,
present: classPerfMap[name].present,
absent: classPerfMap[name].absent
})).sort((a, b) => a.name.localeCompare(b.name));
return {
presentTeachers,
dailyActivities,
dailyClassPerformance,
dateStr
};
}, [selectedDate, teacherEntries, classEntries]);
// Transform Weekly Stats for Chart (Calculated per date trend)
const weeklyData = useMemo(() => {
const daysMap = ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'];
const now = new Date();
const currentWeekData = daysMap.map((day, idx) => {
// Find date for this day of current week
const diff = idx - now.getDay();
const targetDay = new Date(now);
targetDay.setDate(now.getDate() + diff);
const targetDateStr = targetDay.toISOString().split('T')[0];
const count = classEntries.filter(ce =>
ce.date === targetDateStr &&
(ce.teacherPresence === 'Hadir' || ce.teacherPresence === 'Tugas')
).length;
return { day, count };
});
return currentWeekData;
}, [classEntries]);
return (
<div className="space-y-8 animate-in pb-20">
{/* Header Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="group relative overflow-hidden rounded-2xl p-6 text-white shadow-lg transition-all duration-300 hover:shadow-xl hover:-translate-y-1"
style={{ background: 'linear-gradient(135deg, #1e3a5f 0%, #3b6ea5 100%)' }}>
<div className="relative flex items-center gap-4">
<div className="p-3 bg-white/20 rounded-xl backdrop-blur-sm">
<Book size={28} className="text-amber-300" />
</div>
<div>
<p className="text-blue-200 text-sm font-medium">Total Jurnal Guru</p>
<h3 className="text-3xl font-heading font-bold">{stats.totalEntries}</h3>
</div>
</div>
</div>
<div className="group relative overflow-hidden rounded-2xl p-6 text-white shadow-lg transition-all duration-300 hover:shadow-xl hover:-translate-y-1"
style={{ background: 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)' }}>
<div className="relative flex items-center gap-4">
<div className="p-3 bg-white/20 rounded-xl backdrop-blur-sm">
<Clock size={28} className="text-white" />
</div>
<div>
<p className="text-white/80 text-sm font-medium">Kehadiran Guru (Harian)</p>
<h3 className="text-3xl font-heading font-bold">{dailyStats.presentTeachers}</h3>
<p className="text-[10px] text-white/60">Tgl: {dailyStats.dateStr}</p>
</div>
</div>
</div>
<div className="group relative overflow-hidden rounded-2xl p-6 text-white shadow-lg transition-all duration-300 hover:shadow-xl hover:-translate-y-1"
style={{ background: 'linear-gradient(135deg, #059669 0%, #34d399 100%)' }}>
<div className="relative flex items-center gap-4">
<div className="p-3 bg-white/20 rounded-xl backdrop-blur-sm">
<TrendingUp size={28} className="text-emerald-200" />
</div>
<div>
<p className="text-emerald-200 text-sm font-medium">Hari Aktif (Sistem)</p>
<h3 className="text-3xl font-heading font-bold">{stats.weeklyActivity.filter(w => w.count > 0).length}</h3>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left Column: Charts */}
<div className="xl:col-span-2 space-y-8">
{/* Teacher Activity Chart based on Class Journal */}
<div className="bg-white p-6 rounded-2xl shadow-lg border border-slate-100">
<div className="mb-6">
<h3 className="font-heading font-bold text-lg text-school-900 flex items-center gap-2">
<TrendingUp size={20} /> Grafik Kehadiran Guru Mengajar
</h3>
<p className="text-sm text-slate-500">Jumlah kehadiran guru berdasarkan entri sekretaris minggu ini</p>
</div>
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={weeklyData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
<XAxis dataKey="day" axisLine={false} tickLine={false} tick={{ fill: '#64748b', fontSize: 12 }} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#64748b', fontSize: 12 }} />
<Tooltip
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
cursor={{ fill: '#f1f5f9' }}
/>
<Bar dataKey="count" fill="#1e3a8a" radius={[4, 4, 0, 0]} barSize={40}>
{weeklyData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={index === new Date().getDay() ? '#facc15' : '#1e3a8a'} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Class Performance Chart for Selected Date */}
<div className="bg-white p-6 rounded-2xl shadow-lg border border-slate-100">
<div className="mb-6">
<h3 className="font-heading font-bold text-lg text-school-900 flex items-center gap-2">
<BarChart2 size={20} /> Grafik Statistik Per Kelas ({dailyStats.dateStr})
</h3>
<p className="text-sm text-slate-500">Perbandingan kehadiran siswa per kelas pada tanggal yang dipilih</p>
</div>
{dailyStats.dailyClassPerformance.length > 0 ? (
<div className="h-80 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={dailyStats.dailyClassPerformance} margin={{ top: 20, right: 30, left: -10, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: '#64748b', fontSize: 10 }}
dy={10}
interval={0}
angle={-45}
textAnchor="end"
height={70}
/>
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#64748b', fontSize: 12 }} />
<Tooltip contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
<Legend verticalAlign="top" wrapperStyle={{ paddingBottom: '20px' }} />
<Bar name="Hadir" dataKey="present" stackId="a" fill="#22c55e" barSize={25} />
<Bar name="Tidak Hadir" dataKey="absent" stackId="a" fill="#ef4444" radius={[4, 4, 0, 0]} barSize={25} />
</BarChart>
</ResponsiveContainer>
</div>
) : (
<div className="h-40 flex items-center justify-center text-slate-400 bg-slate-50 rounded-xl">
Tidak ada data kehadiran kelas untuk tanggal ini
</div>
)}
</div>
</div>
{/* Right Column: Calendar & Filtered Activity List */}
<div className="space-y-8">
{/* Calendar Widget */}
<div className="bg-white p-6 rounded-2xl shadow-lg border border-slate-100">
<div className="flex items-center justify-between mb-6">
<h3 className="font-heading font-bold text-lg text-school-900 flex items-center gap-2">
<CalendarIcon size={20} /> Kalender
</h3>
<div className="flex items-center gap-2">
<button onClick={() => changeMonth(-1)} className="p-1 hover:bg-slate-100 rounded-full">
<ChevronLeft size={20} />
</button>
<span className="text-xs font-bold text-slate-700 w-24 text-center lowercase first-letter:uppercase">
{months[currentDate.getMonth()]} {currentDate.getFullYear()}
</span>
<button onClick={() => changeMonth(1)} className="p-1 hover:bg-slate-100 rounded-full">
<ChevronRight size={20} />
</button>
</div>
</div>
<div className="grid grid-cols-7 gap-1 text-center mb-2">
{['M', 'S', 'S', 'R', 'K', 'J', 'S'].map((d, i) => (
<span key={i} className="text-[10px] font-bold text-slate-400">{d}</span>
))}
</div>
<div className="grid grid-cols-7 gap-1">
{getCalendarDays().map((day, idx) => {
const isSelected = day ? isSelectedDay(day) : false;
const hasEntry = day ? hasEntryOnDate(day) : false;
return (
<div
key={idx}
onClick={() => day && handleDayClick(day)}
className={`aspect-square flex flex-col items-center justify-center rounded-lg text-xs transition-all cursor-pointer
${isSelected ? 'bg-school-900 text-white shadow-md' : 'hover:bg-blue-50'}
${hasEntry && !isSelected ? 'text-school-900 font-bold border border-blue-100 bg-blue-50/50' : ''}
${!day ? 'opacity-0 cursor-default' : ''}
`}
>
{day}
{hasEntry && (
<div className={`h-1 w-1 rounded-full mt-0.5 ${isSelected ? 'bg-accent-yellow' : 'bg-blue-500'}`}></div>
)}
</div>
);
})}
</div>
</div>
{/* New Requirement: Daily Activity List */}
<div className="bg-white p-6 rounded-2xl shadow-lg border border-slate-100">
<div className="mb-4">
<h3 className="font-heading font-bold text-lg text-school-900">Aktivitas Terbaru</h3>
<p className="text-[10px] text-slate-500">Filter: {dailyStats.dateStr}</p>
</div>
<div className="space-y-4 max-h-[400px] overflow-y-auto pr-2 custom-scrollbar">
{dailyStats.dailyActivities.length === 0 ? (
<div className="text-center py-8">
<Clock size={32} className="mx-auto text-slate-200 mb-2" />
<p className="text-sm text-slate-400">Tidak ada aktivitas pada tanggal ini</p>
</div>
) : (
dailyStats.dailyActivities.map((entry: any) => (
<div key={entry.id} className="flex items-start gap-3 pb-3 border-b border-slate-50 last:border-0 group">
<div className={`h-9 w-9 rounded-full flex items-center justify-center text-[10px] font-bold shrink-0 transition-colors ${entry.type === 'teacher' ? 'bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white' : 'bg-amber-50 text-amber-600 group-hover:bg-amber-600 group-hover:text-white'
}`}>
{entry.className.split(' ')[0]}
</div>
<div className="overflow-hidden">
<p className="text-sm font-bold text-slate-700 truncate">{entry.subject}</p>
<p className="text-[10px] text-slate-500 truncate">{entry.teacherName}</p>
<p className="text-[9px] text-slate-400 mt-0.5 italic flex items-center gap-1">
{entry.type === 'teacher' ? 'Jurnal Guru' : 'Jurnal Kelas'} {entry.startTime}
</p>
</div>
</div>
))
)}
</div>
{selectedDate && (
<button
onClick={() => setSelectedDate(new Date())}
className="w-full mt-4 py-2 border-2 border-dashed border-slate-200 rounded-xl text-xs font-bold text-slate-400 hover:border-school-800 hover:text-school-800 transition"
>
Kembali ke Hari Ini
</button>
)}
</div>
</div>
</div>
{/* Detail Table */}
<div className="bg-white p-6 rounded-2xl shadow-lg border border-slate-100">
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
<div>
<h3 className="font-heading font-bold text-lg text-school-900 flex items-center gap-2">
<Filter size={20} className="text-accent-yellow" />
Detail Aktivitas Jurnal ({dailyStats.dateStr})
</h3>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-slate-200 text-slate-500 text-[10px] uppercase tracking-wider bg-slate-50/50">
<th className="p-4 font-semibold rounded-tl-xl text-center">Tipe</th>
<th className="p-4 font-semibold">Guru</th>
<th className="p-4 font-semibold">Kelas</th>
<th className="p-4 font-semibold">Materi/Catatan</th>
<th className="p-4 font-semibold text-center rounded-tr-xl">Status/Siswa</th>
</tr>
</thead>
<tbody className="text-[11px]">
{dailyStats.dailyActivities.length === 0 ? (
<tr>
<td colSpan={5} className="p-12 text-center text-slate-300">
Tidak ada aktivitas.
</td>
</tr>
) : (
dailyStats.dailyActivities.map((entry: any) => (
<tr key={entry.id} className="border-b border-slate-50 hover:bg-slate-50 transition-colors">
<td className="p-4 text-center">
<span className={`px-2 py-0.5 rounded-full text-[9px] font-bold ${entry.type === 'teacher' ? 'bg-blue-100 text-blue-700' : 'bg-amber-100 text-amber-700'
}`}>
{entry.type === 'teacher' ? 'Guru' : 'Kelas'}
</span>
</td>
<td className="p-4 text-slate-600 font-medium">{entry.teacherName}</td>
<td className="p-4">
<span className="px-2 py-0.5 rounded-md bg-slate-100 text-slate-700 font-bold">
{entry.className}
</span>
</td>
<td className="p-4 text-slate-500 max-w-[200px] truncate">
{entry.type === 'teacher' ? entry.topic : entry.notes || '-'}
</td>
<td className="p-4 text-center">
{entry.type === 'teacher' ? (
<div className="flex items-center justify-center gap-1 font-bold">
<span className="text-green-600">{entry.studentsPresent}</span>
<span className="text-slate-200">/</span>
<span className="text-red-500">{entry.studentsAbsent}</span>
</div>
) : (
<span className={`${entry.teacherPresence === 'Hadir' ? 'text-green-600' : 'text-red-600'} font-bold`}>
{entry.teacherPresence}
</span>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
};
export default Dashboard;