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:
@@ -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;
|
||||
Reference in New Issue
Block a user