Exportar datos de osp de la db en un excel

This commit is contained in:
2026-05-07 16:07:44 -04:00
parent f9eef8ebd6
commit 4ed110bafa
5 changed files with 299 additions and 3 deletions

View File

@@ -3,11 +3,13 @@ import {
Controller,
Delete,
Get,
Header,
Param,
Patch,
Post,
Query,
Req,
StreamableFile,
UploadedFiles,
UseInterceptors,
} from '@nestjs/common';
@@ -24,6 +26,7 @@ import { CreateTrainingDto } from './dto/create-training.dto';
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
import { UpdateTrainingDto } from './dto/update-training.dto';
import { TrainingService } from './training.service';
import { Public } from '@/common/decorators';
@ApiTags('training')
@Controller('training')
@@ -76,6 +79,38 @@ export class TrainingController {
const data = await this.trainingService.getStatistics(filterDto);
return { message: 'Training statistics fetched successfully', data };
}
// ========== //
// @Get('export/all')
// @ApiOperation({ summary: 'Export all training records to Excel' })
// @ApiResponse({
// status: 200,
// description: 'Return training records Excel.',
// })
// async exportAll(@Query() filterDto: TrainingStatisticsFilterDto) {
// const data = await this.trainingService.exportAll(filterDto);
// return new StreamableFile(data, {
// type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
// disposition: 'attachment; filename=training_surveys.xlsx',
// });
// }
@Public()
@Get('export/all')
@ApiOperation({ summary: 'Export all training records to Excel' })
@ApiResponse({
status: 200,
description: 'Return training template.',
content: { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: { type: 'string', format: 'binary' } } }
})
@Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
@Header('Content-Disposition', 'attachment; filename=export_osp.xlsx')
async exportAll(@Query() filterDto: TrainingStatisticsFilterDto) {
const data = await this.trainingService.exportAll(filterDto);
return new StreamableFile(data, {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
disposition: 'attachment; filename=training_surveys.xlsx',
});
}
// ========== //
@Get(':id')

View File

@@ -5,6 +5,11 @@ 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';
@@ -98,7 +103,7 @@ export class TrainingService {
if (municipalityId)
filters.push(eq(trainingSurveys.municipality, municipalityId));
if (parishId) filters.push(eq(trainingSurveys.parish, parishId));
if (ospType) filters.push(eq(trainingSurveys.ospType, ospType));
if (ospType && ospType !== 'all') filters.push(eq(trainingSurveys.ospType, ospType));
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
@@ -864,4 +869,178 @@ export class TrainingService {
// 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,
);
}
}
}

View File

@@ -6,6 +6,7 @@ import {
TrainingSchema,
trainingApiResponseSchema,
} from '../schemas/training';
import z from 'zod';
export const getTrainingStatisticsAction = async (
params: {
@@ -159,3 +160,39 @@ export const getTrainingByIdAction = async (id: number) => {
return response?.data;
};
export const exportTrainingAction = async (
params: {
startDate?: string;
endDate?: string;
stateId?: number;
municipalityId?: number;
parishId?: number;
ospType?: string;
} = {},
) => {
const searchParams = new URLSearchParams();
if (params.startDate) searchParams.append('startDate', params.startDate);
if (params.endDate) searchParams.append('endDate', params.endDate);
if (params.stateId) searchParams.append('stateId', params.stateId.toString());
if (params.municipalityId)
searchParams.append('municipalityId', params.municipalityId.toString());
if (params.parishId)
searchParams.append('parishId', params.parishId.toString());
if (params.ospType) searchParams.append('ospType', params.ospType);
const [error, response] = await safeFetchApi(
z.any(), //Schema
`/training/export/all?${searchParams.toString()}`,
'GET',
undefined,
{ responseType: 'arraybuffer' },
);
if (error) {
throw new Error(error.message || 'Error al exportar los datos');
}
return Array.from(new Uint8Array(response));
};

View File

@@ -37,6 +37,8 @@ import {
YAxis,
} from 'recharts';
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
import { exportTrainingAction } from '../actions/training-actions';
import { Download } from 'lucide-react';
const OSP_TYPES = [
'EPSD',
@@ -89,6 +91,38 @@ export function TrainingStatistics() {
setOspType('');
};
const [isExporting, setIsExporting] = useState(false);
const handleExport = async () => {
try {
setIsExporting(true);
const bytes = await exportTrainingAction({
startDate: startDate || undefined,
endDate: endDate || undefined,
stateId: stateId || undefined,
municipalityId: municipalityId || undefined,
parishId: parishId || undefined,
ospType: ospType || undefined,
});
const blob = new Blob([new Uint8Array(bytes)], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `entrenamientos_${new Date().toISOString().split('T')[0]}.xlsx`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Error exporting:', error);
} finally {
setIsExporting(false);
}
};
if (isLoading) {
return (
<div className="flex justify-center p-8">Cargando estadísticas...</div>
@@ -216,10 +250,19 @@ export function TrainingStatistics() {
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<div className="flex items-end gap-2">
<Button variant="outline" onClick={handleClearFilters}>
Limpiar Filtros
</Button>
<Button
variant="default"
onClick={handleExport}
disabled={isExporting}
className="bg-green-600 hover:bg-green-700"
>
<Download className="mr-2 h-4 w-4" />
{isExporting ? 'Exportando...' : 'Exportar Excel'}
</Button>
</div>
</div>
</CardContent>

View File

@@ -1,6 +1,6 @@
'use server';
import { env } from '@/lib/env';
import axios, { InternalAxiosRequestConfig } from 'axios';
import axios, { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
import { z } from 'zod';
// Crear instancia de Axios con la URL base validada
@@ -32,6 +32,7 @@ export const safeFetchApi = async <T extends z.ZodSchema<any>>(
url: string,
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
data?: any,
config?: AxiosRequestConfig,
): Promise<
[{ type: string; message: string; details?: any } | null, z.infer<T> | null]
> => {
@@ -40,6 +41,7 @@ export const safeFetchApi = async <T extends z.ZodSchema<any>>(
method,
url,
data,
...config,
});
const parsed = schema.safeParse(response.data);