erp-construccion-backend-v2/src/modules/assets/controllers/asset.controller.ts
Adrian Flores Cortes 5f9c30d268 [MAE-015] feat: Implement assets module backend
Complete implementation of the Assets/Machinery/Maintenance module:

Entities (10):
- AssetCategory: Hierarchical asset categorization with depreciation config
- Asset: Main asset entity (machinery, vehicles, tools, equipment)
- AssetAssignment: Asset-to-project assignments tracking
- WorkOrder: Maintenance work orders with workflow
- WorkOrderPart: Parts/materials used in work orders
- MaintenancePlan: Preventive maintenance plans
- MaintenanceHistory: Historical maintenance records
- FuelLog: Fuel consumption tracking with efficiency calculation
- AssetCost: TCO (Total Cost of Ownership) tracking

Services (3):
- AssetService: CRUD, assignments, categories, statistics
- WorkOrderService: CRUD, workflow (start/hold/resume/complete/cancel), parts
- FuelLogService: CRUD, efficiency calculation, statistics

Controllers (3):
- AssetController: REST API for assets, assignments, categories
- WorkOrderController: REST API for work orders, workflow, plans
- FuelLogController: REST API for fuel logs, statistics

Features:
- Multi-tenant support with tenant_id
- Complete workflow for work orders (draft→scheduled→in_progress→completed)
- Automatic efficiency calculation for fuel consumption
- Asset assignment history tracking
- Maintenance plan generation
- TCO tracking by cost type

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 05:41:54 -06:00

361 lines
10 KiB
TypeScript

/**
* AssetController - Controlador de Activos
*
* Endpoints para gestión de activos fijos, maquinaria y equipos.
*
* @module Assets (MAE-015)
*/
import { Router, Request, Response } from 'express';
import { DataSource } from 'typeorm';
import { AssetService } from '../services';
export function createAssetController(dataSource: DataSource): Router {
const router = Router();
const service = new AssetService(dataSource);
// ==================== CRUD DE ACTIVOS ====================
/**
* GET /
* Lista activos con filtros y paginación
*/
router.get('/', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = (req as any).user?.id;
const filters = {
assetType: req.query.assetType as any,
status: req.query.status as any,
ownershipType: req.query.ownershipType as any,
categoryId: req.query.categoryId as string,
projectId: req.query.projectId as string,
search: req.query.search as string,
tags: req.query.tags ? (req.query.tags as string).split(',') : undefined,
};
const pagination = {
page: parseInt(req.query.page as string) || 1,
limit: parseInt(req.query.limit as string) || 20,
};
const result = await service.findAll(tenantId, filters, pagination);
res.json(result);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* GET /statistics
* Obtiene estadísticas de activos
*/
router.get('/statistics', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const stats = await service.getStatistics(tenantId);
res.json(stats);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* GET /maintenance-due
* Lista activos que requieren mantenimiento
*/
router.get('/maintenance-due', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const assets = await service.getAssetsNeedingMaintenance(tenantId);
res.json(assets);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* GET /search
* Búsqueda de activos para autocomplete
*/
router.get('/search', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const query = req.query.q as string;
const limit = parseInt(req.query.limit as string) || 10;
if (!query) {
return res.status(400).json({ error: 'Se requiere parámetro de búsqueda (q)' });
}
const assets = await service.search(tenantId, query, limit);
res.json(assets);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* GET /by-code/:code
* Obtiene un activo por código
*/
router.get('/by-code/:code', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const asset = await service.findByCode(tenantId, req.params.code);
if (!asset) {
return res.status(404).json({ error: 'Activo no encontrado' });
}
res.json(asset);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* GET /:id
* Obtiene un activo por ID
*/
router.get('/:id', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const asset = await service.findById(tenantId, req.params.id);
if (!asset) {
return res.status(404).json({ error: 'Activo no encontrado' });
}
res.json(asset);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* POST /
* Crea un nuevo activo
*/
router.post('/', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = (req as any).user?.id;
const asset = await service.create(tenantId, req.body, userId);
res.status(201).json(asset);
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
});
/**
* PUT /:id
* Actualiza un activo
*/
router.put('/:id', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = (req as any).user?.id;
const asset = await service.update(tenantId, req.params.id, req.body, userId);
if (!asset) {
return res.status(404).json({ error: 'Activo no encontrado' });
}
res.json(asset);
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
});
/**
* PATCH /:id/status
* Actualiza el estado de un activo
*/
router.patch('/:id/status', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = (req as any).user?.id;
const { status } = req.body;
if (!status) {
return res.status(400).json({ error: 'Se requiere el nuevo estado' });
}
const asset = await service.updateStatus(tenantId, req.params.id, status, userId);
if (!asset) {
return res.status(404).json({ error: 'Activo no encontrado' });
}
res.json(asset);
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
});
/**
* PATCH /:id/usage
* Actualiza métricas de uso (horas/kilómetros)
*/
router.patch('/:id/usage', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = (req as any).user?.id;
const { hours, kilometers } = req.body;
const asset = await service.updateUsage(tenantId, req.params.id, hours, kilometers, userId);
if (!asset) {
return res.status(404).json({ error: 'Activo no encontrado' });
}
res.json(asset);
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
});
/**
* DELETE /:id
* Elimina un activo (soft delete)
*/
router.delete('/:id', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = (req as any).user?.id;
const deleted = await service.delete(tenantId, req.params.id, userId);
if (!deleted) {
return res.status(404).json({ error: 'Activo no encontrado' });
}
res.status(204).send();
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
});
// ==================== ASIGNACIONES ====================
/**
* GET /:id/assignment
* Obtiene la asignación actual de un activo
*/
router.get('/:id/assignment', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const assignment = await service.getCurrentAssignment(tenantId, req.params.id);
res.json(assignment);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* GET /:id/assignments
* Obtiene el historial de asignaciones de un activo
*/
router.get('/:id/assignments', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const pagination = {
page: parseInt(req.query.page as string) || 1,
limit: parseInt(req.query.limit as string) || 20,
};
const history = await service.getAssignmentHistory(tenantId, req.params.id, pagination);
res.json(history);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* POST /:id/assign
* Asigna un activo a un proyecto
*/
router.post('/:id/assign', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = (req as any).user?.id;
const dto = {
...req.body,
assetId: req.params.id,
};
const assignment = await service.assignToProject(tenantId, dto, userId);
res.status(201).json(assignment);
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
});
/**
* POST /:id/return
* Retorna un activo de un proyecto
*/
router.post('/:id/return', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = (req as any).user?.id;
const { endDate } = req.body;
const result = await service.returnFromProject(
tenantId,
req.params.id,
endDate ? new Date(endDate) : new Date(),
userId
);
if (!result) {
return res.status(400).json({ error: 'No hay asignación activa para este activo' });
}
res.json({ success: true, message: 'Activo retornado exitosamente' });
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
});
// ==================== CATEGORÍAS ====================
/**
* GET /categories
* Lista categorías de activos
*/
router.get('/categories/list', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const categories = await service.getCategories(tenantId);
res.json(categories);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* POST /categories
* Crea una nueva categoría
*/
router.post('/categories', async (req: Request, res: Response) => {
try {
const tenantId = req.headers['x-tenant-id'] as string;
const userId = (req as any).user?.id;
const category = await service.createCategory(tenantId, req.body, userId);
res.status(201).json(category);
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
});
return router;
}