Exportar excel con imagen y ahora guarda las imagenes como .png

This commit is contained in:
2026-02-05 18:09:05 -04:00
parent 63c39e399e
commit f1bdce317f
13 changed files with 250 additions and 332 deletions

View File

@@ -15,5 +15,5 @@ DATABASE_URL="postgresql://postgres:local**@localhost:5432/caja_ahorro" #url con
#Mail Configuration
MAIL_HOST=gmail
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_USERNAME="123"
MAIL_PASSWORD="123"

View File

@@ -42,6 +42,7 @@
"@nestjs/platform-express": "11.0.0",
"dotenv": "16.5.0",
"drizzle-orm": "0.40.0",
"exceljs": "^4.4.0",
"express": "5.1.0",
"joi": "17.13.3",
"moment": "2.30.1",
@@ -50,8 +51,7 @@
"pino-pretty": "13.0.0",
"reflect-metadata": "0.2.0",
"rxjs": "7.8.1",
"sharp": "^0.34.5",
"xlsx-populate": "^1.21.0"
"sharp": "^0.34.5"
},
"devDependencies": {
"@nestjs-modules/mailer": "^2.0.2",

View File

@@ -13,6 +13,7 @@ import {
StreamableFile,
Header
} from '@nestjs/common';
import { Readable } from 'stream';
import { FilesInterceptor } from '@nestjs/platform-express';
import {
ApiConsumes,
@@ -33,12 +34,13 @@ import { Public } from '@/common/decorators';
export class TrainingController {
constructor(private readonly trainingService: TrainingService) { }
// export training with excel
@Public()
@Get('export/:id')
@ApiOperation({ summary: 'Export training template' })
@ApiOperation({ summary: 'Export training with excel' })
@ApiResponse({
status: 200,
description: 'Return training template.',
description: 'Return training with excel.',
content: { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: { type: 'string', format: 'binary' } } }
})
@Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
@@ -48,9 +50,10 @@ export class TrainingController {
throw new Error('ID is required');
}
const data = await this.trainingService.exportTemplate(Number(id));
return new StreamableFile(data);
return new StreamableFile(Readable.from([data]));
}
// get all training records
@Get()
@ApiOperation({
summary: 'Get all training records with pagination and filters',
@@ -68,6 +71,7 @@ export class TrainingController {
};
}
// get training statistics
@Get('statistics')
@ApiOperation({ summary: 'Get training statistics' })
@ApiResponse({ status: 200, description: 'Return training statistics.' })
@@ -76,6 +80,7 @@ export class TrainingController {
return { message: 'Training statistics fetched successfully', data };
}
// get training record by id
@Get(':id')
@ApiOperation({ summary: 'Get a training record by ID' })
@ApiResponse({ status: 200, description: 'Return the training record.' })
@@ -85,6 +90,7 @@ export class TrainingController {
return { message: 'Training record fetched successfully', data };
}
// create training record
@Post()
@UseInterceptors(FilesInterceptor('files', 3))
@ApiConsumes('multipart/form-data')
@@ -101,6 +107,7 @@ export class TrainingController {
return { message: 'Training record created successfully', data };
}
// update training record
@Patch(':id')
@UseInterceptors(FilesInterceptor('files', 3))
@ApiConsumes('multipart/form-data')
@@ -123,6 +130,7 @@ export class TrainingController {
return { message: 'Training record updated successfully', data };
}
// delete training record
@Delete(':id')
@ApiOperation({ summary: 'Delete a training record' })
@ApiResponse({

View File

@@ -1,16 +1,19 @@
import { HttpException, HttpStatus, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { and, eq, gte, ilike, lte, or, SQL, sql } from 'drizzle-orm';
import { and, eq, getTableColumns, gte, ilike, lte, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as fs from 'fs';
import * as path from 'path';
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 XlsxPopulate from 'xlsx-populate';
// import XlsxPopulate from 'xlsx-populate';
import ExcelJS from 'exceljs';
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';
import sharp from 'sharp';
@Injectable()
export class TrainingService {
@@ -215,9 +218,17 @@ export class TrainingService {
async findOne(id: number) {
const find = await this.drizzle
.select()
.select({
...getTableColumns(trainingSurveys),
stateName: states.name,
municipalityName: municipalities.name,
parishName: parishes.name,
})
.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))
.where(eq(trainingSurveys.id, id))
if (find.length === 0) {
throw new HttpException(
@@ -239,9 +250,14 @@ export class TrainingService {
const savedPaths: string[] = [];
for (const file of files) {
const fileName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${path.extname(file.originalname)}`;
const fileName = `${Date.now()}-${Math.round(Math.random() * 1e9)}.png`;
const filePath = path.join(uploadDir, fileName);
fs.writeFileSync(filePath, file.buffer);
// Convertir a PNG usando sharp antes de guardar
await sharp(file.buffer)
.png()
.toFile(filePath);
savedPaths.push(`/assets/training/${fileName}`);
}
return savedPaths;
@@ -270,7 +286,6 @@ export class TrainingService {
const photoPaths = await this.saveFiles(files);
// 2. Extraer solo visitDate para formatearlo.
// Ya NO extraemos state, municipality, etc. porque no vienen en el DTO.
const { visitDate, state, municipality, parish, ...rest } = createTrainingDto;
const [newRecord] = await this.drizzle
@@ -288,7 +303,7 @@ export class TrainingService {
photo3: photoPaths[2] ?? null,
state: Number(state) ?? null,
municipality: Number(municipality) ?? null,
parish: Number(parish) ?? null,
parish: Number(parish) ?? null,
})
.returning();
@@ -362,200 +377,18 @@ export class TrainingService {
};
}
// 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({
// firstname: trainingSurveys.firstname,
// lastname: trainingSurveys.lastname,
// 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.firstname);
// sheet.cell(`B${currentRow}`).value(record.lastname);
// 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`);
const record = await this.findOne(id);
if (!record) throw new NotFoundException(`No se encontró 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]);
}
// Formatear fecha y hora
const dateObj = new Date(record.visitDate);
const fechaFormateada = dateObj.toLocaleDateString('es-ES');
const horaFormateada = dateObj.toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit',
});
// Ruta de la plantilla
const templatePath = path.join(
@@ -564,53 +397,143 @@ export class TrainingService {
'excel.osp.xlsx',
);
// Cargar la plantilla
const book = await XlsxPopulate.fromFileAsync(templatePath);
// Cargar la plantilla con ExcelJS
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(templatePath);
const worksheet = workbook.getWorksheet(1); // Usar la primera hoja
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' });
if (!worksheet) {
throw new Error('No se pudo encontrar la hoja de trabajo en la plantilla');
}
// 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);
// Llenar los datos principales
worksheet.getCell('A6').value = record.productiveSector;
worksheet.getCell('B8').value = record.stateName;
worksheet.getCell('E8').value = record.municipalityName;
worksheet.getCell('B9').value = record.parishName;
worksheet.getCell('D6').value = record.ospName;
worksheet.getCell('L5').value = fechaFormateada;
worksheet.getCell('L6').value = horaFormateada;
worksheet.getCell('B10').value = record.ospAddress;
worksheet.getCell('C11').value = record.communeEmail;
worksheet.getCell('C12').value = record.communeSpokespersonName;
worksheet.getCell('G11').value = record.communeRif;
worksheet.getCell('G12').value = record.communeSpokespersonPhone;
worksheet.getCell('C13').value = record.siturCodeCommune;
worksheet.getCell('G13').value = record.siturCodeCommunalCouncil;
worksheet.getCell('G14').value = record.communalCouncilRif;
worksheet.getCell('C15').value = record.communalCouncilSpokespersonName;
worksheet.getCell('G15').value = record.communalCouncilSpokespersonPhone;
worksheet.getCell('C16').value = record.ospType;
worksheet.getCell('C17').value = record.ospName;
worksheet.getCell('C18').value = record.productiveActivity;
worksheet.getCell('C19').value = 'Proveedores';
worksheet.getCell('C20').value = record.companyConstitutionYear;
worksheet.getCell('C21').value = record.infrastructureMt2;
worksheet.getCell('G17').value = record.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');
worksheet.getCell(record.hasTransport === true ? 'J19' : 'L19').value = 'X';
worksheet.getCell(record.structureType === 'CASA' ? 'J20' : 'L20').value =
'X';
worksheet.getCell(record.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);
worksheet.getCell('A24').value = record.ospResponsibleFullname;
worksheet.getCell('C24').value = record.ospResponsibleCedula;
worksheet.getCell('E24').value = record.ospResponsiblePhone;
worksheet.getCell('J24').value = 'N Femenino'; // Placeholder si no hay dato
worksheet.getCell('L24').value = 'N Masculino'; // Placeholder si no hay dato
book.sheet(0).cell('J24').value('N Femenino');
book.sheet(0).cell('L24').value('N Masculino');
// const photo1 = record.photo1;
// const photo2 = record.photo2;
// const photo3 = record.photo3;
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);
if (record.photo1) {
const image = record.photo1.slice(17);
const extension = image.split('.')[1];
return book.outputAsync();
// Validar que sea una imagen png, gif o jpeg
if (extension === 'png' || extension === 'gif' || extension === 'jpeg') {
// Ruta de la imagen
const imagePath = path.join(
__dirname,
'../../../',
`uploads/training/${image}`,
);
// Add an image to the workbook from a file buffer
const logoId = workbook.addImage({
filename: imagePath,
extension: extension,
});
// Anchor the image to a specific cell (e.g., A1)
worksheet.addImage(logoId, `I7:L17`); // Spans from A1 to C3
}
}
// let i = 1;
// while (i <= 3) {
// const element = record[`photo${i}`];
// if (element) {
// const image = element.slice(17);
// const extension: extensionType = image.split('.')[1];
// // Validar que sea una imagen png, gif o jpeg
// if (extension === 'png' || extension === 'gif' || extension === 'jpeg') {
// // Ruta de la imagen
// const imagePath = path.join(
// __dirname,
// '../../../',
// `uploads/training/${image}`,
// );
// // Add an image to the workbook from a file buffer
// const logoId = workbook.addImage({
// filename: imagePath,
// extension: extension,
// });
// // Anchor the image to a specific cell (e.g., A1)
// worksheet.addImage(logoId, `I7:L17`); // Spans from A1 to C3
// i = 4;
// }
// }
// i++;
// }
// Listas (Equipos, Materia Prima, Productos)
const equipmentList = Array.isArray(record.equipmentList)
? record.equipmentList
: [];
const productionList = Array.isArray(record.productionList)
? record.productionList
: [];
const productList = Array.isArray(record.productList)
? record.productList
: [];
// Colocar listas empezando en la fila 28
equipmentList.forEach((item: any, i: number) => {
const row = 28 + i;
worksheet.getCell(`A${row}`).value = item.machine;
worksheet.getCell(`C${row}`).value = item.quantity;
});
productionList.forEach((item: any, i: number) => {
const row = 28 + i;
worksheet.getCell(`E${row}`).value = item.rawMaterial;
worksheet.getCell(`G${row}`).value = item.quantity;
});
productList.forEach((item: any, i: number) => {
const row = 28 + i;
worksheet.getCell(`I${row}`).value = item.productName;
worksheet.getCell(`J${row}`).value = item.dailyCount;
worksheet.getCell(`K${row}`).value = item.weeklyCount;
worksheet.getCell(`L${row}`).value = item.monthlyCount;
});
return await workbook.xlsx.writeBuffer();
}
}