331 lines
18 KiB
JavaScript
331 lines
18 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import Toast from '../Components/Toast';
|
|
import { ChevronLeft, Upload, X, Check } from 'lucide-react';
|
|
|
|
export default function InvestorAdd() {
|
|
const isReceptionist = window.__APP_DATA__?.role === 'receptionist';
|
|
const basePath = isReceptionist ? '/receptionist/investors' : '/owner/investors';
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [branches, setBranches] = useState([]);
|
|
const [toast, setToast] = useState(null);
|
|
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
investment_date: new Date().toISOString().split('T')[0],
|
|
investment_amount: '',
|
|
applicable_to_all_branches: false,
|
|
branch_ids: isReceptionist ? [window.__APP_DATA__?.user?.branch_id] : [],
|
|
roi_type: 'Percentage',
|
|
roi_value: '',
|
|
roi_period: 'Monthly',
|
|
security_proof_document: null
|
|
});
|
|
|
|
const [branchDropdownOpen, setBranchDropdownOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const fetchBranches = async () => {
|
|
try {
|
|
const response = await fetch('/api/branches?status=Active');
|
|
const data = await response.json();
|
|
setBranches(data);
|
|
} catch (error) {
|
|
console.error('Error fetching branches:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchBranches();
|
|
}, []);
|
|
|
|
const handleChange = (e) => {
|
|
const { name, value, type, checked } = e.target;
|
|
setFormData({
|
|
...formData,
|
|
[name]: type === 'checkbox' ? checked : value
|
|
});
|
|
};
|
|
|
|
const handleFileChange = (e) => {
|
|
setFormData({ ...formData, security_proof_document: e.target.files[0] });
|
|
};
|
|
|
|
const toggleBranch = (id) => {
|
|
const newBranchIds = formData.branch_ids.includes(id)
|
|
? formData.branch_ids.filter(bid => bid !== id)
|
|
: [...formData.branch_ids, id];
|
|
setFormData({ ...formData, branch_ids: newBranchIds });
|
|
};
|
|
|
|
const handleSave = async (e) => {
|
|
e.preventDefault();
|
|
|
|
if (saving) return;
|
|
|
|
if (formData.name.length > 30) {
|
|
setToast({ message: 'Investor name cannot exceed 30 characters.', type: 'error' });
|
|
return;
|
|
}
|
|
|
|
if (!formData.security_proof_document) {
|
|
setToast({ message: 'Please upload a security proof document.', type: 'error' });
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
|
|
const data = new FormData();
|
|
Object.keys(formData).forEach(key => {
|
|
if (key === 'branch_ids') {
|
|
formData.branch_ids.forEach(id => data.append('branch_ids[]', id));
|
|
} else if (key === 'security_proof_document' && formData[key]) {
|
|
data.append('security_proof_document', formData[key]);
|
|
} else if (key === 'applicable_to_all_branches') {
|
|
data.append(key, formData[key] ? '1' : '0');
|
|
} else {
|
|
data.append(key, formData[key]);
|
|
}
|
|
});
|
|
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
|
|
try {
|
|
const response = await fetch('/api/investors', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken
|
|
},
|
|
body: data
|
|
});
|
|
|
|
if (response.ok) {
|
|
setToast({ message: 'Investor added successfully!', type: 'success' });
|
|
setTimeout(() => window.location.href = basePath, 1500);
|
|
} else {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
if (errorData.errors) {
|
|
const message = Object.values(errorData.errors).flat().join('\n');
|
|
setToast({ message: 'Validation Error:\n' + message, type: 'error' });
|
|
} else {
|
|
setToast({ message: 'Error saving investor: ' + (errorData.message || response.statusText), type: 'error' });
|
|
}
|
|
setSaving(false);
|
|
}
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
setToast({ message: 'Failed to save investor.', type: 'error' });
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
if (loading) return <div className="p-10 text-center font-medium text-gray-400">Loading form...</div>;
|
|
|
|
const selectedBranchNames = branches
|
|
.filter(b => formData.branch_ids.includes(b.id))
|
|
.map(b => b.name)
|
|
.join(', ');
|
|
|
|
return (
|
|
<>
|
|
{toast && <Toast message={toast.message} type={toast.type} onClose={() => setToast(null)} />}
|
|
|
|
<main className="p-8 max-w-4xl mx-auto">
|
|
<div className="flex items-center gap-4 mb-8">
|
|
<button onClick={() => window.location.href = basePath} className="p-2 hover:bg-gray-100 rounded-full transition-all">
|
|
<ChevronLeft size={24} className="text-gray-600" />
|
|
</button>
|
|
<h1 className="text-2xl font-bold text-[#101828]">Add New Investor</h1>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-3xl border border-gray-100 shadow-sm p-10">
|
|
<form onSubmit={handleSave} className="space-y-8">
|
|
<div className="grid grid-cols-2 gap-8">
|
|
{/* Investor Name */}
|
|
<div className="space-y-2">
|
|
<label className="text-[13px] font-semibold text-gray-600">Investor Name</label>
|
|
<input
|
|
required
|
|
type="text"
|
|
name="name"
|
|
value={formData.name}
|
|
onChange={(e) => {
|
|
if (e.target.value.length <= 30) handleChange(e);
|
|
else setToast({ message: 'Maximum 30 characters allowed', type: 'error' });
|
|
}}
|
|
maxLength={30}
|
|
className="w-full border border-gray-200 rounded-xl px-4 py-3.5 text-sm focus:ring-2 focus:ring-emerald-500 transition-all outline-none"
|
|
placeholder="Enter Investor Name (Max 30 chars)"
|
|
/>
|
|
</div>
|
|
|
|
{/* Investment Date */}
|
|
<div className="space-y-2">
|
|
<label className="text-[13px] font-semibold text-gray-600">Investment Date</label>
|
|
<input
|
|
required
|
|
type="date"
|
|
name="investment_date"
|
|
value={formData.investment_date}
|
|
onChange={handleChange}
|
|
className="w-full border border-gray-200 rounded-xl px-4 py-3.5 text-sm focus:ring-2 focus:ring-emerald-500 transition-all outline-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* Investment Amount */}
|
|
<div className="space-y-2">
|
|
<label className="text-[13px] font-semibold text-gray-600">Investment Amount (AED)</label>
|
|
<input
|
|
required
|
|
type="number"
|
|
name="investment_amount"
|
|
value={formData.investment_amount}
|
|
onChange={handleChange}
|
|
className="w-full border border-gray-200 rounded-xl px-4 py-3.5 text-sm focus:ring-2 focus:ring-emerald-500 transition-all outline-none"
|
|
placeholder="Enter Amount"
|
|
/>
|
|
</div>
|
|
|
|
{/* Branch Selection Toggle */}
|
|
<div className={`space-y-4 ${isReceptionist ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
<label className="text-[13px] font-semibold text-gray-600">Branch Selection</label>
|
|
<div className="flex items-center justify-between p-4 bg-gray-50/50 rounded-xl border border-gray-100">
|
|
<span className="text-sm text-gray-600 font-medium">Applicable to All Branches</span>
|
|
<button
|
|
type="button"
|
|
disabled={isReceptionist}
|
|
onClick={() => setFormData({...formData, applicable_to_all_branches: !formData.applicable_to_all_branches})}
|
|
className={`w-12 h-6 rounded-full transition-all relative ${formData.applicable_to_all_branches ? 'bg-emerald-500' : 'bg-gray-300'}`}
|
|
>
|
|
<div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-all ${formData.applicable_to_all_branches ? 'left-7' : 'left-1'}`}></div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Specific Branch Selector */}
|
|
{!formData.applicable_to_all_branches && !isReceptionist && (
|
|
<div className="col-span-2 space-y-2 animate-in slide-in-from-top-2 duration-300">
|
|
<label className="text-[13px] font-semibold text-gray-600">Select Specific Branches</label>
|
|
<div className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setBranchDropdownOpen(!branchDropdownOpen)}
|
|
className="w-full border border-gray-200 rounded-xl px-4 py-3.5 text-sm text-left flex items-center justify-between bg-white focus:ring-2 focus:ring-emerald-500 transition-all"
|
|
>
|
|
<span className={selectedBranchNames ? 'text-gray-900' : 'text-gray-400'}>
|
|
{selectedBranchNames || 'Select Specific Branches...'}
|
|
</span>
|
|
<ChevronLeft size={18} className={`text-gray-400 transform transition-transform ${branchDropdownOpen ? 'rotate-90' : '-rotate-90'}`} />
|
|
</button>
|
|
|
|
{branchDropdownOpen && (
|
|
<div className="absolute z-10 w-full mt-2 bg-white border border-gray-100 rounded-xl shadow-xl max-h-60 overflow-y-auto p-2">
|
|
{branches.map(branch => (
|
|
<div
|
|
key={branch.id}
|
|
onClick={() => toggleBranch(branch.id)}
|
|
className="flex items-center justify-between p-3 hover:bg-gray-50 rounded-lg cursor-pointer transition-colors"
|
|
>
|
|
<span className="text-sm font-medium text-gray-700">{branch.name}</span>
|
|
{formData.branch_ids.includes(branch.id) && <Check size={16} className="text-emerald-600" />}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ROI Type */}
|
|
<div className="space-y-2">
|
|
<label className="text-[13px] font-semibold text-gray-600">ROI Type</label>
|
|
<select
|
|
name="roi_type"
|
|
value={formData.roi_type}
|
|
onChange={handleChange}
|
|
className="w-full border border-gray-200 rounded-xl px-4 py-3.5 text-sm focus:ring-2 focus:ring-emerald-500 transition-all outline-none appearance-none bg-white"
|
|
>
|
|
<option value="Percentage">Percentage</option>
|
|
<option value="Fixed Amount">Fixed Amount</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* ROI Value */}
|
|
<div className="space-y-2">
|
|
<label className="text-[13px] font-semibold text-gray-600">
|
|
{formData.roi_type === 'Percentage' ? 'ROI (%)' : 'Fixed Amount (AED)'}
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="roi_value"
|
|
value={formData.roi_value}
|
|
onChange={handleChange}
|
|
className="w-full border border-gray-200 rounded-xl px-4 py-3.5 text-sm focus:ring-2 focus:ring-emerald-500 transition-all outline-none"
|
|
placeholder={formData.roi_type === 'Percentage' ? "e.g. 10" : "e.g. 500"}
|
|
/>
|
|
</div>
|
|
|
|
{/* ROI Period */}
|
|
<div className="space-y-2">
|
|
<label className="text-[13px] font-semibold text-gray-600">ROI Period</label>
|
|
<select
|
|
name="roi_period"
|
|
value={formData.roi_period}
|
|
onChange={handleChange}
|
|
className="w-full border border-gray-200 rounded-xl px-4 py-3.5 text-sm focus:ring-2 focus:ring-emerald-500 transition-all outline-none appearance-none bg-white"
|
|
>
|
|
<option value="Monthly">Monthly</option>
|
|
<option value="Quarterly">Quarterly</option>
|
|
<option value="Yearly">Yearly</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* File Upload */}
|
|
<div className="space-y-2">
|
|
<label className="text-[13px] font-semibold text-gray-600">Security Proof Document</label>
|
|
<div className="border-2 border-dashed border-gray-200 rounded-3xl p-12 flex flex-col items-center justify-center space-y-4 hover:border-emerald-300 transition-all group bg-gray-50/20 relative">
|
|
<input
|
|
type="file"
|
|
className="absolute inset-0 opacity-0 cursor-pointer"
|
|
onChange={handleFileChange}
|
|
/>
|
|
<div className="w-16 h-16 rounded-2xl bg-gray-50 flex items-center justify-center text-gray-400 group-hover:bg-emerald-50 group-hover:text-emerald-500 transition-all">
|
|
<Upload size={32} />
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-sm font-semibold text-gray-900">
|
|
{formData.security_proof_document ? formData.security_proof_document.name : <><span className="text-red-500">Upload a file</span> or drag and drop</>}
|
|
</p>
|
|
<p className="text-xs text-gray-400 mt-1 uppercase tracking-wider font-bold">PDF, PNG, JPG up to 10MB</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center justify-end gap-3 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => window.location.href = basePath}
|
|
className="px-8 py-3.5 text-gray-500 font-bold text-sm hover:bg-gray-100 rounded-xl transition-all"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={saving}
|
|
className="px-10 py-3.5 bg-[#00C853] text-white font-bold text-sm rounded-xl hover:bg-[#00B24A] shadow-lg shadow-emerald-100 transition-all flex items-center gap-2"
|
|
>
|
|
<svg size={18} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-4 h-4"><path d="M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M17 21V13H7V21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M7 3V8H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
<span>{saving ? 'Creating...' : 'Create Investor'}</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|