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>
361 lines
10 KiB
TypeScript
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;
|
|
}
|