Files
sistema_base/apps/api/src/features/training/training.service.ts

1047 lines
36 KiB
TypeScript

import { MinioService } from '@/common/minio/minio.service';
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { and, eq, gte, ilike, lte, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
import * as schema from 'src/database/index';
import { municipalities, parishes, states, trainingSurveys } from 'src/database/index';
import * as fs from 'fs';
import * as path from 'path';
// @ts-ignore
import XlsxPopulate from 'xlsx-populate';
import { PaginationDto } from '../../common/dto/pagination.dto';
import { CreateTrainingDto } from './dto/create-training.dto';
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
import { UpdateTrainingDto } from './dto/update-training.dto';
// TRUE: para mostrar los logs de errores en la api
// Actualmente estás solo en crear registro. Despues lo implemento en los demas
const debug = false;
type User = {
role: string;
id: number;
};
@Injectable()
export class TrainingService {
constructor(
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
private readonly minioService: MinioService,
) { }
async findAll(paginationDto?: PaginationDto, user?: User) {
const {
page = 1,
limit = 10,
search = '',
sortBy = 'id',
sortOrder = 'asc',
} = paginationDto || {};
const offset = (page - 1) * limit;
let searchCondition: SQL<unknown> | undefined;
if (search) {
searchCondition = or(ilike(trainingSurveys.ospName, `%${search}%`));
}
if (user?.role == 'coordinators') {
searchCondition = eq(trainingSurveys.createdBy, user.id)
}
const orderBy =
sortOrder === 'asc'
? sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} asc`
: sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} desc`;
const totalCountResult = await this.drizzle
.select({ count: sql<number>`count(*)` })
.from(trainingSurveys)
.where(searchCondition);
const totalCount = Number(totalCountResult[0].count);
const totalPages = Math.ceil(totalCount / limit);
const data = await this.drizzle
.select()
.from(trainingSurveys)
.where(searchCondition)
.orderBy(orderBy)
.limit(limit)
.offset(offset);
const meta = {
page,
limit,
totalCount,
totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
nextPage: page < totalPages ? page + 1 : null,
previousPage: page > 1 ? page - 1 : null,
};
return { data, meta };
}
async getStatistics(filterDto: TrainingStatisticsFilterDto) {
const { startDate, endDate, stateId, municipalityId, parishId, ospType } =
filterDto;
const filters: SQL[] = [];
if (startDate)
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
if (endDate)
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
if (stateId) filters.push(eq(trainingSurveys.state, stateId));
if (municipalityId)
filters.push(eq(trainingSurveys.municipality, municipalityId));
if (parishId) filters.push(eq(trainingSurveys.parish, parishId));
if (ospType && ospType !== 'all') filters.push(eq(trainingSurveys.ospType, ospType));
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
// Ejecutamos todas las consultas en paralelo con Promise.all para mayor velocidad
const [
totalOspsResult,
// totalProducersResult,
totalProductsResult,
statusDistribution,
activityDistribution,
typeDistribution,
stateDistribution,
yearDistribution,
ecoSectorDistribution,
productiveSectorDistribution,
centralActivityDistribution,
mainActivityDistribution,
structureTypeDistribution,
isOpenSpaceDistribution,
hasTransportDistribution,
genderResult,
municipalityDistribution,
parishDistribution,
] = await Promise.all([
// 1. Total OSPs
this.drizzle
.select({ count: sql<number>`count(*)` })
.from(trainingSurveys)
.where(whereCondition),
// 2. Total Productores (Columna plana que mantuviste)
// this.drizzle
// .select({
// sum: sql<number>`SUM(${trainingSurveys.womenCount} + ${trainingSurveys.menCount})`,
// })
// .from(trainingSurveys)
// .where(whereCondition),
// 3. NUEVO: Total Productos (Contamos el largo del array JSON productList)
this.drizzle
.select({
sum: sql<number>`sum(jsonb_array_length(${trainingSurveys.productList}))`,
})
.from(trainingSurveys)
.where(whereCondition),
// 4. Distribución por Estatus
this.drizzle
.select({
name: trainingSurveys.currentStatus,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.currentStatus),
// 5. Distribución por Actividad (General)
this.drizzle
.select({
name: trainingSurveys.productiveActivity,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.productiveActivity),
// 6. Distribución por Tipo
this.drizzle
.select({
name: trainingSurveys.ospType,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.ospType),
// 7. Distribución por Estado (CORREGIDO con COALESCE)
this.drizzle
.select({
// Si states.name es NULL, devuelve 'Sin Asignar'
name: sql<string>`COALESCE(${states.name}, 'Sin Asignar')`,
value: sql<number>`count(${trainingSurveys.id})`,
})
.from(trainingSurveys)
.leftJoin(states, eq(trainingSurveys.state, states.id))
.where(whereCondition)
// Importante: Agrupar también por el resultado del COALESCE o por states.name
.groupBy(states.name),
// 8. Distribución por Año
this.drizzle
.select({
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.companyConstitutionYear)
.orderBy(trainingSurveys.companyConstitutionYear),
// 9. Distribución por Sector Económico
this.drizzle
.select({
name: trainingSurveys.ecoSector,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.ecoSector),
// 10. Distribución por Sector Productivo
this.drizzle
.select({
name: trainingSurveys.productiveSector,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.productiveSector),
// 11. Distribución por Actividad Central Productiva
this.drizzle
.select({
name: trainingSurveys.centralProductiveActivity,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.centralProductiveActivity),
// 12. Distribución por Actividad Productiva Principal
this.drizzle
.select({
name: trainingSurveys.mainProductiveActivity,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.mainProductiveActivity),
// 13. Distribución por Tipo de Estructura
this.drizzle
.select({
name: trainingSurveys.structureType,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.structureType),
// 14. Distribución por Espacio Abierto
this.drizzle
.select({
name: sql<string>`case when ${trainingSurveys.isOpenSpace} then 'Sí' else 'No' end`,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.isOpenSpace),
// 15. Distribución por Transporte
this.drizzle
.select({
name: sql<string>`case when ${trainingSurveys.hasTransport} then 'Sí' else 'No' end`,
value: sql<number>`count(*)`,
})
.from(trainingSurveys)
.where(whereCondition)
.groupBy(trainingSurveys.hasTransport),
// 16. Distribución por Género
this.drizzle
.select({
women: sql<number>`sum(${trainingSurveys.womenCount})`,
men: sql<number>`sum(${trainingSurveys.menCount})`,
})
.from(trainingSurveys)
.where(whereCondition),
// 17. Distribución por Municipio
this.drizzle
.select({
name: sql<string>`COALESCE(${municipalities.name}, 'Sin Asignar')`,
value: sql<number>`count(${trainingSurveys.id})`,
})
.from(trainingSurveys)
.leftJoin(
municipalities,
eq(trainingSurveys.municipality, municipalities.id),
)
.where(whereCondition)
.groupBy(municipalities.name),
// 18. Distribución por Parroquia
this.drizzle
.select({
name: sql<string>`COALESCE(${parishes.name}, 'Sin Asignar')`,
value: sql<number>`count(${trainingSurveys.id})`,
})
.from(trainingSurveys)
.leftJoin(parishes, eq(trainingSurveys.parish, parishes.id))
.where(whereCondition)
.groupBy(parishes.name),
]);
return {
totalOsps: Number(totalOspsResult[0]?.count || 0),
// totalProducers: Number(totalProducersResult[0]?.sum || 0),
totalProducts: Number(totalProductsResult[0]?.sum || 0), // Dato extraído del JSON
statusDistribution: statusDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
activityDistribution: activityDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
typeDistribution: typeDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
stateDistribution: stateDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
yearDistribution: yearDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
ecoSectorDistribution: ecoSectorDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
productiveSectorDistribution: productiveSectorDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
centralActivityDistribution: centralActivityDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
mainActivityDistribution: mainActivityDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
structureTypeDistribution: structureTypeDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
isOpenSpaceDistribution: isOpenSpaceDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
hasTransportDistribution: hasTransportDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
genderDistribution: [
{ name: 'Mujeres', value: Number(genderResult[0]?.women || 0) },
{ name: 'Hombres', value: Number(genderResult[0]?.men || 0) },
],
municipalityDistribution: municipalityDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
parishDistribution: parishDistribution.map((item) => ({
...item,
value: Number(item.value),
})),
};
}
async findOne(id: number) {
const find = await this.drizzle
.select()
.from(trainingSurveys)
.where(eq(trainingSurveys.id, id));
if (find.length === 0) {
throw new HttpException(
'Training record not found',
HttpStatus.NOT_FOUND,
);
}
return find[0];
}
private async saveFiles(files: Express.Multer.File[]): Promise<string[]> {
if (!files || files.length === 0) return [];
const savedPaths: string[] = [];
for (const file of files) {
const objectName = await this.minioService.upload(file, 'training');
const fileUrl = this.minioService.getPublicUrl(objectName);
savedPaths.push(fileUrl);
}
return savedPaths;
}
private async deleteFile(fileUrl: string) {
if (!fileUrl) return;
try {
// If it's a full URL, we need to extract the part after the bucket name
if (fileUrl.startsWith('http')) {
const url = new URL(fileUrl);
const pathname = url.pathname; // /bucket/folder/filename
const parts = pathname.split('/').filter(Boolean); // ['bucket', 'folder', 'filename']
// The first part is the bucket name, the rest is the object name
if (parts.length >= 2) {
const objectName = parts.slice(1).join('/');
await this.minioService.delete(objectName);
return;
}
}
// If it's not a URL or doesn't match the expected format, pass it as is
await this.minioService.delete(fileUrl);
} catch (error) {
// Fallback if URL parsing fails
await this.minioService.delete(fileUrl);
}
}
// ========== Guardar registro ========== //
async create(
createTrainingDto: CreateTrainingDto,
files: Express.Multer.File[],
userId: number,
) {
try {
// 1. Guardar fotos
const photoPaths = await this.saveFiles(files);
// const photoPaths = [];
// 2. Extraer solo visitDate para formatearlo.
// Ya NO extraemos state, municipality, etc. porque no vienen en el DTO.
const { visitDate, state, municipality, parish, productiveActivityOther, ...rest } =
createTrainingDto;
const [newRecord] = await this.drizzle
.insert(trainingSurveys)
.values({
// Insertamos el resto de datos planos y las listas (arrays)
...rest,
// Conversión de fecha
visitDate: new Date(visitDate),
// Borra las tildes y cambia el texto a mayusculas
productiveActivityOther: productiveActivityOther ? productiveActivityOther.toUpperCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "") : '',
// 3. Asignar fotos de forma segura
photo1: photoPaths[0] ?? null,
photo2: photoPaths[1] ?? null,
photo3: photoPaths[2] ?? null,
state: Number(state) ?? null,
municipality: Number(municipality) ?? null,
parish: Number(parish) ?? null,
hasTransport: rest.hasTransport === 'true' ? true : false,
isOpenSpace: rest.isOpenSpace === 'true' ? true : false,
isExporting: rest.isExporting === 'true' ? true : false,
createdBy: userId,
updatedBy: userId,
})
.returning();
return newRecord;
} catch (e) {
if (debug) console.log(e);
return null // null para que de error
}
}
// ========== Actualizar registro ========== //
async update(
id: number,
updateTrainingDto: UpdateTrainingDto,
files: Express.Multer.File[],
userId: number,
) {
const currentRecord = await this.findOne(id);
const photoFields = ['photo1', 'photo2', 'photo3'] as const;
// 1. Guardar fotos nuevas en MinIO
const newFilePaths = await this.saveFiles(files);
const updateData: any = { ...updateTrainingDto };
// 2. Determinar el estado final de las fotos (diff)
// - Si el DTO tiene un valor (URL existente o ''), lo usamos.
// - Si el DTO no tiene el campo (undefined), mantenemos el de la DB.
const finalPhotos: (string | null)[] = photoFields.map((field) => {
const dtoValue = updateData[field];
if (dtoValue !== undefined) {
return dtoValue === '' ? null : dtoValue;
}
return currentRecord[field];
});
// 3. Asignar los nuevos paths subidos a los slots que quedaron vacíos
if (newFilePaths.length > 0) {
let newIdx = 0;
for (let i = 0; i < 3 && newIdx < newFilePaths.length; i++) {
if (!finalPhotos[i]) {
finalPhotos[i] = newFilePaths[newIdx];
newIdx++;
}
}
}
// 4. LIMPIEZA: Borrar de MinIO los archivos que ya no están en ningún slot
const oldPhotos = photoFields
.map((f) => currentRecord[f])
.filter((p): p is string => Boolean(p));
const newPhotosSet = new Set(finalPhotos.filter(Boolean));
for (const oldPath of oldPhotos) {
if (!newPhotosSet.has(oldPath)) {
await this.deleteFile(oldPath);
}
}
// 5. Preparar datos finales para la DB
updateData.photo1 = finalPhotos[0];
updateData.photo2 = finalPhotos[1];
updateData.photo3 = finalPhotos[2];
if (updateTrainingDto.visitDate) {
updateData.visitDate = new Date(updateTrainingDto.visitDate);
}
// actualizamos el id del usuario que actualizo el registro
updateData.updatedBy = userId;
updateData.hasTransport =
updateTrainingDto.hasTransport === 'true' ? true : false;
updateData.isOpenSpace =
updateTrainingDto.isOpenSpace === 'true' ? true : false;
updateData.isExporting =
updateTrainingDto.isExporting === 'true' ? true : false;
const [updatedRecord] = await this.drizzle
.update(trainingSurveys)
.set(updateData)
.where(eq(trainingSurveys.id, id))
.returning();
return updatedRecord;
}
// ========== Eliminar registro ========== //
async remove(id: number) {
const record = await this.findOne(id);
// Delete associated files
if (record.photo1) await this.deleteFile(record.photo1);
if (record.photo2) await this.deleteFile(record.photo2);
if (record.photo3) await this.deleteFile(record.photo3);
const [deletedRecord] = await this.drizzle
.delete(trainingSurveys)
.where(eq(trainingSurveys.id, id))
.returning();
return {
message: 'Training record deleted successfully',
data: deletedRecord,
};
}
// async exportTemplate() {
// const templatePath = path.join(
// __dirname,
// 'export_template',
// 'excel.osp.xlsx',
// );
// const templateBuffer = fs.readFileSync(templatePath);
// const workbook: any = await XlsxPopulate.fromDataAsync(templateBuffer);
// const sheet = workbook.sheet(0);
// const records = await this.drizzle
// .select({
// coorFullName: trainingSurveys.coorFullName,
// visitDate: trainingSurveys.visitDate,
// stateName: states.name,
// municipalityName: municipalities.name,
// parishName: parishes.name,
// communeName: trainingSurveys.communeName,
// siturCodeCommune: trainingSurveys.siturCodeCommune,
// communalCouncil: trainingSurveys.communalCouncil,
// siturCodeCommunalCouncil: trainingSurveys.siturCodeCommunalCouncil,
// productiveActivity: trainingSurveys.productiveActivity,
// ospName: trainingSurveys.ospName,
// ospAddress: trainingSurveys.ospAddress,
// ospRif: trainingSurveys.ospRif,
// ospType: trainingSurveys.ospType,
// currentStatus: trainingSurveys.currentStatus,
// companyConstitutionYear: trainingSurveys.companyConstitutionYear,
// ospResponsibleFullname: trainingSurveys.ospResponsibleFullname,
// ospResponsibleCedula: trainingSurveys.ospResponsibleCedula,
// ospResponsibleRif: trainingSurveys.ospResponsibleRif,
// ospResponsiblePhone: trainingSurveys.ospResponsiblePhone,
// ospResponsibleEmail: trainingSurveys.ospResponsibleEmail,
// civilState: trainingSurveys.civilState,
// familyBurden: trainingSurveys.familyBurden,
// numberOfChildren: trainingSurveys.numberOfChildren,
// generalObservations: trainingSurveys.generalObservations,
// paralysisReason: trainingSurveys.paralysisReason,
// productList: trainingSurveys.productList,
// infrastructureMt2: trainingSurveys.infrastructureMt2,
// photo1: trainingSurveys.photo1,
// photo2: trainingSurveys.photo2,
// photo3: trainingSurveys.photo3,
// })
// .from(trainingSurveys)
// .leftJoin(states, eq(trainingSurveys.state, states.id))
// .leftJoin(
// municipalities,
// eq(trainingSurveys.municipality, municipalities.id),
// )
// .leftJoin(parishes, eq(trainingSurveys.parish, parishes.id))
// .execute();
// let currentRow = 2;
// for (const record of records) {
// const date = new Date(record.visitDate);
// const dateStr = date.toLocaleDateString('es-VE');
// const timeStr = date.toLocaleTimeString('es-VE');
// sheet.cell(`A${currentRow}`).value(record.coorFullName);
// sheet.cell(`C${currentRow}`).value(dateStr);
// sheet.cell(`D${currentRow}`).value(timeStr);
// sheet.cell(`E${currentRow}`).value(record.stateName || '');
// sheet.cell(`F${currentRow}`).value(record.municipalityName || '');
// sheet.cell(`G${currentRow}`).value(record.parishName || '');
// sheet.cell(`H${currentRow}`).value(record.communeName);
// sheet.cell(`I${currentRow}`).value(record.siturCodeCommune);
// sheet.cell(`J${currentRow}`).value(record.communalCouncil);
// sheet.cell(`K${currentRow}`).value(record.siturCodeCommunalCouncil);
// sheet.cell(`L${currentRow}`).value(record.productiveActivity);
// sheet.cell(`M${currentRow}`).value(''); // requerimiento financiero description
// sheet.cell(`N${currentRow}`).value(record.ospName);
// sheet.cell(`O${currentRow}`).value(record.ospAddress);
// sheet.cell(`P${currentRow}`).value(record.ospRif);
// sheet.cell(`Q${currentRow}`).value(record.ospType);
// sheet.cell(`R${currentRow}`).value(record.currentStatus);
// sheet.cell(`S${currentRow}`).value(record.companyConstitutionYear);
// const products = (record.productList as any[]) || [];
// const totalProducers = products.reduce(
// (sum, p) =>
// sum + (Number(p.menCount) || 0) + (Number(p.womenCount) || 0),
// 0,
// );
// const productsDesc = products.map((p) => p.name).join(', ');
// sheet.cell(`T${currentRow}`).value(totalProducers);
// sheet.cell(`U${currentRow}`).value(productsDesc);
// sheet.cell(`V${currentRow}`).value(record.infrastructureMt2);
// sheet.cell(`W${currentRow}`).value('');
// sheet.cell(`X${currentRow}`).value(record.paralysisReason || '');
// sheet.cell(`Y${currentRow}`).value(record.ospResponsibleFullname);
// sheet.cell(`Z${currentRow}`).value(record.ospResponsibleCedula);
// sheet.cell(`AA${currentRow}`).value(record.ospResponsibleRif);
// sheet.cell(`AB${currentRow}`).value(record.ospResponsiblePhone);
// sheet.cell(`AC${currentRow}`).value(record.ospResponsibleEmail);
// sheet.cell(`AD${currentRow}`).value(record.civilState);
// sheet.cell(`AE${currentRow}`).value(record.familyBurden);
// sheet.cell(`AF${currentRow}`).value(record.numberOfChildren);
// sheet.cell(`AG${currentRow}`).value(record.generalObservations || '');
// sheet.cell(`AH${currentRow}`).value(record.photo1 || '');
// sheet.cell(`AI${currentRow}`).value(record.photo2 || '');
// sheet.cell(`AJ${currentRow}`).value(record.photo3 || '');
// currentRow++;
// }
// return await workbook.outputAsync();
// }
// async exportTemplate(id: number) {
// // Validar que el registro exista
// const exist = await this.findOne(id);
// if (!exist) throw new NotFoundException(`No se encontro el registro`);
// // Obtener los datos del registro
// const records = await this.drizzle
// .select({
// // id: trainingSurveys.id,
// visitDate: trainingSurveys.visitDate,
// ospName: trainingSurveys.ospName,
// productiveSector: trainingSurveys.productiveSector,
// ospAddress: trainingSurveys.ospAddress,
// ospRif: trainingSurveys.ospRif,
// siturCodeCommune: trainingSurveys.siturCodeCommune,
// communeEmail: trainingSurveys.communeEmail,
// communeRif: trainingSurveys.communeRif,
// communeSpokespersonName: trainingSurveys.communeSpokespersonName,
// communeSpokespersonPhone: trainingSurveys.communeSpokespersonPhone,
// siturCodeCommunalCouncil: trainingSurveys.siturCodeCommunalCouncil,
// communalCouncilRif: trainingSurveys.communalCouncilRif,
// communalCouncilSpokespersonName:
// trainingSurveys.communalCouncilSpokespersonName,
// communalCouncilSpokespersonPhone:
// trainingSurveys.communalCouncilSpokespersonPhone,
// ospType: trainingSurveys.ospType,
// productiveActivity: trainingSurveys.productiveActivity, // Sector Productivo
// companyConstitutionYear: trainingSurveys.companyConstitutionYear,
// infrastructureMt2: trainingSurveys.infrastructureMt2,
// hasTransport: trainingSurveys.hasTransport,
// structureType: trainingSurveys.structureType,
// isOpenSpace: trainingSurveys.isOpenSpace,
// ospResponsibleFullname: trainingSurveys.ospResponsibleFullname,
// ospResponsibleCedula: trainingSurveys.ospResponsibleCedula,
// ospResponsiblePhone: trainingSurveys.ospResponsiblePhone,
// productList: trainingSurveys.productList,
// equipmentList: trainingSurveys.equipmentList,
// productionList: trainingSurveys.productionList,
// // photo1: trainingSurveys.photo1
// })
// .from(trainingSurveys)
// .where(eq(trainingSurveys.id, id));
// // .leftJoin(states, eq(trainingSurveys.state, states.id))
// // .leftJoin(municipalities,eq(trainingSurveys.municipality, municipalities.id))
// // .leftJoin(parishes, eq(trainingSurveys.parish, parishes.id))
// let equipmentList: any[] = Array.isArray(records[0].equipmentList)
// ? records[0].equipmentList
// : [];
// let productList: any[] = Array.isArray(records[0].productList)
// ? records[0].productList
// : [];
// let productionList: any[] = Array.isArray(records[0].productionList)
// ? records[0].productionList
// : [];
// console.log('equipmentList', equipmentList);
// console.log('productList', productList);
// console.log('productionList', productionList);
// let equipmentListArray: any[] = [];
// let productListArray: any[] = [];
// let productionListArray: any[] = [];
// const equipmentListCount = equipmentList.length;
// for (let i = 0; i < equipmentListCount; i++) {
// equipmentListArray.push([
// equipmentList[i].machine,
// '',
// equipmentList[i].quantity,
// ]);
// }
// const productListCount = productList.length;
// for (let i = 0; i < productListCount; i++) {
// productListArray.push([
// productList[i].productName,
// productList[i].dailyCount,
// productList[i].weeklyCount,
// productList[i].monthlyCount,
// ]);
// }
// const productionListCount = productionList.length;
// for (let i = 0; i < productionListCount; i++) {
// productionListArray.push([
// productionList[i].rawMaterial,
// '',
// productionList[i].quantity,
// ]);
// }
// // Ruta de la plantilla
// const templatePath = path.join(
// __dirname,
// 'export_template',
// 'excel.osp.xlsx',
// );
// // Cargar la plantilla
// const book = await XlsxPopulate.fromFileAsync(templatePath);
// const isoString = records[0].visitDate;
// const dateObj = new Date(isoString);
// const fechaFormateada = dateObj.toLocaleDateString('es-ES');
// const horaFormateada = dateObj.toLocaleTimeString('es-ES', {
// hour: '2-digit',
// minute: '2-digit',
// });
// // Llenar los datos
// book.sheet(0).cell('A6').value(records[0].productiveSector);
// book.sheet(0).cell('D6').value(records[0].ospName);
// book.sheet(0).cell('L5').value(fechaFormateada);
// book.sheet(0).cell('L6').value(horaFormateada);
// book.sheet(0).cell('B10').value(records[0].ospAddress);
// book.sheet(0).cell('C11').value(records[0].communeEmail);
// book.sheet(0).cell('C12').value(records[0].communeSpokespersonName);
// book.sheet(0).cell('G11').value(records[0].communeRif);
// book.sheet(0).cell('G12').value(records[0].communeSpokespersonPhone);
// book.sheet(0).cell('C13').value(records[0].siturCodeCommune);
// book.sheet(0).cell('G13').value(records[0].siturCodeCommunalCouncil);
// book.sheet(0).cell('G14').value(records[0].communalCouncilRif);
// book.sheet(0).cell('C15').value(records[0].communalCouncilSpokespersonName);
// book
// .sheet(0)
// .cell('G15')
// .value(records[0].communalCouncilSpokespersonPhone);
// book.sheet(0).cell('C16').value(records[0].ospType);
// book.sheet(0).cell('C17').value(records[0].ospName);
// book.sheet(0).cell('C18').value(records[0].productiveActivity);
// book.sheet(0).cell('C19').value('Proveedores');
// book.sheet(0).cell('C20').value(records[0].companyConstitutionYear);
// book.sheet(0).cell('C21').value(records[0].infrastructureMt2);
// book.sheet(0).cell('G17').value(records[0].ospRif);
// book
// .sheet(0)
// .cell(records[0].hasTransport === true ? 'J19' : 'L19')
// .value('X');
// book
// .sheet(0)
// .cell(records[0].structureType === 'CASA' ? 'J20' : 'L20')
// .value('X');
// book
// .sheet(0)
// .cell(records[0].isOpenSpace === true ? 'J21' : 'L21')
// .value('X');
// book.sheet(0).cell('A24').value(records[0].ospResponsibleFullname);
// book.sheet(0).cell('C24').value(records[0].ospResponsibleCedula);
// book.sheet(0).cell('E24').value(records[0].ospResponsiblePhone);
// book.sheet(0).cell('J24').value('N Femenino');
// book.sheet(0).cell('L24').value('N Masculino');
// book
// .sheet(0)
// .range(`A28:C${equipmentListCount + 28}`)
// .value(equipmentListArray);
// book
// .sheet(0)
// .range(`E28:G${productionListCount + 28}`)
// .value(productionListArray);
// book
// .sheet(0)
// .range(`I28:L${productListCount + 28}`)
// .value(productListArray);
// return book.outputAsync();
// }
async exportAll(filterDto: TrainingStatisticsFilterDto) {
try {
const { startDate, endDate, stateId, municipalityId, parishId, ospType } =
filterDto;
const filters: SQL[] = [];
if (startDate)
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
if (endDate)
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
if (stateId) filters.push(eq(trainingSurveys.state, stateId));
if (municipalityId)
filters.push(eq(trainingSurveys.municipality, municipalityId));
if (parishId) filters.push(eq(trainingSurveys.parish, parishId));
if (ospType && ospType !== 'all')
filters.push(eq(trainingSurveys.ospType, ospType));
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
const records = await this.drizzle
.select({
coorFullName: trainingSurveys.coorFullName,
visitDate: trainingSurveys.visitDate,
stateName: states.name,
municipalityName: municipalities.name,
parishName: parishes.name,
communeName: trainingSurveys.communeName,
siturCodeCommune: trainingSurveys.siturCodeCommune,
communalCouncil: trainingSurveys.communalCouncil,
siturCodeCommunalCouncil: trainingSurveys.siturCodeCommunalCouncil,
productiveActivity: trainingSurveys.productiveActivity,
ospName: trainingSurveys.ospName,
ospAddress: trainingSurveys.ospAddress,
ospRif: trainingSurveys.ospRif,
ospType: trainingSurveys.ospType,
currentStatus: trainingSurveys.currentStatus,
companyConstitutionYear: trainingSurveys.companyConstitutionYear,
ospResponsibleFullname: trainingSurveys.ospResponsibleFullname,
ospResponsibleCedula: trainingSurveys.ospResponsibleCedula,
ospResponsibleRif: trainingSurveys.ospResponsibleRif,
ospResponsiblePhone: trainingSurveys.ospResponsiblePhone,
ospResponsibleEmail: trainingSurveys.ospResponsibleEmail,
civilState: trainingSurveys.civilState,
familyBurden: trainingSurveys.familyBurden,
numberOfChildren: trainingSurveys.numberOfChildren,
generalObservations: trainingSurveys.generalObservations,
paralysisReason: trainingSurveys.paralysisReason,
productList: trainingSurveys.productList,
infrastructureMt2: trainingSurveys.infrastructureMt2,
photo1: trainingSurveys.photo1,
photo2: trainingSurveys.photo2,
photo3: trainingSurveys.photo3,
})
.from(trainingSurveys)
.leftJoin(states, eq(trainingSurveys.state, states.id))
.leftJoin(
municipalities,
eq(trainingSurveys.municipality, municipalities.id),
)
.leftJoin(parishes, eq(trainingSurveys.parish, parishes.id))
.where(whereCondition)
.execute();
const workbook: any = await XlsxPopulate.fromBlankAsync();
const sheet = workbook.sheet(0);
const headers = [
'Coordinador',
'Fecha',
'Hora',
'Estado',
'Municipio',
'Parroquia',
'Comuna',
'Código SITUR Comuna',
'Consejo Comunal',
'Código SITUR C.C.',
'Actividad Productiva',
'Nombre OSP',
'Dirección OSP',
'RIF OSP',
'Tipo OSP',
'Estatus Actual',
'Año Constitución',
'Total Productores',
'Productos',
'Infraestructura (mt2)',
'Motivo Paralización',
'Responsable',
'Cédula',
'RIF Responsable',
'Teléfono',
'Email',
'Estado Civil',
'Carga Familiar',
'Nro Hijos',
'Observaciones',
// 'Foto 1',
// 'Foto 2',
// 'Foto 3',
];
// Configurar encabezados
sheet.range('A1:AG1').value([headers]).style({
bold: true,
fill: 'BFBFBF',
});
let currentRow = 2;
for (const record of records) {
const date = new Date(record.visitDate);
const dateStr = date.toLocaleDateString('es-VE');
const timeStr = date.toLocaleTimeString('es-VE');
const products = (record.productList as any[]) || [];
const totalProducers = products.reduce(
(sum, p) =>
sum + (Number(p.menCount) || 0) + (Number(p.womenCount) || 0),
0,
);
const productsDesc = products.map((p) => p.name).join(', ');
const rowData = [
record.coorFullName,
dateStr,
timeStr,
record.stateName || '',
record.municipalityName || '',
record.parishName || '',
record.communeName,
record.siturCodeCommune,
record.communalCouncil,
record.siturCodeCommunalCouncil,
record.productiveActivity,
record.ospName,
record.ospAddress,
record.ospRif,
record.ospType,
record.currentStatus,
record.companyConstitutionYear,
totalProducers,
productsDesc,
record.infrastructureMt2,
record.paralysisReason || '',
record.ospResponsibleFullname,
record.ospResponsibleCedula,
record.ospResponsibleRif,
record.ospResponsiblePhone,
record.ospResponsibleEmail,
record.civilState,
record.familyBurden,
record.numberOfChildren,
record.generalObservations || '',
// record.photo1 || '',
// record.photo2 || '',
// record.photo3 || '',
];
sheet.cell(`A${currentRow}`).value([rowData]);
currentRow++;
}
return await workbook.outputAsync();
} catch (error: any) {
console.error('Export Error:', error);
throw new HttpException(
'Error al generar el archivo Excel: ' + error.message,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}