seeding app
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
10
database/migrations/V9__application_plan_seeding_mode.sql
Normal file
10
database/migrations/V9__application_plan_seeding_mode.sql
Normal file
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user