seed stuff
This commit is contained in:
@@ -406,7 +406,7 @@ router.get('/products', async (req, res, next) => {
|
||||
// @access Private (Admin)
|
||||
router.post('/products', validateRequest(productSchema), async (req, res, next) => {
|
||||
try {
|
||||
const { name, brand, categoryId, productType, activeIngredients, description } = req.body;
|
||||
const { name, brand, categoryId, productType, activeIngredients, description, seedBlend } = req.body;
|
||||
|
||||
// Check if category exists
|
||||
const categoryCheck = await pool.query(
|
||||
@@ -419,10 +419,10 @@ router.post('/products', validateRequest(productSchema), async (req, res, next)
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO products (name, brand, category_id, product_type, active_ingredients, description)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`INSERT INTO products (name, brand, category_id, product_type, active_ingredients, description, seed_blend)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[name, brand, categoryId, productType, activeIngredients, description]
|
||||
[name, brand, categoryId, productType, activeIngredients, description, seedBlend ? JSON.stringify(seedBlend) : null]
|
||||
);
|
||||
|
||||
const product = result.rows[0];
|
||||
@@ -439,7 +439,8 @@ router.post('/products', validateRequest(productSchema), async (req, res, next)
|
||||
productType: product.product_type,
|
||||
activeIngredients: product.active_ingredients,
|
||||
description: product.description,
|
||||
createdAt: product.created_at
|
||||
createdAt: product.created_at,
|
||||
seedBlend: product.seed_blend
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -454,7 +455,7 @@ router.post('/products', validateRequest(productSchema), async (req, res, next)
|
||||
router.put('/products/:id', validateParams(idParamSchema), validateRequest(productSchema), async (req, res, next) => {
|
||||
try {
|
||||
const productId = req.params.id;
|
||||
const { name, brand, categoryId, productType, activeIngredients, description } = req.body;
|
||||
const { name, brand, categoryId, productType, activeIngredients, description, seedBlend } = req.body;
|
||||
|
||||
// Check if product exists
|
||||
const productCheck = await pool.query(
|
||||
@@ -479,10 +480,10 @@ router.put('/products/:id', validateParams(idParamSchema), validateRequest(produ
|
||||
const result = await pool.query(
|
||||
`UPDATE products
|
||||
SET name = $1, brand = $2, category_id = $3, product_type = $4,
|
||||
active_ingredients = $5, description = $6
|
||||
WHERE id = $7
|
||||
active_ingredients = $5, description = $6, seed_blend = $7
|
||||
WHERE id = $8
|
||||
RETURNING *`,
|
||||
[name, brand, categoryId, productType, activeIngredients, description, productId]
|
||||
[name, brand, categoryId, productType, activeIngredients, description, seedBlend ? JSON.stringify(seedBlend) : null, productId]
|
||||
);
|
||||
|
||||
const product = result.rows[0];
|
||||
@@ -498,6 +499,7 @@ router.put('/products/:id', validateParams(idParamSchema), validateRequest(produ
|
||||
categoryId: product.category_id,
|
||||
productType: product.product_type,
|
||||
activeIngredients: product.active_ingredients,
|
||||
seedBlend: product.seed_blend,
|
||||
description: product.description,
|
||||
createdAt: product.created_at
|
||||
}
|
||||
|
||||
@@ -989,6 +989,7 @@ router.get('/logs', async (req, res, next) => {
|
||||
averageSpeed: parseFloat(log.average_speed),
|
||||
areaCovered: parseFloat(log.area_covered),
|
||||
notes: log.notes,
|
||||
seedingType: log.seeding_type,
|
||||
sectionName: log.section_name,
|
||||
sectionArea: parseFloat(log.section_area),
|
||||
propertyName: log.property_name,
|
||||
@@ -1020,7 +1021,8 @@ router.post('/logs', validateRequest(applicationLogSchema), async (req, res, nex
|
||||
averageSpeed,
|
||||
areaCovered,
|
||||
notes,
|
||||
products
|
||||
products,
|
||||
seedingType
|
||||
} = req.body;
|
||||
|
||||
// Start transaction
|
||||
@@ -1083,6 +1085,8 @@ router.post('/logs', validateRequest(applicationLogSchema), async (req, res, nex
|
||||
|
||||
// Add products to log
|
||||
console.log('Adding products to log:', products);
|
||||
// Track seed product names for lawn section update
|
||||
const seedProductNames = [];
|
||||
for (const product of products) {
|
||||
const {
|
||||
productId,
|
||||
@@ -1109,6 +1113,17 @@ router.post('/logs', validateRequest(applicationLogSchema), async (req, res, nex
|
||||
);
|
||||
|
||||
console.log('Product inserted successfully');
|
||||
|
||||
// Determine if this product is of type 'seed' and capture its name
|
||||
try {
|
||||
if (productId) {
|
||||
const pr = await client.query('SELECT name, product_type FROM products WHERE id=$1', [productId]);
|
||||
if (pr.rows[0]?.product_type === 'seed') seedProductNames.push(pr.rows[0].name);
|
||||
} else if (userProductId) {
|
||||
const upr = await client.query('SELECT custom_name, custom_product_type FROM user_products WHERE id=$1 AND user_id=$2', [userProductId, req.user.id]);
|
||||
if (upr.rows[0]?.custom_product_type === 'seed') seedProductNames.push(upr.rows[0].custom_name);
|
||||
}
|
||||
} catch (e) { console.warn('Seed product detection failed', e.message); }
|
||||
}
|
||||
|
||||
// If this was from a plan, mark the plan as completed
|
||||
@@ -1119,6 +1134,15 @@ router.post('/logs', validateRequest(applicationLogSchema), async (req, res, nex
|
||||
);
|
||||
}
|
||||
|
||||
// If seed was applied, update the lawn section grass type to reflect seeds
|
||||
if (seedProductNames.length > 0) {
|
||||
const blendLabel = seedProductNames.join(' + ');
|
||||
const suffix = seedingType === 'new_seed' ? ' (New Seed)' : (seedingType === 'overseed' ? ' (Overseed)' : '');
|
||||
try {
|
||||
await client.query('UPDATE lawn_sections SET grass_type=$1, updated_at=CURRENT_TIMESTAMP WHERE id=$2', [blendLabel + suffix, lawnSectionId]);
|
||||
} catch (e) { console.warn('Failed to update grass_type for section', lawnSectionId, e.message); }
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
res.status(201).json({
|
||||
|
||||
@@ -110,7 +110,7 @@ router.get('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
|
||||
// Get lawn sections
|
||||
const sectionsResult = await pool.query(
|
||||
'SELECT * FROM lawn_sections WHERE property_id = $1 ORDER BY name',
|
||||
'SELECT *, grass_types FROM lawn_sections WHERE property_id = $1 ORDER BY name',
|
||||
[propertyId]
|
||||
);
|
||||
|
||||
@@ -132,6 +132,7 @@ router.get('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
area: parseFloat(section.area),
|
||||
polygonData: section.polygon_data,
|
||||
grassType: section.grass_type,
|
||||
grassTypes: section.grass_types || null,
|
||||
soilType: section.soil_type,
|
||||
createdAt: section.created_at,
|
||||
updatedAt: section.updated_at
|
||||
@@ -277,7 +278,7 @@ router.delete('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
router.post('/:id/sections', validateParams(idParamSchema), validateRequest(lawnSectionSchema), async (req, res, next) => {
|
||||
try {
|
||||
const propertyId = req.params.id;
|
||||
const { name, area, polygonData, grassType, soilType } = req.body;
|
||||
const { name, area, polygonData, grassType, grassTypes, soilType } = req.body;
|
||||
|
||||
// Check if property exists and belongs to user
|
||||
const propertyResult = await pool.query(
|
||||
@@ -299,10 +300,18 @@ router.post('/:id/sections', validateParams(idParamSchema), validateRequest(lawn
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO lawn_sections (property_id, name, area, polygon_data, grass_type, soil_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`INSERT INTO lawn_sections (property_id, name, area, polygon_data, grass_type, grass_types, soil_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[propertyId, name, calculatedArea, JSON.stringify(polygonData), grassType, soilType]
|
||||
[
|
||||
propertyId,
|
||||
name,
|
||||
calculatedArea,
|
||||
JSON.stringify(polygonData),
|
||||
grassType || (Array.isArray(grassTypes) ? grassTypes.join(', ') : null),
|
||||
grassTypes ? JSON.stringify(grassTypes) : null,
|
||||
soilType
|
||||
]
|
||||
);
|
||||
|
||||
const section = result.rows[0];
|
||||
@@ -317,6 +326,7 @@ router.post('/:id/sections', validateParams(idParamSchema), validateRequest(lawn
|
||||
area: parseFloat(section.area),
|
||||
polygonData: section.polygon_data,
|
||||
grassType: section.grass_type,
|
||||
grassTypes: section.grass_types,
|
||||
soilType: section.soil_type,
|
||||
createdAt: section.created_at,
|
||||
updatedAt: section.updated_at
|
||||
@@ -334,7 +344,7 @@ router.post('/:id/sections', validateParams(idParamSchema), validateRequest(lawn
|
||||
router.put('/:propertyId/sections/:sectionId', async (req, res, next) => {
|
||||
try {
|
||||
const { propertyId, sectionId } = req.params;
|
||||
const { name, area, polygonData, grassType, soilType } = req.body;
|
||||
const { name, area, polygonData, grassType, grassTypes, soilType } = req.body;
|
||||
|
||||
// Check if section exists and user owns the property
|
||||
const checkResult = await pool.query(
|
||||
@@ -357,10 +367,18 @@ router.put('/:propertyId/sections/:sectionId', async (req, res, next) => {
|
||||
|
||||
const result = await pool.query(
|
||||
`UPDATE lawn_sections
|
||||
SET name = $1, area = $2, polygon_data = $3, grass_type = $4, soil_type = $5, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $6
|
||||
SET name = $1, area = $2, polygon_data = $3, grass_type = $4, grass_types=$5, soil_type = $6, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $7
|
||||
RETURNING *`,
|
||||
[name, calculatedArea, JSON.stringify(polygonData), grassType, soilType, sectionId]
|
||||
[
|
||||
name,
|
||||
calculatedArea,
|
||||
JSON.stringify(polygonData),
|
||||
grassType || (Array.isArray(grassTypes) ? grassTypes.join(', ') : null),
|
||||
grassTypes ? JSON.stringify(grassTypes) : null,
|
||||
soilType,
|
||||
sectionId
|
||||
]
|
||||
);
|
||||
|
||||
const section = result.rows[0];
|
||||
@@ -375,6 +393,7 @@ router.put('/:propertyId/sections/:sectionId', async (req, res, next) => {
|
||||
area: parseFloat(section.area),
|
||||
polygonData: section.polygon_data,
|
||||
grassType: section.grass_type,
|
||||
grassTypes: section.grass_types,
|
||||
soilType: section.soil_type,
|
||||
createdAt: section.created_at,
|
||||
updatedAt: section.updated_at
|
||||
|
||||
@@ -63,9 +63,13 @@ const productSchema = Joi.object({
|
||||
name: Joi.string().max(255).required(),
|
||||
brand: Joi.string().max(100),
|
||||
categoryId: Joi.number().integer().positive().required(),
|
||||
productType: Joi.string().valid('granular', 'liquid').required(),
|
||||
productType: Joi.string().valid('granular', 'liquid', 'seed').required(),
|
||||
activeIngredients: Joi.string(),
|
||||
description: Joi.string()
|
||||
description: Joi.string(),
|
||||
seedBlend: Joi.alternatives().try(Joi.array().items(Joi.object({
|
||||
cultivar: Joi.string().max(200).required(),
|
||||
percent: Joi.number().min(0).max(100).required()
|
||||
})), Joi.object()).optional()
|
||||
});
|
||||
|
||||
const productRateSchema = Joi.object({
|
||||
@@ -169,6 +173,7 @@ const applicationLogSchema = Joi.object({
|
||||
averageSpeed: Joi.number().positive(),
|
||||
areaCovered: Joi.number().positive(),
|
||||
notes: Joi.string(),
|
||||
seedingType: Joi.string().valid('overseed','new_seed').allow(null).optional(),
|
||||
products: Joi.array().items(Joi.object({
|
||||
productId: Joi.number().integer().positive(),
|
||||
userProductId: Joi.number().integer().positive(),
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
-- Migration: add JSONB grass types to lawn_sections, enable seed products, and store seeding type on logs
|
||||
-- 1) Lawn sections: add grass_types JSONB and backfill from grass_type CSV
|
||||
ALTER TABLE lawn_sections
|
||||
ADD COLUMN IF NOT EXISTS grass_types JSONB;
|
||||
|
||||
-- Backfill: split grass_type CSV to JSON array when present
|
||||
UPDATE lawn_sections
|
||||
SET grass_types = (
|
||||
CASE
|
||||
WHEN grass_type IS NULL OR trim(grass_type) = '' THEN NULL
|
||||
ELSE (
|
||||
SELECT jsonb_agg(trim(x)) FROM (
|
||||
SELECT regexp_split_to_table(grass_type, '\s*,\s*') AS x
|
||||
) s WHERE trim(x) <> ''
|
||||
)
|
||||
END
|
||||
)
|
||||
WHERE grass_types IS NULL;
|
||||
|
||||
-- 2) Products: allow 'seed' type and add seed_blend JSONB
|
||||
DO $$
|
||||
DECLARE cname text;
|
||||
BEGIN
|
||||
SELECT conname INTO cname
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'products'::regclass AND contype='c' AND conname LIKE '%product_type%';
|
||||
IF cname IS NOT NULL THEN
|
||||
EXECUTE format('ALTER TABLE products DROP CONSTRAINT %I', cname);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE products
|
||||
ADD CONSTRAINT products_product_type_check CHECK (product_type IN ('granular','liquid','seed'));
|
||||
|
||||
ALTER TABLE products
|
||||
ADD COLUMN IF NOT EXISTS seed_blend JSONB;
|
||||
|
||||
-- 3) Application logs: store seeding type
|
||||
ALTER TABLE application_logs
|
||||
ADD COLUMN IF NOT EXISTS seeding_type VARCHAR(20) CHECK (seeding_type IN ('overseed','new_seed'));
|
||||
|
||||
@@ -20,6 +20,7 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
const [sections, setSections] = useState([]);
|
||||
const [mapCenter, setMapCenter] = useState(null);
|
||||
const [planDetails, setPlanDetails] = useState(null);
|
||||
const [seedingType, setSeedingType] = useState('overseed');
|
||||
|
||||
// Debug: Log the application data to understand structure
|
||||
useEffect(() => {
|
||||
@@ -114,6 +115,11 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
return 3.0; // 3.0 mph default target
|
||||
}, [application]);
|
||||
|
||||
// Determine if this is a seed application based on plan details
|
||||
const isSeedApplication = useMemo(() => {
|
||||
return (planDetails?.products || []).some(p => (p.productType || '').toLowerCase() === 'seed');
|
||||
}, [planDetails?.products]);
|
||||
|
||||
const speedStatus = useMemo(() => {
|
||||
if (!targetSpeed || !currentSpeed) return 'normal';
|
||||
|
||||
@@ -301,6 +307,9 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
notes: `Application completed via mobile tracking. Duration: ${Math.round(duration/60)} minutes, Distance: ${(totalDistance * 3.28084).toFixed(0)} ft, Points: ${gpsTrack.length}`,
|
||||
products: validProducts
|
||||
};
|
||||
if (isSeedApplication) {
|
||||
logData.seedingType = seedingType;
|
||||
}
|
||||
|
||||
// Debug: log the exact data being sent
|
||||
console.log('Sending log data:', JSON.stringify(logData, null, 2));
|
||||
@@ -453,6 +462,23 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Seeding options for seed products */}
|
||||
{isSeedApplication && (
|
||||
<div className="bg-green-50 p-4 rounded-lg mb-6">
|
||||
<h4 className="font-medium mb-2">Seeding Details</h4>
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
<label className="inline-flex items-center gap-2">
|
||||
<input type="radio" name="seedingType" value="overseed" checked={seedingType==='overseed'} onChange={()=> setSeedingType('overseed')} />
|
||||
Overseed
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-2">
|
||||
<input type="radio" name="seedingType" value="new_seed" checked={seedingType==='new_seed'} onChange={()=> setSeedingType('new_seed')} />
|
||||
New Seed
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tracking Stats */}
|
||||
{isTracking && (
|
||||
<div className="bg-blue-50 p-4 rounded-lg mb-6">
|
||||
|
||||
@@ -458,6 +458,7 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories })
|
||||
});
|
||||
|
||||
const [spreaderSettings, setSpreaderSettings] = useState([]);
|
||||
const [seedBlend, setSeedBlend] = useState([]); // [{cultivar:'', percent:0}]
|
||||
const [availableSpreaders, setAvailableSpreaders] = useState([]);
|
||||
const [loadingSpreaders, setLoadingSpreaders] = useState(false);
|
||||
const [newSpreaderSetting, setNewSpreaderSetting] = useState({
|
||||
@@ -543,6 +544,11 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories })
|
||||
spreaderSettings: formData.productType === 'granular' ? spreaderSettings : []
|
||||
};
|
||||
|
||||
// If this is a seed product and user entered a blend, pack it into activeIngredients as JSON
|
||||
if (formData.productType === 'seed' && Array.isArray(seedBlend) && seedBlend.length > 0) {
|
||||
submitData.activeIngredients = JSON.stringify({ seedBlend });
|
||||
}
|
||||
|
||||
onSubmit(submitData);
|
||||
};
|
||||
|
||||
@@ -618,6 +624,10 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.productType === 'seed' && (
|
||||
<SeedBlendEditor value={seedBlend} onChange={setSeedBlend} />
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="label">Brand</label>
|
||||
<input
|
||||
@@ -1460,4 +1470,38 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor
|
||||
);
|
||||
};
|
||||
|
||||
// Editor component for seed blends
|
||||
const SeedBlendEditor = ({ value = [], onChange }) => {
|
||||
const [rows, setRows] = React.useState(value || []);
|
||||
React.useEffect(()=>{ onChange && onChange(rows); }, [rows]);
|
||||
const addRow = () => setRows([...(rows||[]), { cultivar: '', percent: '' }]);
|
||||
const updateRow = (i, field, v) => setRows((rows||[]).map((r,idx)=> idx===i? { ...r, [field]: v } : r));
|
||||
const removeRow = (i) => setRows((rows||[]).filter((_,idx)=> idx!==i));
|
||||
const total = (rows||[]).reduce((s,r)=> s + (parseFloat(r.percent)||0), 0);
|
||||
return (
|
||||
<div className="border rounded-lg p-3">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="label m-0">Seed Blend</label>
|
||||
<button type="button" className="text-sm text-blue-600" onClick={addRow}>Add Cultivar</button>
|
||||
</div>
|
||||
{(rows||[]).length === 0 ? (
|
||||
<div className="text-sm text-gray-500">No cultivars added</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{(rows||[]).map((row, i) => (
|
||||
<div key={i} className="grid grid-cols-6 gap-2 items-center">
|
||||
<input className="input col-span-4" placeholder="Cultivar name" value={row.cultivar} onChange={(e)=> updateRow(i,'cultivar', e.target.value)} />
|
||||
<div className="col-span-1 flex items-center gap-1">
|
||||
<input type="number" step="0.1" className="input" placeholder="%" value={row.percent} onChange={(e)=> updateRow(i,'percent', e.target.value)} />
|
||||
</div>
|
||||
<button type="button" className="text-red-600" onClick={()=> removeRow(i)}>Remove</button>
|
||||
</div>
|
||||
))}
|
||||
<div className={`text-xs ${Math.abs(total-100) < 0.01 ? 'text-green-700' : 'text-gray-600'}`}>Total: {total.toFixed(1)}%</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Products;
|
||||
@@ -19,6 +19,52 @@ import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||
import toast from 'react-hot-toast';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
|
||||
// Simple tag input and suggestion components for grass types
|
||||
const COOL_SEASON_GRASSES = [
|
||||
'Turf Type Tall Fescue',
|
||||
'Kentucky Bluegrass',
|
||||
'Perennial Ryegrass',
|
||||
'Fine Fescue',
|
||||
'Creeping Red Fescue',
|
||||
'Chewings Fescue',
|
||||
'Hard Fescue',
|
||||
'Annual Ryegrass'
|
||||
];
|
||||
|
||||
const TagInput = ({ value = [], onChange }) => {
|
||||
const [input, setInput] = useState('');
|
||||
const add = (val) => { const v = val.trim(); if (!v) return; if (!value.includes(v)) onChange([...(value||[]), v]); setInput(''); };
|
||||
return (
|
||||
<div className="border rounded p-2">
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{(value||[]).map((t) => (
|
||||
<span key={t} className="px-2 py-1 bg-gray-100 rounded text-xs flex items-center gap-1">
|
||||
{t}
|
||||
<button className="text-gray-500 hover:text-gray-700" onClick={() => onChange((value||[]).filter(x=>x!==t))}>×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
className="w-full border-0 focus:outline-none text-sm"
|
||||
placeholder="Type and press Enter to add"
|
||||
value={input}
|
||||
onChange={(e)=> setInput(e.target.value)}
|
||||
onKeyDown={(e)=> { if (e.key==='Enter'){ e.preventDefault(); add(input); } }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SuggestionChips = ({ onPick }) => (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{COOL_SEASON_GRASSES.map(g => (
|
||||
<button key={g} type="button" onClick={()=> onPick(g)} className="px-2 py-1 bg-blue-50 hover:bg-blue-100 text-blue-700 rounded text-xs">
|
||||
{g}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Fix for default markers
|
||||
delete Icon.Default.prototype._getIconUrl;
|
||||
Icon.Default.mergeOptions({
|
||||
@@ -302,8 +348,10 @@ const PropertyDetail = () => {
|
||||
const [showNameModal, setShowNameModal] = useState(false);
|
||||
const [pendingSection, setPendingSection] = useState(null);
|
||||
const [sectionName, setSectionName] = useState('');
|
||||
const [sectionGrassTypes, setSectionGrassTypes] = useState([]);
|
||||
const [editingSection, setEditingSection] = useState(null);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editGrassTypes, setEditGrassTypes] = useState([]);
|
||||
|
||||
// Recent history state for this property
|
||||
const [completedApplications, setCompletedApplications] = useState([]);
|
||||
@@ -376,7 +424,9 @@ const PropertyDetail = () => {
|
||||
name: section.name,
|
||||
coordinates: section.polygonData?.coordinates?.[0] || [],
|
||||
color: section.polygonData?.color || SECTION_COLORS[0],
|
||||
area: section.area
|
||||
area: section.area,
|
||||
grassType: section.grassType || '',
|
||||
grassTypes: section.grassTypes || null
|
||||
}));
|
||||
setLawnSections(convertedSections);
|
||||
}
|
||||
@@ -417,7 +467,8 @@ const PropertyDetail = () => {
|
||||
coordinates: [pendingSection.coordinates],
|
||||
color: pendingSection.color
|
||||
},
|
||||
grassType: null,
|
||||
grassType: sectionGrassTypes.join(', '),
|
||||
grassTypes: sectionGrassTypes,
|
||||
soilType: null
|
||||
};
|
||||
|
||||
@@ -438,6 +489,7 @@ const PropertyDetail = () => {
|
||||
|
||||
// Reset and cycle color
|
||||
setSectionName('');
|
||||
setSectionGrassTypes([]);
|
||||
setPendingSection(null);
|
||||
setShowNameModal(false);
|
||||
const nextIndex = (SECTION_COLORS.findIndex(c => c.value === currentColor.value) + 1) % SECTION_COLORS.length;
|
||||
@@ -466,6 +518,8 @@ const PropertyDetail = () => {
|
||||
setSectionName(section.name);
|
||||
setCurrentColor(section.color);
|
||||
setShowEditModal(true);
|
||||
const existing = (section.grassType || '').split(',').map(s=>s.trim()).filter(Boolean);
|
||||
setEditGrassTypes(existing);
|
||||
};
|
||||
|
||||
const saveEditedSection = async () => {
|
||||
@@ -482,7 +536,8 @@ const PropertyDetail = () => {
|
||||
coordinates: [editingSection.coordinates],
|
||||
color: currentColor
|
||||
},
|
||||
grassType: null,
|
||||
grassType: editGrassTypes.join(', '),
|
||||
grassTypes: editGrassTypes,
|
||||
soilType: null
|
||||
};
|
||||
|
||||
@@ -501,6 +556,7 @@ const PropertyDetail = () => {
|
||||
setSectionName('');
|
||||
setEditingSection(null);
|
||||
setShowEditModal(false);
|
||||
setEditGrassTypes([]);
|
||||
} catch (error) {
|
||||
console.error('Failed to update section:', error);
|
||||
toast.error('Failed to update section. Please try again.');
|
||||
@@ -516,7 +572,8 @@ const PropertyDetail = () => {
|
||||
coordinates: [updatedSection.coordinates],
|
||||
color: updatedSection.color
|
||||
},
|
||||
grassType: null,
|
||||
grassType: (updatedSection.grassType || ''),
|
||||
grassTypes: (updatedSection.grassTypes || null),
|
||||
soilType: null
|
||||
};
|
||||
|
||||
@@ -767,6 +824,13 @@ const PropertyDetail = () => {
|
||||
<div>
|
||||
<p className="font-medium text-sm">{section.name}</p>
|
||||
<p className="text-xs text-gray-600">{section.area.toLocaleString()} sq ft</p>
|
||||
{(section.grassTypes && section.grassTypes.length > 0) || section.grassType ? (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{((section.grassTypes && section.grassTypes.length > 0) ? section.grassTypes : (section.grassType||'').split(',').map(s=>s.trim()).filter(Boolean)).map((g, idx)=>(
|
||||
<span key={idx} className="px-1.5 py-0.5 bg-green-100 text-green-800 rounded text-[10px]">{g}</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
@@ -901,6 +965,14 @@ const PropertyDetail = () => {
|
||||
/>
|
||||
<span>{pendingSection?.area.toLocaleString()} sq ft</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Grass Types</label>
|
||||
<TagInput
|
||||
value={sectionGrassTypes}
|
||||
onChange={setSectionGrassTypes}
|
||||
/>
|
||||
<SuggestionChips onPick={(v)=> setSectionGrassTypes(prev=> prev.includes(v)? prev : [...prev, v])} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button onClick={saveLawnSection} className="btn-primary flex-1">
|
||||
@@ -911,6 +983,7 @@ const PropertyDetail = () => {
|
||||
setShowNameModal(false);
|
||||
setPendingSection(null);
|
||||
setSectionName('');
|
||||
setSectionGrassTypes([]);
|
||||
}}
|
||||
className="btn-secondary flex-1"
|
||||
>
|
||||
@@ -955,6 +1028,14 @@ const PropertyDetail = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label">Grass Types</label>
|
||||
<TagInput
|
||||
value={editGrassTypes}
|
||||
onChange={setEditGrassTypes}
|
||||
/>
|
||||
<SuggestionChips onPick={(v)=> setEditGrassTypes(prev=> prev.includes(v)? prev : [...prev, v])} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<div
|
||||
@@ -973,6 +1054,7 @@ const PropertyDetail = () => {
|
||||
setShowEditModal(false);
|
||||
setEditingSection(null);
|
||||
setSectionName('');
|
||||
setEditGrassTypes([]);
|
||||
}}
|
||||
className="btn-secondary flex-1"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user