import { FastifyInstance } from 'fastify'; import { PrismaClient } from '@prisma/client'; import { z } from 'zod'; const prisma = new PrismaClient(); const createZoneSchema = z.object({ name: z.string().min(2), code: z.string().min(2).max(10), description: z.string().optional(), escortRequired: z.boolean().default(false), badgeRequired: z.boolean().default(true), ndaRequired: z.boolean().default(false), allowedTypes: z.array(z.enum(['VISITOR', 'CONTRACTOR', 'INSPECTOR', 'VENDOR', 'DELIVERY', 'OTHER'])).optional(), maxOccupancy: z.number().int().positive().optional(), parentZoneId: z.string().uuid().optional() }); export async function accessZoneRoutes(fastify: FastifyInstance) { // Auth middleware fastify.addHook('onRequest', async (request) => { try { await request.jwtVerify(); } catch (err) { throw err; } }); /** * GET /zones * List all access zones */ fastify.get('/', { handler: async (request, reply) => { try { const zones = await prisma.accessZone.findMany({ include: { parentZone: { select: { id: true, name: true, code: true } }, childZones: { select: { id: true, name: true, code: true } } }, orderBy: { name: 'asc' } }); return zones; } catch (error) { fastify.log.error(error); return reply.status(500).send({ error: 'Failed to fetch zones' }); } } }); /** * POST /zones * Create new access zone */ fastify.post('/', { handler: async (request, reply) => { try { const data = createZoneSchema.parse(request.body); const zone = await prisma.accessZone.create({ data: { ...data, allowedTypes: data.allowedTypes || [] } }); return zone; } catch (error: any) { if (error.code === 'P2002') { return reply.status(400).send({ error: 'Zone code already exists' }); } if (error.name === 'ZodError') { return reply.status(400).send({ error: 'Validation failed', details: error.errors }); } fastify.log.error(error); return reply.status(500).send({ error: 'Failed to create zone' }); } } }); /** * GET /zones/:id * Get zone details with hierarchy */ fastify.get('/:id', { handler: async (request, reply) => { try { const { id } = request.params as any; const zone = await prisma.accessZone.findUnique({ where: { id }, include: { parentZone: true, childZones: true } }); if (!zone) { return reply.status(404).send({ error: 'Zone not found' }); } return zone; } catch (error) { fastify.log.error(error); return reply.status(500).send({ error: 'Failed to fetch zone' }); } } }); /** * PUT /zones/:id * Update access zone */ fastify.put('/:id', { handler: async (request, reply) => { try { const { id } = request.params as any; const data = createZoneSchema.partial().parse(request.body); const zone = await prisma.accessZone.update({ where: { id }, data }); return zone; } catch (error: any) { if (error.code === 'P2025') { return reply.status(404).send({ error: 'Zone not found' }); } fastify.log.error(error); return reply.status(500).send({ error: 'Failed to update zone' }); } } }); /** * DELETE /zones/:id * Delete access zone */ fastify.delete('/:id', { handler: async (request, reply) => { try { const { id } = request.params as any; // Check for child zones const childCount = await prisma.accessZone.count({ where: { parentZoneId: id } }); if (childCount > 0) { return reply.status(400).send({ error: 'Cannot delete zone with child zones', childCount }); } await prisma.accessZone.delete({ where: { id } }); return { success: true }; } catch (error: any) { if (error.code === 'P2025') { return reply.status(404).send({ error: 'Zone not found' }); } fastify.log.error(error); return reply.status(500).send({ error: 'Failed to delete zone' }); } } }); /** * GET /zones/:id/occupancy * Get current occupancy of a zone */ fastify.get('/:id/occupancy', { handler: async (request, reply) => { try { const { id } = request.params as any; const zone = await prisma.accessZone.findUnique({ where: { id } }); if (!zone) { return reply.status(404).send({ error: 'Zone not found' }); } // Count visitors currently in this zone const activeVisitors = await prisma.visitorLog.findMany({ where: { status: 'CHECKED_IN', exitTime: null, zones: { has: id } }, include: { visitor: { select: { id: true, name: true, company: true, type: true } } } }); return { zone: { id: zone.id, name: zone.name, code: zone.code }, maxOccupancy: zone.maxOccupancy, currentOccupancy: activeVisitors.length, atCapacity: zone.maxOccupancy ? activeVisitors.length >= zone.maxOccupancy : false, visitors: activeVisitors.map(log => ({ logId: log.id, ...log.visitor, entryTime: log.entryTime })) }; } catch (error) { fastify.log.error(error); return reply.status(500).send({ error: 'Failed to get zone occupancy' }); } } }); /** * POST /zones/:id/log-entry * Log a visitor entering a zone */ fastify.post('/:id/log-entry', { handler: async (request, reply) => { try { const { id } = request.params as any; const { visitorLogId } = request.body as any; const zone = await prisma.accessZone.findUnique({ where: { id } }); if (!zone) { return reply.status(404).send({ error: 'Zone not found' }); } const log = await prisma.visitorLog.findUnique({ where: { id: visitorLogId }, include: { visitor: true } }); if (!log) { return reply.status(404).send({ error: 'Visitor log not found' }); } if (log.status !== 'CHECKED_IN') { return reply.status(400).send({ error: 'Visitor not currently checked in' }); } // Check if visitor type is allowed if (zone.allowedTypes.length > 0 && !zone.allowedTypes.includes(log.visitor.type)) { return reply.status(403).send({ error: 'Visitor type not allowed in this zone', allowed: zone.allowedTypes, visitorType: log.visitor.type }); } // Check NDA requirement if (zone.ndaRequired && !log.visitor.ndaAccepted) { return reply.status(403).send({ error: 'NDA required for this zone' }); } // Check escort requirement if (zone.escortRequired && !log.escortId) { return reply.status(403).send({ error: 'Escort required for this zone' }); } // Check capacity if (zone.maxOccupancy) { const count = await prisma.visitorLog.count({ where: { status: 'CHECKED_IN', exitTime: null, zones: { has: id } } }); if (count >= zone.maxOccupancy) { return reply.status(403).send({ error: 'Zone at maximum capacity', maxOccupancy: zone.maxOccupancy }); } } // Add zone to visitor's zones array const updatedLog = await prisma.visitorLog.update({ where: { id: visitorLogId }, data: { zones: [...new Set([...log.zones, id])] } }); return { success: true, zone: { id: zone.id, name: zone.name, code: zone.code }, zonesVisited: updatedLog.zones.length }; } catch (error) { fastify.log.error(error); return reply.status(500).send({ error: 'Failed to log zone entry' }); } } }); }