import React, { useState, useEffect, useCallback } from 'react'; import { Shop, Product, ShoppingEventCreate, ProductInEvent, ShoppingEvent, BrandInShop } from '../types'; import { shopApi, productApi, shoppingEventApi, brandInShopApi } from '../services/api'; import { useBodyScrollLock } from '../hooks/useBodyScrollLock'; interface AddShoppingEventModalProps { isOpen: boolean; onClose: () => void; onEventAdded: () => void; editEvent?: ShoppingEvent | null; } const AddShoppingEventModal: React.FC = ({ isOpen, onClose, onEventAdded, editEvent }) => { // Use body scroll lock when modal is open useBodyScrollLock(isOpen); const [shops, setShops] = useState([]); const [products, setProducts] = useState([]); const [shopBrands, setShopBrands] = useState([]); const [loading, setLoading] = useState(false); const [message, setMessage] = useState(''); const isEditMode = Boolean(editEvent); const [formData, setFormData] = useState({ shop_id: 0, date: new Date().toISOString().split('T')[0], total_amount: undefined, notes: '', products: [] }); const [selectedProducts, setSelectedProducts] = useState([]); const [newProductItem, setNewProductItem] = useState({ product_id: 0, amount: 1, price: 0, discount: false }); const [autoCalculate, setAutoCalculate] = useState(true); // Calculate total amount from selected products const calculateTotal = (products: ProductInEvent[]): number => { const total = products.reduce((total, item) => total + (item.amount * item.price), 0); return Math.round(total * 100) / 100; // Round to 2 decimal places to avoid floating-point errors }; const loadEventData = useCallback(() => { if (editEvent) { // Use the date directly if it's already in YYYY-MM-DD format, otherwise format it let formattedDate = editEvent.date; if (editEvent.date.includes('T') || editEvent.date.length > 10) { // If the date includes time or is longer than YYYY-MM-DD, extract just the date part formattedDate = editEvent.date.split('T')[0]; } // Map products to the format we need const mappedProducts = editEvent.products.map(p => ({ product_id: p.id, amount: p.amount, price: p.price, discount: p.discount })); // Calculate the sum of all products const calculatedTotal = calculateTotal(mappedProducts); // Check if existing total matches calculated total (with small tolerance for floating point) const existingTotal = editEvent.total_amount || 0; const totalMatches = Math.abs(existingTotal - calculatedTotal) < 0.01; setFormData({ shop_id: editEvent.shop.id, date: formattedDate, total_amount: editEvent.total_amount, notes: editEvent.notes || '', products: [] }); setSelectedProducts(mappedProducts); setAutoCalculate(totalMatches); // Enable auto-calc if totals match, disable if they don't } else { // Reset form for adding new event setFormData({ shop_id: 0, date: new Date().toISOString().split('T')[0], total_amount: undefined, notes: '', products: [] }); setSelectedProducts([]); setAutoCalculate(true); } setMessage(''); }, [editEvent]); useEffect(() => { if (isOpen) { fetchShops(); fetchProducts(); loadEventData(); } }, [isOpen, loadEventData]); const handleSubmit = useCallback(async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setMessage(''); try { const eventData = { ...formData, products: selectedProducts }; if (isEditMode && editEvent) { await shoppingEventApi.update(editEvent.id, eventData); setMessage('Shopping event updated successfully!'); } else { await shoppingEventApi.create(eventData); setMessage('Shopping event created successfully!'); } setTimeout(() => { onEventAdded(); onClose(); }, 1500); } catch (error) { console.error('Error saving shopping event:', error); setMessage(`Error ${isEditMode ? 'updating' : 'creating'} shopping event. Please try again.`); setTimeout(() => setMessage(''), 3000); } finally { setLoading(false); } }, [formData, selectedProducts, isEditMode, editEvent, onEventAdded, onClose]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (!isOpen) return; if (event.key === 'Escape') { onClose(); } else if (event.key === 'Enter' && !event.shiftKey && !loading) { // Only trigger submit if not in a textarea and form is valid const target = event.target as HTMLElement; if (target.tagName !== 'TEXTAREA') { event.preventDefault(); if (formData.shop_id > 0 && selectedProducts.length > 0) { handleSubmit(event as any); } } } }; if (isOpen) { document.addEventListener('keydown', handleKeyDown); } return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [isOpen, formData, selectedProducts, loading, onClose, handleSubmit]); // Update total amount whenever selectedProducts changes useEffect(() => { if (autoCalculate) { const calculatedTotal = calculateTotal(selectedProducts); setFormData(prev => ({ ...prev, total_amount: calculatedTotal > 0 ? calculatedTotal : undefined })); } }, [selectedProducts, autoCalculate]); const fetchShops = async () => { try { const response = await shopApi.getAll(); setShops(response.data); } catch (error) { console.error('Error fetching shops:', error); setMessage('Error loading shops. Please try again.'); setTimeout(() => setMessage(''), 3000); } }; const fetchProducts = async () => { try { // If we have a shopping date, get products available for that date // Otherwise, get all non-deleted products const response = formData.date ? await productApi.getAvailableForShopping(formData.date) : await productApi.getAll(false); // false = don't show deleted setProducts(response.data); } catch (error) { console.error('Error fetching products:', error); setMessage('Error loading products. Please try again.'); setTimeout(() => setMessage(''), 3000); } }; const fetchShopBrands = async (shopId: number) => { if (shopId === 0) { setShopBrands([]); return; } try { const response = await brandInShopApi.getByShop(shopId); setShopBrands(response.data); } catch (error) { console.error('Error fetching shop brands:', error); setShopBrands([]); } }; // Effect to load shop brands when shop selection changes useEffect(() => { if (formData.shop_id > 0) { fetchShopBrands(formData.shop_id); } else { setShopBrands([]); } }, [formData.shop_id]); // Effect to refetch products when shopping date changes useEffect(() => { if (isOpen && formData.date) { fetchProducts(); } }, [formData.date, isOpen]); const addProductToEvent = () => { if (newProductItem.product_id > 0 && newProductItem.amount > 0 && newProductItem.price >= 0) { setSelectedProducts([...selectedProducts, { ...newProductItem }]); setNewProductItem({ product_id: 0, amount: 1, price: 0, discount: false }); } }; const removeProductFromEvent = (index: number) => { setSelectedProducts(selectedProducts.filter((_, i) => i !== index)); }; const editProductFromEvent = (index: number) => { const productToEdit = selectedProducts[index]; // Load the product data into the input fields setNewProductItem({ product_id: productToEdit.product_id, amount: productToEdit.amount, price: productToEdit.price, discount: productToEdit.discount }); // Remove the item from the selected list setSelectedProducts(selectedProducts.filter((_, i) => i !== index)); }; const getProductName = (id: number) => { const product = products.find(p => p.id === id); if (!product) return 'Unknown'; const weightInfo = product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit; const organicEmoji = product.organic ? ' 🌱' : ''; const brandInfo = product.brand ? ` (${product.brand.name})` : ''; return `${product.name}${organicEmoji} ${weightInfo}${brandInfo}`; }; // Filter products based on selected shop's brands const getFilteredProducts = () => { // If no shop is selected or shop has no brands, show all products if (formData.shop_id === 0 || shopBrands.length === 0) { return products; } // Get brand IDs available in the selected shop const availableBrandIds = shopBrands.map(sb => sb.brand_id); // Filter products to only show those with brands available in the shop // Also include products without brands (brand_id is null/undefined) return products.filter(product => !product.brand_id || availableBrandIds.includes(product.brand_id) ); }; if (!isOpen) return null; return (
{ // Close modal if clicking on backdrop if (e.target === e.currentTarget) { onClose(); } }} >
e.stopPropagation()} >

{isEditMode ? 'Edit Shopping Event' : 'Add New Shopping Event'}

{message && (
{message}
)}
{/* Shop and Date Selection */}
setFormData({...formData, date: e.target.value})} className="w-full h-12 md:h-10 border border-gray-300 rounded-md px-3 py-2 text-base md:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" required />
{/* Add Products Section */}
{/* Mobile Product Form - Stacked */}
setNewProductItem({...newProductItem, amount: parseFloat(e.target.value)})} className="w-full h-12 border border-gray-300 rounded-md px-3 py-2 text-base focus:outline-none focus:ring-2 focus:ring-blue-500" />
setNewProductItem({...newProductItem, price: parseFloat(e.target.value)})} className="w-full h-12 border border-gray-300 rounded-md px-3 py-2 text-base focus:outline-none focus:ring-2 focus:ring-blue-500" />
{/* Desktop Product Form - Horizontal */}
setNewProductItem({...newProductItem, amount: parseFloat(e.target.value)})} className="w-24 h-10 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
setNewProductItem({...newProductItem, price: parseFloat(e.target.value)})} className="w-24 h-10 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" />
{/* Selected Products List */}
{selectedProducts.map((product, index) => ( ))}
Product Amount Price ($) Discount Total ($) Edit
{getProductName(product.product_id)} {product.amount} {product.price.toFixed(2)} {product.discount ? 'Yes' : 'No'} {(product.amount * product.price).toFixed(2)}
{/* Total Amount and Notes */}
setFormData({...formData, total_amount: parseFloat(e.target.value)})} className="w-full h-12 md:h-10 border border-gray-300 rounded-md px-3 py-2 text-base md:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />