Download generated DOCX from UI
@ -317,6 +317,7 @@ const certificatePersonName = new Paragraph({
|
|||||||
font: "Arial",
|
font: "Arial",
|
||||||
allCaps: true,
|
allCaps: true,
|
||||||
size: 48,
|
size: 48,
|
||||||
|
underline: {},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
alignment: AlignmentType.CENTER,
|
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 { renderToString } from "solid-js/web";
|
||||||
import { Certs } from "../../views/Certs";
|
import { Certs } from "../../views/Certs";
|
||||||
import { template } from "./certificate.template";
|
import { template } from "./certificate.template";
|
||||||
import { CertificateService } from "./certificate.service";
|
import { CertificateService } from "./certificate.service";
|
||||||
import { CertificateDto } from "./certificate.dto";
|
import { CertificateDto } from "./certificate.dto";
|
||||||
|
import type { Response } from "express";
|
||||||
|
|
||||||
|
|
||||||
@Controller("certificate")
|
@Controller("certificate")
|
||||||
export class CertificateController {
|
export class CertificateController {
|
||||||
@ -27,6 +29,17 @@ export class CertificateController {
|
|||||||
await this.certificateService.create(subjectDto);
|
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")
|
@Delete(":id")
|
||||||
async delete(@Param() param: {id: number}) {
|
async delete(@Param() param: {id: number}) {
|
||||||
|
@ -5,7 +5,9 @@ import { RegisterReturn } from "../../types/RegisterReturn";
|
|||||||
import { CertificateDto } from "./certificate.dto";
|
import { CertificateDto } from "./certificate.dto";
|
||||||
import { Persona } from "../../model/Persona/persona.entity";
|
import { Persona } from "../../model/Persona/persona.entity";
|
||||||
import { CursoGIE } from "../../model/CursoGIE/cursoGIE.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()
|
@Injectable()
|
||||||
@ -14,7 +16,7 @@ export class CertificateService {
|
|||||||
personGIERepository: Repository<Persona>;
|
personGIERepository: Repository<Persona>;
|
||||||
subjectRepo: Repository<CursoGIE>;
|
subjectRepo: Repository<CursoGIE>;
|
||||||
|
|
||||||
constructor(private dataSource: DataSource) {
|
constructor(dataSource: DataSource) {
|
||||||
this.registroGIERepository = dataSource.getRepository(RegistroGIE);
|
this.registroGIERepository = dataSource.getRepository(RegistroGIE);
|
||||||
this.personGIERepository = dataSource.getRepository(Persona);
|
this.personGIERepository = dataSource.getRepository(Persona);
|
||||||
this.subjectRepo = dataSource.getRepository(CursoGIE);
|
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) {
|
async create(data: CertificateDto) {
|
||||||
const certificate = new RegistroGIE();
|
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">CÓDIGO</th>
|
||||||
<th class="p-2"></th>
|
<th class="p-2"></th>
|
||||||
<th class="p-2"></th>
|
<th class="p-2"></th>
|
||||||
|
<th class="p-2"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
@ -89,7 +90,6 @@ function Register(props: {cert: RegisterReturn, onUpdate: () => void}) {
|
|||||||
const [deleteText, setDeleteText] = createSignal("Eliminar");
|
const [deleteText, setDeleteText] = createSignal("Eliminar");
|
||||||
|
|
||||||
const deleteRegister = async() => {
|
const deleteRegister = async() => {
|
||||||
console.log(":D");
|
|
||||||
if (deleteConfirmation()) {
|
if (deleteConfirmation()) {
|
||||||
// Actually delete from DB
|
// Actually delete from DB
|
||||||
const response = await fetch(`/certificate/${props.cert.id}`,{
|
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 (
|
return (
|
||||||
<tr class="odd:bg-c-surface-variant">
|
<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.nombre}</td>
|
||||||
<td class="py-2 px-4">{props.cert.curso_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 font-mono">{props.cert.fecha_inscripcion.toString()}</td>
|
||||||
<td class="py-2 px-4">{props.cert.codigo}</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">
|
<td class="py-2 pl-8 pr-4">
|
||||||
<button
|
<button
|
||||||
class="rounded-full py-1 px-2 shadow
|
class="rounded-full py-1 px-2 shadow
|
||||||
|