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

438 lines
19 KiB
TypeScript
Executable File

import React, { useState, useEffect, useMemo } from 'react';
import { Save, UserCheck, Clock, BookOpen, Calendar, Users, CheckCircle, RotateCcw, UserX, ClipboardList, User } from 'lucide-react';
import { saveClassJournalEntry } from '../services/apiService';
import { Student, Teacher, AttendanceDetail, AuthUser, Class } from '../types';
interface ClassJournalFormProps {
onSuccess: () => void;
students: Student[];
teachers: Teacher[];
subjects: string[];
classes?: Class[]; // Optional: classes from database
currentUser: AuthUser;
}
const ClassJournalForm: React.FC<ClassJournalFormProps> = ({ onSuccess, students, teachers, subjects, classes = [], currentUser }) => {
const [loading, setLoading] = useState(false);
const [selectedClass, setSelectedClass] = useState<string>('');
// Form State
const [formData, setFormData] = useState({
teacherName: '',
subject: '',
date: new Date().toISOString().split('T')[0],
startTime: '1',
endTime: '2',
teacherPresence: 'Hadir' as 'Hadir' | 'Tugas' | 'Tidak Hadir',
notes: '',
classSecretary: ''
});
// Pre-fill class if user is Secretary
useEffect(() => {
if (currentUser.role === 'SEKRETARIS' && currentUser.className) {
setSelectedClass(currentUser.className);
setFormData(prev => ({ ...prev, classSecretary: currentUser.name }));
}
}, [currentUser]);
// Attendance State: studentId -> Status
const [attendanceMap, setAttendanceMap] = useState<Record<string, 'H' | 'S' | 'I' | 'A' | 'D'>>({});
// Get Unique Classes - prefer classes prop, fallback to extracting from students
const availableClasses = useMemo(() => {
// If classes prop is provided and has data, use it
if (classes && classes.length > 0) {
return classes.map(c => c.name).sort();
}
// Fallback: extract unique class names from students
const classSet = new Set(students.map(s => s.className).filter(Boolean));
return Array.from(classSet).sort();
}, [classes, students]);
const classStudents = useMemo(() => {
if (!selectedClass) return [];
return students
.filter(s => s.className === selectedClass)
.sort((a, b) => a.name.localeCompare(b.name));
}, [selectedClass, students]);
// Reset attendance and secretary when class changes
useEffect(() => {
const newMap: Record<string, any> = {};
classStudents.forEach(s => {
newMap[s.id] = 'H';
});
setAttendanceMap(newMap);
// Only reset secretary if not logged in as one
if (currentUser.role !== 'SEKRETARIS') {
setFormData(prev => ({ ...prev, classSecretary: '' }));
}
}, [classStudents, currentUser.role]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleAttendanceChange = (studentId: string, status: 'H' | 'S' | 'I' | 'A' | 'D') => {
setAttendanceMap(prev => ({
...prev,
[studentId]: status
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.teacherName || !selectedClass || !formData.subject || !formData.classSecretary) {
alert('Mohon lengkapi data Guru, Mapel, Kelas, dan Sekretaris.');
return;
}
setLoading(true);
let present = 0, sakit = 0, izin = 0, alpha = 0, disp = 0;
const attendanceDetailsList: AttendanceDetail[] = [];
classStudents.forEach(student => {
const status = attendanceMap[student.id] || 'H';
if (status === 'H') present++;
else if (status === 'S') { sakit++; }
else if (status === 'I') { izin++; }
else if (status === 'A') { alpha++; }
else if (status === 'D') { disp++; }
if (status !== 'H') {
attendanceDetailsList.push({
studentName: student.name,
status: status
});
}
});
const attendancePayload = attendanceDetailsList.length > 0 ? attendanceDetailsList : "NIHIL";
const success = await saveClassJournalEntry({
teacherName: formData.teacherName,
subject: formData.subject,
className: selectedClass,
teacherPresence: formData.teacherPresence,
classSecretary: formData.classSecretary,
...formData,
studentsPresent: present + disp,
sakit, izin, alpha, dispen: disp,
attendanceDetails: attendancePayload,
});
setLoading(false);
if (success) {
alert('Jurnal Kelas berhasil disimpan!');
onSuccess();
} else {
alert('Gagal menyimpan jurnal. Cek koneksi internet.');
}
};
const periods = Array.from({ length: 10 }, (_, i) => i + 1);
return (
<div className="max-w-5xl mx-auto animate-fade-in pb-20">
<div className="bg-white rounded-2xl shadow-lg overflow-hidden border border-slate-100">
<div className="bg-indigo-900 p-6 text-white flex items-center justify-between">
<div>
<h2 className="font-heading text-2xl font-bold">Input Jurnal Kelas</h2>
<p className="text-indigo-200 text-sm">Diisi oleh Sekretaris atau Piket Kelas</p>
</div>
<div className="h-12 w-12 bg-white/10 rounded-full flex items-center justify-center backdrop-blur-sm">
<ClipboardList className="text-accent-yellow" size={24} />
</div>
</div>
<form onSubmit={handleSubmit} className="p-6 md:p-8 space-y-8">
{/* Header Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-semibold text-slate-600 flex items-center gap-2">
<Calendar size={16} /> Tanggal
</label>
<input
type="date"
name="date"
required
className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-800 focus:ring-2 focus:ring-indigo-100 transition-all outline-none"
value={formData.date}
onChange={handleChange}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-slate-600 flex items-center gap-2">
<Users size={16} /> Kelas
</label>
<select
value={selectedClass}
onChange={(e) => setSelectedClass(e.target.value)}
disabled={currentUser.role === 'SEKRETARIS'}
className={`w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-800 focus:ring-2 focus:ring-indigo-100 transition-all outline-none bg-white ${currentUser.role === 'SEKRETARIS' ? 'bg-slate-100 text-slate-500 cursor-not-allowed' : ''}`}
required
>
<option value="">-- Pilih Kelas --</option>
{availableClasses.map((c, idx) => (
<option key={idx} value={c}>{c}</option>
))}
</select>
</div>
{selectedClass && (
<div className="space-y-2 animate-fade-in">
<label className="text-sm font-semibold text-slate-600 flex items-center gap-2">
<User size={16} /> Sekretaris Kelas (Pengisi Jurnal)
</label>
<select
name="classSecretary"
value={formData.classSecretary}
onChange={handleChange}
disabled={currentUser.role === 'SEKRETARIS'}
className={`w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-800 focus:ring-2 focus:ring-indigo-100 transition-all outline-none bg-white ${currentUser.role === 'SEKRETARIS' ? 'bg-slate-100 text-slate-500' : ''}`}
required
>
<option value="">-- Pilih Nama Sekretaris --</option>
{classStudents.map((s, idx) => (
<option key={idx} value={s.name}>{s.name}</option>
))}
</select>
</div>
)}
{/* Placeholder if no class selected to maintain grid layout */}
{!selectedClass && <div className="hidden md:block"></div>}
<div className="space-y-2">
<label className="text-sm font-semibold text-slate-600 flex items-center gap-2">
<UserCheck size={16} /> Nama Guru
</label>
<select
name="teacherName"
value={formData.teacherName}
onChange={handleChange}
className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-800 focus:ring-2 focus:ring-indigo-100 transition-all outline-none bg-white"
required
>
<option value="">-- Pilih Guru --</option>
{teachers.map((t, idx) => (
<option key={idx} value={t.name}>{t.name}</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-slate-600 flex items-center gap-2">
<BookOpen size={16} /> Mata Pelajaran
</label>
<select
name="subject"
value={formData.subject}
onChange={handleChange}
className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-800 focus:ring-2 focus:ring-indigo-100 transition-all outline-none bg-white"
required
>
<option value="">-- Pilih Mata Pelajaran --</option>
{subjects.map((s, idx) => (
<option key={idx} value={s}>{s}</option>
))}
</select>
</div>
</div>
{/* Presence & Time */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<label className="text-sm font-semibold text-slate-600 flex items-center gap-2">
<UserX size={16} /> Kehadiran Guru
</label>
<div className="flex gap-4">
{['Hadir', 'Tugas', 'Tidak Hadir'].map((status) => (
<label key={status} className={`flex-1 cursor-pointer border rounded-xl p-3 flex items-center justify-center gap-2 transition-all ${formData.teacherPresence === status ? 'bg-indigo-50 border-indigo-500 text-indigo-900 font-bold shadow-sm' : 'border-slate-200 hover:bg-slate-50'}`}>
<input
type="radio"
name="teacherPresence"
value={status}
checked={formData.teacherPresence === status}
onChange={handleChange}
className="hidden"
/>
{status === 'Hadir' && <CheckCircle size={18} className="text-green-500" />}
{status === 'Tugas' && <ClipboardList size={18} className="text-blue-500" />}
{status === 'Tidak Hadir' && <UserX size={18} className="text-red-500" />}
{status}
</label>
))}
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-slate-600 flex items-center gap-2">
<Clock size={16} /> Jam Ke- (Mulai - Selesai)
</label>
<div className="flex items-center gap-2">
<select
name="startTime"
className="w-full px-3 py-3 rounded-xl border border-slate-200 outline-none text-center bg-white"
value={formData.startTime}
onChange={handleChange}
>
{periods.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
<span className="text-slate-400 font-bold">-</span>
<select
name="endTime"
className="w-full px-3 py-3 rounded-xl border border-slate-200 outline-none text-center bg-white"
value={formData.endTime}
onChange={handleChange}
>
{periods.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
</div>
</div>
</div>
<hr className="border-slate-100" />
{/* Student Attendance Table */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-heading font-bold text-school-900 flex items-center gap-2">
<CheckCircle size={20} className="text-accent-green" /> Presensi Siswa
</h3>
<div className="text-sm text-slate-500">
Total Siswa: <span className="font-bold text-school-900">{classStudents.length}</span>
</div>
</div>
{!selectedClass ? (
<div className="bg-slate-50 rounded-xl p-8 text-center text-slate-500 border border-dashed border-slate-300">
Silahkan pilih kelas terlebih dahulu untuk memuat daftar siswa.
</div>
) : (
<div className="overflow-x-auto rounded-xl border border-slate-200 shadow-sm">
<table className="w-full text-sm text-left">
<thead className="bg-slate-50 text-slate-600 font-bold uppercase text-xs tracking-wider">
<tr>
<th className="p-3 w-10 text-center">No</th>
<th className="p-3">Nama Siswa</th>
<th className="p-3 text-center w-20 bg-yellow-50">Sakit</th>
<th className="p-3 text-center w-20 bg-blue-50">Izin</th>
<th className="p-3 text-center w-20 bg-red-50">Alpha</th>
<th className="p-3 text-center w-20 bg-purple-50">Disp.</th>
<th className="p-3 text-center w-10">Reset</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{classStudents.map((student, index) => {
const status = attendanceMap[student.id] || 'H';
const isAbsent = status !== 'H';
return (
<tr key={student.id} className={`hover:bg-gray-50 transition-colors ${isAbsent ? 'bg-red-50/10' : ''}`}>
<td className="p-3 text-center text-slate-500">{index + 1}</td>
<td className="p-3 font-medium text-slate-700">
{student.name}
<div className="text-[10px] text-slate-400">{student.nis}</div>
</td>
<td className="p-3 text-center bg-yellow-50/30">
<input
type="radio"
name={`att-${student.id}`}
checked={status === 'S'}
onChange={() => handleAttendanceChange(student.id, 'S')}
className="w-5 h-5 accent-yellow-500 cursor-pointer"
/>
</td>
<td className="p-3 text-center bg-blue-50/30">
<input
type="radio"
name={`att-${student.id}`}
checked={status === 'I'}
onChange={() => handleAttendanceChange(student.id, 'I')}
className="w-5 h-5 accent-blue-500 cursor-pointer"
/>
</td>
<td className="p-3 text-center bg-red-50/30">
<input
type="radio"
name={`att-${student.id}`}
checked={status === 'A'}
onChange={() => handleAttendanceChange(student.id, 'A')}
className="w-5 h-5 accent-red-500 cursor-pointer"
/>
</td>
<td className="p-3 text-center bg-purple-50/30">
<input
type="radio"
name={`att-${student.id}`}
checked={status === 'D'}
onChange={() => handleAttendanceChange(student.id, 'D')}
className="w-5 h-5 accent-purple-500 cursor-pointer"
/>
</td>
<td className="p-3 text-center">
{isAbsent && (
<button
type="button"
onClick={() => handleAttendanceChange(student.id, 'H')}
className="p-1 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-full transition"
title="Reset ke Hadir"
>
<RotateCcw size={16} />
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-slate-600">Keterangan Tambahan</label>
<textarea
name="notes"
rows={2}
placeholder="Catatan kegiatan kelas atau kejadian penting..."
className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-800 focus:ring-2 focus:ring-indigo-100 transition-all outline-none resize-none"
value={formData.notes}
onChange={handleChange}
/>
</div>
<div className="pt-4">
<button
type="submit"
disabled={loading}
className={`w-full py-4 rounded-xl font-heading font-bold text-lg text-white shadow-lg transform transition-all hover:-translate-y-1 flex items-center justify-center gap-2 ${loading ? 'bg-slate-400 cursor-not-allowed' : 'bg-indigo-600 hover:bg-indigo-700 hover:shadow-indigo-500/30'
}`}
>
{loading ? 'Menyimpan Data...' : (
<><Save size={20} /> SIMPAN JURNAL KELAS</>
)}
</button>
</div>
</form>
</div>
</div>
);
};
export default ClassJournalForm;