diff --git a/backend/src/routes/layout.routes.ts b/backend/src/routes/layout.routes.ts index 5d2d26e..37132f6 100644 --- a/backend/src/routes/layout.routes.ts +++ b/backend/src/routes/layout.routes.ts @@ -713,459 +713,456 @@ export async function layoutRoutes(fastify: FastifyInstance, options: FastifyPlu return reply.status(500).send({ error: 'Failed to move plant' }); } } - }; -} catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to move plant' }); -}); + }); -// Update Plant (Tag, Notes) -fastify.patch('/plants/:id', { - handler: async (request, reply) => { - try { - const { id } = request.params as any; - const { tagNumber, notes } = request.body as any; - const userId = (request.user as any)?.id || 'system'; + // Update Plant (Tag, Notes) + fastify.patch('/plants/:id', { + handler: async (request, reply) => { + try { + const { id } = request.params as any; + const { tagNumber, notes } = request.body as any; + const userId = (request.user as any)?.id || 'system'; - const plant = await prisma.facilityPlant.findUnique({ where: { id } }); - if (!plant) return reply.status(404).send({ error: 'Plant not found' }); + const plant = await prisma.facilityPlant.findUnique({ where: { id } }); + if (!plant) return reply.status(404).send({ error: 'Plant not found' }); - await prisma.$transaction(async (tx) => { - await tx.facilityPlant.update({ - where: { id }, - data: { - tagNumber: tagNumber || undefined, - // schema doesn't have notes on plant, but maybe we add it or log it as event? - // wait, layoutApi defines updatePlant(id, {tagNumber, notes}). - // schema.prisma: model FacilityPlant { notes String? ... }? - // Let's check schema. + await prisma.$transaction(async (tx) => { + await tx.facilityPlant.update({ + where: { id }, + data: { + tagNumber: tagNumber || undefined, + // schema doesn't have notes on plant, but maybe we add it or log it as event? + // wait, layoutApi defines updatePlant(id, {tagNumber, notes}). + // schema.prisma: model FacilityPlant { notes String? ... }? + // Let's check schema. + } + }); + + // Log event + if (tagNumber && tagNumber !== plant.tagNumber) { + await tx.plantLifecycleEvent.create({ + data: { + plantId: id, + type: 'NOTE', // Or COMPLIANCE_CHECK? Or generic AUDIT? + metadata: { note: `Tag changed from ${plant.tagNumber} to ${tagNumber}` }, + createdById: userId + } + }); } }); - // Log event - if (tagNumber && tagNumber !== plant.tagNumber) { + return { success: true }; + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to update plant' }); + } + } + }); + + // Destroy Plant + fastify.post('/plants/:id/destroy', { + handler: async (request, reply) => { + try { + const { id } = request.params as any; + const { reason, method } = request.body as any; + const userId = (request.user as any)?.id || 'system'; + + const plant = await prisma.facilityPlant.findUnique({ where: { id } }); + if (!plant) return reply.status(404).send({ error: 'Plant not found' }); + + await prisma.$transaction(async (tx) => { + // 1. Update plant status + await tx.facilityPlant.update({ + where: { id }, + data: { status: 'DESTROYED' } + }); + + // 2. Free up the position + await tx.facilityPosition.update({ + where: { id: plant.positionId }, + data: { status: 'EMPTY' } + }); + + // 3. Log event await tx.plantLifecycleEvent.create({ data: { plantId: id, - type: 'NOTE', // Or COMPLIANCE_CHECK? Or generic AUDIT? - metadata: { note: `Tag changed from ${plant.tagNumber} to ${tagNumber}` }, + type: 'DESTROY', + metadata: { reason, method }, createdById: userId } }); - } - }); - - return { success: true }; - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to update plant' }); - } - } -}); - -// Destroy Plant -fastify.post('/plants/:id/destroy', { - handler: async (request, reply) => { - try { - const { id } = request.params as any; - const { reason, method } = request.body as any; - const userId = (request.user as any)?.id || 'system'; - - const plant = await prisma.facilityPlant.findUnique({ where: { id } }); - if (!plant) return reply.status(404).send({ error: 'Plant not found' }); - - await prisma.$transaction(async (tx) => { - // 1. Update plant status - await tx.facilityPlant.update({ - where: { id }, - data: { status: 'DESTROYED' } }); - // 2. Free up the position - await tx.facilityPosition.update({ - where: { id: plant.positionId }, - data: { status: 'EMPTY' } - }); - - // 3. Log event - await tx.plantLifecycleEvent.create({ - data: { - plantId: id, - type: 'DESTROY', - metadata: { reason, method }, - createdById: userId - } - }); - }); - - return { success: true }; - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to destroy plant' }); - } - } -}); - -// Harvest Plant -fastify.post('/plants/:id/harvest', { - handler: async (request, reply) => { - try { - const { id } = request.params as any; - const { weight, unit, notes } = request.body as any; - const userId = (request.user as any)?.id || 'system'; - - const plant = await prisma.facilityPlant.findUnique({ where: { id } }); - if (!plant) return reply.status(404).send({ error: 'Plant not found' }); - - await prisma.$transaction(async (tx) => { - // 1. Update plant status - await tx.facilityPlant.update({ - where: { id }, - data: { status: 'HARVESTED' } - }); - - // 2. Free up the position (Harvest removes from rack) - await tx.facilityPosition.update({ - where: { id: plant.positionId }, - data: { status: 'EMPTY' } - }); - - // 3. Log event - await tx.plantLifecycleEvent.create({ - data: { - plantId: id, - type: 'HARVEST', - metadata: { weight, unit, notes }, - createdById: userId - } - }); - }); - - return { success: true }; - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to harvest plant' }); - } - } -}); - -// Bulk Fill Section (Place plants in all empty positions) -fastify.post('/sections/:id/fill', { - handler: async (request, reply) => { - try { - const { id } = request.params as any; - const { batchId, maxCount } = request.body as any; - - // Get section with room info for address generation - const section = await prisma.facilitySection.findUnique({ - where: { id }, - include: { - room: true, - positions: { - where: { status: 'EMPTY' }, - orderBy: [{ row: 'asc' }, { column: 'asc' }] - } - } - }); - - if (!section) return reply.status(404).send({ error: 'Section not found' }); - - const emptyPositions = section.positions; - const positionsToFill = maxCount - ? emptyPositions.slice(0, maxCount) - : emptyPositions; - - if (positionsToFill.length === 0) { - return reply.status(400).send({ error: 'No empty positions available' }); + return { success: true }; + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to destroy plant' }); } + } + }); - const room = section.room; - let plantsCreated = 0; + // Harvest Plant + fastify.post('/plants/:id/harvest', { + handler: async (request, reply) => { + try { + const { id } = request.params as any; + const { weight, unit, notes } = request.body as any; + const userId = (request.user as any)?.id || 'system'; - await prisma.$transaction(async (tx) => { - for (const pos of positionsToFill) { - const address = `${room.code}-${section.code}-${pos.row}-${pos.column}`; + const plant = await prisma.facilityPlant.findUnique({ where: { id } }); + if (!plant) return reply.status(404).send({ error: 'Plant not found' }); - await tx.facilityPlant.create({ + await prisma.$transaction(async (tx) => { + // 1. Update plant status + await tx.facilityPlant.update({ + where: { id }, + data: { status: 'HARVESTED' } + }); + + // 2. Free up the position (Harvest removes from rack) + await tx.facilityPosition.update({ + where: { id: plant.positionId }, + data: { status: 'EMPTY' } + }); + + // 3. Log event + await tx.plantLifecycleEvent.create({ data: { - tagNumber: `P-${Date.now()}-${pos.row}${pos.column}-${Math.floor(Math.random() * 100)}`, - batchId, - positionId: pos.id, - address, - status: 'ACTIVE' + plantId: id, + type: 'HARVEST', + metadata: { weight, unit, notes }, + createdById: userId } }); - - await tx.facilityPosition.update({ - where: { id: pos.id }, - data: { status: 'PLANTED' } - }); - - plantsCreated++; - } - }); - - return { - success: true, - plantsCreated, - message: `Placed ${plantsCreated} plants in ${section.name}` - }; - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to fill section' }); - } - } -}); - -// ======================================== -// SECTION ROUTES -// ======================================== - -// Create section with positions -fastify.post('/sections', { - handler: async (request, reply) => { - try { - const { roomId, name, code, type, posX, posY, width, height, rows, columns, spacing, tiers } = request.body as any; - const numTiers = tiers || 1; - - // Generate positions for all rows, columns, and tiers - const positionsToCreate = []; - for (let r = 1; r <= rows; r++) { - for (let c = 1; c <= columns; c++) { - for (let t = 1; t <= numTiers; t++) { - positionsToCreate.push({ - row: r, - column: c, - tier: t, - slot: 1, - status: 'EMPTY' - }); - } - } - } - - const section = await prisma.facilitySection.create({ - data: { - roomId, - name, - code, - type: type as SectionType, - posX, - posY, - width, - height, - rows, - columns, - tiers: numTiers, - spacing: spacing || 12, - positions: { - create: positionsToCreate - } - }, - include: { positions: true } - }); - - return reply.status(201).send(section); - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to create section' }); - } - } -}); - -// Get section with positions -fastify.get('/sections/:id', { - handler: async (request, reply) => { - try { - const { id } = request.params as any; - const section = await prisma.facilitySection.findUnique({ - where: { id }, - include: { - positions: { - include: { plant: true }, - orderBy: [{ row: 'asc' }, { column: 'asc' }] - }, - room: true - } - }); - - if (!section) { - return reply.status(404).send({ error: 'Section not found' }); - } - - return section; - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to fetch section' }); - } - } -}); - -// ======================================== -// BULK SAVE LAYOUT -// ======================================== - -// Save entire floor layout -fastify.post('/floors/:id/layout', { - handler: async (request, reply) => { - try { - const { id: floorId } = request.params as any; - const { rooms } = request.body as any; - - await prisma.$transaction(async (tx) => { - // Get existing room IDs - const existingRooms = await tx.facilityRoom.findMany({ - where: { floorId }, - select: { id: true } }); - const existingIds = new Set(existingRooms.map(r => r.id)); - const incomingIds = new Set((rooms as any[]).map(r => r.id)); - // Process each room - for (const room of rooms) { - const roomData = { - name: room.name, - code: room.code, - type: room.type, - posX: room.posX, - posY: room.posY, - width: room.width, - height: room.height, - color: room.color, - rotation: room.rotation || 0 - }; + return { success: true }; + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to harvest plant' }); + } + } + }); - if (existingIds.has(room.id)) { - // Update existing room - await tx.facilityRoom.update({ - where: { id: room.id }, - data: roomData - }); - } else { - // Create new room - await tx.facilityRoom.create({ + // Bulk Fill Section (Place plants in all empty positions) + fastify.post('/sections/:id/fill', { + handler: async (request, reply) => { + try { + const { id } = request.params as any; + const { batchId, maxCount } = request.body as any; + + // Get section with room info for address generation + const section = await prisma.facilitySection.findUnique({ + where: { id }, + include: { + room: true, + positions: { + where: { status: 'EMPTY' }, + orderBy: [{ row: 'asc' }, { column: 'asc' }] + } + } + }); + + if (!section) return reply.status(404).send({ error: 'Section not found' }); + + const emptyPositions = section.positions; + const positionsToFill = maxCount + ? emptyPositions.slice(0, maxCount) + : emptyPositions; + + if (positionsToFill.length === 0) { + return reply.status(400).send({ error: 'No empty positions available' }); + } + + const room = section.room; + let plantsCreated = 0; + + await prisma.$transaction(async (tx) => { + for (const pos of positionsToFill) { + const address = `${room.code}-${section.code}-${pos.row}-${pos.column}`; + + await tx.facilityPlant.create({ data: { - id: room.id, - floorId, - ...roomData + tagNumber: `P-${Date.now()}-${pos.row}${pos.column}-${Math.floor(Math.random() * 100)}`, + batchId, + positionId: pos.id, + address, + status: 'ACTIVE' } }); - } - // Handle Sections - const sections = room.sections || []; - const existingSections = await tx.facilitySection.findMany({ - where: { roomId: room.id }, + await tx.facilityPosition.update({ + where: { id: pos.id }, + data: { status: 'PLANTED' } + }); + + plantsCreated++; + } + }); + + return { + success: true, + plantsCreated, + message: `Placed ${plantsCreated} plants in ${section.name}` + }; + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to fill section' }); + } + } + }); + + // ======================================== + // SECTION ROUTES + // ======================================== + + // Create section with positions + fastify.post('/sections', { + handler: async (request, reply) => { + try { + const { roomId, name, code, type, posX, posY, width, height, rows, columns, spacing, tiers } = request.body as any; + const numTiers = tiers || 1; + + // Generate positions for all rows, columns, and tiers + const positionsToCreate = []; + for (let r = 1; r <= rows; r++) { + for (let c = 1; c <= columns; c++) { + for (let t = 1; t <= numTiers; t++) { + positionsToCreate.push({ + row: r, + column: c, + tier: t, + slot: 1, + status: 'EMPTY' + }); + } + } + } + + const section = await prisma.facilitySection.create({ + data: { + roomId, + name, + code, + type: type as SectionType, + posX, + posY, + width, + height, + rows, + columns, + tiers: numTiers, + spacing: spacing || 12, + positions: { + create: positionsToCreate + } + }, + include: { positions: true } + }); + + return reply.status(201).send(section); + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to create section' }); + } + } + }); + + // Get section with positions + fastify.get('/sections/:id', { + handler: async (request, reply) => { + try { + const { id } = request.params as any; + const section = await prisma.facilitySection.findUnique({ + where: { id }, + include: { + positions: { + include: { plant: true }, + orderBy: [{ row: 'asc' }, { column: 'asc' }] + }, + room: true + } + }); + + if (!section) { + return reply.status(404).send({ error: 'Section not found' }); + } + + return section; + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to fetch section' }); + } + } + }); + + // ======================================== + // BULK SAVE LAYOUT + // ======================================== + + // Save entire floor layout + fastify.post('/floors/:id/layout', { + handler: async (request, reply) => { + try { + const { id: floorId } = request.params as any; + const { rooms } = request.body as any; + + await prisma.$transaction(async (tx) => { + // Get existing room IDs + const existingRooms = await tx.facilityRoom.findMany({ + where: { floorId }, select: { id: true } }); - const existingSecIds = new Set(existingSections.map(s => s.id)); - const incomingSecIds = new Set(sections.map((s: any) => s.id)); + const existingIds = new Set(existingRooms.map(r => r.id)); + const incomingIds = new Set((rooms as any[]).map(r => r.id)); - for (const sec of sections) { - const secData = { - roomId: room.id, - name: sec.name, - code: sec.code, - type: sec.type, - posX: sec.posX, - posY: sec.posY, - width: sec.width, - height: sec.height, - rows: sec.rows, - columns: sec.columns, - tiers: sec.tiers || 1, - spacing: sec.spacing || 12 + // Process each room + for (const room of rooms) { + const roomData = { + name: room.name, + code: room.code, + type: room.type, + posX: room.posX, + posY: room.posY, + width: room.width, + height: room.height, + color: room.color, + rotation: room.rotation || 0 }; - if (existingSecIds.has(sec.id)) { - // Update existing section - await tx.facilitySection.update({ - where: { id: sec.id }, - data: secData + if (existingIds.has(room.id)) { + // Update existing room + await tx.facilityRoom.update({ + where: { id: room.id }, + data: roomData }); } else { - // Create new section with positions - const numTiers = sec.tiers || 1; - const positionsToCreate = []; - for (let r = 1; r <= sec.rows; r++) { - for (let c = 1; c <= sec.columns; c++) { - for (let t = 1; t <= numTiers; t++) { - positionsToCreate.push({ - row: r, - column: c, - tier: t, - slot: 1, - status: 'EMPTY' - }); - } - } - } - - await tx.facilitySection.create({ + // Create new room + await tx.facilityRoom.create({ data: { - id: sec.id, - ...secData, - positions: { - create: positionsToCreate + id: room.id, + floorId, + ...roomData + } + }); + } + + // Handle Sections + const sections = room.sections || []; + const existingSections = await tx.facilitySection.findMany({ + where: { roomId: room.id }, + select: { id: true } + }); + const existingSecIds = new Set(existingSections.map(s => s.id)); + const incomingSecIds = new Set(sections.map((s: any) => s.id)); + + for (const sec of sections) { + const secData = { + roomId: room.id, + name: sec.name, + code: sec.code, + type: sec.type, + posX: sec.posX, + posY: sec.posY, + width: sec.width, + height: sec.height, + rows: sec.rows, + columns: sec.columns, + tiers: sec.tiers || 1, + spacing: sec.spacing || 12 + }; + + if (existingSecIds.has(sec.id)) { + // Update existing section + await tx.facilitySection.update({ + where: { id: sec.id }, + data: secData + }); + } else { + // Create new section with positions + const numTiers = sec.tiers || 1; + const positionsToCreate = []; + for (let r = 1; r <= sec.rows; r++) { + for (let c = 1; c <= sec.columns; c++) { + for (let t = 1; t <= numTiers; t++) { + positionsToCreate.push({ + row: r, + column: c, + tier: t, + slot: 1, + status: 'EMPTY' + }); + } } } + + await tx.facilitySection.create({ + data: { + id: sec.id, + ...secData, + positions: { + create: positionsToCreate + } + } + }); + } + } + + // Delete removed sections + const secToDelete = [...existingSecIds].filter(id => !incomingSecIds.has(id)); + if (secToDelete.length > 0) { + await tx.facilitySection.deleteMany({ + where: { id: { in: secToDelete } } }); } } - // Delete removed sections - const secToDelete = [...existingSecIds].filter(id => !incomingSecIds.has(id)); - if (secToDelete.length > 0) { - await tx.facilitySection.deleteMany({ - where: { id: { in: secToDelete } } + // Delete rooms that were removed + const toDelete = [...existingIds].filter(id => !incomingIds.has(id)); + if (toDelete.length > 0) { + await tx.facilityRoom.deleteMany({ + where: { id: { in: toDelete } } }); } - } + }); - // Delete rooms that were removed - const toDelete = [...existingIds].filter(id => !incomingIds.has(id)); - if (toDelete.length > 0) { - await tx.facilityRoom.deleteMany({ - where: { id: { in: toDelete } } - }); - } - }); + // Return updated floor + const floor = await prisma.facilityFloor.findUnique({ + where: { id: floorId }, + include: { rooms: { include: { sections: true } } } + }); - // Return updated floor - const floor = await prisma.facilityFloor.findUnique({ - where: { id: floorId }, - include: { rooms: { include: { sections: true } } } - }); - - return floor; - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to save layout' }); + return floor; + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to save layout' }); + } } - } -}); + }); -// ======================================== -// POSITION ROUTES -// ======================================== + // ======================================== + // POSITION ROUTES + // ======================================== -// Get hierarchical address for a position -fastify.get('/positions/:id/address', { - handler: async (request, reply) => { - try { - const { id } = request.params as any; - const position = await prisma.facilityPosition.findUnique({ - where: { id }, - include: { - section: { - include: { - room: { - include: { - floor: { - include: { - building: { - include: { - property: true + // Get hierarchical address for a position + fastify.get('/positions/:id/address', { + handler: async (request, reply) => { + try { + const { id } = request.params as any; + const position = await prisma.facilityPosition.findUnique({ + where: { id }, + include: { + section: { + include: { + room: { + include: { + floor: { + include: { + building: { + include: { + property: true + } } } } @@ -1174,167 +1171,166 @@ fastify.get('/positions/:id/address', { } } } + }); + + if (!position) { + return reply.status(404).send({ error: 'Position not found' }); } - }); - if (!position) { - return reply.status(404).send({ error: 'Position not found' }); + // Construct hierarchical address + // Format: PROP.BLDG.FLR.RM.SEC.R{Row}.C{Col}.S{Slot} + const p = position.section.room.floor.building.property; + const b = position.section.room.floor.building; + const f = position.section.room.floor; + const r = position.section.room; + const s = position.section; + + // Helper to sanitize codes (remove spaces, uppercase) + const code = (str: string) => str.toUpperCase().replace(/[^A-Z0-9]/g, ''); + + const address = [ + code(p.name).substring(0, 4), + code(b.code), + `F${f.number}`, + code(r.code), + code(s.code), + `R${position.row}`, + `C${position.column}` + ].join('.'); + + return { address, details: position }; + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to generate address' }); } - - // Construct hierarchical address - // Format: PROP.BLDG.FLR.RM.SEC.R{Row}.C{Col}.S{Slot} - const p = position.section.room.floor.building.property; - const b = position.section.room.floor.building; - const f = position.section.room.floor; - const r = position.section.room; - const s = position.section; - - // Helper to sanitize codes (remove spaces, uppercase) - const code = (str: string) => str.toUpperCase().replace(/[^A-Z0-9]/g, ''); - - const address = [ - code(p.name).substring(0, 4), - code(b.code), - `F${f.number}`, - code(r.code), - code(s.code), - `R${position.row}`, - `C${position.column}` - ].join('.'); - - return { address, details: position }; - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to generate address' }); } - } -}); + }); -// ======================================== -// PLANT TYPE LIBRARY ROUTES (Rackula-inspired) -// ======================================== + // ======================================== + // PLANT TYPE LIBRARY ROUTES (Rackula-inspired) + // ======================================== -// Get all plant types -fastify.get('/plant-types', { - handler: async (request, reply) => { - try { - const plantTypes = await prisma.plantType.findMany({ - orderBy: { name: 'asc' } - }); - return plantTypes; - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to fetch plant types' }); - } - } -}); - -// Get plant type by slug -fastify.get('/plant-types/:slug', { - handler: async (request, reply) => { - try { - const { slug } = request.params as { slug: string }; - const plantType = await prisma.plantType.findUnique({ - where: { slug } - }); - - if (!plantType) { - return reply.status(404).send({ error: 'Plant type not found' }); + // Get all plant types + fastify.get('/plant-types', { + handler: async (request, reply) => { + try { + const plantTypes = await prisma.plantType.findMany({ + orderBy: { name: 'asc' } + }); + return plantTypes; + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to fetch plant types' }); } - - return plantType; - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to fetch plant type' }); } - } -}); + }); -// Create plant type -fastify.post('/plant-types', { - handler: async (request, reply) => { - try { - const { name, strain, category, colour, growthDays, yieldGrams, notes, tags, customFields } = request.body as any; + // Get plant type by slug + fastify.get('/plant-types/:slug', { + handler: async (request, reply) => { + try { + const { slug } = request.params as { slug: string }; + const plantType = await prisma.plantType.findUnique({ + where: { slug } + }); - // Generate slug from name - const baseSlug = name - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .replace(/-+/g, '-'); - - // Check for existing slug and make unique if needed - let slug = baseSlug; - let counter = 1; - while (await prisma.plantType.findUnique({ where: { slug } })) { - slug = `${baseSlug}-${counter}`; - counter++; - } - - const plantType = await prisma.plantType.create({ - data: { - slug, - name, - strain, - category, - colour, - growthDays, - yieldGrams, - notes, - tags: tags || [], - customFields + if (!plantType) { + return reply.status(404).send({ error: 'Plant type not found' }); } - }); - return reply.status(201).send(plantType); - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to create plant type' }); + return plantType; + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to fetch plant type' }); + } } - } -}); + }); -// Update plant type -fastify.put('/plant-types/:slug', { - handler: async (request, reply) => { - try { - const { slug } = request.params as { slug: string }; - const { name, strain, category, colour, growthDays, yieldGrams, notes, tags, customFields } = request.body as any; + // Create plant type + fastify.post('/plant-types', { + handler: async (request, reply) => { + try { + const { name, strain, category, colour, growthDays, yieldGrams, notes, tags, customFields } = request.body as any; - const plantType = await prisma.plantType.update({ - where: { slug }, - data: { - name, - strain, - category, - colour, - growthDays, - yieldGrams, - notes, - tags, - customFields + // Generate slug from name + const baseSlug = name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .replace(/-+/g, '-'); + + // Check for existing slug and make unique if needed + let slug = baseSlug; + let counter = 1; + while (await prisma.plantType.findUnique({ where: { slug } })) { + slug = `${baseSlug}-${counter}`; + counter++; } - }); - return plantType; - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to update plant type' }); - } - } -}); + const plantType = await prisma.plantType.create({ + data: { + slug, + name, + strain, + category, + colour, + growthDays, + yieldGrams, + notes, + tags: tags || [], + customFields + } + }); -// Delete plant type -fastify.delete('/plant-types/:slug', { - handler: async (request, reply) => { - try { - const { slug } = request.params as { slug: string }; - await prisma.plantType.delete({ where: { slug } }); - return reply.status(204).send(); - } catch (error) { - fastify.log.error(error); - return reply.status(500).send({ error: 'Failed to delete plant type' }); + return reply.status(201).send(plantType); + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to create plant type' }); + } } - } -}); + }); + + // Update plant type + fastify.put('/plant-types/:slug', { + handler: async (request, reply) => { + try { + const { slug } = request.params as { slug: string }; + const { name, strain, category, colour, growthDays, yieldGrams, notes, tags, customFields } = request.body as any; + + const plantType = await prisma.plantType.update({ + where: { slug }, + data: { + name, + strain, + category, + colour, + growthDays, + yieldGrams, + notes, + tags, + customFields + } + }); + + return plantType; + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to update plant type' }); + } + } + }); + + // Delete plant type + fastify.delete('/plant-types/:slug', { + handler: async (request, reply) => { + try { + const { slug } = request.params as { slug: string }; + await prisma.plantType.delete({ where: { slug } }); + return reply.status(204).send(); + } catch (error) { + fastify.log.error(error); + return reply.status(500).send({ error: 'Failed to delete plant type' }); + } + } + }); } diff --git a/frontend/src/components/layout-editor/LayoutEditor.tsx b/frontend/src/components/layout-editor/LayoutEditor.tsx index 7205eb6..1d3b140 100644 --- a/frontend/src/components/layout-editor/LayoutEditor.tsx +++ b/frontend/src/components/layout-editor/LayoutEditor.tsx @@ -236,7 +236,7 @@ export function LayoutEditor({ floorId, className }: LayoutEditorProps) { if (newTag && newTag !== selectedSlot.plant?.tagNumber) { layoutApi.updatePlant(selectedSlot.plant!.id, { tagNumber: newTag }) .then(() => { - toast({ title: 'Tag Updated', description: newTag }); + toast.info('Tag Updated', { description: newTag }); reloadFloorData(); setSelectedSlot(prev => prev ? ({ ...prev, plant: { ...prev.plant!, tagNumber: newTag } }) : null); }); @@ -319,7 +319,7 @@ export function LayoutEditor({ floorId, className }: LayoutEditorProps) {