381 lines
24 KiB
JavaScript
381 lines
24 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { X, Save, MapPin, User, Calendar, CheckSquare, Upload, Trash2, Box, Plus } from 'lucide-react';
|
|
|
|
export default function EditBranchModal({ isOpen, onClose, onRefresh, branch }) {
|
|
const [loading, setLoading] = useState(false);
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
location: '',
|
|
manager_name: '',
|
|
operational_start_date: '',
|
|
status: 'Active',
|
|
payroll_from_day: 1,
|
|
payroll_to_day: 28,
|
|
salary_generation_day: 2,
|
|
});
|
|
|
|
const [newDocs, setNewDocs] = useState([]);
|
|
const [activeStaff, setActiveStaff] = useState([]);
|
|
const [loadingStaff, setLoadingStaff] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (branch) {
|
|
setFormData({
|
|
name: branch.name || '',
|
|
location: branch.location || '',
|
|
manager_name: branch.manager_name || '',
|
|
operational_start_date: branch.operational_start_date || '',
|
|
status: branch.status || 'Active',
|
|
payroll_from_day: branch.payroll_from_day || 1,
|
|
payroll_to_day: branch.payroll_to_day || 28,
|
|
salary_generation_day: branch.salary_generation_day || 2,
|
|
});
|
|
setNewDocs([]);
|
|
setActiveStaff([]);
|
|
}
|
|
}, [branch]);
|
|
|
|
// Fetch active staff when status becomes Inactive
|
|
useEffect(() => {
|
|
if (formData.status === 'Inactive' && branch?.id) {
|
|
const fetchActiveStaff = async () => {
|
|
setLoadingStaff(true);
|
|
try {
|
|
const res = await fetch(`/api/branches/${branch.id}/active-staff`);
|
|
const data = await res.json();
|
|
setActiveStaff(data);
|
|
} catch (error) {
|
|
console.error('Error fetching active staff:', error);
|
|
} finally {
|
|
setLoadingStaff(false);
|
|
}
|
|
};
|
|
fetchActiveStaff();
|
|
} else {
|
|
setActiveStaff([]);
|
|
}
|
|
}, [formData.status, branch?.id]);
|
|
|
|
const handleAddDocRow = () => {
|
|
setNewDocs([...newDocs, { name: '', file: null, document_number: '', expiry_date: '', reminder_days: 30 }]);
|
|
};
|
|
|
|
const handleNewDocChange = (index, field, value) => {
|
|
const updated = [...newDocs];
|
|
updated[index][field] = value;
|
|
setNewDocs(updated);
|
|
};
|
|
|
|
const handleRemoveDocRow = (index) => {
|
|
setNewDocs(newDocs.filter((_, i) => i !== index));
|
|
};
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
|
|
const data = new FormData();
|
|
data.append('_method', 'PUT'); // For Laravel to handle PUT via POST
|
|
data.append('name', formData.name);
|
|
data.append('location', formData.location);
|
|
data.append('manager_name', formData.manager_name);
|
|
data.append('operational_start_date', formData.operational_start_date);
|
|
data.append('status', formData.status);
|
|
data.append('payroll_from_day', formData.payroll_from_day);
|
|
data.append('payroll_to_day', formData.payroll_to_day);
|
|
data.append('salary_generation_day', formData.salary_generation_day);
|
|
|
|
newDocs.forEach((doc, index) => {
|
|
if (doc.file) {
|
|
data.append(`new_docs[${index}][file]`, doc.file);
|
|
data.append(`new_docs[${index}][name]`, doc.name || `New Document ${index + 1}`);
|
|
data.append(`new_docs[${index}][document_number]`, doc.document_number || '');
|
|
data.append(`new_docs[${index}][expiry_date]`, doc.expiry_date);
|
|
data.append(`new_docs[${index}][reminder_days]`, doc.reminder_days || 30);
|
|
}
|
|
});
|
|
|
|
try {
|
|
const res = await fetch(`/api/branches/${branch.id}`, {
|
|
method: 'POST', // Using POST with _method=PUT for FormData
|
|
headers: {
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
|
},
|
|
body: data,
|
|
});
|
|
|
|
if (res.ok) {
|
|
onRefresh();
|
|
onClose();
|
|
} else {
|
|
const err = await res.json();
|
|
alert(err.message || 'Error updating branch');
|
|
}
|
|
} catch (error) {
|
|
alert('An error occurred. Please try again.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (!isOpen || !branch) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-in fade-in duration-300">
|
|
<div className="bg-white w-full max-w-4xl rounded-xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-300">
|
|
{/* Header */}
|
|
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-900 tracking-tight">Edit Branch</h2>
|
|
<p className="text-sm font-medium text-gray-500">Update details for {branch.name}.</p>
|
|
</div>
|
|
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-lg transition-all text-gray-400 hover:text-gray-900">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-10 max-h-[70vh] overflow-y-auto space-y-8 no-scrollbar">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
{/* Left: Info */}
|
|
<div className="space-y-6">
|
|
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
|
<MapPin size={18} className="text-red-500" />
|
|
Basic Information
|
|
</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Branch Name</label>
|
|
<input
|
|
type="text" required
|
|
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500 transition-all text-sm"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Location</label>
|
|
<input
|
|
type="text" required
|
|
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500 transition-all text-sm"
|
|
value={formData.location}
|
|
onChange={(e) => setFormData({...formData, location: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Manager Name</label>
|
|
<input
|
|
type="text" required
|
|
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500 transition-all text-sm"
|
|
value={formData.manager_name}
|
|
onChange={(e) => setFormData({...formData, manager_name: e.target.value})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
|
|
<input
|
|
type="date" required
|
|
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500 transition-all text-sm"
|
|
value={formData.operational_start_date}
|
|
onChange={(e) => setFormData({...formData, operational_start_date: e.target.value})}
|
|
/>
|
|
</div>
|
|
|
|
<div className="p-4 bg-red-50/50 rounded-2xl border border-red-100 space-y-4">
|
|
<h4 className="text-xs font-bold text-red-900 flex items-center gap-2">
|
|
<Calendar size={14} />
|
|
PAYROLL CYCLE CONFIG
|
|
</h4>
|
|
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div>
|
|
<label className="block text-[10px] font-bold text-red-800/60 uppercase mb-1">From Day</label>
|
|
<input
|
|
type="number" min="1" max="31" required
|
|
className="w-full px-3 py-1.5 bg-white border border-red-100 rounded-lg focus:outline-none focus:border-red-500 transition-all text-xs font-bold"
|
|
value={formData.payroll_from_day}
|
|
onChange={(e) => setFormData({...formData, payroll_from_day: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-[10px] font-bold text-red-800/60 uppercase mb-1">To Day</label>
|
|
<input
|
|
type="number" min="1" max="31" required
|
|
className="w-full px-3 py-1.5 bg-white border border-red-100 rounded-lg focus:outline-none focus:border-red-500 transition-all text-xs font-bold"
|
|
value={formData.payroll_to_day}
|
|
onChange={(e) => setFormData({...formData, payroll_to_day: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-[10px] font-bold text-red-800/60 uppercase mb-1">Generate</label>
|
|
<input
|
|
type="number" min="1" max="31" required
|
|
className="w-full px-3 py-1.5 bg-white border border-red-100 rounded-lg focus:outline-none focus:border-red-500 transition-all text-xs font-bold"
|
|
value={formData.salary_generation_day}
|
|
onChange={(e) => setFormData({...formData, salary_generation_day: e.target.value})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
|
<select
|
|
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500 transition-all text-sm"
|
|
value={formData.status}
|
|
onChange={(e) => setFormData({...formData, status: e.target.value})}
|
|
>
|
|
<option value="Active">Active</option>
|
|
<option value="Inactive">Inactive</option>
|
|
<option value="Maintenance">Maintenance</option>
|
|
</select>
|
|
</div>
|
|
|
|
{formData.status === 'Inactive' && (loadingStaff ? (
|
|
<div className="p-4 bg-gray-50 rounded-xl text-center text-xs text-gray-400">Checking for active staff...</div>
|
|
) : activeStaff.length > 0 && (
|
|
<div className="p-5 bg-orange-50 rounded-2xl border border-orange-100 space-y-4 animate-in slide-in-from-top-2">
|
|
<div className="flex items-center gap-2 text-orange-800">
|
|
<User size={16} />
|
|
<h4 className="text-sm font-bold mt-1">Active Staff Detected ({activeStaff.length})</h4>
|
|
</div>
|
|
<p className="text-[11px] text-orange-700 font-bold leading-relaxed">
|
|
Warning: Inactivating this branch will automatically inactivate all {activeStaff.length} active staff members.
|
|
</p>
|
|
|
|
<div className="max-h-32 overflow-y-auto no-scrollbar space-y-1 pr-1 border-t border-orange-100/50 pt-2">
|
|
{activeStaff.map(s => (
|
|
<div key={s.id} className="px-3 py-1.5 bg-white/50 rounded flex items-center justify-between text-[10px] text-orange-900 border border-orange-100/50">
|
|
<span className="font-bold">{s.full_name}</span>
|
|
<span className="opacity-60">{s.role}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: Docs */}
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
|
|
<Upload size={18} className="text-red-500" />
|
|
Documents
|
|
</h3>
|
|
<button
|
|
type="button"
|
|
onClick={handleAddDocRow}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-red-50 text-red-500 rounded-lg text-xs font-bold hover:bg-red-100 transition-all border border-red-100"
|
|
>
|
|
<Plus size={14} />
|
|
<span>Add Another</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{/* Existing Docs */}
|
|
<div className="space-y-3">
|
|
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest ml-1">Existing Files</p>
|
|
{branch.documents && branch.documents.length > 0 ? (
|
|
branch.documents.map((doc) => (
|
|
<div key={doc.id} className="p-3 bg-gray-50 rounded-xl flex items-center justify-between border border-transparent hover:border-red-500/20 transition-all">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center text-red-500 shadow-sm">
|
|
<Box size={14} />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-bold text-gray-900">{doc.name} - <span className="text-gray-400 font-medium">{doc.document_number || 'N/A'}</span></p>
|
|
<p className="text-[10px] text-gray-400">Expires: {new Date(doc.expiry_date).toLocaleDateString()}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-xs text-gray-400 italic ml-1">No documents uploaded.</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* New Doc Rows */}
|
|
{newDocs.map((doc, index) => (
|
|
<div key={index} className="p-4 bg-red-50/30 rounded-2xl border border-red-100 space-y-3 animate-in slide-in-from-top-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-[10px] font-bold text-red-500 uppercase">New Document Row</span>
|
|
<button type="button" onClick={() => handleRemoveDocRow(index)} className="text-red-400 hover:text-red-600 transition-colors">
|
|
<Trash2 size={14} />
|
|
</button>
|
|
</div>
|
|
<input
|
|
type="text" required
|
|
placeholder="Document Name"
|
|
className="w-full px-4 py-2 bg-white border border-transparent rounded-xl text-xs font-medium focus:border-red-500 outline-none"
|
|
value={doc.name}
|
|
onChange={(e) => handleNewDocChange(index, 'name', e.target.value)}
|
|
/>
|
|
<div className="grid grid-cols-4 gap-2">
|
|
<div className="relative">
|
|
<input
|
|
type="file" required
|
|
className="hidden"
|
|
id={`edit-file-${index}`}
|
|
onChange={(e) => handleNewDocChange(index, 'file', e.target.files[0])}
|
|
/>
|
|
<label htmlFor={`edit-file-${index}`} className={`flex items-center justify-center gap-2 py-2 rounded-xl border border-dashed transition-all cursor-pointer text-[10px] font-bold ${doc.file ? 'bg-emerald-50 border-emerald-200 text-emerald-600' : 'bg-white border-red-200 text-red-400 hover:border-red-500'}`}>
|
|
<Upload size={12} />
|
|
{doc.file ? (doc.file.name ? doc.file.name.substring(0, 5) + '...' : 'Uploaded') : 'Upload'}
|
|
</label>
|
|
</div>
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
placeholder="Doc Number"
|
|
className="w-full px-2 py-2 bg-white border border-transparent rounded-xl text-[10px] font-medium focus:border-red-500 outline-none"
|
|
value={doc.document_number}
|
|
onChange={(e) => handleNewDocChange(index, 'document_number', e.target.value)}
|
|
/>
|
|
<p className="text-[7px] text-gray-400 mt-0.5 ml-1 font-bold">Doc Number</p>
|
|
</div>
|
|
<div className="relative">
|
|
<input
|
|
type="date" required
|
|
className="w-full px-2 py-2 bg-white border border-transparent rounded-xl text-[10px] font-medium focus:border-red-500 outline-none"
|
|
value={doc.expiry_date}
|
|
onChange={(e) => handleNewDocChange(index, 'expiry_date', e.target.value)}
|
|
/>
|
|
<p className="text-[7px] text-gray-400 mt-0.5 ml-1 font-bold">Expiry</p>
|
|
</div>
|
|
<div className="relative">
|
|
<input
|
|
type="number" required
|
|
placeholder="Remind (Days)"
|
|
className="w-full px-2 py-2 bg-white border border-transparent rounded-xl text-[10px] font-medium focus:border-red-500 outline-none"
|
|
value={doc.reminder_days}
|
|
onChange={(e) => handleNewDocChange(index, 'reminder_days', e.target.value)}
|
|
/>
|
|
<p className="text-[7px] text-gray-400 mt-0.5 ml-1 font-bold">Days Before</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-3 pt-6 border-t border-gray-100">
|
|
<button type="button" onClick={onClose} className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-all text-sm font-medium">Cancel</button>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="flex items-center gap-2 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-all text-sm font-medium disabled:opacity-50"
|
|
>
|
|
<Save size={16} />
|
|
{loading ? 'Saving...' : 'Save Changes'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|