Compare commits
10 Commits
40914aab1a
...
246bec540c
Author | SHA1 | Date | |
---|---|---|---|
|
246bec540c | ||
|
d70d1edbd6 | ||
|
1eccc62056 | ||
|
e144178c27 | ||
|
7007b00e4c | ||
|
cfbd275a0f | ||
|
ab378de775 | ||
|
89bdc1499a | ||
|
7f163db1cd | ||
|
13f4ca0f51 |
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@ import {
|
|||||||
AlignmentType,
|
AlignmentType,
|
||||||
BorderStyle,
|
BorderStyle,
|
||||||
} from "docx";
|
} from "docx";
|
||||||
import { cmText, createSimpleText, createSimpleTextP, getImage, getQR } from "./utils";
|
import { cmText, createSimpleTextP, getImage, getQR } from "./utils";
|
||||||
import { CertData } from "./CertData";
|
import { CertData } from "./CertData";
|
||||||
|
|
||||||
const imgFondoDoc = getImage({
|
const imgFondoDoc = getImage({
|
||||||
@ -30,7 +30,7 @@ const imgEATE = getImage({
|
|||||||
height: 2.28,
|
height: 2.28,
|
||||||
width: 3.39,
|
width: 3.39,
|
||||||
horizontalOffset: 0.94,
|
horizontalOffset: 0.94,
|
||||||
verticalOffset: 13.77,
|
verticalOffset: 15.67,
|
||||||
});
|
});
|
||||||
|
|
||||||
const imgMTC = getImage({
|
const imgMTC = getImage({
|
||||||
@ -38,7 +38,7 @@ const imgMTC = getImage({
|
|||||||
height: 1.6,
|
height: 1.6,
|
||||||
width: 5,
|
width: 5,
|
||||||
horizontalOffset: 0.9,
|
horizontalOffset: 0.9,
|
||||||
verticalOffset: 17.22,
|
verticalOffset: 18.3,
|
||||||
});
|
});
|
||||||
|
|
||||||
const imgOSHA = getImage({
|
const imgOSHA = getImage({
|
||||||
@ -73,7 +73,7 @@ const tCertificate = createSimpleTextP({
|
|||||||
// Se expide el presente
|
// Se expide el presente
|
||||||
const tLabel3 = createSimpleTextP({
|
const tLabel3 = createSimpleTextP({
|
||||||
xPosition: 11.08,
|
xPosition: 11.08,
|
||||||
yPosition: 4.62,
|
yPosition: 5.43,
|
||||||
width: 7.74,
|
width: 7.74,
|
||||||
height: 0.5,
|
height: 0.5,
|
||||||
text: "Se expide el presente a:",
|
text: "Se expide el presente a:",
|
||||||
@ -84,7 +84,7 @@ const tLabel3 = createSimpleTextP({
|
|||||||
// SUPERVISOR ESCOLTA MATPEL
|
// SUPERVISOR ESCOLTA MATPEL
|
||||||
const tCourse = createSimpleTextP({
|
const tCourse = createSimpleTextP({
|
||||||
xPosition: 7.28,
|
xPosition: 7.28,
|
||||||
yPosition: 7.94,
|
yPosition: 8.75,
|
||||||
width: 15.42,
|
width: 15.42,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
text: "SUPERVISOR ESCOLTA MATPEL",
|
text: "SUPERVISOR ESCOLTA MATPEL",
|
||||||
@ -95,7 +95,7 @@ const tCourse = createSimpleTextP({
|
|||||||
// Por haber aprobado la dormacion...
|
// Por haber aprobado la dormacion...
|
||||||
const tLabel2 = createSimpleTextP({
|
const tLabel2 = createSimpleTextP({
|
||||||
xPosition: 10.93,
|
xPosition: 10.93,
|
||||||
yPosition: 7.14,
|
yPosition: 7.95,
|
||||||
width: 7.74,
|
width: 7.74,
|
||||||
height: 0.6,
|
height: 0.6,
|
||||||
text: "Por haber aprobado la formación en el curso",
|
text: "Por haber aprobado la formación en el curso",
|
||||||
@ -106,7 +106,7 @@ const tLabel2 = createSimpleTextP({
|
|||||||
// Temas tratados...
|
// Temas tratados...
|
||||||
const tTopics = createSimpleTextP({
|
const tTopics = createSimpleTextP({
|
||||||
xPosition: 5.04,
|
xPosition: 5.04,
|
||||||
yPosition: 9.49,
|
yPosition: 10.3,
|
||||||
width: 19.34,
|
width: 19.34,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
text: "Temas tratados: Inspección de Unidades y Llenado de Herramientas de Gestión Check List, IPERC, Inspección básica de Kit de Emergencias, Parada de Controles e Inspección, Delimitación y Evaluación del Evento, Sistemas de Comando de Incidentes, Practicas Reales en caso de Derrames de MATPEL con Trajes, Evacuación de Pacientes Afectados con Camioneta, Primeros Auxilios básico, Con una duración de 36 horas lectivas.",
|
text: "Temas tratados: Inspección de Unidades y Llenado de Herramientas de Gestión Check List, IPERC, Inspección básica de Kit de Emergencias, Parada de Controles e Inspección, Delimitación y Evaluación del Evento, Sistemas de Comando de Incidentes, Practicas Reales en caso de Derrames de MATPEL con Trajes, Evacuación de Pacientes Afectados con Camioneta, Primeros Auxilios básico, Con una duración de 36 horas lectivas.",
|
||||||
@ -118,7 +118,7 @@ const tTopics = createSimpleTextP({
|
|||||||
// Respaldado por:
|
// Respaldado por:
|
||||||
const tHours = createSimpleTextP({
|
const tHours = createSimpleTextP({
|
||||||
xPosition: 1.15,
|
xPosition: 1.15,
|
||||||
yPosition: 13.15,
|
yPosition: 15,
|
||||||
width: 3.07,
|
width: 3.07,
|
||||||
height: 0.5,
|
height: 0.5,
|
||||||
text: "Respaldado por:",
|
text: "Respaldado por:",
|
||||||
@ -131,7 +131,7 @@ const tHours = createSimpleTextP({
|
|||||||
// Chile
|
// Chile
|
||||||
const tChile = createSimpleTextP({
|
const tChile = createSimpleTextP({
|
||||||
xPosition: 3.07,
|
xPosition: 3.07,
|
||||||
yPosition: 15.98,
|
yPosition: 17.83,
|
||||||
width: 1.43,
|
width: 1.43,
|
||||||
height: 0.5,
|
height: 0.5,
|
||||||
text: "CHILE",
|
text: "CHILE",
|
||||||
@ -144,7 +144,7 @@ const tChile = createSimpleTextP({
|
|||||||
// Se expide certificado...
|
// Se expide certificado...
|
||||||
const tFinishLabel = createSimpleTextP({
|
const tFinishLabel = createSimpleTextP({
|
||||||
xPosition: 8.22,
|
xPosition: 8.22,
|
||||||
yPosition: 11.87,
|
yPosition: 12.68,
|
||||||
width: 13.15,
|
width: 13.15,
|
||||||
height: 0.5,
|
height: 0.5,
|
||||||
text: "Se expide el presente certificado para los fines que se estime conveniente",
|
text: "Se expide el presente certificado para los fines que se estime conveniente",
|
||||||
@ -160,7 +160,7 @@ const photoSection = new Paragraph({
|
|||||||
frame: {
|
frame: {
|
||||||
position: {
|
position: {
|
||||||
x: cmText(25),
|
x: cmText(25),
|
||||||
y: cmText(4.37),
|
y: cmText(5.2),
|
||||||
},
|
},
|
||||||
height: cmText(3.57),
|
height: cmText(3.57),
|
||||||
width: cmText(2.81),
|
width: cmText(2.81),
|
||||||
@ -199,13 +199,13 @@ export async function supervisorEscolta(props: CertData<null>): Promise<Buffer>
|
|||||||
height: 2.5,
|
height: 2.5,
|
||||||
width: 2.5,
|
width: 2.5,
|
||||||
horizontalOffset: 25.85,
|
horizontalOffset: 25.85,
|
||||||
verticalOffset: 15.88,
|
verticalOffset: 17.49,
|
||||||
});
|
});
|
||||||
|
|
||||||
// FERNANDO ARAOZ
|
// FERNANDO ARAOZ
|
||||||
const tName = createSimpleTextP({
|
const tName = createSimpleTextP({
|
||||||
xPosition: 3.78,
|
xPosition: 3.78,
|
||||||
yPosition: 5.21,
|
yPosition: 6.02,
|
||||||
width: 22.07,
|
width: 22.07,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
text: props.personFullName,
|
text: props.personFullName,
|
||||||
@ -219,7 +219,7 @@ export async function supervisorEscolta(props: CertData<null>): Promise<Buffer>
|
|||||||
frame: {
|
frame: {
|
||||||
position: {
|
position: {
|
||||||
x: cmText(11.84),
|
x: cmText(11.84),
|
||||||
y: cmText(6.54),
|
y: cmText(7.35),
|
||||||
},
|
},
|
||||||
width: cmText(6.02),
|
width: cmText(6.02),
|
||||||
height: cmText(0.6),
|
height: cmText(0.6),
|
||||||
@ -249,7 +249,7 @@ export async function supervisorEscolta(props: CertData<null>): Promise<Buffer>
|
|||||||
frame: {
|
frame: {
|
||||||
position: {
|
position: {
|
||||||
x: cmText(19.62),
|
x: cmText(19.62),
|
||||||
y: cmText(17.58),
|
y: cmText(19.2),
|
||||||
},
|
},
|
||||||
width: cmText(5.87),
|
width: cmText(5.87),
|
||||||
height: cmText(0.75),
|
height: cmText(0.75),
|
||||||
@ -277,7 +277,7 @@ export async function supervisorEscolta(props: CertData<null>): Promise<Buffer>
|
|||||||
// N° XXXX-20XX-EEG
|
// N° XXXX-20XX-EEG
|
||||||
const tCertCode = createSimpleTextP({
|
const tCertCode = createSimpleTextP({
|
||||||
xPosition: 24.59,
|
xPosition: 24.59,
|
||||||
yPosition: 8.07,
|
yPosition: 9,
|
||||||
width: 3.67,
|
width: 3.67,
|
||||||
height: 0.5,
|
height: 0.5,
|
||||||
text: `N° ${props.certCode}-${props.certYear}-EEG`,
|
text: `N° ${props.certCode}-${props.certYear}-EEG`,
|
||||||
|
@ -61,7 +61,9 @@ export async function getQR(data : {
|
|||||||
verticalOffset: number,
|
verticalOffset: number,
|
||||||
behindDocument?: boolean,
|
behindDocument?: boolean,
|
||||||
}): Promise<ImageRun> {
|
}): Promise<ImageRun> {
|
||||||
const qr = await QR.toDataURL(`https://www.eegsac.com/alumnoscertificados.php?DNI=${data.dni}&iid=${data.iid}`, {margin: 1});
|
// Old URL: https://www.eegsac.com/alumnoscertificados.php?DNI=${dni()}
|
||||||
|
// New URL: https://eegsac.com/certificado/${dni()}
|
||||||
|
const qr = await QR.toDataURL(`https://eegsac.com/certificado/${data.dni}?iid=${data.iid}`, {margin: 1});
|
||||||
|
|
||||||
return new ImageRun({
|
return new ImageRun({
|
||||||
data: qr,
|
data: qr,
|
||||||
|
@ -3,7 +3,7 @@ import { renderToString } from "solid-js/web";
|
|||||||
import { CertsBatch } from "src/views/BatchCerts";
|
import { CertsBatch } from "src/views/BatchCerts";
|
||||||
import { template } from "./BatchCerts.template";
|
import { template } from "./BatchCerts.template";
|
||||||
|
|
||||||
@Controller("batch-certs")
|
@Controller("batch-mode")
|
||||||
export class BatchCertController {
|
export class BatchCertController {
|
||||||
@Get()
|
@Get()
|
||||||
entry(): string {
|
entry(): string {
|
||||||
|
@ -9,6 +9,7 @@ export function template(ssr: string): string {
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="/static/styles.css?t=${Date.now()}" />
|
<link rel="stylesheet" href="/static/styles.css?t=${Date.now()}" />
|
||||||
|
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||||
<!-- Phosphor icons -->
|
<!-- Phosphor icons -->
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
|
@ -184,7 +184,7 @@ export class CertificateService {
|
|||||||
certificate.curso = data.subjectId;
|
certificate.curso = data.subjectId;
|
||||||
certificate.codigo = await this.getNextRegisterCode(data.subjectId);
|
certificate.codigo = await this.getNextRegisterCode(data.subjectId);
|
||||||
certificate.fecha_actual = new Date();
|
certificate.fecha_actual = new Date();
|
||||||
certificate.fecha_inscripcion = new Date(data.date);
|
certificate.fecha_inscripcion = data.date as unknown as Date ; // new Date(data.date);
|
||||||
certificate.curso_nombre = subject.nombre;
|
certificate.curso_nombre = subject.nombre;
|
||||||
|
|
||||||
certificate.persona = person;
|
certificate.persona = person;
|
||||||
|
@ -8,6 +8,7 @@ export function template(ssr: string): string {
|
|||||||
<title>Registrar certificados - EEGSAC</title>
|
<title>Registrar certificados - EEGSAC</title>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||||
<link rel="stylesheet" href="/static/styles.css?t=${Date.now()}" />
|
<link rel="stylesheet" href="/static/styles.css?t=${Date.now()}" />
|
||||||
<!-- Phosphor icons -->
|
<!-- Phosphor icons -->
|
||||||
<link
|
<link
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { createSignal, Match, onMount, Show, Switch } from "solid-js";
|
import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js";
|
||||||
import { Person } from "src/types/Person";
|
import { Person } from "src/types/Person";
|
||||||
import { DniRegister } from "./DniEntry/DniRegister";
|
import { DniRegister } from "./DniEntry/DniRegister";
|
||||||
|
import { RegisterReturn } from "src/types/RegisterReturn";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sample data
|
* Sample data
|
||||||
@ -27,9 +28,11 @@ enum Status {
|
|||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DniEntry(props: {dni: string}) {
|
export function DniEntry(props: {dni: string, remove: (_: string) => void}) {
|
||||||
const [person, setPerson] = createSignal<Person | null>(null);
|
const [person, setPerson] = createSignal<Person | null>(null);
|
||||||
const [status, setStatus] = createSignal<Status>(Status.Empty);
|
const [status, setStatus] = createSignal<Status>(Status.Empty);
|
||||||
|
const [certificates, setCertificates] = createSignal<Array<RegisterReturn>>([]);
|
||||||
|
const [certStatus, setCertStatus] = createSignal<Status>(Status.Empty);
|
||||||
|
|
||||||
const loadPerson = async() => {
|
const loadPerson = async() => {
|
||||||
setStatus(Status.Loading);
|
setStatus(Status.Loading);
|
||||||
@ -40,7 +43,7 @@ export function DniEntry(props: {dni: string}) {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setPerson(body);
|
setPerson(body);
|
||||||
setStatus(Status.Ok);
|
setStatus(Status.Ok);
|
||||||
|
loadCertificates();
|
||||||
} else if (response.status === 404) {
|
} else if (response.status === 404) {
|
||||||
console.error(body);
|
console.error(body);
|
||||||
setStatus(Status.Error);
|
setStatus(Status.Error);
|
||||||
@ -58,6 +61,23 @@ export function DniEntry(props: {dni: string}) {
|
|||||||
|
|
||||||
onMount(loadPerson);
|
onMount(loadPerson);
|
||||||
|
|
||||||
|
const loadCertificates = async() => {
|
||||||
|
setCertStatus(Status.Loading);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/certificate/${props.dni}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const body = await response.json();
|
||||||
|
setCertificates(body);
|
||||||
|
setCertStatus(Status.Ok);
|
||||||
|
} else {
|
||||||
|
setCertStatus(Status.Error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setCertStatus(Status.Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={status() !== Status.Error}>
|
<Show when={status() !== Status.Error}>
|
||||||
@ -96,11 +116,24 @@ export function DniEntry(props: {dni: string}) {
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button
|
<button
|
||||||
class={"rounded-full px-2 hover:bg-c-error first:text-c-error first:hover:text-c-on-error"}
|
class={"rounded-full px-2 hover:bg-c-error first:text-c-error first:hover:text-c-on-error"}
|
||||||
|
onclick={() => props.remove(props.dni)}
|
||||||
>
|
>
|
||||||
<i class="ph ph-x"></i>
|
<i class="ph ph-x"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-l"></div>
|
<div class="border-l">
|
||||||
|
<Switch>
|
||||||
|
<Match when={certStatus() === Status.Loading}>
|
||||||
|
<p>Loading...</p>
|
||||||
|
</Match>
|
||||||
|
<Match when={certStatus() === Status.Ok}>
|
||||||
|
<CertArray array={certificates()} />
|
||||||
|
</Match>
|
||||||
|
<Match when={certStatus() === Status.Error}>
|
||||||
|
<p class="text-c-error">Error loading certificates.</p>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
</Switch>
|
</Switch>
|
||||||
@ -113,3 +146,34 @@ export function DniEntry(props: {dni: string}) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CertArray(props: {array: Array<RegisterReturn>}) {
|
||||||
|
const filtered = createMemo(() => {
|
||||||
|
const nowMs = Date.now();
|
||||||
|
return props.array.filter((cert) => {
|
||||||
|
const date = new Date(cert.fecha_inscripcion);
|
||||||
|
const ms = date.getTime();
|
||||||
|
|
||||||
|
const difference = nowMs - ms;
|
||||||
|
|
||||||
|
return difference < (1000 * 60 * 60 * 24 * 366);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
[
|
||||||
|
<For each={filtered()}>
|
||||||
|
{(cert) => (
|
||||||
|
<div class="mr-4 inline-block underline">
|
||||||
|
{cert.curso_nombre}
|
||||||
|
<span class="font-mono">
|
||||||
|
{cert.fecha_inscripcion}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
]
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import { For } from "solid-js";
|
import { For, createSignal, onMount } from "solid-js";
|
||||||
import { DniEntry } from "./DniEntry";
|
import { DniEntry } from "./DniEntry";
|
||||||
|
import { NewRegister } from "../components/NewRegister";
|
||||||
|
import { subjects } from "../subjects";
|
||||||
|
|
||||||
export function DniGroup(props: {group: string, index: number}) {
|
export function DniGroup(props: {group: string, index: number}) {
|
||||||
const dnis = () => [...props.group.matchAll(/\d+/g)];
|
const [dnis, setDnis] = createSignal<Array<string>>([]);
|
||||||
|
|
||||||
console.log("Loading group...");
|
onMount(() => {
|
||||||
|
setDnis([...props.group.matchAll(/\d+/g)].map((x) => x.toString()));
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeDni = (dni: string) => {
|
||||||
|
setDnis((prev) => prev.filter((x) => x !== dni));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class=" grid-cols-[53rem_auto] gap-2 my-8">
|
<div class=" grid-cols-[53rem_auto] gap-2 my-8">
|
||||||
@ -19,7 +27,7 @@ export function DniGroup(props: {group: string, index: number}) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<For each={dnis()}>
|
<For each={dnis()}>
|
||||||
{(dni) => <DniEntry dni={dni.toString()} />}
|
{(dni) => <DniEntry dni={dni} remove={removeDni} />}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -27,7 +35,63 @@ export function DniGroup(props: {group: string, index: number}) {
|
|||||||
<h2 class="font-medium text-xl text-c-success pb-2">
|
<h2 class="font-medium text-xl text-c-success pb-2">
|
||||||
Grupo #{props.index + 1} - cursos y fechas
|
Grupo #{props.index + 1} - cursos y fechas
|
||||||
</h2>
|
</h2>
|
||||||
|
<hr />
|
||||||
|
<CourseManager personAmount={dnis().length} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CourseManager(props: {personAmount: number}) {
|
||||||
|
const [courses, setCourses] = createSignal<Array<[number, string]>>([]);
|
||||||
|
|
||||||
|
async function registerCourse(_: number, subjectId: number, date: string): Promise<null | string> {
|
||||||
|
setCourses((x) => [...x, [subjectId, date]]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<NewRegister
|
||||||
|
personId={0}
|
||||||
|
onSuccess={() => {}}
|
||||||
|
registerFn={registerCourse}
|
||||||
|
labelText={"Seleccionar cursos"}
|
||||||
|
/>
|
||||||
|
<div class="p-4">
|
||||||
|
<h2 class="mb-4 font-bold text-xl">Verificar y confirmar cursos</h2>
|
||||||
|
<div class="">
|
||||||
|
<For each={courses()}>
|
||||||
|
{(course) => <Course
|
||||||
|
id={course[0]}
|
||||||
|
date={course[1]}
|
||||||
|
onDelete={(id) => setCourses((x) => x.filter((y) => y[0] !== id))}
|
||||||
|
/>}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="my-2 bg-c-primary text-c-on-primary px-4 py-2 rounded-md cursor-pointer
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Registrar las <b>{props.personAmount} personas</b> en los <b>{courses().length} cursos</b>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Course(props: {id: number, date: string, onDelete: (id: number) => void}) {
|
||||||
|
const courseName = () => subjects().find((x) => x.id === props.id)?.nombre ?? "NOT FOUND";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="grid grid-cols-[auto_10rem_5rem] gap-2 hover:bg-c-surface-variant hover:text-c-on-surface-variant font-mono">
|
||||||
|
<p>{courseName()}</p>
|
||||||
|
<p>{props.date}</p>
|
||||||
|
<div>
|
||||||
|
<button class="underline" onclick={() => props.onDelete(props.id)}>Eliminar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { createSignal } from "solid-js";
|
import { createSignal, onMount } from "solid-js";
|
||||||
import { DniTable } from "./AulaVirtual/DniTable";
|
import { DniTable } from "./AulaVirtual/DniTable";
|
||||||
import { Dnis } from "./AulaVirtual/Dnis";
|
import { Dnis } from "./AulaVirtual/Dnis";
|
||||||
|
import { ensureColors } from "./components/colors";
|
||||||
|
|
||||||
export function CertsBatch() {
|
export function CertsBatch() {
|
||||||
const [dniGroups, setDniGroups] = createSignal<Array<string>>([]);
|
const [dniGroups, setDniGroups] = createSignal<Array<string>>([]);
|
||||||
|
|
||||||
|
onMount(ensureColors);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 class="px-4 py-2 text-2xl font-bold">
|
<h1 class="px-4 py-2 text-2xl font-bold">
|
||||||
|
@ -1,27 +1,24 @@
|
|||||||
import { createSignal } from "solid-js";
|
import { createSignal, onMount } from "solid-js";
|
||||||
import { Search } from "./components/Search";
|
import { Search } from "./components/Search";
|
||||||
import { Person } from "../types/Person";
|
import { Person } from "../types/Person";
|
||||||
import { Registers } from "./components/Registers";
|
import { Registers } from "./components/Registers";
|
||||||
import { NewRegister } from "./components/NewRegister";
|
import { NewRegister } from "./components/NewRegister";
|
||||||
|
import { ensureColors } from "./components/colors";
|
||||||
|
|
||||||
export function Certs() {
|
export function Certs() {
|
||||||
const [person, setPerson] = createSignal<Person | null>(null);
|
const [person, setPerson] = createSignal<Person | null>(null);
|
||||||
const [lastUpdate, setLastUpdate] = createSignal(0);
|
const [lastUpdate, setLastUpdate] = createSignal(0);
|
||||||
|
|
||||||
|
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
|
||||||
person={person()}
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,16 +1,83 @@
|
|||||||
import { createSignal, onMount, Show } from "solid-js";
|
import { createSignal, Show } from "solid-js";
|
||||||
import type { CursoGIE } from "../../model/CursoGIE/cursoGIE.entity";
|
|
||||||
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 { Person } from "src/types/Person";
|
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 & {
|
||||||
submitter: HTMLElement;
|
submitter: HTMLElement;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function NewRegister(props: {person: Person | null, onSuccess: () => void}) {
|
|
||||||
const [subjects, setSubjects] = createSignal<Array<CursoGIE>>([]);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterFn = (personId: number, subjectId: number, date: string) => Promise<null | string>;
|
||||||
|
|
||||||
|
const defaultLabelText = "3. Crear nuevos registros";
|
||||||
|
|
||||||
|
type TabType = "Presets" | "Manual";
|
||||||
|
|
||||||
|
export function NewRegister(props: {
|
||||||
|
personId: number | null,
|
||||||
|
onSuccess: () => void,
|
||||||
|
registerFn?: RegisterFn,
|
||||||
|
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
|
||||||
@ -27,23 +94,6 @@ export function NewRegister(props: {person: Person | null, onSuccess: () => void
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// Loads subjects from DB
|
|
||||||
onMount(async() => {
|
|
||||||
console.log("Retrieve all subjects");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/subject/");
|
|
||||||
if (response.ok) {
|
|
||||||
setSubjects(await response.json());
|
|
||||||
} else {
|
|
||||||
setError("No se pudo cargar cursos");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setError(`No se pudo cargar cursos. ${JSON.stringify(e)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const register: HTMLEventFn = async(ev) => {
|
const register: HTMLEventFn = async(ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
@ -65,24 +115,17 @@ export function NewRegister(props: {person: Person | null, onSuccess: () => void
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const response = await fetch("/certificate", {
|
const result = await (props.registerFn ?? defaultNewRegisterFn)(
|
||||||
method: "POST",
|
props.personId ?? -1,
|
||||||
headers: {
|
subject,
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
personId: props.person?.id ?? -1,
|
|
||||||
subjectId: subject,
|
|
||||||
date,
|
date,
|
||||||
}),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (result === null) {
|
||||||
props.onSuccess();
|
props.onSuccess();
|
||||||
setCount((x) => x + 1);
|
setCount((x) => x + 1);
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
setError(result);
|
||||||
setError(JSON.stringify(data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -90,9 +133,9 @@ export function NewRegister(props: {person: Person | null, onSuccess: () => void
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h2 class="mb-4 font-bold text-xl">3. Crear nuevos registros</h2>
|
<h2 class="mb-4 font-bold text-xl">{props.labelText ?? defaultLabelText}</h2>
|
||||||
|
|
||||||
<Show when={props.person?.id !== -1}>
|
<Show when={props.personId !== -1}>
|
||||||
<form
|
<form
|
||||||
class="px-4 grid"
|
class="px-4 grid"
|
||||||
style={{"grid-template-columns": "30rem 12rem 10rem auto", "grid-column-gap": "1rem"}}
|
style={{"grid-template-columns": "30rem 12rem 10rem auto", "grid-column-gap": "1rem"}}
|
||||||
@ -133,3 +176,104 @@ export function NewRegister(props: {person: Person | null, onSuccess: () => void
|
|||||||
</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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { createEffect, createMemo, createSignal, For, JSX, Show } from "solid-js";
|
import { createEffect, createSignal, For } from "solid-js";
|
||||||
import type {CursoGIE} from "../../../model/CursoGIE/cursoGIE.entity";
|
import type {CursoGIE} from "../../../model/CursoGIE/cursoGIE.entity";
|
||||||
import { isServer } from "solid-js/web";
|
import { isServer } from "solid-js/web";
|
||||||
|
|
||||||
@ -15,7 +15,14 @@ export function SearchableSelect(props: {
|
|||||||
const inputEl = ev.target as HTMLInputElement;
|
const inputEl = ev.target as HTMLInputElement;
|
||||||
// Clear current selection
|
// Clear current selection
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
setFilter(inputEl.value.toLowerCase());
|
|
||||||
|
let filter: string = inputEl.value.toLowerCase();
|
||||||
|
filter = filter.replace("á", "a");
|
||||||
|
filter = filter.replace("é", "e");
|
||||||
|
filter = filter.replace("í", "i");
|
||||||
|
filter = filter.replace("ó", "o");
|
||||||
|
filter = filter.replace("ú", "u");
|
||||||
|
setFilter(filter);
|
||||||
};
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@ -58,23 +65,36 @@ export function SearchableSelect(props: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredOptions = () => {
|
||||||
|
const filterText = filter();
|
||||||
|
|
||||||
|
return props.subjects.filter((subject) => {
|
||||||
|
let subjectText = subject.nombre.toLowerCase();
|
||||||
|
subjectText = subjectText.replace("á", "a");
|
||||||
|
subjectText = subjectText.replace("é", "e");
|
||||||
|
subjectText = subjectText.replace("í", "i");
|
||||||
|
subjectText = subjectText.replace("ó", "o");
|
||||||
|
subjectText = subjectText.replace("ú", "u");
|
||||||
|
|
||||||
|
return selected() === null && subjectText.indexOf(filterText) !== -1;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{inputElement}
|
{inputElement}
|
||||||
<br/>
|
<br/>
|
||||||
<div
|
<div
|
||||||
class="border-c-outline border-2 rounded overflow-y-scroll"
|
class="border-c-outline border-2 rounded overflow-y-scroll h-[10rem]"
|
||||||
style={{"max-height": "18rem", "min-height": "12rem"}}
|
|
||||||
>
|
>
|
||||||
<For each={props.subjects}>
|
<For each={filteredOptions()}>
|
||||||
{(s) => (
|
{(s) => (
|
||||||
<button
|
<button
|
||||||
class={`w-full text-left py-1 px-2
|
class="w-full text-left py-1 px-2
|
||||||
hover:bg-c-primary-container hover:text-c-on-primary-container
|
hover:bg-c-primary-container hover:text-c-on-primary-container"
|
||||||
${s.nombre.toLowerCase().indexOf(filter()) !== -1 && selected() === null ? "block" : "hidden"}`}
|
|
||||||
onclick={(ev) => {
|
onclick={(ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
console.log("Click! :D");
|
|
||||||
setSelected(s.id);
|
setSelected(s.id);
|
||||||
setInputValue(s.nombre);
|
setInputValue(s.nombre);
|
||||||
}}
|
}}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Show, createEffect, createSignal, For, JSX } from "solid-js";
|
import { Show, createEffect, createSignal, For, JSX, createMemo } from "solid-js";
|
||||||
import { Person } from "../../types/Person";
|
import { Person } from "../../types/Person";
|
||||||
import { RegisterReturn } from "../../types/RegisterReturn";
|
import { RegisterReturn } from "../../types/RegisterReturn";
|
||||||
|
|
||||||
@ -33,57 +33,12 @@ export function Registers(props: { person: Person | null, lastUpdate: number })
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sortedRegisters = createMemo(() => registers().sort((r1, r2) => ((r1.fecha_inscripcion < r2.fecha_inscripcion) ? 1 : -1)));
|
||||||
|
|
||||||
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" }}
|
||||||
@ -105,7 +60,7 @@ export function Registers(props: { person: Person | null, lastUpdate: number })
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={registers().sort((r1, r2) => ((r1.fecha_actual < r2.fecha_actual) ? 1 : -1))}>
|
<For each={sortedRegisters()}>
|
||||||
{(register) => <Register cert={register} onUpdate={loadCertificates} />}
|
{(register) => <Register cert={register} onUpdate={loadCertificates} />}
|
||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -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,11 +22,14 @@ 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(() => {
|
||||||
if (dni() !== "") {
|
if (dni() !== "") {
|
||||||
QR.toDataURL(`https://www.eegsac.com/alumnoscertificados.php?DNI=${dni()}`, {margin: 1}, (err, res) => {
|
// Old URL: https://www.eegsac.com/alumnoscertificados.php?DNI=${dni()}
|
||||||
|
// New URL: https://eegsac.com/certificado/${dni()}
|
||||||
|
QR.toDataURL(`https://eegsac.com/certificado/${dni()}`, {margin: 1}, (err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Error creating QR code");
|
console.error("Error creating QR code");
|
||||||
return;
|
return;
|
||||||
@ -51,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);
|
||||||
@ -67,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
|
||||||
@ -104,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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -120,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"
|
||||||
@ -146,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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -159,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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
166
src/views/components/colors.ts
Normal file
166
src/views/components/colors.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { isServer } from "solid-js/web";
|
||||||
|
|
||||||
|
const accentOrange = "";
|
||||||
|
|
||||||
|
const accentGreen = `
|
||||||
|
:root {
|
||||||
|
--c-primary: #7cdc6d;
|
||||||
|
--c-on-primary: #003a02;
|
||||||
|
--c-primary-container: #005304;
|
||||||
|
--c-on-primary-container: #97f986;
|
||||||
|
--c-background: #1a1c18;
|
||||||
|
--c-on-background: #e2e3dd;
|
||||||
|
--c-surface: #1a1c18;
|
||||||
|
--c-on-surface: #e2e3dd;
|
||||||
|
--c-outline: #8d9387;
|
||||||
|
--c-surface-variant: #43483f;
|
||||||
|
--c-on-surface-variant: #c3c8bc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--c-primary: #006e08;
|
||||||
|
--c-on-primary: #ffffff;
|
||||||
|
--c-primary-container: #97f986;
|
||||||
|
--c-on-primary-container: #002201;
|
||||||
|
--c-background: #fcfdf6;
|
||||||
|
--c-on-background: #1a1c18;
|
||||||
|
--c-surface: #fcfdf6;
|
||||||
|
--c-on-surface: #1a1c18;
|
||||||
|
--c-outline: #73796e;
|
||||||
|
--c-surface-variant: #dfe4d8;
|
||||||
|
--c-on-surface-variant: #43483f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const accentBlue = `
|
||||||
|
:root {
|
||||||
|
--c-primary: #adc6ff;
|
||||||
|
--c-on-primary: #002e69;
|
||||||
|
--c-primary-container: #0e448e;
|
||||||
|
--c-on-primary-container: #d8e2ff;
|
||||||
|
--c-background: #1b1b1f;
|
||||||
|
--c-on-background: #e3e2e6;
|
||||||
|
--c-surface: #1b1b1f;
|
||||||
|
--c-on-surface: #e3e2e6;
|
||||||
|
--c-outline: #8e9099;
|
||||||
|
--c-surface-variant: #44474f;
|
||||||
|
--c-on-surface-variant: #c4c6d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--c-primary: #315da8;
|
||||||
|
--c-on-primary: #ffffff;
|
||||||
|
--c-primary-container: #d8e2ff;
|
||||||
|
--c-on-primary-container: #001a41;
|
||||||
|
--c-background: #fefbff;
|
||||||
|
--c-on-background: #1b1b1f;
|
||||||
|
--c-surface: #fefbff;
|
||||||
|
--c-on-surface: #1b1b1f;
|
||||||
|
--c-outline: #74777f;
|
||||||
|
--c-surface-variant: #e1e2ec;
|
||||||
|
--c-on-surface-variant: #44474f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const accentYellow = `
|
||||||
|
:root {
|
||||||
|
--c-primary: #f5bf31;
|
||||||
|
--c-on-primary: #3f2e00;
|
||||||
|
--c-primary-container: #5b4300;
|
||||||
|
--c-on-primary-container: #ffdf9b;
|
||||||
|
--c-background: #1e1b16;
|
||||||
|
--c-on-background: #e9e1d9;
|
||||||
|
--c-surface: #1e1b16;
|
||||||
|
--c-on-surface: #e9e1d9;
|
||||||
|
--c-outline: #999080;
|
||||||
|
--c-surface-variant: #4d4639;
|
||||||
|
--c-on-surface-variant: #d0c5b4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--c-primary: #785a00;
|
||||||
|
--c-on-primary: #ffffff;
|
||||||
|
--c-primary-container: #ffdf9b;
|
||||||
|
--c-on-primary-container: #251a00;
|
||||||
|
--c-background: #fffbff;
|
||||||
|
--c-on-background: #1e1b16;
|
||||||
|
--c-surface: #fffbff;
|
||||||
|
--c-on-surface: #1e1b16;
|
||||||
|
--c-outline: #7f7667;
|
||||||
|
--c-surface-variant: #ede1cf;
|
||||||
|
--c-on-surface-variant: #4d4639;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const accentPink = `
|
||||||
|
:root {
|
||||||
|
--c-primary: #ffade5;
|
||||||
|
--c-on-primary: #5e0051;
|
||||||
|
--c-primary-container: #80156e;
|
||||||
|
--c-on-primary-container: #ffd7ef;
|
||||||
|
--c-background: #1f1a1d;
|
||||||
|
--c-on-background: #eae0e3;
|
||||||
|
--c-surface: #1f1a1d;
|
||||||
|
--c-on-surface: #eae0e3;
|
||||||
|
--c-outline: #9b8d94;
|
||||||
|
--c-surface-variant: #4f444a;
|
||||||
|
--c-on-surface-variant: #d2c2ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--c-primary: #9d3288;
|
||||||
|
--c-on-primary: #ffffff;
|
||||||
|
--c-primary-container: #ffd7ef;
|
||||||
|
--c-on-primary-container: #3a0031;
|
||||||
|
--c-background: #fffbff;
|
||||||
|
--c-on-background: #1f1a1d;
|
||||||
|
--c-surface: #fffbff;
|
||||||
|
--c-on-surface: #1f1a1d;
|
||||||
|
--c-outline: #81737a;
|
||||||
|
--c-surface-variant: #efdee6;
|
||||||
|
--c-on-surface-variant: #4f444a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ensureColors = () => {
|
||||||
|
if (!isServer) {
|
||||||
|
console.log("Running in the client!");
|
||||||
|
|
||||||
|
const colorScheme = localStorage.getItem("color-scheme") ?? "orange";
|
||||||
|
|
||||||
|
const styleEl = document.createElement("style");
|
||||||
|
|
||||||
|
switch (colorScheme) {
|
||||||
|
case "orange": {
|
||||||
|
styleEl.innerHTML = accentOrange;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "green": {
|
||||||
|
styleEl.innerHTML = accentGreen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "blue": {
|
||||||
|
styleEl.innerHTML = accentBlue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "yellow": {
|
||||||
|
styleEl.innerHTML = accentYellow;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "pink": {
|
||||||
|
styleEl.innerHTML = accentPink;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.head.appendChild(styleEl);
|
||||||
|
}
|
||||||
|
};
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
16
src/views/subjects.ts
Normal file
16
src/views/subjects.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createSignal } from "solid-js";
|
||||||
|
import { CursoGIE } from "src/model/CursoGIE/cursoGIE.entity";
|
||||||
|
|
||||||
|
export const [subjects, setSubjects] = createSignal<Array<CursoGIE>>([]);
|
||||||
|
|
||||||
|
(async() => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/subject/");
|
||||||
|
if (response.ok) {
|
||||||
|
setSubjects(await response.json());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 353 KiB |
Loading…
Reference in New Issue
Block a user