change compose to publish fix some other product stuff
This commit is contained in:
@@ -432,19 +432,15 @@ router.put('/user/:id', validateParams(idParamSchema), validateRequest(userProdu
|
||||
customRateUnit,
|
||||
notes,
|
||||
isAdvancedEdit,
|
||||
// Advanced edit fields (will be ignored for now as schema doesn't support them)
|
||||
// Advanced edit fields
|
||||
brand,
|
||||
categoryId,
|
||||
productType,
|
||||
activeIngredients,
|
||||
description
|
||||
description,
|
||||
spreaderSettings
|
||||
} = req.body;
|
||||
|
||||
// Log warning if advanced edit is attempted (not fully supported yet)
|
||||
if (isAdvancedEdit) {
|
||||
console.warn(`Advanced edit attempted for user product ${userProductId}. Only basic fields will be updated.`);
|
||||
}
|
||||
|
||||
// Check if user product exists and belongs to user
|
||||
const checkResult = await pool.query(
|
||||
'SELECT id FROM user_products WHERE id = $1 AND user_id = $2',
|
||||
@@ -491,6 +487,32 @@ router.put('/user/:id', validateParams(idParamSchema), validateRequest(userProdu
|
||||
|
||||
const userProduct = result.rows[0];
|
||||
|
||||
// Handle spreader settings for granular products
|
||||
if (spreaderSettings && Array.isArray(spreaderSettings) && productType === 'granular') {
|
||||
// First, delete existing spreader settings for this user product
|
||||
await pool.query(
|
||||
'DELETE FROM product_spreader_settings WHERE user_product_id = $1',
|
||||
[userProductId]
|
||||
);
|
||||
|
||||
// Then add the new settings
|
||||
for (const setting of spreaderSettings) {
|
||||
await pool.query(
|
||||
`INSERT INTO product_spreader_settings
|
||||
(user_product_id, spreader_brand, spreader_model, setting_value, rate_description, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[
|
||||
userProductId,
|
||||
setting.spreaderBrand,
|
||||
setting.spreaderModel,
|
||||
setting.settingValue,
|
||||
setting.rateDescription,
|
||||
setting.notes
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Custom product updated successfully',
|
||||
|
||||
@@ -28,6 +28,7 @@ services:
|
||||
- "traefik.http.routers.turftracker-frontend.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.turftracker-frontend.loadbalancer.server.port=3000"
|
||||
- "traefik.docker.network=proxy"
|
||||
- "traefik.constraint=proxy-public"
|
||||
restart: unless-stopped
|
||||
|
||||
backend:
|
||||
|
||||
@@ -80,7 +80,7 @@ const Products = () => {
|
||||
spreaderModel: setting.spreaderModel || null,
|
||||
settingValue: setting.settingValue,
|
||||
rateDescription: setting.rateDescription || null,
|
||||
notes: setting.notes || null
|
||||
notes: setting.notes && setting.notes.trim() ? setting.notes.trim() : null
|
||||
})
|
||||
);
|
||||
|
||||
@@ -717,7 +717,7 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories })
|
||||
);
|
||||
};
|
||||
|
||||
// Edit Product Modal Component
|
||||
// Edit Product Modal Component
|
||||
const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categories }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
productId: product.baseProductId || '',
|
||||
@@ -734,6 +734,41 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor
|
||||
});
|
||||
|
||||
const [editMode, setEditMode] = useState('basic'); // 'basic' or 'advanced'
|
||||
const [editSpreaderSettings, setEditSpreaderSettings] = useState([]);
|
||||
const [newEditSpreaderSetting, setNewEditSpreaderSetting] = useState({
|
||||
spreaderBrand: '',
|
||||
spreaderModel: '',
|
||||
settingValue: '',
|
||||
rateDescription: '',
|
||||
notes: ''
|
||||
});
|
||||
const [loadingSettings, setLoadingSettings] = useState(false);
|
||||
|
||||
// Load existing spreader settings when modal opens
|
||||
useEffect(() => {
|
||||
const loadSpreaderSettings = async () => {
|
||||
if (product.productType === 'granular' && product.id) {
|
||||
setLoadingSettings(true);
|
||||
try {
|
||||
const response = await fetch(`/api/product-spreader-settings/user-product/${product.id}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setEditSpreaderSettings(data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load spreader settings:', error);
|
||||
} finally {
|
||||
setLoadingSettings(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadSpreaderSettings();
|
||||
}, [product.id, product.productType]);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -774,20 +809,51 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor
|
||||
customName: formData.customName,
|
||||
customRateAmount: formData.customRateAmount ? parseFloat(formData.customRateAmount) : null,
|
||||
customRateUnit: formData.customRateUnit || null,
|
||||
notes: formData.notes || null,
|
||||
notes: formData.notes && formData.notes.trim() ? formData.notes.trim() : null,
|
||||
// Additional fields for advanced mode
|
||||
brand: formData.brand || null,
|
||||
categoryId: formData.categoryId ? parseInt(formData.categoryId) : null,
|
||||
productType: formData.productType || null,
|
||||
activeIngredients: formData.activeIngredients || null,
|
||||
description: formData.description || null,
|
||||
isAdvancedEdit: true // Flag to indicate this is an advanced edit
|
||||
isAdvancedEdit: true, // Flag to indicate this is an advanced edit
|
||||
// Include spreader settings for granular products
|
||||
...(formData.productType === 'granular' && editSpreaderSettings.length > 0 && {
|
||||
spreaderSettings: editSpreaderSettings.map(setting => ({
|
||||
spreaderBrand: setting.spreaderBrand,
|
||||
spreaderModel: setting.spreaderModel && setting.spreaderModel.trim() ? setting.spreaderModel.trim() : null,
|
||||
settingValue: setting.settingValue,
|
||||
rateDescription: setting.rateDescription && setting.rateDescription.trim() ? setting.rateDescription.trim() : null,
|
||||
notes: setting.notes && setting.notes.trim() ? setting.notes.trim() : null
|
||||
}))
|
||||
})
|
||||
};
|
||||
|
||||
onSubmit(submitData);
|
||||
}
|
||||
};
|
||||
|
||||
// Handler functions for edit spreader settings
|
||||
const handleAddEditSpreaderSetting = () => {
|
||||
if (!newEditSpreaderSetting.spreaderBrand.trim() || !newEditSpreaderSetting.settingValue.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEditSpreaderSettings([...editSpreaderSettings, { ...newEditSpreaderSetting }]);
|
||||
setNewEditSpreaderSetting({
|
||||
spreaderBrand: '',
|
||||
spreaderModel: '',
|
||||
settingValue: '',
|
||||
rateDescription: '',
|
||||
notes: ''
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveExistingSpreaderSetting = (index) => {
|
||||
const updated = editSpreaderSettings.filter((_, i) => i !== index);
|
||||
setEditSpreaderSettings(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
@@ -1013,6 +1079,101 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor
|
||||
placeholder="Special mixing instructions, storage notes, etc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.productType === 'granular' && (
|
||||
<div>
|
||||
<label className="label">Spreader Settings</label>
|
||||
<div className="border border-gray-200 rounded-lg p-4 space-y-3">
|
||||
{/* Existing spreader settings */}
|
||||
{editSpreaderSettings.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Current Settings:</h4>
|
||||
{editSpreaderSettings.map((setting, index) => (
|
||||
<div key={index} className="bg-gray-50 p-3 rounded border text-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<div><strong>{setting.spreaderBrand}</strong> {setting.spreaderModel && `(${setting.spreaderModel})`}</div>
|
||||
<div>Setting: <span className="font-medium">{setting.settingValue}</span></div>
|
||||
{setting.rateDescription && <div className="text-gray-600">{setting.rateDescription}</div>}
|
||||
{setting.notes && <div className="text-gray-600 italic">{setting.notes}</div>}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveExistingSpreaderSetting(index)}
|
||||
className="text-red-600 hover:text-red-800 text-sm"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add new spreader setting */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Add New Setting:</h4>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="input text-sm"
|
||||
placeholder="Spreader Brand (e.g., LESCO, PermaGreen)"
|
||||
value={newEditSpreaderSetting.spreaderBrand}
|
||||
onChange={(e) => setNewEditSpreaderSetting({ ...newEditSpreaderSetting, spreaderBrand: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="input text-sm"
|
||||
placeholder="Model (optional)"
|
||||
value={newEditSpreaderSetting.spreaderModel}
|
||||
onChange={(e) => setNewEditSpreaderSetting({ ...newEditSpreaderSetting, spreaderModel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="input text-sm"
|
||||
placeholder="Setting (e.g., #14, 2.5)"
|
||||
value={newEditSpreaderSetting.settingValue}
|
||||
onChange={(e) => setNewEditSpreaderSetting({ ...newEditSpreaderSetting, settingValue: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="input text-sm"
|
||||
placeholder="Rate description (optional)"
|
||||
value={newEditSpreaderSetting.rateDescription}
|
||||
onChange={(e) => setNewEditSpreaderSetting({ ...newEditSpreaderSetting, rateDescription: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<textarea
|
||||
className="input text-sm"
|
||||
rows="2"
|
||||
placeholder="Notes (optional)"
|
||||
value={newEditSpreaderSetting.notes}
|
||||
onChange={(e) => setNewEditSpreaderSetting({ ...newEditSpreaderSetting, notes: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddEditSpreaderSetting}
|
||||
disabled={!newEditSpreaderSetting.spreaderBrand.trim() || !newEditSpreaderSetting.settingValue.trim()}
|
||||
className="mt-3 px-3 py-1.5 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Add Setting
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user