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

265 lines
13 KiB
TypeScript
Executable File

import React, { useState, useEffect } from 'react';
import { LayoutDashboard, Plus, Pencil, Trash2, X, Save, Search, ArrowUpCircle, GraduationCap } from 'lucide-react';
import { useToast } from '../App';
import { addClass, updateClass, deleteClass, promoteClasses } from '../services/apiService';
import { Class } from '../types';
interface ManageClassesProps {
onClose?: () => void;
onRefresh: () => void;
classes: Class[];
}
const ManageClasses: React.FC<ManageClassesProps> = ({ onClose, onRefresh, classes }) => {
const { toast } = useToast();
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [editingClass, setEditingClass] = useState<Class | null>(null);
const [formData, setFormData] = useState({ name: '', grade: 'X' as any });
const [saving, setSaving] = useState(false);
const [isPromoting, setIsPromoting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
try {
const success = editingClass
? await updateClass(editingClass.id, formData)
: await addClass(formData);
if (success) {
toast.success(editingClass ? 'Kelas diperbarui' : 'Kelas ditambahkan');
onRefresh();
setShowForm(false);
setEditingClass(null);
setFormData({ name: '', grade: 'X' });
} else {
toast.error('Gagal menyimpan data');
}
} catch (error) {
toast.error('Terjadi kesalahan');
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
if (!confirm('Yakin ingin menghapus kelas ini?')) return;
try {
const success = await deleteClass(id);
if (success) {
toast.success('Kelas dihapus');
onRefresh();
} else {
toast.error('Gagal menghapus kelas');
}
} catch (error) {
toast.error('Terjadi kesalahan');
}
};
const handleEdit = (cls: Class) => {
setEditingClass(cls);
setFormData({
name: cls.name,
grade: cls.grade
});
setShowForm(true);
};
const handlePromote = async () => {
if (!confirm('AWAS! Proses "Naik Kelas" akan memperbarui data seluruh siswa:\n- Kelas X -> XI\n- Kelas XI -> XII\n- Kelas XII -> LULUS\n\nApakah anda yakin ingin melanjutkan?')) return;
setIsPromoting(true);
try {
const success = await promoteClasses();
if (success) {
toast.success('Kenaikan kelas berhasil diproses!');
onRefresh();
} else {
toast.error('Gagal memproses kenaikan kelas');
}
} catch (error) {
toast.error('Terjadi kesalahan fatal');
} finally {
setIsPromoting(false);
}
};
// Ensure classes is always an array to prevent crashes
const safeClasses = Array.isArray(classes) ? classes : [];
const filteredClasses = safeClasses.filter(c =>
c && c.name && c.grade && (
c.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.grade.toLowerCase().includes(searchTerm.toLowerCase())
)
);
return (
<div className="bg-white rounded-2xl shadow-lg border border-slate-100 overflow-hidden">
<div className="bg-school-900 p-6 text-white flex items-center justify-between">
<div className="flex items-center gap-3">
<GraduationCap size={24} className="text-accent-yellow" />
<div>
<h2 className="font-heading text-xl font-bold">Kelola Data Kelas</h2>
<p className="text-blue-200 text-sm">Atur referensi kelas dan sistem kenaikan kelas</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={handlePromote}
disabled={isPromoting}
className="flex items-center gap-2 px-4 py-2 bg-accent-yellow text-school-900 rounded-xl hover:bg-yellow-400 transition font-bold shadow-lg disabled:opacity-50"
>
<ArrowUpCircle size={20} />
{isPromoting ? 'Memproses...' : 'Naik Kelas (Periodik)'}
</button>
{onClose && (
<button onClick={onClose} className="text-white/70 hover:text-white ml-2">
<X size={24} />
</button>
)}
</div>
</div>
<div className="p-6 space-y-4">
{/* Actions Bar */}
<div className="flex flex-wrap gap-3 items-center justify-between">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
<input
type="text"
placeholder="Cari kelas atau tingkat..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 rounded-xl border border-slate-200 focus:border-school-800 outline-none"
/>
</div>
<button
onClick={() => { setShowForm(true); setEditingClass(null); setFormData({ name: '', grade: 'X' }); }}
className="flex items-center gap-2 px-4 py-2 bg-school-900 text-white rounded-xl hover:bg-school-800 transition font-medium"
>
<Plus size={18} /> Tambah Kelas
</button>
</div>
{/* Form Modal */}
{showForm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-md shadow-2xl">
<h3 className="text-lg font-bold mb-4">{editingClass ? 'Edit Kelas' : 'Tambah Kelas Baru'}</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">Nama Kelas *</label>
<input
type="text"
required
placeholder="Contoh: X MIPA 1"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-4 py-2 rounded-xl border border-slate-200 focus:border-school-800 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">Tingkat *</label>
<select
required
value={formData.grade}
onChange={(e) => setFormData({ ...formData, grade: e.target.value as any })}
className="w-full px-4 py-2 rounded-xl border border-slate-200 focus:border-school-800 outline-none bg-white"
>
<option value="X">Kelas X</option>
<option value="XI">Kelas XI</option>
<option value="XII">Kelas XII</option>
<option value="Lulus">Lulus</option>
</select>
</div>
<div className="flex gap-2 pt-2">
<button
type="button"
onClick={() => setShowForm(false)}
className="flex-1 px-4 py-2 bg-slate-100 text-slate-700 rounded-xl hover:bg-slate-200 transition font-medium"
>
Batal
</button>
<button
type="submit"
disabled={saving}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-school-900 text-white rounded-xl hover:bg-school-800 transition font-medium disabled:opacity-50"
>
<Save size={18} /> {saving ? 'Menyimpan...' : 'Simpan'}
</button>
</div>
</form>
</div>
</div>
)}
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-slate-50 border-b border-slate-200">
<th className="text-left px-4 py-3 font-semibold text-slate-600">No</th>
<th className="text-left px-4 py-3 font-semibold text-slate-600">Nama Kelas</th>
<th className="text-left px-4 py-3 font-semibold text-slate-600">Tingkat</th>
<th className="text-center px-4 py-3 font-semibold text-slate-600">Aksi</th>
</tr>
</thead>
<tbody>
{filteredClasses.map((cls, index) => (
<tr key={cls.id} className="border-b border-slate-100 hover:bg-slate-50">
<td className="px-4 py-3 text-slate-500">{index + 1}</td>
<td className="px-4 py-3 font-medium">{cls.name}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-xs font-bold ${cls.grade === 'X' ? 'bg-emerald-100 text-emerald-700' :
cls.grade === 'XI' ? 'bg-blue-100 text-blue-700' :
cls.grade === 'XII' ? 'bg-purple-100 text-purple-700' :
'bg-slate-100 text-slate-700'
}`}>
Kelas {cls.grade}
</span>
</td>
<td className="px-4 py-3">
<div className="flex justify-center gap-2">
<button
onClick={() => handleEdit(cls)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition"
>
<Pencil size={16} />
</button>
<button
onClick={() => handleDelete(cls.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition"
>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))}
{filteredClasses.length === 0 && (
<tr>
<td colSpan={4} className="px-4 py-8 text-center text-slate-400">
Tidak ada data kelas
</td>
</tr>
)}
</tbody>
</table>
</div>
<div className="text-sm text-slate-500 pt-2">
Total: {filteredClasses.length} kelas
</div>
</div>
</div>
);
};
export default ManageClasses;