291 lines
14 KiB
JavaScript
291 lines
14 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { Plus, Edit2, Trash2, Loader2, Save, X } from 'lucide-react';
|
|
import DeleteConfirmationModal from '../Components/DeleteConfirmationModal';
|
|
|
|
export default function MasterTable({ type, onNotify, btnLabel }) {
|
|
const [data, setData] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [editingItem, setEditingItem] = useState(null);
|
|
const [formData, setFormData] = useState({ name: '', status: 'Active' });
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
// Delete Confirmation State
|
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
const [itemToDelete, setItemToDelete] = useState(null);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [type]);
|
|
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/masters/${type}`);
|
|
const result = await response.json();
|
|
setData(result);
|
|
} catch (error) {
|
|
console.error('Error fetching data:', error);
|
|
onNotify('Failed to fetch data.', 'error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleOpenModal = (item = null) => {
|
|
if (item) {
|
|
setEditingItem(item);
|
|
setFormData({ name: item.name, status: item.status });
|
|
} else {
|
|
setEditingItem(null);
|
|
setFormData({ name: '', status: 'Active' });
|
|
}
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleSave = async (e) => {
|
|
e.preventDefault();
|
|
setIsSaving(true);
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
|
|
try {
|
|
const url = editingItem ? `/api/masters/${type}/${editingItem.id}` : `/api/masters/${type}`;
|
|
const method = editingItem ? 'PUT' : 'POST';
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken
|
|
},
|
|
body: JSON.stringify(formData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
onNotify(`${editingItem ? 'Updated' : 'Added'} successfully!`, 'success');
|
|
setIsModalOpen(false);
|
|
fetchData();
|
|
} else {
|
|
const err = await response.json().catch(() => ({}));
|
|
onNotify(err.message || 'Error occurred while saving.', 'error');
|
|
}
|
|
} catch (error) {
|
|
onNotify('Server error.', 'error');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleToggleStatus = async (item) => {
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
const newStatus = item.status === 'Active' ? 'Inactive' : 'Active';
|
|
|
|
try {
|
|
const response = await fetch(`/api/masters/${type}/${item.id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken
|
|
},
|
|
body: JSON.stringify({ ...item, status: newStatus })
|
|
});
|
|
|
|
if (response.ok) {
|
|
setData(data.map(i => i.id === item.id ? { ...i, status: newStatus } : i));
|
|
}
|
|
} catch (error) {
|
|
onNotify('Failed to update status.', 'error');
|
|
}
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!itemToDelete) return;
|
|
setIsDeleting(true);
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
|
|
try {
|
|
const response = await fetch(`/api/masters/${type}/${itemToDelete}`, {
|
|
method: 'DELETE',
|
|
headers: { 'X-CSRF-TOKEN': csrfToken }
|
|
});
|
|
|
|
if (response.ok) {
|
|
onNotify('Deleted successfully!', 'success');
|
|
setIsDeleteModalOpen(false);
|
|
setItemToDelete(null);
|
|
fetchData();
|
|
} else {
|
|
const err = await response.json().catch(() => ({}));
|
|
onNotify(err.message || 'Failed to delete.', 'error');
|
|
}
|
|
} catch (error) {
|
|
onNotify('Failed to delete.', 'error');
|
|
} finally {
|
|
setIsDeleting(false);
|
|
}
|
|
};
|
|
|
|
const confirmDelete = (id) => {
|
|
setItemToDelete(id);
|
|
setIsDeleteModalOpen(true);
|
|
};
|
|
|
|
if (loading) return (
|
|
<div className="p-12 flex justify-center items-center gap-2 text-gray-400">
|
|
<Loader2 className="animate-spin" size={20} />
|
|
<span className="font-medium">Loading data...</span>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="relative">
|
|
<div className="absolute top-[-5.5rem] right-6">
|
|
<button
|
|
onClick={() => handleOpenModal()}
|
|
className="flex justify-center items-center gap-2 bg-[#F34444] text-white px-5 py-2.5 rounded-xl font-bold text-sm hover:bg-[#D32F2F] transition-all shadow-lg shadow-red-100"
|
|
>
|
|
<Plus size={18} />
|
|
Add {btnLabel}
|
|
</button>
|
|
</div>
|
|
|
|
<table className="w-full">
|
|
<thead className="bg-[#F9FAFB] border-b border-gray-100">
|
|
<tr>
|
|
<th className="px-6 py-4 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Name</th>
|
|
<th className="px-6 py-4 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Status</th>
|
|
<th className="px-6 py-4 text-right text-[11px] font-bold text-gray-500 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-50">
|
|
{data.length === 0 ? (
|
|
<tr>
|
|
<td colSpan="3" className="px-6 py-12 text-center text-gray-400 text-sm italic">
|
|
No records found.
|
|
</td>
|
|
</tr>
|
|
) : data.map((item) => (
|
|
<tr key={item.id} className="hover:bg-[#FDFDFD] transition-colors group">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className="font-semibold text-gray-900">{item.name}</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => handleToggleStatus(item)}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${
|
|
item.status === 'Active' ? 'bg-emerald-500' : 'bg-gray-200'
|
|
}`}
|
|
>
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
item.status === 'Active' ? 'translate-x-6' : 'translate-x-1'
|
|
}`} />
|
|
</button>
|
|
<span className={`text-xs font-bold uppercase tracking-wider ${
|
|
item.status === 'Active' ? 'text-emerald-500' : 'text-gray-400'
|
|
}`}>
|
|
{item.status}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm">
|
|
<div className="flex items-center justify-end gap-2">
|
|
<button
|
|
onClick={() => handleOpenModal(item)}
|
|
className="p-2 text-gray-400 hover:text-emerald-500 hover:bg-emerald-50 rounded-lg transition-all"
|
|
>
|
|
<Edit2 size={16} />
|
|
</button>
|
|
{item.name !== 'Product Sale' && (
|
|
<button
|
|
onClick={() => confirmDelete(item.id)}
|
|
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-all"
|
|
>
|
|
<Trash2 size={16} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
|
|
{/* Modal */}
|
|
{isModalOpen && (
|
|
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm animate-in fade-in duration-200">
|
|
<div className="bg-white rounded-3xl w-full max-w-md shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
|
|
<div className="p-6 border-b border-gray-100 flex items-center justify-between">
|
|
<h3 className="text-xl font-bold text-[#111827]">
|
|
{editingItem ? `Edit ${btnLabel}` : `Add New ${btnLabel}`}
|
|
</h3>
|
|
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-50 rounded-full transition-colors">
|
|
<X size={20} className="text-gray-400" />
|
|
</button>
|
|
</div>
|
|
<form onSubmit={handleSave} className="p-6">
|
|
<div className="space-y-6">
|
|
<div>
|
|
<label className="block text-sm font-bold text-gray-700 mb-2">Name *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-2xl focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500 transition-all text-gray-900 placeholder:text-gray-400"
|
|
placeholder={`Enter ${btnLabel.toLowerCase()} name`}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-2xl border border-gray-100">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`p-2 rounded-xl ${formData.status === 'Active' ? 'bg-emerald-100 text-emerald-600' : 'bg-gray-100 text-gray-500'}`}>
|
|
<Save size={18} />
|
|
</div>
|
|
<span className="text-sm font-bold text-gray-700">Set as Active</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setFormData({ ...formData, status: formData.status === 'Active' ? 'Inactive' : 'Active' })}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${
|
|
formData.status === 'Active' ? 'bg-emerald-500' : 'bg-gray-200'
|
|
}`}
|
|
>
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
formData.status === 'Active' ? 'translate-x-6' : 'translate-x-1'
|
|
}`} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="mt-8 flex gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsModalOpen(false)}
|
|
className="flex-1 py-3.5 border border-gray-100 rounded-2xl font-bold text-gray-500 hover:bg-gray-50 transition-all text-sm"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isSaving}
|
|
className="flex-1 py-3.5 bg-[#F34444] text-white rounded-2xl font-bold hover:bg-[#D32F2F] transition-all disabled:opacity-50 disabled:cursor-not-allowed text-sm shadow-lg shadow-red-100 flex items-center justify-center gap-2"
|
|
>
|
|
{isSaving ? <Loader2 className="animate-spin" size={18} /> : (editingItem ? 'Update Mastery' : 'Save Mastery')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<DeleteConfirmationModal
|
|
isOpen={isDeleteModalOpen}
|
|
isDeleting={isDeleting}
|
|
onClose={() => setIsDeleteModalOpen(false)}
|
|
onConfirm={handleDelete}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|