[Certs] Migrate UI from failed Electron rewrite
This commit is contained in:
parent
1eccc62056
commit
d70d1edbd6
@ -12,19 +12,13 @@ export function Certs() {
|
|||||||
onMount(ensureColors);
|
onMount(ensureColors);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="grid grid-cols-[18rem_25rem_1fr]">
|
||||||
<h1 class="px-4 py-2 text-2xl font-bold">
|
|
||||||
Registrar certificado
|
|
||||||
</h1>
|
|
||||||
<Search setPerson={setPerson}/>
|
<Search setPerson={setPerson}/>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<Registers person={person()} lastUpdate={lastUpdate()} />
|
|
||||||
<NewRegister
|
<NewRegister
|
||||||
personId={person()?.id ?? -1}
|
personId={person()?.id ?? null}
|
||||||
onSuccess={() => setLastUpdate((x) => x + 1)}
|
onSuccess={() => setLastUpdate((x) => x + 1)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Registers person={person()} lastUpdate={lastUpdate()} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
9
src/views/components/FilledCard.tsx
Normal file
9
src/views/components/FilledCard.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
export function FilledCard(props: {children?: Array<JSX.Element> | JSX.Element, class?: string}) {
|
||||||
|
return (
|
||||||
|
<div class={`bg-c-surface-variant text-c-on-surface-variant rounded-xl m-2 shadow ${props.class ?? ""}`}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -2,6 +2,8 @@ import { createSignal, Show } from "solid-js";
|
|||||||
import { SearchableSelect } from "./NewRegister/SearchableSelect";
|
import { SearchableSelect } from "./NewRegister/SearchableSelect";
|
||||||
import { JSX } from "solid-js/jsx-runtime";
|
import { JSX } from "solid-js/jsx-runtime";
|
||||||
import { subjects } from "../subjects";
|
import { subjects } from "../subjects";
|
||||||
|
import { FilledCard } from "./FilledCard";
|
||||||
|
import { RegisterPreview } from "./NewRegister/RegisterPreview";
|
||||||
|
|
||||||
|
|
||||||
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
|
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
|
||||||
@ -34,12 +36,48 @@ type RegisterFn = (personId: number, subjectId: number, date: string) => Promise
|
|||||||
|
|
||||||
const defaultLabelText = "3. Crear nuevos registros";
|
const defaultLabelText = "3. Crear nuevos registros";
|
||||||
|
|
||||||
|
type TabType = "Presets" | "Manual";
|
||||||
|
|
||||||
export function NewRegister(props: {
|
export function NewRegister(props: {
|
||||||
personId: number | null,
|
personId: number | null,
|
||||||
onSuccess: () => void,
|
onSuccess: () => void,
|
||||||
registerFn?: RegisterFn,
|
registerFn?: RegisterFn,
|
||||||
labelText?: string,
|
labelText?: string,
|
||||||
}) {
|
}) {
|
||||||
|
const [active, setActive] = createSignal<TabType>("Manual");
|
||||||
|
const [selections, setSelections] = createSignal<Array<[number, string]>>([]);
|
||||||
|
|
||||||
|
const onRegister = () => {
|
||||||
|
setSelections([]);
|
||||||
|
props.onSuccess();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="h-screen overflow-y-scroll">
|
||||||
|
<FilledCard class="border border-c-outline overflow-hidden">
|
||||||
|
<h2 class="p-4 font-bold text-xl">{props.labelText ?? defaultLabelText}</h2>
|
||||||
|
|
||||||
|
<RegisterTabs active={active()} setActive={setActive} />
|
||||||
|
|
||||||
|
<div class="bg-c-surface p-4 h-[22rem]">
|
||||||
|
<Show when={active() === "Presets"}>
|
||||||
|
<p>Proximamente...</p>
|
||||||
|
</Show>
|
||||||
|
<Show when={active() === "Manual"}>
|
||||||
|
<ManualCerts personId={props.personId} onAdd={(v) => setSelections((x) => [...x, v])} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</FilledCard>
|
||||||
|
|
||||||
|
<RegisterPreview
|
||||||
|
selections={selections()}
|
||||||
|
personId={props.personId}
|
||||||
|
onDelete={(deleteId) => setSelections((s) => [...s.filter(([id]) => id !== deleteId)])}
|
||||||
|
onRegister={onRegister}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const [error, setError] = createSignal("");
|
const [error, setError] = createSignal("");
|
||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
// Used to update SearchableSelect.tsx manually
|
// Used to update SearchableSelect.tsx manually
|
||||||
@ -138,3 +176,104 @@ export function NewRegister(props: {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function RegisterTabs(props: {active: TabType, setActive: (v: TabType) => void}) {
|
||||||
|
const presetsClasses = () => ((props.active === "Presets") ? "font-bold border-c-primary" : "border-c-transparent");
|
||||||
|
const manualClasses = () => ((props.active === "Manual") ? "font-bold border-c-primary" : "border-c-transparent");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="grid grid-cols-2">
|
||||||
|
<button
|
||||||
|
class={`py-2 border-b-4 ${presetsClasses()}`}
|
||||||
|
onclick={() => props.setActive("Presets")}
|
||||||
|
>
|
||||||
|
Presets
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class={`py-2 border-b-4 ${manualClasses()}`}
|
||||||
|
onclick={() => props.setActive("Manual")}
|
||||||
|
>
|
||||||
|
Manual
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManualCerts(props: {personId: number | null, onAdd: (v: [number, string]) => void}) {
|
||||||
|
// Used to update SearchableSelect.tsx manually
|
||||||
|
const [count, setCount] = createSignal(0);
|
||||||
|
const [error, setError] = createSignal("");
|
||||||
|
|
||||||
|
const [selectedSubject, setSelectedSubject] = createSignal<number | null>(null);
|
||||||
|
|
||||||
|
const datePicker = (
|
||||||
|
<input
|
||||||
|
id="create-date"
|
||||||
|
class="bg-c-surface text-c-on-surface border border-c-outline rounded-lg p-2"
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const register: HTMLEventFn = async(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
const subject = selectedSubject();
|
||||||
|
const date = (datePicker as HTMLInputElement).value;
|
||||||
|
|
||||||
|
if (subject === null) {
|
||||||
|
setError("Selecciona un curso");
|
||||||
|
|
||||||
|
setTimeout(() => setError(""), 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (date === "") {
|
||||||
|
setError("Selecciona una fecha");
|
||||||
|
|
||||||
|
setTimeout(() => setError(""), 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onAdd([subject, date]);
|
||||||
|
// This is used to update & refresh the <SearchableSelect> component
|
||||||
|
setCount((x) => x + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Person ID: ${props.personId}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form onsubmit={register}>
|
||||||
|
<div>
|
||||||
|
<SearchableSelect
|
||||||
|
subjects={subjects()}
|
||||||
|
onChange={setSelectedSubject}
|
||||||
|
count={count()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative my-4">
|
||||||
|
{datePicker}
|
||||||
|
<label for="create-date" class="absolute -top-2 left-2 text-xs bg-c-surface px-1">Fecha</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-full cursor-pointer
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="submit"
|
||||||
|
value="Agregar"
|
||||||
|
disabled={props.personId === null}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="my-2 p-1 rounded w-fit mx-4 bg-c-error text-c-on-error"
|
||||||
|
style={{opacity: error() === "" ? "0" : "1", "user-select": "none"}}
|
||||||
|
>
|
||||||
|
{error()}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
95
src/views/components/NewRegister/RegisterPreview.tsx
Normal file
95
src/views/components/NewRegister/RegisterPreview.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
|
||||||
|
import { FilledCard } from "../FilledCard";
|
||||||
|
import { For } from "solid-js";
|
||||||
|
import { XIcon } from "src/views/icons/XIcon";
|
||||||
|
import { subjects } from "src/views/subjects";
|
||||||
|
|
||||||
|
function isoDateToLocalDate(date: string): string {
|
||||||
|
const [,month, day] = /\d{4}-(\d{2})-(\d{2})/.exec(date) ?? "";
|
||||||
|
return `${day}/${month}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegisterPreview(props: {selections: Array<[number, string]>, personId: number | null, onDelete: (v: number) => void, onRegister: () => void}) {
|
||||||
|
const submit = async() => {
|
||||||
|
console.log("Submit...");
|
||||||
|
|
||||||
|
for (const [courseId, date] of props.selections) {
|
||||||
|
const result = await defaultNewRegisterFn(
|
||||||
|
props.personId ?? -1,
|
||||||
|
courseId,
|
||||||
|
date,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
console.log("Success");
|
||||||
|
} else {
|
||||||
|
console.log(`error. ${result}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onRegister();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilledCard class="border border-c-outline overflow-hidden">
|
||||||
|
<h2 class="p-4 font-bold text-xl">Confirmar registro</h2>
|
||||||
|
<div class="bg-c-surface p-4">
|
||||||
|
<For each={props.selections}>
|
||||||
|
{([courseId, date]) => <Register courseId={courseId} date={date} onDelete={props.onDelete} />}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-full cursor-pointer mt-4
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="button"
|
||||||
|
disabled={props.selections.length === 0}
|
||||||
|
onclick={submit}
|
||||||
|
>
|
||||||
|
Registrar los {props.selections.length} cursos
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</FilledCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Register(props: {courseId: number, date: string, onDelete: (v: number) => void}) {
|
||||||
|
const courseName = () => {
|
||||||
|
const courses = subjects();
|
||||||
|
return courses.find((c) => c.id === props.courseId)?.nombre ?? "!";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="grid grid-cols-[auto_4rem_1.5rem] py-1 px-2 rounded-md border border-c-outline my-1">
|
||||||
|
{courseName()}
|
||||||
|
<span class="font-mono">{isoDateToLocalDate(props.date)}</span>
|
||||||
|
<button
|
||||||
|
class="hover:bg-c-surface-variant rounded-md"
|
||||||
|
onclick={() => props.onDelete(props.courseId)}
|
||||||
|
>
|
||||||
|
<XIcon fill="var(--c-on-surface)" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function defaultNewRegisterFn(personId: number, subjectId: number, date: string): Promise<null | string> {
|
||||||
|
const response = await fetch("/certificate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
personId,
|
||||||
|
subjectId,
|
||||||
|
date,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
}
|
@ -85,7 +85,7 @@ export function SearchableSelect(props: {
|
|||||||
{inputElement}
|
{inputElement}
|
||||||
<br/>
|
<br/>
|
||||||
<div
|
<div
|
||||||
class="border-c-outline border-2 rounded overflow-y-scroll h-[18rem]"
|
class="border-c-outline border-2 rounded overflow-y-scroll h-[10rem]"
|
||||||
>
|
>
|
||||||
<For each={filteredOptions()}>
|
<For each={filteredOptions()}>
|
||||||
{(s) => (
|
{(s) => (
|
||||||
|
@ -37,55 +37,8 @@ export function Registers(props: { person: Person | null, lastUpdate: number })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h2 class="mb-4 font-bold text-xl">2. Revisar registros actuales</h2>
|
|
||||||
|
|
||||||
<div class="px-4">
|
<div class="px-4">
|
||||||
<Show when={props.person !== null}>
|
<Show when={props.person !== null}>
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
disabled
|
|
||||||
value={props.person?.nombreCompleto}
|
|
||||||
class="bg-c-background text-c-on-background border-c-outline border rounded px-2 py-1
|
|
||||||
disabled:cursor-text w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-[3fr_3fr_2fr_2fr_1fr_1fr] gap-1">
|
|
||||||
<CopyButton
|
|
||||||
copyText={`${props.person!.nombres} ${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno}`}
|
|
||||||
>
|
|
||||||
NOM <b>AP</b>
|
|
||||||
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i>
|
|
||||||
</CopyButton>
|
|
||||||
<CopyButton
|
|
||||||
copyText={`${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno} ${props.person!.nombres}`}
|
|
||||||
>
|
|
||||||
<b>AP</b> NOM
|
|
||||||
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i>
|
|
||||||
</CopyButton>
|
|
||||||
<CopyButton
|
|
||||||
copyText={`${props.person!.nombres}`}
|
|
||||||
>
|
|
||||||
NOM
|
|
||||||
</CopyButton>
|
|
||||||
<CopyButton
|
|
||||||
copyText={`${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno}`}
|
|
||||||
>
|
|
||||||
<b>AP</b>
|
|
||||||
</CopyButton>
|
|
||||||
<CopyButton
|
|
||||||
copyText={`${props.person!.apellidoPaterno}`}
|
|
||||||
>
|
|
||||||
<b><i>Ap</i></b>
|
|
||||||
</CopyButton>
|
|
||||||
<CopyButton
|
|
||||||
copyText={`${props.person!.apellidoMaterno}`}
|
|
||||||
>
|
|
||||||
<b><i>Am</i></b>
|
|
||||||
</CopyButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="my-2"
|
class="my-2"
|
||||||
style={{ display: loading() ? "block" : "none" }}
|
style={{ display: loading() ? "block" : "none" }}
|
||||||
|
@ -3,6 +3,9 @@ import { JSX } from "solid-js/jsx-runtime";
|
|||||||
import { Person } from "../../types/Person";
|
import { Person } from "../../types/Person";
|
||||||
import { RegisterPerson } from "./Search/RegisterPerson";
|
import { RegisterPerson } from "./Search/RegisterPerson";
|
||||||
import QR from "qrcode";
|
import QR from "qrcode";
|
||||||
|
import { CopyIcon } from "../icons/CopyIcon";
|
||||||
|
import { MagnifyingGlassIcon } from "../icons/MagnifyingGlassIcon";
|
||||||
|
import { XIcon } from "../icons/XIcon";
|
||||||
|
|
||||||
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
|
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
|
||||||
submitter: HTMLElement;
|
submitter: HTMLElement;
|
||||||
@ -19,6 +22,7 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
|
|||||||
const [error, setError] = createSignal("");
|
const [error, setError] = createSignal("");
|
||||||
const [warning, setWarning] = createSignal("");
|
const [warning, setWarning] = createSignal("");
|
||||||
const [qrBase64, setQrBase64] = createSignal<string | null>(null);
|
const [qrBase64, setQrBase64] = createSignal<string | null>(null);
|
||||||
|
const [persona, setPersona] = createSignal<Person | null>(null);
|
||||||
|
|
||||||
// Update QR
|
// Update QR
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@ -53,6 +57,7 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
|
|||||||
const response = await fetch(`/person/${dni()}`);
|
const response = await fetch(`/person/${dni()}`);
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
setPersona(body);
|
||||||
props.setPerson(body);
|
props.setPerson(body);
|
||||||
} else if (response.status === 404) {
|
} else if (response.status === 404) {
|
||||||
console.error(body);
|
console.error(body);
|
||||||
@ -69,23 +74,48 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const nombresYApellidos = () => {
|
||||||
<div class="p-4 grid" style={{"grid-template-columns": "20rem auto"}}>
|
const p = persona();
|
||||||
<div>
|
if (p === null) {
|
||||||
<h2 class="my-2 font-bold text-xl">1. Buscar persona</h2>
|
return "";
|
||||||
<form onSubmit={searchDNI} class="px-4">
|
}
|
||||||
<label for="search-dni">DNI</label>
|
return `${p.nombres} ${p.apellidoPaterno} ${p.apellidoMaterno}`;
|
||||||
<br />
|
};
|
||||||
<InputBox dni={dni()} setDni={setDni} loading={loading()} />
|
|
||||||
|
|
||||||
<br />
|
const apellidosYNombres = () => {
|
||||||
<input
|
const p = persona();
|
||||||
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-md cursor-pointer
|
if (p === null) {
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
return "";
|
||||||
type="submit"
|
}
|
||||||
value="Buscar"
|
return `${p.apellidoPaterno} ${p.apellidoMaterno} ${p.nombres}`;
|
||||||
disabled={loading()}
|
};
|
||||||
/>
|
|
||||||
|
const apellidos = () => {
|
||||||
|
const p = persona();
|
||||||
|
if (p === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `${p.apellidoPaterno} ${p.apellidoMaterno}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nombres = () => {
|
||||||
|
const p = persona();
|
||||||
|
if (p === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return p.nombres;
|
||||||
|
};
|
||||||
|
|
||||||
|
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={searchDNI} class="px-4">
|
||||||
|
<InputBox dni={dni()} setDni={setDni} loading={loading()} />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
@ -106,13 +136,45 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
|
|||||||
{warning()}
|
{warning()}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class={`${persona() === null ? "opacity-50 cursor-not-allowed" : ""}`}>
|
||||||
|
<MaterialLabel text={persona()?.apellidoPaterno ?? null} resource="Apellido Paterno" />
|
||||||
|
<MaterialLabel text={persona()?.apellidoMaterno ?? null} resource="Apellido Materno" />
|
||||||
|
<MaterialLabel text={persona()?.nombres ?? null} resource="Nombres" />
|
||||||
|
|
||||||
|
<div class={"relative max-w-[14rem] mx-auto my-6"}>
|
||||||
|
<CopyButton copyText={nombresYApellidos()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
Nombres y <b>Apellidos</b>
|
||||||
|
</CopyButton>
|
||||||
|
|
||||||
|
<CopyButton copyText={apellidosYNombres()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
<b>Apellidos</b> y Nombres
|
||||||
|
</CopyButton>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<CopyButton copyText={apellidos()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
<b>Apellidos</b>
|
||||||
|
</CopyButton>
|
||||||
|
<CopyButton copyText={nombres()}>
|
||||||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||||||
|
|
||||||
|
Nombres
|
||||||
|
</CopyButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Show when={warning() !== ""}>
|
<Show when={warning() !== ""}>
|
||||||
<RegisterPerson dni={dni()} onSuccess={search} />
|
<RegisterPerson dni={dni()} onSuccess={search} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<img src={qrBase64() ?? ""} height="150" width="150" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -122,12 +184,10 @@ function InputBox(props: {
|
|||||||
dni: string,
|
dni: string,
|
||||||
setDni: (v: string) => void,
|
setDni: (v: string) => void,
|
||||||
}) {
|
}) {
|
||||||
const [successAnimation, setSuccessAnimation] = createSignal(false);
|
|
||||||
|
|
||||||
const inputElement = (
|
const inputElement = (
|
||||||
<input
|
<input
|
||||||
id="search-dni"
|
id="search-dni"
|
||||||
class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1
|
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
|
invalid:border-c-error invalid:text-c-error
|
||||||
focus:border-c-primary outline-none font-mono
|
focus:border-c-primary outline-none font-mono
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
@ -148,8 +208,6 @@ function InputBox(props: {
|
|||||||
|
|
||||||
if (props.dni.length === 8) {
|
if (props.dni.length === 8) {
|
||||||
navigator.clipboard.writeText(props.dni);
|
navigator.clipboard.writeText(props.dni);
|
||||||
setSuccessAnimation(true);
|
|
||||||
setTimeout(() => setSuccessAnimation(false), 1000);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -161,18 +219,86 @@ function InputBox(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="grid gap-2 grid-cols-[10rem_3rem_3rem]">
|
<div class="relative max-w-[14rem] mx-auto">
|
||||||
{inputElement}
|
{inputElement}
|
||||||
|
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1">DNI</label>
|
||||||
<button
|
<button
|
||||||
class={`${successAnimation() ? "bg-c-success" : "bg-c-primary"} rounded transition-colors`}
|
class="absolute top-1 right-[3.75rem] rounded hover:bg-c-surface-variant"
|
||||||
onclick={copyToClipboard}
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<i class={`${successAnimation() ? "text-c-on-success" : "text-c-on-primary"} ph ph-clipboard-text text-2xl align-middle`}></i>
|
<MagnifyingGlassIcon fill="var(--c-on-surface)" />
|
||||||
</button>
|
</button>
|
||||||
<button class="bg-c-error rounded" onclick={clearDni} type="button">
|
<button
|
||||||
<i class="ph ph-trash text-2xl text-c-on-primary align-middle"></i>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function MaterialLabel(props: {text: string | null, resource: string}) {
|
||||||
|
const copyToClipboard: HTMLButtonEvent = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (props.text !== null) {
|
||||||
|
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 text-c-on-primary"
|
||||||
|
} rounded-lg transition-colors py-1 my-1 relative overflow-hidden inline-block w-full`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
5
src/views/icons/CopyIcon.tsx
Normal file
5
src/views/icons/CopyIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function CopyIcon(props: {fill: string}) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
src/views/icons/DocxIcon.tsx
Normal file
5
src/views/icons/DocxIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function DocxIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M52,144H36a8,8,0,0,0-8,8v56a8,8,0,0,0,8,8H52a36,36,0,0,0,0-72Zm0,56H44V160h8a20,20,0,0,1,0,40Zm169.53-4.91a8,8,0,0,1,.25,11.31A30.06,30.06,0,0,1,200,216c-17.65,0-32-16.15-32-36s14.35-36,32-36a30.06,30.06,0,0,1,21.78,9.6,8,8,0,0,1-11.56,11.06A14.24,14.24,0,0,0,200,160c-8.82,0-16,9-16,20s7.18,20,16,20a14.24,14.24,0,0,0,10.22-4.66A8,8,0,0,1,221.53,195.09ZM128,144c-17.65,0-32,16.15-32,36s14.35,36,32,36,32-16.15,32-36S145.65,144,128,144Zm0,56c-8.82,0-16-9-16-20s7.18-20,16-20,16,9,16,20S136.82,200,128,200ZM48,120a8,8,0,0,0,8-8V40h88V88a8,8,0,0,0,8,8h48v16a8,8,0,0,0,16,0V88a8,8,0,0,0-2.34-5.66l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v72A8,8,0,0,0,48,120ZM160,51.31,188.69,80H160Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
src/views/icons/DownloadIcon.tsx
Normal file
5
src/views/icons/DownloadIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function DownloadIcon(props: {fill: string}) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H72a8,8,0,0,1,0,16H32v64H224V136H184a8,8,0,0,1,0-16h40A16,16,0,0,1,240,136Zm-117.66-2.34a8,8,0,0,0,11.32,0l48-48a8,8,0,0,0-11.32-11.32L136,108.69V24a8,8,0,0,0-16,0v84.69L85.66,74.34A8,8,0,0,0,74.34,85.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
src/views/icons/HomeIcon.tsx
Normal file
5
src/views/icons/HomeIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function HomeIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M218.83,103.77l-80-75.48a1.14,1.14,0,0,1-.11-.11,16,16,0,0,0-21.53,0l-.11.11L37.17,103.77A16,16,0,0,0,32,115.55V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V115.55A16,16,0,0,0,218.83,103.77ZM208,208H48V115.55l.11-.1L128,40l79.9,75.43.11.1Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
src/views/icons/KeyIcon.tsx
Normal file
5
src/views/icons/KeyIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function KeyIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M48,56V200a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0Zm84,54.5L112,117V96a8,8,0,0,0-16,0v21L76,110.5a8,8,0,0,0-5,15.22l20,6.49-12.34,17a8,8,0,1,0,12.94,9.4l12.34-17,12.34,17a8,8,0,1,0,12.94-9.4l-12.34-17,20-6.49A8,8,0,0,0,132,110.5ZM238,115.64A8,8,0,0,0,228,110.5L208,117V96a8,8,0,0,0-16,0v21l-20-6.49a8,8,0,0,0-4.95,15.22l20,6.49-12.34,17a8,8,0,1,0,12.94,9.4l12.34-17,12.34,17a8,8,0,1,0,12.94-9.4l-12.34-17,20-6.49A8,8,0,0,0,238,115.64Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
src/views/icons/MagnifyingGlassIcon.tsx
Normal file
5
src/views/icons/MagnifyingGlassIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function MagnifyingGlassIcon(props: {fill: string}) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
src/views/icons/PaletteIcon.tsx
Normal file
5
src/views/icons/PaletteIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function PaletteIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M200.77,53.89A103.27,103.27,0,0,0,128,24h-1.07A104,104,0,0,0,24,128c0,43,26.58,79.06,69.36,94.17A32,32,0,0,0,136,192a16,16,0,0,1,16-16h46.21a31.81,31.81,0,0,0,31.2-24.88,104.43,104.43,0,0,0,2.59-24A103.28,103.28,0,0,0,200.77,53.89Zm13,93.71A15.89,15.89,0,0,1,198.21,160H152a32,32,0,0,0-32,32,16,16,0,0,1-21.31,15.07C62.49,194.3,40,164,40,128a88,88,0,0,1,87.09-88h.9a88.35,88.35,0,0,1,88,87.25A88.86,88.86,0,0,1,213.81,147.6ZM140,76a12,12,0,1,1-12-12A12,12,0,0,1,140,76ZM96,100A12,12,0,1,1,84,88,12,12,0,0,1,96,100Zm0,56a12,12,0,1,1-12-12A12,12,0,0,1,96,156Zm88-56a12,12,0,1,1-12-12A12,12,0,0,1,184,100Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
src/views/icons/ScanIcon.tsx
Normal file
5
src/views/icons/ScanIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function ScanIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M224,40V80a8,8,0,0,1-16,0V48H176a8,8,0,0,1,0-16h40A8,8,0,0,1,224,40ZM80,208H48V176a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H80a8,8,0,0,0,0-16Zm136-40a8,8,0,0,0-8,8v32H176a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V176A8,8,0,0,0,216,168ZM40,88a8,8,0,0,0,8-8V48H80a8,8,0,0,0,0-16H40a8,8,0,0,0-8,8V80A8,8,0,0,0,40,88Zm128,96H88a16,16,0,0,1-16-16V88A16,16,0,0,1,88,72h80a16,16,0,0,1,16,16v80A16,16,0,0,1,168,184ZM88,168h80V88H88Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
src/views/icons/StackIcon.tsx
Normal file
5
src/views/icons/StackIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function StackIcon() {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M230.91,172A8,8,0,0,1,228,182.91l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,36,169.09l92,53.65,92-53.65A8,8,0,0,1,230.91,172ZM220,121.09l-92,53.65L36,121.09A8,8,0,0,0,28,134.91l96,56a8,8,0,0,0,8.06,0l96-56A8,8,0,1,0,220,121.09ZM24,80a8,8,0,0,1,4-6.91l96-56a8,8,0,0,1,8.06,0l96,56a8,8,0,0,1,0,13.82l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,24,80Zm23.88,0L128,126.74,208.12,80,128,33.26Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
5
src/views/icons/TrashIcon.tsx
Normal file
5
src/views/icons/TrashIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export function TrashIcon(props: {fill: string}) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
6
src/views/icons/XIcon.tsx
Normal file
6
src/views/icons/XIcon.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export function XIcon(props: {fill: string}) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M165.66,101.66,139.31,128l26.35,26.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user