ca-grow-ops-manager/backend/src/routes/access-zones.routes.ts
fullsizemalt 32fd739ccf
Some checks failed
Deploy to Production / deploy (push) Failing after 0s
Test / backend-test (push) Failing after 0s
Test / frontend-test (push) Failing after 0s
feat: Complete Phases 8-13 implementation
Phase 8: Visitor Management
- Visitor/VisitorLog/AccessZone models
- Check-in/out with badge generation
- Zone occupancy tracking
- Kiosk and management pages

Phase 9: Messaging & Communication
- Announcements with priority levels
- Acknowledgement tracking
- Shift notes for team handoffs
- AnnouncementBanner component

Phase 10: Compliance & Audit Trail
- Immutable AuditLog model
- Document versioning and approval workflow
- Acknowledgement tracking for SOPs
- CSV export for audit logs

Phase 11: Accessibility & i18n
- WCAG 2.1 AA compliance utilities
- react-i18next with EN/ES translations
- User preferences context (theme, font size, etc)
- High contrast and reduced motion support

Phase 12: Hardware Integration
- QR code generation for batches/plants/visitors
- Printable label system
- Visitor badge printing

Phase 13: Advanced Features
- Environmental monitoring (sensors, readings, alerts)
- Financial tracking (transactions, P&L reports)
- AI/ML insights (yield predictions, anomaly detection)
2025-12-11 00:26:25 -08:00

300 lines
10 KiB
TypeScript

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' });
}
}
});
}