From 44485393077da3eac25d67c0e4c1f6be3b69d7a0 Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 12 May 2023 16:01:00 -0500 Subject: [PATCH] Download generated DOCX from UI --- docx/main.ts | 1 + src/certs/MATPEL.ts | 441 ++++++++++++++++++ src/certs/README.md | 5 + {docx => src/certs}/img/aguila_logo.png | Bin {docx => src/certs}/img/cee_logo.png | Bin {docx => src/certs}/img/ceem_logo.jpg | Bin .../certs}/img/colegio_ingenieros_logo.png | Bin {docx => src/certs}/img/eate_logo.jpg | Bin {docx => src/certs}/img/eeg2_logo.png | Bin {docx => src/certs}/img/eeg_f_logo.png | Bin {docx => src/certs}/img/eeg_logo.png | Bin {docx => src/certs}/img/fondo_certificado.png | Bin {docx => src/certs}/img/matpel-logo.png | Bin {docx => src/certs}/img/michigan_logo.png | Bin {docx => src/certs}/img/mtc_logo.png | Bin {docx => src/certs}/img/osha_logo.png | Bin src/certs/utils.ts | 103 ++++ .../certificate/certificate.controller.ts | 15 +- .../certificate/certificate.service.ts | 63 ++- .../dto/GenerateCertificate.dto.ts | 39 ++ src/views/components/Registers.tsx | 48 +- 21 files changed, 711 insertions(+), 4 deletions(-) create mode 100644 src/certs/MATPEL.ts create mode 100644 src/certs/README.md rename {docx => src/certs}/img/aguila_logo.png (100%) rename {docx => src/certs}/img/cee_logo.png (100%) rename {docx => src/certs}/img/ceem_logo.jpg (100%) rename {docx => src/certs}/img/colegio_ingenieros_logo.png (100%) rename {docx => src/certs}/img/eate_logo.jpg (100%) rename {docx => src/certs}/img/eeg2_logo.png (100%) rename {docx => src/certs}/img/eeg_f_logo.png (100%) rename {docx => src/certs}/img/eeg_logo.png (100%) rename {docx => src/certs}/img/fondo_certificado.png (100%) rename {docx => src/certs}/img/matpel-logo.png (100%) rename {docx => src/certs}/img/michigan_logo.png (100%) rename {docx => src/certs}/img/mtc_logo.png (100%) rename {docx => src/certs}/img/osha_logo.png (100%) create mode 100644 src/certs/utils.ts create mode 100644 src/controller/certificate/dto/GenerateCertificate.dto.ts diff --git a/docx/main.ts b/docx/main.ts index ec57f4f..edb8710 100644 --- a/docx/main.ts +++ b/docx/main.ts @@ -317,6 +317,7 @@ const certificatePersonName = new Paragraph({ font: "Arial", allCaps: true, size: 48, + underline: {}, }), ], alignment: AlignmentType.CENTER, diff --git a/src/certs/MATPEL.ts b/src/certs/MATPEL.ts new file mode 100644 index 0000000..f2c6933 --- /dev/null +++ b/src/certs/MATPEL.ts @@ -0,0 +1,441 @@ +import { + Document, Packer, Paragraph, PageOrientation, ImageRun, + HorizontalPositionRelativeFrom, VerticalPositionRelativeFrom, + FrameAnchorType, + HorizontalPositionAlign, + VerticalPositionAlign, + TextRun, + AlignmentType, +} from "docx"; +import * as QR from "qrcode"; +import { Matpel, cm, cmText, cmToEmu, getImage, getMatpelHours, getMatpelLabel } from "./utils"; + + + +const imgFondoDoc = getImage({ + name: "fondo_certificado.png", + height: 20.97, + width: 29.8, + horizontalOffset: 0, + verticalOffset: 0, + behindDocument: true, +}); + +const imgMatpel = getImage({ + name: "matpel-logo.png", + height: 2.81, + width: 2.81, + horizontalOffset: 0.7, + verticalOffset: 0.7, +}); + +const imgCIP = getImage({ + name: "colegio_ingenieros_logo.png", + height: 2.15, + width: 2.15, + horizontalOffset: 0.91, + verticalOffset: 3.64, +}); + +const imgCEE = getImage({ + name: "cee_logo.png", + height: 2.22, + width: 3.11, + horizontalOffset: 0.29, + verticalOffset: 18, +}); + +const imgMTC = getImage({ + name: "mtc_logo.png", + height: 1.3, + width: 4.08, + horizontalOffset: 5.13, + verticalOffset: 19.33, +}); + +const imgEEG = getImage({ + name: "eeg_f_logo.png", + height: 4.39, + width: 6.4, + horizontalOffset: 23.35, + verticalOffset: 0, +}); + +const imgOSHA = getImage({ + name: "osha_logo.png", + height: 1.55, + width: 4.46, + horizontalOffset: 24.34, + verticalOffset: 4.95, +}); + +const imgAguila = getImage({ + name: "aguila_logo.png", + height: 2.62, + width: 2.68, + horizontalOffset: 25.45, + verticalOffset: 6.57, +}); + +const imgMichigan = getImage({ + name: "michigan_logo.png", + height: 2.47, + width: 2.6, + horizontalOffset: 25.59, + verticalOffset: 9.54, +}); + +const imgCEEM = getImage({ + name: "ceem_logo.jpg", + height: 1, + width: 3.99, + horizontalOffset: 24.97, + verticalOffset: 12.5, +}); + +const imgEATE = getImage({ + name: "eate_logo.jpg", + height: 2.21, + width: 3.28, + horizontalOffset: 25.54, + verticalOffset: 14.09, +}); + + + + +export type CertData = { + matpel: Matpel, + /** + * Full name in format: "FIRST_NAMES LAST_NAME_1 LAST_NAME_2" + */ + personFullName: string, + /** + * DNI as a string. + * + * E.g.: "04123523" + */ + personDni: string, + /** + * Cert code as a string. Must have length = 4. + * + * E.g.: "0322" + */ + certCode: string, + /** + * Year of emission as a string. + * + * E.g.: "2023" + */ + certYear: string, + /** + * Month of emission. + * + * E.g.: "05" + */ + certMonth: string, + /** + * Day of emission. + * + * E.g.: "23" + */ + certDay: string, +} +/** + * Generates a certificate in format DOCX and in-place. + * + * @param props Data to use in the certificate + * @returns A buffer of the DOCX document + */ +export async function matpelCert(props: CertData): Promise { + const certificateName = new Paragraph({ + frame: { + position: { + x: cmText(3.13), + y: cmText(1.9), + }, + width: cmText(14.28), + height: cmText(3.19), + anchor: { + horizontal: FrameAnchorType.MARGIN, + vertical: FrameAnchorType.MARGIN, + }, + alignment: { + x: HorizontalPositionAlign.CENTER, + y: VerticalPositionAlign.TOP, + }, + }, + children: [ + new TextRun({ + text: `CAPACITACIÓN EN MANEJO; MANIPULACIÓN Y TRANSPORTE DE MATERIALES Y RESIDUOS PELIGROSOS - ${getMatpelLabel(props.matpel)}`, + bold: true, + font: "Arial", + size: 32, + }), + ], + alignment: AlignmentType.CENTER, + }); + + const certificatePersonName = new Paragraph({ + frame: { + position: { + x: cmText(0), + y: cmText(6.5), + }, + width: cmText(23), + height: cmText(1.5), + anchor: { + horizontal: FrameAnchorType.MARGIN, + vertical: FrameAnchorType.MARGIN, + }, + alignment: { + x: HorizontalPositionAlign.CENTER, + y: VerticalPositionAlign.TOP, + }, + }, + children: [ + new TextRun({ + text: props.personFullName.toUpperCase(), + bold: true, + font: "Arial", + allCaps: true, + size: 48, + underline: {}, + }), + ], + alignment: AlignmentType.CENTER, + }); + + // "Se expide el presente certificado a:" + const certificateExpedite = new Paragraph({ + frame: { + position: { + x: cmText(2.85), + y: cmText(5.15), + }, + width: cmText(14.28), + height: cmText(1), + anchor: { + horizontal: FrameAnchorType.MARGIN, + vertical: FrameAnchorType.MARGIN, + }, + alignment: { + x: HorizontalPositionAlign.CENTER, + y: VerticalPositionAlign.TOP, + }, + }, + children: [ + new TextRun({ + text: "Se expide el presente certificado a:", + font: "Arial", + size: 22, + }), + ], + alignment: AlignmentType.CENTER, + }); + + // N° XXXX-202X-EEG + const certificateCode = new Paragraph({ + frame: { + position: { + x: cmText(17.1), + y: cmText(5.35), + }, + width: cmText(3.68), + height: cmText(0.7), + anchor: { + horizontal: FrameAnchorType.MARGIN, + vertical: FrameAnchorType.MARGIN, + }, + alignment: { + x: HorizontalPositionAlign.CENTER, + y: VerticalPositionAlign.TOP, + }, + }, + children: [ + new TextRun({ + text: `N° ${props.certCode}-${props.certYear}-EEG`, + font: "Arial", + size: 20, + }), + ], + alignment: AlignmentType.CENTER, + }); + + // Identificado con dni ... + const certificateBody = new Paragraph({ + frame: { + position: { + x: cmText(1.51), + y: cmText(8.35), + }, + width: cmText(20.08), + height: cmText(3.7), + anchor: { + horizontal: FrameAnchorType.MARGIN, + vertical: FrameAnchorType.MARGIN, + }, + alignment: { + x: HorizontalPositionAlign.CENTER, + y: VerticalPositionAlign.TOP, + }, + }, + children: [ + new TextRun({ + text: "Identificado con DNI N° ", + font: "Arial", + size: 24, + }), + new TextRun({ + text: props.personDni, + font: "Arial", + size: 24, + bold: true, + }), + new TextRun({ + text: ", al haber aprobado el curso de capacitación sobre ", + font: "Arial", + size: 24, + }), + new TextRun({ + text: `MANEJO DE MATERIALES Y RESIDUOS PELIGROSOS - ${getMatpelLabel(props.matpel)}`, + font: "Arial", + size: 24, + bold: true, + }), + new TextRun({ + text: `, según lo estipulado en la Ley N°28256 (ley que regula el Transporte de Materiales y Residuos Peligrosos) y Decreto Supremo N° 021-2008-MTC (Reglamento Nacional de Transporte Terrestre de Materiales y Residuos Peligrosos) con una duración de ${getMatpelHours(props.matpel)} horas lectivas.`, + font: "Arial", + size: 24, + }), + ], + alignment: AlignmentType.JUSTIFIED, + }); + + // Se expide certificado... + const certificateFinishLabel = new Paragraph({ + frame: { + position: { + x: cmText(1.52), + y: cmText(11.25), + }, + width: cmText(20.1), + height: cmText(0.8), + anchor: { + horizontal: FrameAnchorType.MARGIN, + vertical: FrameAnchorType.MARGIN, + }, + alignment: { + x: HorizontalPositionAlign.CENTER, + y: VerticalPositionAlign.TOP, + }, + }, + children: [ + new TextRun({ + text: "Se expide certificado para los fines que se estime conveniente.", + font: "Arial", + size: 24, + }), + ], + alignment: AlignmentType.CENTER, + }); + + // Fecha de Emision: ... + const certificateDate = new Paragraph({ + frame: { + position: { + x: cmText(16), + y: cmText(16.5), + }, + width: cmText(9), + height: cmText(1.4), + anchor: { + horizontal: FrameAnchorType.MARGIN, + vertical: FrameAnchorType.MARGIN, + }, + alignment: { + x: HorizontalPositionAlign.CENTER, + y: VerticalPositionAlign.TOP, + }, + }, + children: [ + new TextRun({ + text: `Fecha de Emisión:\t\t${props.certDay} / ${props.certMonth} / ${props.certYear}`, + font: "Arial", + size: 20, + break: 1, + }), + new TextRun({ + text: `Fecha de Expiración:\t\t${props.certDay} / ${props.certMonth} / ${parseInt(props.certYear, 10) + 1}`, + font: "Arial", + size: 20, + break: 1, + }), + ], + alignment: AlignmentType.LEFT, + }); + + const qr = await QR.toDataURL(`https://www.eegsac.com/alumnoscertificados.php?DNI=${props.personDni}`, {margin: 1}); + + const imgQR = new ImageRun({ + data: qr, + transformation: { + height: cm(2.47), + width: cm(2.47), + }, + floating: { + zIndex: 1, + horizontalPosition: { + relative: HorizontalPositionRelativeFrom.LEFT_MARGIN, + offset: cmToEmu(26.3), + }, + verticalPosition: { + relative: VerticalPositionRelativeFrom.TOP_MARGIN, + offset: cmToEmu(16.48), + }, + }, + }); + + const doc = new Document({ + sections: [ + { + properties: { + page: { + size: { + orientation: PageOrientation.LANDSCAPE, + }, + }, + }, + children: [ + certificateName, + certificatePersonName, + certificateExpedite, + certificateCode, + certificateBody, + certificateFinishLabel, + certificateDate, + new Paragraph({ + children: [ + imgFondoDoc, + imgMatpel, + imgCIP, + imgCEE, + imgMTC, + imgEEG, + imgOSHA, + imgAguila, + imgMichigan, + imgCEEM, + imgEATE, + imgQR, + ], + }), + ], + }, + ], + }); + + // Return document as a buffer + return await Packer.toBuffer(doc); +} + diff --git a/src/certs/README.md b/src/certs/README.md new file mode 100644 index 0000000..82cd5ec --- /dev/null +++ b/src/certs/README.md @@ -0,0 +1,5 @@ +# Certs + +This folder contains files that generate certificates in DOCX format. + + diff --git a/docx/img/aguila_logo.png b/src/certs/img/aguila_logo.png similarity index 100% rename from docx/img/aguila_logo.png rename to src/certs/img/aguila_logo.png diff --git a/docx/img/cee_logo.png b/src/certs/img/cee_logo.png similarity index 100% rename from docx/img/cee_logo.png rename to src/certs/img/cee_logo.png diff --git a/docx/img/ceem_logo.jpg b/src/certs/img/ceem_logo.jpg similarity index 100% rename from docx/img/ceem_logo.jpg rename to src/certs/img/ceem_logo.jpg diff --git a/docx/img/colegio_ingenieros_logo.png b/src/certs/img/colegio_ingenieros_logo.png similarity index 100% rename from docx/img/colegio_ingenieros_logo.png rename to src/certs/img/colegio_ingenieros_logo.png diff --git a/docx/img/eate_logo.jpg b/src/certs/img/eate_logo.jpg similarity index 100% rename from docx/img/eate_logo.jpg rename to src/certs/img/eate_logo.jpg diff --git a/docx/img/eeg2_logo.png b/src/certs/img/eeg2_logo.png similarity index 100% rename from docx/img/eeg2_logo.png rename to src/certs/img/eeg2_logo.png diff --git a/docx/img/eeg_f_logo.png b/src/certs/img/eeg_f_logo.png similarity index 100% rename from docx/img/eeg_f_logo.png rename to src/certs/img/eeg_f_logo.png diff --git a/docx/img/eeg_logo.png b/src/certs/img/eeg_logo.png similarity index 100% rename from docx/img/eeg_logo.png rename to src/certs/img/eeg_logo.png diff --git a/docx/img/fondo_certificado.png b/src/certs/img/fondo_certificado.png similarity index 100% rename from docx/img/fondo_certificado.png rename to src/certs/img/fondo_certificado.png diff --git a/docx/img/matpel-logo.png b/src/certs/img/matpel-logo.png similarity index 100% rename from docx/img/matpel-logo.png rename to src/certs/img/matpel-logo.png diff --git a/docx/img/michigan_logo.png b/src/certs/img/michigan_logo.png similarity index 100% rename from docx/img/michigan_logo.png rename to src/certs/img/michigan_logo.png diff --git a/docx/img/mtc_logo.png b/src/certs/img/mtc_logo.png similarity index 100% rename from docx/img/mtc_logo.png rename to src/certs/img/mtc_logo.png diff --git a/docx/img/osha_logo.png b/src/certs/img/osha_logo.png similarity index 100% rename from docx/img/osha_logo.png rename to src/certs/img/osha_logo.png diff --git a/src/certs/utils.ts b/src/certs/utils.ts new file mode 100644 index 0000000..6325d2f --- /dev/null +++ b/src/certs/utils.ts @@ -0,0 +1,103 @@ +import { HorizontalPositionRelativeFrom, ImageRun, VerticalPositionRelativeFrom, convertMillimetersToTwip } from "docx"; +import * as fs from "fs"; +import { join } from "path"; + + +// To define centimeters in a image's size +export function cm(centimeters: number) { + return convertMillimetersToTwip((100 * centimeters) / 150); +} + +// To define centimeters in a floating image's position +export function cmToEmu(cm: number) { + return Math.round(cm * 360000); +} + +// To define centimeters in a text +export function cmText(cm: number) { + return Math.round(cm * 567); +} + +type ImgConfig = { + name: string, + height: number, + width: number, + horizontalOffset: number, + verticalOffset: number, + behindDocument?: boolean, +} + +export function getImage(data: ImgConfig): ImageRun { + return new ImageRun({ + data: fs.readFileSync(join(__dirname, "img", data.name)), + transformation: { + height: cm(data.height), + width: cm(data.width), + }, + floating: { + zIndex: 0, + horizontalPosition: { + relative: HorizontalPositionRelativeFrom.LEFT_MARGIN, + offset: cmToEmu(data.horizontalOffset), + }, + verticalPosition: { + relative: VerticalPositionRelativeFrom.TOP_MARGIN, + offset: cmToEmu(data.verticalOffset), + }, + behindDocument: data.behindDocument ?? false, + }, + }); +} + + +export enum Matpel { + _1, + _2, + _3, +} + +const matpel1Label = "MATPEL I - Advertencia"; +const matpel2Label = "MATPEL II - Operaciones"; +const matpel3Label = "MATPEL III - Técnico"; + +export function getMatpelLabel(matpel: Matpel) { + switch (matpel) { + case Matpel._1: { + return matpel1Label; + } + case Matpel._2: { + return matpel2Label; + } + case Matpel._3: { + return matpel3Label; + } + } +} + +export function getMatpelHours(matpel: Matpel): number { + switch (matpel) { + case Matpel._1: { + return 12; + } + case Matpel._2: { + return 12; + } + case Matpel._3: { + return 40; + } + } +} + +export function getMatpelFileName(matpel: Matpel) { + switch (matpel) { + case Matpel._1: { + return "MATPEL 1"; + } + case Matpel._2: { + return "MATPEL 2"; + } + case Matpel._3: { + return "MATPEL 3"; + } + } +} diff --git a/src/controller/certificate/certificate.controller.ts b/src/controller/certificate/certificate.controller.ts index 2736fa3..bbd570a 100644 --- a/src/controller/certificate/certificate.controller.ts +++ b/src/controller/certificate/certificate.controller.ts @@ -1,9 +1,11 @@ -import { Body, Controller, Delete, Get, InternalServerErrorException, Param, Post } from "@nestjs/common"; +import { Body, Controller, Delete, Get, InternalServerErrorException, Param, Post, Res, StreamableFile } from "@nestjs/common"; import { renderToString } from "solid-js/web"; import { Certs } from "../../views/Certs"; import { template } from "./certificate.template"; import { CertificateService } from "./certificate.service"; import { CertificateDto } from "./certificate.dto"; +import type { Response } from "express"; + @Controller("certificate") export class CertificateController { @@ -27,6 +29,17 @@ export class CertificateController { await this.certificateService.create(subjectDto); } + @Post("generate/:id") + async generate(@Param() param: {id: string}, @Res({ passthrough: true }) res: Response) { + const [stream, filename] = await this.certificateService.generate(parseInt(param.id, 10)); + + const str = res.writeHead(201, { + "Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "Content-Disposition": `attachment; filename="${filename}"}`, + }); + + str.write(stream); + } @Delete(":id") async delete(@Param() param: {id: number}) { diff --git a/src/controller/certificate/certificate.service.ts b/src/controller/certificate/certificate.service.ts index da754a4..a6015f6 100644 --- a/src/controller/certificate/certificate.service.ts +++ b/src/controller/certificate/certificate.service.ts @@ -5,7 +5,9 @@ import { RegisterReturn } from "../../types/RegisterReturn"; import { CertificateDto } from "./certificate.dto"; import { Persona } from "../../model/Persona/persona.entity"; import { CursoGIE } from "../../model/CursoGIE/cursoGIE.entity"; -import { data } from "autoprefixer"; +import { GenerateCertificateDto } from "./dto/GenerateCertificate.dto"; +import { CertData, matpelCert } from "src/certs/MATPEL"; +import { Matpel, getMatpelFileName } from "src/certs/utils"; @Injectable() @@ -14,7 +16,7 @@ export class CertificateService { personGIERepository: Repository; subjectRepo: Repository; - constructor(private dataSource: DataSource) { + constructor(dataSource: DataSource) { this.registroGIERepository = dataSource.getRepository(RegistroGIE); this.personGIERepository = dataSource.getRepository(Persona); this.subjectRepo = dataSource.getRepository(CursoGIE); @@ -39,6 +41,63 @@ export class CertificateService { })); } + /** + * Generates a certificate. + * @param id Id of the register + * @returns The certificate and its filename + */ + async generate(id: number): Promise<[Buffer, string]> { + const registerR = await this.registroGIERepository.find({ + relations: { + persona: true, + }, + where: { + id, + }, + }); + + + if (registerR.length === 0) { + throw new BadRequestException("ID invalido"); + } + + const register = registerR[0]; + + const person = register.persona; + // YYYY-MM-DD + const date = register.fecha_inscripcion.toString(); + const [, certYear, certMonth, certDay] = /(\d\d\d\d)-(\d\d)-(\d\d)/.exec(date)!; + + // Matpel 1, 2 & 3 + // TODO: Get actual ids from db instead of hard coding them? + if (register.curso !== 10 && register.curso !== 11 && register.curso !== 12) { + throw new BadRequestException("Curso no soportado"); + } + + let matpel = Matpel._1; + if (register.curso === 10) { + matpel = Matpel._1; + } else if (register.curso === 11) { + matpel = Matpel._2; + } else if (register.curso === 12) { + matpel = Matpel._3; + } + + const personFullName = `${person.nombres} ${person.apellidoPaterno} ${person.apellidoMaterno}`; + + const data: CertData = { + matpel, + personFullName: personFullName, + personDni: register.dni, + certCode: register.codigo.toString().padStart(4, "0"), + certYear, + certMonth, + certDay, + }; + + return [await matpelCert(data), `${getMatpelFileName(matpel)} - ${personFullName.toUpperCase()}.docx`]; + } + async create(data: CertificateDto) { const certificate = new RegistroGIE(); diff --git a/src/controller/certificate/dto/GenerateCertificate.dto.ts b/src/controller/certificate/dto/GenerateCertificate.dto.ts new file mode 100644 index 0000000..ea53046 --- /dev/null +++ b/src/controller/certificate/dto/GenerateCertificate.dto.ts @@ -0,0 +1,39 @@ +import { Matpel } from "src/certs/utils"; + +export class GenerateCertificateDto { + matpel: Matpel; + /** + * Full name in format: "FIRST_NAMES LAST_NAME_1 LAST_NAME_2" + */ + personFullName: string; + /** + * DNI as a string. + * + * E.g.: "04123523" + */ + personDni: string; + /** + * Cert code as a string. Must have length = 4. + * + * E.g.: "0322" + */ + certCode: string; + /** + * Year of emission as a string. + * + * E.g.: "2023" + */ + certYear: string; + /** + * Month of emission. + * + * E.g.: "05" + */ + certMonth: string; + /** + * Day of emission. + * + * E.g.: "23" + */ + certDay: string; +} diff --git a/src/views/components/Registers.tsx b/src/views/components/Registers.tsx index 239a3bf..8aa91fb 100644 --- a/src/views/components/Registers.tsx +++ b/src/views/components/Registers.tsx @@ -66,6 +66,7 @@ export function Registers(props: { person: Person | null, lastUpdate: number }) CÓDIGO + @@ -89,7 +90,6 @@ function Register(props: {cert: RegisterReturn, onUpdate: () => void}) { const [deleteText, setDeleteText] = createSignal("Eliminar"); const deleteRegister = async() => { - console.log(":D"); if (deleteConfirmation()) { // Actually delete from DB const response = await fetch(`/certificate/${props.cert.id}`,{ @@ -113,12 +113,58 @@ function Register(props: {cert: RegisterReturn, onUpdate: () => void}) { } }; + const getCertificate = async() => { + const response = await fetch(`/certificate/generate/${props.cert.id}`,{ + method: "POST", + }); + + if (response.ok) { + const h = response.headers; + + let filename = "certificate.docx"; + const disposition = h.get("Content-Disposition"); + if (disposition && disposition.indexOf("attachment") !== -1) { + const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; + const matches = filenameRegex.exec(disposition); + if (matches !== null && matches[1]) { + filename = matches[1].replace(/['"]/g, ""); + } + } + + const blob = await response.blob(); + + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.style.display = "none"; + a.href = url; + + a.download = filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + + return; + + } else { + alert("Error downloading..."); + } + }; + return ( {props.cert.nombre} {props.cert.curso_nombre} {props.cert.fecha_inscripcion.toString()} {props.cert.codigo} + + +