[Classroom][FE] UI for classroom enrolling
This commit is contained in:
parent
60e461f3c0
commit
b162b14eb3
42
frontend/src/OnlineClassroom/ClassroomRegistration.tsx
Normal file
42
frontend/src/OnlineClassroom/ClassroomRegistration.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
96
frontend/src/OnlineClassroom/ClassroomSearchableSelect.tsx
Normal file
96
frontend/src/OnlineClassroom/ClassroomSearchableSelect.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -5,6 +5,7 @@ import { FilledCard } from "../components/FilledCard";
|
||||
import { ClassroomUserCreation } from "./ClassroomUserCreation";
|
||||
import { ClassroomVinculation } from "./ClassroomVinculation";
|
||||
import { ClassroomUserCourses } from "./ClassroomUserCourses";
|
||||
import { ClassroomRegistration } from "./ClassroomRegistration";
|
||||
|
||||
type TabType = "Vinculate" | "Create";
|
||||
|
||||
@ -14,7 +15,8 @@ export function OnlineClassroom() {
|
||||
return (
|
||||
<div class="grid grid-cols-[16rem_25rem_1fr]">
|
||||
<Search setPerson={setPerson} />
|
||||
<div>
|
||||
|
||||
<ClassroomRegistration />
|
||||
|
||||
<Show when={person() !== null && person()!.person_classroom_id === null}>
|
||||
<ClassroomUser
|
||||
@ -27,7 +29,6 @@ export function OnlineClassroom() {
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { SearchableSelect } from "./SearchableSelect";
|
||||
import { CustomLabelSelect } from "./CustomLabelSelect";
|
||||
import { courseMap } from "../../utils/allCourses";
|
||||
import { RegistrationPreview } from ".";
|
||||
import { customLabelsMap } from "../../utils/allCustomLabels";
|
||||
|
||||
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
|
||||
submitter: HTMLElement;
|
||||
@ -16,7 +17,7 @@ export function ManualRegistration(props: {
|
||||
const [count, setCount] = createSignal(0);
|
||||
const [error, setError] = createSignal("");
|
||||
|
||||
const [selectedCourseId, seSelectedCourseId] = createSignal<number | null>(null);
|
||||
const [selectedCourseId, setSelectedCourseId] = createSignal<number | null>(null);
|
||||
const [customLabel, setCustomLabel] = createSignal("");
|
||||
const [isPreview, setIsPreview] = createSignal(false);
|
||||
|
||||
@ -65,11 +66,16 @@ export function ManualRegistration(props: {
|
||||
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 = {
|
||||
courseId: subject,
|
||||
date,
|
||||
customLabel: customLabel(),
|
||||
is_preview: isPreview(),
|
||||
custom_label_id,
|
||||
};
|
||||
|
||||
props.onAdd(data);
|
||||
@ -83,7 +89,7 @@ export function ManualRegistration(props: {
|
||||
<form onsubmit={register}>
|
||||
<div class="h-52">
|
||||
<SearchableSelect
|
||||
onChange={seSelectedCourseId}
|
||||
onChange={setSelectedCourseId}
|
||||
count={count()}
|
||||
/>
|
||||
</div>
|
||||
|
@ -88,6 +88,7 @@ export function RegisterPresets(props: {
|
||||
date: dateYYYYMMDD,
|
||||
customLabel: "",
|
||||
is_preview: isPreview(),
|
||||
custom_label_id: 1,
|
||||
});
|
||||
|
||||
// Substract current date for the next course
|
||||
|
@ -26,12 +26,13 @@ export function RegisterPreview(props: {selections: Array<RegistrationPreview>,
|
||||
|
||||
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!,
|
||||
course_id: courseId,
|
||||
date,
|
||||
custom_label: customLabel,
|
||||
is_preview,
|
||||
custom_label_id,
|
||||
}));
|
||||
|
||||
const result = await createRegisters(registers);
|
||||
|
@ -12,6 +12,7 @@ export type RegistrationPreview = {
|
||||
date: string,
|
||||
customLabel: string,
|
||||
is_preview: boolean,
|
||||
custom_label_id: number,
|
||||
}
|
||||
|
||||
export function NewRegister(props: {
|
||||
|
@ -1,3 +1,48 @@
|
||||
export type ClassroomCourse = {
|
||||
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",
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ import { CustomLabel } from "../types/CustomLabel";
|
||||
|
||||
type CustomLabelsMap = {[k: number]: CustomLabel};
|
||||
|
||||
// TODO: Refactor, this is inefficient
|
||||
export const [customLabelsMap, setCustomLabelsMap] = createSignal<{[k: number]: CustomLabel}>({});
|
||||
|
||||
export function loadCustomLabels() {
|
||||
|
Loading…
Reference in New Issue
Block a user