redesing product lists
This commit is contained in:
@@ -35,7 +35,8 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
const [newProductItem, setNewProductItem] = useState<ProductInEvent>({
|
||||
product_id: 0,
|
||||
amount: 1,
|
||||
price: 0
|
||||
price: 0,
|
||||
discount: false
|
||||
});
|
||||
const [autoCalculate, setAutoCalculate] = useState<boolean>(true);
|
||||
|
||||
@@ -58,7 +59,8 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
const mappedProducts = editEvent.products.map(p => ({
|
||||
product_id: p.id,
|
||||
amount: p.amount,
|
||||
price: p.price
|
||||
price: p.price,
|
||||
discount: p.discount
|
||||
}));
|
||||
|
||||
// Calculate the sum of all products
|
||||
@@ -220,7 +222,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
const addProductToEvent = () => {
|
||||
if (newProductItem.product_id > 0 && newProductItem.amount > 0 && newProductItem.price >= 0) {
|
||||
setSelectedProducts([...selectedProducts, { ...newProductItem }]);
|
||||
setNewProductItem({ product_id: 0, amount: 1, price: 0 });
|
||||
setNewProductItem({ product_id: 0, amount: 1, price: 0, discount: false });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -234,7 +236,8 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
setNewProductItem({
|
||||
product_id: productToEdit.product_id,
|
||||
amount: productToEdit.amount,
|
||||
price: productToEdit.price
|
||||
price: productToEdit.price,
|
||||
discount: productToEdit.discount
|
||||
});
|
||||
// Remove the item from the selected list
|
||||
setSelectedProducts(selectedProducts.filter((_, i) => i !== index));
|
||||
@@ -246,7 +249,8 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
|
||||
const weightInfo = product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit;
|
||||
const organicEmoji = product.organic ? ' 🌱' : '';
|
||||
return `${product.name}${organicEmoji} ${weightInfo}`;
|
||||
const brandInfo = product.brand ? ` (${product.brand.name})` : '';
|
||||
return `${product.name}${organicEmoji} ${weightInfo}${brandInfo}`;
|
||||
};
|
||||
|
||||
// Filter products based on selected shop's brands
|
||||
@@ -297,38 +301,38 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Shop Selection */}
|
||||
<div>
|
||||
<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 border border-gray-300 rounded-md px-3 py-2 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>
|
||||
|
||||
{/* Date */}
|
||||
<div>
|
||||
<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 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
{/* Shop and Date Selection */}
|
||||
<div className="flex space-x-4">
|
||||
<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-10 border border-gray-300 rounded-md px-3 py-2 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-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-10 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Products Section */}
|
||||
@@ -344,7 +348,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
<select
|
||||
value={newProductItem.product_id}
|
||||
onChange={(e) => setNewProductItem({...newProductItem, product_id: parseInt(e.target.value)})}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
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(
|
||||
@@ -364,7 +368,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(product => (
|
||||
<option key={product.id} value={product.id}>
|
||||
{product.name}{product.organic ? '🌱' : ''}{product.brand ? ` (${product.brand.name})` : ''} {product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit}
|
||||
{product.name}{product.organic ? ' 🌱' : ''}{product.weight ? ` ${product.weight}${product.weight_unit}` : product.weight_unit}{product.brand ? ` (${product.brand.name})` : ''}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
@@ -391,7 +395,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
placeholder="1"
|
||||
value={newProductItem.amount}
|
||||
onChange={(e) => setNewProductItem({...newProductItem, amount: parseFloat(e.target.value)})}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
@@ -405,14 +409,30 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
placeholder="0.00"
|
||||
value={newProductItem.price}
|
||||
onChange={(e) => setNewProductItem({...newProductItem, price: parseFloat(e.target.value)})}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<div className="w-20">
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1 text-center">
|
||||
Discount
|
||||
</label>
|
||||
<div className="h-10 flex items-center justify-center border border-gray-300 rounded-md bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newProductItem.discount}
|
||||
onChange={(e) => setNewProductItem({...newProductItem, discount: e.target.checked})}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-16">
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1 opacity-0">
|
||||
Action
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addProductToEvent}
|
||||
className="bg-green-500 hover:bg-green-700 text-white px-4 py-2 rounded-md"
|
||||
className="w-full h-10 bg-green-500 hover:bg-green-700 text-white px-3 py-2 rounded-md font-medium"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
@@ -423,32 +443,52 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
{selectedProducts.length > 0 && (
|
||||
<div className="bg-gray-50 rounded-md p-4 max-h-40 overflow-y-auto">
|
||||
<h4 className="font-medium text-gray-700 mb-2">Selected Items:</h4>
|
||||
{selectedProducts.map((item, index) => (
|
||||
<div key={index} className="flex justify-between items-center py-2 border-b last:border-b-0">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-gray-900">
|
||||
{getProductName(item.product_id)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">
|
||||
{item.amount} × ${item.price.toFixed(2)} = ${(item.amount * item.price).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => editProductFromEvent(index)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeProductFromEvent(index)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
{Object.entries(
|
||||
selectedProducts.reduce((groups, item, index) => {
|
||||
const product = products.find(p => p.id === item.product_id);
|
||||
const category = product?.category.name || 'Unknown';
|
||||
if (!groups[category]) {
|
||||
groups[category] = [];
|
||||
}
|
||||
groups[category].push({ ...item, index });
|
||||
return groups;
|
||||
}, {} as Record<string, (ProductInEvent & { index: number })[]>)
|
||||
)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([category, categoryItems]) => (
|
||||
<div key={category} className="mb-3 last:mb-0">
|
||||
<div className="text-xs font-semibold text-gray-600 uppercase tracking-wide mb-1 border-b border-gray-300 pb-1">
|
||||
{category}
|
||||
</div>
|
||||
{categoryItems.map((item) => (
|
||||
<div key={item.index} className="flex justify-between items-center py-2 pl-2">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-gray-900">
|
||||
{getProductName(item.product_id)}
|
||||
<span className="text-xs text-gray-600 ml-2">
|
||||
{item.amount} × ${item.price.toFixed(2)} = ${(item.amount * item.price).toFixed(2)}
|
||||
{item.discount && <span className="ml-2 text-green-600 font-medium">🏷️</span>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => editProductFromEvent(item.index)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeProductFromEvent(item.index)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -431,34 +431,33 @@ const ShoppingEventList: React.FC = () => {
|
||||
onMouseEnter={() => setShowItemsPopup(true)}
|
||||
onMouseLeave={handleItemsLeave}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{hoveredEvent.products.map((product, index) => (
|
||||
<div key={index} className="border-b border-gray-100 pb-2 last:border-b-0">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{product.name} {product.organic ? '🌱' : ''}
|
||||
<div className="space-y-3">
|
||||
{Object.entries(
|
||||
hoveredEvent.products.reduce((groups, product) => {
|
||||
const category = product.category?.name || 'Unknown';
|
||||
if (!groups[category]) {
|
||||
groups[category] = [];
|
||||
}
|
||||
groups[category].push(product);
|
||||
return groups;
|
||||
}, {} as Record<string, typeof hoveredEvent.products>)
|
||||
)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([category, categoryProducts]) => (
|
||||
<div key={category} className="mb-3 last:mb-0">
|
||||
<div className="text-xs font-semibold text-gray-600 uppercase tracking-wide mb-1 border-b border-gray-300 pb-1">
|
||||
{category}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{categoryProducts.map((product, index) => (
|
||||
<div key={index} className="text-sm text-gray-900 pl-2">
|
||||
{product.name} {product.organic ? '🌱' : ''} {product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit}
|
||||
<span className="text-xs text-gray-600 ml-2">
|
||||
{product.amount} × ${product.price.toFixed(2)} = ${(product.amount * product.price).toFixed(2)}
|
||||
{product.discount && <span className="ml-1 text-green-600">🏷️</span>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">
|
||||
{product.category?.name || 'Unknown category'}
|
||||
</div>
|
||||
{product.brand && (
|
||||
<div className="text-xs text-gray-500">
|
||||
Brand: {product.brand.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right ml-2">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
${product.price.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Qty: {product.amount}
|
||||
</div>
|
||||
<div className="text-xs font-medium text-green-600">
|
||||
${(product.amount * product.price).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface ProductInEvent {
|
||||
product_id: number;
|
||||
amount: number;
|
||||
price: number;
|
||||
discount: boolean;
|
||||
}
|
||||
|
||||
export interface ProductWithEventData {
|
||||
@@ -74,6 +75,7 @@ export interface ProductWithEventData {
|
||||
weight_unit: string;
|
||||
amount: number;
|
||||
price: number;
|
||||
discount: boolean;
|
||||
}
|
||||
|
||||
export interface ShoppingEvent {
|
||||
|
||||
Reference in New Issue
Block a user