Merge pull request #3 from vespo92/claude/complete-agent-tasks-01Rj86JxKG7JWx1X8UgZFFPg
Complete tasks from Agent Report
This commit is contained in:
commit
d76b079e49
27 changed files with 2033 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -40,3 +40,4 @@ bun-debug.log*
|
||||||
|
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
cypress/videos
|
cypress/videos
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
|
||||||
48
pages/api/demand/forecast.ts
Normal file
48
pages/api/demand/forecast.ts
Normal 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
71
pages/api/demand/match.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
94
pages/api/demand/preferences.ts
Normal file
94
pages/api/demand/preferences.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
67
pages/api/demand/recommendations.ts
Normal file
67
pages/api/demand/recommendations.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
60
pages/api/demand/signal.ts
Normal file
60
pages/api/demand/signal.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
78
pages/api/demand/supply.ts
Normal file
78
pages/api/demand/supply.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
93
pages/api/transport/distribution.ts
Normal file
93
pages/api/transport/distribution.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
38
pages/api/transport/footprint/[userId].ts
Normal file
38
pages/api/transport/footprint/[userId].ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
89
pages/api/transport/growing.ts
Normal file
89
pages/api/transport/growing.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
103
pages/api/transport/harvest.ts
Normal file
103
pages/api/transport/harvest.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
45
pages/api/transport/journey/[plantId].ts
Normal file
45
pages/api/transport/journey/[plantId].ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
91
pages/api/transport/planting.ts
Normal file
91
pages/api/transport/planting.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
48
pages/api/transport/qr/[id].ts
Normal file
48
pages/api/transport/qr/[id].ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
101
pages/api/transport/seed-acquisition.ts
Normal file
101
pages/api/transport/seed-acquisition.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
101
pages/api/transport/seed-saving.ts
Normal file
101
pages/api/transport/seed-saving.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
95
pages/api/transport/seed-sharing.ts
Normal file
95
pages/api/transport/seed-sharing.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
70
pages/api/transport/verify/[blockHash].ts
Normal file
70
pages/api/transport/verify/[blockHash].ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
56
pages/api/vertical-farm/[farmId]/analytics.ts
Normal file
56
pages/api/vertical-farm/[farmId]/analytics.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
45
pages/api/vertical-farm/[farmId]/index.ts
Normal file
45
pages/api/vertical-farm/[farmId]/index.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
126
pages/api/vertical-farm/[farmId]/zones.ts
Normal file
126
pages/api/vertical-farm/[farmId]/zones.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
106
pages/api/vertical-farm/batch/[batchId]/environment.ts
Normal file
106
pages/api/vertical-farm/batch/[batchId]/environment.ts
Normal 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;
|
||||||
|
}
|
||||||
81
pages/api/vertical-farm/batch/[batchId]/harvest.ts
Normal file
81
pages/api/vertical-farm/batch/[batchId]/harvest.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
52
pages/api/vertical-farm/batch/[batchId]/index.ts
Normal file
52
pages/api/vertical-farm/batch/[batchId]/index.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
81
pages/api/vertical-farm/batch/start.ts
Normal file
81
pages/api/vertical-farm/batch/start.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
70
pages/api/vertical-farm/recipes.ts
Normal file
70
pages/api/vertical-farm/recipes.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
122
pages/api/vertical-farm/register.ts
Normal file
122
pages/api/vertical-farm/register.ts
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue