[Classroom][FE] UI for classroom enrolling

This commit is contained in:
Araozu 2023-10-04 16:53:54 -05:00
parent 60e461f3c0
commit b162b14eb3
10 changed files with 270 additions and 14 deletions

View File

@ -0,0 +1,42 @@
import { createSignal } from "solid-js";
import { FilledCard } from "../components/FilledCard";
import { ClassroomRegistrationPreview } from "./ClassroomRegistrationPreview";
import { ClassroomSearchableSelect } from "./ClassroomSearchableSelect";
import { ClassroomCourseValue } from "../types/ClassroomCourse";
export function ClassroomRegistration() {
const [selections, setSelections] = createSignal<Array<ClassroomCourseValue>>([]);
return (
<div class="h-screen overflow-y-scroll">
<FilledCard class="border border-c-outline overflow-hidden">
<h2 class="p-3 font-bold text-xl">Registrar cursos</h2>
<div class="bg-c-surface p-4 h-[23rem]">
<ManualClassroomRegistration
onAdd={(x) => setSelections((s) => ([...s, x]))}
/>
</div>
</FilledCard>
<ClassroomRegistrationPreview
selections={selections()}
deleteRegister={(course_key) => setSelections((course) => course.filter((v) => v !== course_key))}
/>
</div>
);
}
function ManualClassroomRegistration(props: {onAdd: (k: ClassroomCourseValue) => void}) {
return (
<form>
<p>Haz click en un curso para agregarlo</p>
<br />
<ClassroomSearchableSelect
onAdd={props.onAdd}
/>
</form>
);
}

View File

@ -0,0 +1,62 @@
import { For, createMemo } from "solid-js";
import { FilledCard } from "../components/FilledCard";
import { LoadingIcon } from "../icons/LoadingIcon";
import { LoadingStatus, useLoading } from "../utils/functions";
import { ClassroomCourseValue, allClassrooomCourses } from "../types/ClassroomCourse";
import { XIcon } from "../icons/XIcon";
export function ClassroomRegistrationPreview(props: {
selections: Array<ClassroomCourseValue>,
deleteRegister: (k: string) => void,
}) {
const {status} = useLoading();
const loading = createMemo(() => status() === LoadingStatus.Loading);
const submit = async() => {
};
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}>
{(course_key) => (
<div class="grid grid-cols-[auto_1.5rem] py-1 px-2 rounded-md border border-c-outline my-1">
<span class="font-mono">
{allClassrooomCourses[course_key]}
</span>
<button
class="hover:bg-c-surface-variant rounded-md"
onclick={() => props.deleteRegister(course_key)}
>
<XIcon fill="var(--c-on-surface)" />
</button>
</div>
)}
</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 relative"
type="button"
disabled={props.selections.length === 0 || loading()}
onclick={submit}
>
<span
class="absolute top-1 left-2"
style={{display: loading() ? "inline-block" : "none"}}
>
<LoadingIcon
class="animate-spin"
fill="var(--c-primary-container)"
/>
</span>
<span class="ml-6">
Matricular en {props.selections.length} cursos
</span>
</button>
</div>
</FilledCard>
);
}

View File

@ -0,0 +1,96 @@
import { createSignal, For } from "solid-js";
import { isServer } from "solid-js/web";
import { allClassrooomCourses, ClassroomCourseValue } from "../types/ClassroomCourse";
export function ClassroomSearchableSelect(props: {
onAdd: (key: ClassroomCourseValue) => void,
}) {
const [filter, setFilter] = createSignal("");
const [inputValue, setInputValue] = createSignal("");
const iHandler = (ev: KeyboardEvent & {currentTarget: HTMLInputElement, target: Element}) => {
const inputEl = ev.target as HTMLInputElement;
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);
};
const selectCourse = (key: ClassroomCourseValue) => {
props.onAdd(key);
setFilter("");
setInputValue("");
};
const inputElement = (
<input
id="create-subject"
class={`bg-c-background text-c-on-background
border-2 rounded-tl rounded-tr px-2 py-1
w-full
invalid:border-c-error invalid:text-c-error
focus:border-c-primary outline-none
disabled:opacity-50 disabled:cursor-not-allowed`}
type="text"
placeholder="Curso"
onkeyup={iHandler}
value={inputValue()}
onchange={(ev) => setInputValue(ev.target.value)}
autocomplete="off"
/>
);
if (!isServer) {
(inputElement as HTMLInputElement).addEventListener("keydown", (ev) => {
if (ev.code === "Enter") {
ev.preventDefault();
}
});
}
const filteredOptions = () => {
const filterText = filter();
return Object.entries(allClassrooomCourses).filter(([, course]) => {
let courseText = course.toLowerCase();
courseText = courseText.replace("á", "a");
courseText = courseText.replace("é", "e");
courseText = courseText.replace("í", "i");
courseText = courseText.replace("ó", "o");
courseText = courseText.replace("ú", "u");
return courseText.indexOf(filterText) !== -1;
});
};
return (
<>
{inputElement}
<br />
<div
class="border-c-outline border-l-2 border-b-2 border-r-2
rounded-bl rounded-br overflow-y-scroll h-[10rem]"
>
<For each={filteredOptions()}>
{([courseKey, courseName]) => (
<button
class="w-full text-left py-1 px-2
hover:bg-c-primary-container hover:text-c-on-primary-container"
onclick={(ev) => {
ev.preventDefault();
selectCourse(courseKey as ClassroomCourseValue);
}}
>
{courseName}
</button>
)}
</For>
</div>
</>
);
}

View File

@ -5,6 +5,7 @@ import { FilledCard } from "../components/FilledCard";
import { ClassroomUserCreation } from "./ClassroomUserCreation"; import { ClassroomUserCreation } from "./ClassroomUserCreation";
import { ClassroomVinculation } from "./ClassroomVinculation"; import { ClassroomVinculation } from "./ClassroomVinculation";
import { ClassroomUserCourses } from "./ClassroomUserCourses"; import { ClassroomUserCourses } from "./ClassroomUserCourses";
import { ClassroomRegistration } from "./ClassroomRegistration";
type TabType = "Vinculate" | "Create"; type TabType = "Vinculate" | "Create";
@ -14,7 +15,8 @@ export function OnlineClassroom() {
return ( return (
<div class="grid grid-cols-[16rem_25rem_1fr]"> <div class="grid grid-cols-[16rem_25rem_1fr]">
<Search setPerson={setPerson} /> <Search setPerson={setPerson} />
<div>
<ClassroomRegistration />
<Show when={person() !== null && person()!.person_classroom_id === null}> <Show when={person() !== null && person()!.person_classroom_id === null}>
<ClassroomUser <ClassroomUser
@ -27,7 +29,6 @@ export function OnlineClassroom() {
</Show> </Show>
</div> </div>
</div>
); );
} }

View File

@ -3,6 +3,7 @@ import { SearchableSelect } from "./SearchableSelect";
import { CustomLabelSelect } from "./CustomLabelSelect"; import { CustomLabelSelect } from "./CustomLabelSelect";
import { courseMap } from "../../utils/allCourses"; import { courseMap } from "../../utils/allCourses";
import { RegistrationPreview } from "."; import { RegistrationPreview } from ".";
import { customLabelsMap } from "../../utils/allCustomLabels";
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & { type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
submitter: HTMLElement; submitter: HTMLElement;
@ -16,7 +17,7 @@ export function ManualRegistration(props: {
const [count, setCount] = createSignal(0); const [count, setCount] = createSignal(0);
const [error, setError] = createSignal(""); const [error, setError] = createSignal("");
const [selectedCourseId, seSelectedCourseId] = createSignal<number | null>(null); const [selectedCourseId, setSelectedCourseId] = createSignal<number | null>(null);
const [customLabel, setCustomLabel] = createSignal(""); const [customLabel, setCustomLabel] = createSignal("");
const [isPreview, setIsPreview] = createSignal(false); const [isPreview, setIsPreview] = createSignal(false);
@ -65,11 +66,16 @@ export function ManualRegistration(props: {
return; return;
} }
const label = customLabel();
// TODO: Refactor along with allCustomLabel.tsx
const custom_label_id = Object.entries(customLabelsMap()).find(([, v]) => (v.custom_label_value === label))?.[1].custom_label_id ?? -1;
const data: RegistrationPreview = { const data: RegistrationPreview = {
courseId: subject, courseId: subject,
date, date,
customLabel: customLabel(), customLabel: customLabel(),
is_preview: isPreview(), is_preview: isPreview(),
custom_label_id,
}; };
props.onAdd(data); props.onAdd(data);
@ -83,7 +89,7 @@ export function ManualRegistration(props: {
<form onsubmit={register}> <form onsubmit={register}>
<div class="h-52"> <div class="h-52">
<SearchableSelect <SearchableSelect
onChange={seSelectedCourseId} onChange={setSelectedCourseId}
count={count()} count={count()}
/> />
</div> </div>

View File

@ -88,6 +88,7 @@ export function RegisterPresets(props: {
date: dateYYYYMMDD, date: dateYYYYMMDD,
customLabel: "", customLabel: "",
is_preview: isPreview(), is_preview: isPreview(),
custom_label_id: 1,
}); });
// Substract current date for the next course // Substract current date for the next course

View File

@ -26,12 +26,13 @@ export function RegisterPreview(props: {selections: Array<RegistrationPreview>,
await wait(2000); await wait(2000);
const registers: RegisterBatchCreate = props.selections.map(({courseId, date, customLabel, is_preview}) => ({ const registers: RegisterBatchCreate = props.selections.map(({courseId, date, customLabel, is_preview, custom_label_id}) => ({
person_id: props.personId!, person_id: props.personId!,
course_id: courseId, course_id: courseId,
date, date,
custom_label: customLabel, custom_label: customLabel,
is_preview, is_preview,
custom_label_id,
})); }));
const result = await createRegisters(registers); const result = await createRegisters(registers);

View File

@ -12,6 +12,7 @@ export type RegistrationPreview = {
date: string, date: string,
customLabel: string, customLabel: string,
is_preview: boolean, is_preview: boolean,
custom_label_id: number,
} }
export function NewRegister(props: { export function NewRegister(props: {

View File

@ -1,3 +1,48 @@
export type ClassroomCourse = { export type ClassroomCourse = {
name: string name: string
} }
export type ClassroomCourseValue =
| "MATPELNIVEL1"
| "MATPELNIVEL2"
| "MATPELNIVEL3"
| "CAJAEATONFULLER"
| "CAMIONMINERO797CAT"
| "ESPACIOSCONFINADOS"
| "HERRAMIENTASDEGESTION"
| "IPERC"
| "MANEJODEFENSIVO"
| "PREVENCIONDERIESGOSLABORALES"
| "PREVENCIONYPROTECCIONCONTRAINCENDIOS"
| "PRIMEROSAUXILIOS"
| "SBC"
| "SEGURIDADENLAOPERACION"
| "SEGURIDADMATPEL123"
| "SUPERVISORDEALTORIESGO"
| "SUPERVISORDESEGURIDADMINEROINDUSTRIA"
| "SUPERVISORESCOLTAMATPEL"
| "TRABAJOSENALTURA"
;
export const allClassrooomCourses = {
"MATPELNIVEL1": "MATPEL 1",
"MATPELNIVEL2": "MATPEL 2",
"MATPELNIVEL3": "MATPEL 3",
"SUPERVISORESCOLTAMATPEL": "SUPERVISOR ESCOLTA - MATPEL",
"MANEJODEFENSIVO": "MANEJO DEFENSIVO",
"SEGURIDADENLAOPERACION": "SEGURIDAD EN LA OPERACIÓN",
"HERRAMIENTASDEGESTION": "HERRAMIENTAS DE GESTIÓN",
"CAJAEATONFULLER": "CAJA EATON FULLER",
"CAMIONMINERO797CAT": "CAMION MINERO 797 CAT",
"ESPACIOSCONFINADOS": "ESPACIOS CONFINADOS",
"IPERC": "IPERC",
"PREVENCIONDERIESGOSLABORALES": "PREVENCION DE RIESGOS LABORALES",
"PREVENCIONYPROTECCIONCONTRAINCENDIOS": "PREVENCIÓN Y PROTECCIÓN CONTRA INCENDIOS",
"PRIMEROSAUXILIOS": "PRIMEROS AUXILIOS",
"SBC": "SEGURIDAD BASADA EN EL COMPORTAMIENTO \"SBC\"",
"SEGURIDADMATPEL123": "SEGURIDAD CON MATERIALES Y RESIDUOS PELIGROSOS - NIVEL 1,2 y 3",
"SUPERVISORDEALTORIESGO": "SUPERVISOR DE ALTO RIESGO",
"SUPERVISORDESEGURIDADMINEROINDUSTRIA": "SUPERVISOR DE SEGURIDAD MINERO INDUSTRIAL",
"TRABAJOSENALTURA": "TRABAJOS EN ALTURA",
};

View File

@ -3,6 +3,7 @@ import { CustomLabel } from "../types/CustomLabel";
type CustomLabelsMap = {[k: number]: CustomLabel}; type CustomLabelsMap = {[k: number]: CustomLabel};
// TODO: Refactor, this is inefficient
export const [customLabelsMap, setCustomLabelsMap] = createSignal<{[k: number]: CustomLabel}>({}); export const [customLabelsMap, setCustomLabelsMap] = createSignal<{[k: number]: CustomLabel}>({});
export function loadCustomLabels() { export function loadCustomLabels() {