ca-grow-ops-manager/backend/src/routes/admin.routes.ts
fullsizemalt f91fbc2237
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
feat: add Reseed Demo Plants button to DevTools (realistic plant layout)
2025-12-18 12:00:22 -08:00

165 lines
6.3 KiB
TypeScript

import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
const DEMO_PREFIX = '[DEMO]';
/**
* Admin routes for demo/testing operations
* These should only be available in development or testing environments
*/
export async function adminRoutes(fastify: FastifyInstance) {
// Auth check - require OWNER or ADMIN role
fastify.addHook('onRequest', async (request, reply) => {
try {
await request.jwtVerify();
const user = request.user as { role?: string };
if (!['OWNER', 'ADMIN'].includes(user?.role || '')) {
return reply.code(403).send({ error: 'Admin access required' });
}
} catch (err) {
return reply.code(401).send({ error: 'Unauthorized' });
}
});
/**
* Reseed demo plants in a realistic layout
* - Each batch stays in appropriate room based on stage
* - Veg batches → Veg rooms, Flower batches → Flower rooms, etc.
*/
fastify.post('/reseed-plants', async (request: FastifyRequest, reply: FastifyReply) => {
const prisma = fastify.prisma;
try {
fastify.log.info('🌿 Reseeding plants in realistic layout...');
// Clear existing plants
await prisma.facilityPlant.deleteMany({});
await prisma.facilityPosition.updateMany({
data: { status: 'EMPTY' }
});
// Get all demo batches
const batches = await prisma.batch.findMany({
where: { name: { startsWith: DEMO_PREFIX } },
orderBy: { startDate: 'asc' }
});
if (batches.length === 0) {
return reply.code(400).send({
error: 'No demo batches found. Run seed-demo.js first.'
});
}
// Section mapping by room type
const sectionMapping: Record<string, string[]> = {
VEG_PRIMARY: ['T1', 'T2', 'T3', 'T4'],
MOTHER: ['M1', 'M2'],
FLOWER_A: ['A1', 'A2', 'A3', 'A4', 'A5', 'A6'],
FLOWER_B: ['B1', 'B2', 'B3', 'B4', 'B5', 'B6'],
DRY: ['D1', 'D2'],
CURE: ['C1', 'C2'],
};
const results: { batch: string; planted: number }[] = [];
for (const batch of batches) {
let targetSections: string[] = [];
let fillPercentage = 0.85;
switch (batch.stage) {
case 'CLONE_IN':
case 'VEGETATIVE':
targetSections = sectionMapping.VEG_PRIMARY;
fillPercentage = 0.9;
break;
case 'FLOWERING':
if (batch.strain === 'Gorilla Glue #4') {
targetSections = sectionMapping.FLOWER_A;
} else if (batch.strain === 'Wedding Cake') {
targetSections = sectionMapping.FLOWER_B;
} else {
targetSections = ['A4', 'A5', 'A6'];
}
fillPercentage = 0.95;
break;
case 'DRYING':
targetSections = sectionMapping.DRY;
fillPercentage = 0.6;
break;
case 'CURING':
targetSections = sectionMapping.CURE;
fillPercentage = 0.5;
break;
default:
continue;
}
// Get sections
const sections = await prisma.facilitySection.findMany({
where: { code: { in: targetSections } },
include: { positions: true }
});
// Collect available positions
let allPositions: any[] = [];
for (const section of sections) {
const available = section.positions.filter((p: any) => p.status === 'EMPTY');
allPositions = allPositions.concat(
available.map((p: any) => ({ ...p, sectionCode: section.code }))
);
}
const targetCount = Math.min(
Math.floor(allPositions.length * fillPercentage),
batch.plantCount || allPositions.length
);
const plantsToCreate = [];
const positionIds = [];
for (let i = 0; i < targetCount && i < allPositions.length; i++) {
const pos = allPositions[i];
const tagPrefix = batch.strain?.substring(0, 2).toUpperCase() || 'XX';
const tagNumber = `1A4-${tagPrefix}-${batch.id.substring(0, 4).toUpperCase()}-${String(i + 1).padStart(4, '0')}`;
plantsToCreate.push({
tagNumber,
batchId: batch.id,
positionId: pos.id,
address: `${pos.sectionCode}-R${pos.row}-C${pos.column}`,
status: 'ACTIVE'
});
positionIds.push(pos.id);
}
if (plantsToCreate.length > 0) {
await prisma.facilityPlant.createMany({
data: plantsToCreate,
skipDuplicates: true
});
await prisma.facilityPosition.updateMany({
where: { id: { in: positionIds } },
data: { status: 'OCCUPIED' }
});
}
results.push({ batch: batch.name || batch.id, planted: plantsToCreate.length });
}
const totalPlants = await prisma.facilityPlant.count();
return {
success: true,
message: 'Plants reseeded in realistic layout',
totalPlants,
batches: results
};
} catch (error) {
fastify.log.error(error);
return reply.code(500).send({
error: 'Failed to reseed plants',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
}