[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
|
||||
* @module Quality
|
||||
*
|
||||
* Barrel file exporting all quality module controllers.
|
||||
*
|
||||
* @module Quality (MAI-009)
|
||||
*/
|
||||
|
||||
// Inspection management
|
||||
export * from './inspection.controller';
|
||||
|
||||
// Post-sale tickets
|
||||
export * from './ticket.controller';
|
||||
|
||||
// Checklist management (English API)
|
||||
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;
|
||||
}
|
||||
|
||||
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