- construction/torre: Service + Controller for vertical buildings - construction/nivel: Service + Controller for floor levels - construction/departamento: Service + Controller for units - purchase/purchase-order-construction: Controller for PO extensions - purchase/supplier-construction: Controller for supplier extensions - quality/checklist: Controller for inspection templates All endpoints follow existing patterns with multi-tenancy support. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
419 lines
13 KiB
TypeScript
419 lines
13 KiB
TypeScript
/**
|
|
* SupplierConstruction Controller
|
|
* API endpoints para extensión de proveedores de construcción
|
|
*
|
|
* @module Purchase (MAI-004)
|
|
* @prefix /api/v1/suppliers-construction
|
|
*/
|
|
|
|
import { Router, Request, Response, NextFunction } from 'express';
|
|
import {
|
|
SupplierConstructionService,
|
|
CreateSupplierConstructionDto,
|
|
UpdateSupplierConstructionDto,
|
|
EvaluateSupplierDto,
|
|
} from '../services/supplier-construction.service';
|
|
|
|
const router = Router();
|
|
const supplierConstructionService = new SupplierConstructionService(null as any);
|
|
|
|
/**
|
|
* GET /api/v1/suppliers-construction
|
|
* Lista todas las extensiones de proveedores del tenant
|
|
*/
|
|
router.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const { isMaterialsSupplier, isServicesSupplier, isEquipmentSupplier, minRating, hasValidDocuments, specialty, page, limit } = req.query;
|
|
|
|
const result = await supplierConstructionService.findAll(
|
|
{ tenantId },
|
|
{
|
|
isMaterialsSupplier: isMaterialsSupplier !== undefined ? isMaterialsSupplier === 'true' : undefined,
|
|
isServicesSupplier: isServicesSupplier !== undefined ? isServicesSupplier === 'true' : undefined,
|
|
isEquipmentSupplier: isEquipmentSupplier !== undefined ? isEquipmentSupplier === 'true' : undefined,
|
|
minRating: minRating ? Number(minRating) : undefined,
|
|
hasValidDocuments: hasValidDocuments !== undefined ? hasValidDocuments === 'true' : undefined,
|
|
specialty: specialty as string,
|
|
page: page ? Number(page) : undefined,
|
|
limit: limit ? Number(limit) : undefined,
|
|
},
|
|
);
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: result.data,
|
|
total: result.total,
|
|
page: result.page,
|
|
limit: result.limit,
|
|
});
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/suppliers-construction/statistics
|
|
* Estadísticas de proveedores
|
|
*/
|
|
router.get('/statistics', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const stats = await supplierConstructionService.getSupplierStatistics({ tenantId });
|
|
return res.json({ success: true, data: stats });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/suppliers-construction/top-rated
|
|
* Proveedores mejor calificados
|
|
*/
|
|
router.get('/top-rated', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const { limit } = req.query;
|
|
const data = await supplierConstructionService.findTopRated({ tenantId }, limit ? Number(limit) : 10);
|
|
|
|
return res.json({ success: true, data, count: data.length });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/suppliers-construction/by-specialty/:specialty
|
|
* Proveedores por especialidad
|
|
*/
|
|
router.get('/by-specialty/:specialty', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const data = await supplierConstructionService.findBySpecialty({ tenantId }, req.params.specialty);
|
|
return res.json({ success: true, data, count: data.length });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/suppliers-construction/materials
|
|
* Proveedores de materiales
|
|
*/
|
|
router.get('/materials', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const data = await supplierConstructionService.findMaterialsSuppliers({ tenantId });
|
|
return res.json({ success: true, data, count: data.length });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/suppliers-construction/services
|
|
* Proveedores de servicios
|
|
*/
|
|
router.get('/services', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const data = await supplierConstructionService.findServicesSuppliers({ tenantId });
|
|
return res.json({ success: true, data, count: data.length });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/suppliers-construction/equipment
|
|
* Proveedores de equipos
|
|
*/
|
|
router.get('/equipment', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const data = await supplierConstructionService.findEquipmentSuppliers({ tenantId });
|
|
return res.json({ success: true, data, count: data.length });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/suppliers-construction/expiring-documents
|
|
* Proveedores con documentos por vencer
|
|
*/
|
|
router.get('/expiring-documents', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const { daysAhead } = req.query;
|
|
const data = await supplierConstructionService.findWithExpiringDocuments(
|
|
{ tenantId },
|
|
daysAhead ? Number(daysAhead) : 30,
|
|
);
|
|
|
|
return res.json({ success: true, data, count: data.length });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/suppliers-construction/by-supplier/:supplierId
|
|
* Obtiene extensión por ID de proveedor original
|
|
*/
|
|
router.get('/by-supplier/:supplierId', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const extension = await supplierConstructionService.findBySupplierId({ tenantId }, req.params.supplierId);
|
|
if (!extension) {
|
|
return res.status(404).json({ error: 'Extensión de proveedor no encontrada' });
|
|
}
|
|
|
|
return res.json({ success: true, data: extension });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v1/suppliers-construction/:id
|
|
* Obtiene una extensión de proveedor por ID
|
|
*/
|
|
router.get('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const extension = await supplierConstructionService.findById({ tenantId }, req.params.id);
|
|
if (!extension) {
|
|
return res.status(404).json({ error: 'Extensión de proveedor no encontrada' });
|
|
}
|
|
|
|
return res.json({ success: true, data: extension });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v1/suppliers-construction
|
|
* Crea una nueva extensión de proveedor
|
|
*/
|
|
router.post('/', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const userId = (req as any).user?.id;
|
|
const dto: CreateSupplierConstructionDto = req.body;
|
|
|
|
if (!dto.supplierId) {
|
|
return res.status(400).json({ error: 'supplierId es requerido' });
|
|
}
|
|
|
|
const extension = await supplierConstructionService.create({ tenantId, userId }, dto);
|
|
return res.status(201).json({ success: true, data: extension });
|
|
} catch (error: any) {
|
|
if (error.message?.includes('already exists')) {
|
|
return res.status(409).json({ error: error.message });
|
|
}
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/v1/suppliers-construction/:id
|
|
* Actualiza una extensión de proveedor
|
|
*/
|
|
router.patch('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const userId = (req as any).user?.id;
|
|
const dto: UpdateSupplierConstructionDto = req.body;
|
|
|
|
const extension = await supplierConstructionService.update({ tenantId, userId }, req.params.id, dto);
|
|
if (!extension) {
|
|
return res.status(404).json({ error: 'Extensión de proveedor no encontrada' });
|
|
}
|
|
|
|
return res.json({ success: true, data: extension });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v1/suppliers-construction/:id/evaluate
|
|
* Evalúa un proveedor
|
|
*/
|
|
router.post('/:id/evaluate', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const userId = (req as any).user?.id;
|
|
const dto: EvaluateSupplierDto = req.body;
|
|
|
|
if (dto.qualityRating === undefined || dto.deliveryRating === undefined || dto.priceRating === undefined) {
|
|
return res.status(400).json({ error: 'qualityRating, deliveryRating y priceRating son requeridos' });
|
|
}
|
|
|
|
const extension = await supplierConstructionService.evaluate({ tenantId, userId }, req.params.id, dto);
|
|
if (!extension) {
|
|
return res.status(404).json({ error: 'Extensión de proveedor no encontrada' });
|
|
}
|
|
|
|
return res.json({ success: true, data: extension, message: 'Evaluación registrada' });
|
|
} catch (error: any) {
|
|
if (error.message?.includes('between')) {
|
|
return res.status(400).json({ error: error.message });
|
|
}
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/v1/suppliers-construction/:id/documents
|
|
* Actualiza el estado de documentos de un proveedor
|
|
*/
|
|
router.patch('/:id/documents', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const userId = (req as any).user?.id;
|
|
const { hasValidDocuments, expiryDate } = req.body;
|
|
|
|
if (hasValidDocuments === undefined) {
|
|
return res.status(400).json({ error: 'hasValidDocuments es requerido' });
|
|
}
|
|
|
|
const extension = await supplierConstructionService.updateDocumentStatus(
|
|
{ tenantId, userId },
|
|
req.params.id,
|
|
hasValidDocuments,
|
|
expiryDate ? new Date(expiryDate) : undefined,
|
|
);
|
|
|
|
if (!extension) {
|
|
return res.status(404).json({ error: 'Extensión de proveedor no encontrada' });
|
|
}
|
|
|
|
return res.json({ success: true, data: extension, message: 'Estado de documentos actualizado' });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/v1/suppliers-construction/:id/credit
|
|
* Actualiza los términos de crédito de un proveedor
|
|
*/
|
|
router.patch('/:id/credit', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const userId = (req as any).user?.id;
|
|
const { creditLimit, paymentDays } = req.body;
|
|
|
|
if (creditLimit === undefined || paymentDays === undefined) {
|
|
return res.status(400).json({ error: 'creditLimit y paymentDays son requeridos' });
|
|
}
|
|
|
|
const extension = await supplierConstructionService.updateCreditTerms(
|
|
{ tenantId, userId },
|
|
req.params.id,
|
|
Number(creditLimit),
|
|
Number(paymentDays),
|
|
);
|
|
|
|
if (!extension) {
|
|
return res.status(404).json({ error: 'Extensión de proveedor no encontrada' });
|
|
}
|
|
|
|
return res.json({ success: true, data: extension, message: 'Términos de crédito actualizados' });
|
|
} catch (error: any) {
|
|
if (error.message?.includes('cannot be negative')) {
|
|
return res.status(400).json({ error: error.message });
|
|
}
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/v1/suppliers-construction/:id
|
|
* Elimina una extensión de proveedor (soft delete)
|
|
*/
|
|
router.delete('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const tenantId = req.headers['x-tenant-id'] as string;
|
|
if (!tenantId) {
|
|
return res.status(400).json({ error: 'X-Tenant-Id header required' });
|
|
}
|
|
|
|
const userId = (req as any).user?.id;
|
|
const deleted = await supplierConstructionService.softDelete({ tenantId, userId }, req.params.id);
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ error: 'Extensión de proveedor no encontrada' });
|
|
}
|
|
|
|
return res.json({ success: true, message: 'Extensión de proveedor eliminada' });
|
|
} catch (error) {
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
export default router;
|