change compose to publish fix some other product stuff

This commit is contained in:
Jake Kasper
2025-08-24 14:11:47 -04:00
parent bd844d0a2e
commit 1758252d54
3 changed files with 195 additions and 11 deletions

View File

@@ -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',

View File

@@ -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:

View File

@@ -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
})
);
@@ -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>
)}
</>
)}