199 lines
6.5 KiB
TypeScript
199 lines
6.5 KiB
TypeScript
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";
|
|
import { backend, wait } from "../../utils/functions";
|
|
import { JsonResult } from "../../types/JsonResult";
|
|
import { LoadingIcon } from "../../icons/LoadingIcon";
|
|
import { AxiosError } from "axios";
|
|
|
|
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);
|
|
|
|
// Change tab title
|
|
createEffect(() => {
|
|
if (person() === null) {
|
|
document.title = "EEGSAC - Sistema interno";
|
|
} else {
|
|
document.title = `${person()!.person_names} ${person()!.person_paternal_surname} ${person()!.person_maternal_surname}`;
|
|
}
|
|
});
|
|
|
|
// 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("");
|
|
setManualCreate(false);
|
|
|
|
if (import.meta.env.DEV) await wait(1500);
|
|
|
|
backend.get<JsonResult<Person>>(`/api/person/${dni()}`)
|
|
.then((response) => {
|
|
if (response.status === 200) {
|
|
setPerson(response.data.Ok);
|
|
props.setPerson(response.data.Ok);
|
|
} else {
|
|
setError(response.data.Error.reason);
|
|
setManualCreate(false);
|
|
}
|
|
})
|
|
.catch((err: AxiosError<JsonResult<Person>>) => {
|
|
if (err.response?.status === 404) {
|
|
console.error(err.response.data);
|
|
|
|
setError("No encontrado. Ingresar datos manualmente.");
|
|
setManualCreate(true);
|
|
props.setPerson(null);
|
|
} else {
|
|
setError(`Error fatal: ${err.message}`);
|
|
console.error(err.response?.status);
|
|
}
|
|
})
|
|
.finally(() => {
|
|
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={15}
|
|
pattern="[0-9]{8,15}"
|
|
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>
|
|
|
|
<span
|
|
class="absolute top-[2px] right-14 rounded hover:bg-c-surface-variant"
|
|
style={{display: props.loading ? "inline-block" : "none"}}
|
|
>
|
|
<LoadingIcon class="animate-spin" fill="var(--c-on-surface)" />
|
|
</span>
|
|
<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>
|
|
);
|
|
}
|
|
|
|
|