From 3ddbc32fe0be708d7d980c4b9fb4fa35309abdb1 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Thu, 4 Sep 2025 10:00:27 -0500 Subject: [PATCH] seeding app --- backend/src/routes/applications.js | 21 ++++++++++++------ backend/src/routes/products.js | 22 +++++++++++++++++++ backend/src/utils/validation.js | 2 ++ .../V9__application_plan_seeding_mode.sql | 10 +++++++++ .../Applications/ApplicationPlanModal.js | 14 +++++++----- .../src/pages/Applications/Applications.js | 4 ++++ 6 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 database/migrations/V9__application_plan_seeding_mode.sql diff --git a/backend/src/routes/applications.js b/backend/src/routes/applications.js index 33df83c..e33537c 100644 --- a/backend/src/routes/applications.js +++ b/backend/src/routes/applications.js @@ -192,6 +192,8 @@ router.get('/plans', async (req, res, next) => { status: plan.status, plannedDate: plan.planned_date, notes: plan.notes, + applicationType: plan.application_type, + seedingMode: plan.seeding_mode, sectionNames: plan.section_names, // Multiple section names comma-separated sectionCount: parseInt(plan.section_count), totalSectionArea: parseFloat(plan.total_section_area), @@ -296,6 +298,8 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) = status: plan.status, plannedDate: plan.planned_date, notes: plan.notes, + applicationType: plan.application_type, + seedingMode: plan.seeding_mode, sections: sections, // Array of sections instead of single section totalArea: totalArea, property: { @@ -353,7 +357,9 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n products, areaSquareFeet, equipment, - nozzle + nozzle, + applicationType: planApplicationType, + seedingMode } = req.body; // Handle both single and multiple lawn sections @@ -427,10 +433,10 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n // Create application plan (no longer has lawn_section_id column) const planResult = await client.query( - `INSERT INTO application_plans (user_id, equipment_id, nozzle_id, planned_date, notes) - VALUES ($1, $2, $3, $4, $5) + `INSERT INTO application_plans (user_id, equipment_id, nozzle_id, planned_date, notes, application_type, seeding_mode) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`, - [req.user.id, equipmentId, nozzleId, plannedDate, notes] + [req.user.id, equipmentId, nozzleId, plannedDate, notes, planApplicationType || null, seedingMode || null] ); const plan = planResult.rows[0]; @@ -686,10 +692,11 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat const updateResult = await client.query( `UPDATE application_plans SET equipment_id = $1, nozzle_id = $2, - planned_date = $3, notes = $4, updated_at = CURRENT_TIMESTAMP - WHERE id = $5 + planned_date = $3, notes = $4, application_type = $5, seeding_mode = $6, + updated_at = CURRENT_TIMESTAMP + WHERE id = $7 RETURNING *`, - [equipmentId, nozzleId, plannedDate, notes, planId] + [equipmentId, nozzleId, plannedDate, notes, req.body.applicationType || null, req.body.seedingMode || null, planId] ); const plan = updateResult.rows[0]; diff --git a/backend/src/routes/products.js b/backend/src/routes/products.js index 36aa3b0..3f61b65 100644 --- a/backend/src/routes/products.js +++ b/backend/src/routes/products.js @@ -99,6 +99,27 @@ router.get('/', async (req, res, next) => { const userResult = await pool.query(userProductsQuery, [req.user.id]); + // Attach base product rates to user products so seed scenarios can pick proper rates + let baseRatesById = {}; + const baseIds = userResult.rows.map(r => r.product_id).filter(Boolean); + if (baseIds.length) { + const ratesRes = await pool.query( + `SELECT product_id, id, application_type, rate_amount, rate_unit, notes + FROM product_rates + WHERE product_id = ANY($1::int[])`, [baseIds] + ); + ratesRes.rows.forEach(r => { + if (!baseRatesById[r.product_id]) baseRatesById[r.product_id] = []; + baseRatesById[r.product_id].push({ + id: r.id, + applicationType: r.application_type, + rateAmount: parseFloat(r.rate_amount), + rateUnit: r.rate_unit, + notes: r.notes + }); + }); + } + res.json({ success: true, data: { @@ -133,6 +154,7 @@ router.get('/', async (req, res, next) => { customRateAmount: parseFloat(product.custom_rate_amount), customRateUnit: product.custom_rate_unit, notes: product.notes, + rates: baseRatesById[product.product_id] || [], isShared: false, createdAt: product.created_at, updatedAt: product.updated_at diff --git a/backend/src/utils/validation.js b/backend/src/utils/validation.js index 3b9e9f7..7fe9191 100644 --- a/backend/src/utils/validation.js +++ b/backend/src/utils/validation.js @@ -140,6 +140,8 @@ const applicationPlanSchema = Joi.object({ nozzleId: Joi.number().integer().positive().optional(), plannedDate: Joi.date().required(), notes: Joi.string().allow('').optional(), + applicationType: Joi.string().valid('liquid','granular','seed').optional(), + seedingMode: Joi.string().valid('overseed','new_seed').allow(null).optional(), areaSquareFeet: Joi.number().positive().optional(), equipment: Joi.object({ id: Joi.number().integer().positive().optional(), diff --git a/database/migrations/V9__application_plan_seeding_mode.sql b/database/migrations/V9__application_plan_seeding_mode.sql new file mode 100644 index 0000000..554079a --- /dev/null +++ b/database/migrations/V9__application_plan_seeding_mode.sql @@ -0,0 +1,10 @@ +-- Add application_type and seeding_mode to application_plans for seed-aware planning +ALTER TABLE application_plans + ADD COLUMN IF NOT EXISTS application_type VARCHAR(20) CHECK (application_type IN ('granular','liquid','seed')), + ADD COLUMN IF NOT EXISTS seeding_mode VARCHAR(20) CHECK (seeding_mode IN ('overseed','new_seed')); + +-- Helpful index for querying users' plans by type +CREATE INDEX IF NOT EXISTS idx_application_plans_user_type ON application_plans(user_id, application_type); + +SELECT 'Added application_type and seeding_mode to application_plans' as migration_status; + diff --git a/frontend/src/components/Applications/ApplicationPlanModal.js b/frontend/src/components/Applications/ApplicationPlanModal.js index 510132e..42a1c06 100644 --- a/frontend/src/components/Applications/ApplicationPlanModal.js +++ b/frontend/src/components/Applications/ApplicationPlanModal.js @@ -95,11 +95,12 @@ const ApplicationPlanModal = ({ setPlannedDate(editingPlan.plannedDate || new Date().toISOString().split('T')[0]); setNotes(editingPlan.notes || ''); - // Determine application type + // Determine application type (prefer plan fields) const ptypes = (editingPlan.products || []).map(p => (p.productType || '').toLowerCase()); - const derivedType = ptypes.includes('liquid') ? 'liquid' : (ptypes.includes('seed') ? 'seed' : 'granular'); - setApplicationType(derivedType); - if (derivedType === 'seed') setSeedMode('overseed'); + const inferred = ptypes.includes('liquid') ? 'liquid' : (ptypes.includes('seed') ? 'seed' : 'granular'); + const t = (editingPlan.applicationType || inferred || 'granular').toLowerCase(); + setApplicationType(t); + if (t === 'seed') setSeedMode(editingPlan.seedingMode || 'overseed'); // Map products into modal structure const mapped = (editingPlan.products || []).map(p => ({ @@ -531,6 +532,7 @@ const ApplicationPlanModal = ({ equipmentId: selectedEquipmentId, nozzleId: selectedNozzleId || null, applicationType, + seedingMode: applicationType === 'seed' ? seedMode : null, plannedDate, notes: applicationType === 'seed' ? `${notes || ''} [Seeding: ${seedMode.replace('_',' ')}]`.trim() : notes }; @@ -793,8 +795,8 @@ const ApplicationPlanModal = ({ let rateSet = false; - // For shared products: check rates array - if (selectedProduct.isShared && selectedProduct.rates && selectedProduct.rates.length > 0) { + // If rates exist (shared or custom with base rates), use them + if (selectedProduct.rates && selectedProduct.rates.length > 0) { let chosen = selectedProduct.rates[0]; if (applicationType === 'seed') { const picked = pickSeedRate(selectedProduct.rates, seedMode); diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js index 2fbceb0..8f53e1a 100644 --- a/frontend/src/pages/Applications/Applications.js +++ b/frontend/src/pages/Applications/Applications.js @@ -842,6 +842,8 @@ const Applications = () => { ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0], notes: planData.notes || '', + applicationType: planData.applicationType, + seedingMode: planData.seedingMode || null, areaSquareFeet: totalAreaSquareFeet, equipment: { id: selectedEquipment?.id, @@ -899,6 +901,8 @@ const Applications = () => { ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), plannedDate: new Date().toISOString().split('T')[0], notes: planData.notes || '', + applicationType: planData.applicationType, + seedingMode: planData.seedingMode || null, areaSquareFeet: totalAreaSquareFeet, equipment: { id: selectedEquipment?.id,