- ViajesService: Trip management with state machine, dispatch, delivery - UnidadesService: Fleet units management, availability, maintenance alerts - OperadoresService: Operators management, license tracking - TrackingService: GPS events, geofences, position history - OrdenesTransporteService: Transport orders with workflow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
272 lines
7.3 KiB
TypeScript
272 lines
7.3 KiB
TypeScript
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<CreateUnidadDto> {
|
|
estado?: EstadoUnidad;
|
|
}
|
|
|
|
export class UnidadesService {
|
|
constructor(private readonly unidadRepository: Repository<Unidad>) {}
|
|
|
|
async findAll(params: UnidadSearchParams): Promise<{ data: Unidad[]; total: number }> {
|
|
const {
|
|
tenantId,
|
|
search,
|
|
tipo,
|
|
estado,
|
|
estados,
|
|
sucursalId,
|
|
limit = 50,
|
|
offset = 0,
|
|
} = params;
|
|
|
|
const where: FindOptionsWhere<Unidad>[] = [];
|
|
const baseWhere: FindOptionsWhere<Unidad> = { 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<Unidad | null> {
|
|
return this.unidadRepository.findOne({
|
|
where: { id, tenantId },
|
|
});
|
|
}
|
|
|
|
async findByNumeroEconomico(numeroEconomico: string, tenantId: string): Promise<Unidad | null> {
|
|
return this.unidadRepository.findOne({
|
|
where: { numeroEconomico, tenantId },
|
|
});
|
|
}
|
|
|
|
async findByPlacas(placas: string, tenantId: string): Promise<Unidad | null> {
|
|
return this.unidadRepository.findOne({
|
|
where: { placas, tenantId },
|
|
});
|
|
}
|
|
|
|
async create(tenantId: string, dto: CreateUnidadDto, createdBy: string): Promise<Unidad> {
|
|
// 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<Unidad | null> {
|
|
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<Unidad | null> {
|
|
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<Unidad | null> {
|
|
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<Unidad[]> {
|
|
const where: FindOptionsWhere<Unidad> = {
|
|
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<Unidad[]> {
|
|
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<Unidad[]> {
|
|
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<boolean> {
|
|
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;
|
|
}
|
|
}
|