erp-construccion-backend/src/modules/progress/services/bitacora-obra.service.ts

210 lines
5.2 KiB
TypeScript

/**
* BitacoraObraService - Bitácora de Obra
*
* Gestiona el registro diario de bitácora de obra.
* Genera automáticamente el número de entrada secuencial.
*
* @module Progress
*/
import { Repository } from 'typeorm';
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
import { BitacoraObra } from '../entities/bitacora-obra.entity';
export interface CreateBitacoraDto {
fraccionamientoId: string;
entryDate: Date;
weather?: string;
temperatureMax?: number;
temperatureMin?: number;
workersCount?: number;
description: string;
observations?: string;
incidents?: string;
}
export interface UpdateBitacoraDto {
weather?: string;
temperatureMax?: number;
temperatureMin?: number;
workersCount?: number;
description?: string;
observations?: string;
incidents?: string;
}
export interface BitacoraFilters {
dateFrom?: Date;
dateTo?: Date;
hasIncidents?: boolean;
}
export class BitacoraObraService extends BaseService<BitacoraObra> {
constructor(repository: Repository<BitacoraObra>) {
super(repository);
}
/**
* Crear nueva entrada de bitácora
*/
async createEntry(
ctx: ServiceContext,
data: CreateBitacoraDto
): Promise<BitacoraObra> {
const entryNumber = await this.getNextEntryNumber(ctx, data.fraccionamientoId);
return this.create(ctx, {
...data,
entryNumber,
registeredById: ctx.userId,
});
}
/**
* Obtener siguiente número de entrada
*/
private async getNextEntryNumber(
ctx: ServiceContext,
fraccionamientoId: string
): Promise<number> {
const result = await this.repository
.createQueryBuilder('b')
.select('MAX(b.entry_number)', 'maxNumber')
.where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId })
.andWhere('b.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
.getRawOne();
return (result?.maxNumber || 0) + 1;
}
/**
* Obtener bitácora por fraccionamiento
*/
async findByFraccionamiento(
ctx: ServiceContext,
fraccionamientoId: string,
page = 1,
limit = 20
): Promise<PaginatedResult<BitacoraObra>> {
return this.findAll(ctx, {
page,
limit,
where: { fraccionamientoId } as any,
});
}
/**
* Obtener bitácora con filtros
*/
async findWithFilters(
ctx: ServiceContext,
fraccionamientoId: string,
filters: BitacoraFilters,
page = 1,
limit = 20
): Promise<PaginatedResult<BitacoraObra>> {
const qb = this.repository
.createQueryBuilder('b')
.where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId })
.andWhere('b.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
.andWhere('b.deleted_at IS NULL');
if (filters.dateFrom) {
qb.andWhere('b.entry_date >= :dateFrom', { dateFrom: filters.dateFrom });
}
if (filters.dateTo) {
qb.andWhere('b.entry_date <= :dateTo', { dateTo: filters.dateTo });
}
if (filters.hasIncidents !== undefined) {
if (filters.hasIncidents) {
qb.andWhere('b.incidents IS NOT NULL');
} else {
qb.andWhere('b.incidents IS NULL');
}
}
const skip = (page - 1) * limit;
qb.orderBy('b.entry_date', 'DESC').skip(skip).take(limit);
const [data, total] = await qb.getManyAndCount();
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
/**
* Obtener entrada por fecha
*/
async findByDate(
ctx: ServiceContext,
fraccionamientoId: string,
date: Date
): Promise<BitacoraObra | null> {
return this.findOne(ctx, {
fraccionamientoId,
entryDate: date,
} as any);
}
/**
* Obtener última entrada
*/
async findLatest(
ctx: ServiceContext,
fraccionamientoId: string
): Promise<BitacoraObra | null> {
const entries = await this.find(ctx, {
where: { fraccionamientoId } as any,
order: { entryNumber: 'DESC' },
take: 1,
});
return entries[0] || null;
}
/**
* Obtener estadísticas de bitácora
*/
async getStats(
ctx: ServiceContext,
fraccionamientoId: string
): Promise<BitacoraStats> {
const totalEntries = await this.count(ctx, { fraccionamientoId } as any);
const incidentsCount = await this.repository
.createQueryBuilder('b')
.where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId })
.andWhere('b.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
.andWhere('b.deleted_at IS NULL')
.andWhere('b.incidents IS NOT NULL')
.getCount();
const avgWorkers = await this.repository
.createQueryBuilder('b')
.select('AVG(b.workers_count)', 'avg')
.where('b.tenant_id = :tenantId', { tenantId: ctx.tenantId })
.andWhere('b.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
.andWhere('b.deleted_at IS NULL')
.getRawOne();
return {
totalEntries,
entriesWithIncidents: incidentsCount,
avgWorkersCount: parseFloat(avgWorkers?.avg || '0'),
};
}
}
interface BitacoraStats {
totalEntries: number;
entriesWithIncidents: number;
avgWorkersCount: number;
}