908 lines
60 KiB
JavaScript
908 lines
60 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import Toast from '../Components/Toast';
|
||
import { Shield, ChevronLeft, Save, History, Calculator, Clock, CheckCircle2, X, Plus, Trash2 } from 'lucide-react';
|
||
|
||
export default function StaffEdit({ id }) {
|
||
const isReceptionist = window.__APP_DATA__?.role === 'receptionist';
|
||
const basePath = isReceptionist ? '/receptionist' : '/owner';
|
||
const [loading, setLoading] = useState(true);
|
||
const [saving, setSaving] = useState(false);
|
||
const [branches, setBranches] = useState([]);
|
||
const [history, setHistory] = useState([]);
|
||
const [showHistory, setShowHistory] = useState(false);
|
||
const [loadingHistory, setLoadingHistory] = useState(false);
|
||
const [toast, setToast] = useState(null);
|
||
const [initialCommission, setInitialCommission] = useState({ amount: '', count: '' });
|
||
const [commissionChanged, setCommissionChanged] = useState(false);
|
||
const [periodSelected, setPeriodSelected] = useState(false);
|
||
const [isAdvanceConfirmOpen, setIsAdvanceConfirmOpen] = useState(false);
|
||
const [advanceHistory, setAdvanceHistory] = useState([]);
|
||
const [loadingAdvanceHistory, setLoadingAdvanceHistory] = useState(false);
|
||
const [formData, setFormData] = useState({
|
||
full_name: '',
|
||
email: '',
|
||
phone: '',
|
||
role: 'Trainer',
|
||
branch_id: '',
|
||
joining_date: '',
|
||
status: 'Active',
|
||
salary_type: 'Fixed',
|
||
salary_amount: '',
|
||
cycle_effective_date: '',
|
||
advance_enabled: false,
|
||
advance_amount: '',
|
||
advance_repayment_mode: 'Full Next Month',
|
||
advance_months: '',
|
||
commission_enabled: false,
|
||
commission_amount: '',
|
||
commission_member_count: '',
|
||
apply_from: 'this_month',
|
||
emirates_id: '',
|
||
emirates_id_expiry: '',
|
||
emirates_id_reminder_days: 30,
|
||
salary_reminder_enabled: true,
|
||
document_expiry_enabled: true,
|
||
family_members: [],
|
||
documents: []
|
||
});
|
||
|
||
useEffect(() => {
|
||
const fetchData = async () => {
|
||
try {
|
||
// Fetch Branches
|
||
const bRes = await fetch('/api/branches');
|
||
const bData = await bRes.json();
|
||
setBranches(bData);
|
||
|
||
// Fetch Staff Details
|
||
const sRes = await fetch(`/api/staff/${id}`);
|
||
const sData = await sRes.json();
|
||
|
||
if (sData) {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const autoGetAdvance = urlParams.get('get_advance') === 'true';
|
||
|
||
setFormData({
|
||
...formData,
|
||
full_name: sData.full_name || '',
|
||
email: sData.email || '',
|
||
phone: sData.phone || '',
|
||
role: sData.role || 'Trainer',
|
||
branch_id: sData.branch_id || '',
|
||
joining_date: sData.joining_date || '',
|
||
status: sData.status || 'Active',
|
||
salary_type: sData.salary_type || 'Monthly',
|
||
salary_amount: sData.salary_amount || '',
|
||
cycle_effective_date: sData.cycle_effective_date || '',
|
||
advance_enabled: autoGetAdvance ? true : !!sData.advance_enabled,
|
||
advance_amount: sData.advance_amount || '',
|
||
advance_repayment_mode: sData.advance_repayment_mode || 'Full Next Month',
|
||
advance_months: sData.advance_months || '',
|
||
commission_enabled: !!sData.commission_enabled,
|
||
commission_amount: sData.commission_amount || '',
|
||
commission_member_count: sData.commission_member_count || '',
|
||
emirates_id: sData.emirates_id || '',
|
||
emirates_id_expiry: sData.emirates_id_expiry || '',
|
||
emirates_id_reminder_days: sData.emirates_id_reminder_days || 30,
|
||
family_members: sData.family_members && sData.family_members.length > 0 ? sData.family_members : [{ name: '', relation: '', contact: '' }],
|
||
documents: sData.documents || [],
|
||
apply_from: '' // Reset to force choice if changed
|
||
});
|
||
|
||
setInitialCommission({
|
||
amount: sData.commission_amount || '',
|
||
count: sData.commission_member_count || ''
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching data:', error);
|
||
setToast({ message: 'Error loading staff details. Please refresh.', type: 'error' });
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
fetchData();
|
||
fetchAdvanceHistory();
|
||
}, [id]);
|
||
|
||
const fetchAdvanceHistory = async () => {
|
||
setLoadingAdvanceHistory(true);
|
||
try {
|
||
const res = await fetch(`/api/staff/${id}/advance-history`);
|
||
const data = await res.json();
|
||
setAdvanceHistory(data);
|
||
} catch (error) {
|
||
console.error('Error fetching advance history:', error);
|
||
} finally {
|
||
setLoadingAdvanceHistory(false);
|
||
}
|
||
};
|
||
|
||
const fetchHistory = async () => {
|
||
setLoadingHistory(true);
|
||
setShowHistory(true);
|
||
try {
|
||
const res = await fetch(`/api/staff/${id}/commission-history`);
|
||
const data = await res.json();
|
||
setHistory(data);
|
||
} catch (error) {
|
||
console.error('Error fetching history:', error);
|
||
} finally {
|
||
setLoadingHistory(false);
|
||
}
|
||
};
|
||
|
||
const handleChange = (e) => {
|
||
const { name, value, type, checked } = e.target;
|
||
const newFormData = {
|
||
...formData,
|
||
[name]: type === 'checkbox' ? checked : value
|
||
};
|
||
|
||
// Detect commission changes
|
||
if (name === 'commission_amount' || name === 'commission_member_count' || name === 'commission_enabled') {
|
||
const hasChanged =
|
||
newFormData.commission_enabled !== (initialCommission.enabled ?? false) ||
|
||
newFormData.commission_amount !== initialCommission.amount ||
|
||
newFormData.commission_member_count !== initialCommission.count;
|
||
|
||
setCommissionChanged(hasChanged);
|
||
if (!hasChanged) setPeriodSelected(false);
|
||
}
|
||
|
||
setFormData(newFormData);
|
||
};
|
||
|
||
const handleDocumentChange = (index, e) => {
|
||
const { name, value } = e.target;
|
||
const newDocs = [...formData.documents];
|
||
newDocs[index][name] = value;
|
||
setFormData({ ...formData, documents: newDocs });
|
||
};
|
||
|
||
const addDocumentRow = () => {
|
||
setFormData({
|
||
...formData,
|
||
documents: [...formData.documents, { name: '', document_number: '', expiry_date: '', reminder_days: 30, file: null }]
|
||
});
|
||
};
|
||
|
||
const removeDocumentRow = (index) => {
|
||
const newDocs = formData.documents.filter((_, i) => i !== index);
|
||
setFormData({ ...formData, documents: newDocs });
|
||
};
|
||
|
||
const handleFamilyMemberChange = (index, e) => {
|
||
const { name, value } = e.target;
|
||
const newMembers = [...formData.family_members];
|
||
newMembers[index][name] = value;
|
||
setFormData({ ...formData, family_members: newMembers });
|
||
};
|
||
|
||
const addFamilyMemberRow = () => {
|
||
setFormData({
|
||
...formData,
|
||
family_members: [...formData.family_members, { name: '', relation: '', contact: '' }]
|
||
});
|
||
};
|
||
|
||
const removeFamilyMemberRow = (index) => {
|
||
const newMembers = formData.family_members.filter((_, i) => i !== index);
|
||
setFormData({ ...formData, family_members: newMembers });
|
||
};
|
||
|
||
const handleSave = async (e) => {
|
||
if (e) e.preventDefault();
|
||
|
||
// Validation for commission change
|
||
if (commissionChanged && !formData.apply_from) {
|
||
setToast({ message: 'Please select when to apply commission changes (This Month or Next Month).', type: 'error' });
|
||
// Scroll to commission section
|
||
const el = document.getElementById('commission-section');
|
||
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
||
return;
|
||
}
|
||
|
||
// Branch Start Date Validation - REMOVED AS PER USER REQUEST
|
||
/*
|
||
const selectedBranch = branches.find(b => b.id == formData.branch_id);
|
||
if (selectedBranch && formData.joining_date && selectedBranch.operational_start_date) {
|
||
if (formData.joining_date < selectedBranch.operational_start_date) {
|
||
setToast({
|
||
message: `Error: Joining date (${formData.joining_date}) cannot be before branch start date (${selectedBranch.operational_start_date}).`,
|
||
type: 'error'
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
*/
|
||
|
||
// Phone Validation
|
||
if (formData.phone && !/^(\+91|91|0)?[6-9]\d{9}$|^(\+971|971|0)?5[024568]\d{7}$/.test(formData.phone.replace(/[\s-]/g, ''))) {
|
||
setToast({ message: 'Error: Invalid Phone format. Only Indian (+91) and UAE (+971) numbers are allowed.', type: 'error' });
|
||
return;
|
||
}
|
||
|
||
// Family Contact Validation
|
||
for (let i = 0; i < formData.family_members.length; i++) {
|
||
const member = formData.family_members[i];
|
||
if (member.contact && !/^(\+91|91|0)?[6-9]\d{9}$|^(\+971|971|0)?5[024568]\d{7}$/.test(member.contact.replace(/[\s-]/g, ''))) {
|
||
setToast({ message: `Error: Invalid Family Contact format for ${member.name || (i+1)}. Only Indian (+91) and UAE (+971) numbers are allowed.`, type: 'error' });
|
||
return;
|
||
}
|
||
}
|
||
|
||
setSaving(true);
|
||
|
||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||
|
||
const data = new FormData();
|
||
Object.keys(formData).forEach(key => {
|
||
if (key === 'documents') {
|
||
formData.documents.forEach((doc, index) => {
|
||
if (doc.id) data.append(`documents[${index}][id]`, doc.id);
|
||
data.append(`documents[${index}][name]`, doc.name);
|
||
data.append(`documents[${index}][document_number]`, doc.document_number || '');
|
||
data.append(`documents[${index}][expiry_date]`, doc.expiry_date || '');
|
||
data.append(`documents[${index}][reminder_days]`, doc.reminder_days || 30);
|
||
if (doc.file) {
|
||
data.append(`documents[${index}][file]`, doc.file);
|
||
}
|
||
});
|
||
} else if (key === 'family_members') {
|
||
formData.family_members.forEach((member, index) => {
|
||
data.append(`family_members[${index}][name]`, member.name);
|
||
data.append(`family_members[${index}][relation]`, member.relation);
|
||
data.append(`family_members[${index}][contact]`, member.contact);
|
||
});
|
||
} else if (formData[key] !== null && formData[key] !== undefined) {
|
||
let value = formData[key];
|
||
if (typeof value === 'boolean') {
|
||
value = value ? '1' : '0';
|
||
}
|
||
data.append(key, value);
|
||
}
|
||
});
|
||
|
||
// Use POST with _method=PUT for multipart support in Laravel
|
||
data.append('_method', 'PUT');
|
||
|
||
try {
|
||
const response = await fetch(`/api/staff/${id}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Accept': 'application/json',
|
||
'X-CSRF-TOKEN': csrfToken
|
||
},
|
||
body: data
|
||
});
|
||
|
||
if (response.ok) {
|
||
setToast({ message: 'Staff updated successfully!', type: 'success' });
|
||
setTimeout(() => window.location.href = `${basePath}/staff`, 1500);
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
if (errorData.errors) {
|
||
const message = Object.entries(errorData.errors)
|
||
.map(([field, msgs]) => `${field.replace('_', ' ')}: ${msgs.join(', ')}`)
|
||
.join('\n');
|
||
setToast({ message: 'Validation Error:\n' + message, type: 'error' });
|
||
} else {
|
||
setToast({ message: 'Error updating staff details: ' + (errorData.message || response.statusText), type: 'error' });
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('API Error:', error);
|
||
setToast({ message: 'Failed to update staff.', type: 'error' });
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
if (loading) return <div className="p-10 text-center font-medium text-gray-400">Loading details...</div>;
|
||
|
||
return (
|
||
<>
|
||
{toast && <Toast message={toast.message} type={toast.type} onClose={() => setToast(null)} />}
|
||
<form onSubmit={handleSave} className="p-8 max-w-5xl mx-auto space-y-8">
|
||
{/* Top Actions */}
|
||
<div className="flex items-center justify-between underline-offset-4">
|
||
<button type="button" onClick={() => window.location.href = `${basePath}/staff`} className="flex items-center gap-2 text-sm font-bold text-gray-500 hover:text-gray-900 transition-colors">
|
||
<ChevronLeft size={18} />
|
||
<span>Back to Staff List</span>
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={saving}
|
||
className="flex items-center gap-2 px-6 py-2.5 bg-red-500 text-white rounded-xl font-bold text-sm hover:bg-red-600 transition-all shadow-lg shadow-red-100 disabled:opacity-50"
|
||
>
|
||
<Save size={18} />
|
||
<span>{saving ? 'Saving...' : 'Update Staff Details'}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-8">
|
||
{/* Card 1: Basic Details */}
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||
<div className="p-6 border-b border-gray-50 bg-gray-50/30">
|
||
<h3 className="text-lg font-bold text-gray-900">Basic Details</h3>
|
||
</div>
|
||
<div className="p-8 grid grid-cols-2 gap-6">
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Full Name *</label>
|
||
<input required type="text" name="full_name" value={formData.full_name} onChange={handleChange} className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium" />
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Email *</label>
|
||
<input required type="email" name="email" value={formData.email} onChange={handleChange} className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium" />
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Phone (India/UAE)</label>
|
||
<input type="text" name="phone" value={formData.phone} onChange={handleChange} className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium" placeholder="Ex: +91 98765 43210 or +971 50 123 4567" />
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Role *</label>
|
||
<select name="role" value={formData.role} onChange={handleChange} className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium appearance-none">
|
||
<option value="Trainer">Trainer</option>
|
||
<option value="Receptionist">Receptionist</option>
|
||
<option value="Manager">Manager</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Joining Date *</label>
|
||
<input required type="date" name="joining_date" value={formData.joining_date} onChange={handleChange} className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium" />
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Status</label>
|
||
<select name="status" value={formData.status} onChange={handleChange} className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium appearance-none">
|
||
<option value="Active">Active</option>
|
||
<option value="Inactive">Inactive</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Card 2: Salary Details */}
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||
<div className="p-6 border-b border-gray-50 bg-gray-50/30">
|
||
<h3 className="text-lg font-bold text-gray-900">Salary Details</h3>
|
||
</div>
|
||
<div className="p-8 grid grid-cols-2 gap-6">
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Salary Type</label>
|
||
<select name="salary_type" value={formData.salary_type || 'Monthly'} onChange={handleChange} className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium appearance-none">
|
||
<option value="Monthly">Monthly</option>
|
||
<option value="Daily">Daily</option>
|
||
<option value="Weekly">Weekly</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Amount (AED) *</label>
|
||
<input required type="number" name="salary_amount" value={formData.salary_amount || ''} onChange={handleChange} className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium" placeholder="0.00" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Card 3: Salary Advance */}
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||
<div className="p-6 border-b border-gray-50 bg-gray-50/30">
|
||
<h3 className="text-lg font-bold text-gray-900">Salary Advance</h3>
|
||
</div>
|
||
<div className="p-8 space-y-6">
|
||
{!formData.advance_enabled ? (
|
||
<div className="py-8 text-center bg-gray-50 rounded-2xl border-2 border-dashed border-gray-100">
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormData({...formData, advance_enabled: true})}
|
||
className="px-6 py-2.5 bg-white border border-gray-200 text-gray-700 rounded-xl text-sm font-bold hover:bg-gray-50 transition-all shadow-sm flex items-center gap-2 mx-auto"
|
||
>
|
||
Get Advance
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="animate-in slide-in-from-top-2 duration-300 space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-2 h-2 rounded-full bg-orange-500 shadow-[0_0_10px_rgba(249,115,22,0.5)]" />
|
||
<span className="text-gray-900 font-bold text-sm">Advance Configuration Active</span>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsAdvanceConfirmOpen(true)}
|
||
className="text-xs font-bold text-red-500 hover:text-red-600 transition-colors uppercase tracking-widest"
|
||
>
|
||
Remove Advance
|
||
</button>
|
||
</div>
|
||
|
||
{/* Advance History Section */}
|
||
{advanceHistory.length > 0 && (
|
||
<div className="bg-gray-50/50 rounded-2xl border border-gray-100 overflow-hidden">
|
||
<div className="px-4 py-3 bg-gray-100/50 border-b border-gray-100 flex items-center justify-between">
|
||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest text-[#101828]">Advance History</span>
|
||
<div className="flex gap-4">
|
||
<span className="text-[9px] font-bold text-gray-400">
|
||
Due: {advanceHistory
|
||
.filter(h => h.status === 'Pending')
|
||
.reduce((sum, h) => sum + (parseFloat(h.advance_amount) - parseFloat(h.paid_amount || 0)), 0)
|
||
.toLocaleString()} AED
|
||
</span>
|
||
<span className="text-[9px] font-bold text-emerald-500">
|
||
Total Paid: {advanceHistory.reduce((sum, h) => sum + parseFloat(h.paid_amount || 0), 0).toLocaleString()} AED
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="divide-y divide-gray-100 max-h-48 overflow-y-auto">
|
||
{advanceHistory.map((h, i) => (
|
||
<div key={i} className="px-4 py-3 flex items-center justify-between hover:bg-white transition-colors">
|
||
<div className="flex items-center gap-3">
|
||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center font-bold text-[10px] ${h.status === 'Closed' ? 'bg-emerald-50 text-emerald-600' : 'bg-orange-50 text-orange-600'}`}>
|
||
{h.status === 'Closed' ? <CheckCircle2 size={14} /> : <Clock size={14} />}
|
||
</div>
|
||
<div>
|
||
<p className="text-xs font-bold text-gray-900">{h.advance_amount.toLocaleString()} AED</p>
|
||
<p className="text-[10px] text-gray-400 font-medium">Taken on {new Date(h.created_at).toLocaleDateString()}</p>
|
||
</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-[10px] font-black uppercase tracking-widest text-emerald-500">{parseFloat(h.paid_amount || 0).toLocaleString()} Paid</p>
|
||
<p className="text-[9px] font-bold text-gray-300">{(parseFloat(h.advance_amount) - parseFloat(h.paid_amount || 0)).toLocaleString()} Pending</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-2 gap-6 pt-4">
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Advance Amount (AED)</label>
|
||
<input type="number" name="advance_amount" value={formData.advance_amount || ''} onChange={handleChange} className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium" placeholder="e.g. 3000" />
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 text-center sm:text-left">Repayment Mode</label>
|
||
<div className="flex gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormData({...formData, advance_repayment_mode: 'Full Next Month'})}
|
||
className={`flex-1 py-3 text-sm font-bold rounded-xl border transition-all ${formData.advance_repayment_mode === 'Full Next Month' ? 'border-orange-500 bg-orange-50/50 text-orange-600' : 'border-gray-200 text-gray-500 bg-white'}`}
|
||
>
|
||
Full Next Month
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormData({...formData, advance_repayment_mode: 'Divide by Months'})}
|
||
className={`flex-1 py-3 text-sm font-bold rounded-xl border transition-all ${formData.advance_repayment_mode === 'Divide by Months' ? 'border-orange-500 bg-orange-50/50 text-orange-600' : 'border-gray-200 text-gray-500 bg-white'}`}
|
||
>
|
||
Divide by Months
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{formData.advance_repayment_mode === 'Divide by Months' && (
|
||
<div className="grid grid-cols-2 gap-6 p-6 bg-gray-50 rounded-2xl border border-gray-100 animate-in zoom-in-95 duration-200">
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Number of Months</label>
|
||
<input
|
||
type="number"
|
||
name="advance_months"
|
||
value={formData.advance_months || ''}
|
||
onChange={handleChange}
|
||
className="w-full bg-white border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-red-500 transition-all font-medium"
|
||
placeholder="e.g. 3"
|
||
/>
|
||
</div>
|
||
<div className="flex flex-col justify-center">
|
||
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mb-1">Monthly Deduction</p>
|
||
<p className="text-xl font-black text-orange-600">
|
||
AED {((parseFloat(formData.advance_amount) || 0) / (parseInt(formData.advance_months) || 1)).toFixed(2)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Card 4: Commission Settings */}
|
||
<div id="commission-section" className={`bg-white rounded-2xl border shadow-sm overflow-hidden text-purple-600 transition-all ${commissionChanged && !formData.apply_from ? 'border-purple-300 ring-4 ring-purple-50' : 'border-gray-100'}`}>
|
||
<div className="p-6 border-b border-gray-50 bg-gray-50/30 font-bold flex items-center justify-between">
|
||
<span>Commission Settings</span>
|
||
{id !== 'new' && (
|
||
<button
|
||
type="button"
|
||
onClick={fetchHistory}
|
||
className="flex items-center gap-2 px-4 py-2 bg-purple-50 text-purple-600 rounded-xl text-xs font-bold hover:bg-purple-100 transition-all border border-purple-100"
|
||
>
|
||
<History size={14} />
|
||
<span>View History</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div className="p-8 space-y-6">
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
type="checkbox"
|
||
name="commission_enabled"
|
||
checked={formData.commission_enabled}
|
||
onChange={handleChange}
|
||
className="w-5 h-5 rounded border-gray-300 text-purple-600 focus:ring-purple-500 cursor-pointer"
|
||
/>
|
||
<span className="text-[#344054] font-medium text-sm">Enable per-person commission</span>
|
||
</div>
|
||
|
||
{formData.commission_enabled && (
|
||
<div className="space-y-6 animate-in slide-in-from-top-2 duration-300">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Number of Members</label>
|
||
<div className="relative">
|
||
<input
|
||
type="number"
|
||
name="commission_member_count"
|
||
value={formData.commission_member_count || ''}
|
||
onChange={handleChange}
|
||
className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-purple-500 transition-all font-medium pl-10"
|
||
placeholder="e.g. 10"
|
||
/>
|
||
<div className="absolute left-3 top-3.5 text-gray-400">
|
||
<Calculator size={16} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Amount per Person (AED)</label>
|
||
<input
|
||
type="number"
|
||
name="commission_amount"
|
||
value={formData.commission_amount || ''}
|
||
onChange={handleChange}
|
||
className="w-full bg-gray-50 border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-purple-500 transition-all font-medium"
|
||
placeholder="e.g. 50"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Instant Total Calculation */}
|
||
<div className="p-4 bg-purple-50 rounded-2xl border border-purple-100 flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="p-2 bg-white rounded-xl shadow-sm text-purple-500">
|
||
<Calculator size={18} />
|
||
</div>
|
||
<div>
|
||
<p className="text-sm font-bold text-gray-900">Total Monthly Commission</p>
|
||
<p className="text-xs text-gray-400 font-medium">Calculated instantly (Members × Rate)</p>
|
||
</div>
|
||
</div>
|
||
<div className="text-xl font-black text-purple-600">
|
||
AED {( (parseFloat(formData.commission_member_count) || 0) * (parseFloat(formData.commission_amount) || 0) ).toFixed(2)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Apply Period Selection */}
|
||
<div className="space-y-3">
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider">Start Applying From</label>
|
||
<div className="flex gap-4">
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormData({...formData, apply_from: 'this_month'})}
|
||
className={`flex-1 p-4 rounded-2xl border-2 transition-all flex flex-col gap-1 items-start ${formData.apply_from === 'this_month' ? 'border-purple-500 bg-purple-50/50 text-purple-700' : 'border-gray-100 bg-gray-50 text-gray-500 hover:border-gray-200'}`}
|
||
>
|
||
<div className="flex items-center gap-2 font-bold text-sm">
|
||
{formData.apply_from === 'this_month' ? <CheckCircle2 size={16} /> : <Clock size={16} />}
|
||
<span>This Month</span>
|
||
</div>
|
||
<span className="text-[10px] opacity-70">Will apply to {new Date().toLocaleString('default', { month: 'long', year: 'numeric' })}</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setFormData({...formData, apply_from: 'next_month'})}
|
||
className={`flex-1 p-4 rounded-2xl border-2 transition-all flex flex-col gap-1 items-start ${formData.apply_from === 'next_month' ? 'border-purple-500 bg-purple-50/50 text-purple-700' : 'border-gray-100 bg-gray-50 text-gray-500 hover:border-gray-200'}`}
|
||
>
|
||
<div className="flex items-center gap-2 font-bold text-sm">
|
||
{formData.apply_from === 'next_month' ? <CheckCircle2 size={16} /> : <Clock size={16} />}
|
||
<span>Next Month</span>
|
||
</div>
|
||
<span className="text-[10px] opacity-70">Will apply from {new Date(new Date().setMonth(new Date().getMonth() + 1)).toLocaleString('default', { month: 'long', year: 'numeric' })}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-8">
|
||
{/* Documentation Card */}
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden text-blue-600">
|
||
<div className="p-6 border-b border-gray-50 bg-gray-50/30 flex items-center justify-between font-bold">
|
||
<span>Documentation</span>
|
||
<button
|
||
type="button"
|
||
onClick={addDocumentRow}
|
||
className="flex items-center gap-2 px-3 py-1.5 bg-blue-50 text-blue-600 rounded-lg text-xs hover:bg-blue-100 transition-all border border-blue-100"
|
||
>
|
||
<Plus size={14} />
|
||
<span>Add Document</span>
|
||
</button>
|
||
</div>
|
||
<div className="p-8 space-y-8">
|
||
{formData.documents.map((doc, index) => (
|
||
<div key={index} className="relative p-6 bg-gray-50 rounded-2xl border border-gray-100 animate-in slide-in-from-top-2 duration-300">
|
||
<button
|
||
type="button"
|
||
onClick={() => removeDocumentRow(index)}
|
||
className="absolute top-4 right-4 text-red-400 hover:text-red-600 transition-colors"
|
||
>
|
||
<Trash2 size={18} />
|
||
</button>
|
||
<div className="grid grid-cols-2 gap-6">
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Document Name</label>
|
||
<input
|
||
type="text"
|
||
name="name"
|
||
value={doc.name}
|
||
onChange={(e) => handleDocumentChange(index, e)}
|
||
className="w-full bg-white border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 transition-all font-medium"
|
||
placeholder="Ex: Visa, Emirates ID, Passport"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Document Number</label>
|
||
<input
|
||
type="text"
|
||
name="document_number"
|
||
value={doc.document_number || ''}
|
||
onChange={(e) => handleDocumentChange(index, e)}
|
||
className="w-full bg-white border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 transition-all font-medium"
|
||
placeholder="Number / ID"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Expiry Date</label>
|
||
<input
|
||
type="date"
|
||
name="expiry_date"
|
||
value={doc.expiry_date || ''}
|
||
onChange={(e) => handleDocumentChange(index, e)}
|
||
className="w-full bg-white border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 transition-all font-medium"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Days Before</label>
|
||
<input
|
||
type="number"
|
||
name="reminder_days"
|
||
value={doc.reminder_days || 30}
|
||
onChange={(e) => handleDocumentChange(index, e)}
|
||
className="w-full bg-white border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500 transition-all font-medium"
|
||
placeholder="30"
|
||
/>
|
||
</div>
|
||
<div className="col-span-2">
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">
|
||
{doc.path ? 'Update Document (Keep empty to retain existing)' : 'Upload Document'}
|
||
</label>
|
||
<div className="flex items-center gap-4">
|
||
<input
|
||
type="file"
|
||
name="file"
|
||
onChange={(e) => handleDocumentChange(index, e)}
|
||
className="flex-1 bg-white border-none rounded-xl px-4 py-2 text-xs focus:ring-2 focus:ring-blue-500 transition-all font-medium file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-xs file:font-black file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
|
||
/>
|
||
{doc.path && (
|
||
<span className="text-[10px] font-bold text-emerald-500 bg-emerald-50 px-3 py-1.5 rounded-lg flex items-center gap-1">
|
||
<CheckCircle2 size={12} />
|
||
File Exists
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
{formData.documents.length === 0 && (
|
||
<div className="text-center py-6 text-gray-400 font-medium italic">
|
||
No documents added yet.
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Family Members Card */}
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden text-emerald-600">
|
||
<div className="p-6 border-b border-gray-50 bg-gray-50/30 flex items-center justify-between font-bold">
|
||
<span>Family Members</span>
|
||
<button
|
||
type="button"
|
||
onClick={addFamilyMemberRow}
|
||
className="flex items-center gap-2 px-3 py-1.5 bg-emerald-50 text-emerald-600 rounded-lg text-xs hover:bg-emerald-100 transition-all border border-emerald-100"
|
||
>
|
||
<Plus size={14} />
|
||
<span>Add Member</span>
|
||
</button>
|
||
</div>
|
||
<div className="p-8 space-y-6">
|
||
{formData.family_members.map((member, index) => (
|
||
<div key={index} className="relative p-6 bg-gray-50 rounded-2xl border border-gray-100 animate-in slide-in-from-top-2 duration-300">
|
||
{formData.family_members.length > 1 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => removeFamilyMemberRow(index)}
|
||
className="absolute top-4 right-4 text-red-400 hover:text-red-600 transition-colors"
|
||
>
|
||
<Trash2 size={18} />
|
||
</button>
|
||
)}
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Member Name</label>
|
||
<input
|
||
type="text"
|
||
name="name"
|
||
value={member.name}
|
||
onChange={(e) => handleFamilyMemberChange(index, e)}
|
||
className="w-full bg-white border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-emerald-500 transition-all font-medium"
|
||
placeholder="e.g. Jane Doe"
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Relation</label>
|
||
<input
|
||
type="text"
|
||
name="relation"
|
||
value={member.relation}
|
||
onChange={(e) => handleFamilyMemberChange(index, e)}
|
||
className="w-full bg-white border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-emerald-500 transition-all font-medium"
|
||
placeholder="e.g. Spouse"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Contact Number</label>
|
||
<input
|
||
type="text"
|
||
name="contact"
|
||
value={member.contact}
|
||
onChange={(e) => handleFamilyMemberChange(index, e)}
|
||
className="w-full bg-white border-none rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-emerald-500 transition-all font-medium"
|
||
placeholder="05XXXXXXXX"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
{formData.family_members.length === 0 && (
|
||
<div className="text-center py-6 text-gray-400 font-medium italic">
|
||
No family members added yet.
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
</div>
|
||
</form>
|
||
|
||
{/* Commission History Modal */}
|
||
{showHistory && (
|
||
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
||
<div className="bg-white rounded-[2.5rem] w-full max-w-2xl max-h-[85vh] overflow-hidden flex flex-col shadow-2xl animate-in zoom-in-95 duration-200">
|
||
<div className="p-8 flex items-center justify-between border-b border-gray-100">
|
||
<div>
|
||
<h3 className="text-xl font-bold text-gray-900">Commission History</h3>
|
||
<p className="text-sm text-gray-400 font-medium">Past member counts and commissions</p>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowHistory(false)}
|
||
className="w-10 h-10 flex items-center justify-center rounded-full hover:bg-gray-100 text-gray-400 hover:text-gray-900 transition-all"
|
||
>
|
||
<X size={20} />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-auto p-8">
|
||
{loadingHistory ? (
|
||
<div className="text-center py-10 font-medium text-gray-400">Loading history...</div>
|
||
) : history.length > 0 ? (
|
||
<div className="overflow-hidden border border-gray-100 rounded-2xl">
|
||
<table className="w-full text-left border-collapse">
|
||
<thead>
|
||
<tr className="bg-gray-50/50">
|
||
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-wider">Month</th>
|
||
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-wider text-center">Members</th>
|
||
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-wider text-right">Per Head</th>
|
||
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-wider text-right">Total</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-50">
|
||
{history.map((row) => (
|
||
<tr key={row.id} className="hover:bg-gray-50/30 transition-colors">
|
||
<td className="px-6 py-4">
|
||
<span className="text-sm font-bold text-gray-900 capitalize">
|
||
{new Date(row.effective_month + "-01").toLocaleString('default', { month: 'long', year: 'numeric' })}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4 text-center">
|
||
<span className="px-3 py-1 bg-purple-50 text-purple-600 text-xs font-black rounded-lg">
|
||
{row.member_count}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4 text-right text-sm font-semibold text-gray-500">
|
||
AED {row.amount_per_head}
|
||
</td>
|
||
<td className="px-6 py-4 text-right">
|
||
<span className="text-sm font-bold text-emerald-600">AED {row.total_amount}</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-20 bg-gray-50 rounded-3xl border-2 border-dashed border-gray-100">
|
||
<p className="text-gray-400 font-medium">No history found for this staff member.</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="p-8 border-t border-gray-100 bg-gray-50/30 flex justify-end">
|
||
<button
|
||
onClick={() => setShowHistory(false)}
|
||
className="px-8 py-3 bg-gray-900 text-white rounded-xl text-sm font-bold hover:bg-black transition-all shadow-lg shadow-gray-200"
|
||
>
|
||
Close History
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{/* Advance Confirmation Modal */}
|
||
{isAdvanceConfirmOpen && (
|
||
<div className="fixed inset-0 z-[70] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
|
||
<div className="bg-white rounded-[2rem] w-full max-w-md shadow-2xl border border-gray-100 overflow-hidden animate-in zoom-in-95 duration-200">
|
||
<div className="p-8 text-center">
|
||
<div className="w-16 h-16 bg-red-50 text-red-500 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||
<Shield size={32} />
|
||
</div>
|
||
<h3 className="text-xl font-bold text-gray-900">Confirm Advance Removal</h3>
|
||
<p className="text-sm text-gray-400 font-medium mt-2 mb-8">
|
||
Are you sure you want to disable the current advance? This will stop future deductions.
|
||
{advanceHistory.find(h => h.status === 'Active') && (
|
||
<div className="mt-4 p-4 bg-orange-50 rounded-xl border border-orange-100">
|
||
<p className="text-xs font-bold text-orange-700">
|
||
Warning: There is a pending balance of {(advanceHistory.find(h => h.status === 'Active').advance_amount - advanceHistory.find(h => h.status === 'Active').paid_amount).toLocaleString()} AED.
|
||
</p>
|
||
</div>
|
||
)}
|
||
</p>
|
||
<div className="flex gap-4">
|
||
<button
|
||
onClick={() => setIsAdvanceConfirmOpen(false)}
|
||
className="flex-1 px-6 py-3 bg-gray-100 text-gray-600 rounded-xl font-bold text-sm hover:bg-gray-200 transition-all uppercase tracking-widest"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setFormData({...formData, advance_enabled: false, advance_amount: '', advance_months: ''});
|
||
setIsAdvanceConfirmOpen(false);
|
||
}}
|
||
className="flex-1 px-6 py-3 bg-red-500 text-white rounded-xl font-bold text-sm hover:bg-red-600 transition-all uppercase tracking-widest shadow-lg shadow-red-100"
|
||
>
|
||
Remove
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
}
|