[Certs] Create person in FE & BE
This commit is contained in:
parent
a10d06dbd8
commit
42bb837233
@ -2,6 +2,7 @@ use reqwest::Client;
|
|||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
|
|
||||||
|
use crate::model::person::PersonCreate;
|
||||||
use crate::model::reniec_person::ReniecPerson;
|
use crate::model::reniec_person::ReniecPerson;
|
||||||
use crate::{db, model::person::Person};
|
use crate::{db, model::person::Person};
|
||||||
|
|
||||||
@ -94,3 +95,20 @@ pub async fn get_by_dni(dni: i32) -> (Status, Json<Person>) {
|
|||||||
// Return error
|
// Return error
|
||||||
(Status::NotFound, Json(Person::default()))
|
(Status::NotFound, Json(Person::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[options("/person")]
|
||||||
|
pub fn options() -> Status {
|
||||||
|
Status::Ok
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/person", format = "json", data = "<person>")]
|
||||||
|
pub async fn create_person(person: Json<PersonCreate>) -> Status {
|
||||||
|
match person.create().await {
|
||||||
|
Ok(_) => Status::Created,
|
||||||
|
Err(reason) => {
|
||||||
|
eprintln!("Error creating person: {:?}", reason);
|
||||||
|
|
||||||
|
Status::InternalServerError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -40,6 +40,8 @@ async fn rocket() -> _ {
|
|||||||
"/api",
|
"/api",
|
||||||
routes![
|
routes![
|
||||||
controller::person::get_by_dni,
|
controller::person::get_by_dni,
|
||||||
|
controller::person::options,
|
||||||
|
controller::person::create_person,
|
||||||
controller::course::get_all,
|
controller::course::get_all,
|
||||||
controller::register::insert_all,
|
controller::register::insert_all,
|
||||||
controller::register::options,
|
controller::register::options,
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
use serde::Serialize;
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
use crate::db;
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
pub struct Person {
|
pub struct Person {
|
||||||
@ -33,3 +35,30 @@ impl Person {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PersonCreate {
|
||||||
|
pub person_dni: String,
|
||||||
|
pub person_names: String,
|
||||||
|
pub person_paternal_surname: String,
|
||||||
|
pub person_maternal_surname: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersonCreate {
|
||||||
|
pub async fn create(&self) -> Result<(), sqlx::Error> {
|
||||||
|
let db = db();
|
||||||
|
|
||||||
|
sqlx::query!("INSERT INTO person (person_dni, person_names, person_paternal_surname, person_maternal_surname) VALUES (?, ?, ?, ?)",
|
||||||
|
self.person_dni,
|
||||||
|
self.person_names,
|
||||||
|
self.person_paternal_surname,
|
||||||
|
self.person_maternal_surname)
|
||||||
|
.execute(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,287 +0,0 @@
|
|||||||
import { Show, createEffect, createSignal } from "solid-js";
|
|
||||||
import { JSX } from "solid-js/jsx-runtime";
|
|
||||||
import QR from "qrcode";
|
|
||||||
import { CopyIcon } from "../icons/CopyIcon";
|
|
||||||
import { XIcon } from "../icons/XIcon";
|
|
||||||
import { Person } from "../types/Person";
|
|
||||||
|
|
||||||
type HTMLButtonEvent = JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Form that retrieves a user from the DB given an ID
|
|
||||||
*/
|
|
||||||
export function Search(props: {setPerson: (p: Person | null) => void}) {
|
|
||||||
const [dni, setDni] = createSignal("");
|
|
||||||
const [loading, setLoading] = createSignal(false);
|
|
||||||
const [error, setError] = createSignal("");
|
|
||||||
const [qrBase64, setQrBase64] = createSignal<string | null>(null);
|
|
||||||
const [person, setPerson] = createSignal<Person | null>(null);
|
|
||||||
|
|
||||||
// Update QR and automatically search when DNI is changed
|
|
||||||
createEffect(() => {
|
|
||||||
const dniT = dni();
|
|
||||||
|
|
||||||
if (dniT.length >= 8) {
|
|
||||||
search();
|
|
||||||
QR.toDataURL(`https://eegsac.com/certificado/${dniT}`, {margin: 1}, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
console.error("Error creating QR code");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setQrBase64(res);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setQrBase64(null);
|
|
||||||
setPerson(null);
|
|
||||||
props.setPerson(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
Get the user data from the DB
|
|
||||||
*/
|
|
||||||
const search = async() => {
|
|
||||||
setLoading(true);
|
|
||||||
setError("");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${import.meta.env.VITE_BACKEND_URL}/api/person/${dni()}`);
|
|
||||||
const body = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setPerson(body);
|
|
||||||
props.setPerson(body);
|
|
||||||
} else if (response.status === 404) {
|
|
||||||
console.error(body);
|
|
||||||
|
|
||||||
setError("No encontrado. Ingresar datos manualmente.");
|
|
||||||
props.setPerson(null);
|
|
||||||
} else {
|
|
||||||
setError(body);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setError(JSON.stringify(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const namesAndSurnames = () => {
|
|
||||||
const p = person();
|
|
||||||
if (p === null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return `${p.person_names} ${p.person_paternal_surname} ${p.person_maternal_surname}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const surnamesAndNames = () => {
|
|
||||||
const p = person();
|
|
||||||
if (p === null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return `${p.person_paternal_surname} ${p.person_maternal_surname} ${p.person_names}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const surnames = () => {
|
|
||||||
const p = person();
|
|
||||||
if (p === null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return `${p.person_paternal_surname} ${p.person_maternal_surname}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const personNames = () => {
|
|
||||||
const p = person();
|
|
||||||
if (p === null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return p.person_names;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="my-4 inline-block w-[10rem] h-[10rem] rounded-lg bg-c-surface-variant overflow-hidden">
|
|
||||||
<img class={`${qrBase64() === null ? "hidden" : ""} inline-block w-[10rem] h-[10rem]`} src={qrBase64() ?? ""} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<form onSubmit={(ev) => ev.preventDefault()} class="px-4">
|
|
||||||
<InputBox dni={dni()} setDni={setDni} loading={loading()} />
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p
|
|
||||||
class="relative max-w-[14rem] mx-auto p-1 text-c-error text-sm select-none"
|
|
||||||
style={{opacity: error() === "" ? "0" : "1"}}
|
|
||||||
>
|
|
||||||
Error: {error()}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
<div class={`${person() === null ? "opacity-50 cursor-not-allowed" : ""}`}>
|
|
||||||
<MaterialLabel text={person()?.person_paternal_surname ?? ""} resource="Apellido Paterno" />
|
|
||||||
<MaterialLabel text={person()?.person_maternal_surname ?? ""} resource="Apellido Materno" />
|
|
||||||
<MaterialLabel text={personNames()} resource="Nombres" />
|
|
||||||
|
|
||||||
<Show when={person() !== null}>
|
|
||||||
<div class={"relative max-w-[14rem] mx-auto my-6"}>
|
|
||||||
<CopyButton copyText={namesAndSurnames()}>
|
|
||||||
<CopyIcon fill="var(--c-on-primary)" />
|
|
||||||
|
|
||||||
Nombres y <b>Apellidos</b>
|
|
||||||
</CopyButton>
|
|
||||||
|
|
||||||
<CopyButton copyText={surnamesAndNames()}>
|
|
||||||
<CopyIcon fill="var(--c-on-primary)" />
|
|
||||||
|
|
||||||
<b>Apellidos</b> y Nombres
|
|
||||||
</CopyButton>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<CopyButton copyText={surnames()}>
|
|
||||||
<CopyIcon fill="var(--c-on-primary)" />
|
|
||||||
|
|
||||||
<b>Apellidos</b>
|
|
||||||
</CopyButton>
|
|
||||||
<CopyButton copyText={personNames()}>
|
|
||||||
<CopyIcon fill="var(--c-on-primary)" />
|
|
||||||
|
|
||||||
Nombres
|
|
||||||
</CopyButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
<Show when={warning() !== ""}>
|
|
||||||
<RegisterPerson dni={dni()} onSuccess={search} />
|
|
||||||
</Show>
|
|
||||||
*/}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputBox(props: {
|
|
||||||
loading: boolean,
|
|
||||||
dni: string,
|
|
||||||
setDni: (v: string) => void,
|
|
||||||
}) {
|
|
||||||
let inputElement: HTMLInputElement | undefined;
|
|
||||||
|
|
||||||
const copyToClipboard: HTMLButtonEvent = (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
if (props.dni.length === 8) {
|
|
||||||
navigator.clipboard.writeText(props.dni);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearDni: HTMLButtonEvent = (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
props.setDni("");
|
|
||||||
(inputElement as HTMLInputElement).focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="relative max-w-[14rem] mx-auto">
|
|
||||||
<input
|
|
||||||
ref={inputElement}
|
|
||||||
id="search-dni"
|
|
||||||
class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1 w-full
|
|
||||||
invalid:border-c-error invalid:text-c-error
|
|
||||||
focus:border-c-primary outline-none font-mono
|
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
type="text"
|
|
||||||
minLength={8}
|
|
||||||
maxLength={8}
|
|
||||||
pattern="[0-9]{8}"
|
|
||||||
placeholder="Número de DNI"
|
|
||||||
value={props.dni}
|
|
||||||
required
|
|
||||||
onInput={(e) => props.setDni(e.target.value)}
|
|
||||||
disabled={props.loading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1 select-none">DNI</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="absolute top-1 right-8 rounded hover:bg-c-surface-variant"
|
|
||||||
onclick={copyToClipboard}
|
|
||||||
>
|
|
||||||
<CopyIcon fill="var(--c-on-surface)" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="absolute top-1 right-1 rounded hover:bg-c-surface-variant"
|
|
||||||
onclick={clearDni}
|
|
||||||
>
|
|
||||||
<XIcon fill="var(--c-on-surface)" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function MaterialLabel(props: {text: string, resource: string}) {
|
|
||||||
const copyToClipboard: HTMLButtonEvent = (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
if (props.text !== "") {
|
|
||||||
navigator.clipboard.writeText(props.text);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="relative max-w-[14rem] mx-auto my-6">
|
|
||||||
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1 select-none">
|
|
||||||
{props.resource}
|
|
||||||
</label>
|
|
||||||
<span
|
|
||||||
class="bg-c-background text-c-on-background border-c-outline
|
|
||||||
border-2 rounded px-2 py-1 w-full inline-block font-mono
|
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{props.text}
|
|
||||||
<span class="select-none"> </span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="absolute top-1 right-1 rounded hover:bg-c-surface-variant"
|
|
||||||
onclick={copyToClipboard}
|
|
||||||
>
|
|
||||||
<CopyIcon fill="var(--c-on-surface)" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CopyButton(props: {copyText: string, children: Array<JSX.Element> | JSX.Element}) {
|
|
||||||
const [successAnimation, setSuccessAnimation] = createSignal(false);
|
|
||||||
|
|
||||||
const onclick = () => {
|
|
||||||
if (props.copyText !== "") {
|
|
||||||
navigator.clipboard.writeText(props.copyText);
|
|
||||||
setSuccessAnimation(true);
|
|
||||||
setTimeout(() => setSuccessAnimation(false), 500);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onclick={onclick}
|
|
||||||
class={
|
|
||||||
`${successAnimation() ? "bg-c-success text-c-on-success" : "bg-c-primary-container text-c-on-primary-container"
|
|
||||||
} rounded-lg transition-colors py-1 my-1 relative overflow-hidden inline-block w-full`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
25
frontend/src/certs/Search/CopyButton.tsx
Normal file
25
frontend/src/certs/Search/CopyButton.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { JSX, createSignal } from "solid-js";
|
||||||
|
|
||||||
|
export function CopyButton(props: {copyText: string, children: Array<JSX.Element> | JSX.Element}) {
|
||||||
|
const [successAnimation, setSuccessAnimation] = createSignal(false);
|
||||||
|
|
||||||
|
const onclick = () => {
|
||||||
|
if (props.copyText !== "") {
|
||||||
|
navigator.clipboard.writeText(props.copyText);
|
||||||
|
setSuccessAnimation(true);
|
||||||
|
setTimeout(() => setSuccessAnimation(false), 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onclick={onclick}
|
||||||
|
class={
|
||||||
|
`${successAnimation() ? "bg-c-success text-c-on-success" : "bg-c-primary-container text-c-on-primary-container"
|
||||||
|
} rounded-lg transition-colors py-1 my-1 relative overflow-hidden inline-block w-full`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
102
frontend/src/certs/Search/PersonDisplay.tsx
Normal file
102
frontend/src/certs/Search/PersonDisplay.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { JSX } from "solid-js";
|
||||||
|
import { CopyIcon } from "../../icons/CopyIcon";
|
||||||
|
import { Person } from "../../types/Person";
|
||||||
|
import { CopyButton } from "./CopyButton";
|
||||||
|
|
||||||
|
type HTMLButtonEvent = JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
|
||||||
|
|
||||||
|
export function PersonDisplay(props: {person: Person}) {
|
||||||
|
const namesAndSurnames = () => {
|
||||||
|
const p = props.person;
|
||||||
|
return `${p.person_names} ${p.person_paternal_surname} ${p.person_maternal_surname}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const surnamesAndNames = () => {
|
||||||
|
const p = props.person;
|
||||||
|
return `${p.person_paternal_surname} ${p.person_maternal_surname} ${p.person_names}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const surnames = () => {
|
||||||
|
const p = props.person;
|
||||||
|
return `${p.person_paternal_surname} ${p.person_maternal_surname}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const personNames = () => {
|
||||||
|
const p = props.person;
|
||||||
|
return p.person_names;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MaterialLabel text={props.person.person_paternal_surname} resource="Apellido Paterno" />
|
||||||
|
<MaterialLabel text={props.person.person_maternal_surname} resource="Apellido Materno" />
|
||||||
|
<MaterialLabel text={personNames()} resource="Nombres" />
|
||||||
|
|
||||||
|
|
||||||
|
<div class={"relative max-w-[14rem] mx-auto my-6"}>
|
||||||
|
<CopyButton copyText={namesAndSurnames()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
Nombres y <b>Apellidos</b>
|
||||||
|
</CopyButton>
|
||||||
|
|
||||||
|
<CopyButton copyText={surnamesAndNames()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
<b>Apellidos</b> y Nombres
|
||||||
|
</CopyButton>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<CopyButton copyText={surnames()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
<b>Apellidos</b>
|
||||||
|
</CopyButton>
|
||||||
|
<CopyButton copyText={personNames()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
Nombres
|
||||||
|
</CopyButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function MaterialLabel(props: {text: string, resource: string}) {
|
||||||
|
const copyToClipboard: HTMLButtonEvent = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (props.text !== "") {
|
||||||
|
navigator.clipboard.writeText(props.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative max-w-[14rem] mx-auto my-6">
|
||||||
|
<label class="absolute -top-2 left-2 text-xs bg-c-surface px-1 select-none">
|
||||||
|
{props.resource}
|
||||||
|
</label>
|
||||||
|
<span
|
||||||
|
class="bg-c-background text-c-on-background border-c-outline
|
||||||
|
border-2 rounded px-2 py-1 w-full inline-block font-mono
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{props.text}
|
||||||
|
<span class="select-none"> </span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1 right-1 rounded hover:bg-c-surface-variant"
|
||||||
|
onclick={copyToClipboard}
|
||||||
|
>
|
||||||
|
<CopyIcon fill="var(--c-on-surface)" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
111
frontend/src/certs/Search/PersonRegister.tsx
Normal file
111
frontend/src/certs/Search/PersonRegister.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
|
export function PersonRegister(props: {dni: string, onRegister: () => void}) {
|
||||||
|
const [paternalSurname, setPaternalSurname] = createSignal("");
|
||||||
|
const [maternalSurname, setMaternalSurname] = createSignal("");
|
||||||
|
const [names, setNames] = createSignal("");
|
||||||
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
|
||||||
|
const register = async() => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const paternal = paternalSurname();
|
||||||
|
const maternal = maternalSurname();
|
||||||
|
const name = names();
|
||||||
|
|
||||||
|
if (paternal === "" || maternal === "" || name === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
person_dni: props.dni,
|
||||||
|
person_names: name.toUpperCase(),
|
||||||
|
person_paternal_surname: paternal.toUpperCase(),
|
||||||
|
person_maternal_surname: maternal.toUpperCase(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_BACKEND_URL}/api/person`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
props.onRegister();
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative max-w-[14rem] mx-auto">
|
||||||
|
<form
|
||||||
|
onSubmit={(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
register();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaterialInput
|
||||||
|
name="Apellido paterno"
|
||||||
|
value={paternalSurname()}
|
||||||
|
setValue={(v) => setPaternalSurname(v.toUpperCase())}
|
||||||
|
loading={loading()}
|
||||||
|
/>
|
||||||
|
<MaterialInput
|
||||||
|
name="Apellido materno"
|
||||||
|
value={maternalSurname()}
|
||||||
|
setValue={(v) => setMaternalSurname(v.toUpperCase())}
|
||||||
|
loading={loading()}
|
||||||
|
/>
|
||||||
|
<MaterialInput
|
||||||
|
name="Nombres"
|
||||||
|
value={names()}
|
||||||
|
setValue={(v) => setNames(v.toUpperCase())}
|
||||||
|
loading={loading()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-full cursor-pointer
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Registrar
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MaterialInput(props: {
|
||||||
|
name: string,
|
||||||
|
loading: boolean,
|
||||||
|
value: string,
|
||||||
|
setValue: (v: string) => void,
|
||||||
|
}) {
|
||||||
|
let inputElement: HTMLInputElement | undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative my-6">
|
||||||
|
<input
|
||||||
|
ref={inputElement}
|
||||||
|
id="search-dni"
|
||||||
|
class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1 w-full
|
||||||
|
invalid:border-c-error invalid:text-c-error
|
||||||
|
focus:border-c-primary outline-none font-mono
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="text"
|
||||||
|
placeholder={props.name}
|
||||||
|
value={props.value}
|
||||||
|
required
|
||||||
|
onInput={(e) => props.setValue(e.target.value)}
|
||||||
|
disabled={props.loading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1 select-none">
|
||||||
|
{props.name}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
173
frontend/src/certs/Search/index.tsx
Normal file
173
frontend/src/certs/Search/index.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { Show, createEffect, createSignal } from "solid-js";
|
||||||
|
import { JSX } from "solid-js/jsx-runtime";
|
||||||
|
import QR from "qrcode";
|
||||||
|
import { CopyIcon } from "../../icons/CopyIcon";
|
||||||
|
import { XIcon } from "../../icons/XIcon";
|
||||||
|
import { Person } from "../../types/Person";
|
||||||
|
import { PersonDisplay } from "./PersonDisplay";
|
||||||
|
import { PersonRegister } from "./PersonRegister";
|
||||||
|
|
||||||
|
type HTMLButtonEvent = JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Form that retrieves a user from the DB given an ID
|
||||||
|
*/
|
||||||
|
export function Search(props: {setPerson: (p: Person | null) => void}) {
|
||||||
|
const [dni, setDni] = createSignal("");
|
||||||
|
const [loading, setLoading] = createSignal(false);
|
||||||
|
const [error, setError] = createSignal("");
|
||||||
|
const [qrBase64, setQrBase64] = createSignal<string | null>(null);
|
||||||
|
const [person, setPerson] = createSignal<Person | null>(null);
|
||||||
|
const [manualCreate, setManualCreate] = createSignal(false);
|
||||||
|
|
||||||
|
// Update QR and automatically search when DNI is changed
|
||||||
|
createEffect(() => {
|
||||||
|
const dniT = dni();
|
||||||
|
|
||||||
|
if (dniT.length >= 8) {
|
||||||
|
search();
|
||||||
|
QR.toDataURL(`https://eegsac.com/certificado/${dniT}`, {margin: 1}, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Error creating QR code");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setQrBase64(res);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setQrBase64(null);
|
||||||
|
setPerson(null);
|
||||||
|
props.setPerson(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get the user data from the DB
|
||||||
|
*/
|
||||||
|
const search = async() => {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_BACKEND_URL}/api/person/${dni()}`);
|
||||||
|
const body = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setPerson(body);
|
||||||
|
props.setPerson(body);
|
||||||
|
setManualCreate(false);
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
console.error(body);
|
||||||
|
|
||||||
|
setError("No encontrado. Ingresar datos manualmente.");
|
||||||
|
setManualCreate(true);
|
||||||
|
props.setPerson(null);
|
||||||
|
} else {
|
||||||
|
setError(body);
|
||||||
|
setManualCreate(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError(JSON.stringify(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="my-4 inline-block w-[10rem] h-[10rem] rounded-lg bg-c-surface-variant overflow-hidden">
|
||||||
|
<img class={`${qrBase64() === null ? "hidden" : ""} inline-block w-[10rem] h-[10rem]`} src={qrBase64() ?? ""} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form onSubmit={(ev) => ev.preventDefault()} class="px-4">
|
||||||
|
<InputBox value={dni()} setValue={setDni} loading={loading()} />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="relative max-w-[14rem] mx-auto p-1 text-c-error text-sm select-none"
|
||||||
|
style={{opacity: error() === "" ? "0" : "1"}}
|
||||||
|
>
|
||||||
|
Error: {error()}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<Show when={person() !== null}>
|
||||||
|
<PersonDisplay person={person()!} />
|
||||||
|
</Show>
|
||||||
|
<Show when={manualCreate()}>
|
||||||
|
<PersonRegister dni={dni()} onRegister={search} />
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function InputBox(props: {
|
||||||
|
loading: boolean,
|
||||||
|
value: string,
|
||||||
|
setValue: (v: string) => void,
|
||||||
|
}) {
|
||||||
|
let inputElement: HTMLInputElement | undefined;
|
||||||
|
|
||||||
|
const copyToClipboard: HTMLButtonEvent = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (props.value.length === 8) {
|
||||||
|
navigator.clipboard.writeText(props.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearDni: HTMLButtonEvent = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
props.setValue("");
|
||||||
|
(inputElement as HTMLInputElement).focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative max-w-[14rem] mx-auto">
|
||||||
|
<input
|
||||||
|
ref={inputElement}
|
||||||
|
id="search-dni"
|
||||||
|
class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1 w-full
|
||||||
|
invalid:border-c-error invalid:text-c-error
|
||||||
|
focus:border-c-primary outline-none font-mono
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="text"
|
||||||
|
minLength={8}
|
||||||
|
maxLength={8}
|
||||||
|
pattern="[0-9]{8}"
|
||||||
|
placeholder="Número de DNI"
|
||||||
|
value={props.value}
|
||||||
|
required
|
||||||
|
onInput={(e) => props.setValue(e.target.value)}
|
||||||
|
disabled={props.loading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1 select-none">DNI</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1 right-8 rounded hover:bg-c-surface-variant"
|
||||||
|
onclick={copyToClipboard}
|
||||||
|
>
|
||||||
|
<CopyIcon fill="var(--c-on-surface)" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1 right-1 rounded hover:bg-c-surface-variant"
|
||||||
|
onclick={clearDni}
|
||||||
|
>
|
||||||
|
<XIcon fill="var(--c-on-surface)" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user