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

298 lines
14 KiB
TypeScript
Executable File

import React, { useState, useEffect, useRef } from 'react';
import { Users, Plus, Pencil, Trash2, Upload, Download, X, Save, Search } from 'lucide-react';
interface Teacher {
id: number;
name: string;
nip: string;
}
interface ManageTeachersProps {
onClose?: () => void;
}
const ManageTeachers: React.FC<ManageTeachersProps> = ({ onClose }) => {
const [teachers, setTeachers] = useState<Teacher[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [editingTeacher, setEditingTeacher] = useState<Teacher | null>(null);
const [formData, setFormData] = useState({ name: '', nip: '' });
const [saving, setSaving] = useState(false);
const [importResult, setImportResult] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const API_URL = import.meta.env.VITE_API_URL || '/api';
useEffect(() => {
fetchTeachers();
}, []);
const fetchTeachers = async () => {
try {
const response = await fetch(`${API_URL}/teachers`);
const result = await response.json();
if (result.status === 'success') {
setTeachers(result.data);
}
} catch (error) {
console.error('Error fetching teachers:', error);
} finally {
setLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
try {
const url = editingTeacher
? `${API_URL}/teachers/${editingTeacher.id}`
: `${API_URL}/teachers`;
const response = await fetch(url, {
method: editingTeacher ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.status === 'success') {
alert(result.message);
fetchTeachers();
setShowForm(false);
setEditingTeacher(null);
setFormData({ name: '', nip: '' });
} else {
alert(result.message);
}
} catch (error) {
alert('Gagal menyimpan data');
} finally {
setSaving(false);
}
};
const handleDelete = async (id: number) => {
if (!confirm('Yakin ingin menghapus guru ini?')) return;
try {
const response = await fetch(`${API_URL}/teachers/${id}`, { method: 'DELETE' });
const result = await response.json();
if (result.status === 'success') {
fetchTeachers();
}
} catch (error) {
alert('Gagal menghapus data');
}
};
const handleEdit = (teacher: Teacher) => {
setEditingTeacher(teacher);
setFormData({ name: teacher.name, nip: teacher.nip || '' });
setShowForm(true);
};
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch(`${API_URL}/teachers/import`, {
method: 'POST',
body: formData
});
const result = await response.json();
setImportResult(result.message);
fetchTeachers();
} catch (error) {
setImportResult('Gagal import data');
}
if (fileInputRef.current) fileInputRef.current.value = '';
};
const downloadTemplate = () => {
window.open(`${API_URL}/teachers/template`, '_blank');
};
const filteredTeachers = teachers.filter(t =>
t.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(t.nip && t.nip.includes(searchTerm))
);
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">
<Users size={24} className="text-accent-yellow" />
<div>
<h2 className="font-heading text-xl font-bold">Kelola Data Guru</h2>
<p className="text-blue-200 text-sm">Tambah, edit, hapus, atau import dari Excel</p>
</div>
</div>
{onClose && (
<button onClick={onClose} className="text-white/70 hover:text-white">
<X size={24} />
</button>
)}
</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 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
<input
type="text"
placeholder="Cari guru..."
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>
<div className="flex gap-2">
<button
onClick={downloadTemplate}
className="flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-xl hover:bg-slate-200 transition font-medium"
>
<Download size={18} /> Template
</button>
<label className="flex items-center gap-2 px-4 py-2 bg-green-500 text-white rounded-xl hover:bg-green-600 transition font-medium cursor-pointer">
<Upload size={18} /> Import Excel
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.xls"
onChange={handleImport}
className="hidden"
/>
</label>
<button
onClick={() => { setShowForm(true); setEditingTeacher(null); setFormData({ name: '', nip: '' }); }}
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 Guru
</button>
</div>
</div>
{/* Import Result */}
{importResult && (
<div className="bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-xl flex justify-between items-center">
<span>{importResult}</span>
<button onClick={() => setImportResult(null)}><X size={18} /></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">
<h3 className="text-lg font-bold mb-4">{editingTeacher ? 'Edit Guru' : 'Tambah Guru Baru'}</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-600 mb-1">Nama Guru *</label>
<input
type="text"
required
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">NIP</label>
<input
type="text"
value={formData.nip}
onChange={(e) => setFormData({ ...formData, nip: e.target.value })}
className="w-full px-4 py-2 rounded-xl border border-slate-200 focus:border-school-800 outline-none"
/>
</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 */}
{loading ? (
<div className="text-center py-8 text-slate-400">Memuat data...</div>
) : (
<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 Guru</th>
<th className="text-left px-4 py-3 font-semibold text-slate-600">NIP</th>
<th className="text-center px-4 py-3 font-semibold text-slate-600">Aksi</th>
</tr>
</thead>
<tbody>
{filteredTeachers.map((teacher, index) => (
<tr key={teacher.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">{teacher.name}</td>
<td className="px-4 py-3 text-slate-600">{teacher.nip || '-'}</td>
<td className="px-4 py-3">
<div className="flex justify-center gap-2">
<button
onClick={() => handleEdit(teacher)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition"
>
<Pencil size={16} />
</button>
<button
onClick={() => handleDelete(teacher.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition"
>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))}
{filteredTeachers.length === 0 && (
<tr>
<td colSpan={4} className="px-4 py-8 text-center text-slate-400">
Tidak ada data guru
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
<div className="text-sm text-slate-500 pt-2">
Total: {filteredTeachers.length} guru
</div>
</div>
</div>
);
};
export default ManageTeachers;