add mobile support
This commit is contained in:
@@ -274,15 +274,15 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div className="relative top-10 mx-auto p-5 border w-full max-w-4xl shadow-lg rounded-md bg-white">
|
||||
<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">
|
||||
<div className="mt-3">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
<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"
|
||||
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" />
|
||||
@@ -302,7 +302,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Shop and Date Selection */}
|
||||
<div className="flex space-x-4">
|
||||
<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
|
||||
@@ -310,7 +310,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
<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"
|
||||
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>
|
||||
@@ -321,7 +321,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-48">
|
||||
<div className="md:w-48">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Date
|
||||
</label>
|
||||
@@ -329,7 +329,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
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"
|
||||
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>
|
||||
@@ -340,7 +340,99 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Add Products
|
||||
</label>
|
||||
<div className="flex space-x-2 mb-4">
|
||||
|
||||
{/* 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
|
||||
@@ -375,14 +467,6 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
{formData.shop_id > 0 && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
@@ -439,9 +523,18 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
</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 overflow-y-auto">
|
||||
<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) => {
|
||||
@@ -461,28 +554,28 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
{category}
|
||||
</div>
|
||||
{categoryItems.map((item) => (
|
||||
<div key={item.index} className="flex justify-between items-center py-2 pl-2">
|
||||
<div className="flex-1">
|
||||
<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)}
|
||||
<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 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">
|
||||
<div className="flex space-x-2 md:flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => editProductFromEvent(item.index)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
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="text-red-500 hover:text-red-700"
|
||||
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>
|
||||
@@ -497,24 +590,19 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
|
||||
{/* Total Amount */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Total Amount
|
||||
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>
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoCalculate}
|
||||
onChange={(e) => setAutoCalculate(e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
<span className={`text-xs px-2 py-1 rounded ${autoCalculate ? 'text-green-600 bg-green-50' : 'text-gray-500 bg-gray-100'}`}>
|
||||
Auto-calculated
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
@@ -522,52 +610,48 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
value={formData.total_amount || ''}
|
||||
onChange={(e) => {
|
||||
setFormData({...formData, total_amount: e.target.value ? parseFloat(e.target.value) : undefined});
|
||||
setAutoCalculate(false); // Disable auto-calculation when manually editing
|
||||
}}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-right"
|
||||
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' : ''
|
||||
}`}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{autoCalculate
|
||||
? "This field is automatically calculated from your selected items. You can manually edit it if needed."
|
||||
: "Auto-calculation is disabled. You can manually enter the total amount or enable auto-calculation above."
|
||||
}
|
||||
</p>
|
||||
{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)
|
||||
Notes (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={formData.notes}
|
||||
onChange={(e) => setFormData({...formData, notes: 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"
|
||||
placeholder="Any additional notes about this purchase..."
|
||||
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 justify-end space-x-3 pt-4">
|
||||
<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="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md"
|
||||
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>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || formData.shop_id === 0 || selectedProducts.length === 0}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
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"
|
||||
>
|
||||
{loading
|
||||
? (isEditMode ? 'Updating...' : 'Creating...')
|
||||
: (isEditMode ? 'Update Shopping Event' : 'Create Shopping Event')
|
||||
}
|
||||
{loading ? 'Saving...' : (isEditMode ? 'Update Event' : 'Create Event')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user