195 lines
6.1 KiB
JavaScript
195 lines
6.1 KiB
JavaScript
"use strict";
|
|
/**
|
|
* AvanceObraService - Gestión de Avances de Obra
|
|
*
|
|
* Gestiona el registro y aprobación de avances físicos de obra.
|
|
* Incluye workflow de captura -> revisión -> aprobación.
|
|
*
|
|
* @module Progress
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.AvanceObraService = void 0;
|
|
const base_service_1 = require("../../../shared/services/base.service");
|
|
class AvanceObraService extends base_service_1.BaseService {
|
|
fotoRepository;
|
|
constructor(repository, fotoRepository) {
|
|
super(repository);
|
|
this.fotoRepository = fotoRepository;
|
|
}
|
|
/**
|
|
* Crear nuevo avance (captura)
|
|
*/
|
|
async createAvance(ctx, data) {
|
|
if (!data.loteId && !data.departamentoId) {
|
|
throw new Error('Either loteId or departamentoId is required');
|
|
}
|
|
if (data.loteId && data.departamentoId) {
|
|
throw new Error('Cannot specify both loteId and departamentoId');
|
|
}
|
|
return this.create(ctx, {
|
|
...data,
|
|
status: 'captured',
|
|
capturedById: ctx.userId,
|
|
});
|
|
}
|
|
/**
|
|
* Obtener avances por lote
|
|
*/
|
|
async findByLote(ctx, loteId, page = 1, limit = 20) {
|
|
return this.findAll(ctx, {
|
|
page,
|
|
limit,
|
|
where: { loteId },
|
|
});
|
|
}
|
|
/**
|
|
* Obtener avances por departamento
|
|
*/
|
|
async findByDepartamento(ctx, departamentoId, page = 1, limit = 20) {
|
|
return this.findAll(ctx, {
|
|
page,
|
|
limit,
|
|
where: { departamentoId },
|
|
});
|
|
}
|
|
/**
|
|
* Obtener avances con filtros
|
|
*/
|
|
async findWithFilters(ctx, filters, page = 1, limit = 20) {
|
|
const qb = this.repository
|
|
.createQueryBuilder('a')
|
|
.where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
|
.andWhere('a.deleted_at IS NULL');
|
|
if (filters.loteId) {
|
|
qb.andWhere('a.lote_id = :loteId', { loteId: filters.loteId });
|
|
}
|
|
if (filters.departamentoId) {
|
|
qb.andWhere('a.departamento_id = :departamentoId', { departamentoId: filters.departamentoId });
|
|
}
|
|
if (filters.conceptoId) {
|
|
qb.andWhere('a.concepto_id = :conceptoId', { conceptoId: filters.conceptoId });
|
|
}
|
|
if (filters.status) {
|
|
qb.andWhere('a.status = :status', { status: filters.status });
|
|
}
|
|
if (filters.dateFrom) {
|
|
qb.andWhere('a.capture_date >= :dateFrom', { dateFrom: filters.dateFrom });
|
|
}
|
|
if (filters.dateTo) {
|
|
qb.andWhere('a.capture_date <= :dateTo', { dateTo: filters.dateTo });
|
|
}
|
|
const skip = (page - 1) * limit;
|
|
qb.orderBy('a.capture_date', 'DESC').skip(skip).take(limit);
|
|
const [data, total] = await qb.getManyAndCount();
|
|
return {
|
|
data,
|
|
meta: {
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages: Math.ceil(total / limit),
|
|
},
|
|
};
|
|
}
|
|
/**
|
|
* Obtener avance con fotos
|
|
*/
|
|
async findWithFotos(ctx, id) {
|
|
return this.repository.findOne({
|
|
where: {
|
|
id,
|
|
tenantId: ctx.tenantId,
|
|
deletedAt: null,
|
|
},
|
|
relations: ['fotos', 'concepto', 'capturedBy'],
|
|
});
|
|
}
|
|
/**
|
|
* Agregar foto al avance
|
|
*/
|
|
async addFoto(ctx, avanceId, data) {
|
|
const avance = await this.findById(ctx, avanceId);
|
|
if (!avance) {
|
|
throw new Error('Avance not found');
|
|
}
|
|
const location = data.location
|
|
? `POINT(${data.location.lng} ${data.location.lat})`
|
|
: null;
|
|
const foto = this.fotoRepository.create({
|
|
tenantId: ctx.tenantId,
|
|
avanceId,
|
|
fileUrl: data.fileUrl,
|
|
fileName: data.fileName,
|
|
fileSize: data.fileSize,
|
|
mimeType: data.mimeType,
|
|
description: data.description,
|
|
location,
|
|
createdById: ctx.userId,
|
|
});
|
|
return this.fotoRepository.save(foto);
|
|
}
|
|
/**
|
|
* Revisar avance
|
|
*/
|
|
async review(ctx, avanceId) {
|
|
const avance = await this.findById(ctx, avanceId);
|
|
if (!avance || avance.status !== 'captured') {
|
|
return null;
|
|
}
|
|
return this.update(ctx, avanceId, {
|
|
status: 'reviewed',
|
|
reviewedById: ctx.userId,
|
|
reviewedAt: new Date(),
|
|
});
|
|
}
|
|
/**
|
|
* Aprobar avance
|
|
*/
|
|
async approve(ctx, avanceId) {
|
|
const avance = await this.findById(ctx, avanceId);
|
|
if (!avance || avance.status !== 'reviewed') {
|
|
return null;
|
|
}
|
|
return this.update(ctx, avanceId, {
|
|
status: 'approved',
|
|
approvedById: ctx.userId,
|
|
approvedAt: new Date(),
|
|
});
|
|
}
|
|
/**
|
|
* Rechazar avance
|
|
*/
|
|
async reject(ctx, avanceId, reason) {
|
|
const avance = await this.findById(ctx, avanceId);
|
|
if (!avance || !['captured', 'reviewed'].includes(avance.status)) {
|
|
return null;
|
|
}
|
|
return this.update(ctx, avanceId, {
|
|
status: 'rejected',
|
|
notes: reason,
|
|
});
|
|
}
|
|
/**
|
|
* Calcular avance acumulado por concepto
|
|
*/
|
|
async getAccumulatedProgress(ctx, loteId, departamentoId) {
|
|
const qb = this.repository
|
|
.createQueryBuilder('a')
|
|
.select('a.concepto_id', 'conceptoId')
|
|
.addSelect('SUM(a.quantity_executed)', 'totalQuantity')
|
|
.addSelect('AVG(a.percentage_executed)', 'avgPercentage')
|
|
.where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
|
.andWhere('a.deleted_at IS NULL')
|
|
.andWhere('a.status = :status', { status: 'approved' })
|
|
.groupBy('a.concepto_id');
|
|
if (loteId) {
|
|
qb.andWhere('a.lote_id = :loteId', { loteId });
|
|
}
|
|
if (departamentoId) {
|
|
qb.andWhere('a.departamento_id = :departamentoId', { departamentoId });
|
|
}
|
|
return qb.getRawMany();
|
|
}
|
|
}
|
|
exports.AvanceObraService = AvanceObraService;
|
|
//# sourceMappingURL=avance-obra.service.js.map
|