seeding app

This commit is contained in:
Jake Kasper
2025-09-04 10:00:27 -05:00
parent 9d8b1a385c
commit 3ddbc32fe0
6 changed files with 60 additions and 13 deletions

View File

@@ -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];

View File

@@ -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

View File

@@ -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(),

View 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;

View File

@@ -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);

View File

@@ -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,