From 172ec7690894fa7cd7507d0bcd81a502e738965a Mon Sep 17 00:00:00 2001 From: Araozu Date: Mon, 30 Oct 2023 15:07:23 -0500 Subject: [PATCH] [FE][Certs] Generate Matpel 3 carnet w/o QR --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 48 ++++++ frontend/src/App.tsx | 13 +- .../src/certs/Registers/RegisterElement.tsx | 148 ++++++++++++++++++ .../src/certs/Registers/RegisterSidebar.tsx | 59 ++++++- frontend/src/certs/Registers/index.tsx | 147 +---------------- frontend/src/utils/carnetGenerator.ts | 78 +++++++++ frontend/src/utils/functions.ts | 18 +++ 8 files changed, 364 insertions(+), 148 deletions(-) create mode 100644 frontend/src/certs/Registers/RegisterElement.tsx create mode 100644 frontend/src/utils/carnetGenerator.ts diff --git a/frontend/package.json b/frontend/package.json index 55f6bf4..fc04c0f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "@types/file-saver": "^2.0.5", "@types/qrcode": "^1.5.1", "axios": "^1.5.1", + "canvg": "^4.0.1", "file-saver": "^2.0.5", "qrcode": "^1.5.3", "solid-js": "^1.7.6" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b893ed0..c33583d 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: axios: specifier: ^1.5.1 version: 1.5.1 + canvg: + specifier: ^4.0.1 + version: 4.0.1 file-saver: specifier: ^2.0.5 version: 2.0.5 @@ -956,12 +959,20 @@ packages: resolution: {integrity: sha512-c0Snqx/IpY+QHnCqEQzo9NjhBNth6gm6/4wDgWt1ML24ldNiStXmxuKJUKM7ob/iAhQWH/dUH1STRt5eTUixlw==} dev: false + /@types/offscreencanvas@2019.7.2: + resolution: {integrity: sha512-ujCjOxeA07IbEBQYAkoOI+XFw5sT3nhWJ/xZfPR6reJppDG7iPQPZacQiLTtWH1b3a2NYXWlxvYqa40y/LAixQ==} + dev: false + /@types/qrcode@1.5.1: resolution: {integrity: sha512-HpSN675K0PmxIDRpjMI3Mc2GiKo3dNu+X/F5SoItiaDS1lVfgC6Wac1c5lQDfKWbTJUSHWiHKzpJpBZG7k9gaA==} dependencies: '@types/node': 20.5.5 dev: false + /@types/raf@3.4.2: + resolution: {integrity: sha512-sM4HyDVlDFl4goOXPF+g9nNHJFZQGot+HgySjM4cRjqXzjdatcEvYrtG4Ia8XumR9T6k8G2tW9B7hnUj51Uf0A==} + dev: false + /@types/semver@7.5.0: resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} dev: true @@ -1345,6 +1356,18 @@ packages: resolution: {integrity: sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==} dev: true + /canvg@4.0.1: + resolution: {integrity: sha512-5gD/d6SiCCT7baLnVr0hokYe93DfcHW2rSqdKOuOQD84YMlyfttnZ8iQsThTdX6koYam+PROz/FuQTo500zqGw==} + engines: {node: '>=12.0.0'} + dependencies: + '@types/offscreencanvas': 2019.7.2 + '@types/raf': 3.4.2 + raf: 3.4.1 + rgbcolor: 1.0.1 + stackblur-canvas: 2.6.0 + svg-pathdata: 6.0.3 + dev: false + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -2603,6 +2626,10 @@ packages: engines: {node: '>=8'} dev: true + /performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + dev: false + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true @@ -2734,6 +2761,12 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + dependencies: + performance-now: 2.1.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true @@ -2809,6 +2842,11 @@ packages: engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true + /rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + dev: false + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -2944,6 +2982,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /stackblur-canvas@2.6.0: + resolution: {integrity: sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==} + engines: {node: '>=0.1.14'} + dev: false + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3035,6 +3078,11 @@ packages: engines: {node: '>= 0.4'} dev: true + /svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + dev: false + /tailwindcss@3.3.3: resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==} engines: {node: '>=14.0.0'} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c6ca00d..0014b03 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { Route, Routes } from "@solidjs/router"; -import type { Component } from "solid-js"; +import { type Component } from "solid-js"; import { Certs } from "./certs"; import { NavRail } from "./components/NavRail"; import { OnlineClassroom } from "./OnlineClassroom"; @@ -8,7 +8,7 @@ const App: Component = () => (
-

En construccion

} /> + } />

En construccion

} /> @@ -17,4 +17,13 @@ const App: Component = () => ( ); +function Builder() { + return ( +
+

En construccion

+

:D

+
+ ); +} + export default App; diff --git a/frontend/src/certs/Registers/RegisterElement.tsx b/frontend/src/certs/Registers/RegisterElement.tsx new file mode 100644 index 0000000..c65dee7 --- /dev/null +++ b/frontend/src/certs/Registers/RegisterElement.tsx @@ -0,0 +1,148 @@ +import { Show } from "solid-js"; +import { certGenerator } from "../../certGenerator"; +import { Person } from "../../types/Person"; +import { Register } from "../../types/Register"; +import { courseMap } from "../../utils/allCourses"; +import { customLabelsMap } from "../../utils/allCustomLabels"; +import { DownloadSimpleIcon } from "../../icons/DownloadSimpleIcon"; +import { CaretRight } from "../../icons/CaretRight"; + +export function RegisterElement(props: {register: Register, person: Person, onClick: () => void}) { + + const dateComponents = () => { + const [, year, month, day] = /(\d{4})-(\d{2})-(\d{2})/.exec(props.register.register_display_date) ?? []; + return {year, month, day}; + }; + + const displayDate = () => { + const {year, month, day} = dateComponents(); + return `${day}/${month}/${year}`; + }; + + const courseName = () => { + const course = courseMap()[props.register.register_course_id]; + return course?.course_name ?? "Curso no encontrado"; + }; + + const customLabel = () => { + const labelId = props.register.register_custom_label; + if (labelId === 1) return ""; + return customLabelsMap()[labelId]?.custom_label_value ?? ""; + }; + + const genCert = () => { + const person = props.person; + const register = props.register; + + generateCert(person, register); + }; + + const generateable = () => { + const courseN = courseName(); + return certGenerator[courseN] !== undefined; + }; + + const createdTodayClasses = () => { + // current dat in YYYY-MM-DD format + const today = new Date().toISOString() + .split("T")[0]; + const createdToday = props.register.register_creation_date === today; + + return createdToday ? "bg-c-surface-variant" : ""; + }; + + return ( +
+
+

+ {courseName()} +

+

+ + {customLabel()} + +   +

+

+ + {displayDate()} + + + + +  sin firmas + + + + +  -  + + { + const code = props.register.register_code.toString(); + const paddedCode = code.padStart(4, "0"); + navigator.clipboard.writeText(paddedCode); + }} + > + {props.register.register_code} + + +

+
+ + +
+ ); +} + +export function generateCert(person: Person, register: Register) { + const courseN = courseMap()[register.register_course_id]?.course_name ?? "Curso no encontrado"; + + const generator = certGenerator[courseN]; + if (generator === undefined) { + alert(`El cert \`${courseN}\` no está implementado`); + return; + } + + const [, year, month, day] = /(\d{4})-(\d{2})-(\d{2})/.exec(register.register_display_date) ?? []; + const personFullName = `${person.person_names} ${person.person_paternal_surname} ${person.person_maternal_surname}`; + + // Manage custom label + const certCustomLabel = register.register_custom_label === 1 + ? "" + : customLabelsMap()[register.register_custom_label]?.custom_label_value ?? ""; + + const certCode = register.register_is_preview ? "0000" : register.register_code.toString().padStart(4, "0"); + + generator(`${courseN} - ${personFullName} [${register.register_id.toString(16).toUpperCase()}].docx`, { + matpel: null, + personFullName, + personDni: person.person_dni.toString(), + certCode, + certYear: year, + certMonth: month, + certDay: day, + certIId: register.register_code, + certCustomLabel, + }); +} + diff --git a/frontend/src/certs/Registers/RegisterSidebar.tsx b/frontend/src/certs/Registers/RegisterSidebar.tsx index 2152ec9..323653c 100644 --- a/frontend/src/certs/Registers/RegisterSidebar.tsx +++ b/frontend/src/certs/Registers/RegisterSidebar.tsx @@ -1,8 +1,18 @@ +import saveAs from "file-saver"; import { Register } from "../../types/Register"; import { courseMap } from "../../utils/allCourses"; -import { formatDate } from "../../utils/functions"; +import { genCarnet } from "../../utils/carnetGenerator"; +import { formatDate, numberToMonth } from "../../utils/functions"; +import { Canvg } from "canvg"; +import { Show } from "solid-js"; +import { Person } from "../../types/Person"; -export function RegisterSidebar(props: {register: Register, close: () => void, onDelete: () => void}) { +export function RegisterSidebar(props: { + register: Register, + person: Person, + close: () => void, + onDelete: () => void, +}) { const deleteRegister = async() => { const res = await fetch(`${import.meta.env.VITE_BACKEND_URL}/api/register/${props.register.register_id}`,{ @@ -17,6 +27,41 @@ export function RegisterSidebar(props: {register: Register, close: () => void, o const courseName = () => courseMap()[props.register.register_course_id]?.course_name ?? ""; + const generateCarnet = () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; + + const p = props.person; + const r = props.register; + + const [,month] = r.register_display_date.split("-"); + + const monthStr = numberToMonth(parseInt(month, 10)); + + if (monthStr === null) { + alert(`El cert. tiene un mes invalido (${month}), no se puede generar carnet`); + return; + } + + const svgHtml = genCarnet( + `${p.person_names} ${p.person_paternal_surname} ${p.person_maternal_surname}`, + p.person_dni.toString(), + r.register_code.toString().padStart(4, "0"), + monthStr, + ); + const v = Canvg.fromString(ctx, svgHtml); + v.start(); + v.ready().then(() => { + canvas.toBlob((blob) => { + if (blob !== null) { + saveAs(blob, "carnet.png"); + } else { + console.log("blob is null... :c"); + } + }); + }); + }; + return (
¿Es cert. sin firmas?

{props.register.register_is_preview ? "Si" : "No"}

+ {/* 12 is the ID for Matpel 3 in the DB */} + + + - -
- ); -} - -function generateCert(person: Person, register: Register) { - const courseN = courseMap()[register.register_course_id]?.course_name ?? "Curso no encontrado"; - - const generator = certGenerator[courseN]; - if (generator === undefined) { - alert(`El cert \`${courseN}\` no está implementado`); - return; - } - - const [, year, month, day] = /(\d{4})-(\d{2})-(\d{2})/.exec(register.register_display_date) ?? []; - const personFullName = `${person.person_names} ${person.person_paternal_surname} ${person.person_maternal_surname}`; - - // Manage custom label - const certCustomLabel = register.register_custom_label === 1 - ? "" - : customLabelsMap()[register.register_custom_label]?.custom_label_value ?? ""; - - const certCode = register.register_is_preview ? "0000" : register.register_code.toString().padStart(4, "0"); - - generator(`${courseN} - ${personFullName} [${register.register_id.toString(16).toUpperCase()}].docx`, { - matpel: null, - personFullName, - personDni: person.person_dni.toString(), - certCode, - certYear: year, - certMonth: month, - certDay: day, - certIId: register.register_code, - certCustomLabel, - }); -} diff --git a/frontend/src/utils/carnetGenerator.ts b/frontend/src/utils/carnetGenerator.ts new file mode 100644 index 0000000..ea24334 --- /dev/null +++ b/frontend/src/utils/carnetGenerator.ts @@ -0,0 +1,78 @@ +export function genCarnet(fullname: string, dni: string, code: string, expiryMonth: string) { + return ` + + + + ${fullname.toUpperCase()} + + + DNI: ${dni} + + + ${code}-EEG-${(new Date()).getFullYear()} + + + ${expiryMonth.toUpperCase()} ${(new Date()).getFullYear() + 1} + + + `; +} diff --git a/frontend/src/utils/functions.ts b/frontend/src/utils/functions.ts index 2d27963..f08f5d7 100644 --- a/frontend/src/utils/functions.ts +++ b/frontend/src/utils/functions.ts @@ -28,3 +28,21 @@ export function useLoading() { return {error, setError, status, setStatus}; } + +export function numberToMonth(n: number): string | null { + switch (n) { + case 1: return "ENERO"; + case 2: return "FEBRERO"; + case 3: return "MARZO"; + case 4: return "ABRIL"; + case 5: return "MAYO"; + case 6: return "JUNIO"; + case 7: return "JULIO"; + case 8: return "AGOSTO"; + case 9: return "SEPTIEMBRE"; + case 10: return "OCTUBRE"; + case 11: return "NOVIEMBRE"; + case 12: return "DICIEMBRE"; + default: return null; + } +}