Minor version bump (1.x.0) is appropriate because:

 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)
This commit is contained in:
2025-05-30 09:49:26 +02:00
parent 56c3c16f6d
commit 0b42a74fe9
16 changed files with 1438 additions and 237 deletions

View File

@@ -190,7 +190,11 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
const fetchProducts = async () => {
try {
const response = await productApi.getAll();
// 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);
@@ -223,6 +227,13 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
}
}, [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 }]);
@@ -483,7 +494,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
))}
</select>
</div>
<div className="w-24">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Amount
</label>
@@ -494,10 +505,10 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
placeholder="1"
value={newProductItem.amount}
onChange={(e) => setNewProductItem({...newProductItem, amount: parseFloat(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"
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="w-24">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Price ($)
</label>
@@ -508,165 +519,133 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
placeholder="0.00"
value={newProductItem.price}
onChange={(e) => setNewProductItem({...newProductItem, price: parseFloat(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"
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="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">
<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-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
className="h-5 w-5 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
<span className="text-xs font-medium text-gray-700">Discount</span>
</label>
</div>
<div className="flex items-end">
<button
type="button"
onClick={addProductToEvent}
className="w-full h-10 bg-green-500 hover:bg-green-700 text-white px-3 py-2 rounded-md font-medium"
className="px-6 py-2 bg-green-500 hover:bg-green-700 text-white rounded-md font-medium text-sm"
>
Add
Add Product
</button>
</div>
</div>
{formData.shop_id > 0 && (
<p className="text-xs text-gray-500 mb-4">
{shopBrands.length === 0
? `Showing all ${products.length} products (no brand restrictions for this shop)`
: `Showing ${getFilteredProducts().length} of ${products.length} products (filtered by shop's available brands)`
}
</p>
)}
{/* Selected Products List */}
{selectedProducts.length > 0 && (
<div className="bg-gray-50 rounded-md p-4 max-h-40 md:max-h-48 overflow-y-auto">
<h4 className="font-medium text-gray-700 mb-2">Selected Items:</h4>
{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 flex-col md:flex-row md:justify-between md:items-center py-2 pl-2 space-y-2 md:space-y-0">
<div className="flex-1 min-w-0">
<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)}
{item.discount && <span className="ml-2 text-green-600 font-medium">🏷</span>}
</div>
</div>
<div className="flex space-x-2 md:flex-shrink-0">
<button
type="button"
onClick={() => editProductFromEvent(item.index)}
className="flex-1 md:flex-none px-3 py-1 text-blue-500 hover:text-blue-700 border border-blue-300 hover:bg-blue-50 rounded text-sm"
>
Edit
</button>
<button
type="button"
onClick={() => removeProductFromEvent(item.index)}
className="flex-1 md:flex-none px-3 py-1 text-red-500 hover:text-red-700 border border-red-300 hover:bg-red-50 rounded text-sm"
>
Remove
</button>
</div>
</div>
))}
</div>
))}
</div>
)}
<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 */}
<div>
<div className="flex items-center space-x-2 mb-2">
<label className="block text-sm font-medium text-gray-700">
{/* 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>
<label className="flex items-center space-x-1">
<input
type="checkbox"
checked={autoCalculate}
onChange={(e) => setAutoCalculate(e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<span className="text-xs text-gray-600">Auto-calculate</span>
</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>
<input
type="number"
step="0.01"
min="0"
placeholder="0.00"
value={formData.total_amount || ''}
onChange={(e) => setFormData({...formData, total_amount: e.target.value ? parseFloat(e.target.value) : undefined})}
disabled={autoCalculate}
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 ${
autoCalculate ? 'bg-gray-100 cursor-not-allowed' : ''
}`}
/>
{autoCalculate && selectedProducts.length > 0 && (
<p className="text-xs text-gray-500 mt-1">
Automatically calculated from selected products: ${calculateTotal(selectedProducts).toFixed(2)}
</p>
)}
</div>
{/* Notes */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Notes (Optional)
</label>
<textarea
value={formData.notes}
onChange={(e) => setFormData({...formData, notes: e.target.value})}
rows={3}
className="w-full 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"
placeholder="Add any notes about this shopping event..."
/>
</div>
{/* Submit Button */}
<div className="flex flex-col md:flex-row md:justify-end space-y-3 md:space-y-0 md:space-x-3 pt-4">
<button
type="button"
onClick={onClose}
className="w-full md:w-auto px-6 py-3 md:py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 font-medium text-base md:text-sm"
>
Cancel
</button>
{/* Save Button */}
<div className="flex justify-end">
<button
type="submit"
disabled={loading || formData.shop_id === 0 || selectedProducts.length === 0}
className="w-full md:w-auto px-6 py-3 md:py-2 bg-blue-500 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-md font-medium text-base md:text-sm"
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...' : (isEditMode ? 'Update Event' : 'Create Event')}
{loading ? 'Saving...' : 'Save'}
</button>
</div>
</form>
@@ -676,4 +655,4 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
);
};
export default AddShoppingEventModal;
export default AddShoppingEventModal;