seed updates
This commit is contained in:
@@ -218,34 +218,105 @@ const Products = () => {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{product.activeIngredients && (
|
{/* Seed blend pretty display */}
|
||||||
<p className="text-sm text-gray-700 mb-3">
|
{product.productType === 'seed' ? (
|
||||||
<strong>Active Ingredients:</strong> {product.activeIngredients}
|
(()=>{
|
||||||
</p>
|
let blend = [];
|
||||||
|
// Shared product
|
||||||
|
if (Array.isArray(product.seedBlend) && product.seedBlend.length) {
|
||||||
|
blend = product.seedBlend;
|
||||||
|
} else if (product.activeIngredients) {
|
||||||
|
// User product stores blend in activeIngredients JSON
|
||||||
|
try {
|
||||||
|
const ai = typeof product.activeIngredients === 'string' ? JSON.parse(product.activeIngredients) : product.activeIngredients;
|
||||||
|
blend = ai?.seedBlend || [];
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="text-sm font-medium text-gray-900">Seed Blend:</p>
|
||||||
|
{blend.length === 0 ? (
|
||||||
|
<p className="text-sm text-gray-600">No blend details</p>
|
||||||
|
) : (
|
||||||
|
<div className="mt-1 flex flex-wrap gap-2">
|
||||||
|
{blend.map((b, idx)=> (
|
||||||
|
<span key={idx} className="px-2 py-1 rounded bg-gray-100 text-gray-800 text-xs">
|
||||||
|
{b.cultivar} — {parseFloat(b.percent || 0).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
) : (
|
||||||
|
product.activeIngredients && (
|
||||||
|
<p className="text-sm text-gray-700 mb-3">
|
||||||
|
<strong>Active Ingredients:</strong> {product.activeIngredients}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Application Rates */}
|
{/* Application Rates */}
|
||||||
{isUserProduct && product.customRateAmount && (
|
{isUserProduct && (
|
||||||
<div className="bg-gray-50 p-3 rounded-lg mb-3">
|
(()=>{
|
||||||
<p className="text-sm font-medium text-gray-900">Your Rate:</p>
|
if (product.productType === 'seed') {
|
||||||
<p className="text-sm text-gray-700">
|
try {
|
||||||
{product.customRateAmount} {product.customRateUnit}
|
const ai = typeof product.activeIngredients === 'string' ? JSON.parse(product.activeIngredients) : product.activeIngredients;
|
||||||
</p>
|
const sr = ai?.seedRates;
|
||||||
</div>
|
if (sr?.new || sr?.overseed) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 p-3 rounded-lg mb-3">
|
||||||
|
<p className="text-sm font-medium text-gray-900 mb-1">Your Rates:</p>
|
||||||
|
{sr.new && <p className="text-sm text-gray-700">New Lawn: {sr.new} {sr.unit || product.customRateUnit}</p>}
|
||||||
|
{sr.overseed && <p className="text-sm text-gray-700">Overseeding: {sr.overseed} {sr.unit || product.customRateUnit}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if (product.customRateAmount) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 p-3 rounded-lg mb-3">
|
||||||
|
<p className="text-sm font-medium text-gray-900">Your Rate:</p>
|
||||||
|
<p className="text-sm text-gray-700">{product.customRateAmount} {product.customRateUnit}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isUserProduct && product.rates && product.rates.length > 0 && (
|
{!isUserProduct && product.rates && product.rates.length > 0 && (
|
||||||
<div className="bg-gray-50 p-3 rounded-lg mb-3">
|
<div className="bg-gray-50 p-3 rounded-lg mb-3">
|
||||||
<p className="text-sm font-medium text-gray-900 mb-2">Application Rates:</p>
|
<p className="text-sm font-medium text-gray-900 mb-2">Application Rates:</p>
|
||||||
{product.rates.slice(0, 2).map((rate, index) => (
|
{product.productType === 'seed' ? (
|
||||||
<div key={index} className="text-sm text-gray-700">
|
(()=>{
|
||||||
<strong>{rate.applicationType}:</strong> {rate.rateAmount} {rate.rateUnit}
|
const newRate = product.rates.find(r=> (r.applicationType||'').toLowerCase().includes('new'));
|
||||||
</div>
|
const overRate = product.rates.find(r=> (r.applicationType||'').toLowerCase().includes('over'));
|
||||||
))}
|
return (
|
||||||
|
<>
|
||||||
|
{newRate && (
|
||||||
|
<div className="text-sm text-gray-700"><strong>New Lawn:</strong> {newRate.rateAmount} {newRate.rateUnit}</div>
|
||||||
|
)}
|
||||||
|
{overRate && (
|
||||||
|
<div className="text-sm text-gray-700"><strong>Overseeding:</strong> {overRate.rateAmount} {overRate.rateUnit}</div>
|
||||||
|
)}
|
||||||
|
{!newRate && !overRate && product.rates.slice(0,2).map((rate, index)=> (
|
||||||
|
<div key={index} className="text-sm text-gray-700"><strong>{rate.applicationType}:</strong> {rate.rateAmount} {rate.rateUnit}</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
) : (
|
||||||
|
product.rates.slice(0, 2).map((rate, index) => (
|
||||||
|
<div key={index} className="text-sm text-gray-700">
|
||||||
|
<strong>{rate.applicationType}:</strong> {rate.rateAmount} {rate.rateUnit}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
{product.rates.length > 2 && (
|
{product.rates.length > 2 && (
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">+{product.rates.length - 2} more rates</p>
|
||||||
+{product.rates.length - 2} more rates
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -464,6 +535,8 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories })
|
|||||||
|
|
||||||
const [spreaderSettings, setSpreaderSettings] = useState([]);
|
const [spreaderSettings, setSpreaderSettings] = useState([]);
|
||||||
const [seedBlend, setSeedBlend] = useState([]); // [{cultivar:'', percent:0}]
|
const [seedBlend, setSeedBlend] = useState([]); // [{cultivar:'', percent:0}]
|
||||||
|
const [seedNewRate, setSeedNewRate] = useState('');
|
||||||
|
const [seedOverRate, setSeedOverRate] = useState('');
|
||||||
const [availableSpreaders, setAvailableSpreaders] = useState([]);
|
const [availableSpreaders, setAvailableSpreaders] = useState([]);
|
||||||
const [loadingSpreaders, setLoadingSpreaders] = useState(false);
|
const [loadingSpreaders, setLoadingSpreaders] = useState(false);
|
||||||
const [newSpreaderSetting, setNewSpreaderSetting] = useState({
|
const [newSpreaderSetting, setNewSpreaderSetting] = useState({
|
||||||
@@ -549,9 +622,17 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories })
|
|||||||
spreaderSettings: formData.productType === 'granular' ? spreaderSettings : []
|
spreaderSettings: formData.productType === 'granular' ? spreaderSettings : []
|
||||||
};
|
};
|
||||||
|
|
||||||
// If this is a seed product and user entered a blend, pack it into activeIngredients as JSON
|
// If seed, include blend and optional seed rates in activeIngredients JSON
|
||||||
if (formData.productType === 'seed' && Array.isArray(seedBlend) && seedBlend.length > 0) {
|
if (formData.productType === 'seed') {
|
||||||
submitData.activeIngredients = JSON.stringify({ seedBlend });
|
const payload = { seedBlend: Array.isArray(seedBlend) ? seedBlend : [] };
|
||||||
|
if (seedNewRate || seedOverRate) {
|
||||||
|
payload.seedRates = {
|
||||||
|
new: seedNewRate ? parseFloat(seedNewRate) : null,
|
||||||
|
overseed: seedOverRate ? parseFloat(seedOverRate) : null,
|
||||||
|
unit: formData.customRateUnit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
submitData.activeIngredients = JSON.stringify(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(submitData);
|
onSubmit(submitData);
|
||||||
@@ -632,7 +713,19 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories })
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formData.productType === 'seed' && (
|
{formData.productType === 'seed' && (
|
||||||
<SeedBlendEditor value={seedBlend} onChange={setSeedBlend} />
|
<>
|
||||||
|
<SeedBlendEditor value={seedBlend} onChange={setSeedBlend} />
|
||||||
|
<div className="grid grid-cols-2 gap-3 mt-3">
|
||||||
|
<div>
|
||||||
|
<label className="label text-xs">New Lawn Seeding Rate</label>
|
||||||
|
<input type="number" step="0.01" className="input" value={seedNewRate} onChange={(e)=> setSeedNewRate(e.target.value)} placeholder="e.g., 7" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label text-xs">Overseeding Rate</label>
|
||||||
|
<input type="number" step="0.01" className="input" value={seedOverRate} onChange={(e)=> setSeedOverRate(e.target.value)} placeholder="e.g., 3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -840,6 +933,9 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor
|
|||||||
activeIngredients: product.activeIngredients || '',
|
activeIngredients: product.activeIngredients || '',
|
||||||
description: product.description || ''
|
description: product.description || ''
|
||||||
});
|
});
|
||||||
|
const [editSeedBlend, setEditSeedBlend] = useState([]);
|
||||||
|
const [editSeedNewRate, setEditSeedNewRate] = useState('');
|
||||||
|
const [editSeedOverRate, setEditSeedOverRate] = useState('');
|
||||||
|
|
||||||
const [editMode, setEditMode] = useState('basic'); // 'basic' or 'advanced'
|
const [editMode, setEditMode] = useState('basic'); // 'basic' or 'advanced'
|
||||||
const [editSpreaderSettings, setEditSpreaderSettings] = useState([]);
|
const [editSpreaderSettings, setEditSpreaderSettings] = useState([]);
|
||||||
@@ -903,6 +999,24 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor
|
|||||||
loadSpreaderSettings();
|
loadSpreaderSettings();
|
||||||
}, [product.id, product.productType]);
|
}, [product.id, product.productType]);
|
||||||
|
|
||||||
|
// Parse seed data from activeIngredients when opening
|
||||||
|
useEffect(() => {
|
||||||
|
if (product.productType === 'seed') {
|
||||||
|
try {
|
||||||
|
const ai = typeof product.activeIngredients === 'string' ? JSON.parse(product.activeIngredients) : product.activeIngredients;
|
||||||
|
if (ai?.seedBlend) setEditSeedBlend(ai.seedBlend);
|
||||||
|
if (ai?.seedRates) {
|
||||||
|
setEditSeedNewRate(ai.seedRates.new || '');
|
||||||
|
setEditSeedOverRate(ai.seedRates.overseed || '');
|
||||||
|
if (ai.seedRates.unit && !formData.customRateUnit) {
|
||||||
|
setFormData(prev => ({ ...prev, customRateUnit: ai.seedRates.unit }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -947,7 +1061,14 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor
|
|||||||
brand: formData.brand || null,
|
brand: formData.brand || null,
|
||||||
categoryId: formData.categoryId ? parseInt(formData.categoryId) : null,
|
categoryId: formData.categoryId ? parseInt(formData.categoryId) : null,
|
||||||
productType: formData.productType || null,
|
productType: formData.productType || null,
|
||||||
activeIngredients: formData.activeIngredients || null,
|
activeIngredients: formData.productType === 'seed' ? JSON.stringify({
|
||||||
|
seedBlend: editSeedBlend,
|
||||||
|
seedRates: (editSeedNewRate || editSeedOverRate) ? {
|
||||||
|
new: editSeedNewRate ? parseFloat(editSeedNewRate) : null,
|
||||||
|
overseed: editSeedOverRate ? parseFloat(editSeedOverRate) : null,
|
||||||
|
unit: formData.customRateUnit
|
||||||
|
} : undefined
|
||||||
|
}) : (formData.activeIngredients || null),
|
||||||
description: formData.description || 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
|
// Include spreader settings for granular products
|
||||||
@@ -1278,16 +1399,32 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{formData.productType === 'seed' ? (
|
||||||
<label className="label">Active Ingredients</label>
|
<>
|
||||||
<input
|
<SeedBlendEditor value={editSeedBlend} onChange={setEditSeedBlend} />
|
||||||
type="text"
|
<div className="grid grid-cols-2 gap-3 mt-3">
|
||||||
className="input"
|
<div>
|
||||||
value={formData.activeIngredients}
|
<label className="label text-xs">New Lawn Seeding Rate</label>
|
||||||
onChange={(e) => setFormData({ ...formData, activeIngredients: e.target.value })}
|
<input type="number" step="0.01" className="input" value={editSeedNewRate} onChange={(e)=> setEditSeedNewRate(e.target.value)} placeholder="e.g., 7" />
|
||||||
placeholder="e.g., 2,4-D 38.3%, Dicamba 2.77%"
|
</div>
|
||||||
/>
|
<div>
|
||||||
</div>
|
<label className="label text-xs">Overseeding Rate</label>
|
||||||
|
<input type="number" step="0.01" className="input" value={editSeedOverRate} onChange={(e)=> setEditSeedOverRate(e.target.value)} placeholder="e.g., 3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<label className="label">Active Ingredients</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input"
|
||||||
|
value={formData.activeIngredients}
|
||||||
|
onChange={(e) => setFormData({ ...formData, activeIngredients: e.target.value })}
|
||||||
|
placeholder="e.g., 2,4-D 38.3%, Dicamba 2.77%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="label">Description</label>
|
<label className="label">Description</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user