Stable 2-ago-2023

This commit is contained in:
2023-08-02 09:12:46 -06:00
parent 6a7c6b7ed9
commit f0cc3c585d
60 changed files with 6497 additions and 908 deletions

237
ts/auditoría.ts Normal file
View File

@@ -0,0 +1,237 @@
import { createApp, reactive } from 'https://unpkg.com/petite-vue?module'
import { text } from 'stream/consumers';
type Registro = {
carrera: string;
carrera_id: number;
comentario: null;
dia: string;
duracion: string;
duracion_id: number;
estado_supervisor_id: number;
facultad: string;
facultad_id: number;
horario_dia: number;
horario_fin: string;
horario_grupo: string;
horario_hora: string;
horario_id: number;
materia: string;
materia_id: number;
nombre: string;
periodo: string;
periodo_id: number;
profesor_clave: string;
profesor_correo: string;
profesor_grado: null;
profesor_id: number;
profesor_nombre: string;
registro_fecha: null;
registro_fecha_ideal: Date;
registro_fecha_supervisor: Date;
registro_id: number;
registro_justificada: null;
registro_retardo: null;
salon: string;
salon_id: number;
supervisor_id: number;
}
type Estado = {
color: string;
icon: string;
estado_supervisor_id: number;
}
type Facultad = {
clave_dependencia: string;
facultad_id: number;
facultad_nombre: string;
}
type Filter = {
type: string;
value: string;
icon: string;
field: string;
label: string;
}
const store = reactive({
loading: false,
current: {
comentario: '',
clase_vista: null,
empty: '',
},
facultades: {
data: [] as Facultad[],
async fetch() {
this.data = [] as Facultad[]
const res = await fetch('action/action_facultad.php')
this.data = await res.json()
},
},
filters: {
facultad_id: null,
fecha: null,
fecha_inicio: null,
fecha_fin: null,
profesor: null,
estados: [],
switchFecha: false,
switchFechas() {
$(function () {
store.filters.fecha_inicio = store.filters.fecha_fin = store.filters.fecha = null
$("#fecha, #fecha_inicio, #fecha_fin").datepicker({
minDate: -3,
maxDate: new Date(),
dateFormat: "yy-mm-dd",
showAnim: "slide",
});
const fecha = $("#fecha"), inicio = $("#fecha_inicio"), fin = $("#fecha_fin")
inicio.on("change", function () {
store.filters.fecha_inicio = inicio.val()
fin.datepicker("option", "minDate", inicio.val());
});
fin.on("change", function () {
store.filters.fecha_fin = fin.val()
inicio.datepicker("option", "maxDate", fin.val());
});
fecha.on("change", function () {
store.filters.fecha = fecha.val()
});
});
}
},
estados: {
data: [] as Estado[],
async fetch() {
this.data = [] as Estado[]
const res = await fetch('action/action_estado_supervisor.php')
this.data = await res.json()
},
getEstado(id: number): Estado {
return this.data.find((estado: Estado) => estado.estado_supervisor_id === id)
},
printEstados() {
if (store.filters.estados.length > 0)
document.querySelector('#estados')!.innerHTML = store.filters.estados.map((estado: number) =>
`<span class="mx-2 badge badge-${store.estados.getEstado(estado).estado_color}">
<i class="${store.estados.getEstado(estado).estado_icon}"></i> ${store.estados.getEstado(estado).nombre}
</span>`
).join('')
else
document.querySelector('#estados')!.innerHTML = `Todos los registros`
}
},
toggle(arr: any, element: any) {
const newArray = arr.includes(element) ? arr.filter((item: any) => item !== element) : [...arr, element]
// if all are selected, then unselect all
if (newArray.length === this.estados.data.length) return []
return newArray
},
})
declare var $: any
type Profesor = {
profesor_id: number;
profesor_nombre: string;
profesor_correo: string;
profesor_clave: string;
profesor_grado: string;
}
createApp({
store,
get clase_vista() {
return store.current.clase_vista
},
registros: {
data: [] as Registro[],
async fetch() {
this.loading = true
this.data = [] as Registro[]
const res = await fetch('action/action_auditoria.php')
this.data = await res.json()
this.loading = false
},
invertir() {
this.data = this.data.reverse()
},
mostrarComentario(registro_id: number) {
const registro = this.data.find((registro: Registro) => registro.registro_id === registro_id)
store.current.comentario = registro.comentario
$('#ver-comentario').modal('show')
},
get relevant() {
/*
facultad_id: null,
fecha: null,
fecha_inicio: null,
fecha_fin: null,
profesor: null,
asistencia: null,
estado_id: null,
if one of the filters is null, then it is not relevant
*/
const filters = Object.keys(store.filters).filter((filtro) => store.filters[filtro] || store.filters[filtro]?.length > 0)
return this.data.filter((registro: Registro) => {
return filters.every((filtro) => {
switch (filtro) {
case 'fecha':
return registro.registro_fecha_ideal === store.filters[filtro];
case 'fecha_inicio':
return registro.registro_fecha_ideal >= store.filters[filtro];
case 'fecha_fin':
return registro.registro_fecha_ideal <= store.filters[filtro];
case 'profesor':
const textoFiltro = store.filters[filtro].toLowerCase();
if (/^\([^)]+\)\s[\s\S]+$/.test(textoFiltro)) {
const clave = registro.profesor_clave.toLowerCase();
const filtroClave = textoFiltro.match(/\((.*?)\)/)?.[1];
console.log(clave, filtroClave);
return clave.includes(filtroClave);
} else {
const nombre = registro.profesor_nombre.toLowerCase();
return nombre.includes(textoFiltro);
}
case 'facultad_id':
return registro.facultad_id === store.filters[filtro];
case 'estados':
if (store.filters[filtro].length === 0) return true;
return store.filters[filtro].includes(registro.estado_supervisor_id);
default:
return true;
}
})
})
},
},
get profesores() {
return this.registros.data.map((registro: Registro) => (
{
profesor_id: registro.profesor_id,
profesor_nombre: registro.profesor_nombre,
profesor_correo: registro.profesor_correo,
profesor_clave: registro.profesor_clave,
profesor_grado: registro.profesor_grado,
}
)).sort((a: Profesor, b: Profesor) =>
a.profesor_nombre.localeCompare(b.profesor_nombre)
)
},
async mounted() {
await this.registros.fetch()
await store.facultades.fetch()
await store.estados.fetch()
store.filters.switchFechas()
}
}).mount('#app')

149
ts/client.ts Normal file
View File

@@ -0,0 +1,149 @@
// @ts-ignore Import module
import { createApp, reactive } from 'https://unpkg.com/petite-vue?module'
export interface PeridoV1 {
IdNivel: number;
IdPeriodo: number;
NombreNivel: string;
NombrePeriodo: string;
in_db: boolean;
linked: boolean;
}
export interface PeridoV2 {
ClaveCarrera: string;
FechaFin: string;
FechaInicio: string;
IdPeriodo: number;
NombreCarrera: string;
}
const webServices = {
getPeriodosV1: async (): Promise<PeridoV1[]> => {
try {
const response = await fetch('periodos.v1.php');
return await response.json();
} catch (error) {
console.log(error);
return [];
}
},
getPeriodosV2: async (): Promise<PeridoV2[]> => {
try {
const response = await fetch('periodos.v2.php');
return await response.json();
} catch (error) {
console.log(error);
return [];
}
}
}
const store = reactive({
periodosV1: [] as PeridoV1[],
periodosV2: [] as PeridoV2[],
errors: [] as string[],
fechas(idPeriodo: number): { inicio: string, fin: string } {
const periodo = this.periodosV2.find((periodo: PeridoV2) => periodo.IdPeriodo === idPeriodo);
return {
inicio: periodo ? periodo.FechaInicio : '',
fin: periodo ? periodo.FechaFin : ''
}
},
periodov1(idPeriodo: number): PeridoV1 | undefined {
return this.periodosV1.find((periodo: PeridoV1) => periodo.IdPeriodo === idPeriodo);
},
periodov2(idPeriodo: number): PeridoV2[] {
return this.periodosV2.filter((periodo: PeridoV2) => periodo.IdPeriodo === idPeriodo);
},
async addPeriodo(periodo: PeridoV1 | PeridoV2) {
try {
const result = await fetch('backend/periodos.php', {
method: 'POST',
body: JSON.stringify({
...periodo,
...this.fechas(periodo.IdPeriodo)
}),
headers: {
'Content-Type': 'application/json'
}
}).then((response) => response.json());
if (result.success) {
this.periodosV1 = this.periodosV1.map((periodoV1: PeridoV1) => {
if (periodoV1.IdPeriodo === periodo.IdPeriodo) {
periodoV1.in_db = true;
}
return periodoV1;
});
return result;
}
else {
this.errors.push(result.message);
}
} catch (error) {
this.errors.push(error);
}
},
async addCarreras(idPeriodo: number) {
try {
const periodoV1 = this.periodov1(idPeriodo) as PeridoV1;
const periodoV2 = this.periodov2(idPeriodo) as PeridoV2[];
const data = periodoV2.map(({ ClaveCarrera, NombreCarrera }: PeridoV2) =>
({
ClaveCarrera: ClaveCarrera,
NombreCarrera: NombreCarrera,
IdNivel: periodoV1.IdNivel,
})
);
const result = await fetch('backend/carreras.php', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
}).then((response) => response.json());
if (result.success) {
await webServices.getPeriodosV1().then((periodosV1) => {
this.periodosV1 = periodosV1;
});
await webServices.getPeriodosV2().then((periodosV2) => {
this.periodosV2 = periodosV2;
});
}
} catch (error) {
this.errors.push(error);
}
}
})
createApp({
store,
info(IdPeriodo: number): PeridoV2 {
const periodo = store.periodosV2.find((periodo: PeridoV2) => periodo.IdPeriodo === IdPeriodo &&
periodo.FechaInicio != '' && periodo.FechaFin != '');
return periodo
},
complete(IdPeriodo: number): boolean {
const info = this.info(IdPeriodo);
return info !== undefined;
},
mounted: async () => {
await webServices.getPeriodosV1().then((periodosV1) => {
store.periodosV1 = periodosV1;
});
await webServices.getPeriodosV2().then((periodosV2) => {
store.periodosV2 = periodosV2;
});
}
}).mount()

View File

@@ -1,347 +0,0 @@
// initial state
const días = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"];
const horas_estándar = /* range from 7 to 22 */ Array.from(Array(22 - 7 + 1).keys()).map(x => x + 7);
// fill the table with empty cells
for (let i = 0; i < horas_estándar.length - 1; i++) {
const hora = horas_estándar[i];
const tr = document.createElement("tr");
tr.id = `hora-${hora}-00`;
tr.classList.add(hora > 13 ? "tarde" : "mañana");
const th = document.createElement("th");
th.classList.add("text-center");
th.scope = "row";
th.rowSpan = 4;
th.innerText = `${hora}:00`;
th.style.verticalAlign = "middle";
tr.appendChild(th);
for (let j = 0; j < días.length; j++) {
const día = días[j];
const td = document.createElement("td");
td.id = `hora-${hora}-00-${día}`;
tr.appendChild(td);
}
document.querySelector("tbody#horario")?.appendChild(tr);
// add 7 rows for each hour
const hours = [15, 30, 45];
for (let j = 1; j < 4; j++) {
const tr = document.createElement("tr");
tr.id = `hora-${hora}-${hours[j - 1]}`;
tr.classList.add(hora > 13 ? "tarde" : "mañana");
for (let k = 0; k < días.length; k++) {
const día = días[k];
const td = document.createElement("td");
td.id = `hora-${hora}-${hours[j - 1]}-${día}`;
// td.innerText = `hora-${hora}-${hours[j - 1]}-${día}`;
tr.appendChild(td);
}
document.querySelector("tbody#horario")?.appendChild(tr);
}
}
// add an inital height to the table cells
const tds = document.querySelectorAll<HTMLTableRowElement>("tbody#horario td");
tds.forEach(td => td.style.height = "2rem");
var table = document.querySelector("table") as HTMLTableElement;
var empty_table = table?.innerHTML || "";
// hide the table
table.style.display = "none";
document.getElementById('dlProfesor')?.addEventListener('input', function () {
var input = document.getElementById('dlProfesor') as HTMLInputElement;
var value = input.value;
var option = document.querySelector(`option[value="${value}"]`);
if (option) {
var id: string = option.getAttribute('data-id')!;
const input_profesor: HTMLInputElement = document.getElementById('editor_profesor') as HTMLInputElement;
input_profesor.value = id;
// remove is invalid class
input.classList.remove("is-invalid");
// add is valid class
input.classList.add("is-valid");
} else {
const input_profesor: HTMLInputElement = document.getElementById('editor_profesor') as HTMLInputElement;
input_profesor.value = "";
// remove is valid class
input.classList.remove("is-valid");
// add is invalid class
input.classList.add("is-invalid");
}
});
/**
* Functions and Methods
**/
const buscarGrupo = async () => {
// Add loading animation in the button
const btn = document.querySelector("#btn-buscar") as HTMLButtonElement;
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Cargando...';
btn.disabled = true;
const carrera = document.querySelector<HTMLInputElement>("#filter_carrera")?.value as string;
const grupo = document.querySelector<HTMLInputElement>("#filter_grupo")?.value as string;
console.log(`Carrera: ${carrera}, Grupo: ${grupo}`);
if (carrera == "" || grupo == "") {
triggerMessage("El nombre del grupo y la carrera son requeridos", "Faltan campos");
// Remove loading animation in the button
btn.innerHTML = '<i class="ing-buscar ing"></i> Buscar';
btn.disabled = false;
return;
}
const formData = new FormData();
formData.append("carrera", carrera);
formData.append("grupo", grupo);
formData.append("periodo", document.querySelector<HTMLInputElement>("#periodo")?.value!);
const thisScript = document.currentScript as HTMLScriptElement;
const facultad = thisScript.getAttribute("data-facultad") as string;
formData.append('facultad', facultad);
try {
const response = await fetch("action/action_horario.php", {
method: "POST",
body: formData
}).then(res => res.json());
if (response.status == "success") {
let limits = {
min: 22,
max: 7
};
let sábado = false;
const horario = response.horario;
// show the table
table.style.display = "table";
// clear the table
table.innerHTML = empty_table;
// fill the table
for (let i = 0; i < horario.length; i++) {
const dia = horario[i].dia;
if (dia == "sábado") sábado = true;
const {
hora,
minutos
} = {
hora: parseInt(horario[i].hora.split(":")[0]),
minutos: horario[i].hora.split(":")[1]
}
// update the limits
if (hora < limits.min) {
limits.min = hora;
}
if (hora > limits.max) {
limits.max = hora;
}
const materia = horario[i].materia;
const profesor = horario[i].profesor;
const salon = horario[i].salon;
const id = horario[i].horario_id;
const prof_id = horario[i].profesor_id;
const cell = document.querySelector(`#hora-${hora}-${minutos}-${dia}`) as HTMLTableCellElement;
cell.innerHTML =
`
<div>
<small class="text-gray">${hora}:${minutos}</small>
<b class="title">${materia}</b> <br>
<br><span>Salón: </span>${salon} <br>
<span class="ing ing-formacion mx-1"></span>${profesor}
</div>
`;
const html = `<div class="menu-flotante p-2">
<a
class="mx-2"
href="#"
data-toggle="modal"
data-target="#modal-editar"
data-dia="${dia}"
data-hora="${hora}:${minutos}"
data-materia="${materia}"
data-profesor="${prof_id}"
data-salon="${salon}"
data-id="${id}"
>
<i class="ing-editar ing"></i>
</a>
<a
class="mx-2"
href="#"
data-toggle="modal"
data-target="#modal-borrar"
data-hoario_id="${id}"
>
<i class="ing-basura ing"></i>
</a>
</div>`;
const td = cell.closest("td") as HTMLTableCellElement;
td.innerHTML += html;
td.classList.add("position-relative");
// this cell spans 4 rows
cell.rowSpan = 6;
cell.classList.add("bloque-clase", "overflow");
for (let j = 1; j < 6; j++) {
const minute = (parseInt(minutos) + j * 15)
const next_minute = (minute % 60).toString().padStart(2, "0");
const next_hour = (hora + Math.floor(minute / 60))
const next_cell = document.querySelector(`#hora-${next_hour}-${next_minute}-${dia}`) as HTMLTableCellElement;
next_cell.remove();
}
}
// remove the elements that are not in the limits
const horas = document.querySelectorAll("tbody#horario tr") as NodeListOf<HTMLTableRowElement>;
for (let i = 0; i < horas.length; i++) {
const hora = horas[i];
const hora_id = parseInt(hora.id.split("-")[1]);
if (hora_id < limits.min || hora_id > limits.max) {
hora.remove();
}
}
// if there is no sábado, remove the column
if (!sábado) {
document.querySelectorAll("tbody#horario td").forEach(td => {
if (td.id.split("-")[3] == "sábado") {
td.remove();
}
});
// remove the header (the last)
const headers = document.querySelector("#headers") as HTMLTableRowElement;
headers.lastElementChild?.remove();
}
// adjust width
const ths = document.querySelectorAll("tr#headers th") as NodeListOf<HTMLTableHeaderCellElement>;
const width = 95 / (ths.length - 1);
ths.forEach((th, key) =>
th.style.width = (key == 0) ? "5%" : `${width}%`
);
// search item animation
const menúFlontantes = document.querySelectorAll(".menu-flotante");
menúFlontantes.forEach((element) => {
element.classList.add("d-none");
element.parentElement?.addEventListener("mouseover", () => {
element.classList.remove("d-none");
});
element.parentElement?.addEventListener("mouseout", () => {
element.classList.add("d-none");
});
});
} else {
triggerMessage(response.message, "Error");
// Remove loading animation in the button
btn.innerHTML = '<i class="ing-buscar ing"></i> Buscar';
btn.disabled = false;
}
} catch (error) {
triggerMessage("Error al cargar el horario", "Error");
triggerMessage(error, "Error");
}
// Remove loading animation in the button
btn.innerHTML = '<i class="ing-buscar ing"></i> Buscar';
btn.disabled = false;
}
async function guardar(id: string) {
interface Data {
hora: HTMLInputElement;
dia: HTMLInputElement;
profesor: HTMLInputElement;
salon: HTMLInputElement;
}
const btn: HTMLButtonElement = document.querySelector("#btn-guardar") as HTMLButtonElement;
const clone: HTMLButtonElement = btn.cloneNode(true) as HTMLButtonElement;
btn.innerHTML = '<i class="ing-cargando ing"></i> Guardando...';
btn.disabled = true;
const data: Data = {
hora: document.querySelector("#editor_hora") as HTMLInputElement,
dia: document.querySelector("#editor_dia") as HTMLInputElement,
salon: document.querySelector("#editor_salón") as HTMLInputElement,
profesor: document.querySelector("#editor_profesor") as HTMLInputElement,
};
const hora = data.hora.value; // h:mm
const { compareHours } = await import('./date_functions');
const hora_antes = compareHours(hora, "07:15") < 0;
const hora_después = compareHours(hora, "21:30") > 0;
if (hora_antes || hora_después) {
alert(`La hora ${hora} no es válida`);
triggerMessage("Selecciona una hora", "Error");
btn.innerHTML = clone.innerHTML;
btn.disabled = false;
data.hora.focus();
data.hora.classList.add("is-invalid");
return;
}
const dia = data.dia.value;
const salon = data.salon.value;
const profesor = data.profesor.value;
const formData = new FormData();
formData.append("id", id);
formData.append("hora", hora);
formData.append("dia", dia);
formData.append("salon", salon);
formData.append("profesor", profesor);
const response = await fetch("action/action_horario_update.php", {
method: "POST",
body: formData
}).then(res => res.json());
if (response.status == "success") {
triggerMessage(response.message, "Éxito", "success");
btn.innerHTML = '<i class="ing-aceptar ing"></i> Guardado';
btn.classList.add("btn-success");
btn.classList.remove("btn-primary");
// return to the initial state
setTimeout(() => {
buscarGrupo();
btn.replaceWith(clone);
$("#modal-editar").modal("hide");
}, 1000);
} else {
triggerMessage(response.message, "Error");
btn.replaceWith(clone);
$("#modal-editar").modal("hide");
}
}
function triggerMessage(message: string, header: string, colour: string = "danger") {
throw new Error('Function not implemented.');
}

View File

@@ -1,107 +0,0 @@
// get this script tag
const script = document.currentScript as HTMLScriptElement;
// get data-facultad attribute from script tag
const dataFacultad = script.getAttribute("data-facultad") as string;
const table = document.querySelector("table") as HTMLTableElement;
// hide the table
table.style.display = "none";
disableDatalist("#filter_grupo");
const buscarGrupo = async () => {
// Add loading animation in the button
const btn = document.querySelector("#btn-buscar") as HTMLButtonElement;
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Cargando...';
btn.disabled = true;
const carrera = document.querySelector("#filter_carrera") as HTMLInputElement;
const grupo = document.querySelector("#filter_grupo") as HTMLInputElement;
const periodo = document.querySelector("#periodo") as HTMLInputElement;
console.log(`Carrera: ${carrera}, Grupo: ${grupo}`);
if (carrera.value == "" || grupo.value == "") {
triggerMessage("El nombre del grupo y la carrera son requeridos", "Faltan campos");
// Remove loading animation in the button
btn.innerHTML = '<i class="ing-buscar ing"></i> Buscar';
btn.disabled = false;
return;
}
const formData = new FormData();
formData.append("carrera", carrera.value);
formData.append("grupo", grupo.value);
formData.append("periodo", periodo.value);
formData.append('facultad', dataFacultad);
try {
const response = await fetch("api/horario.php", {
method: "POST",
body: formData
}).then(res => res.json());
} catch (error) {
triggerMessage("Error al cargar el horario", "Error");
}
// Remove loading animation in the button
btn.innerHTML = '<i class="ing-buscar ing"></i> Buscar';
btn.disabled = false;
}
// on click the li element, inside datalist #dlcarera
const dlcarreras = document.querySelectorAll("#dlcarrera li");
dlcarreras.forEach(li => {
li.addEventListener("click", async () => {
// get the data-id from the li element
const carrera= li.getAttribute("data-id") as string;
const facultad = dataFacultad;
const periodo = document.querySelector("#periodo") as HTMLSelectElement;
const formData = new FormData();
formData.append("carrera", carrera);
formData.append("facultad", facultad);
formData.append("periodo", periodo.value);
try {
const {
status,
grupos
} = await fetch("action/action_grupo.php", {
method: "POST",
body: formData
}).then(res => res.json());
if (status != "success") {
throw new Error("Error al cargar los grupos");
}
const dlgrupo = document.querySelector("#dlgrupo ul") as HTMLUListElement;
const prompt = document.querySelector("#dlgrupo .datalist-input") as HTMLInputElement;
dlgrupo.innerHTML = "";
grupos.forEach(grupo => {
const li = document.createElement("li");
// data-id is the id of the group
li.setAttribute("data-id", grupo);
li.textContent = grupo;
dlgrupo.appendChild(li);
});
// write Seleccionar grupo
prompt.textContent = "Seleccionar grupo";
disableDatalist("#filter_grupo", false);
} catch (error) {
triggerMessage("Error al cargar los grupos", "Error");
console.log(error);
}
});
});

View File

@@ -1,10 +0,0 @@
// function that receives two hours in hh:mm format and compares as a spaceship operator
export function compareHours(h1: string, h2: string): number {
const [h1h, h1m] = h1.split(":").map(Number);
const [h2h, h2m] = h2.split(":").map(Number);
if (h1h > h2h) return 1;
if (h1h < h2h) return -1;
if (h1m > h2m) return 1;
if (h1m < h2m) return -1;
return 0;
}

1
ts/declaration.ts Normal file
View File

@@ -0,0 +1 @@
declare module 'https://*'

532
ts/horario_profesor.ts Normal file
View File

@@ -0,0 +1,532 @@
declare function triggerMessage(message: string, title: string, color?: string): void;
declare const write: boolean;
declare const moment: any;
/**
* Funciones auxiliares
*/
type Profesor = {
id: number,
grado: string,
profesor: string,
clave: string,
}
type Horario = {
id: number,
carrera_id: number,
materia: string,
salon: string,
profesores: Profesor[],
hora: string,
hora_final: string,
dia: string,
duracion: number,
bloques: number,
grupo: string,
materia_id: number,
}
const compareHours = (hora1: string, hora2: string): number => {
const [h1, m1] = hora1.split(":").map(Number);
const [h2, m2] = hora2.split(":").map(Number);
if (h1 !== h2) {
return h1 > h2 ? 1 : -1;
}
if (m1 !== m2) {
return m1 > m2 ? 1 : -1;
}
return 0;
};
let horarios = [] as Horario[];
const table = document.querySelector("table") as HTMLTableElement;
if (!(table instanceof HTMLTableElement)) {
triggerMessage("No se ha encontrado la tabla", "Error", "error");
throw new Error("No se ha encontrado la tabla");
}
[...Array(16).keys()].map(x => x + 7).forEach(hora => {
// add 7 rows for each hour
[0, 15, 30, 45].map((minute: number) => `${minute}`.padStart(2, '0')).forEach((minute: string) => {
const tr = document.createElement("tr") as HTMLTableRowElement;
tr.id = `hora-${hora}:${minute}`;
tr.classList.add(hora > 13 ? "tarde" : "mañana");
if (minute == "00") {
const th = document.createElement("th") as HTMLTableCellElement;
th.classList.add("text-center");
th.scope = "row";
th.rowSpan = 4;
th.innerText = `${hora}:00`;
th.style.verticalAlign = "middle";
tr.appendChild(th);
}
["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"].forEach(día => {
const td = document.createElement("td") as HTMLTableCellElement;
td.id = `hora-${hora}:${minute}-${día}`;
tr.appendChild(td);
});
const tbody = document.querySelector("tbody#horario") as HTMLTableSectionElement;
if (!(tbody instanceof HTMLTableSectionElement)) {
throw new Error("No se ha encontrado el tbody");
}
tbody.appendChild(tr);
});
});
const empty_table = table.cloneNode(true) as HTMLTableElement;
document.querySelectorAll('.hidden').forEach((element: HTMLElement) => {
element.style.display = "none";
});
// hide the table
table.style.display = "none";
function moveHorario(id: string, día: string, hora: string) {
const formData = new FormData();
formData.append("id", id);
formData.append("hora", hora);
formData.append("día", día);
fetch("action/action_horario_update.php", {
method: "POST",
body: formData
}).then(res => res.json()).then(response => {
if (response.status == "success") {
triggerMessage("Horario movido", "Éxito", "success");
} else {
triggerMessage(response.message, "Error");
}
}).then(() => {
renderHorario();
}).catch(err => {
triggerMessage(err, "Error");
});
}
function renderHorario() {
if (horarios.length == 0) {
triggerMessage("Este profesor hay horarios para mostrar", "Error", "info");
table.style.display = "none";
document.querySelectorAll('.hidden').forEach((element: HTMLElement) => element.style.display = "none");
return;
}
// show the table
table.style.display = "table";
document.querySelectorAll('.hidden').forEach((element: HTMLElement) => element.style.display = "block");
// clear the table
table.innerHTML = empty_table.outerHTML;
function conflicts(horario1: Horario, horario2: Horario): boolean {
const { hora: hora_inicio1, hora_final: hora_final1, dia: dia1 } = horario1;
const { hora: hora_inicio2, hora_final: hora_final2, dia: dia2 } = horario2;
if (dia1 !== dia2) {
return false;
}
const compareInicios = compareHours(hora_inicio1, hora_inicio2);
const compareFinales = compareHours(hora_final1, hora_final2);
if (
compareInicios >= 0 && compareInicios <= compareFinales ||
compareFinales >= 0 && compareFinales <= -compareInicios
) {
return true;
}
return false;
}
// remove the next 5 cells
function removeNextCells(horas: number, minutos: number, dia: string, cells: number = 5) {
for (let i = 1; i <= cells; i++) {
const minute = minutos + i * 15;
const nextMinute = (minute % 60).toString().padStart(2, "0");
const nextHour = horas + Math.floor(minute / 60);
const cellId = `hora-${nextHour}:${nextMinute}-${dia}`;
const cellElement = document.getElementById(cellId);
if (cellElement) {
cellElement.remove();
}
else {
console.log(`No se ha encontrado la celda ${cellId}`);
break;
}
}
}
function newBlock(horario: Horario, edit = false) {
function move(horario: Horario, cells: number = 5) {
const [horas, minutos] = horario.hora.split(":").map(Number);
const cell = document.getElementById(`hora-${horas}:${minutos.toString().padStart(2, "0")}-${horario.dia}`);
const { top, left } = cell.getBoundingClientRect();
const block = document.getElementById(`block-${horario.id}`);
block.style.top = `${top}px`;
block.style.left = `${left}px`;
removeNextCells(horas, minutos, horario.dia, cells);
}
const [horas, minutos] = horario.hora.split(":").map(x => parseInt(x));
const hora = `${horas}:${minutos.toString().padStart(2, "0")}`;
horario.hora = hora;
const cell = document.getElementById(`hora-${horario.hora}-${horario.dia}`) as HTMLTableCellElement;
if (!cell) return;
cell.dataset.ids = `${horario.id}`;
const float_menu = edit ?
`<div class="menu-flotante p-2" style="opacity: .7;">
<a class="mx-2" href="#" data-toggle="modal" data-target="#modal-editar">
<i class="ing-editar ing"></i>
</a>
<a class="mx-2" href="#" data-toggle="modal" data-target="#modal-borrar">
<i class="ing-basura ing"></i>
</a>
</div>`
: '';
cell.innerHTML =
`<div style="overflow-y: auto; overflow-x: hidden; height: 100%;" id="block-${horario.id}" class="position-absolute w-100 h-100">
<small class="text-gray">${horario.hora}</small>
<b class="title">${horario.materia}</b> <br>
<br><span>Salón: </span>${horario.salon} <br>
<small class="my-2">
${horario.profesores.map((profesor: Profesor) => ` <span class="ing ing-formacion mx-1"></span>${profesor.grado ?? ''} ${profesor.profesor}`).join("<br>")}
</small>
</div>
${float_menu}`;
cell.classList.add("bloque-clase", "position-relative");
cell.rowSpan = horario.bloques;
// draggable
cell.draggable = write;
if (horario.bloques > 0) {
removeNextCells(horas, minutos, horario.dia, horario.bloques - 1);
}
}
function newConflictBlock(horarios: Horario[], edit = false) {
const first_horario = horarios[0];
const [horas, minutos] = first_horario.hora.split(":").map(x => parseInt(x));
const hora = `${horas}:${minutos.toString().padStart(2, "0")}`;
const ids = horarios.map(horario => horario.id);
const cell = document.getElementById(`hora-${hora}-${first_horario.dia}`);
if (cell == null) {
console.error(`Error: No se encontró la celda: hora-${hora}-${first_horario.dia}`);
return;
}
cell.dataset.ids = ids.join(",");
// replace the content of the cell
cell.innerHTML = `
<small class='text-danger'>
${hora}
</small>
<div class="d-flex justify-content-center align-items-center mt-4">
<div class="d-flex flex-column justify-content-center align-items-center">
<span class="ing ing-importante text-danger" style="font-size: 2rem;"></span>
<b class='text-danger'>
Empalme de ${ids.length} horarios
</b>
<hr>
<i class="text-danger">Ver horarios &#8230;</i>
</div>
</div>
`;
// Add classes and attributes
cell.classList.add("conflict", "bloque-clase");
cell.setAttribute("role", "button");
// Add event listener for the cell
cell.addEventListener("click", () => {
$("#modal-choose").modal("show");
const ids = cell.getAttribute("data-ids").split(",").map(x => parseInt(x));
const tbody = document.querySelector("#modal-choose tbody");
tbody.innerHTML = "";
horarios.filter(horario => ids.includes(horario.id)).sort((a, b) => compareHours(a.hora, b.hora)).forEach(horario => {
tbody.innerHTML += `
<tr data-ids="${horario.id}">
<td><small>${horario.hora.slice(0, -3)}-${horario.hora_final.slice(0, -3)}</small></td>
<td>${horario.materia}</td>
<td>
${horario.profesores.map(({ grado, profesor }) => `${grado ?? ''} ${profesor}`).join(", ")}
</td>
<td>${horario.salon}</td>
${edit ? `
<td class="text-center">
<button class="btn btn-sm btn-primary dismiss-editar" data-toggle="modal" data-target="#modal-editar">
<i class="ing-editar ing"></i>
</button>
</td>
<td class="text-center">
<button class="btn btn-sm btn-danger dismiss-editar" data-toggle="modal" data-target="#modal-borrar">
<i class="ing-basura ing"></i>
</button>
</td>
` : ""}
</tr>`;
});
document.querySelectorAll(".dismiss-editar").forEach(btn => {
btn.addEventListener("click", () => $("#modal-choose").modal("hide"));
});
});
function getDuration(hora_i: string, hora_f: string): number {
const [horas_i, minutos_i] = hora_i.split(":").map(x => parseInt(x));
const [horas_f, minutos_f] = hora_f.split(":").map(x => parseInt(x));
const date_i = new Date(0, 0, 0, horas_i, minutos_i);
const date_f = new Date(0, 0, 0, horas_f, minutos_f);
const diffInMilliseconds = date_f.getTime() - date_i.getTime();
const diffInMinutes = diffInMilliseconds / (1000 * 60);
const diffIn15MinuteIntervals = diffInMinutes / 15;
return Math.floor(diffIn15MinuteIntervals);
}
const maxHoraFinal = horarios.reduce((max: Date, horario: Horario) => {
const [horas, minutos] = horario.hora_final.split(":").map(x => parseInt(x));
const date = new Date(0, 0, 0, horas, minutos);
return date > max ? date : max;
}, new Date(0, 0, 0, 0, 0));
const horaFinalMax = new Date(0, 0, 0, maxHoraFinal.getHours(), maxHoraFinal.getMinutes());
const blocks = getDuration(first_horario.hora, `${horaFinalMax.getHours()}:${horaFinalMax.getMinutes()}`);
cell.setAttribute("rowSpan", blocks.toString());
removeNextCells(horas, minutos, first_horario.dia, blocks - 1);
}
const conflictBlocks = horarios.filter((horario, index, arrayHorario) =>
arrayHorario.filter((_, i) => i != index).some(horario2 =>
conflicts(horario, horario2)))
.sort((a, b) => compareHours(a.hora, b.hora));
const classes = horarios.filter(horario => !conflictBlocks.includes(horario));
const conflictBlocksPacked = []; // array of sets
conflictBlocks.forEach(horario => {
const setIndex = conflictBlocksPacked.findIndex(set => set.some(horario2 => conflicts(horario, horario2)));
if (setIndex === -1) {
conflictBlocksPacked.push([horario]);
} else {
conflictBlocksPacked[setIndex].push(horario);
}
})
classes.forEach(horario =>
newBlock(horario, write)
)
conflictBlocksPacked.forEach(horarios =>
newConflictBlock(horarios, write)
)
// remove the elements that are not in the limits
let max_hour = Math.max(...horarios.map(horario => {
const lastMoment = moment(horario.hora, "HH:mm").add(horario.bloques * 15, "minutes");
const lastHour = moment(`${lastMoment.hours()}:00`, "HH:mm");
const hourInt = parseInt(lastMoment.format("HH"));
return lastMoment.isSame(lastHour) ? hourInt - 1 : hourInt;
}));
let min_hour = Math.min(...horarios.map(horario => parseInt(horario.hora.split(":")[0])));
document.querySelectorAll("tbody#horario tr").forEach(hora => {
const hora_id = parseInt(hora.id.split("-")[1].split(":")[0]);
(hora_id < min_hour || hora_id > max_hour) ? hora.remove() : null;
})
// if there is no sábado, remove the column
if (!horarios.some(horario => horario.dia == "sábado")) {
document.querySelectorAll("tbody#horario td").forEach(td => {
if (td.id.split("-")[2] == "sábado") {
td.remove();
}
});
// remove the header (the last)
document.querySelector("#headers").lastElementChild.remove();
}
// adjust width
const ths = document.querySelectorAll("tr#headers th") as NodeListOf<HTMLTableCellElement>;
ths.forEach((th, key) =>
th.style.width = (key == 0) ? "5%" : `${95 / (ths.length - 1)}%`
);
// search item animation
const menúFlontantes = document.querySelectorAll(".menu-flotante");
menúFlontantes.forEach((element) => {
element.classList.add("d-none");
element.parentElement.addEventListener("mouseover", () =>
element.classList.remove("d-none")
);
element.parentElement.addEventListener("mouseout", (e) =>
element.classList.add("d-none")
);
});
// droppables
// forall the .bloque-elements add the event listeners for drag and drop
document.querySelectorAll(".bloque-clase").forEach(element => {
function dragStart() {
this.classList.add("dragging");
}
function dragEnd() {
this.classList.remove("dragging");
}
element.addEventListener("dragstart", dragStart);
element.addEventListener("dragend", dragEnd);
});
// forall the cells that are not .bloque-clase add the event listeners for drag and drop
document.querySelectorAll("td:not(.bloque-clase)").forEach(element => {
function dragOver(e) {
e.preventDefault();
this.classList.add("dragging-over");
}
function dragLeave() {
this.classList.remove("dragging-over");
}
function drop() {
this.classList.remove("dragging-over");
const dragging = document.querySelector(".dragging");
const id = dragging.getAttribute("data-ids");
const hora = this.id.split("-")[1];
const días = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado"];
let día = this.id.split("-")[2];
día = días.indexOf(día) + 1;
// rowspan
const bloques = parseInt(dragging.getAttribute("rowspan"));
const horaMoment = moment(hora, "HH:mm");
const horaFin = horaMoment.add(bloques * 15, "minutes");
const limit = moment('22:00', 'HH:mm');
if (horaFin.isAfter(limit)) {
triggerMessage("No se puede mover el bloque a esa hora", "Error");
// scroll to the top
window.scrollTo(0, 0);
return;
}
// get the horario
// remove the horario
const bloque = document.querySelector(`.bloque-clase[data-ids="${id}"]`) as HTMLElement;
// remove all children
while (bloque.firstChild) {
bloque.removeChild(bloque.firstChild);
}
// prepend a loading child
const loading = `<div class="spinner-border" role="status" style="width: 3rem; height: 3rem;">
<span class="sr-only">Loading...</span>
</div>`;
bloque.insertAdjacentHTML("afterbegin", loading);
// add style vertical-align: middle
bloque.style.verticalAlign = "middle";
bloque.classList.add("text-center");
// remove draggable
bloque.removeAttribute("draggable");
moveHorario(id, día, hora);
}
element.addEventListener("dragover", dragOver);
element.addEventListener("dragleave", dragLeave);
element.addEventListener("drop", drop);
});
}
const form = document.getElementById('form') as HTMLFormElement;
if (!(form instanceof HTMLFormElement)) {
triggerMessage('No se ha encontrado el formulario', 'Error', 'danger');
throw new Error("No se ha encontrado el formulario");
}
form.querySelector('#clave_profesor').addEventListener('input', function (e) {
const input = form.querySelector('#clave_profesor') as HTMLInputElement;
const option = form.querySelector(`option[value="${input.value}"]`) as HTMLOptionElement;
if (input.value == "") {
input.classList.remove("is-invalid", "is-valid");
return;
}
if (!option) {
input.classList.remove("is-valid");
input.classList.add("is-invalid");
}
else {
const profesor_id = form.querySelector('#profesor_id') as HTMLInputElement;
profesor_id.value = option.dataset.id;
input.classList.remove("is-invalid");
input.classList.add("is-valid");
}
});
form.addEventListener('submit', async function (e) {
e.preventDefault();
const input = form.querySelector('#clave_profesor') as HTMLInputElement;
if (input.classList.contains("is-invalid")) {
triggerMessage('El profesor no se encuentra registrado', 'Error', 'danger');
return;
}
const formData = new FormData(form);
try {
const buttons = document.querySelectorAll("button") as NodeListOf<HTMLButtonElement>;
buttons.forEach(button => {
button.disabled = true;
button.classList.add("disabled");
});
const response = await fetch('action/action_horario_profesor.php', {
method: 'POST',
body: formData,
});
const data = await response.json();
buttons.forEach(button => {
button.disabled = false;
button.classList.remove("disabled");
});
if (data.status == 'success') {
horarios = data.data;
renderHorario();
}
else {
triggerMessage(data.message, 'Error en la consulta', 'warning');
}
} catch (error) {
triggerMessage('Fallo al consutar los datos ', 'Error', 'danger');
console.log(error);
}
});
const input = form.querySelector('#clave_profesor') as HTMLInputElement;
const option = form.querySelector(`option[value="${input.value}"]`) as HTMLOptionElement;

272
ts/reposiciones.ts Normal file
View File

@@ -0,0 +1,272 @@
import { type } from "os";
declare function triggerMessage(message: string, title: string, type?: string): void;
declare const write: boolean;
declare const moment: any;
// from this 'horario_id', 'fecha', 'hora', 'duracion_id', 'descripcion', 'profesor_id', 'salon', 'unidad', 'periodo_id', 'fecha_clase' make a type of ReposicionParams
export interface ReposicionParams {
horario_id: number;
fecha: string;
hora: string;
duracion_id: number;
descripcion: string;
profesor_id: number;
salon: string;
unidad: number;
periodo_id: number;
fecha_clase: string;
}
type Horario = {
id: number;
carrera_id: number;
materia_id: number;
grupo: string;
profesores: Profesor[];
dia: string;
hora: string;
hora_final: string;
salon: string;
fecha_inicio: string;
fecha_final: string;
fecha_carga: string;
nivel_id: number;
periodo_id: number;
facultad_id: number;
materia: string;
horas: number;
minutos: number;
duracion: number;
retardo: boolean;
original_id: number;
last: boolean;
bloques: number;
};
type Profesor = {
id: number;
clave: string;
grado: string;
profesor: string;
nombre: string;
facultad_id: number;
};
// Get references to the HTML elements
const form = document.getElementById('form') as HTMLFormElement;
const steps = Array.from(form.querySelectorAll('.step')) as HTMLElement[];
const nextButton = document.getElementById('next-button') as HTMLButtonElement;
const prevButton = document.getElementById('prev-button') as HTMLButtonElement;
let currentStep = 0;
// #clave_profesor on change => show step 2
const clave_profesor = document.getElementById('clave_profesor') as HTMLInputElement;
const horario_reponer = document.getElementById('horario_reponer') as HTMLInputElement;
const fechas_clase = document.getElementById('fechas_clase') as HTMLInputElement;
const fecha_reponer = $('#fecha_reponer') as JQuery<HTMLElement>;
const hora_reponer = $('#hora_reponer') as JQuery<HTMLElement>;
const minutos_reponer = $('#minutos_reponer') as JQuery<HTMLElement>;
clave_profesor.addEventListener('change', async () => {
const step2 = document.getElementById('step-2') as HTMLElement;
clave_profesor.disabled = true;
// get option which value is the same as clave_profesor.value
const option = document.querySelector(`option[value="${clave_profesor.value}"]`) as HTMLOptionElement;
// make a form data with #form
const profesor_id = document.getElementById('profesor_id') as HTMLInputElement;
profesor_id.value = option.dataset.id;
const formData = new FormData(form);
const response = await fetch(`./action/action_horario_profesor.php`, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data['success'] === false) {
const message = "Hubo un error al obtener los horarios del profesor."
const title = 'Error';
const color = 'danger';
triggerMessage(message, title, color);
return;
}
const horarios = data.data as Horario[];
const initial = document.createElement('option');
initial.value = '';
initial.textContent = 'Seleccione un horario';
initial.selected = true;
initial.disabled = true;
horario_reponer.innerHTML = '';
horario_reponer.appendChild(initial);
horarios.forEach((horario) => {
const dias = ['Lunes', 'Martes', 'Miercoles', 'Jueves', 'Viernes', 'Sabado', 'Domingo'];
const option = document.createElement('option');
option.value = `${horario.id}`;
// materia máx 25 caracteres, if materia.length > 25 then slice(0, 20)
const max = 25;
option.textContent = `${horario.materia.slice(0, max) + (horario.materia.length > max ? '...' : '')} - Grupo: ${horario.grupo} - ${horario.hora.slice(0, 5)}-${horario.hora_final.slice(0, 5)} - Salon: ${horario.salon} - ${horario.dia}`;
option.dataset.materia = `${horario.materia}`;
option.dataset.grupo = `${horario.grupo}`;
option.dataset.hora = `${horario.hora.slice(0, 5)}`; // slice(0, 5) => HH:MM
option.dataset.hora_final = `${horario.hora_final.slice(0, 5)}`;
option.dataset.salon = `${horario.salon}`;
option.dataset.dia = `${horario.dia}`;
option.dataset.id = `${horario.id}`;
horario_reponer.appendChild(option);
});
currentStep = 1;
step2.style.display = 'block';
prevButton.disabled = false;
});
// disable clave_profesor
// from second step to first step
prevButton.addEventListener('click', () => {
const inputs = [clave_profesor, horario_reponer, fechas_clase, fecha_reponer, hora_reponer] as HTMLInputElement[];
switch (currentStep) {
case 1:
case 2:
case 3:
const step = document.getElementById(`step-${currentStep + 1}`) as HTMLElement;
step.style.display = 'none';
inputs[currentStep - 1].disabled = false;
inputs[currentStep - 1].value = '';
if (--currentStep === 0) {
prevButton.disabled = true;
}
break;
case 4:
const step5 = document.getElementById('step-5') as HTMLElement;
step5.style.display = 'none';
fecha_reponer.prop('disabled', false);
fecha_reponer.val('');
hora_reponer.parent().removeClass('disabled');
hora_reponer.siblings('.datalist-input').text('hh');
hora_reponer.val('');
minutos_reponer.parent().removeClass('disabled');
minutos_reponer.siblings('.datalist-input').text('mm');
minutos_reponer.val('');
currentStep--;
break;
}
nextButton.disabled = true;
});
// #horario_reponer on change => show step 3
horario_reponer.addEventListener('change', async () => {
const selected = horario_reponer.querySelector(`option[value="${horario_reponer.value}"]`) as HTMLOptionElement;
horario_reponer.title = `Materia: ${selected.dataset.materia} - Grupo: ${selected.dataset.grupo} - Horario: ${selected.dataset.hora}-${selected.dataset.hora_final} - Salon: ${selected.dataset.salon} - Día: ${selected.dataset.dia}`;
const step3 = document.getElementById('step-3') as HTMLElement;
horario_reponer.disabled = true;
// make a form data with #form
const response = await fetch(`./action/action_fechas_clase.php?horario_id=${horario_reponer.value}`, {
method: 'GET',
});
const data = await response.json();
if (data['success'] === false) {
const message = "Hubo un error al obtener las fechas de clase."
const title = 'Error';
const color = 'danger';
triggerMessage(message, title, color);
return;
}
type Fecha = {
fecha: string;
dia_mes: number;
day: number;
month: number;
year: number;
}
const meses = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
const fechas = data.data as Fecha[];
const initial = document.createElement('option');
initial.value = '';
initial.textContent = 'Seleccione la fecha de la falta';
initial.selected = true;
initial.disabled = true;
fechas_clase.innerHTML = '';
fechas_clase.appendChild(initial);
fechas_clase.title = 'Seleccione la fecha de la falta';
fechas.forEach((fecha) => {
const option = document.createElement('option');
option.value = `${fecha}`;
option.textContent = `${fecha.dia_mes} de ${meses[fecha.month - 1]} de ${fecha.year}`;
fechas_clase.appendChild(option);
});
step3.style.display = 'block';
currentStep = 2;
});
// #fechas_clase on change => show step 4
fechas_clase.addEventListener('change', () => {
const step4 = document.getElementById('step-4') as HTMLElement;
step4.style.display = 'block';
fechas_clase.disabled = true;
currentStep = 3;
});
// when both #fecha_reponer and #hora_reponer are selected => show step 5
const lastStep = () => {
// timeout to wait for the value to be set
setTimeout(() => {
if (fecha_reponer.val() !== '' && hora_reponer.val() !== '' && minutos_reponer.val() !== '') {
const step5 = document.getElementById('step-5') as HTMLElement;
step5.style.display = 'block';
// disable both
fecha_reponer.prop('disabled', true);
hora_reponer.parent().addClass('disabled');
minutos_reponer.parent().addClass('disabled');
const nextButton = document.getElementById('next-button') as HTMLButtonElement;
// remove property disabled
nextButton.removeAttribute('disabled');
currentStep = 4;
}
}, 100);
}
fecha_reponer.on('change', lastStep);
// on click on the sibling ul>li of #hora_reponer and #minutos_reponer
hora_reponer.siblings('ul').children('li').on('click', lastStep);
minutos_reponer.siblings('ul').children('li').on('click', lastStep);
// Initialize the form
hideSteps();
showCurrentStep();
function hideSteps() {
steps.forEach((step) => {
step.style.display = 'none';
});
}
function showCurrentStep() {
steps[currentStep].style.display = 'block';
prevButton.disabled = currentStep === 0;
}
function handleSubmit(event: Event) {
event.preventDefault();
// Handle form submission
// You can access the form data using the FormData API or serialize it manually
}