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