Download generated DOCX from UI
@ -317,6 +317,7 @@ const certificatePersonName = new Paragraph({
|
||||
font: "Arial",
|
||||
allCaps: true,
|
||||
size: 48,
|
||||
underline: {},
|
||||
}),
|
||||
],
|
||||
alignment: AlignmentType.CENTER,
|
||||
|
441
src/certs/MATPEL.ts
Normal file
@ -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<Buffer> {
|
||||
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);
|
||||
}
|
||||
|
5
src/certs/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Certs
|
||||
|
||||
This folder contains files that generate certificates in DOCX format.
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 245 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
103
src/certs/utils.ts
Normal file
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
@ -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}) {
|
||||
|
@ -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<Persona>;
|
||||
subjectRepo: Repository<CursoGIE>;
|
||||
|
||||
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();
|
||||
|
||||
|
39
src/controller/certificate/dto/GenerateCertificate.dto.ts
Normal file
@ -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;
|
||||
}
|
@ -66,6 +66,7 @@ export function Registers(props: { person: Person | null, lastUpdate: number })
|
||||
<th class="p-2">CÓDIGO</th>
|
||||
<th class="p-2"></th>
|
||||
<th class="p-2"></th>
|
||||
<th class="p-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -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 (
|
||||
<tr class="odd:bg-c-surface-variant">
|
||||
<td class="py-2 px-4">{props.cert.nombre}</td>
|
||||
<td class="py-2 px-4">{props.cert.curso_nombre}</td>
|
||||
<td class="py-2 px-4 font-mono">{props.cert.fecha_inscripcion.toString()}</td>
|
||||
<td class="py-2 px-4">{props.cert.codigo}</td>
|
||||
<td class="py-2 pl-8 pr-4">
|
||||
<button
|
||||
class="rounded-full py-1 px-2 shadow"
|
||||
style={{"background-color": "#0055d5", "color": "#ffffff"}}
|
||||
onclick={getCertificate}
|
||||
>
|
||||
DOCX
|
||||
</button>
|
||||
</td>
|
||||
<td class="py-2 pl-8 pr-4">
|
||||
<button
|
||||
class="rounded-full py-1 px-2 shadow
|
||||
|