seeding app
This commit is contained in:
@@ -192,6 +192,8 @@ router.get('/plans', async (req, res, next) => {
|
|||||||
status: plan.status,
|
status: plan.status,
|
||||||
plannedDate: plan.planned_date,
|
plannedDate: plan.planned_date,
|
||||||
notes: plan.notes,
|
notes: plan.notes,
|
||||||
|
applicationType: plan.application_type,
|
||||||
|
seedingMode: plan.seeding_mode,
|
||||||
sectionNames: plan.section_names, // Multiple section names comma-separated
|
sectionNames: plan.section_names, // Multiple section names comma-separated
|
||||||
sectionCount: parseInt(plan.section_count),
|
sectionCount: parseInt(plan.section_count),
|
||||||
totalSectionArea: parseFloat(plan.total_section_area),
|
totalSectionArea: parseFloat(plan.total_section_area),
|
||||||
@@ -296,6 +298,8 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) =
|
|||||||
status: plan.status,
|
status: plan.status,
|
||||||
plannedDate: plan.planned_date,
|
plannedDate: plan.planned_date,
|
||||||
notes: plan.notes,
|
notes: plan.notes,
|
||||||
|
applicationType: plan.application_type,
|
||||||
|
seedingMode: plan.seeding_mode,
|
||||||
sections: sections, // Array of sections instead of single section
|
sections: sections, // Array of sections instead of single section
|
||||||
totalArea: totalArea,
|
totalArea: totalArea,
|
||||||
property: {
|
property: {
|
||||||
@@ -353,7 +357,9 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
products,
|
products,
|
||||||
areaSquareFeet,
|
areaSquareFeet,
|
||||||
equipment,
|
equipment,
|
||||||
nozzle
|
nozzle,
|
||||||
|
applicationType: planApplicationType,
|
||||||
|
seedingMode
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// Handle both single and multiple lawn sections
|
// 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)
|
// Create application plan (no longer has lawn_section_id column)
|
||||||
const planResult = await client.query(
|
const planResult = await client.query(
|
||||||
`INSERT INTO application_plans (user_id, equipment_id, nozzle_id, planned_date, notes)
|
`INSERT INTO application_plans (user_id, equipment_id, nozzle_id, planned_date, notes, application_type, seeding_mode)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[req.user.id, equipmentId, nozzleId, plannedDate, notes]
|
[req.user.id, equipmentId, nozzleId, plannedDate, notes, planApplicationType || null, seedingMode || null]
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = planResult.rows[0];
|
const plan = planResult.rows[0];
|
||||||
@@ -686,10 +692,11 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat
|
|||||||
const updateResult = await client.query(
|
const updateResult = await client.query(
|
||||||
`UPDATE application_plans
|
`UPDATE application_plans
|
||||||
SET equipment_id = $1, nozzle_id = $2,
|
SET equipment_id = $1, nozzle_id = $2,
|
||||||
planned_date = $3, notes = $4, updated_at = CURRENT_TIMESTAMP
|
planned_date = $3, notes = $4, application_type = $5, seeding_mode = $6,
|
||||||
WHERE id = $5
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $7
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[equipmentId, nozzleId, plannedDate, notes, planId]
|
[equipmentId, nozzleId, plannedDate, notes, req.body.applicationType || null, req.body.seedingMode || null, planId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = updateResult.rows[0];
|
const plan = updateResult.rows[0];
|
||||||
|
|||||||
@@ -99,6 +99,27 @@ router.get('/', async (req, res, next) => {
|
|||||||
|
|
||||||
const userResult = await pool.query(userProductsQuery, [req.user.id]);
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@@ -133,6 +154,7 @@ router.get('/', async (req, res, next) => {
|
|||||||
customRateAmount: parseFloat(product.custom_rate_amount),
|
customRateAmount: parseFloat(product.custom_rate_amount),
|
||||||
customRateUnit: product.custom_rate_unit,
|
customRateUnit: product.custom_rate_unit,
|
||||||
notes: product.notes,
|
notes: product.notes,
|
||||||
|
rates: baseRatesById[product.product_id] || [],
|
||||||
isShared: false,
|
isShared: false,
|
||||||
createdAt: product.created_at,
|
createdAt: product.created_at,
|
||||||
updatedAt: product.updated_at
|
updatedAt: product.updated_at
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ const applicationPlanSchema = Joi.object({
|
|||||||
nozzleId: Joi.number().integer().positive().optional(),
|
nozzleId: Joi.number().integer().positive().optional(),
|
||||||
plannedDate: Joi.date().required(),
|
plannedDate: Joi.date().required(),
|
||||||
notes: Joi.string().allow('').optional(),
|
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(),
|
areaSquareFeet: Joi.number().positive().optional(),
|
||||||
equipment: Joi.object({
|
equipment: Joi.object({
|
||||||
id: Joi.number().integer().positive().optional(),
|
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]);
|
setPlannedDate(editingPlan.plannedDate || new Date().toISOString().split('T')[0]);
|
||||||
setNotes(editingPlan.notes || '');
|
setNotes(editingPlan.notes || '');
|
||||||
|
|
||||||
// Determine application type
|
// Determine application type (prefer plan fields)
|
||||||
const ptypes = (editingPlan.products || []).map(p => (p.productType || '').toLowerCase());
|
const ptypes = (editingPlan.products || []).map(p => (p.productType || '').toLowerCase());
|
||||||
const derivedType = ptypes.includes('liquid') ? 'liquid' : (ptypes.includes('seed') ? 'seed' : 'granular');
|
const inferred = ptypes.includes('liquid') ? 'liquid' : (ptypes.includes('seed') ? 'seed' : 'granular');
|
||||||
setApplicationType(derivedType);
|
const t = (editingPlan.applicationType || inferred || 'granular').toLowerCase();
|
||||||
if (derivedType === 'seed') setSeedMode('overseed');
|
setApplicationType(t);
|
||||||
|
if (t === 'seed') setSeedMode(editingPlan.seedingMode || 'overseed');
|
||||||
|
|
||||||
// Map products into modal structure
|
// Map products into modal structure
|
||||||
const mapped = (editingPlan.products || []).map(p => ({
|
const mapped = (editingPlan.products || []).map(p => ({
|
||||||
@@ -531,6 +532,7 @@ const ApplicationPlanModal = ({
|
|||||||
equipmentId: selectedEquipmentId,
|
equipmentId: selectedEquipmentId,
|
||||||
nozzleId: selectedNozzleId || null,
|
nozzleId: selectedNozzleId || null,
|
||||||
applicationType,
|
applicationType,
|
||||||
|
seedingMode: applicationType === 'seed' ? seedMode : null,
|
||||||
plannedDate,
|
plannedDate,
|
||||||
notes: applicationType === 'seed' ? `${notes || ''} [Seeding: ${seedMode.replace('_',' ')}]`.trim() : notes
|
notes: applicationType === 'seed' ? `${notes || ''} [Seeding: ${seedMode.replace('_',' ')}]`.trim() : notes
|
||||||
};
|
};
|
||||||
@@ -793,8 +795,8 @@ const ApplicationPlanModal = ({
|
|||||||
|
|
||||||
let rateSet = false;
|
let rateSet = false;
|
||||||
|
|
||||||
// For shared products: check rates array
|
// If rates exist (shared or custom with base rates), use them
|
||||||
if (selectedProduct.isShared && selectedProduct.rates && selectedProduct.rates.length > 0) {
|
if (selectedProduct.rates && selectedProduct.rates.length > 0) {
|
||||||
let chosen = selectedProduct.rates[0];
|
let chosen = selectedProduct.rates[0];
|
||||||
if (applicationType === 'seed') {
|
if (applicationType === 'seed') {
|
||||||
const picked = pickSeedRate(selectedProduct.rates, seedMode);
|
const picked = pickSeedRate(selectedProduct.rates, seedMode);
|
||||||
|
|||||||
@@ -842,6 +842,8 @@ const Applications = () => {
|
|||||||
...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }),
|
...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }),
|
||||||
plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0],
|
plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0],
|
||||||
notes: planData.notes || '',
|
notes: planData.notes || '',
|
||||||
|
applicationType: planData.applicationType,
|
||||||
|
seedingMode: planData.seedingMode || null,
|
||||||
areaSquareFeet: totalAreaSquareFeet,
|
areaSquareFeet: totalAreaSquareFeet,
|
||||||
equipment: {
|
equipment: {
|
||||||
id: selectedEquipment?.id,
|
id: selectedEquipment?.id,
|
||||||
@@ -899,6 +901,8 @@ const Applications = () => {
|
|||||||
...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }),
|
...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }),
|
||||||
plannedDate: new Date().toISOString().split('T')[0],
|
plannedDate: new Date().toISOString().split('T')[0],
|
||||||
notes: planData.notes || '',
|
notes: planData.notes || '',
|
||||||
|
applicationType: planData.applicationType,
|
||||||
|
seedingMode: planData.seedingMode || null,
|
||||||
areaSquareFeet: totalAreaSquareFeet,
|
areaSquareFeet: totalAreaSquareFeet,
|
||||||
equipment: {
|
equipment: {
|
||||||
id: selectedEquipment?.id,
|
id: selectedEquipment?.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user