import { Repository, FindOptionsWhere, ILike, In } from 'typeorm'; import { Unidad, TipoUnidad, EstadoUnidad } from '../entities'; export interface UnidadSearchParams { tenantId: string; search?: string; tipo?: TipoUnidad; estado?: EstadoUnidad; estados?: EstadoUnidad[]; sucursalId?: string; limit?: number; offset?: number; } export interface CreateUnidadDto { numeroEconomico: string; tipo: TipoUnidad; marca: string; modelo: string; anio: number; placas: string; numeroSerie?: string; numeroMotor?: string; color?: string; capacidadCarga?: number; pesoNeto?: number; pesoBrutoVehicular?: number; numEjes?: number; numLlantas?: number; tipoCarroceria?: string; sucursalId?: string; permisoSct?: string; numPermisoSct?: string; configVehicular?: string; polizaSeguro?: string; aseguradora?: string; vigenciaSeguroInicio?: Date; vigenciaSeguroFin?: Date; verificacionVigencia?: Date; odometroActual?: number; rendimientoEsperado?: number; } export interface UpdateUnidadDto extends Partial { estado?: EstadoUnidad; } export class UnidadesService { constructor(private readonly unidadRepository: Repository) {} async findAll(params: UnidadSearchParams): Promise<{ data: Unidad[]; total: number }> { const { tenantId, search, tipo, estado, estados, sucursalId, limit = 50, offset = 0, } = params; const where: FindOptionsWhere[] = []; const baseWhere: FindOptionsWhere = { tenantId }; if (tipo) { baseWhere.tipo = tipo; } if (estado) { baseWhere.estado = estado; } if (estados && estados.length > 0) { baseWhere.estado = In(estados); } if (sucursalId) { baseWhere.sucursalId = sucursalId; } if (search) { where.push( { ...baseWhere, numeroEconomico: ILike(`%${search}%`) }, { ...baseWhere, placas: ILike(`%${search}%`) }, { ...baseWhere, marca: ILike(`%${search}%`) }, { ...baseWhere, modelo: ILike(`%${search}%`) } ); } else { where.push(baseWhere); } const [data, total] = await this.unidadRepository.findAndCount({ where, take: limit, skip: offset, order: { numeroEconomico: 'ASC' }, }); return { data, total }; } async findOne(id: string, tenantId: string): Promise { return this.unidadRepository.findOne({ where: { id, tenantId }, }); } async findByNumeroEconomico(numeroEconomico: string, tenantId: string): Promise { return this.unidadRepository.findOne({ where: { numeroEconomico, tenantId }, }); } async findByPlacas(placas: string, tenantId: string): Promise { return this.unidadRepository.findOne({ where: { placas, tenantId }, }); } async create(tenantId: string, dto: CreateUnidadDto, createdBy: string): Promise { // Check for existing numero económico const existingNumero = await this.findByNumeroEconomico(dto.numeroEconomico, tenantId); if (existingNumero) { throw new Error('Ya existe una unidad con este número económico'); } // Check for existing placas const existingPlacas = await this.findByPlacas(dto.placas, tenantId); if (existingPlacas) { throw new Error('Ya existe una unidad con estas placas'); } const unidad = this.unidadRepository.create({ ...dto, tenantId, estado: EstadoUnidad.DISPONIBLE, createdById: createdBy, }); return this.unidadRepository.save(unidad); } async update( id: string, tenantId: string, dto: UpdateUnidadDto, updatedBy: string ): Promise { const unidad = await this.findOne(id, tenantId); if (!unidad) return null; // If changing numero económico, check for duplicates if (dto.numeroEconomico && dto.numeroEconomico !== unidad.numeroEconomico) { const existing = await this.findByNumeroEconomico(dto.numeroEconomico, tenantId); if (existing) { throw new Error('Ya existe una unidad con este número económico'); } } // If changing placas, check for duplicates if (dto.placas && dto.placas !== unidad.placas) { const existing = await this.findByPlacas(dto.placas, tenantId); if (existing && existing.id !== id) { throw new Error('Ya existe una unidad con estas placas'); } } Object.assign(unidad, { ...dto, updatedById: updatedBy, }); return this.unidadRepository.save(unidad); } async cambiarEstado( id: string, tenantId: string, nuevoEstado: EstadoUnidad, updatedBy: string ): Promise { const unidad = await this.findOne(id, tenantId); if (!unidad) return null; unidad.estado = nuevoEstado; unidad.updatedById = updatedBy; return this.unidadRepository.save(unidad); } async actualizarOdometro( id: string, tenantId: string, odometro: number, updatedBy: string ): Promise { const unidad = await this.findOne(id, tenantId); if (!unidad) return null; if (odometro < (unidad.odometroActual || 0)) { throw new Error('El odómetro no puede ser menor al actual'); } unidad.odometroActual = odometro; unidad.updatedById = updatedBy; return this.unidadRepository.save(unidad); } // Unidades disponibles async getUnidadesDisponibles(tenantId: string, tipo?: TipoUnidad): Promise { const where: FindOptionsWhere = { tenantId, estado: EstadoUnidad.DISPONIBLE, }; if (tipo) { where.tipo = tipo; } return this.unidadRepository.find({ where, order: { numeroEconomico: 'ASC' }, }); } // Unidades con seguro por vencer async getUnidadesSeguroPorVencer(tenantId: string, diasAntelacion: number = 30): Promise { const fechaLimite = new Date(); fechaLimite.setDate(fechaLimite.getDate() + diasAntelacion); const unidades = await this.unidadRepository .createQueryBuilder('u') .where('u.tenant_id = :tenantId', { tenantId }) .andWhere('u.vigencia_seguro_fin <= :fechaLimite', { fechaLimite }) .andWhere('u.vigencia_seguro_fin > :hoy', { hoy: new Date() }) .orderBy('u.vigencia_seguro_fin', 'ASC') .getMany(); return unidades; } // Unidades con verificación por vencer async getUnidadesVerificacionPorVencer(tenantId: string, diasAntelacion: number = 30): Promise { const fechaLimite = new Date(); fechaLimite.setDate(fechaLimite.getDate() + diasAntelacion); const unidades = await this.unidadRepository .createQueryBuilder('u') .where('u.tenant_id = :tenantId', { tenantId }) .andWhere('u.verificacion_vigencia <= :fechaLimite', { fechaLimite }) .andWhere('u.verificacion_vigencia > :hoy', { hoy: new Date() }) .orderBy('u.verificacion_vigencia', 'ASC') .getMany(); return unidades; } async delete(id: string, tenantId: string): Promise { const unidad = await this.findOne(id, tenantId); if (!unidad) return false; if (unidad.estado === EstadoUnidad.EN_RUTA) { throw new Error('No se puede eliminar una unidad en ruta'); } const result = await this.unidadRepository.softDelete(id); return (result.affected ?? 0) > 0; } }