2026-03-14 17:13:13 +05:30

424 lines
23 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { X, Save, Plus, Search, Trash2, Calendar, Building, CreditCard, Tag, ShoppingBag } from 'lucide-react';
export default function AddCollectionModal({ isOpen, onClose, onSave, branches, types }) {
const [formData, setFormData] = useState({
date: new Date().toISOString().split('T')[0],
branch_id: '',
collection_type_id: '',
payment_method: 'Cash',
amount: '',
remarks: '',
items: []
});
const isReceptionist = window.__APP_DATA__?.role === 'receptionist';
const receptionistBranchId = window.__APP_DATA__?.branch?.id;
const [products, setProducts] = useState([]);
const [loadingProducts, setLoadingProducts] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [showProductSearch, setShowProductSearch] = useState(false);
const [paymentMethods, setPaymentMethods] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (isReceptionist && receptionistBranchId) {
setFormData(prev => ({ ...prev, branch_id: receptionistBranchId }));
} else if (branches.length > 0 && !formData.branch_id) {
setFormData(prev => ({ ...prev, branch_id: branches[0].id }));
}
}, [branches, isReceptionist, receptionistBranchId]);
useEffect(() => {
if (isOpen) {
fetchPaymentMethods();
if (formData.branch_id) {
fetchProducts();
}
}
}, [isOpen, formData.branch_id]);
const fetchPaymentMethods = async () => {
try {
const res = await fetch('/api/masters/payment_method');
if (res.ok) {
const data = await res.json();
setPaymentMethods(data.filter(m => m.status === 'Active'));
// Set default if current is not in active methods or if it's the default 'Cash'
if (data.length > 0 && !data.find(m => m.name === formData.payment_method)) {
setFormData(prev => ({ ...prev, payment_method: data[0].name }));
}
}
} catch (error) {
console.error('Error fetching payment methods:', error);
}
};
const fetchProducts = async () => {
setLoadingProducts(true);
try {
const res = await fetch(`/api/inventory/products?branch_id=${formData.branch_id}`);
if (res.ok) {
setProducts(await res.json());
}
} catch (error) {
console.error('Error fetching products:', error);
} finally {
setLoadingProducts(false);
}
};
if (!isOpen) return null;
const selectedType = types.find(t => t.id.toString() === formData.collection_type_id.toString());
const isProductSale = selectedType && (
selectedType.name.toLowerCase().includes('product sale') ||
selectedType.name.toLowerCase().includes('product saled')
);
const addItem = (product) => {
if (product.current_stock <= 0) {
alert('This product is out of stock in the selected branch.');
return;
}
const existing = formData.items.find(i => i.product_id === product.id);
let newItems;
if (existing) {
if (existing.quantity + 1 > product.current_stock) {
alert(`Only ${product.current_stock} units available in stock.`);
return;
}
newItems = formData.items.map(i =>
i.product_id === product.id ? { ...i, quantity: i.quantity + 1 } : i
);
} else {
newItems = [...formData.items, {
product_id: product.id,
name: product.name,
unit_price: product.selling_price,
quantity: 1,
max_stock: product.current_stock // Store for easy validation
}];
}
const newAmount = newItems.reduce((sum, i) => sum + (i.quantity * i.unit_price), 0);
setFormData({ ...formData, items: newItems, amount: newAmount.toString() });
setShowProductSearch(false);
};
const updateItemQty = (productId, delta) => {
const item = formData.items.find(i => i.product_id === productId);
if (delta > 0 && item.quantity + delta > item.max_stock) {
alert(`Only ${item.max_stock} units available in stock.`);
return;
}
const newItems = formData.items.map(i => {
if (i.product_id === productId) {
return { ...i, quantity: Math.max(1, i.quantity + delta) };
}
return i;
});
const newAmount = newItems.reduce((sum, i) => sum + (i.quantity * i.unit_price), 0);
setFormData({ ...formData, items: newItems, amount: newAmount.toString() });
};
const removeItem = (productId) => {
const newItems = formData.items.filter(i => i.product_id !== productId);
const newAmount = newItems.reduce((sum, i) => sum + (i.quantity * i.unit_price), 0);
setFormData({ ...formData, items: newItems, amount: newAmount.toString() });
};
const handleSubmit = async (e) => {
if (e) e.preventDefault();
if (isProductSale && formData.items.length === 0) {
alert('Please add at least one product for a Product Sale entry.');
return;
}
if (!formData.amount || parseFloat(formData.amount) <= 0) {
alert('Please enter a valid amount.');
return;
}
setLoading(true);
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const res = await fetch('/api/collections', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify(formData)
});
if (res.ok) {
const newCollection = await res.json();
onSave(newCollection);
setFormData({
date: new Date().toISOString().split('T')[0],
branch_id: window.__APP_DATA__?.role === 'receptionist' ? window.__APP_DATA__?.branch?.id : (branches[0]?.id || ''),
collection_type_id: '',
payment_method: 'Cash',
amount: '',
remarks: '',
items: []
});
onClose();
}
} catch (error) {
console.error('Error adding collection:', error);
} finally {
setLoading(false);
}
};
const filteredProducts = products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="fixed inset-0 bg-[#00171F]/40 backdrop-blur-sm z-[999] flex items-center justify-center p-4">
<div className="bg-white rounded-[32px] w-full max-w-2xl overflow-hidden shadow-2xl animate-in zoom-in-95 duration-300 flex flex-col max-h-[90vh]">
<div className="p-8 border-b border-gray-100 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-red-50 rounded-2xl flex items-center justify-center text-red-600">
<Plus size={24} />
</div>
<div>
<h2 className="text-2xl font-black text-gray-900 tracking-tight">New Collection Entry</h2>
<p className="text-xs text-gray-500 font-bold uppercase tracking-widest mt-0.5">Record incoming revenue.</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-xl transition-colors">
<X size={24} className="text-gray-400" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-8 space-y-6 overflow-y-auto">
<div className="grid grid-cols-2 gap-6">
<div>
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Date</label>
<input
required type="date"
className="w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900"
value={formData.date}
onChange={e => setFormData({...formData, date: e.target.value})}
/>
</div>
{!isReceptionist ? (
<div>
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Branch</label>
<select
required
className="w-full max-w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none truncate"
value={formData.branch_id}
onChange={e => setFormData({...formData, branch_id: e.target.value, items: [], amount: ''})}
>
<option value="">Select Branch</option>
{branches.map(b => (
<option key={b.id} value={b.id}>
{b.name.length > 40 ? b.name.substring(0, 40) + '...' : b.name}
</option>
))}
</select>
</div>
) : (
<div className="opacity-60 cursor-not-allowed">
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Branch</label>
<div className="w-full px-6 py-4 bg-gray-100 border border-gray-100 rounded-2xl font-bold text-gray-500">
{window.__APP_DATA__?.branch?.name || 'Assigned Branch'}
</div>
</div>
)}
<div>
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Collection Type</label>
<select
required
className="w-full max-w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none truncate"
value={formData.collection_type_id}
onChange={e => {
const typeId = e.target.value;
const type = types.find(t => t.id.toString() === typeId.toString());
const isProd = type && (
type.name.toLowerCase().includes('product sale') ||
type.name.toLowerCase().includes('product saled')
);
setFormData({...formData, collection_type_id: typeId, items: [], amount: ''});
if (isProd) setShowProductSearch(true);
}}
>
<option value="">Select Type</option>
{types.map(t => (
<option key={t.id} value={t.id}>
{t.name.length > 40 ? t.name.substring(0, 40) + '...' : t.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Payment Method</label>
<select
required
className="w-full max-w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 appearance-none truncate"
value={formData.payment_method}
onChange={e => setFormData({...formData, payment_method: e.target.value})}
>
{paymentMethods.map(m => (
<option key={m.id} value={m.name}>{m.name}</option>
))}
{paymentMethods.length === 0 && (
<>
<option value="Cash">Cash</option>
<option value="Card">Card</option>
<option value="Online">Online</option>
</>
)}
</select>
</div>
{isProductSale ? (
<div className="col-span-2 space-y-4">
<div className="p-6 bg-gray-50/50 border border-gray-100 rounded-[24px]">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<ShoppingBag size={16} className="text-gray-400" />
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Products</span>
</div>
<button
type="button"
onClick={() => setShowProductSearch(true)}
className="text-[10px] font-black text-red-500 uppercase tracking-widest hover:text-red-600 flex items-center gap-1"
>
<Plus size={14} /> Add Product
</button>
</div>
<div className="space-y-3">
{formData.items.map(item => (
<div key={item.product_id} className="flex items-center justify-between bg-white p-3 rounded-xl border border-gray-50">
<div className="flex-1">
<p className="text-sm font-bold text-gray-900">{item.name}</p>
<p className="text-[10px] text-gray-400 font-bold">{item.unit_price} AED x {item.quantity}</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 bg-gray-50 rounded-lg p-1">
<button type="button" onClick={() => updateItemQty(item.product_id, -1)} className="p-1 hover:bg-white rounded shadow-sm text-gray-400">
{item.quantity <= 1 ? <Trash2 size={12} className="text-red-400" /> : <div className="w-3 h-3 flex items-center justify-center font-bold">-</div>}
</button>
<span className="text-xs font-black w-4 text-center">{item.quantity}</span>
<button type="button" onClick={() => updateItemQty(item.product_id, 1)} className="p-1 hover:bg-white rounded shadow-sm text-gray-400">
<Plus size={12} />
</button>
</div>
<button type="button" onClick={() => removeItem(item.product_id)} className="text-gray-300 hover:text-red-500"><Trash2 size={16} /></button>
</div>
</div>
))}
{formData.items.length === 0 && (
<p className="text-center py-6 text-xs text-gray-400 font-bold italic">No products added yet.</p>
)}
</div>
</div>
</div>
) : (
<div className="col-span-2 text-red-500">
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Amount (AED) </label>
<input
required type="number" step="0.01"
className="w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900"
placeholder="0.00"
value={formData.amount}
onChange={e => setFormData({...formData, amount: e.target.value})}
/>
</div>
)}
<div className="col-span-2">
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 ml-1">Remarks</label>
<textarea
className="w-full px-6 py-4 bg-gray-50/50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900 min-h-[100px]"
placeholder="Details about this collection..."
value={formData.remarks}
onChange={e => setFormData({...formData, remarks: e.target.value})}
/>
</div>
</div>
<div className="pt-6 border-t border-gray-100 flex gap-4">
<button
type="button"
onClick={onClose}
className="flex-1 py-4 bg-gray-50 text-gray-400 rounded-2xl font-black text-xs uppercase tracking-widest hover:bg-gray-100 transition-all"
>
Cancel
</button>
<button
disabled={loading || !formData.amount || parseFloat(formData.amount) <= 0}
type="submit"
className="flex-1 py-4 bg-[#10B981] text-white rounded-2xl font-black text-xs uppercase tracking-widest hover:bg-[#059669] transition-all shadow-lg shadow-emerald-100 disabled:opacity-50"
>
{loading ? 'Saving...' : 'Save Entry'}
</button>
</div>
</form>
{/* Product Search Overlay */}
{showProductSearch && (
<div className="absolute inset-0 bg-white/95 backdrop-blur-md z-[1000] p-8 flex flex-col animate-in slide-in-from-bottom-4 duration-300">
<div className="flex items-center justify-between mb-8">
<h3 className="text-xl font-black text-gray-900">Select Product</h3>
<button onClick={() => setShowProductSearch(false)} className="p-2 hover:bg-gray-100 rounded-xl transition-colors text-gray-400">
<X size={24} />
</button>
</div>
<div className="relative mb-6">
<Search className="absolute left-6 top-1/2 -translate-y-1/2 text-gray-400" size={20} />
<input
autoFocus
className="w-full pl-16 pr-6 py-4 bg-gray-50 border border-gray-100 rounded-2xl outline-none focus:ring-2 focus:ring-red-500/10 focus:border-red-500 transition-all font-bold text-gray-900"
placeholder="Search products..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<div className="flex-1 overflow-y-auto space-y-2">
{filteredProducts.map(p => (
<button
key={p.id}
type="button"
onClick={() => addItem(p)}
className="w-full p-4 rounded-2xl border border-gray-50 hover:border-red-100 hover:bg-red-50/30 transition-all text-left flex items-center justify-between group"
>
<div>
<p className="font-bold text-gray-900">{p.name}</p>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest">{p.category?.name} Stock: {p.current_stock}</p>
</div>
<div className="text-right">
<p className="font-black text-red-500">{p.selling_price} AED</p>
<p className="text-[10px] text-green-500 font-bold group-hover:block hidden">Click to add</p>
</div>
</button>
))}
{filteredProducts.length === 0 && (
<div className="text-center py-20 opacity-20">
<Search size={48} className="mx-auto mb-4" />
<p className="font-black uppercase tracking-widest">No products found</p>
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}