Get registers of a user

master
Araozu 2023-05-07 19:01:18 -05:00
parent bcea70c52d
commit 92291b1ec3
18 changed files with 317 additions and 174 deletions

2
frontend-dev/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

View File

@ -19,6 +19,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json",
"ssr:watch": "node esbuild.js",
"ssr:hydration": "node esbuild-client.js",
"ssr": "concurrently \"node esbuild.js\" \"node esbuild-client.js\" \"tailwindcss -i static/tailwind.css -o ./static/styles.css --watch\"",
"tailwind": "tailwindcss -i static/tailwind.css -o ./static/styles.css --watch"
},
"dependencies": {
@ -48,6 +49,7 @@
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"autoprefixer": "^10.4.14",
"concurrently": "^8.0.1",
"esbuild": "^0.17.18",
"esbuild-plugin-solid": "^0.5.0",
"eslint": "^8.0.1",

View File

@ -20,6 +20,7 @@ specifiers:
axios: ^1.4.0
class-transformer: ^0.5.1
class-validator: ^0.14.0
concurrently: ^8.0.1
esbuild: ^0.17.18
esbuild-plugin-solid: ^0.5.0
eslint: ^8.0.1
@ -72,6 +73,7 @@ devDependencies:
'@typescript-eslint/eslint-plugin': 5.59.2_fwbwffxiq4bvaw374ga4sdje4y
'@typescript-eslint/parser': 5.59.2_jgkqkwom7vrxl4kyi454n2sy2i
autoprefixer: 10.4.14_postcss@8.4.23
concurrently: 8.0.1
esbuild: 0.17.18
esbuild-plugin-solid: 0.5.0_rilwjtuydoglaxv66d4vlm6yn4
eslint: 8.40.0
@ -618,6 +620,13 @@ packages:
- supports-color
dev: true
/@babel/runtime/7.21.5:
resolution: {integrity: sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.11
dev: true
/@babel/template/7.20.7:
resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==}
engines: {node: '>=6.9.0'}
@ -2674,6 +2683,22 @@ packages:
readable-stream: 2.3.8
typedarray: 0.0.6
/concurrently/8.0.1:
resolution: {integrity: sha512-Sh8bGQMEL0TAmAm2meAXMjcASHZa7V0xXQVDBLknCPa9TPtkY9yYs+0cnGGgfdkW0SV1Mlg+hVGfXcoI8d3MJA==}
engines: {node: ^14.13.0 || >=16.0.0}
hasBin: true
dependencies:
chalk: 4.1.2
date-fns: 2.30.0
lodash: 4.17.21
rxjs: 7.8.1
shell-quote: 1.8.1
spawn-command: 0.0.2-1
supports-color: 8.1.1
tree-kill: 1.2.2
yargs: 17.7.2
dev: true
/consola/2.15.3:
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
@ -2748,6 +2773,13 @@ packages:
/csstype/3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
/date-fns/2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
dependencies:
'@babel/runtime': 7.21.5
dev: true
/debug/2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
@ -5305,6 +5337,10 @@ packages:
/reflect-metadata/0.1.13:
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
/regenerator-runtime/0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
dev: true
/require-directory/2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@ -5575,6 +5611,10 @@ packages:
engines: {node: '>=8'}
dev: true
/shell-quote/1.8.1:
resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==}
dev: true
/shelljs/0.8.5:
resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==}
engines: {node: '>=4'}
@ -5651,6 +5691,10 @@ packages:
engines: {node: '>= 8'}
dev: true
/spawn-command/0.0.2-1:
resolution: {integrity: sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==}
dev: true
/split2/4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}

View File

@ -1,22 +0,0 @@
import { Test, TestingModule } from "@nestjs/testing";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
describe("AppController", () => {
let appController: AppController;
beforeEach(async() => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe("root", () => {
it("should return \"Hello World!\"", () => {
expect(appController.getHello()).toBe("Hello World!");
});
});
});

View File

@ -1,13 +0,0 @@
import { Controller, Get } from "@nestjs/common";
import { AppService } from "./app.service";
@Controller()
export class AppController {
constructor(private appService: AppService) {
}
@Get()
getHello(): string {
return "Hello!";
}
}

View File

@ -1,9 +1,7 @@
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CertificateController } from "./certificate/certificate.controller";
import { CertificateService } from "./certificate/certificate.service";
import { CertificateController } from "./controller/certificate/certificate.controller";
import { CertificateService } from "./controller/certificate/certificate.service";
import { Persona } from "./model/Persona/persona.entity";
import { CursoGIE } from "./model/CursoGIE/cursoGIE.entity";
import { RegistroGIE } from "./model/RegistroGIE/registroGIE.entity";
@ -27,7 +25,7 @@ import { PersonService } from "./controller/person/person.service";
synchronize: false,
}),
],
controllers: [AppController, CertificateController, PersonController],
providers: [AppService, CertificateService, PersonService],
controllers: [CertificateController, PersonController],
providers: [CertificateService, PersonService],
})
export class AppModule {}

View File

@ -1,10 +0,0 @@
import { Injectable } from "@nestjs/common";
@Injectable()
export class AppService {
getHello(): string {
const html = ":D"; // renderToString(SolidAppJSX);
console.log(`SSR? ${html}`);
return `Hello world! ${html}`;
}
}

View File

@ -1,22 +0,0 @@
import { Controller, Get, Param } from "@nestjs/common";
import { renderToString } from "solid-js/web";
import { Certs } from "../views/Certs";
import { template } from "./certificate.template";
import { CertificateService } from "./certificate.service";
@Controller("certificate")
export class CertificateController {
constructor(private certificateService: CertificateService) {
}
@Get()
entry(): string {
const html = renderToString(Certs);
return template(html);
}
@Get(":id")
getById(@Param() param: {id: string}) {
return this.certificateService.getByDni(param.id);
}
}

View File

@ -1,81 +0,0 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { Person } from "../types/Person";
import { DataSource, Repository } from "typeorm";
import { Persona } from "../model/Persona/persona.entity";
import { RegistroGIE } from "../model/RegistroGIE/registroGIE.entity";
import axios from "axios";
interface SunatPerson {
apellidoPaterno: string,
apellidoMaterno: string,
nombres: string,
}
@Injectable()
export class CertificateService {
personaRepository: Repository<Persona>;
registroGIERepository;
constructor(private dataSource: DataSource) {
this.personaRepository = dataSource.getRepository(Persona);
this.registroGIERepository = dataSource.getRepository(RegistroGIE);
}
async getByDni(dni: string): Promise<Person> {
// Search person in new tables
const person = await this.personaRepository.findOneBy({
dni,
});
if (person !== null) {
return {
id: person.id,
apellidoMaterno: person.apellidoMaterno,
apellidoPaterno: person.apellidoPaterno,
nombres: person.nombres,
nombreCompleto: `${person.nombres} ${person.apellidoPaterno} ${person.apellidoMaterno}`,
};
}
// TODO: Move to env variables
const token = "apis-token-1.aTSI1U7KEuT-6bbbCguH-4Y8TI6KS73N";
// Search person in SUNAT API
let personSunat: SunatPerson | null = null;
try {
const personSunatR = await axios.get<SunatPerson>(`https://api.apis.net.pe/v1/dni?numero=${dni}`, {
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
"charset": "utf-8",
},
});
personSunat = personSunatR.data;
} catch (e) { /* empty */
}
// If person exists in SUNAT, add to DB and return
if (personSunat !== null) {
const p = new Persona();
p.dni = dni;
p.apellidoPaterno = personSunat.apellidoPaterno;
p.apellidoMaterno = personSunat.apellidoMaterno;
p.nombres = personSunat.nombres;
await this.personaRepository.save(p);
return {
id: p.id,
apellidoPaterno: p.apellidoPaterno,
apellidoMaterno: p.apellidoMaterno,
nombres: p.nombres,
nombreCompleto: `${p.nombres} ${p.apellidoPaterno} ${p.apellidoMaterno}`,
};
}
// 404
throw new NotFoundException("Persona no encontrada", {
description: "La persona no se encontró ni en BD ni en API",
});
}
}

View File

@ -0,0 +1,32 @@
import { Controller, Delete, Get, InternalServerErrorException, Param } from "@nestjs/common";
import { renderToString } from "solid-js/web";
import { Certs } from "../../views/Certs";
import { template } from "./certificate.template";
import { CertificateService } from "./certificate.service";
@Controller("certificate")
export class CertificateController {
constructor(private certificateService: CertificateService) {
}
@Get()
entry(): string {
const html = renderToString(Certs);
return template(html);
}
@Get(":dni")
getByDni(@Param() param: {dni: string}) {
return this.certificateService.getByDni(param.dni);
}
@Delete(":id")
async delete(@Param() param: {id: number}) {
console.log(param);
const result = await this.certificateService.deleteById(param.id);
if (!result) {
throw new InternalServerErrorException();
}
}
}

View File

@ -0,0 +1,34 @@
import { Injectable } from "@nestjs/common";
import { DataSource, Repository } from "typeorm";
import { RegistroGIE } from "../../model/RegistroGIE/registroGIE.entity";
@Injectable()
export class CertificateService {
registroGIERepository: Repository<RegistroGIE>;
constructor(private dataSource: DataSource) {
this.registroGIERepository = dataSource.getRepository(RegistroGIE);
}
async getByDni(dni: string) {
return await this.registroGIERepository.findBy({
dni,
});
}
async deleteById(id: number) {
// TODO: Should do logical deletion
const certificate = await this.registroGIERepository.findOneBy({
id,
});
console.log(certificate);
if (certificate !== null) {
await this.registroGIERepository.delete(certificate);
return true;
} else {
return false;
}
}
}

View File

@ -1,10 +1,24 @@
import { Body, Controller, InternalServerErrorException, Post, ValidationPipe } from "@nestjs/common";
import {
Body,
Controller,
Delete,
Get,
InternalServerErrorException,
Param,
Post,
ValidationPipe,
} from "@nestjs/common";
import { PersonDto } from "./person.dto";
import { PersonService } from "./person.service";
import { CertificateService } from "../certificate/certificate.service";
@Controller("person")
export class PersonController {
constructor(private personService: PersonService) {
constructor(private personService: PersonService,) {}
@Get(":dni")
getById(@Param() param: {dni: string}) {
return this.personService.getByDni(param.dni);
}
@Post()

View File

@ -1,7 +1,15 @@
import { Injectable, InternalServerErrorException } from "@nestjs/common";
import { Injectable, InternalServerErrorException, NotFoundException } from "@nestjs/common";
import { PersonDto } from "./person.dto";
import { DataSource, Repository } from "typeorm";
import { Persona } from "../../model/Persona/persona.entity";
import { Person } from "../../types/Person";
import axios from "axios";
interface SunatPerson {
apellidoPaterno: string,
apellidoMaterno: string,
nombres: string,
}
@Injectable()
export class PersonService {
@ -25,4 +33,64 @@ export class PersonService {
return false;
}
}
async getByDni(dni: string): Promise<Person> {
// Search person in new tables
const person = await this.personaRepository.findOneBy({
dni,
});
if (person !== null) {
return {
id: person.id,
apellidoMaterno: person.apellidoMaterno,
apellidoPaterno: person.apellidoPaterno,
nombres: person.nombres,
nombreCompleto: `${person.nombres} ${person.apellidoPaterno} ${person.apellidoMaterno}`,
dni,
};
}
// TODO: Move to env variables
const token = "apis-token-1.aTSI1U7KEuT-6bbbCguH-4Y8TI6KS73N";
// Search person in SUNAT API
let personSunat: SunatPerson | null = null;
try {
const personSunatR = await axios.get<SunatPerson>(`https://api.apis.net.pe/v1/dni?numero=${dni}`, {
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
"charset": "utf-8",
},
});
personSunat = personSunatR.data;
} catch (e) { /* empty */
}
// If person exists in SUNAT, add to DB and return
if (personSunat !== null) {
const p = new Persona();
p.dni = dni;
p.apellidoPaterno = personSunat.apellidoPaterno;
p.apellidoMaterno = personSunat.apellidoMaterno;
p.nombres = personSunat.nombres;
await this.personaRepository.save(p);
return {
id: p.id,
apellidoPaterno: p.apellidoPaterno,
apellidoMaterno: p.apellidoMaterno,
nombres: p.nombres,
nombreCompleto: `${p.nombres} ${p.apellidoPaterno} ${p.apellidoMaterno}`,
dni,
};
}
// 404
throw new NotFoundException("Persona no encontrada", {
description: "La persona no se encontró ni en BD ni en API",
});
}
}

View File

@ -1,9 +1,7 @@
import { NestFactory } from "@nestjs/core";
import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify";
import { AppModule } from "./app.module";
import * as express from "express";
import * as path from "path";
import { ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);

View File

@ -8,4 +8,5 @@ export interface Person {
apellidoMaterno: string,
/** Names of the person. May be more than 1 word. */
nombres: string,
dni: string,
}

View File

@ -1,20 +1,35 @@
import { Show, createEffect, createSignal } from "solid-js";
import { Show, createEffect, createSignal, For } from "solid-js";
import { Person } from "../../types/Person";
import type { RegistroGIE } from "../../model/RegistroGIE/registroGIE.entity";
export function Registers(props: { person: Person | null }) {
const [loading, setLoading] = createSignal(false);
const [registers, setRegisters] = createSignal<Array<RegistroGIE>>([]);
const x: Array<RegistroGIE> = JSON.parse("[{\"id\":12086,\"dni\":\"47269725\",\"nombre\":\"PEÑAFIEL ROJAS NATHALY\",\"curso\":10,\"codigo\":5619,\"fecha_actual\":\"2023-05-06\",\"fecha_inscripcion\":\"2023-05-04\",\"curso_nombre\":\"Matpel I\"},{\"id\":12087,\"dni\":\"47269725\",\"nombre\":\"PEÑAFIEL ROJAS NATHALY\",\"curso\":11,\"codigo\":5620,\"fecha_actual\":\"2023-05-06\",\"fecha_inscripcion\":\"2023-05-05\",\"curso_nombre\":\"Matpel II\"},{\"id\":12088,\"dni\":\"47269725\",\"nombre\":\"PEÑAFIEL ROJAS NATHALY\",\"curso\":1,\"codigo\":1620,\"fecha_actual\":\"2023-05-06\",\"fecha_inscripcion\":\"2023-05-06\",\"curso_nombre\":\"Manejo Defensivo\"}]");
createEffect(() => {
const person = props.person;
if (person !== null) {
// Load certificates from DB
loadCertificates();
}
});
const loadCertificates = () => {
const loadCertificates = async() => {
console.log("loading from db...");
setLoading(true);
if (props.person === null) return;
const response = await fetch(`/certificate/${props.person.dni}`);
if (response.ok) {
const data: Array<RegistroGIE> = await response.json();
setRegisters(data);
} else {
setRegisters([]);
}
setLoading(false);
};
return (
@ -23,6 +38,8 @@ export function Registers(props: {person: Person | null}) {
<div class="px-4">
<Show when={props.person !== null}>
<span>Nombres y Apellidos</span>
<br/>
<input
type="text"
disabled
@ -36,6 +53,27 @@ export function Registers(props: {person: Person | null}) {
>
Recuperando registros...
</p>
<Show when={!loading()}>
<table class="table-auto border border-c-outline my-4">
<thead>
<tr>
<th class="p-2">APELLIDOS Y NOMBRES</th>
<th class="p-2">CURSO</th>
<th class="p-2">FECHA</th>
<th class="p-2">CÓDIGO</th>
<th class="p-2"></th>
<th class="p-2"></th>
</tr>
</thead>
<tbody>
<For each={registers()}>
{(register) => <Register cert={register} onUpdate={loadCertificates} />}
</For>
</tbody>
</table>
</Show>
</Show>
@ -43,3 +81,63 @@ export function Registers(props: {person: Person | null}) {
</div>
);
}
function Register(props: {cert: RegistroGIE, onUpdate: () => void}) {
const [deleteConfirmation, setDeleteConfirmation] = createSignal(false);
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}`,{
method: "DELETE",
});
if (response.ok) {
props.onUpdate();
}
} else {
// Show delete confirmation
setDeleteText("Estas seguro?");
setDeleteConfirmation(true);
// Exit confirmation after 3 seconds
setTimeout(() => {
setDeleteText("Eliminar");
setDeleteConfirmation(false);
}, 3000);
}
};
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
bg-c-primary
text-c-on-primary
disabled:opacity-50 disabled:cursor-not-allowed"
disabled
>
Editar
</button>
</td>
<td class="py-2 pl-4 pr-8">
<button
class={`rounded-full py-1 px-2 shadow
${deleteConfirmation() ? "bg-c-error-container text-c-on-error-container" : "bg-c-error text-c-on-error"}
cursor-pointer`}
onclick={deleteRegister}
>
{deleteText()}
</button>
</td>
</tr>
);
}

View File

@ -15,7 +15,7 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
const [dni, setDni] = createSignal("");
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal("");
const [advertencia, setAdvertencia] = createSignal("");
const [warning, setWarning] = createSignal("");
/*
Get the user data from the DB
@ -28,17 +28,17 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
const search = async() => {
setLoading(true);
setError("");
setAdvertencia("");
setWarning("");
try {
const response = await fetch(`/certificate/${dni()}`);
const response = await fetch(`/person/${dni()}`);
const body = await response.json();
if (response.ok) {
props.setPerson(body);
} else if (response.status === 404) {
console.error(body);
setAdvertencia("Persona no encontrada. Se debe insertar manualmente sus datos.");
setWarning("Persona no encontrada. Se debe insertar manualmente sus datos.");
} else {
setError(body);
}
@ -93,14 +93,14 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
<p
class="my-2 mx-4 p-1 rounded border border-c-error text-c-error font-medium w-fit"
style={{display: advertencia() === "" ? "none" : "block"}}
style={{display: warning() === "" ? "none" : "block"}}
>
Advertencia:
<br />
{advertencia()}
{warning()}
</p>
<Show when={advertencia() !== ""}>
<Show when={warning() !== ""}>
<RegisterPerson dni={dni()} onSuccess={search} />
</Show>
</div>