diff --git a/apps/api/nest-cli.json b/apps/api/nest-cli.json index 579ca35..72880aa 100644 --- a/apps/api/nest-cli.json +++ b/apps/api/nest-cli.json @@ -5,6 +5,13 @@ "compilerOptions": { "deleteOutDir": true, "builder": "swc", - "typeCheck": true + "typeCheck": true, + "assets": [ + { + "include": "features/training/export_template/*.xlsx", + "outDir": "dist", + "watchAssets": true + } + ] } -} +} \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 22218ea..c32d8ff 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -50,7 +50,8 @@ "pino-pretty": "13.0.0", "reflect-metadata": "0.2.0", "rxjs": "7.8.1", - "sharp": "^0.34.5" + "sharp": "^0.34.5", + "xlsx-populate": "^1.21.0" }, "devDependencies": { "@nestjs-modules/mailer": "^2.0.2", diff --git a/apps/api/src/features/training/export_template/excel.osp.xlsx b/apps/api/src/features/training/export_template/excel.osp.xlsx new file mode 100644 index 0000000..ea80f0d Binary files /dev/null and b/apps/api/src/features/training/export_template/excel.osp.xlsx differ diff --git a/apps/api/src/features/training/training.controller.ts b/apps/api/src/features/training/training.controller.ts index b28c2a1..de5eada 100644 --- a/apps/api/src/features/training/training.controller.ts +++ b/apps/api/src/features/training/training.controller.ts @@ -7,8 +7,11 @@ import { Patch, Post, Query, + Res, UploadedFiles, UseInterceptors, + StreamableFile, + Header } from '@nestjs/common'; import { FilesInterceptor } from '@nestjs/platform-express'; import { @@ -23,11 +26,30 @@ 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') export class TrainingController { - constructor(private readonly trainingService: TrainingService) {} + constructor(private readonly trainingService: TrainingService) { } + + @Public() + @Get('export/:id') + @ApiOperation({ summary: 'Export training template' }) + @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 exportTemplate(@Param('id') id: string) { + if (!Number(id)) { + throw new Error('ID is required'); + } + const data = await this.trainingService.exportTemplate(Number(id)); + return new StreamableFile(data); + } @Get() @ApiOperation({ diff --git a/apps/api/src/features/training/training.service.ts b/apps/api/src/features/training/training.service.ts index 6b4528d..f6d0641 100644 --- a/apps/api/src/features/training/training.service.ts +++ b/apps/api/src/features/training/training.service.ts @@ -1,11 +1,12 @@ -import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { and, eq, 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 { states, trainingSurveys } from 'src/database/index'; +import { municipalities, parishes, states, trainingSurveys } from 'src/database/index'; +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'; @@ -15,7 +16,7 @@ import { UpdateTrainingDto } from './dto/update-training.dto'; export class TrainingService { constructor( @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, - ) {} + ) { } async findAll(paginationDto?: PaginationDto) { const { @@ -285,10 +286,6 @@ export class TrainingService { photo1: photoPaths[0] ?? null, photo2: photoPaths[1] ?? null, photo3: photoPaths[2] ?? null, - - // NOTA: Como las columnas state, municipality, etc. en la BD - // tienen "onDelete: set null" o son nullables, al no pasarlas aquí, - // Postgres automáticamente las guardará como NULL. }) .returning(); @@ -361,4 +358,256 @@ export class TrainingService { 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({ + // 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`); + + // 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(); + } } diff --git a/apps/web/app/dashboard/formulario/page.tsx b/apps/web/app/dashboard/formulario/page.tsx index 35a0044..e963ddf 100644 --- a/apps/web/app/dashboard/formulario/page.tsx +++ b/apps/web/app/dashboard/formulario/page.tsx @@ -5,6 +5,8 @@ import TrainingTableAction from '@/feactures/training/components/training-tables import { searchParamsCache } from '@repo/shadcn/lib/searchparams'; import { SearchParams } from 'nuqs'; +import { env } from '@/lib/env'; + export const metadata = { title: 'Registro de OSP', }; @@ -29,6 +31,7 @@ export default async function Page({ searchParams }: PageProps) { initialPage={page} initialSearch={searchQuery} initialLimit={limit || 10} + apiUrl={env.API_URL} /> diff --git a/apps/web/feactures/training/components/training-list.tsx b/apps/web/feactures/training/components/training-list.tsx index 78bcd77..f471c05 100644 --- a/apps/web/feactures/training/components/training-list.tsx +++ b/apps/web/feactures/training/components/training-list.tsx @@ -1,5 +1,4 @@ 'use client'; - import { DataTable } from '@repo/shadcn/table/data-table'; import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton'; import { useTrainingQuery } from '../hooks/use-training'; @@ -9,12 +8,14 @@ interface TrainingListProps { initialPage: number; initialSearch?: string | null; initialLimit: number; + apiUrl: string; } export default function TrainingList({ initialPage, initialSearch, initialLimit, + apiUrl, }: TrainingListProps) { const filters = { page: initialPage, @@ -30,7 +31,7 @@ export default function TrainingList({ return ( = ({ data }) => { +export const CellAction: React.FC = ({ data, apiUrl }) => { const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const [viewOpen, setViewOpen] = useState(false); @@ -37,6 +38,10 @@ export const CellAction: React.FC = ({ data }) => { } }; + const handleExport = (id?: number | undefined) => { + window.open(`${apiUrl}/training/export/${id}`, '_blank'); + }; + return ( <> = ({ data }) => { + + + + + + +

Exportar Excel

+
+
+
+ diff --git a/apps/web/feactures/training/components/training-tables/columns.tsx b/apps/web/feactures/training/components/training-tables/columns.tsx index 70dbca4..38dcf31 100644 --- a/apps/web/feactures/training/components/training-tables/columns.tsx +++ b/apps/web/feactures/training/components/training-tables/columns.tsx @@ -4,42 +4,48 @@ import { Badge } from '@repo/shadcn/badge'; import { ColumnDef } from '@tanstack/react-table'; import { CellAction } from './cell-action'; -export const columns: ColumnDef[] = [ - { - accessorKey: 'ospName', - header: 'Nombre OSP', - }, - { - accessorKey: 'ospRif', - header: 'RIF', - }, - { - accessorKey: 'ospType', - header: 'Tipo', - }, - { - accessorKey: 'currentStatus', - header: 'Estatus', - cell: ({ row }) => { - const status = row.getValue('currentStatus') as string; - return ( - - {status} - - ); +interface ColumnsProps { + apiUrl: string; +} + +export function columns({ apiUrl }: ColumnsProps): ColumnDef[] { + return [ + { + accessorKey: 'ospName', + header: 'Nombre OSP', }, - }, - { - accessorKey: 'visitDate', - header: 'Fecha Visita', - cell: ({ row }) => { - const date = row.getValue('visitDate') as string; - return date ? new Date(date).toLocaleString() : 'N/A'; + { + accessorKey: 'ospRif', + header: 'RIF', }, - }, - { - id: 'actions', - header: 'Acciones', - cell: ({ row }) => , - }, -]; + { + accessorKey: 'ospType', + header: 'Tipo', + }, + { + accessorKey: 'currentStatus', + header: 'Estatus', + cell: ({ row }) => { + const status = row.getValue('currentStatus') as string; + return ( + + {status} + + ); + }, + }, + { + accessorKey: 'visitDate', + header: 'Fecha Visita', + cell: ({ row }) => { + const date = row.getValue('visitDate') as string; + return date ? new Date(date).toLocaleString() : 'N/A'; + }, + }, + { + id: 'actions', + header: 'Acciones', + cell: ({ row }) => , + }, + ]; +} diff --git a/apps/web/feactures/training/components/training-view-modal.tsx b/apps/web/feactures/training/components/training-view-modal.tsx index 4074a99..78c94d1 100644 --- a/apps/web/feactures/training/components/training-view-modal.tsx +++ b/apps/web/feactures/training/components/training-view-modal.tsx @@ -314,10 +314,10 @@ export function TrainingViewModal({ ))} {(!data.equipmentList || data.equipmentList.length === 0) && ( -

- No hay equipamiento registrado. -

- )} +

+ No hay equipamiento registrado. +

+ )} @@ -345,10 +345,10 @@ export function TrainingViewModal({ ))} {(!data.productionList || data.productionList.length === 0) && ( -

- No hay materia prima registrada. -

- )} +

+ No hay materia prima registrada. +

+ )} @@ -421,7 +421,7 @@ export function TrainingViewModal({ {data.paralysisReason && (

- Motivo Paralización + Motivo de Paralización

{data.paralysisReason}