[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 { 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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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: {
|
||||||
|
@ -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",
|
||||||
|
};
|
||||||
|
@ -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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user