✅ New functionality added (soft delete system) ✅ Backward compatible (existing features unchanged) ✅ Significant enhancement (complete temporal tracking system) ✅ API additions (new endpoints, parameters) ✅ UI enhancements (new components, visual indicators)
658 lines
26 KiB
TypeScript
658 lines
26 KiB
TypeScript
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<AddShoppingEventModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
onEventAdded,
|
|
editEvent
|
|
}) => {
|
|
// Use body scroll lock when modal is open
|
|
useBodyScrollLock(isOpen);
|
|
|
|
const [shops, setShops] = useState<Shop[]>([]);
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
const [shopBrands, setShopBrands] = useState<BrandInShop[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [message, setMessage] = useState('');
|
|
|
|
const isEditMode = Boolean(editEvent);
|
|
|
|
const [formData, setFormData] = useState<ShoppingEventCreate>({
|
|
shop_id: 0,
|
|
date: new Date().toISOString().split('T')[0],
|
|
total_amount: undefined,
|
|
notes: '',
|
|
products: []
|
|
});
|
|
|
|
const [selectedProducts, setSelectedProducts] = useState<ProductInEvent[]>([]);
|
|
const [newProductItem, setNewProductItem] = useState<ProductInEvent>({
|
|
product_id: 0,
|
|
amount: 1,
|
|
price: 0,
|
|
discount: false
|
|
});
|
|
const [autoCalculate, setAutoCalculate] = useState<boolean>(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 (
|
|
<div
|
|
className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"
|
|
onClick={(e) => {
|
|
// Close modal if clicking on backdrop
|
|
if (e.target === e.currentTarget) {
|
|
onClose();
|
|
}
|
|
}}
|
|
>
|
|
<div
|
|
className="relative min-h-screen md:min-h-0 md:top-10 mx-auto p-4 md:p-5 w-full md:max-w-4xl md:shadow-lg md:rounded-md bg-white"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="mt-3">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-lg md:text-xl font-medium text-gray-900">
|
|
{isEditMode ? 'Edit Shopping Event' : 'Add New Shopping Event'}
|
|
</h3>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-gray-400 hover:text-gray-600 p-2"
|
|
>
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{message && (
|
|
<div className={`mb-4 px-4 py-3 rounded ${
|
|
message.includes('Error')
|
|
? 'bg-red-50 border border-red-200 text-red-700'
|
|
: 'bg-green-50 border border-green-200 text-green-700'
|
|
}`}>
|
|
{message}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Shop and Date Selection */}
|
|
<div className="flex flex-col md:flex-row md:space-x-4 space-y-4 md:space-y-0">
|
|
<div className="flex-1">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Shop
|
|
</label>
|
|
<select
|
|
value={formData.shop_id}
|
|
onChange={(e) => setFormData({...formData, shop_id: parseInt(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
|
|
>
|
|
<option value={0}>Select a shop</option>
|
|
{shops.map(shop => (
|
|
<option key={shop.id} value={shop.id}>
|
|
{shop.name} - {shop.city}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="w-full md:w-48">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={formData.date}
|
|
onChange={(e) => 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
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Add Products Section */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Add Products
|
|
</label>
|
|
|
|
{/* Mobile Product Form - Stacked */}
|
|
<div className="md:hidden space-y-4 mb-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Product
|
|
</label>
|
|
<select
|
|
value={newProductItem.product_id}
|
|
onChange={(e) => setNewProductItem({...newProductItem, product_id: parseInt(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"
|
|
>
|
|
<option value={0}>Select a product</option>
|
|
{Object.entries(
|
|
getFilteredProducts().reduce((groups, product) => {
|
|
const category = product.category.name;
|
|
if (!groups[category]) {
|
|
groups[category] = [];
|
|
}
|
|
groups[category].push(product);
|
|
return groups;
|
|
}, {} as Record<string, typeof products>)
|
|
)
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([category, categoryProducts]) => (
|
|
<optgroup key={category} label={category}>
|
|
{categoryProducts
|
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
.map(product => (
|
|
<option key={product.id} value={product.id}>
|
|
{product.name}{product.organic ? ' 🌱' : ''}{product.weight ? ` ${product.weight}${product.weight_unit}` : product.weight_unit}{product.brand ? ` (${product.brand.name})` : ''}
|
|
</option>
|
|
))
|
|
}
|
|
</optgroup>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Amount
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="1"
|
|
min="1"
|
|
placeholder="1"
|
|
value={newProductItem.amount}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Price ($)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
placeholder="0.00"
|
|
value={newProductItem.price}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<label className="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={newProductItem.discount}
|
|
onChange={(e) => setNewProductItem({...newProductItem, discount: e.target.checked})}
|
|
className="h-5 w-5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
/>
|
|
<span className="text-sm font-medium text-gray-700">Discount</span>
|
|
</label>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={addProductToEvent}
|
|
className="px-6 py-3 bg-green-500 hover:bg-green-700 text-white rounded-md font-medium text-base"
|
|
>
|
|
Add Product
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Desktop Product Form - Horizontal */}
|
|
<div className="hidden md:flex space-x-2 mb-4">
|
|
<div className="flex-1">
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
Product
|
|
</label>
|
|
<select
|
|
value={newProductItem.product_id}
|
|
onChange={(e) => setNewProductItem({...newProductItem, product_id: parseInt(e.target.value)})}
|
|
className="w-full h-10 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
<option value={0}>Select a product</option>
|
|
{Object.entries(
|
|
getFilteredProducts().reduce((groups, product) => {
|
|
const category = product.category.name;
|
|
if (!groups[category]) {
|
|
groups[category] = [];
|
|
}
|
|
groups[category].push(product);
|
|
return groups;
|
|
}, {} as Record<string, typeof products>)
|
|
)
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([category, categoryProducts]) => (
|
|
<optgroup key={category} label={category}>
|
|
{categoryProducts
|
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
.map(product => (
|
|
<option key={product.id} value={product.id}>
|
|
{product.name}{product.organic ? ' 🌱' : ''}{product.weight ? ` ${product.weight}${product.weight_unit}` : product.weight_unit}{product.brand ? ` (${product.brand.name})` : ''}
|
|
</option>
|
|
))
|
|
}
|
|
</optgroup>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
Amount
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="1"
|
|
min="1"
|
|
placeholder="1"
|
|
value={newProductItem.amount}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
Price ($)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
placeholder="0.00"
|
|
value={newProductItem.price}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center">
|
|
<label className="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={newProductItem.discount}
|
|
onChange={(e) => setNewProductItem({...newProductItem, discount: e.target.checked})}
|
|
className="h-5 w-5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
/>
|
|
<span className="text-xs font-medium text-gray-700">Discount</span>
|
|
</label>
|
|
</div>
|
|
<div className="flex items-end">
|
|
<button
|
|
type="button"
|
|
onClick={addProductToEvent}
|
|
className="px-6 py-2 bg-green-500 hover:bg-green-700 text-white rounded-md font-medium text-sm"
|
|
>
|
|
Add Product
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Selected Products List */}
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Product
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Amount
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Price ($)
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Discount
|
|
</th>
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Total ($)
|
|
</th>
|
|
<th scope="col" className="relative px-6 py-3">
|
|
<span className="sr-only">Edit</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{selectedProducts.map((product, index) => (
|
|
<tr key={index}>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
{getProductName(product.product_id)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{product.amount}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{product.price.toFixed(2)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{product.discount ? 'Yes' : 'No'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{(product.amount * product.price).toFixed(2)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<button
|
|
type="button"
|
|
onClick={() => editProductFromEvent(index)}
|
|
className="text-indigo-600 hover:text-indigo-900 mr-2"
|
|
>
|
|
Edit
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeProductFromEvent(index)}
|
|
className="text-red-600 hover:text-red-900"
|
|
>
|
|
Remove
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Total Amount and Notes */}
|
|
<div className="flex flex-col md:flex-row md:space-x-4 space-y-4 md:space-y-0">
|
|
<div className="flex-1">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Total Amount ($)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
placeholder="0.00"
|
|
value={formData.total_amount}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</div>
|
|
<div className="flex-1">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Notes
|
|
</label>
|
|
<textarea
|
|
value={formData.notes}
|
|
onChange={(e) => setFormData({...formData, notes: e.target.value})}
|
|
className="w-full h-24 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"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Save Button */}
|
|
<div className="flex justify-end">
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="px-6 py-3 bg-blue-500 hover:bg-blue-700 text-white rounded-md font-medium text-base disabled:opacity-50"
|
|
>
|
|
{loading ? 'Saving...' : 'Save'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AddShoppingEventModal; |