Merge pull request #3 from vespo92/claude/complete-agent-tasks-01Rj86JxKG7JWx1X8UgZFFPg

Complete tasks from Agent Report
This commit is contained in:
Vinnie Esposito 2025-11-22 12:45:22 -06:00 committed by GitHub
commit d76b079e49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 2033 additions and 1 deletions

1
.gitignore vendored
View file

@ -40,3 +40,4 @@ bun-debug.log*
cypress/screenshots
cypress/videos
tsconfig.tsbuildinfo

View file

@ -0,0 +1,48 @@
/**
* API Route: Get demand forecast
* GET /api/demand/forecast
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getDemandForecaster } from '../../../lib/demand/forecaster';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { regionName, forecastWeeks } = req.query;
// Validate required fields
if (!regionName) {
return res.status(400).json({
success: false,
error: 'Missing required query parameter: regionName'
});
}
const weeks = forecastWeeks ? parseInt(forecastWeeks as string, 10) : 12;
if (isNaN(weeks) || weeks < 1 || weeks > 52) {
return res.status(400).json({
success: false,
error: 'forecastWeeks must be a number between 1 and 52'
});
}
const forecaster = getDemandForecaster();
const forecast = forecaster.generateForecast(regionName as string, weeks);
res.status(200).json({
success: true,
data: forecast
});
} catch (error: any) {
console.error('Error generating forecast:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

71
pages/api/demand/match.ts Normal file
View file

@ -0,0 +1,71 @@
/**
* API Route: Create market match
* POST /api/demand/match
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getDemandForecaster } from '../../../lib/demand/forecaster';
import { MarketMatch } from '../../../lib/demand/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
demandSignalId,
supplyCommitmentId,
consumerId,
growerId,
produceType,
matchedQuantityKg,
agreedPricePerKg,
currency,
deliveryDate,
deliveryMethod,
deliveryLocation
} = req.body;
// Validate required fields
if (!demandSignalId || !supplyCommitmentId || !consumerId || !growerId || !produceType || !matchedQuantityKg || !agreedPricePerKg || !deliveryDate || !deliveryMethod || !deliveryLocation) {
return res.status(400).json({
success: false,
error: 'Missing required fields: demandSignalId, supplyCommitmentId, consumerId, growerId, produceType, matchedQuantityKg, agreedPricePerKg, deliveryDate, deliveryMethod, deliveryLocation'
});
}
const match: MarketMatch = {
id: `match-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
demandSignalId,
supplyCommitmentId,
consumerId,
growerId,
produceType,
matchedQuantityKg,
agreedPricePerKg,
totalPrice: matchedQuantityKg * agreedPricePerKg,
currency: currency || 'USD',
deliveryDate,
deliveryMethod,
deliveryLocation,
status: 'pending'
};
// Store the match (in a real implementation, this would update the forecaster's internal state)
// For now, we'll just return the match
// The forecaster would track this to update supply commitment remaining quantities
res.status(201).json({
success: true,
data: match
});
} catch (error: any) {
console.error('Error creating market match:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,94 @@
/**
* API Route: Consumer preferences
* POST - Register/update consumer preferences
* GET - Get consumer preferences
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getDemandForecaster } from '../../../lib/demand/forecaster';
import { ConsumerPreference } from '../../../lib/demand/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const forecaster = getDemandForecaster();
if (req.method === 'POST') {
try {
const preference = req.body as ConsumerPreference;
// Validate required fields
if (!preference.consumerId || !preference.location || !preference.preferredItems) {
return res.status(400).json({
success: false,
error: 'Missing required fields: consumerId, location, preferredItems'
});
}
// Add timestamps
const now = new Date().toISOString();
preference.createdAt = preference.createdAt || now;
preference.updatedAt = now;
// Set defaults
preference.householdSize = preference.householdSize || 1;
preference.dietaryType = preference.dietaryType || ['omnivore'];
preference.allergies = preference.allergies || [];
preference.dislikes = preference.dislikes || [];
preference.preferredCategories = preference.preferredCategories || [];
preference.certificationPreferences = preference.certificationPreferences || [];
preference.freshnessImportance = preference.freshnessImportance || 3;
preference.priceImportance = preference.priceImportance || 3;
preference.sustainabilityImportance = preference.sustainabilityImportance || 3;
preference.deliveryPreferences = preference.deliveryPreferences || {
method: ['home_delivery'],
frequency: 'weekly',
preferredDays: ['saturday']
};
forecaster.registerPreference(preference);
res.status(201).json({
success: true,
data: preference
});
} catch (error: any) {
console.error('Error registering preference:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
} else if (req.method === 'GET') {
try {
const { consumerId } = req.query;
if (!consumerId || typeof consumerId !== 'string') {
return res.status(400).json({
success: false,
error: 'Consumer ID is required'
});
}
// Access internal state for lookup
const state = forecaster.toJSON() as any;
const preferences = state.preferences as [string, ConsumerPreference][];
const preference = preferences.find(([id]) => id === consumerId);
if (!preference) {
return res.status(404).json({
success: false,
error: `Preference not found for consumer: ${consumerId}`
});
}
res.status(200).json({
success: true,
data: preference[1]
});
} catch (error: any) {
console.error('Error fetching preference:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
} else {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
}

View file

@ -0,0 +1,67 @@
/**
* API Route: Get planting recommendations
* GET /api/demand/recommendations
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getDemandForecaster } from '../../../lib/demand/forecaster';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
growerId,
latitude,
longitude,
radiusKm,
availableSpaceSqm,
season
} = req.query;
// Validate required fields
if (!growerId || !latitude || !longitude || !radiusKm || !availableSpaceSqm || !season) {
return res.status(400).json({
success: false,
error: 'Missing required query parameters: growerId, latitude, longitude, radiusKm, availableSpaceSqm, season'
});
}
// Validate season
const validSeasons = ['spring', 'summer', 'fall', 'winter'];
if (!validSeasons.includes(season as string)) {
return res.status(400).json({
success: false,
error: 'Invalid season. Must be one of: spring, summer, fall, winter'
});
}
const forecaster = getDemandForecaster();
const recommendations = forecaster.generatePlantingRecommendations(
growerId as string,
parseFloat(latitude as string),
parseFloat(longitude as string),
parseFloat(radiusKm as string),
parseFloat(availableSpaceSqm as string),
season as 'spring' | 'summer' | 'fall' | 'winter'
);
res.status(200).json({
success: true,
data: {
growerId,
season,
totalRecommendations: recommendations.length,
recommendations
}
});
} catch (error: any) {
console.error('Error generating recommendations:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,60 @@
/**
* API Route: Generate demand signal
* POST /api/demand/signal
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getDemandForecaster } from '../../../lib/demand/forecaster';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
centerLat,
centerLon,
radiusKm,
regionName,
season
} = req.body;
// Validate required fields
if (centerLat === undefined || centerLon === undefined || !radiusKm || !regionName || !season) {
return res.status(400).json({
success: false,
error: 'Missing required fields: centerLat, centerLon, radiusKm, regionName, season'
});
}
// Validate season
const validSeasons = ['spring', 'summer', 'fall', 'winter'];
if (!validSeasons.includes(season)) {
return res.status(400).json({
success: false,
error: 'Invalid season. Must be one of: spring, summer, fall, winter'
});
}
const forecaster = getDemandForecaster();
const signal = forecaster.generateDemandSignal(
centerLat,
centerLon,
radiusKm,
regionName,
season
);
res.status(201).json({
success: true,
data: signal
});
} catch (error: any) {
console.error('Error generating demand signal:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,78 @@
/**
* API Route: Register supply commitment
* POST /api/demand/supply
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getDemandForecaster } from '../../../lib/demand/forecaster';
import { SupplyCommitment } from '../../../lib/demand/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
growerId,
produceType,
variety,
committedQuantityKg,
availableFrom,
availableUntil,
pricePerKg,
currency,
minimumOrderKg,
certifications,
freshnessGuaranteeHours,
deliveryRadiusKm,
deliveryMethods,
bulkDiscountThreshold,
bulkDiscountPercent
} = req.body;
// Validate required fields
if (!growerId || !produceType || !committedQuantityKg || !availableFrom || !availableUntil || !pricePerKg) {
return res.status(400).json({
success: false,
error: 'Missing required fields: growerId, produceType, committedQuantityKg, availableFrom, availableUntil, pricePerKg'
});
}
const commitment: SupplyCommitment = {
id: `supply-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
growerId,
timestamp: new Date().toISOString(),
produceType,
variety,
committedQuantityKg,
availableFrom,
availableUntil,
pricePerKg,
currency: currency || 'USD',
minimumOrderKg: minimumOrderKg || 0.5,
bulkDiscountThreshold,
bulkDiscountPercent,
certifications: certifications || [],
freshnessGuaranteeHours: freshnessGuaranteeHours || 24,
deliveryRadiusKm: deliveryRadiusKm || 50,
deliveryMethods: deliveryMethods || ['customer_pickup', 'grower_delivery'],
status: 'available',
remainingKg: committedQuantityKg
};
const forecaster = getDemandForecaster();
forecaster.registerSupply(commitment);
res.status(201).json({
success: true,
data: commitment
});
} catch (error: any) {
console.error('Error registering supply commitment:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,93 @@
/**
* API Route: Record distribution event
* POST /api/transport/distribution
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTransportChain } from '../../../lib/transport/tracker';
import { DistributionEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
batchIds,
destinationType,
orderId,
customerType,
deliveryWindow,
fromLocation,
toLocation,
transportMethod,
senderId,
receiverId,
distanceKm,
durationMinutes,
actualDeliveryTime,
deliveryAttempts,
handoffVerified,
recipientName,
notes
} = req.body;
// Validate required fields
if (!batchIds || !destinationType || !customerType || !deliveryWindow || !fromLocation || !toLocation || !senderId || !receiverId) {
return res.status(400).json({
success: false,
error: 'Missing required fields: batchIds, destinationType, customerType, deliveryWindow, fromLocation, toLocation, senderId, receiverId'
});
}
const transportChain = getTransportChain();
const event: DistributionEvent = {
id: `dist-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
eventType: 'distribution',
batchIds: Array.isArray(batchIds) ? batchIds : [batchIds],
destinationType,
orderId,
customerType,
deliveryWindow,
actualDeliveryTime,
deliveryAttempts: deliveryAttempts || 1,
handoffVerified: handoffVerified || false,
recipientName,
fromLocation: fromLocation as TransportLocation,
toLocation: toLocation as TransportLocation,
distanceKm: distanceKm || 0,
durationMinutes: durationMinutes || 0,
transportMethod: (transportMethod as TransportMethod) || 'local_delivery',
carbonFootprintKg: 0,
senderId,
receiverId,
status: actualDeliveryTime ? 'delivered' : 'in_transit',
notes
};
const block = transportChain.recordEvent(event);
res.status(201).json({
success: true,
data: {
event,
block: {
index: block.index,
hash: block.hash,
timestamp: block.timestamp,
cumulativeCarbonKg: block.cumulativeCarbonKg,
cumulativeFoodMiles: block.cumulativeFoodMiles
}
}
});
} catch (error: any) {
console.error('Error recording distribution event:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,38 @@
/**
* API Route: Get environmental impact for a user
* GET /api/transport/footprint/[userId]
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTransportChain } from '../../../../lib/transport/tracker';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { userId } = req.query;
if (!userId || typeof userId !== 'string') {
return res.status(400).json({
success: false,
error: 'User ID is required'
});
}
const transportChain = getTransportChain();
const impact = transportChain.getEnvironmentalImpact(userId);
res.status(200).json({
success: true,
data: impact
});
} catch (error: any) {
console.error('Error fetching environmental impact:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,89 @@
/**
* API Route: Record growing transport event
* POST /api/transport/growing
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTransportChain } from '../../../lib/transport/tracker';
import { GrowingTransportEvent, TransportLocation, TransportMethod, PlantStage } from '../../../lib/transport/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
plantIds,
reason,
plantStage,
handlingMethod,
fromLocation,
toLocation,
transportMethod,
senderId,
receiverId,
distanceKm,
durationMinutes,
rootDisturbance,
acclimatizationRequired,
acclimatizationDays,
notes
} = req.body;
// Validate required fields
if (!plantIds || !reason || !plantStage || !handlingMethod || !fromLocation || !toLocation || !senderId || !receiverId) {
return res.status(400).json({
success: false,
error: 'Missing required fields: plantIds, reason, plantStage, handlingMethod, fromLocation, toLocation, senderId, receiverId'
});
}
const transportChain = getTransportChain();
const event: GrowingTransportEvent = {
id: `growing-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
eventType: 'growing_transport',
plantIds: Array.isArray(plantIds) ? plantIds : [plantIds],
reason,
plantStage: plantStage as PlantStage,
handlingMethod,
fromLocation: fromLocation as TransportLocation,
toLocation: toLocation as TransportLocation,
distanceKm: distanceKm || 0,
durationMinutes: durationMinutes || 0,
transportMethod: (transportMethod as TransportMethod) || 'walking',
carbonFootprintKg: 0,
senderId,
receiverId,
status: 'verified',
rootDisturbance: rootDisturbance || 'minimal',
acclimatizationRequired: acclimatizationRequired || false,
acclimatizationDays,
notes
};
const block = transportChain.recordEvent(event);
res.status(201).json({
success: true,
data: {
event,
block: {
index: block.index,
hash: block.hash,
timestamp: block.timestamp,
cumulativeCarbonKg: block.cumulativeCarbonKg,
cumulativeFoodMiles: block.cumulativeFoodMiles
}
}
});
} catch (error: any) {
console.error('Error recording growing transport:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,103 @@
/**
* API Route: Record harvest event
* POST /api/transport/harvest
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTransportChain } from '../../../lib/transport/tracker';
import { HarvestEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
plantIds,
harvestBatchId,
harvestType,
produceType,
grossWeight,
netWeight,
weightUnit,
itemCount,
qualityGrade,
packagingType,
temperatureRequired,
shelfLifeHours,
fromLocation,
toLocation,
transportMethod,
senderId,
receiverId,
distanceKm,
durationMinutes,
seedsSaved,
seedBatchIdCreated,
notes
} = req.body;
// Validate required fields
if (!plantIds || !harvestBatchId || !harvestType || !produceType || !grossWeight || !netWeight || !fromLocation || !toLocation || !senderId || !receiverId) {
return res.status(400).json({
success: false,
error: 'Missing required fields: plantIds, harvestBatchId, harvestType, produceType, grossWeight, netWeight, fromLocation, toLocation, senderId, receiverId'
});
}
const transportChain = getTransportChain();
const event: HarvestEvent = {
id: `harvest-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
eventType: 'harvest',
plantIds: Array.isArray(plantIds) ? plantIds : [plantIds],
harvestBatchId,
harvestType,
produceType,
grossWeight,
netWeight,
weightUnit: weightUnit || 'kg',
itemCount,
qualityGrade,
packagingType: packagingType || 'bulk',
temperatureRequired: temperatureRequired || { min: 2, max: 8, optimal: 4, unit: 'celsius' },
shelfLifeHours: shelfLifeHours || 168,
fromLocation: fromLocation as TransportLocation,
toLocation: toLocation as TransportLocation,
distanceKm: distanceKm || 0,
durationMinutes: durationMinutes || 0,
transportMethod: (transportMethod as TransportMethod) || 'walking',
carbonFootprintKg: 0,
senderId,
receiverId,
status: 'verified',
seedsSaved: seedsSaved || false,
seedBatchIdCreated,
notes
};
const block = transportChain.recordEvent(event);
res.status(201).json({
success: true,
data: {
event,
block: {
index: block.index,
hash: block.hash,
timestamp: block.timestamp,
cumulativeCarbonKg: block.cumulativeCarbonKg,
cumulativeFoodMiles: block.cumulativeFoodMiles
}
}
});
} catch (error: any) {
console.error('Error recording harvest event:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,45 @@
/**
* API Route: Get plant journey
* GET /api/transport/journey/[plantId]
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTransportChain } from '../../../../lib/transport/tracker';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { plantId } = req.query;
if (!plantId || typeof plantId !== 'string') {
return res.status(400).json({
success: false,
error: 'Plant ID is required'
});
}
const transportChain = getTransportChain();
const journey = transportChain.getPlantJourney(plantId);
if (!journey) {
return res.status(404).json({
success: false,
error: `No journey found for plant: ${plantId}`
});
}
res.status(200).json({
success: true,
data: journey
});
} catch (error: any) {
console.error('Error fetching plant journey:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,91 @@
/**
* API Route: Record planting event
* POST /api/transport/planting
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTransportChain } from '../../../lib/transport/tracker';
import { PlantingEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
seedBatchId,
plantIds,
plantingMethod,
quantityPlanted,
growingEnvironment,
fromLocation,
toLocation,
transportMethod,
senderId,
receiverId,
distanceKm,
durationMinutes,
sowingDepth,
spacing,
expectedHarvestDate,
notes
} = req.body;
// Validate required fields
if (!seedBatchId || !plantIds || !plantingMethod || !quantityPlanted || !fromLocation || !toLocation || !senderId || !receiverId) {
return res.status(400).json({
success: false,
error: 'Missing required fields: seedBatchId, plantIds, plantingMethod, quantityPlanted, fromLocation, toLocation, senderId, receiverId'
});
}
const transportChain = getTransportChain();
const event: PlantingEvent = {
id: `planting-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
eventType: 'planting',
seedBatchId,
plantIds: Array.isArray(plantIds) ? plantIds : [plantIds],
plantingMethod,
quantityPlanted,
growingEnvironment: growingEnvironment || 'outdoor',
fromLocation: fromLocation as TransportLocation,
toLocation: toLocation as TransportLocation,
distanceKm: distanceKm || 0,
durationMinutes: durationMinutes || 0,
transportMethod: (transportMethod as TransportMethod) || 'walking',
carbonFootprintKg: 0,
senderId,
receiverId,
status: 'verified',
sowingDepth,
spacing,
expectedHarvestDate,
notes
};
const block = transportChain.recordEvent(event);
res.status(201).json({
success: true,
data: {
event,
block: {
index: block.index,
hash: block.hash,
timestamp: block.timestamp,
cumulativeCarbonKg: block.cumulativeCarbonKg,
cumulativeFoodMiles: block.cumulativeFoodMiles
}
}
});
} catch (error: any) {
console.error('Error recording planting event:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,48 @@
/**
* API Route: Generate QR code data
* GET /api/transport/qr/[id]
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTransportChain } from '../../../../lib/transport/tracker';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { id, type } = req.query;
if (!id || typeof id !== 'string') {
return res.status(400).json({
success: false,
error: 'ID is required'
});
}
const transportChain = getTransportChain();
// Determine if this is a plant ID or batch ID
const idType = type === 'batch' ? 'batch' : 'plant';
const qrData = idType === 'batch'
? transportChain.generateQRData(undefined, id)
: transportChain.generateQRData(id, undefined);
res.status(200).json({
success: true,
data: {
...qrData,
qrContent: JSON.stringify(qrData),
scanUrl: qrData.quickLookupUrl
}
});
} catch (error: any) {
console.error('Error generating QR data:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,101 @@
/**
* API Route: Record seed acquisition event
* POST /api/transport/seed-acquisition
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTransportChain } from '../../../lib/transport/tracker';
import { SeedAcquisitionEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
seedBatchId,
sourceType,
species,
variety,
quantity,
quantityUnit,
generation,
fromLocation,
toLocation,
transportMethod,
senderId,
receiverId,
distanceKm,
durationMinutes,
germinationRate,
certifications,
notes
} = req.body;
// Validate required fields
if (!seedBatchId || !sourceType || !species || !quantity || !fromLocation || !toLocation || !senderId || !receiverId) {
return res.status(400).json({
success: false,
error: 'Missing required fields: seedBatchId, sourceType, species, quantity, fromLocation, toLocation, senderId, receiverId'
});
}
const transportChain = getTransportChain();
// Calculate distance if not provided
const calculatedDistance = distanceKm ??
(fromLocation && toLocation ?
Math.sqrt(
Math.pow((toLocation.latitude - fromLocation.latitude) * 111, 2) +
Math.pow((toLocation.longitude - fromLocation.longitude) * 111 * Math.cos(fromLocation.latitude * Math.PI / 180), 2)
) : 0);
const event: SeedAcquisitionEvent = {
id: `seed-acq-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
eventType: 'seed_acquisition',
seedBatchId,
sourceType,
species,
variety,
quantity,
quantityUnit: quantityUnit || 'seeds',
generation: generation || 0,
fromLocation: fromLocation as TransportLocation,
toLocation: toLocation as TransportLocation,
distanceKm: calculatedDistance,
durationMinutes: durationMinutes || 0,
transportMethod: (transportMethod as TransportMethod) || 'local_delivery',
carbonFootprintKg: 0, // Will be calculated by tracker
senderId,
receiverId,
status: 'verified',
germinationRate,
certifications,
notes
};
const block = transportChain.recordEvent(event);
res.status(201).json({
success: true,
data: {
event,
block: {
index: block.index,
hash: block.hash,
timestamp: block.timestamp,
cumulativeCarbonKg: block.cumulativeCarbonKg,
cumulativeFoodMiles: block.cumulativeFoodMiles
}
}
});
} catch (error: any) {
console.error('Error recording seed acquisition:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,101 @@
/**
* API Route: Record seed saving event
* POST /api/transport/seed-saving
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTransportChain } from '../../../lib/transport/tracker';
import { SeedSavingEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
parentPlantIds,
newSeedBatchId,
collectionMethod,
seedCount,
seedWeight,
seedWeightUnit,
storageConditions,
storageLocationId,
newGenerationNumber,
fromLocation,
toLocation,
transportMethod,
senderId,
receiverId,
distanceKm,
durationMinutes,
viabilityTestDate,
germinationRate,
availableForSharing,
sharingTerms,
notes
} = req.body;
// Validate required fields
if (!parentPlantIds || !newSeedBatchId || !collectionMethod || !storageConditions || !storageLocationId || !fromLocation || !toLocation || !senderId || !receiverId) {
return res.status(400).json({
success: false,
error: 'Missing required fields: parentPlantIds, newSeedBatchId, collectionMethod, storageConditions, storageLocationId, fromLocation, toLocation, senderId, receiverId'
});
}
const transportChain = getTransportChain();
const event: SeedSavingEvent = {
id: `seed-save-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
eventType: 'seed_saving',
parentPlantIds: Array.isArray(parentPlantIds) ? parentPlantIds : [parentPlantIds],
newSeedBatchId,
collectionMethod,
seedCount,
seedWeight,
seedWeightUnit,
storageConditions,
storageLocationId,
newGenerationNumber: newGenerationNumber || 1,
viabilityTestDate,
germinationRate,
availableForSharing: availableForSharing || false,
sharingTerms,
fromLocation: fromLocation as TransportLocation,
toLocation: toLocation as TransportLocation,
distanceKm: distanceKm || 0,
durationMinutes: durationMinutes || 0,
transportMethod: (transportMethod as TransportMethod) || 'walking',
carbonFootprintKg: 0,
senderId,
receiverId,
status: 'verified',
notes
};
const block = transportChain.recordEvent(event);
res.status(201).json({
success: true,
data: {
event,
block: {
index: block.index,
hash: block.hash,
timestamp: block.timestamp,
cumulativeCarbonKg: block.cumulativeCarbonKg,
cumulativeFoodMiles: block.cumulativeFoodMiles
}
}
});
} catch (error: any) {
console.error('Error recording seed saving event:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,95 @@
/**
* API Route: Record seed sharing event
* POST /api/transport/seed-sharing
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTransportChain } from '../../../lib/transport/tracker';
import { SeedSharingEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
seedBatchId,
quantityShared,
quantityUnit,
sharingType,
fromLocation,
toLocation,
transportMethod,
senderId,
receiverId,
distanceKm,
durationMinutes,
tradeDetails,
saleAmount,
saleCurrency,
recipientAgreement,
growingCommitment,
reportBackRequired,
notes
} = req.body;
// Validate required fields
if (!seedBatchId || !quantityShared || !sharingType || !fromLocation || !toLocation || !senderId || !receiverId) {
return res.status(400).json({
success: false,
error: 'Missing required fields: seedBatchId, quantityShared, sharingType, fromLocation, toLocation, senderId, receiverId'
});
}
const transportChain = getTransportChain();
const event: SeedSharingEvent = {
id: `seed-share-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
eventType: 'seed_sharing',
seedBatchId,
quantityShared,
quantityUnit: quantityUnit || 'seeds',
sharingType,
tradeDetails,
saleAmount,
saleCurrency,
recipientAgreement: recipientAgreement || false,
growingCommitment,
reportBackRequired: reportBackRequired || false,
fromLocation: fromLocation as TransportLocation,
toLocation: toLocation as TransportLocation,
distanceKm: distanceKm || 0,
durationMinutes: durationMinutes || 0,
transportMethod: (transportMethod as TransportMethod) || 'local_delivery',
carbonFootprintKg: 0,
senderId,
receiverId,
status: 'verified',
notes
};
const block = transportChain.recordEvent(event);
res.status(201).json({
success: true,
data: {
event,
block: {
index: block.index,
hash: block.hash,
timestamp: block.timestamp,
cumulativeCarbonKg: block.cumulativeCarbonKg,
cumulativeFoodMiles: block.cumulativeFoodMiles
}
}
});
} catch (error: any) {
console.error('Error recording seed sharing event:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,70 @@
/**
* API Route: Verify block integrity
* GET /api/transport/verify/[blockHash]
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTransportChain } from '../../../../lib/transport/tracker';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { blockHash } = req.query;
if (!blockHash || typeof blockHash !== 'string') {
return res.status(400).json({
success: false,
error: 'Block hash is required'
});
}
const transportChain = getTransportChain();
// Find the block with the given hash
const block = transportChain.chain.find(b => b.hash === blockHash);
if (!block) {
return res.status(404).json({
success: false,
error: `Block not found with hash: ${blockHash}`
});
}
// Verify chain integrity
const isChainValid = transportChain.isChainValid();
// Verify this specific block's position in chain
const blockIndex = transportChain.chain.findIndex(b => b.hash === blockHash);
const previousBlock = blockIndex > 0 ? transportChain.chain[blockIndex - 1] : null;
const isBlockValid = previousBlock ? block.previousHash === previousBlock.hash : block.index === 0;
res.status(200).json({
success: true,
data: {
block: {
index: block.index,
hash: block.hash,
previousHash: block.previousHash,
timestamp: block.timestamp,
eventType: block.transportEvent.eventType,
eventId: block.transportEvent.id
},
verification: {
isChainValid,
isBlockValid,
blockPosition: blockIndex,
totalBlocks: transportChain.chain.length
}
}
});
} catch (error: any) {
console.error('Error verifying block:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,56 @@
/**
* API Route: Get farm analytics
* GET /api/vertical-farm/[farmId]/analytics
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getVerticalFarmController } from '../../../../lib/vertical-farming/controller';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { farmId, periodDays } = req.query;
if (!farmId || typeof farmId !== 'string') {
return res.status(400).json({
success: false,
error: 'Farm ID is required'
});
}
const days = periodDays ? parseInt(periodDays as string, 10) : 30;
if (isNaN(days) || days < 1 || days > 365) {
return res.status(400).json({
success: false,
error: 'periodDays must be a number between 1 and 365'
});
}
const controller = getVerticalFarmController();
const farm = controller.getFarm(farmId);
if (!farm) {
return res.status(404).json({
success: false,
error: `Farm not found: ${farmId}`
});
}
const analytics = controller.generateAnalytics(farmId, days);
res.status(200).json({
success: true,
data: analytics
});
} catch (error: any) {
console.error('Error generating analytics:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,45 @@
/**
* API Route: Get farm details
* GET /api/vertical-farm/[farmId]
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getVerticalFarmController } from '../../../../lib/vertical-farming/controller';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { farmId } = req.query;
if (!farmId || typeof farmId !== 'string') {
return res.status(400).json({
success: false,
error: 'Farm ID is required'
});
}
const controller = getVerticalFarmController();
const farm = controller.getFarm(farmId);
if (!farm) {
return res.status(404).json({
success: false,
error: `Farm not found: ${farmId}`
});
}
res.status(200).json({
success: true,
data: farm
});
} catch (error: any) {
console.error('Error fetching farm:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,126 @@
/**
* API Route: Manage zones
* GET - List zones for a farm
* POST - Add a new zone
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getVerticalFarmController } from '../../../../lib/vertical-farming/controller';
import { GrowingZone, ZoneEnvironmentTargets, ZoneEnvironmentReadings } from '../../../../lib/vertical-farming/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { farmId } = req.query;
if (!farmId || typeof farmId !== 'string') {
return res.status(400).json({
success: false,
error: 'Farm ID is required'
});
}
const controller = getVerticalFarmController();
const farm = controller.getFarm(farmId);
if (!farm) {
return res.status(404).json({
success: false,
error: `Farm not found: ${farmId}`
});
}
if (req.method === 'GET') {
try {
res.status(200).json({
success: true,
data: {
farmId,
totalZones: farm.zones.length,
zones: farm.zones
}
});
} catch (error: any) {
console.error('Error fetching zones:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
} else if (req.method === 'POST') {
try {
const {
name,
level,
areaSqm,
lengthM,
widthM,
growingMethod,
plantPositions
} = req.body;
// Validate required fields
if (!name || level === undefined || !areaSqm || !growingMethod || !plantPositions) {
return res.status(400).json({
success: false,
error: 'Missing required fields: name, level, areaSqm, growingMethod, plantPositions'
});
}
const defaultTargets: ZoneEnvironmentTargets = {
temperatureC: { min: 18, max: 26, target: 22 },
humidityPercent: { min: 50, max: 80, target: 65 },
co2Ppm: { min: 400, max: 1500, target: 1000 },
lightPpfd: { min: 100, max: 500, target: 300 },
lightHours: 16,
nutrientEc: { min: 1.0, max: 2.0, target: 1.5 },
nutrientPh: { min: 5.5, max: 6.5, target: 6.0 },
waterTempC: { min: 18, max: 24, target: 20 }
};
const defaultReadings: ZoneEnvironmentReadings = {
timestamp: new Date().toISOString(),
temperatureC: 22,
humidityPercent: 65,
co2Ppm: 800,
vpd: 1.0,
ppfd: 300,
dli: 17,
waterTempC: 20,
ec: 1.5,
ph: 6.0,
dissolvedOxygenPpm: 8,
airflowMs: 0.5,
alerts: []
};
const zone: GrowingZone = {
id: `zone-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
name,
level,
areaSqm,
lengthM: lengthM || Math.sqrt(areaSqm),
widthM: widthM || Math.sqrt(areaSqm),
growingMethod,
plantPositions,
currentCrop: '',
plantIds: [],
plantingDate: '',
expectedHarvestDate: '',
environmentTargets: defaultTargets,
currentEnvironment: defaultReadings,
status: 'empty'
};
farm.zones.push(zone);
res.status(201).json({
success: true,
data: zone
});
} catch (error: any) {
console.error('Error adding zone:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
} else {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
}

View file

@ -0,0 +1,106 @@
/**
* API Route: Record environment reading
* PUT /api/vertical-farm/batch/[batchId]/environment
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getVerticalFarmController } from '../../../../../lib/vertical-farming/controller';
import { ZoneEnvironmentReadings } from '../../../../../lib/vertical-farming/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'PUT') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { batchId } = req.query;
const {
temperatureC,
humidityPercent,
co2Ppm,
ppfd,
waterTempC,
ec,
ph,
dissolvedOxygenPpm,
airflowMs
} = req.body;
if (!batchId || typeof batchId !== 'string') {
return res.status(400).json({
success: false,
error: 'Batch ID is required'
});
}
// Validate required readings
if (temperatureC === undefined || humidityPercent === undefined || ec === undefined || ph === undefined) {
return res.status(400).json({
success: false,
error: 'Missing required fields: temperatureC, humidityPercent, ec, ph'
});
}
const controller = getVerticalFarmController();
// Access internal state to find the batch
const state = controller.toJSON() as any;
const batches = state.batches as [string, any][];
const batchEntry = batches.find(([id]) => id === batchId);
if (!batchEntry) {
return res.status(404).json({
success: false,
error: `Batch not found: ${batchId}`
});
}
const batch = batchEntry[1];
const readings: ZoneEnvironmentReadings = {
timestamp: new Date().toISOString(),
temperatureC,
humidityPercent,
co2Ppm: co2Ppm || 800,
vpd: calculateVpd(temperatureC, humidityPercent),
ppfd: ppfd || 300,
dli: (ppfd || 300) * 16 * 3600 / 1000000, // Approximate DLI
waterTempC: waterTempC || 20,
ec,
ph,
dissolvedOxygenPpm: dissolvedOxygenPpm || 8,
airflowMs: airflowMs || 0.5,
alerts: []
};
const alerts = controller.recordEnvironment(batch.zoneId, readings);
res.status(200).json({
success: true,
data: {
readings,
alerts,
batchId,
zoneId: batch.zoneId
}
});
} catch (error: any) {
console.error('Error recording environment:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}
/**
* Calculate Vapor Pressure Deficit
*/
function calculateVpd(tempC: number, humidityPercent: number): number {
// Saturation vapor pressure (kPa)
const svp = 0.6108 * Math.exp((17.27 * tempC) / (tempC + 237.3));
// Actual vapor pressure
const avp = svp * (humidityPercent / 100);
// VPD
return Math.round((svp - avp) * 100) / 100;
}

View file

@ -0,0 +1,81 @@
/**
* API Route: Complete harvest
* POST /api/vertical-farm/batch/[batchId]/harvest
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getVerticalFarmController } from '../../../../../lib/vertical-farming/controller';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { batchId } = req.query;
const { actualYieldKg, qualityGrade } = req.body;
if (!batchId || typeof batchId !== 'string') {
return res.status(400).json({
success: false,
error: 'Batch ID is required'
});
}
// Validate required fields
if (actualYieldKg === undefined || !qualityGrade) {
return res.status(400).json({
success: false,
error: 'Missing required fields: actualYieldKg, qualityGrade'
});
}
// Validate quality grade
const validGrades = ['A', 'B', 'C', 'processing'];
if (!validGrades.includes(qualityGrade)) {
return res.status(400).json({
success: false,
error: 'Invalid qualityGrade. Must be one of: A, B, C, processing'
});
}
if (actualYieldKg < 0) {
return res.status(400).json({
success: false,
error: 'actualYieldKg must be a positive number'
});
}
const controller = getVerticalFarmController();
// Verify batch exists
const state = controller.toJSON() as any;
const batches = state.batches as [string, any][];
const batchEntry = batches.find(([id]) => id === batchId);
if (!batchEntry) {
return res.status(404).json({
success: false,
error: `Batch not found: ${batchId}`
});
}
const completedBatch = controller.completeHarvest(batchId, actualYieldKg, qualityGrade);
res.status(200).json({
success: true,
data: {
batch: completedBatch,
yieldEfficiency: completedBatch.expectedYieldKg > 0
? Math.round((actualYieldKg / completedBatch.expectedYieldKg) * 100)
: 0
}
});
} catch (error: any) {
console.error('Error completing harvest:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,52 @@
/**
* API Route: Get batch details
* GET /api/vertical-farm/batch/[batchId]
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getVerticalFarmController } from '../../../../../lib/vertical-farming/controller';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { batchId } = req.query;
if (!batchId || typeof batchId !== 'string') {
return res.status(400).json({
success: false,
error: 'Batch ID is required'
});
}
const controller = getVerticalFarmController();
// Access internal state to find the batch
const state = controller.toJSON() as any;
const batches = state.batches as [string, any][];
const batchEntry = batches.find(([id]) => id === batchId);
if (!batchEntry) {
return res.status(404).json({
success: false,
error: `Batch not found: ${batchId}`
});
}
// Update batch progress
const batch = controller.updateBatchProgress(batchId);
res.status(200).json({
success: true,
data: batch
});
} catch (error: any) {
console.error('Error fetching batch:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,81 @@
/**
* API Route: Start crop batch
* POST /api/vertical-farm/batch/start
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getVerticalFarmController } from '../../../../lib/vertical-farming/controller';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
farmId,
zoneId,
recipeId,
seedBatchId,
plantCount
} = req.body;
// Validate required fields
if (!farmId || !zoneId || !recipeId || !seedBatchId || !plantCount) {
return res.status(400).json({
success: false,
error: 'Missing required fields: farmId, zoneId, recipeId, seedBatchId, plantCount'
});
}
if (plantCount < 1) {
return res.status(400).json({
success: false,
error: 'plantCount must be at least 1'
});
}
const controller = getVerticalFarmController();
// Verify farm exists
const farm = controller.getFarm(farmId);
if (!farm) {
return res.status(404).json({
success: false,
error: `Farm not found: ${farmId}`
});
}
// Verify zone exists
const zone = farm.zones.find(z => z.id === zoneId);
if (!zone) {
return res.status(404).json({
success: false,
error: `Zone not found: ${zoneId}`
});
}
// Verify recipe exists
const recipes = controller.getRecipes();
const recipe = recipes.find(r => r.id === recipeId);
if (!recipe) {
return res.status(404).json({
success: false,
error: `Recipe not found: ${recipeId}`
});
}
const batch = controller.startCropBatch(farmId, zoneId, recipeId, seedBatchId, plantCount);
res.status(201).json({
success: true,
data: batch
});
} catch (error: any) {
console.error('Error starting crop batch:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,70 @@
/**
* API Route: List growing recipes
* GET /api/vertical-farm/recipes
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getVerticalFarmController } from '../../../lib/vertical-farming/controller';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const { cropType, source } = req.query;
const controller = getVerticalFarmController();
let recipes = controller.getRecipes();
// Filter by crop type if provided
if (cropType && typeof cropType === 'string') {
recipes = recipes.filter(r => r.cropType.toLowerCase() === cropType.toLowerCase());
}
// Filter by source if provided
if (source && typeof source === 'string') {
recipes = recipes.filter(r => r.source === source);
}
// Sort by rating (highest first) then by times used
recipes.sort((a, b) => {
if ((b.rating || 0) !== (a.rating || 0)) {
return (b.rating || 0) - (a.rating || 0);
}
return b.timesUsed - a.timesUsed;
});
res.status(200).json({
success: true,
data: {
totalRecipes: recipes.length,
recipes: recipes.map(recipe => ({
id: recipe.id,
name: recipe.name,
cropType: recipe.cropType,
variety: recipe.variety,
version: recipe.version,
expectedDays: recipe.expectedDays,
expectedYieldGrams: recipe.expectedYieldGrams,
expectedYieldPerSqm: recipe.expectedYieldPerSqm,
requirements: recipe.requirements,
source: recipe.source,
rating: recipe.rating,
timesUsed: recipe.timesUsed,
stages: recipe.stages.map(s => ({
name: s.name,
daysStart: s.daysStart,
daysEnd: s.daysEnd
}))
}))
}
});
} catch (error: any) {
console.error('Error fetching recipes:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,122 @@
/**
* API Route: Register new vertical farm
* POST /api/vertical-farm/register
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getVerticalFarmController } from '../../../lib/vertical-farming/controller';
import { VerticalFarm } from '../../../lib/vertical-farming/types';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
try {
const {
name,
ownerId,
location,
specs,
zones,
environmentalControl,
irrigationSystem,
lightingSystem,
nutrientSystem,
automationLevel,
automationSystems
} = req.body;
// Validate required fields
if (!name || !ownerId || !location || !specs) {
return res.status(400).json({
success: false,
error: 'Missing required fields: name, ownerId, location, specs'
});
}
const controller = getVerticalFarmController();
const farm: VerticalFarm = {
id: `farm-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
name,
ownerId,
location,
specs: {
...specs,
certifications: specs.certifications || [],
currentActivePlants: specs.currentActivePlants || 0
},
zones: zones || [],
environmentalControl: environmentalControl || {
hvacUnits: [],
co2Injection: { type: 'tank', capacityKg: 0, currentLevelKg: 0, injectionRateKgPerHour: 0, status: 'off' },
humidification: { type: 'ultrasonic', capacityLPerHour: 0, status: 'off', currentOutput: 0 },
airCirculation: { fans: [] },
controlMode: 'manual'
},
irrigationSystem: irrigationSystem || {
type: 'recirculating',
freshWaterTankL: 1000,
freshWaterLevelL: 500,
nutrientTankL: 500,
nutrientLevelL: 250,
wasteTankL: 200,
wasteLevelL: 0,
waterTreatment: { ro: false, uv: false, ozone: false, filtration: 'basic' },
pumps: [],
irrigationSchedule: []
},
lightingSystem: lightingSystem || {
type: 'LED',
fixtures: [],
lightSchedules: [],
totalWattage: 0,
currentWattage: 0,
efficacyUmolJ: 2.5
},
nutrientSystem: nutrientSystem || {
mixingMethod: 'manual',
stockSolutions: [],
dosingPumps: [],
currentRecipe: {
id: 'default',
name: 'Default Recipe',
cropType: 'general',
growthStage: 'vegetative',
targetEc: 1.5,
targetPh: 6.0,
ratios: { n: 150, p: 50, k: 200, ca: 200, mg: 50, s: 60, fe: 3, mn: 0.5, zn: 0.05, cu: 0.02, b: 0.5, mo: 0.01 },
dosingRatiosMlPerL: []
},
monitoring: {
ec: 0,
ph: 0,
lastCalibration: new Date().toISOString(),
calibrationDue: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
}
},
automationLevel: automationLevel || 'manual',
automationSystems: automationSystems || [],
status: 'operational',
operationalSince: new Date().toISOString(),
lastMaintenanceDate: new Date().toISOString(),
currentCapacityUtilization: 0,
averageYieldEfficiency: 0,
energyEfficiencyScore: 50
};
controller.registerFarm(farm);
res.status(201).json({
success: true,
data: farm
});
} catch (error: any) {
console.error('Error registering farm:', error);
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
}
}