[SPRINT-2] feat: Complete Quality module controllers
- Add non-conformity.controller.ts with CRUD endpoints - Add corrective-action.controller.ts with workflow management - Add protocolo-calidad.controller.ts for quality protocols - Update inspection.controller.ts and exports index Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
164186cec6
commit
99fadef0ba
521
src/modules/quality/controllers/corrective-action.controller.ts
Normal file
521
src/modules/quality/controllers/corrective-action.controller.ts
Normal file
@ -0,0 +1,521 @@
|
|||||||
|
/**
|
||||||
|
* CorrectiveActionController - REST API for corrective actions (Acciones Correctivas CAPA)
|
||||||
|
*
|
||||||
|
* Endpoints para gestion de acciones correctivas y preventivas (CAPA).
|
||||||
|
*
|
||||||
|
* @module Quality (MAI-009)
|
||||||
|
* @routes /api/acciones-correctivas
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import {
|
||||||
|
CorrectiveActionService,
|
||||||
|
CorrectiveActionFilters,
|
||||||
|
CreateCorrectiveActionDto,
|
||||||
|
UpdateCorrectiveActionDto,
|
||||||
|
CompleteActionDto,
|
||||||
|
VerifyActionDto,
|
||||||
|
} from '../services/corrective-action.service';
|
||||||
|
import { CorrectiveAction, ActionType, ActionStatus } from '../entities/corrective-action.entity';
|
||||||
|
import { NonConformity } from '../entities/non-conformity.entity';
|
||||||
|
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||||
|
import { AuthService } from '../../auth/services/auth.service';
|
||||||
|
import { User } from '../../core/entities/user.entity';
|
||||||
|
import { Tenant } from '../../core/entities/tenant.entity';
|
||||||
|
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||||
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the CorrectiveAction controller with all REST endpoints
|
||||||
|
* @param dataSource - TypeORM DataSource for database operations
|
||||||
|
* @returns Express Router with all endpoints configured
|
||||||
|
*/
|
||||||
|
export function createCorrectiveActionController(dataSource: DataSource): Router {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
const actionRepo = dataSource.getRepository(CorrectiveAction);
|
||||||
|
const ncRepo = dataSource.getRepository(NonConformity);
|
||||||
|
const userRepository = dataSource.getRepository(User);
|
||||||
|
const tenantRepository = dataSource.getRepository(Tenant);
|
||||||
|
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||||
|
|
||||||
|
// Services
|
||||||
|
const service = new CorrectiveActionService(actionRepo, ncRepo);
|
||||||
|
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||||
|
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to extract service context from request
|
||||||
|
*/
|
||||||
|
const getContext = (req: Request): ServiceContext => {
|
||||||
|
if (!req.tenantId) {
|
||||||
|
throw new Error('Tenant ID is required');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tenantId: req.tenantId,
|
||||||
|
userId: req.user?.sub,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/acciones-correctivas
|
||||||
|
* List corrective actions with filters and pagination
|
||||||
|
*
|
||||||
|
* Query params:
|
||||||
|
* - nonConformityId: Filter by non-conformity
|
||||||
|
* - responsibleId: Filter by responsible user
|
||||||
|
* - actionType: Filter by type (corrective, preventive, improvement)
|
||||||
|
* - status: Filter by status (pending, in_progress, completed, verified)
|
||||||
|
* - effectivenessVerified: Filter by effectiveness verification
|
||||||
|
* - dueDateFrom/To: Due date range filters
|
||||||
|
* - search: Search in description
|
||||||
|
* - page: Page number (default: 1)
|
||||||
|
* - limit: Items per page (default: 20, max: 100)
|
||||||
|
*/
|
||||||
|
router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters: CorrectiveActionFilters = {};
|
||||||
|
if (req.query.nonConformityId) filters.nonConformityId = req.query.nonConformityId as string;
|
||||||
|
if (req.query.responsibleId) filters.responsibleId = req.query.responsibleId as string;
|
||||||
|
if (req.query.actionType) filters.actionType = req.query.actionType as ActionType;
|
||||||
|
if (req.query.status) filters.status = req.query.status as ActionStatus;
|
||||||
|
if (req.query.effectivenessVerified !== undefined) {
|
||||||
|
filters.effectivenessVerified = req.query.effectivenessVerified === 'true';
|
||||||
|
}
|
||||||
|
if (req.query.dueDateFrom) filters.dueDateFrom = new Date(req.query.dueDateFrom as string);
|
||||||
|
if (req.query.dueDateTo) filters.dueDateTo = new Date(req.query.dueDateTo as string);
|
||||||
|
if (req.query.search) filters.search = req.query.search as string;
|
||||||
|
|
||||||
|
const page = parseInt(req.query.page as string) || 1;
|
||||||
|
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||||||
|
|
||||||
|
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result.data,
|
||||||
|
pagination: {
|
||||||
|
total: result.total,
|
||||||
|
page: result.page,
|
||||||
|
limit: result.limit,
|
||||||
|
totalPages: Math.ceil(result.total / result.limit),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/acciones-correctivas/vencidas
|
||||||
|
* Get overdue corrective actions
|
||||||
|
*/
|
||||||
|
router.get('/vencidas', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overdueActions = await service.getOverdueActions(getContext(req));
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: overdueActions,
|
||||||
|
count: overdueActions.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/acciones-correctivas/por-responsable/:responsibleId
|
||||||
|
* Get corrective action statistics by responsible user
|
||||||
|
*/
|
||||||
|
router.get('/por-responsable/:responsibleId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = await service.getStatsByResponsible(getContext(req), req.params.responsibleId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: stats,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/acciones-correctivas/por-no-conformidad/:nonConformityId
|
||||||
|
* Get corrective actions for a specific non-conformity
|
||||||
|
*/
|
||||||
|
router.get('/por-no-conformidad/:nonConformityId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = await service.findByNonConformity(getContext(req), req.params.nonConformityId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: actions,
|
||||||
|
count: actions.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/acciones-correctivas/:id
|
||||||
|
* Get corrective action detail with relations
|
||||||
|
*/
|
||||||
|
router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = await service.findWithDetails(getContext(req), req.params.id);
|
||||||
|
if (!action) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Accion correctiva no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: action,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/acciones-correctivas
|
||||||
|
* Create new corrective action
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - nonConformityId: (required) UUID of the non-conformity
|
||||||
|
* - actionType: (required) 'corrective' | 'preventive' | 'improvement'
|
||||||
|
* - description: (required) Description text
|
||||||
|
* - responsibleId: (required) UUID of responsible user
|
||||||
|
* - dueDate: (required) Due date
|
||||||
|
*/
|
||||||
|
router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
const { nonConformityId, actionType, description, responsibleId, dueDate } = req.body;
|
||||||
|
if (!nonConformityId || !actionType || !description || !responsibleId || !dueDate) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'nonConformityId, actionType, description, responsibleId y dueDate son requeridos',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate actionType enum
|
||||||
|
const validTypes: ActionType[] = ['corrective', 'preventive', 'improvement'];
|
||||||
|
if (!validTypes.includes(actionType)) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: `Tipo de accion invalido. Valores validos: ${validTypes.join(', ')}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: CreateCorrectiveActionDto = {
|
||||||
|
nonConformityId,
|
||||||
|
actionType,
|
||||||
|
description,
|
||||||
|
responsibleId,
|
||||||
|
dueDate: new Date(dueDate),
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = await service.create(getContext(req), dto);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: action,
|
||||||
|
message: 'Accion correctiva creada exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('not found')) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (error.message?.includes('Cannot add')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/acciones-correctivas/:id
|
||||||
|
* Update corrective action
|
||||||
|
*/
|
||||||
|
router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: UpdateCorrectiveActionDto = {};
|
||||||
|
if (req.body.actionType !== undefined) dto.actionType = req.body.actionType;
|
||||||
|
if (req.body.description !== undefined) dto.description = req.body.description;
|
||||||
|
if (req.body.responsibleId !== undefined) dto.responsibleId = req.body.responsibleId;
|
||||||
|
if (req.body.dueDate !== undefined) dto.dueDate = new Date(req.body.dueDate);
|
||||||
|
|
||||||
|
const action = await service.update(getContext(req), req.params.id, dto);
|
||||||
|
if (!action) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Accion correctiva no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: action,
|
||||||
|
message: 'Accion correctiva actualizada exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Cannot update')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/acciones-correctivas/:id/iniciar
|
||||||
|
* Start work on corrective action
|
||||||
|
*/
|
||||||
|
router.post('/:id/iniciar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = await service.startWork(getContext(req), req.params.id);
|
||||||
|
if (!action) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Accion correctiva no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: action,
|
||||||
|
message: 'Trabajo iniciado en accion correctiva',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Can only start')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/acciones-correctivas/:id/completar
|
||||||
|
* Mark corrective action as completed
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - completionNotes: (required) Notes about the completion
|
||||||
|
*/
|
||||||
|
router.post('/:id/completar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { completionNotes } = req.body;
|
||||||
|
if (!completionNotes) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'completionNotes es requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: CompleteActionDto = { completionNotes };
|
||||||
|
|
||||||
|
const action = await service.complete(getContext(req), req.params.id, dto);
|
||||||
|
if (!action) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Accion correctiva no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: action,
|
||||||
|
message: 'Accion correctiva marcada como completada',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Can only complete')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/acciones-correctivas/:id/verificar
|
||||||
|
* Verify effectiveness of corrective action
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - effectivenessVerified: (required) boolean indicating if action was effective
|
||||||
|
* - verificationNotes: (optional) Notes about verification
|
||||||
|
*/
|
||||||
|
router.post('/:id/verificar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { effectivenessVerified, verificationNotes } = req.body;
|
||||||
|
if (effectivenessVerified === undefined) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'effectivenessVerified es requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: VerifyActionDto = {
|
||||||
|
effectivenessVerified: Boolean(effectivenessVerified),
|
||||||
|
verificationNotes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = await service.verify(getContext(req), req.params.id, dto);
|
||||||
|
if (!action) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Accion correctiva no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: action,
|
||||||
|
message: action.effectivenessVerified
|
||||||
|
? 'Accion correctiva verificada como efectiva'
|
||||||
|
: 'Accion correctiva verificada como no efectiva',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Can only verify')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/acciones-correctivas/:id/reabrir
|
||||||
|
* Reopen completed or verified corrective action
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - reason: (required) Reason for reopening
|
||||||
|
*/
|
||||||
|
router.post('/:id/reabrir', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { reason } = req.body;
|
||||||
|
if (!reason) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'reason es requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = await service.reopen(getContext(req), req.params.id, reason);
|
||||||
|
if (!action) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Accion correctiva no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: action,
|
||||||
|
message: 'Accion correctiva reabierta exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Can only reopen')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/acciones-correctivas/:id
|
||||||
|
* Delete pending corrective action
|
||||||
|
*/
|
||||||
|
router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await service.delete(getContext(req), req.params.id);
|
||||||
|
if (!deleted) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Accion correctiva no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Accion correctiva eliminada exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Can only delete')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
@ -1,8 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* Quality Controllers Index
|
* Quality Controllers Index
|
||||||
* @module Quality
|
*
|
||||||
|
* Barrel file exporting all quality module controllers.
|
||||||
|
*
|
||||||
|
* @module Quality (MAI-009)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Inspection management
|
||||||
export * from './inspection.controller';
|
export * from './inspection.controller';
|
||||||
|
|
||||||
|
// Post-sale tickets
|
||||||
export * from './ticket.controller';
|
export * from './ticket.controller';
|
||||||
|
|
||||||
|
// Checklist management (English API)
|
||||||
export { default as checklistController } from './checklist.controller';
|
export { default as checklistController } from './checklist.controller';
|
||||||
|
|
||||||
|
// Non-conformity management
|
||||||
|
export * from './non-conformity.controller';
|
||||||
|
|
||||||
|
// Corrective actions (CAPA)
|
||||||
|
export * from './corrective-action.controller';
|
||||||
|
|
||||||
|
// Quality protocols (Spanish API for checklists)
|
||||||
|
export * from './protocolo-calidad.controller';
|
||||||
|
|||||||
@ -254,5 +254,81 @@ export function createInspectionController(dataSource: DataSource): Router {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/inspections/:id/checklist
|
||||||
|
* Get checklist associated with inspection
|
||||||
|
*/
|
||||||
|
router.get('/:id/checklist', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inspection = await service.findWithDetails(getContext(req), req.params.id);
|
||||||
|
if (!inspection) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Inspection not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
checklist: inspection.checklist,
|
||||||
|
results: inspection.results,
|
||||||
|
totalItems: inspection.totalItems,
|
||||||
|
passedItems: inspection.passedItems,
|
||||||
|
failedItems: inspection.failedItems,
|
||||||
|
passRate: inspection.passRate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/inspections/:id
|
||||||
|
* Update inspection
|
||||||
|
*/
|
||||||
|
router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inspection = await service.findById(getContext(req), req.params.id);
|
||||||
|
if (!inspection) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Inspection not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inspection.status !== 'pending') {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Can only update pending inspections' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update allowed fields
|
||||||
|
if (req.body.inspectionDate) {
|
||||||
|
inspection.inspectionDate = new Date(req.body.inspectionDate);
|
||||||
|
}
|
||||||
|
if (req.body.inspectorId) {
|
||||||
|
inspection.inspectorId = req.body.inspectorId;
|
||||||
|
}
|
||||||
|
if (req.body.notes !== undefined) {
|
||||||
|
inspection.notes = req.body.notes;
|
||||||
|
}
|
||||||
|
inspection.updatedById = req.user?.sub || '';
|
||||||
|
|
||||||
|
const updated = await inspectionRepo.save(inspection);
|
||||||
|
res.status(200).json({ success: true, data: updated });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
687
src/modules/quality/controllers/non-conformity.controller.ts
Normal file
687
src/modules/quality/controllers/non-conformity.controller.ts
Normal file
@ -0,0 +1,687 @@
|
|||||||
|
/**
|
||||||
|
* NonConformityController - REST API for non-conformities (No Conformidades)
|
||||||
|
*
|
||||||
|
* Endpoints para gestion de no conformidades detectadas en inspecciones de calidad.
|
||||||
|
*
|
||||||
|
* @module Quality (MAI-009)
|
||||||
|
* @routes /api/no-conformidades
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import {
|
||||||
|
NonConformityService,
|
||||||
|
NonConformityFilters,
|
||||||
|
CreateNonConformityDto,
|
||||||
|
UpdateNonConformityDto,
|
||||||
|
CloseNonConformityDto,
|
||||||
|
} from '../services/non-conformity.service';
|
||||||
|
import {
|
||||||
|
CorrectiveActionService,
|
||||||
|
CreateCorrectiveActionDto,
|
||||||
|
} from '../services/corrective-action.service';
|
||||||
|
import { NonConformity, NCSeverity, NCStatus } from '../entities/non-conformity.entity';
|
||||||
|
import { CorrectiveAction } from '../entities/corrective-action.entity';
|
||||||
|
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||||
|
import { AuthService } from '../../auth/services/auth.service';
|
||||||
|
import { User } from '../../core/entities/user.entity';
|
||||||
|
import { Tenant } from '../../core/entities/tenant.entity';
|
||||||
|
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||||
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the NonConformity controller with all REST endpoints
|
||||||
|
* @param dataSource - TypeORM DataSource for database operations
|
||||||
|
* @returns Express Router with all endpoints configured
|
||||||
|
*/
|
||||||
|
export function createNonConformityController(dataSource: DataSource): Router {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
const ncRepo = dataSource.getRepository(NonConformity);
|
||||||
|
const actionRepo = dataSource.getRepository(CorrectiveAction);
|
||||||
|
const userRepository = dataSource.getRepository(User);
|
||||||
|
const tenantRepository = dataSource.getRepository(Tenant);
|
||||||
|
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||||
|
|
||||||
|
// Services
|
||||||
|
const service = new NonConformityService(ncRepo, actionRepo);
|
||||||
|
const actionService = new CorrectiveActionService(actionRepo, ncRepo);
|
||||||
|
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||||
|
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to extract service context from request
|
||||||
|
*/
|
||||||
|
const getContext = (req: Request): ServiceContext => {
|
||||||
|
if (!req.tenantId) {
|
||||||
|
throw new Error('Tenant ID is required');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tenantId: req.tenantId,
|
||||||
|
userId: req.user?.sub,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/no-conformidades
|
||||||
|
* List non-conformities with filters and pagination
|
||||||
|
*
|
||||||
|
* Query params:
|
||||||
|
* - inspectionId: Filter by inspection
|
||||||
|
* - loteId: Filter by lote
|
||||||
|
* - contractorId: Filter by contractor
|
||||||
|
* - severity: Filter by severity (minor, major, critical)
|
||||||
|
* - status: Filter by status (open, in_progress, closed, verified)
|
||||||
|
* - category: Filter by category (partial match)
|
||||||
|
* - detectionDateFrom/To: Date range filters
|
||||||
|
* - dueDateFrom/To: Due date range filters
|
||||||
|
* - search: Search in ncNumber, description, category
|
||||||
|
* - page: Page number (default: 1)
|
||||||
|
* - limit: Items per page (default: 20, max: 100)
|
||||||
|
*/
|
||||||
|
router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters: NonConformityFilters = {};
|
||||||
|
if (req.query.inspectionId) filters.inspectionId = req.query.inspectionId as string;
|
||||||
|
if (req.query.loteId) filters.loteId = req.query.loteId as string;
|
||||||
|
if (req.query.contractorId) filters.contractorId = req.query.contractorId as string;
|
||||||
|
if (req.query.severity) filters.severity = req.query.severity as NCSeverity;
|
||||||
|
if (req.query.status) filters.status = req.query.status as NCStatus;
|
||||||
|
if (req.query.category) filters.category = req.query.category as string;
|
||||||
|
if (req.query.detectionDateFrom) filters.detectionDateFrom = new Date(req.query.detectionDateFrom as string);
|
||||||
|
if (req.query.detectionDateTo) filters.detectionDateTo = new Date(req.query.detectionDateTo as string);
|
||||||
|
if (req.query.dueDateFrom) filters.dueDateFrom = new Date(req.query.dueDateFrom as string);
|
||||||
|
if (req.query.dueDateTo) filters.dueDateTo = new Date(req.query.dueDateTo as string);
|
||||||
|
if (req.query.search) filters.search = req.query.search as string;
|
||||||
|
|
||||||
|
const page = parseInt(req.query.page as string) || 1;
|
||||||
|
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||||||
|
|
||||||
|
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result.data,
|
||||||
|
pagination: {
|
||||||
|
total: result.total,
|
||||||
|
page: result.page,
|
||||||
|
limit: result.limit,
|
||||||
|
totalPages: Math.ceil(result.total / result.limit),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/no-conformidades/estadisticas
|
||||||
|
* Get statistics for non-conformities
|
||||||
|
*/
|
||||||
|
router.get('/estadisticas', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = await service.getStatistics(getContext(req));
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: stats,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/no-conformidades/vencidas
|
||||||
|
* Get overdue non-conformities
|
||||||
|
*/
|
||||||
|
router.get('/vencidas', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overdueNCs = await service.getOverdueNCs(getContext(req));
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: overdueNCs,
|
||||||
|
count: overdueNCs.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/no-conformidades/por-severidad
|
||||||
|
* Get open non-conformities count by severity
|
||||||
|
*/
|
||||||
|
router.get('/por-severidad', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bySeverity = await service.getOpenBySeverity(getContext(req));
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: bySeverity,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/no-conformidades/por-contratista/:contractorId
|
||||||
|
* Get non-conformity statistics by contractor
|
||||||
|
*/
|
||||||
|
router.get('/por-contratista/:contractorId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = await service.getStatsByContractor(getContext(req), req.params.contractorId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: stats,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/no-conformidades/por-lote/:loteId
|
||||||
|
* Get non-conformities for a specific lote
|
||||||
|
*/
|
||||||
|
router.get('/por-lote/:loteId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ncs = await service.findByLote(getContext(req), req.params.loteId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: ncs,
|
||||||
|
count: ncs.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/no-conformidades/por-inspeccion/:inspectionId
|
||||||
|
* Get non-conformities for a specific inspection
|
||||||
|
*/
|
||||||
|
router.get('/por-inspeccion/:inspectionId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ncs = await service.findByInspection(getContext(req), req.params.inspectionId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: ncs,
|
||||||
|
count: ncs.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/no-conformidades/por-numero/:ncNumber
|
||||||
|
* Get non-conformity by number
|
||||||
|
*/
|
||||||
|
router.get('/por-numero/:ncNumber', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nc = await service.findByNumber(getContext(req), req.params.ncNumber);
|
||||||
|
if (!nc) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'No conformidad no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: nc,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/no-conformidades/:id
|
||||||
|
* Get non-conformity detail with relations
|
||||||
|
*/
|
||||||
|
router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nc = await service.findWithDetails(getContext(req), req.params.id);
|
||||||
|
if (!nc) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'No conformidad no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: nc,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/no-conformidades
|
||||||
|
* Create new non-conformity
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - loteId: (required) UUID of the lote
|
||||||
|
* - detectionDate: (required) Date of detection
|
||||||
|
* - category: (required) Category string
|
||||||
|
* - severity: (required) 'minor' | 'major' | 'critical'
|
||||||
|
* - description: (required) Description text
|
||||||
|
* - inspectionId: (optional) UUID of related inspection
|
||||||
|
* - rootCause: (optional) Root cause analysis
|
||||||
|
* - photoUrl: (optional) Evidence photo URL
|
||||||
|
* - contractorId: (optional) UUID of responsible contractor
|
||||||
|
* - dueDate: (optional) Due date (auto-calculated based on severity if not provided)
|
||||||
|
*/
|
||||||
|
router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
const { loteId, detectionDate, category, severity, description } = req.body;
|
||||||
|
if (!loteId || !detectionDate || !category || !severity || !description) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'loteId, detectionDate, category, severity y description son requeridos',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate severity enum
|
||||||
|
const validSeverities: NCSeverity[] = ['minor', 'major', 'critical'];
|
||||||
|
if (!validSeverities.includes(severity)) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: `Severidad invalida. Valores validos: ${validSeverities.join(', ')}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: CreateNonConformityDto = {
|
||||||
|
inspectionId: req.body.inspectionId,
|
||||||
|
loteId,
|
||||||
|
detectionDate: new Date(detectionDate),
|
||||||
|
category,
|
||||||
|
severity,
|
||||||
|
description,
|
||||||
|
rootCause: req.body.rootCause,
|
||||||
|
photoUrl: req.body.photoUrl,
|
||||||
|
contractorId: req.body.contractorId,
|
||||||
|
dueDate: req.body.dueDate ? new Date(req.body.dueDate) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nc = await service.create(getContext(req), dto);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: nc,
|
||||||
|
message: 'No conformidad registrada exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/no-conformidades/:id
|
||||||
|
* Update non-conformity
|
||||||
|
*/
|
||||||
|
router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: UpdateNonConformityDto = {};
|
||||||
|
if (req.body.loteId !== undefined) dto.loteId = req.body.loteId;
|
||||||
|
if (req.body.category !== undefined) dto.category = req.body.category;
|
||||||
|
if (req.body.severity !== undefined) dto.severity = req.body.severity;
|
||||||
|
if (req.body.description !== undefined) dto.description = req.body.description;
|
||||||
|
if (req.body.rootCause !== undefined) dto.rootCause = req.body.rootCause;
|
||||||
|
if (req.body.photoUrl !== undefined) dto.photoUrl = req.body.photoUrl;
|
||||||
|
if (req.body.contractorId !== undefined) dto.contractorId = req.body.contractorId;
|
||||||
|
if (req.body.dueDate !== undefined) dto.dueDate = new Date(req.body.dueDate);
|
||||||
|
|
||||||
|
const nc = await service.update(getContext(req), req.params.id, dto);
|
||||||
|
if (!nc) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'No conformidad no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: nc,
|
||||||
|
message: 'No conformidad actualizada exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Cannot update')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/no-conformidades/:id/accion-correctiva
|
||||||
|
* Add corrective action to non-conformity
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - actionType: (required) 'corrective' | 'preventive' | 'improvement'
|
||||||
|
* - description: (required) Action description
|
||||||
|
* - responsibleId: (required) UUID of responsible user
|
||||||
|
* - dueDate: (required) Due date for the action
|
||||||
|
*/
|
||||||
|
router.post('/:id/accion-correctiva', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { actionType, description, responsibleId, dueDate } = req.body;
|
||||||
|
if (!actionType || !description || !responsibleId || !dueDate) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'actionType, description, responsibleId y dueDate son requeridos',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: CreateCorrectiveActionDto = {
|
||||||
|
nonConformityId: req.params.id,
|
||||||
|
actionType,
|
||||||
|
description,
|
||||||
|
responsibleId,
|
||||||
|
dueDate: new Date(dueDate),
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = await actionService.create(getContext(req), dto);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: action,
|
||||||
|
message: 'Accion correctiva agregada exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('not found')) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (error.message?.includes('Cannot add')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/no-conformidades/:id/asignar-contratista
|
||||||
|
* Assign contractor to non-conformity
|
||||||
|
*/
|
||||||
|
router.post('/:id/asignar-contratista', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { contractorId } = req.body;
|
||||||
|
if (!contractorId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'contractorId es requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nc = await service.assignContractor(getContext(req), req.params.id, contractorId);
|
||||||
|
if (!nc) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'No conformidad no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: nc,
|
||||||
|
message: 'Contratista asignado exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Cannot assign')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/no-conformidades/:id/causa-raiz
|
||||||
|
* Set root cause analysis
|
||||||
|
*/
|
||||||
|
router.post('/:id/causa-raiz', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality', 'resident'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rootCause } = req.body;
|
||||||
|
if (!rootCause) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'rootCause es requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nc = await service.setRootCause(getContext(req), req.params.id, rootCause);
|
||||||
|
if (!nc) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'No conformidad no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: nc,
|
||||||
|
message: 'Causa raiz registrada exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Cannot modify')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/no-conformidades/:id/cerrar
|
||||||
|
* Close non-conformity
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - closurePhotoUrl: (optional) URL of closure evidence photo
|
||||||
|
* - closureNotes: (optional) Closure notes
|
||||||
|
*/
|
||||||
|
router.post('/:id/cerrar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: CloseNonConformityDto = {
|
||||||
|
closurePhotoUrl: req.body.closurePhotoUrl,
|
||||||
|
closureNotes: req.body.closureNotes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nc = await service.close(getContext(req), req.params.id, dto);
|
||||||
|
if (!nc) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'No conformidad no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: nc,
|
||||||
|
message: 'No conformidad cerrada exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Can only close') || error.message?.includes('Cannot close')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/no-conformidades/:id/verificar
|
||||||
|
* Verify closed non-conformity
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - verificationNotes: (optional) Verification notes
|
||||||
|
*/
|
||||||
|
router.post('/:id/verificar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto = {
|
||||||
|
verificationNotes: req.body.verificationNotes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nc = await service.verify(getContext(req), req.params.id, dto);
|
||||||
|
if (!nc) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'No conformidad no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: nc,
|
||||||
|
message: 'No conformidad verificada exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Can only verify') || error.message?.includes('Cannot verify')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/no-conformidades/:id/reabrir
|
||||||
|
* Reopen closed or verified non-conformity
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - reason: (required) Reason for reopening
|
||||||
|
*/
|
||||||
|
router.post('/:id/reabrir', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { reason } = req.body;
|
||||||
|
if (!reason) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'reason es requerido' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nc = await service.reopen(getContext(req), req.params.id, reason);
|
||||||
|
if (!nc) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'No conformidad no encontrada' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: nc,
|
||||||
|
message: 'No conformidad reabierta exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('Can only reopen')) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
617
src/modules/quality/controllers/protocolo-calidad.controller.ts
Normal file
617
src/modules/quality/controllers/protocolo-calidad.controller.ts
Normal file
@ -0,0 +1,617 @@
|
|||||||
|
/**
|
||||||
|
* ProtocoloCalidadController - REST API for quality protocols (Protocolos de Calidad)
|
||||||
|
*
|
||||||
|
* Wrapper controller que expone checklists como protocolos de calidad con endpoints en espanol.
|
||||||
|
* Un protocolo de calidad es esencialmente un checklist con su configuracion de items.
|
||||||
|
*
|
||||||
|
* @module Quality (MAI-009)
|
||||||
|
* @routes /api/protocolos-calidad
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import {
|
||||||
|
ChecklistService,
|
||||||
|
CreateChecklistDto,
|
||||||
|
UpdateChecklistDto,
|
||||||
|
ChecklistFilters,
|
||||||
|
} from '../services/checklist.service';
|
||||||
|
import { Checklist, ChecklistStage } from '../entities/checklist.entity';
|
||||||
|
import { ChecklistItem } from '../entities/checklist-item.entity';
|
||||||
|
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
||||||
|
import { AuthService } from '../../auth/services/auth.service';
|
||||||
|
import { User } from '../../core/entities/user.entity';
|
||||||
|
import { Tenant } from '../../core/entities/tenant.entity';
|
||||||
|
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||||
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the ProtocoloCalidad controller with all REST endpoints
|
||||||
|
* @param dataSource - TypeORM DataSource for database operations
|
||||||
|
* @returns Express Router with all endpoints configured
|
||||||
|
*/
|
||||||
|
export function createProtocoloCalidadController(dataSource: DataSource): Router {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
const checklistRepo = dataSource.getRepository(Checklist);
|
||||||
|
const itemRepo = dataSource.getRepository(ChecklistItem);
|
||||||
|
const userRepository = dataSource.getRepository(User);
|
||||||
|
const tenantRepository = dataSource.getRepository(Tenant);
|
||||||
|
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
||||||
|
|
||||||
|
// Services
|
||||||
|
const service = new ChecklistService(checklistRepo, itemRepo);
|
||||||
|
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
||||||
|
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to extract service context from request
|
||||||
|
*/
|
||||||
|
const getContext = (req: Request): ServiceContext => {
|
||||||
|
if (!req.tenantId) {
|
||||||
|
throw new Error('Tenant ID is required');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tenantId: req.tenantId,
|
||||||
|
userId: req.user?.sub,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid stages for quality protocols
|
||||||
|
*/
|
||||||
|
const VALID_STAGES: ChecklistStage[] = ['foundation', 'structure', 'installations', 'finishes', 'delivery', 'custom'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/protocolos-calidad
|
||||||
|
* List quality protocols (checklists) with filters and pagination
|
||||||
|
*
|
||||||
|
* Query params:
|
||||||
|
* - stage: Filter by construction stage
|
||||||
|
* - prototypeId: Filter by prototype
|
||||||
|
* - isActive: Filter active/inactive protocols
|
||||||
|
* - search: Search in name, code, description
|
||||||
|
* - page: Page number (default: 1)
|
||||||
|
* - limit: Items per page (default: 20)
|
||||||
|
*/
|
||||||
|
router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters: ChecklistFilters = {};
|
||||||
|
if (req.query.stage) filters.stage = req.query.stage as ChecklistStage;
|
||||||
|
if (req.query.prototypeId) filters.prototypeId = req.query.prototypeId as string;
|
||||||
|
if (req.query.isActive !== undefined) filters.isActive = req.query.isActive === 'true';
|
||||||
|
if (req.query.search) filters.search = req.query.search as string;
|
||||||
|
filters.page = parseInt(req.query.page as string) || 1;
|
||||||
|
filters.limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||||||
|
|
||||||
|
const result = await service.findAll(getContext(req), filters);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result.data,
|
||||||
|
pagination: {
|
||||||
|
total: result.total,
|
||||||
|
page: result.page,
|
||||||
|
limit: result.limit,
|
||||||
|
totalPages: Math.ceil(result.total / result.limit),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/protocolos-calidad/estadisticas
|
||||||
|
* Get statistics for quality protocols
|
||||||
|
*/
|
||||||
|
router.get('/estadisticas', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = await service.getStatistics(getContext(req));
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: stats,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/protocolos-calidad/por-etapa/:stage
|
||||||
|
* Get protocols by construction stage
|
||||||
|
*/
|
||||||
|
router.get('/por-etapa/:stage', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stage = req.params.stage as ChecklistStage;
|
||||||
|
if (!VALID_STAGES.includes(stage)) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: `Etapa invalida. Valores validos: ${VALID_STAGES.join(', ')}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocols = await service.findByStage(getContext(req), stage);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: protocols,
|
||||||
|
count: protocols.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/protocolos-calidad/por-codigo/:code
|
||||||
|
* Get protocol by code
|
||||||
|
*/
|
||||||
|
router.get('/por-codigo/:code', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = await service.findByCode(getContext(req), req.params.code);
|
||||||
|
if (!protocol) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Protocolo no encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: protocol,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/protocolos-calidad/:id
|
||||||
|
* Get protocol detail with checklist items
|
||||||
|
*/
|
||||||
|
router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = await service.findWithItems(getContext(req), req.params.id);
|
||||||
|
if (!protocol) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Protocolo no encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: protocol,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/protocolos-calidad
|
||||||
|
* Create new quality protocol
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - code: (required) Unique protocol code
|
||||||
|
* - name: (required) Protocol name
|
||||||
|
* - stage: (required) Construction stage
|
||||||
|
* - description: (optional) Protocol description
|
||||||
|
* - prototypeId: (optional) Associated prototype UUID
|
||||||
|
*/
|
||||||
|
router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code, name, stage, description, prototypeId } = req.body;
|
||||||
|
if (!code || !name || !stage) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'code, name y stage son requeridos',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VALID_STAGES.includes(stage)) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: `Etapa invalida. Valores validos: ${VALID_STAGES.join(', ')}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: CreateChecklistDto = {
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
stage,
|
||||||
|
description,
|
||||||
|
prototypeId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const protocol = await service.create(getContext(req), dto);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: protocol,
|
||||||
|
message: 'Protocolo de calidad creado exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('already exists')) {
|
||||||
|
res.status(409).json({ error: 'Conflict', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/protocolos-calidad/:id
|
||||||
|
* Update quality protocol
|
||||||
|
*/
|
||||||
|
router.put('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: UpdateChecklistDto = {};
|
||||||
|
if (req.body.name !== undefined) dto.name = req.body.name;
|
||||||
|
if (req.body.description !== undefined) dto.description = req.body.description;
|
||||||
|
if (req.body.stage !== undefined) {
|
||||||
|
if (!VALID_STAGES.includes(req.body.stage)) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: `Etapa invalida. Valores validos: ${VALID_STAGES.join(', ')}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dto.stage = req.body.stage;
|
||||||
|
}
|
||||||
|
if (req.body.prototypeId !== undefined) dto.prototypeId = req.body.prototypeId;
|
||||||
|
if (req.body.isActive !== undefined) dto.isActive = req.body.isActive;
|
||||||
|
|
||||||
|
const protocol = await service.update(getContext(req), req.params.id, dto);
|
||||||
|
if (!protocol) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Protocolo no encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: protocol,
|
||||||
|
message: 'Protocolo actualizado exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/protocolos-calidad/:id/activar
|
||||||
|
* Activate quality protocol
|
||||||
|
*/
|
||||||
|
router.post('/:id/activar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = await service.activate(getContext(req), req.params.id);
|
||||||
|
if (!protocol) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Protocolo no encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: protocol,
|
||||||
|
message: 'Protocolo activado exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/protocolos-calidad/:id/desactivar
|
||||||
|
* Deactivate quality protocol
|
||||||
|
*/
|
||||||
|
router.post('/:id/desactivar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = await service.deactivate(getContext(req), req.params.id);
|
||||||
|
if (!protocol) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Protocolo no encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: protocol,
|
||||||
|
message: 'Protocolo desactivado exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/protocolos-calidad/:id/duplicar
|
||||||
|
* Duplicate quality protocol
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - newCode: (required) Code for the new protocol
|
||||||
|
* - newName: (required) Name for the new protocol
|
||||||
|
*/
|
||||||
|
router.post('/:id/duplicar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newCode, newName } = req.body;
|
||||||
|
if (!newCode || !newName) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'newCode y newName son requeridos',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = await service.duplicate(getContext(req), req.params.id, newCode, newName);
|
||||||
|
if (!protocol) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Protocolo no encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: protocol,
|
||||||
|
message: 'Protocolo duplicado exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('already exists')) {
|
||||||
|
res.status(409).json({ error: 'Conflict', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/protocolos-calidad/:id
|
||||||
|
* Soft delete quality protocol
|
||||||
|
*/
|
||||||
|
router.delete('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await service.softDelete(getContext(req), req.params.id);
|
||||||
|
if (!deleted) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Protocolo no encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Protocolo eliminado exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// ITEMS ENDPOINTS (Checklist items as protocol criteria)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/protocolos-calidad/:id/criterios
|
||||||
|
* Add criterion (checklist item) to protocol
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - sequenceNumber: (required) Order number
|
||||||
|
* - category: (required) Category/section name
|
||||||
|
* - description: (required) Criterion description
|
||||||
|
* - isCritical: (optional) Is this a critical criterion
|
||||||
|
* - requiresPhoto: (optional) Does this require photo evidence
|
||||||
|
* - acceptanceCriteria: (optional) Acceptance criteria text
|
||||||
|
*/
|
||||||
|
router.post('/:id/criterios', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sequenceNumber, category, description, isCritical, requiresPhoto, acceptanceCriteria } = req.body;
|
||||||
|
if (sequenceNumber === undefined || !category || !description) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'sequenceNumber, category y description son requeridos',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = await service.addItem(getContext(req), req.params.id, {
|
||||||
|
sequenceNumber,
|
||||||
|
category,
|
||||||
|
description,
|
||||||
|
isCritical,
|
||||||
|
requiresPhoto,
|
||||||
|
acceptanceCriteria,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: item,
|
||||||
|
message: 'Criterio agregado exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes('not found')) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/protocolos-calidad/:protocolId/criterios/:itemId
|
||||||
|
* Update criterion in protocol
|
||||||
|
*/
|
||||||
|
router.patch('/:protocolId/criterios/:itemId', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: any = {};
|
||||||
|
if (req.body.sequenceNumber !== undefined) dto.sequenceNumber = req.body.sequenceNumber;
|
||||||
|
if (req.body.category !== undefined) dto.category = req.body.category;
|
||||||
|
if (req.body.description !== undefined) dto.description = req.body.description;
|
||||||
|
if (req.body.isCritical !== undefined) dto.isCritical = req.body.isCritical;
|
||||||
|
if (req.body.requiresPhoto !== undefined) dto.requiresPhoto = req.body.requiresPhoto;
|
||||||
|
if (req.body.acceptanceCriteria !== undefined) dto.acceptanceCriteria = req.body.acceptanceCriteria;
|
||||||
|
if (req.body.isActive !== undefined) dto.isActive = req.body.isActive;
|
||||||
|
|
||||||
|
const item = await service.updateItem(getContext(req), req.params.itemId, dto);
|
||||||
|
if (!item) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Criterio no encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: item,
|
||||||
|
message: 'Criterio actualizado exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/protocolos-calidad/:protocolId/criterios/:itemId
|
||||||
|
* Remove criterion from protocol
|
||||||
|
*/
|
||||||
|
router.delete('/:protocolId/criterios/:itemId', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await service.removeItem(getContext(req), req.params.itemId);
|
||||||
|
if (!deleted) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Criterio no encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Criterio eliminado exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/protocolos-calidad/:id/criterios/reordenar
|
||||||
|
* Reorder criteria in protocol
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - itemOrders: Array of { itemId: string, sequenceNumber: number }
|
||||||
|
*/
|
||||||
|
router.post('/:id/criterios/reordenar', authMiddleware.authenticate, authMiddleware.authorize('admin', 'quality'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tenantId = req.tenantId;
|
||||||
|
if (!tenantId) {
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { itemOrders } = req.body;
|
||||||
|
if (!itemOrders || !Array.isArray(itemOrders)) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'itemOrders es requerido y debe ser un array',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await service.reorderItems(getContext(req), req.params.id, itemOrders);
|
||||||
|
if (!success) {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'Protocolo no encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Criterios reordenados exitosamente',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user