diff --git a/.env b/.env new file mode 100644 index 0000000..15aeb4a --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +DB_HOST="72.56.71.92" +DB_PORT="5433" +DB_USER="student" +DB_PASSWORD="qL8jZQlj6YSLV4zpXaVw" +DB_NAME="unid" diff --git a/.gitignore b/.gitignore index e242433..554df8f 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c785808 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "2guys1account.quackrack-cursor" + ] +} \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/backend/config/db.js b/backend/config/db.js new file mode 100644 index 0000000..c16a2bd --- /dev/null +++ b/backend/config/db.js @@ -0,0 +1,11 @@ +require("dotenv").config(); +const { Pool } = require("pg"); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: { + rejectUnauthorized: false, + }, +}); + +module.exports = pool; \ No newline at end of file diff --git a/backend/controllers/SystemConfigController.js b/backend/controllers/SystemConfigController.js new file mode 100644 index 0000000..a27a045 --- /dev/null +++ b/backend/controllers/SystemConfigController.js @@ -0,0 +1,90 @@ +const { + getAllConfigs, + getConfigById, + createConfig, + updateConfig, + deleteConfig, + getworkdays, + updateworkday, + createworkday // ✅ CORREGIDO: faltaba esta importación +} = require('../models/SystemConfigModel'); + +// Manejador para obtener todas las configuraciones +exports.getConfigs = async (req, res) => { + try { + const configs = await getAllConfigs(); + res.json(configs); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +// Manejador para obtener una configuración por ID +exports.getConfig = async (req, res) => { + try { + const { id } = req.params; + const config = await getConfigById(id); + if (!config) return res.status(404).json({ message: 'Configuración no encontrada' }); + res.json(config); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +exports.createConfig = async (req, res) => { + try { + const newConfig = await createConfig(req.body); + res.status(201).json(newConfig); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +exports.updateConfig = async (req, res) => { + try { + const { id } = req.params; + const updated = await updateConfig(id, req.body); + res.json(updated); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +exports.deleteConfig = async (req, res) => { + try { + const { id } = req.params; + const result = await deleteConfig(id); + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +// Manejadores para los días laborales (Work_Days) +exports.getworkdays = async (req, res) => { + try { + const days = await getworkdays(); + res.json(days); + } catch (error) { + res.status(500).json({ error: 'Error al obtener días laborales' }); + } +}; + +exports.createworkday = async (req, res) => { + try { + const newDay = await createworkday(req.body); + res.status(201).json(newDay); + } catch (error) { + res.status(500).json({ error: 'Error al crear día laboral' }); + } +}; + +exports.updateworkday = async (req, res) => { + try { + const { id } = req.params; + const updated = await updateworkday(id, req.body); + res.json(updated); + } catch (error) { + res.status(500).json({ error: 'Error al actualizar día laboral' }); + } +}; \ No newline at end of file diff --git a/backend/controllers/recordsControllers.js b/backend/controllers/recordsControllers.js new file mode 100644 index 0000000..8a8dcee --- /dev/null +++ b/backend/controllers/recordsControllers.js @@ -0,0 +1,45 @@ +//controlador para registros de asistencia +const recordsModel = require('../modelos/recordsModel'); + +exports.createRecord = async (req, res) => { + try { + const record = await recordsModel.createRecord(req.body); + res.status(201).json(record); + } catch (error) { + console.error('Error al crear registro:', error.message); + + if (error.message.includes('No existe')) { + res.status(404).json({ error: error.message }); + } else if ( + error.message.includes('Ya registraste') || + error.message.includes('Primero debes registrar') || + error.message.includes('obligatoria') || + error.message.includes('tipo de registro') + ) { + res.status(400).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Error al registrar asistencia' }); + } + } +} + +exports.getRecords = async (req, res) => { + try { + const records = await recordsModel.getRecords(); + res.json(records); + } catch (error) { + console.error('Error al obtener registros:', error); + res.status(500).json({ error: 'Error al obtener registros' }); + } +} + +exports.getRecordsByMatricula = async (req, res) => { + try { + const { matricula } = req.params; + const records = await recordsModel.getRecordsByMatricula(matricula); + res.json(records); + } catch (error) { + console.error('Error al obtener registros:', error); + res.status(500).json({ error: 'Error al obtener registros' }); + } +} diff --git a/backend/controllers/reportsControllers.js b/backend/controllers/reportsControllers.js new file mode 100644 index 0000000..216d7ab --- /dev/null +++ b/backend/controllers/reportsControllers.js @@ -0,0 +1,50 @@ +const reportsModel = require('../modelos/reportsModel'); + +exports.getSummaryReport = async (req, res) => { + try { + const { startDate, endDate } = req.query; + + if (!startDate || !endDate) { + return res.status(400).json({ error: 'startDate y endDate son obligatorios' }); + } + + const data = await reportsModel.getSummaryReport(startDate, endDate); + res.json(data); + } catch (error) { + console.error('Error al generar reporte:', error.message); + + if ( + error.message.includes('formato') || + error.message.includes('inicio no puede ser mayor') + ) { + return res.status(400).json({ error: error.message }); + } + + res.status(500).json({ error: 'Error al generar reporte' }); + } +}; + +exports.getTeacherReportDetails = async (req, res) => { + try { + const { matricula } = req.params; + const { startDate, endDate } = req.query; + + if (!startDate || !endDate) { + return res.status(400).json({ error: 'startDate y endDate son obligatorios' }); + } + + const data = await reportsModel.getTeacherReportDetails(matricula, startDate, endDate); + res.json(data); + } catch (error) { + console.error('Error al generar detalle del reporte:', error.message); + + if ( + error.message.includes('formato') || + error.message.includes('inicio no puede ser mayor') + ) { + return res.status(400).json({ error: error.message }); + } + + res.status(500).json({ error: 'Error al generar detalle del reporte' }); + } +}; diff --git a/backend/controllers/schedulesController.js b/backend/controllers/schedulesController.js new file mode 100644 index 0000000..5802331 --- /dev/null +++ b/backend/controllers/schedulesController.js @@ -0,0 +1,115 @@ +const schedulesModel = require('../models/schedulesModel'); +const teachersModel = require('../models/teachesModel'); + +exports.getSchedules = async (req, res) => { + try { + const schedules = await schedulesModel.getAllSchedules(); + res.json(schedules); + } catch (error) { + console.error('Error getting schedules:', error); + res.status(500).json({ error: 'Failed to fetch schedules' }); + } +}; + +exports.getScheduleById = async (req, res) => { + try { + const { id } = req.params; + const schedule = await schedulesModel.getScheduleById(id); + if (!schedule) { + return res.status(404).json({ error: 'Schedule not found' }); + } + res.json(schedule); + } catch (error) { + console.error('Error getting schedule:', error); + res.status(500).json({ error: 'Failed to fetch schedule' }); + } +}; + +exports.getSchedulesByTeacher = async (req, res) => { + try { + const { teacherId } = req.params; + const schedules = await schedulesModel.getSchedulesByTeacher(teacherId); + res.json(schedules); + } catch (error) { + console.error('Error getting schedules by teacher:', error); + res.status(500).json({ error: 'Failed to fetch schedules by teacher' }); + } +}; + +exports.createSchedule = async (req, res) => { + try { + const { teacher_id, subject, day, start_time, end_time, room, group_name } = req.body; + + // Validate required fields + if (!teacher_id || !subject || !day || !start_time || !end_time || !room || !group_name) { + return res.status(400).json({ + error: 'Missing required fields: teacher_id, subject, day, start_time, end_time, room, group_name' + }); + } + + // Verify teacher exists + const teacher = await teachersModel.getTeacherById(teacher_id); + if (!teacher) { + return res.status(404).json({ error: 'Teacher not found' }); + } + + const schedule = await schedulesModel.createSchedule({ + teacher_id, + subject, + day, + start_time, + end_time, + room, + group_name + }); + + res.status(201).json(schedule); + } catch (error) { + console.error('Error creating schedule:', error); + res.status(500).json({ error: 'Failed to create schedule' }); + } +}; + +exports.updateSchedule = async (req, res) => { + try { + const { id } = req.params; + const { teacher_id, subject, day, start_time, end_time, room, group_name } = req.body; + + const schedule = await schedulesModel.updateSchedule(id, { + teacher_id, + subject, + day, + start_time, + end_time, + room, + group_name + }); + + res.json(schedule); + } catch (error) { + console.error('Error updating schedule:', error); + res.status(500).json({ error: 'Failed to update schedule' }); + } +}; + +exports.deleteSchedule = async (req, res) => { + try { + const { id } = req.params; + await schedulesModel.deleteSchedule(id); + res.status(204).end(); + } catch (error) { + console.error('Error deleting schedule:', error); + res.status(500).json({ error: 'Failed to delete schedule' }); + } +}; + +exports.getTeachers = async (req, res) => { + try { + const teachers = await teachersModel.getAllTeachers(); + res.json(teachers); + } catch (error) { + console.error('Error getting teachers:', error); + res.status(500).json({ error: 'Failed to fetch teachers' }); + } +}; + diff --git a/backend/controllers/teachersController.js b/backend/controllers/teachersController.js new file mode 100644 index 0000000..b373de3 --- /dev/null +++ b/backend/controllers/teachersController.js @@ -0,0 +1,38 @@ +const teacherModel = require('../models/teachesModel'); + + +exports.getTeachers = async (req, res) => { + const teachers = await teacherModel.getAllTeachers(); + res.json(teachers); +} + +exports.newTeacher = async (req, res) => { + const { name, email, phone, mat } = req.body; + const newTeacher = await teacherModel.newTeacheradd({ name, email, phone, mat }); + res.status(201).json(newTeacher); +} + +exports.getTeacherById = async (req, res) => { + const { id } = req.params; + const teacher = await teacherModel.getTeacherById(id); + res.json(teacher); +} + +exports.createTeacher = async (req, res) => { + const { name, subject, email, phone, degree, status, avatar } = req.body; + const newTeacher = await teacherModel.createTeacher({ name, subject, email, phone, degree, status, avatar }); + res.status(201).json(newTeacher); +} + +exports.updateTeacher = async (req, res) => { + const { id } = req.params; + const { name, subject, email, phone, degree, status, avatar } = req.body; + const updatedTeacher = await teacherModel.updateTeacher(id, { name, subject, email, phone, degree, status, avatar }); + res.json(updatedTeacher); +} + +exports.deleteTeacher = async (req, res) => { + const { id } = req.params; + await teacherModel.deleteTeacher(id); + res.status(204).end(); +} diff --git a/backend/controllers/teachersControllers.js b/backend/controllers/teachersControllers.js new file mode 100644 index 0000000..3fc6e4d --- /dev/null +++ b/backend/controllers/teachersControllers.js @@ -0,0 +1,141 @@ +// Controlador para la gestión de docentes + +const teachersModel = require('../models/teachersModel'); + +// GET /api/teachers — Obtener todos los docentes +exports.getAllTeachers = async (req, res) => { + try { + const teachers = await teachersModel.getAllTeachers(); + res.status(200).json({ + success: true, + data: teachers, + }); + } catch (error) { + console.error('Error al obtener docentes:', error); + res.status(500).json({ + success: false, + message: 'Error interno al obtener los docentes.', + }); + } +}; + +// POST /api/teachers — Agregar un nuevo docente +exports.createTeacher = async (req, res) => { + try { + const { matricula, name, subject, email, phone, degree, status, avatar } = req.body; + + // Validación de campos obligatorios + if (!name || !subject || !email) { + return res.status(400).json({ + success: false, + message: 'Los campos nombre, materia y correo son obligatorios.', + }); + } + + const newTeacher = await teachersModel.createTeacher({ + matricula, + name, + subject, + email, + phone, + degree, + status, + avatar, + }); + + res.status(201).json({ + success: true, + message: 'Docente creado exitosamente.', + data: newTeacher, + }); + } catch (error) { + console.error('Error al crear docente:', error); + + // Manejo de correo duplicado (unique constraint de PostgreSQL) + if (error.code === '23505') { + return res.status(409).json({ + success: false, + message: 'Ya existe un docente registrado con ese correo.', + }); + } + + res.status(500).json({ + success: false, + message: 'Error interno al crear el docente.', + }); + } +}; + +// PUT /api/teachers/:id — Editar un docente existente +exports.updateTeacher = async (req, res) => { + try { + const { id } = req.params; + const { matricula, name, subject, email, phone, degree, status, avatar } = req.body; + + // Validación de campos obligatorios + if (!name || !subject || !email) { + return res.status(400).json({ + success: false, + message: 'Los campos nombre, materia y correo son obligatorios.', + }); + } + + const updatedTeacher = await teachersModel.updateTeacher(id, { + matricula, + name, + subject, + email, + phone, + degree, + status, + avatar, + }); + + if (!updatedTeacher) { + return res.status(404).json({ + success: false, + message: `No se encontró ningún docente con el ID ${id}.`, + }); + } + + res.status(200).json({ + success: true, + message: 'Docente actualizado exitosamente.', + data: updatedTeacher, + }); + } catch (error) { + console.error('Error al actualizar docente:', error); + res.status(500).json({ + success: false, + message: 'Error interno al actualizar el docente.', + }); + } +}; + +// DELETE /api/teachers/:id — Borrar un docente +exports.deleteTeacher = async (req, res) => { + try { + const { id } = req.params; + + const result = await teachersModel.deleteTeacher(id); + + if (!result) { + return res.status(404).json({ + success: false, + message: `No se encontró ningún docente con el ID ${id}.`, + }); + } + + res.status(200).json({ + success: true, + message: 'Docente eliminado exitosamente.', + data: result, + }); + } catch (error) { + console.error('Error al eliminar docente:', error); + res.status(500).json({ + success: false, + message: 'Error interno al eliminar el docente.', + }); + } +}; \ No newline at end of file diff --git a/backend/createRecordsTable.js b/backend/createRecordsTable.js new file mode 100644 index 0000000..3327d4a --- /dev/null +++ b/backend/createRecordsTable.js @@ -0,0 +1,24 @@ +const pool = require('./config/db'); + +async function createRecordsTable() { + try { + await pool.query(` + CREATE TABLE IF NOT EXISTS records ( + id SERIAL PRIMARY KEY, + matricula VARCHAR(50) NOT NULL, + nombre VARCHAR(150) NOT NULL, + hora_entrada TIME, + hora_salida TIME, + fecha DATE NOT NULL, + CONSTRAINT records_unique_matricula_fecha UNIQUE (matricula, fecha) + ) + `); + console.log('✓ Tabla records creada exitosamente'); + } catch (err) { + console.error('Error creando tabla records:', err.message); + } finally { + pool.end(); + } +} + +createRecordsTable(); diff --git a/backend/database.sql b/backend/database.sql new file mode 100644 index 0000000..e69de29 diff --git a/backend/diagnoseTeachersApi.js b/backend/diagnoseTeachersApi.js new file mode 100644 index 0000000..f9465ac --- /dev/null +++ b/backend/diagnoseTeachersApi.js @@ -0,0 +1,60 @@ +require('dotenv').config(); +const { Client } = require('pg'); + +const baseConfig = { + user: process.env.PGUSER || process.env.DB_USER || 'postgres', + host: process.env.PGHOST || process.env.DB_HOST || 'localhost', + password: process.env.PGPASSWORD || process.env.DB_PASSWORD || 'postgres', + port: Number(process.env.PGPORT || process.env.DB_PORT || 5432), +}; + +async function getDatabases() { + const client = new Client({ ...baseConfig, database: process.env.PGDATABASE || process.env.DB_NAME || 'postgres' }); + await client.connect(); + const { rows } = await client.query("SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname"); + await client.end(); + return rows.map((r) => r.datname); +} + +async function inspectDb(database) { + const client = new Client({ ...baseConfig, database }); + try { + await client.connect(); + const table = await client.query("SELECT to_regclass('public.teachers') AS teachers_table"); + if (!table.rows[0].teachers_table) { + return { database, hasTeachers: false }; + } + + const columns = await client.query( + "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'teachers' ORDER BY ordinal_position" + ); + const count = await client.query("SELECT COUNT(*)::int AS total FROM teachers"); + + let sample = []; + const colNames = columns.rows.map((c) => c.column_name); + if (colNames.includes('matricula') && colNames.includes('nombre')) { + const preview = await client.query("SELECT matricula, nombre FROM teachers ORDER BY nombre LIMIT 5"); + sample = preview.rows; + } + + return { + database, + hasTeachers: true, + columns: colNames, + total: count.rows[0].total, + sample, + }; + } catch (error) { + return { database, error: error.message }; + } finally { + await client.end(); + } +} + +(async () => { + const databases = await getDatabases(); + for (const db of databases) { + const result = await inspectDb(db); + console.log(JSON.stringify(result)); + } +})(); diff --git a/backend/inspectRecordsSchema.js b/backend/inspectRecordsSchema.js new file mode 100644 index 0000000..d1b412f --- /dev/null +++ b/backend/inspectRecordsSchema.js @@ -0,0 +1,30 @@ +const pool = require('./config/db'); + +(async () => { + try { + const table = await pool.query("SELECT to_regclass('public.records') AS records_table"); + console.log('table:', table.rows[0]); + + const rel = await pool.query( + "SELECT c.relkind, n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 'records'" + ); + console.log('relations:', rel.rows); + + const cols = await pool.query( + "SELECT column_name, data_type FROM information_schema.columns WHERE table_schema='public' AND table_name='records' ORDER BY ordinal_position" + ); + console.log('columns:', cols.rows); + + const attrs = await pool.query( + "SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod) AS type FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relname = 'records' AND n.nspname = 'public' AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum" + ); + console.log('pg attrs:', attrs.rows); + + const preview = await pool.query('SELECT * FROM records LIMIT 3'); + console.log('preview:', preview.rows); + } catch (error) { + console.error(error.message); + } finally { + await pool.end(); + } +})(); diff --git a/backend/migrations/001_create_schedules_table.js b/backend/migrations/001_create_schedules_table.js new file mode 100644 index 0000000..bb73169 --- /dev/null +++ b/backend/migrations/001_create_schedules_table.js @@ -0,0 +1,28 @@ +const pool = require('../config/db'); + +async function createSchedulesTable() { + try { + await pool.query(` + CREATE TABLE IF NOT EXISTS schedules ( + id SERIAL PRIMARY KEY, + teacher_id INTEGER NOT NULL REFERENCES teachers(id) ON DELETE CASCADE, + subject VARCHAR(255) NOT NULL, + day VARCHAR(50) NOT NULL, + start_time VARCHAR(5) NOT NULL, + end_time VARCHAR(5) NOT NULL, + room VARCHAR(100) NOT NULL, + group_name VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + console.log('Done'); + process.exit(0); + } catch (error) { + console.error('Error:', error); + process.exit(1); + } +} + +createSchedulesTable(); + diff --git a/backend/modelos/recordsModel.js b/backend/modelos/recordsModel.js new file mode 100644 index 0000000..d58896a --- /dev/null +++ b/backend/modelos/recordsModel.js @@ -0,0 +1,216 @@ +//modelo para registros de asistencia +const pool = require('../config/db'); + +function getLocalDateString(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +function getLocalTimeString(date) { + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${hours}:${minutes}:${seconds}`; +} + +async function getTeachersColumns() { + const result = await pool.query( + "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'teachers'" + ); + return new Set(result.rows.map((row) => row.column_name)); +} + +async function ensureRecordsTable() { + const createTableSql = ` + CREATE TABLE IF NOT EXISTS records ( + id SERIAL PRIMARY KEY, + matricula VARCHAR(50) NOT NULL, + nombre VARCHAR(150) NOT NULL, + hora_entrada TIME, + hora_salida TIME, + fecha DATE NOT NULL, + CONSTRAINT records_unique_matricula_fecha UNIQUE (matricula, fecha) + ) + `; + + await pool.query(createTableSql); + + const columnsResult = await pool.query( + "SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'records'" + ); + + if (columnsResult.rows.length === 0) { + await pool.query('DROP TABLE IF EXISTS records'); + await pool.query(createTableSql); + return; + } + + const existingColumns = new Set(columnsResult.rows.map((row) => row.column_name)); + const fechaColumn = columnsResult.rows.find((row) => row.column_name === 'fecha'); + + if (!existingColumns.has('matricula')) { + await pool.query('ALTER TABLE records ADD COLUMN matricula VARCHAR(50)'); + } + if (!existingColumns.has('nombre')) { + await pool.query('ALTER TABLE records ADD COLUMN nombre VARCHAR(150)'); + } + if (!existingColumns.has('hora_entrada')) { + await pool.query('ALTER TABLE records ADD COLUMN hora_entrada TIME'); + } + if (!existingColumns.has('hora_salida')) { + await pool.query('ALTER TABLE records ADD COLUMN hora_salida TIME'); + } + if (!existingColumns.has('fecha')) { + await pool.query('ALTER TABLE records ADD COLUMN fecha DATE'); + await pool.query('UPDATE records SET fecha = CURRENT_DATE WHERE fecha IS NULL'); + } else if (fechaColumn && fechaColumn.data_type !== 'date') { + await pool.query( + `ALTER TABLE records + ALTER COLUMN fecha TYPE DATE + USING CASE + WHEN fecha IS NULL THEN CURRENT_DATE + ELSE DATE(fecha) + END` + ); + } + + await pool.query('UPDATE records SET nombre = COALESCE(nombre, \'Sin nombre\')'); + await pool.query('UPDATE records SET fecha = COALESCE(fecha, CURRENT_DATE)'); + + await pool.query('ALTER TABLE records ALTER COLUMN matricula SET NOT NULL'); + await pool.query('ALTER TABLE records ALTER COLUMN nombre SET NOT NULL'); + await pool.query('ALTER TABLE records ALTER COLUMN fecha SET NOT NULL'); + + await pool.query( + `DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'records_unique_matricula_fecha' + ) THEN + BEGIN + ALTER TABLE records + ADD CONSTRAINT records_unique_matricula_fecha UNIQUE (matricula, fecha); + EXCEPTION WHEN unique_violation THEN + NULL; + END; + END IF; + END $$;` + ); +} + +async function getTeacherByMatricula(matricula) { + const columns = await getTeachersColumns(); + const nameField = columns.has('nombre') + ? 'nombre' + : columns.has('name') + ? 'name' + : null; + + if (!columns.has('matricula') || !nameField) { + throw new Error('La tabla teachers debe tener columnas matricula y nombre/name'); + } + + const result = await pool.query( + `SELECT matricula, ${nameField} AS nombre + FROM teachers + WHERE CAST(matricula AS TEXT) = $1 + OR COALESCE(NULLIF(LTRIM(CAST(matricula AS TEXT), '0'), ''), '0') + = COALESCE(NULLIF(LTRIM($1, '0'), ''), '0')`, + [matricula] + ); + + return result.rows[0] || null; +} + +exports.createRecord = async (record) => { + const { matricula, tipo_registro } = record; + const matriculaLimpia = String(matricula || '').trim(); + + if (!matriculaLimpia) { + throw new Error('La matrícula es obligatoria'); + } + + if (tipo_registro !== 'entrada' && tipo_registro !== 'salida') { + throw new Error('El tipo de registro debe ser entrada o salida'); + } + + await ensureRecordsTable(); + + const teacher = await getTeacherByMatricula(matriculaLimpia); + if (!teacher) { + throw new Error(`No existe un docente con la matrícula: ${matricula}`); + } + + const now = new Date(); + const fechaHoy = getLocalDateString(now); + const horaActual = getLocalTimeString(now); + + const existingResult = await pool.query( + 'SELECT * FROM records WHERE matricula = $1 AND fecha = $2', + [teacher.matricula, fechaHoy] + ); + + const existing = existingResult.rows[0] || null; + + if (tipo_registro === 'entrada') { + if (existing && existing.hora_entrada) { + throw new Error('Ya registraste una entrada hoy'); + } + + const result = existing + ? await pool.query( + `UPDATE records + SET nombre = $1, + hora_entrada = $2 + WHERE id = $3 + RETURNING *`, + [teacher.nombre, horaActual, existing.id] + ) + : await pool.query( + `INSERT INTO records (matricula, nombre, hora_entrada, fecha) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [teacher.matricula, teacher.nombre, horaActual, fechaHoy] + ); + + return result.rows[0]; + } + + if (!existing) { + throw new Error('Primero debes registrar una entrada hoy'); + } + + if (existing.hora_salida) { + throw new Error('Ya registraste una salida hoy'); + } + + const result = await pool.query( + `UPDATE records + SET nombre = $1, + hora_salida = $2 + WHERE id = $3 + RETURNING *`, + [teacher.nombre, horaActual, existing.id] + ); + + return result.rows[0]; +} + +exports.getRecords = async () => { + await ensureRecordsTable(); + const result = await pool.query('SELECT * FROM records ORDER BY fecha DESC, id DESC'); + return result.rows; +} + +exports.getRecordsByMatricula = async (matricula) => { + await ensureRecordsTable(); + const result = await pool.query( + 'SELECT * FROM records WHERE matricula = $1 ORDER BY fecha DESC, id DESC', + [matricula] + ); + return result.rows; +} diff --git a/backend/modelos/reportsModel.js b/backend/modelos/reportsModel.js new file mode 100644 index 0000000..3ca52f9 --- /dev/null +++ b/backend/modelos/reportsModel.js @@ -0,0 +1,94 @@ +const pool = require('../config/db'); + +function validateDate(value) { + return /^\d{4}-\d{2}-\d{2}$/.test(String(value || '')); +} + +async function getTeachersNameField() { + const result = await pool.query( + "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'teachers'" + ); + const columns = new Set(result.rows.map((row) => row.column_name)); + + if (columns.has('nombre')) { + return 'nombre'; + } + if (columns.has('name')) { + return 'name'; + } + + throw new Error('La tabla teachers debe tener columna nombre o name'); +} + +function getTotalDays(startDate, endDate) { + const start = new Date(`${startDate}T00:00:00`); + const end = new Date(`${endDate}T00:00:00`); + const diffMs = end.getTime() - start.getTime(); + return Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1; +} + +exports.getSummaryReport = async (startDate, endDate) => { + if (!validateDate(startDate) || !validateDate(endDate)) { + throw new Error('Las fechas deben tener formato YYYY-MM-DD'); + } + + if (startDate > endDate) { + throw new Error('La fecha de inicio no puede ser mayor a la fecha final'); + } + + const totalDays = getTotalDays(startDate, endDate); + const nameField = await getTeachersNameField(); + + const result = await pool.query( + `SELECT + t.matricula::text AS matricula, + t.${nameField} AS nombre, + COUNT(r.id) FILTER (WHERE r.hora_entrada IS NOT NULL)::int AS asistencias + FROM teachers t + LEFT JOIN records r + ON r.matricula::text = t.matricula::text + AND r.fecha BETWEEN $1::date AND $2::date + GROUP BY t.matricula, t.${nameField} + ORDER BY t.${nameField} ASC`, + [startDate, endDate] + ); + + return result.rows.map((row) => { + const asistencias = Number(row.asistencias || 0); + const faltas = Math.max(totalDays - asistencias, 0); + + return { + matricula: row.matricula, + profesor: row.nombre, + asistencias, + faltas, + totalDias: totalDays, + }; + }); +}; + +exports.getTeacherReportDetails = async (matricula, startDate, endDate) => { + if (!validateDate(startDate) || !validateDate(endDate)) { + throw new Error('Las fechas deben tener formato YYYY-MM-DD'); + } + + if (startDate > endDate) { + throw new Error('La fecha de inicio no puede ser mayor a la fecha final'); + } + + const result = await pool.query( + `SELECT + gs.fecha::date AS fecha, + r.hora_entrada, + r.hora_salida, + CASE WHEN r.hora_entrada IS NOT NULL THEN 'Asistencia' ELSE 'Falta' END AS estado + FROM generate_series($2::date, $3::date, interval '1 day') AS gs(fecha) + LEFT JOIN records r + ON r.matricula::text = $1::text + AND r.fecha = gs.fecha::date + ORDER BY gs.fecha ASC`, + [String(matricula), startDate, endDate] + ); + + return result.rows; +}; diff --git a/backend/modelos/teachersModel.js b/backend/modelos/teachersModel.js new file mode 100644 index 0000000..bf42bb4 --- /dev/null +++ b/backend/modelos/teachersModel.js @@ -0,0 +1,104 @@ +//modelos para la base de datos de teachers +const pool = require('../config/db'); + +async function getTeachersColumns() { + const result = await pool.query( + "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'teachers'" + ); + return new Set(result.rows.map((row) => row.column_name)); +} + +exports.getTeachers = async () => { + const columns = await getTeachersColumns(); + const nameField = columns.has('nombre') + ? 'nombre' + : columns.has('name') + ? 'name' + : null; + + if (!columns.has('matricula') || !nameField) { + throw new Error('La tabla teachers debe tener columnas matricula y nombre/name'); + } + + const extraField = columns.has('departamento') + ? 'departamento' + : columns.has('materia') + ? 'materia' + : null; + + const query = extraField + ? `SELECT matricula, ${nameField} AS nombre, ${extraField} AS departamento FROM teachers ORDER BY ${nameField} ASC` + : `SELECT matricula, ${nameField} AS nombre FROM teachers ORDER BY ${nameField} ASC`; + + const result = await pool.query(query); + return result.rows; +}; + +exports.createTeacher = async (teacher) => { + const { matricula, nombre, departamento, materia } = teacher; + const columns = await getTeachersColumns(); + + if (columns.has('departamento')) { + const result = await pool.query( + 'INSERT INTO teachers (matricula, nombre, departamento) VALUES ($1, $2, $3) RETURNING matricula, nombre, departamento', + [matricula, nombre, departamento || materia || null] + ); + return result.rows[0]; + } + + if (columns.has('materia')) { + const result = await pool.query( + 'INSERT INTO teachers (matricula, nombre, materia) VALUES ($1, $2, $3) RETURNING matricula, nombre, materia AS departamento', + [matricula, nombre, materia || departamento || null] + ); + return result.rows[0]; + } + + const result = await pool.query( + 'INSERT INTO teachers (matricula, nombre) VALUES ($1, $2) RETURNING matricula, nombre', + [matricula, nombre] + ); + return result.rows[0]; +}; + +exports.updateTeacher = async (matricula, teacher) => { + const { matricula: nuevaMatricula, nombre, departamento, materia } = teacher; + const columns = await getTeachersColumns(); + + const depColumn = columns.has('departamento') + ? 'departamento' + : columns.has('materia') + ? 'materia' + : null; + + const depValue = departamento || materia || null; + + const result = depColumn + ? await pool.query( + `UPDATE teachers + SET matricula = COALESCE($1, matricula), + nombre = COALESCE($2, nombre), + ${depColumn} = COALESCE($3, ${depColumn}) + WHERE matricula = $4 + RETURNING matricula, nombre, ${depColumn} AS departamento`, + [nuevaMatricula || null, nombre || null, depValue, matricula] + ) + : await pool.query( + `UPDATE teachers + SET matricula = COALESCE($1, matricula), + nombre = COALESCE($2, nombre) + WHERE matricula = $3 + RETURNING matricula, nombre`, + [nuevaMatricula || null, nombre || null, matricula] + ); + + return result.rows[0] || null; +}; + +exports.deleteTeacher = async (matricula) => { + await pool.query( + 'DELETE FROM teachers WHERE matricula = $1', + [matricula] + ); + return { message: 'docente eliminado' }; +}; \ No newline at end of file diff --git a/backend/models/SystemConfigModel.js b/backend/models/SystemConfigModel.js new file mode 100644 index 0000000..bf9eb4b --- /dev/null +++ b/backend/models/SystemConfigModel.js @@ -0,0 +1,89 @@ +const pool = require('../config/db'); + +// ── Configuración General (Tabla: "system_Config") ── +exports.getAllConfigs = async () => { + const result = await pool.query('SELECT * FROM "system_Config" ORDER BY id'); + return result.rows; +}; + +exports.getConfigById = async (id) => { + const result = await pool.query('SELECT * FROM "system_Config" WHERE id = $1', [id]); + return result.rows[0]; +}; + +exports.createConfig = async (config) => { + const { + nombre_institucion, codigo_institucional, idioma, zona_horaria, + tolerancia_entrada, tolerancia_salida, inasistencias_permitidas, + email_asistencia, email_reportes, push_alertas, + resumen_diario, alertas_faltas, notif_docentes + } = config; + + const result = await pool.query( + `INSERT INTO "system_Config" ( + nombre_institucion, codigo_institucional, idioma, zona_horaria, + tolerancia_entrada, tolerancia_salida, inasistencias_permitidas, + email_asistencia, email_reportes, push_alertas, + resumen_diario, alertas_faltas, notif_docentes + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING *`, + [nombre_institucion, codigo_institucional, idioma, zona_horaria, + tolerancia_entrada, tolerancia_salida, inasistencias_permitidas, + email_asistencia, email_reportes, push_alertas, + resumen_diario, alertas_faltas, notif_docentes] + ); + return result.rows[0]; +}; + +exports.updateConfig = async (id, config) => { + const { + nombre_institucion, codigo_institucional, idioma, zona_horaria, + tolerancia_entrada, tolerancia_salida, inasistencias_permitidas, + email_asistencia, email_reportes, push_alertas, + resumen_diario, alertas_faltas, notif_docentes + } = config; + + const result = await pool.query( + `UPDATE "system_Config" SET + nombre_institucion=$1, codigo_institucional=$2, idioma=$3, zona_horaria=$4, + tolerancia_entrada=$5, tolerancia_salida=$6, inasistencias_permitidas=$7, + email_asistencia=$8, email_reportes=$9, push_alertas=$10, + resumen_diario=$11, alertas_faltas=$12, notif_docentes=$13, + fecha_actualizacion=CURRENT_TIMESTAMP + WHERE id=$14 RETURNING *`, + [nombre_institucion, codigo_institucional, idioma, zona_horaria, + tolerancia_entrada, tolerancia_salida, inasistencias_permitidas, + email_asistencia, email_reportes, push_alertas, + resumen_diario, alertas_faltas, notif_docentes, id] + ); + return result.rows[0]; +}; + +exports.deleteConfig = async (id) => { + await pool.query('DELETE FROM "system_Config" WHERE id = $1', [id]); + return { message: 'Configuración eliminada' }; +}; + +// ── Work Days (Tabla: "work_days") ── +exports.getworkdays = async () => { + const result = await pool.query('SELECT * FROM "work_days"'); + return result.rows; +}; + +exports.updateworkday = async (id, { nombre, dia, hora_inicio, hora_fin, activo }) => { + const result = await pool.query( + `UPDATE "work_days" + SET nombre=$1, dia=$2, hora_inicio=$3, hora_fin=$4, activo=$5 + WHERE id=$6 RETURNING *`, + [nombre, dia, hora_inicio, hora_fin, activo, id] + ); + return result.rows[0]; +}; + +exports.createworkday = async ({ nombre, dia, hora_inicio, hora_fin, activo }) => { + const result = await pool.query( + `INSERT INTO "work_days" (nombre, dia, hora_inicio, hora_fin, activo) + VALUES ($1, $2, $3, $4, $5) RETURNING *`, + [nombre, dia, hora_inicio, hora_fin, activo] + ); + return result.rows[0]; +}; \ No newline at end of file diff --git a/backend/models/schedulesModel.js b/backend/models/schedulesModel.js new file mode 100644 index 0000000..b734933 --- /dev/null +++ b/backend/models/schedulesModel.js @@ -0,0 +1,120 @@ +const pool = require('../config/db'); + +exports.getAllSchedules = async () => { + const result = await pool.query(` + SELECT + s.id, + s.teacher_id, + s.subject, + s.day, + s.start_time, + s.end_time, + s.room, + s.group_name as group, + s.created_at, + t.name as teacher_name, + t.subject as teacher_subject + FROM schedules s + LEFT JOIN teachers t ON s.teacher_id = t.id + ORDER BY s.day, s.start_time + `); + return result.rows; +}; + +exports.getScheduleById = async (id) => { + const result = await pool.query(` + SELECT + s.id, + s.teacher_id, + s.subject, + s.day, + s.start_time, + s.end_time, + s.room, + s.group_name as group, + s.created_at, + t.name as teacher_name, + t.subject as teacher_subject + FROM schedules s + LEFT JOIN teachers t ON s.teacher_id = t.id + WHERE s.id = $1 + `, [id]); + return result.rows[0]; +}; + +exports.getSchedulesByTeacher = async (teacherId) => { + const result = await pool.query(` + SELECT + s.id, + s.teacher_id, + s.subject, + s.day, + s.start_time, + s.end_time, + s.room, + s.group_name as group, + s.created_at, + t.name as teacher_name, + t.subject as teacher_subject + FROM schedules s + LEFT JOIN teachers t ON s.teacher_id = t.id + WHERE s.teacher_id = $1 + ORDER BY s.day, s.start_time + `, [teacherId]); + return result.rows; +}; + +exports.createSchedule = async (schedule) => { + const { teacher_id, subject, day, start_time, end_time, room, group_name } = schedule; + + const result = await pool.query( + `INSERT INTO schedules (teacher_id, subject, day, start_time, end_time, room, group_name) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING + id, + teacher_id, + subject, + day, + start_time, + end_time, + room, + group_name as group, + created_at + `, + [teacher_id, subject, day, start_time, end_time, room, group_name] + ); + return result.rows[0]; +}; + +exports.updateSchedule = async (id, schedule) => { + const { teacher_id, subject, day, start_time, end_time, room, group_name } = schedule; + + const result = await pool.query( + `UPDATE schedules + SET teacher_id = $1, subject = $2, day = $3, start_time = $4, end_time = $5, room = $6, group_name = $7, updated_at = CURRENT_TIMESTAMP + WHERE id = $8 + RETURNING + id, + teacher_id, + subject, + day, + start_time, + end_time, + room, + group_name as group, + created_at, + updated_at + `, + [teacher_id, subject, day, start_time, end_time, room, group_name, id] + ); + return result.rows[0]; +}; + +exports.deleteSchedule = async (id) => { + const result = await pool.query( + 'DELETE FROM schedules WHERE id = $1 RETURNING id', + [id] + ); + return result.rows[0]; +}; + diff --git a/backend/models/teachersModel.js b/backend/models/teachersModel.js new file mode 100644 index 0000000..cb4fb44 --- /dev/null +++ b/backend/models/teachersModel.js @@ -0,0 +1,69 @@ +//Modelo para acceso a la base de datos de los profesores + +const pool = require('../config/db.js'); // Asume que tienes conexión a DB + +// GET — Obtener todos los docentes +const getAllTeachers = async () => { + try { + const query = 'SELECT * FROM teachers ORDER BY id DESC'; + const result = await pool.query(query); + return result.rows; + } catch (error) { + throw error; + } +}; + +// POST — Crear nuevo docente +const createTeacher = async (teacherData) => { + try { + const { matricula, name, subject, email, phone, degree, status, avatar } = teacherData; + + const query = ` + INSERT INTO teachers (matricula, name, subject, email, phone, degree, status, avatar) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING * + `; + + const result = await pool.query(query, [matricula, name, subject, email, phone, degree, status, avatar]); + return result.rows[0]; + } catch (error) { + throw error; + } +}; + +// PUT — Actualizar docente +const updateTeacher = async (id, teacherData) => { + try { + const { matricula, name, subject, email, phone, degree, status, avatar } = teacherData; + + const query = ` + UPDATE teachers + SET matricula = $1, name = $2, subject = $3, email = $4, phone = $5, degree = $6, status = $7, avatar = $8 + WHERE id = $9 + RETURNING * + `; + + const result = await pool.query(query, [matricula, name, subject, email, phone, degree, status, avatar, id]); + return result.rows[0]; + } catch (error) { + throw error; + } +}; + +// DELETE — Eliminar docente +const deleteTeacher = async (id) => { + try { + const query = 'DELETE FROM teachers WHERE id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } catch (error) { + throw error; + } +}; + +module.exports = { + getAllTeachers, + createTeacher, + updateTeacher, + deleteTeacher, +}; diff --git a/backend/models/teachesModel.js b/backend/models/teachesModel.js new file mode 100644 index 0000000..1af2a0b --- /dev/null +++ b/backend/models/teachesModel.js @@ -0,0 +1,48 @@ +const pool = require('../config/db'); + + +exports.getAllTeachers = async () => { + const result = await pool.query('SELECT * FROM teachers order by name'); + return result.rows; +} + +exports.newTeacher = async () => { + const {name, email, phone, mat} = newteacher + + const result = await pool.query('INSERT INTO teachers (name, email, phone, mat) VALUES ($1, $2, $3, $4) RETURNING *', [name, email, phone, mat]) + return result.rows[0]; + +} + +exports.getTeacherById = async (id) => { + const result = await pool.query('SELECT * FROM teachers WHERE id = $1', [id]); + return result.rows[0]; +} + +exports.createTeacher = async (teacher) => { + const {name, subject, email, phone, degree, status, avatar} = teacher + + const result = await pool.query( + 'INSERT INTO teachers (name, subject, email, phone, degree, status, avatar) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *', + [name, subject, email, phone, degree, status, avatar] + ) + return result.rows[0]; +} + +exports.updateTeacher = async (id, teacher) => { + const {name, subject, email, phone, degree, status, avatar} = teacher + + const result = await pool.query( + 'UPDATE teachers SET name = $1, subject = $2, email = $3, phone = $4, degree = $5, status = $6, avatar = $7 WHERE id = $8 RETURNING *', + [name, subject, email, phone, degree, status, avatar, id] + ) + return result.rows[0]; +}; + +exports.deleteTeacher = async (id) => { + const result = await pool.query( + 'DELETE FROM teachers WHERE id = $1 RETURNING *', + [id] + ) + return result.rows[0]; +} \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..addb31e --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1015 @@ +{ + "name": "backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^4.19.2", + "pg": "^8.20.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..356eb60 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,21 @@ +{ + "name": "backend", + "version": "1.0.0", + "description": "Backend onTimeClock", + "main": "server.js", + "type": "commonjs", + "scripts": { + "dev": "nodemon server.js", + "start": "node server.js", + "test": "echo \"No tests\" && exit 0" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^4.19.2", + "pg": "^8.20.0" + } +} \ No newline at end of file diff --git a/backend/routes/SystemConfigRoutes.js b/backend/routes/SystemConfigRoutes.js new file mode 100644 index 0000000..97df61a --- /dev/null +++ b/backend/routes/SystemConfigRoutes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const router = express.Router(); +const SystemConfigController = require('../controllers/SystemConfigController'); + +// Rutas para Work Days +router.get('/work-days', SystemConfigController.getworkdays); +router.post('/work-days', SystemConfigController.createworkday); +router.put('/work-days/:id', SystemConfigController.updateworkday); + +// Rutas para Configuración General +router.get('/', SystemConfigController.getConfigs); +router.get('/:id', SystemConfigController.getConfig); +router.post('/', SystemConfigController.createConfig); +router.put('/:id', SystemConfigController.updateConfig); +router.delete('/:id', SystemConfigController.deleteConfig); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/recordsRoutes.js b/backend/routes/recordsRoutes.js new file mode 100644 index 0000000..a61ee30 --- /dev/null +++ b/backend/routes/recordsRoutes.js @@ -0,0 +1,15 @@ +//rutas para registros de asistencia +const express = require("express"); +const router = express.Router(); + +const { + createRecord, + getRecords, + getRecordsByMatricula +} = require('../controllers/recordsControllers'); + +router.post("/", createRecord); +router.get("/", getRecords); +router.get("/:matricula", getRecordsByMatricula); + +module.exports = router; diff --git a/backend/routes/reportsRoutes.js b/backend/routes/reportsRoutes.js new file mode 100644 index 0000000..688dcf6 --- /dev/null +++ b/backend/routes/reportsRoutes.js @@ -0,0 +1,12 @@ +const express = require('express'); +const router = express.Router(); + +const { + getSummaryReport, + getTeacherReportDetails, +} = require('../controllers/reportsControllers'); + +router.get('/', getSummaryReport); +router.get('/:matricula/details', getTeacherReportDetails); + +module.exports = router; diff --git a/backend/routes/schedulesRoutes.js b/backend/routes/schedulesRoutes.js new file mode 100644 index 0000000..4606f22 --- /dev/null +++ b/backend/routes/schedulesRoutes.js @@ -0,0 +1,27 @@ +const express = require('express'); +const router = express.Router(); +const controller = require('../controllers/schedulesController'); + +// Get all schedules +router.get('/', controller.getSchedules); + +// Get schedule by ID +router.get('/:id', controller.getScheduleById); + +// Get schedules by teacher +router.get('/teacher/:teacherId', controller.getSchedulesByTeacher); + +// Get all teachers (for the form dropdown) +router.get('/data/teachers', controller.getTeachers); + +// Create new schedule +router.post('/', controller.createSchedule); + +// Update schedule +router.put('/:id', controller.updateSchedule); + +// Delete schedule +router.delete('/:id', controller.deleteSchedule); + +module.exports = router; + diff --git a/backend/routes/teachersRoutes.js b/backend/routes/teachersRoutes.js new file mode 100644 index 0000000..817ea11 --- /dev/null +++ b/backend/routes/teachersRoutes.js @@ -0,0 +1,39 @@ +// Rutas para la gestión de docentes + +const express = require('express'); +const router = express.Router(); +const teachersController = require("../controllers/teachersControllers"); + +/** + * @route GET /api/teachers + * @desc Obtener todos los docentes + * @access Public + */ +router.get('/', teachersController.getAllTeachers); + +/** + * @route POST /api/teachers + * @desc Agregar un nuevo docente + * @body { name, subject, email, phone, degree, status, avatar } + * @access Public + */ +router.post('/', teachersController.createTeacher); + +/** + * @route PUT /api/teachers/:id + * @desc Editar un docente existente + * @param id — ID del docente + * @body { name, subject, email, phone, degree, status, avatar } + * @access Public + */ +router.put('/:id', teachersController.updateTeacher); + +/** + * @route DELETE /api/teachers/:id + * @desc Borrar un docente + * @param id — ID del docente + * @access Public + */ +router.delete('/:id', teachersController.deleteTeacher); + +module.exports = router; \ No newline at end of file diff --git a/backend/seed.js b/backend/seed.js new file mode 100644 index 0000000..66606ef --- /dev/null +++ b/backend/seed.js @@ -0,0 +1,105 @@ +const pool = require('./config/db'); + +async function createTables() { + try { + // Crear tabla teachers si no existe + await pool.query(` + CREATE TABLE IF NOT EXISTS teachers ( + id SERIAL PRIMARY KEY, + matricula VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + subject VARCHAR(100), + email VARCHAR(100), + phone VARCHAR(20), + degree VARCHAR(100), + status VARCHAR(20), + avatar VARCHAR(255) + ) + `); + console.log('Tabla teachers lista ✓'); + + // Crear tabla records si no existe + await pool.query(` + CREATE TABLE IF NOT EXISTS records ( + id SERIAL PRIMARY KEY, + matricula VARCHAR(50) NOT NULL, + tipo_registro VARCHAR(20) NOT NULL, + fecha TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (matricula) REFERENCES teachers(matricula) + ) + `); + console.log('Tabla records lista ✓'); + + } catch (err) { + console.error('Error creando tablas:', err.message); + } +} + +async function seedTeachers() { + try { + const teachers = [ + { + matricula: '00834138', + name: 'María González', + subject: 'Matemáticas', + email: 'maria.gonzalez@example.com', + phone: '+1234567890', + degree: 'Licenciatura en Matemáticas', + status: 'active', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Maria' + }, + { + matricula: '00834139', + name: 'Carlos Rodríguez', + subject: 'Historia', + email: 'carlos.rodriguez@example.com', + phone: '+1234567891', + degree: 'Licenciatura en Historia', + status: 'active', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Carlos' + }, + { + matricula: '00834140', + name: 'Ana López', + subject: 'Inglés', + email: 'ana.lopez@example.com', + phone: '+1234567892', + degree: 'Licenciatura en Inglés', + status: 'active', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Ana' + }, + { + matricula: '00834141', + name: 'Pedro Martínez', + subject: 'Ciencias', + email: 'pedro.martinez@example.com', + phone: '+1234567893', + degree: 'Licenciatura en Biología', + status: 'active', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Pedro' + } + ]; + + for (const teacher of teachers) { + const { matricula, name, subject, email, phone, degree, status, avatar } = teacher; + await pool.query( + "INSERT INTO teachers (matricula, name, subject, email, phone, degree, status, avatar) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + [matricula, name, subject, email, phone, degree, status, avatar] + ); + } + + console.log('Datos de prueba insertados exitosamente!'); + console.log('Teachers agregados:', teachers.length); + } catch (err) { + console.error('Error insertando datos:', err.message); + } finally { + pool.end(); + } +} + +async function init() { + await createTables(); + await seedTeachers(); +} + +init(); \ No newline at end of file diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..9335ae8 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,46 @@ +const express = require("express"); +const cors = require("cors"); + +// Crear app +const app = express(); + +// Middlewares +app.use(cors()); +app.use(express.json()); + +// ================== ROUTES ================== + +// Teachers +const teachersRoutes = require("./routes/teachersRoutes"); +app.use("/api/teachers", teachersRoutes); + +// Schedules +const schedulesRoutes = require("./routes/schedulesRoutes"); +app.use("/api/schedules", schedulesRoutes); + +// Records +const recordsRoutes = require("./routes/recordsRoutes"); +app.use("/api/records", recordsRoutes); + +// Reports +const reportsRoutes = require("./routes/reportsRoutes"); +app.use("/api/reports", reportsRoutes); + +// Configuración (Chiquil) +const systemConfigRoutes = require("./routes/SystemConfigRoutes"); +app.use("/api/systemconfig", systemConfigRoutes); + +// ============================================ + +// Ruta base +app.get("/", (req, res) => { + res.send("API onTimeClock funcionando 🚀"); +}); + +// Puerto +const PORT = process.env.PORT || 3000; + +// Servidor +app.listen(PORT, () => { + console.log(`Servidor corriendo en http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/backend/setupRecordsTable.js b/backend/setupRecordsTable.js new file mode 100644 index 0000000..f574a3f --- /dev/null +++ b/backend/setupRecordsTable.js @@ -0,0 +1,29 @@ +const pool = require('./config/db'); + +async function setupRecordsTable() { + try { + // Primero eliminar la tabla si existe + await pool.query('DROP TABLE IF EXISTS records CASCADE'); + console.log('✓ Tabla anterior eliminada'); + + // Crear la tabla records con la estructura correcta + await pool.query(` + CREATE TABLE records ( + id SERIAL PRIMARY KEY, + matricula VARCHAR(50) NOT NULL, + nombre VARCHAR(150) NOT NULL, + hora_entrada TIME, + hora_salida TIME, + fecha DATE NOT NULL, + CONSTRAINT records_unique_matricula_fecha UNIQUE (matricula, fecha) + ) + `); + console.log('✓ Tabla records creada exitosamente'); + } catch (err) { + console.error('Error:', err.message); + } finally { + pool.end(); + } +} + +setupRecordsTable(); diff --git a/backend/teachersRoutes.js b/backend/teachersRoutes.js new file mode 100644 index 0000000..e69de29 diff --git a/eslint.config.js b/eslint.config.js index 4fa125d..9b81d51 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,8 +6,20 @@ import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ globalIgnores(['dist']), + { + files: ['backend/**/*.js'], + languageOptions: { + ecmaVersion: 2020, + sourceType: 'commonjs', + globals: globals.node, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, { files: ['**/*.{js,jsx}'], + ignores: ['backend/**'], extends: [ js.configs.recommended, reactHooks.configs.flat.recommended, diff --git a/package-lock.json b/package-lock.json index be38d2a..0c4b7aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,15 @@ { - "name": "react-intro", + "name": "ontime-clock", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "react-intro", + "name": "ontime-clock", "version": "0.0.0", + "license": "ISC", "dependencies": { + "@heroicons/react": "^2.2.0", "jspdf": "^4.2.0", "jspdf-autotable": "^5.0.7", "react": "^19.2.0", @@ -24,6 +26,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "nodemon": "^3.1.14", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", "vite": "^7.2.4" @@ -43,9 +46,9 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { @@ -58,9 +61,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -68,21 +71,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -99,14 +102,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -215,27 +218,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -277,9 +280,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -301,18 +304,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -320,9 +323,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -334,9 +337,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -351,9 +354,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -368,9 +371,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -385,9 +388,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -402,9 +405,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -419,9 +422,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -436,9 +439,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -453,9 +456,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -470,9 +473,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -487,9 +490,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -504,9 +507,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -521,9 +524,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -538,9 +541,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -555,9 +558,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -572,9 +575,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -589,9 +592,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -606,9 +609,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -623,9 +626,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -640,9 +643,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -657,9 +660,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -674,9 +677,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -691,9 +694,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -708,9 +711,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -725,9 +728,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -742,9 +745,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -759,9 +762,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -818,15 +821,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -859,20 +862,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -896,9 +899,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -932,6 +935,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1073,16 +1085,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", "cpu": [ "arm" ], @@ -1094,9 +1106,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", "cpu": [ "arm64" ], @@ -1108,9 +1120,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", "cpu": [ "arm64" ], @@ -1122,9 +1134,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", "cpu": [ "x64" ], @@ -1136,9 +1148,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", "cpu": [ "arm64" ], @@ -1150,9 +1162,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", "cpu": [ "x64" ], @@ -1164,9 +1176,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", "cpu": [ "arm" ], @@ -1178,9 +1190,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", "cpu": [ "arm" ], @@ -1192,9 +1204,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", "cpu": [ "arm64" ], @@ -1206,9 +1218,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", "cpu": [ "arm64" ], @@ -1220,9 +1232,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", "cpu": [ "loong64" ], @@ -1234,9 +1246,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", "cpu": [ "loong64" ], @@ -1248,9 +1260,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", "cpu": [ "ppc64" ], @@ -1262,9 +1274,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", "cpu": [ "ppc64" ], @@ -1276,9 +1288,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", "cpu": [ "riscv64" ], @@ -1290,9 +1302,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", "cpu": [ "riscv64" ], @@ -1304,9 +1316,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", "cpu": [ "s390x" ], @@ -1318,9 +1330,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "cpu": [ "x64" ], @@ -1332,9 +1344,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", "cpu": [ "x64" ], @@ -1346,9 +1358,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", "cpu": [ "x64" ], @@ -1360,9 +1372,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", "cpu": [ "arm64" ], @@ -1374,9 +1386,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", "cpu": [ "arm64" ], @@ -1388,9 +1400,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", "cpu": [ "ia32" ], @@ -1402,9 +1414,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", "cpu": [ "x64" ], @@ -1416,9 +1428,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", "cpu": [ "x64" ], @@ -1502,9 +1514,9 @@ "optional": true }, "node_modules/@types/react": { - "version": "19.2.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", - "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", "dependencies": { @@ -1529,16 +1541,16 @@ "optional": true }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", - "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.5", + "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.53", + "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, @@ -1546,13 +1558,13 @@ "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -1626,19 +1638,6 @@ "node": ">= 8" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -1654,9 +1653,9 @@ "license": "Python-2.0" }, "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", "dev": true, "funding": [ { @@ -1674,8 +1673,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -1708,13 +1707,16 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", + "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/binary-extensions": { @@ -1731,9 +1733,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -1755,9 +1757,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -1775,11 +1777,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -1809,9 +1811,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001766", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", - "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", "dev": true, "funding": [ { @@ -1962,9 +1964,9 @@ } }, "node_modules/core-js": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", - "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2058,9 +2060,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz", + "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==", "license": "(MPL-2.0 OR Apache-2.0)", "optional": true, "optionalDependencies": { @@ -2068,16 +2070,26 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.282", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.282.tgz", - "integrity": "sha512-FCPkJtpst28UmFzd903iU7PdeVTfY0KAeJy+Lk0GLZRwgwYHn/irRcaCbQQOmr5Vytc/7rcavsYLvTM8RiHYhQ==", + "version": "1.5.340", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", + "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", "dev": true, "license": "ISC" }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2088,32 +2100,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -2140,25 +2152,25 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -2177,7 +2189,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2200,9 +2212,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.0.tgz", + "integrity": "sha512-LDicyhrRFrIaheDYryeM2W8gWyZXnAs4zIr2WVPiOSeTmIu2RjR4x/9N0xLaRWZ+9hssBDGo3AadcohuzAvSvg==", "dev": true, "license": "MIT", "dependencies": { @@ -2216,7 +2228,7 @@ "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-react-refresh": { @@ -2395,24 +2407,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -2477,9 +2471,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -2622,6 +2616,13 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2802,9 +2803,9 @@ } }, "node_modules/jspdf": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz", - "integrity": "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", @@ -2928,19 +2929,6 @@ "node": ">=8.6" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -3000,12 +2988,116 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", "dev": true, "license": "MIT" }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3147,13 +3239,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -3180,9 +3272,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "dev": true, "funding": [ { @@ -3352,6 +3444,13 @@ "node": ">= 0.8.0" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3394,24 +3493,24 @@ } }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.2.5" } }, "node_modules/react-refresh": { @@ -3425,9 +3524,9 @@ } }, "node_modules/react-router": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", - "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", + "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -3447,12 +3546,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", - "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.1.tgz", + "integrity": "sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==", "license": "MIT", "dependencies": { - "react-router": "7.13.0" + "react-router": "7.14.1" }, "engines": { "node": ">=20.0.0" @@ -3485,19 +3584,6 @@ "node": ">=8.10.0" } }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -3506,12 +3592,13 @@ "optional": true }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -3558,9 +3645,9 @@ } }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "dev": true, "license": "MIT", "dependencies": { @@ -3574,31 +3661,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" } }, @@ -3671,6 +3758,32 @@ "node": ">=8" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3835,14 +3948,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -3851,6 +3964,37 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3864,6 +4008,16 @@ "node": ">=8.0" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -3884,6 +4038,13 @@ "node": ">= 0.8.0" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -3943,9 +4104,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { @@ -4017,6 +4178,37 @@ } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index d5be54b..b615303 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "react-intro", + "name": "ontime-clock", "private": true, "version": "0.0.0", "type": "module", @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@heroicons/react": "^2.2.0", "jspdf": "^4.2.0", "jspdf-autotable": "^5.0.7", "react": "^19.2.0", @@ -26,8 +27,13 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "nodemon": "^3.1.14", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", "vite": "^7.2.4" - } + }, + "description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.", + "main": "eslint.config.js", + "author": "", + "license": "ISC" } diff --git a/src/App.jsx b/src/App.jsx index 78f28a5..036e764 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,8 @@ - {/* Maquetar sidebar siempre visible con todas rutas - publicas y administrativas*/} + + +// Maquetar sidebar siempre visible con todas rutas - publicas y administrativas import { BrowserRouter, Routes, Route } from "react-router-dom" import Sidebar from "./components/Sidebar" - import Home from "./pages/Home" import About from "./pages/About" import Contact from "./pages/Contact" @@ -9,14 +10,17 @@ import Dashboard from "./pages/Dashboard" import Teachers from "./pages/Teachers" import Clock from "./pages/Clock" import Reports from "./pages/Reports" +import SystemConfig from "./pages/SystemConfig" +import ScheduleModule from "./modules/schedule/index.jsx"; function App() { return( - {/*Layout Principal side bar siempre visible a pantalla completa*/} + {/* Layout Principal side bar siempre visible a pantalla completa */}
+
} /> @@ -26,11 +30,14 @@ function App() { } /> } /> } /> + } /> + } />
+
) } -export default App +export default App \ No newline at end of file diff --git a/src/api/scheduleAPI.js b/src/api/scheduleAPI.js new file mode 100644 index 0000000..9b9bcdd --- /dev/null +++ b/src/api/scheduleAPI.js @@ -0,0 +1,58 @@ +const API_BASE_URL = 'https://ontimeclock.onrender.com/api'; + +export const scheduleAPI = { + getSchedules: async () => { + const response = await fetch(`${API_BASE_URL}/schedules`); + if (!response.ok) throw new Error('Failed to fetch schedules'); + return response.json(); + }, + + getTeachers: async () => { + const response = await fetch(`${API_BASE_URL}/schedules/data/teachers`); + if (!response.ok) throw new Error('Failed to fetch teachers'); + return response.json(); + }, + + createSchedule: async (scheduleData) => { + const response = await fetch(`${API_BASE_URL}/schedules`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(scheduleData), + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to create schedule'); + } + return response.json(); + }, + + getScheduleById: async (id) => { + const response = await fetch(`${API_BASE_URL}/schedules/${id}`); + if (!response.ok) throw new Error('Failed to fetch schedule'); + return response.json(); + }, + + updateSchedule: async (id, scheduleData) => { + const response = await fetch(`${API_BASE_URL}/schedules/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(scheduleData), + }); + if (!response.ok) throw new Error('Failed to update schedule'); + return response.json(); + }, + + deleteSchedule: async (id) => { + const response = await fetch(`${API_BASE_URL}/schedules/${id}`, { + method: 'DELETE', + }); + if (!response.ok) throw new Error('Failed to delete schedule'); + }, + + getSchedulesByTeacher: async (teacherId) => { + const response = await fetch(`${API_BASE_URL}/schedules/teacher/${teacherId}`); + if (!response.ok) throw new Error('Failed to fetch schedules by teacher'); + return response.json(); + }, +}; + diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index db6cea9..8f2a5ba 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -1,39 +1,185 @@ -import {Link} from 'react-router-dom' - -function Sidebar(){ - return( - - ) +/** + * INSTRUCCIONES DE INSTALACIÓN: + * 1. Instalar Heroicons: npm install @heroicons/react + * 2. Instalar React Router: npm install react-router-dom + * 3. Tener configurado Tailwind CSS en el proyecto. + * * EXTENSIONES RECOMENDADAS EN VS CODE: + * - Tailwind CSS IntelliSense (para autocompletado de clases) + * - ES7+ React/Redux/React-Native snippets + */ +//archivo: src/components/Sidebar.jsx +//nuevo sidebar +import { useState } from "react"; +import { Link, useLocation } from "react-router-dom"; + +import { + HomeIcon, + InformationCircleIcon, + PhoneIcon, + Squares2X2Icon, + UserGroupIcon, + ClockIcon, + DocumentChartBarIcon, + CalendarDaysIcon, + Bars3Icon, + MagnifyingGlassIcon, + ArrowLeftOnRectangleIcon, + UserCircleIcon, + Cog6ToothIcon, +} from "@heroicons/react/24/outline"; + +function Sidebar() { + const [isCollapsed, setIsCollapsed] = useState(false); + const location = useLocation(); + const iconSize = "h-5 w-5"; + + const linkClass = (path) => { + const isActive = + path === "/" + ? location.pathname === "/" + : location.pathname.startsWith(path); + + return `flex items-center gap-3 px-3 py-2 rounded-lg transition-all ${ + isActive + ? "bg-white/10 text-[#f0c02f]" + : "hover:bg-white/10 hover:text-[#f0c02f]" + }`; + }; + + return ( + + ); } -export default Sidebar \ No newline at end of file +export default Sidebar; diff --git a/src/modules/schedule/components/LessonModal.jsx b/src/modules/schedule/components/LessonModal.jsx new file mode 100644 index 0000000..3694d6d --- /dev/null +++ b/src/modules/schedule/components/LessonModal.jsx @@ -0,0 +1,85 @@ +import { TEACHERS, TIME_SLOTS } from "../data/mockData"; + +export default function LessonModal({ lesson, onClose }) { + if (!lesson) return null; + + const teacher = TEACHERS.find(t => t.id === lesson.teacherId); + const startSlot = TIME_SLOTS[lesson.startSlot - 1]; + const endSlot = TIME_SLOTS[lesson.endSlot - 1]; + const duration = lesson.endSlot - lesson.startSlot; + + const rows = [ + { emoji: "👤", label: "Docente", value: teacher?.name, mono: false }, + { emoji: "🪪", label: "ID Docente", value: lesson.teacherId, mono: true }, + { emoji: "🕐", label: "Horario", value: `${startSlot?.start} – ${endSlot?.end} (${duration}h)`, mono: false }, + { emoji: "📍", label: "Aula", value: lesson.room, mono: false }, + { emoji: "👥", label: "Grupo", value: lesson.group, mono: false }, + { emoji: "🏛", label: "Departamento",value: teacher?.department, mono: false }, + ]; + + return ( +
+
e.stopPropagation()} + className="w-full max-w-md rounded-2xl overflow-hidden shadow-2xl" + style={{ animation: "mIn .22s ease-out" }} + > + {/* Header */} +
+ +
+
+ {teacher?.avatar} +
+
+

Clase

+

{lesson.subject}

+

{lesson.day}

+
+
+
+ + {/* Body */} +
+ {rows.map(({ emoji, label, value }) => ( +
+ {emoji} +
+

{label}

+

{value}

+
+
+ ))} +
+
+ + +
+ ); +} diff --git a/src/modules/schedule/components/SearchBar.jsx b/src/modules/schedule/components/SearchBar.jsx new file mode 100644 index 0000000..34f5454 --- /dev/null +++ b/src/modules/schedule/components/SearchBar.jsx @@ -0,0 +1,23 @@ +export default function SearchBar({ query, onChange, placeholder = "Buscar..." }) { + return ( +
+ 🔍 + onChange(e.target.value)} + placeholder={placeholder} + className="w-full pl-9 pr-8 py-2.5 bg-white border border-gray-200 rounded-xl text-sm text-gray-800 placeholder-gray-400 + focus:outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 transition-all shadow-sm" + /> + {query && ( + + )} +
+ ); +} diff --git a/src/modules/schedule/data/mockData.js b/src/modules/schedule/data/mockData.js new file mode 100644 index 0000000..81eceaf --- /dev/null +++ b/src/modules/schedule/data/mockData.js @@ -0,0 +1,56 @@ +export const TEACHERS = [ + { id: "DOC001", name: "Dra. María García López", subject: "Cálculo Diferencial", department: "Matemáticas", avatar: "MG", color: "#4f46e5" }, + { id: "DOC002", name: "Ing. Carlos Ramírez Torres", subject: "Programación Web", department: "Sistemas", avatar: "CR", color: "#0284c7" }, + { id: "DOC003", name: "Lic. Ana Flores Mendoza", subject: "Comunicación Oral", department: "Humanidades", avatar: "AF", color: "#d97706" }, + { id: "DOC004", name: "Dr. Luis Pérez Castillo", subject: "Física General", department: "Ciencias Básicas",avatar: "LP", color: "#059669" }, + { id: "DOC005", name: "Mtra. Sofía Morales Vega", subject: "Base de Datos", department: "Sistemas", avatar: "SM", color: "#db2777" }, + { id: "DOC006", name: "Ing. Roberto Silva Cruz", subject: "Redes y Telecomunicaciones", department: "Sistemas", avatar: "RS", color: "#7c3aed" }, +]; + +export const ROOMS = [ + "Aula 101", "Aula 102", "Aula 103", + "Aula 201", "Aula 202", + "Lab. Cómputo A", "Lab. Cómputo B", "Lab. Física", + "Aula Magna", "Sala de Conferencias", +]; + +export const TIME_SLOTS = [ + { id: 1, start: "07:00", end: "08:00" }, + { id: 2, start: "08:00", end: "09:00" }, + { id: 3, start: "09:00", end: "10:00" }, + { id: 4, start: "10:00", end: "11:00" }, + { id: 5, start: "11:00", end: "12:00" }, + { id: 6, start: "12:00", end: "13:00" }, + { id: 7, start: "13:00", end: "14:00" }, + { id: 8, start: "14:00", end: "15:00" }, + { id: 9, start: "15:00", end: "16:00" }, + { id: 10, start: "16:00", end: "17:00" }, + { id: 11, start: "17:00", end: "18:00" }, + { id: 12, start: "18:00", end: "19:00" }, +]; + +export const DAYS = ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"]; + +export const INITIAL_SCHEDULE = [ + { id: 1, teacherId: "DOC001", day: "Lunes", startSlot: 1, endSlot: 2, room: "Aula 101", group: "1A", subject: "Cálculo Diferencial" }, + { id: 2, teacherId: "DOC001", day: "Miércoles", startSlot: 1, endSlot: 2, room: "Aula 101", group: "1A", subject: "Cálculo Diferencial" }, + { id: 3, teacherId: "DOC001", day: "Viernes", startSlot: 1, endSlot: 2, room: "Aula 102", group: "1B", subject: "Cálculo Diferencial" }, + { id: 4, teacherId: "DOC002", day: "Lunes", startSlot: 3, endSlot: 5, room: "Lab. Cómputo A",group: "3A", subject: "Programación Web" }, + { id: 5, teacherId: "DOC002", day: "Miércoles", startSlot: 3, endSlot: 5, room: "Lab. Cómputo A",group: "3A", subject: "Programación Web" }, + { id: 6, teacherId: "DOC002", day: "Viernes", startSlot: 3, endSlot: 4, room: "Lab. Cómputo B",group: "3B", subject: "Programación Web" }, + { id: 7, teacherId: "DOC003", day: "Martes", startSlot: 2, endSlot: 3, room: "Aula 202", group: "2A", subject: "Comunicación Oral" }, + { id: 8, teacherId: "DOC003", day: "Jueves", startSlot: 2, endSlot: 3, room: "Aula 202", group: "2A", subject: "Comunicación Oral" }, + { id: 9, teacherId: "DOC004", day: "Martes", startSlot: 5, endSlot: 7, room: "Lab. Física", group: "2B", subject: "Física General" }, + { id: 10, teacherId: "DOC004", day: "Jueves", startSlot: 5, endSlot: 7, room: "Lab. Física", group: "2B", subject: "Física General" }, + { id: 11, teacherId: "DOC005", day: "Lunes", startSlot: 7, endSlot: 9, room: "Lab. Cómputo B",group: "4A", subject: "Base de Datos" }, + { id: 12, teacherId: "DOC005", day: "Miércoles", startSlot: 7, endSlot: 9, room: "Lab. Cómputo B",group: "4A", subject: "Base de Datos" }, + { id: 13, teacherId: "DOC006", day: "Martes", startSlot: 9, endSlot: 11, room: "Aula 201", group: "5A", subject: "Redes y Telecomunicaciones" }, + { id: 14, teacherId: "DOC006", day: "Viernes", startSlot: 9, endSlot: 11, room: "Aula 201", group: "5A", subject: "Redes y Telecomunicaciones" }, + { id: 15, teacherId: "DOC001", day: "Jueves", startSlot: 4, endSlot: 5, room: "Aula 103", group: "2C", subject: "Cálculo Diferencial" }, +]; + +export const INITIAL_CLOSED_DAYS = [ + { id: 1, date: "2026-01-01", reason: "Año Nuevo", type: "holiday" }, + { id: 10, date: "2026-03-10", reason: "Mantenimiento de instalaciones", type: "maintenance" }, + { id: 11, date: "2026-06-02", reason: "Junta docente extraordinaria", type: "institutional" }, +]; diff --git a/src/modules/schedule/index.jsx b/src/modules/schedule/index.jsx new file mode 100644 index 0000000..91bee63 --- /dev/null +++ b/src/modules/schedule/index.jsx @@ -0,0 +1,55 @@ +import { Routes, Route, NavLink } from "react-router-dom"; +import WeeklyView from "./views/WeeklyView"; +import CalendarView from "./views/CalendarView"; +import TeachersSchedule from "./views/TeachersSchedule"; +import ClosedDays from "./views/ClosedDays"; +import AddScheduleEntry from "./views/AddScheduleEntry"; + +const NAV = [ + { to: "/horarios", label: "Vista Semanal", icon: "", end: true }, + { to: "/horarios/calendario", label: "Calendario", icon: "" }, + { to: "/horarios/docentes", label: "Docentes", icon: "" }, + { to: "/horarios/dias-cerrados", label: "Días Cerrados", icon: "" }, +]; + +export default function ScheduleModule() { + return ( +
+ {/* Sub-navigation */} +
+
+ +
+
+ + {/* Page content */} +
+ + } /> + } /> + } /> + } /> + } /> + +
+
+ ); +} diff --git a/src/modules/schedule/views/AddScheduleEntry.jsx b/src/modules/schedule/views/AddScheduleEntry.jsx new file mode 100644 index 0000000..d0ccad2 --- /dev/null +++ b/src/modules/schedule/views/AddScheduleEntry.jsx @@ -0,0 +1,243 @@ +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { ROOMS, TIME_SLOTS, DAYS } from "../data/mockData"; +import { scheduleAPI } from "../../../api/scheduleAPI"; + +export default function AddScheduleEntry() { + const navigate = useNavigate(); + const [teachers, setTeachers] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [form, setForm] = useState({ + teacher_id: "", subject: "", day: "", startSlot: "", endSlot: "", room: "", group_name: "", + }); + const [saved, setSaved] = useState(false); + + // Load teachers from API + useEffect(() => { + const fetchTeachers = async () => { + try { + setLoading(true); + const data = await scheduleAPI.getTeachers(); + setTeachers(data); + } catch (err) { + setError("Error loading teachers: " + err.message); + console.error(err); + } finally { + setLoading(false); + } + }; + fetchTeachers(); + }, []); + + const set = (k, v) => setForm(f => ({ ...f, [k]: v })); + + const getTimeFromSlot = (slotId) => { + const slot = TIME_SLOTS.find(s => s.id === parseInt(slotId)); + return slot ? slot.start : ""; + }; + + const getEndTimeFromSlot = (slotId) => { + const slot = TIME_SLOTS.find(s => s.id === parseInt(slotId)); + return slot ? slot.end : ""; + }; + + const isValid = form.teacher_id && form.day && form.startSlot && form.endSlot && form.room && form.group_name + && parseInt(form.endSlot) > parseInt(form.startSlot); + + const handleSave = async () => { + if (!isValid) return; + + try { + setSaved(true); + setError(""); + + const startTime = getTimeFromSlot(form.startSlot); + const endTime = getEndTimeFromSlot(form.endSlot); + + const scheduleData = { + teacher_id: parseInt(form.teacher_id), + subject: form.subject, + day: form.day, + start_time: startTime, + end_time: endTime, + room: form.room, + group_name: form.group_name, + }; + + await scheduleAPI.createSchedule(scheduleData); + + setTimeout(() => navigate("/horarios"), 1200); + } catch (err) { + setError("Error saving schedule: " + err.message); + setSaved(false); + console.error(err); + } + }; + + const selT = teachers.find(t => t.id === parseInt(form.teacher_id)); + + return ( +
+ +

Agregar Clase

+

Registra una nueva sesión en el horario semanal

+ + {error && ( +
+ {error} +
+ )} + +
+ + {/* Teacher */} +
+ + {loading ? ( +

Cargando docentes...

+ ) : teachers.length === 0 ? ( +

No hay docentes disponibles

+ ) : ( +
+ {teachers.map(t => { + const colors = ["#4f46e5", "#0284c7", "#d97706", "#059669", "#db2777", "#7c3aed"]; + const colorIndex = t.id % colors.length; + const color = colors[colorIndex]; + const avatar = t.name.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase(); + + return ( + + ); + })} +
+ )} +
+ +
+
+ + set("subject", e.target.value)} placeholder="Nombre de la materia" + className="w-full px-3 py-2.5 border border-gray-200 rounded-xl text-sm text-gray-800 placeholder-gray-400 + focus:outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 transition-all" + /> +
+ +
+
+ +
+ {DAYS.map(day => ( + + ))} +
+
+
+ + set("group_name", e.target.value)} placeholder="Ej: 3A" + className="w-full px-3 py-2.5 border border-gray-200 rounded-xl text-sm text-gray-800 placeholder-gray-400 + focus:outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100" + /> +
+
+ +
+ +
+
+

Inicio

+ +
+
+

Fin

+ +
+
+ {form.startSlot && form.endSlot && parseInt(form.endSlot) > parseInt(form.startSlot) && ( +

+ Duración: {parseInt(form.endSlot) - parseInt(form.startSlot)} hora(s) +

+ )} +
+ +
+ +
+ {ROOMS.map(room => ( + + ))} +
+
+
+ + {/* Footer */} +
+ +
+
+
+ ); +} diff --git a/src/modules/schedule/views/CalendarView.jsx b/src/modules/schedule/views/CalendarView.jsx new file mode 100644 index 0000000..cae0a29 --- /dev/null +++ b/src/modules/schedule/views/CalendarView.jsx @@ -0,0 +1,235 @@ +import { useState, useMemo, useEffect } from "react"; +import { INITIAL_CLOSED_DAYS, TIME_SLOTS } from "../data/mockData"; +import LessonModal from "../components/LessonModal"; +import { scheduleAPI } from "../../../api/scheduleAPI"; + +const MONTH_NAMES = ["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"]; +const DOW_NAMES = ["Dom","Lun","Mar","Mié","Jue","Vie","Sáb"]; +const DOW_TO_DAY = { 1:"Lunes",2:"Martes",3:"Miércoles",4:"Jueves",5:"Viernes",6:"Sábado" }; + +const TYPE = { + holiday: { label:"Festivo", dot:"#ef4444", bg:"#fef2f2", border:"#fecaca", text:"#b91c1c" }, + maintenance: { label:"Mantenimiento", dot:"#f59e0b", bg:"#fffbeb", border:"#fde68a", text:"#92400e" }, + institutional:{ label:"Institucional", dot:"#0ea5e9", bg:"#f0f9ff", border:"#bae6fd", text:"#0369a1" }, +}; + +export default function CalendarView() { + const now = new Date(); + const [year, setYear] = useState(now.getFullYear()); + const [month, setMonth] = useState(now.getMonth()); + const [selDate, setSel] = useState(null); + const [modal, setModal] = useState(null); + const [schedules, setSchedules] = useState([]); + const [teachers, setTeachers] = useState([]); + + useEffect(() => { + const fetchData = async () => { + try { + const [schedulesData, teachersData] = await Promise.all([ + scheduleAPI.getSchedules(), + scheduleAPI.getTeachers() + ]); + setSchedules(schedulesData); + setTeachers(teachersData); + } catch (error) { + console.error('Error loading data:', error); + } + }; + fetchData(); + }, []); + + const getTeacherColor = (teacherId) => { + const colors = ["#4f46e5", "#0284c7", "#d97706", "#059669", "#db2777", "#7c3aed"]; + return colors[teacherId % colors.length]; + }; + + const convertToSlot = (timeString) => { + const slot = TIME_SLOTS.find(s => s.start === timeString); + return slot ? slot.id : 1; + }; + + const convertedSchedules = useMemo(() => { + return schedules.map(s => ({ + id: s.id, + teacherId: s.teacher_id, + day: s.day, + subject: s.subject, + room: s.room, + group: s.group, + startSlot: convertToSlot(s.start_time), + endSlot: convertToSlot(s.end_time), + teacher_name: s.teacher_name + })); + }, [schedules]); + + const closedMap = useMemo(() => { + const m = {}; + INITIAL_CLOSED_DAYS.forEach(d => { m[d.date] = d; }); + return m; + }, []); + + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const firstDow = new Date(year, month, 1).getDay(); + const cells = [...Array(firstDow).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => i + 1)]; + const fmt = d => `${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`; + const todayStr = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}-${String(now.getDate()).padStart(2,"0")}`; + + const prev = () => month === 0 ? (setMonth(11), setYear(y => y - 1)) : setMonth(m => m - 1); + const next = () => month === 11 ? (setMonth(0), setYear(y => y + 1)) : setMonth(m => m + 1); + + const selLessons = useMemo(() => { + if (!selDate) return []; + const dow = new Date(selDate + "T12:00").getDay(); + const dayName = DOW_TO_DAY[dow]; + return dayName ? convertedSchedules.filter(l => l.day === dayName) : []; + }, [selDate, convertedSchedules]); + return ( +
+

Calendario Académico

+

Festivos, días cerrados y clases por día

+ + {/* Legend */} +
+ {Object.entries(TYPE).map(([k, v]) => ( +
+
+ {v.label} +
+ ))} +
+ +
+ {/* Calendar widget */} +
+ {/* Nav */} +
+ +

{MONTH_NAMES[month]} {year}

+ +
+ {/* DOW header */} +
+ {DOW_NAMES.map(d => ( +
{d}
+ ))} +
+ {/* Days */} +
+ {cells.map((day, i) => { + if (!day) return
; + const ds = fmt(day); + const closed = closedMap[ds]; + const isToday = ds === todayStr; + const isSel = selDate === ds; + const isWeek = new Date(year, month, day).getDay() === 0 || new Date(year, month, day).getDay() === 6; + const cfg = closed ? TYPE[closed.type] : null; + + let cls = "aspect-square flex flex-col items-center justify-center rounded-xl text-sm font-medium cursor-pointer transition-all hover:scale-105 relative select-none border "; + let style = {}; + + if (isSel) { + cls += "bg-indigo-600 text-white border-indigo-600 shadow-md"; + } else if (isToday) { + cls += "bg-indigo-50 text-indigo-700 border-indigo-200 ring-2 ring-indigo-400"; + } else if (closed) { + style = { background: cfg.bg, borderColor: cfg.border, color: cfg.text }; + } else if (isWeek) { + cls += "text-gray-300 border-transparent"; + } else { + cls += "text-gray-600 border-transparent hover:bg-gray-50 hover:border-gray-200"; + } + + return ( + + ); + })} +
+
+ + {/* Side panel */} +
+ {selDate ? ( + <> +
+

+ {new Date(selDate + "T12:00").toLocaleDateString("es-MX", { weekday: "long", day: "numeric", month: "long" })} +

+ {closedMap[selDate] ? ( +
+

+ {closedMap[selDate].reason} +

+

{TYPE[closedMap[selDate].type].label}

+
+ ) : ( +

✓ Día hábil

+ )} +
+ + {!closedMap[selDate] && selLessons.length > 0 && ( +
+

Clases del día

+
+ {selLessons.map(l => { + const t = teachers.find(x => x.id === l.teacherId); + const color = getTeacherColor(l.teacherId); + return ( + + ); + })} +
+
+ )} + + {closedMap[selDate] && ( +
+ 🚫 +

Sin clases este día

+
+ )} + + ) : ( +
+ 📅 +

Selecciona un día para ver detalles

+
+ )} + + {/* Upcoming closed days */} +
+

Días cerrados

+ {INITIAL_CLOSED_DAYS.slice(0, 6).map(d => { + const cfg = TYPE[d.type]; + return ( +
+
+
+

{d.reason}

+

+ {new Date(d.date + "T12:00").toLocaleDateString("es-MX", { day: "numeric", month: "short" })} +

+
+
+ ); + })} +
+
+
+ + setModal(null)} /> +
+ ); +} + diff --git a/src/modules/schedule/views/ClosedDays.jsx b/src/modules/schedule/views/ClosedDays.jsx new file mode 100644 index 0000000..19059e1 --- /dev/null +++ b/src/modules/schedule/views/ClosedDays.jsx @@ -0,0 +1,182 @@ +import { useState } from "react"; +import { INITIAL_CLOSED_DAYS } from "../data/mockData"; + +const TYPE = { + holiday: { label: "Festivo Nacional", emoji: "", bg: "#fef2f2", border: "#fecaca", text: "#b91c1c", dot: "#ef4444" }, + maintenance: { label: "Mantenimiento", emoji: "", bg: "#fffbeb", border: "#fde68a", text: "#92400e", dot: "#f59e0b" }, + institutional:{ label: "Institucional", emoji: "", bg: "#f0f9ff", border: "#bae6fd", text: "#0369a1", dot: "#0ea5e9" }, +}; + +export default function ClosedDays() { + const [days, setDays] = useState(INITIAL_CLOSED_DAYS); + const [showForm, setShowForm] = useState(false); + const [form, setForm] = useState({ date: "", reason: "", type: "holiday" }); + const [filter, setFilter] = useState("all"); + + const handleAdd = () => { + if (!form.date || !form.reason) return; + setDays(prev => [...prev, { id: Date.now(), ...form }]); + setForm({ date: "", reason: "", type: "holiday" }); + setShowForm(false); + }; + + const filtered = filter === "all" ? days : days.filter(d => d.type === filter); + const sorted = [...filtered].sort((a, b) => a.date.localeCompare(b.date)); + + const stats = { + total: days.length, + holiday: days.filter(d => d.type === "holiday").length, + maintenance: days.filter(d => d.type === "maintenance").length, + institutional:days.filter(d => d.type === "institutional").length, + }; + + return ( +
+
+
+

Días Cerrados

+

Festivos, mantenimiento y días sin clases

+
+ +
+ + {/* Stats */} +
+ {[ + { label:"Total", value: stats.total, color:"text-gray-800", bg:"bg-gray-50 border-gray-200" }, + { label:"Festivos", value: stats.holiday, color:"text-red-600", bg:"bg-red-50 border-red-200" }, + { label:"Mantenimiento", value: stats.maintenance, color:"text-amber-600", bg:"bg-amber-50 border-amber-200" }, + { label:"Institucionales",value: stats.institutional, color:"text-sky-600", bg:"bg-sky-50 border-sky-200" }, + ].map(({ label, value, color, bg }) => ( +
+

{value}

+

{label}

+
+ ))} +
+ + {/* Tabs */} +
+ {[["all","Todos"],["holiday","Festivos"],["maintenance","Mantenimiento"],["institutional","Institucionales"]].map(([v, l]) => ( + + ))} +
+ + {/* List */} +
+ {sorted.map(day => { + const cfg = TYPE[day.type]; + return ( +
+ {cfg.emoji} +
+

{day.reason}

+

+ {new Date(day.date + "T12:00").toLocaleDateString("es-MX", { weekday: "long", year: "numeric", month: "long", day: "numeric" })} +

+
+
+ {cfg.label} + +
+
+ ); + })} + + {sorted.length === 0 && ( +
+ 📭 +

No hay días cerrados registrados

+
+ )} +
+ + {/* Modal */} + {showForm && ( +
setShowForm(false)} + > +
e.stopPropagation()} + style={{ animation: "mIn .22s ease-out" }} + > +
+

Agregar Día Cerrado

+ +
+ +
+
+ + setForm(f => ({ ...f, date: e.target.value }))} + className="w-full px-3 py-2.5 border border-gray-200 rounded-xl text-sm text-gray-800 focus:outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100" + /> +
+
+ + setForm(f => ({ ...f, reason: e.target.value }))} + placeholder="Ej: Día de la Constitución" + className="w-full px-3 py-2.5 border border-gray-200 rounded-xl text-sm text-gray-800 placeholder-gray-400 focus:outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100" + /> +
+
+ +
+ {Object.entries(TYPE).map(([type, cfg]) => ( + + ))} +
+
+
+ +
+ + +
+
+
+ )} + + +
+ ); +} diff --git a/src/modules/schedule/views/TeachersSchedule.jsx b/src/modules/schedule/views/TeachersSchedule.jsx new file mode 100644 index 0000000..9c3c785 --- /dev/null +++ b/src/modules/schedule/views/TeachersSchedule.jsx @@ -0,0 +1,205 @@ +import { useState, useMemo, useEffect } from "react"; +import { TIME_SLOTS, DAYS } from "../data/mockData"; +import LessonModal from "../components/LessonModal"; +import SearchBar from "../components/SearchBar"; +import { scheduleAPI } from "../../../api/scheduleAPI"; + +export default function TeachersSchedule() { + const [query, setQuery] = useState(""); + const [modal, setModal] = useState(null); + const [expanded, setExpanded] = useState(null); + const [teachers, setTeachers] = useState([]); + const [schedules, setSchedules] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const [teachersData, schedulesData] = await Promise.all([ + scheduleAPI.getTeachers(), + scheduleAPI.getSchedules() + ]); + setTeachers(teachersData); + setSchedules(schedulesData); + } catch (error) { + console.error('Error loading data:', error); + } finally { + setLoading(false); + } + }; + fetchData(); + }, []); + + const getTeacherColor = (teacherId) => { + const colors = ["#4f46e5", "#0284c7", "#d97706", "#059669", "#db2777", "#7c3aed"]; + return colors[teacherId % colors.length]; + }; + + const getTeacherInitials = (name) => { + return name.split(" ").slice(0, 2).map(n => n[0]).join(""); + }; + + const convertToSlot = (timeString) => { + const slot = TIME_SLOTS.find(s => s.start === timeString); + return slot ? slot.id : 1; + }; + + const filtered = useMemo(() => { + const q = query.toLowerCase(); + if (!q) return teachers; + return teachers.filter(t => + t.id.toLowerCase().includes(q) || + t.name.toLowerCase().includes(q) || + t.subject.toLowerCase().includes(q) + ); + }, [query, teachers]); + + const getLessons = id => schedules.filter(l => l.teacher_id === id).map(s => ({ + id: s.id, + teacherId: s.teacher_id, + day: s.day, + subject: s.subject, + room: s.room, + group: s.group, + startSlot: convertToSlot(s.start_time), + endSlot: convertToSlot(s.end_time) + })); + + const slotStart = id => TIME_SLOTS[id - 1]?.start ?? ""; + const slotEnd = id => TIME_SLOTS[id - 1]?.end ?? ""; + const totalHours = id => getLessons(id).reduce((s, l) => s + (l.endSlot - l.startSlot), 0); + + return ( +
+
+
+

Horarios por Docente

+

Expande para ver el detalle de cada profesor

+
+ + {filtered.length} docentes + +
+ +
+ +
+ +
+ {filtered.map(teacher => { + const lessons = getLessons(teacher.id); + const isOpen = expanded === teacher.id; + const hours = totalHours(teacher.id); + const color = getTeacherColor(teacher.id); + const initials = getTeacherInitials(teacher.name); + + return ( +
+ + + {isOpen && ( +
+ {lessons.length === 0 ? ( +

Sin clases asignadas

+ ) : ( +
+ {lessons.map(lesson => ( + + ))} +
+ )} + +
+

Semana

+
+ {DAYS.map(day => { + const dl = lessons.filter(l => l.day === day); + return ( +
+

{day.slice(0, 3)}

+ {dl.length === 0 ? ( +
+ ) : dl.map(l => ( +
setModal(l)} + > + {slotStart(l.startSlot)} +
+ ))} +
+ ); + })} +
+
+
+ )} +
+ ); + })} +
+ + setModal(null)} /> +
+ ); +} diff --git a/src/modules/schedule/views/WeeklyView.jsx b/src/modules/schedule/views/WeeklyView.jsx new file mode 100644 index 0000000..01795da --- /dev/null +++ b/src/modules/schedule/views/WeeklyView.jsx @@ -0,0 +1,189 @@ +import { useState, useMemo, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { TIME_SLOTS, DAYS } from "../data/mockData"; +import LessonModal from "../components/LessonModal"; +import SearchBar from "../components/SearchBar"; +import { scheduleAPI } from "../../../api/scheduleAPI"; + +export default function WeeklyView() { + const [modal, setModal] = useState(null); + const [query, setQuery] = useState(""); + const [teacherFilter, setTeacher] = useState("all"); + const [schedules, setSchedules] = useState([]); + const [teachers, setTeachers] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const [schedulesData, teachersData] = await Promise.all([ + scheduleAPI.getSchedules(), + scheduleAPI.getTeachers() + ]); + setSchedules(schedulesData); + setTeachers(teachersData); + } catch (error) { + console.error('Error loading schedules:', error); + } finally { + setLoading(false); + } + }; + fetchData(); + }, []); + + const convertToSlot = (timeString) => { + const slot = TIME_SLOTS.find(s => s.start === timeString); + return slot ? slot.id : 1; + }; + + const convertedSchedules = useMemo(() => { + return schedules.map(s => ({ + id: s.id, + teacherId: s.teacher_id, + day: s.day, + subject: s.subject, + room: s.room, + group: s.group, + startSlot: convertToSlot(s.start_time), + endSlot: convertToSlot(s.end_time), + teacher_name: s.teacher_name + })); + }, [schedules]); + + const filtered = useMemo(() => { + let r = convertedSchedules; + if (teacherFilter !== "all") r = r.filter(l => l.teacherId === parseInt(teacherFilter)); + if (query.trim()) { + const q = query.toLowerCase(); + r = r.filter(l => { + const t = teachers.find(t => t.id === l.teacherId); + return ( + String(l.teacherId).toLowerCase().includes(q) || + l.subject.toLowerCase().includes(q) || + t?.name.toLowerCase().includes(q) || + l.room.toLowerCase().includes(q) || + l.group.toLowerCase().includes(q) + ); + }); + } + return r; + }, [query, teacherFilter, convertedSchedules, teachers]); + + const isCont = (day, slotId) => + filtered.some(l => l.day === day && l.startSlot < slotId && l.endSlot >= slotId); + + const getTeacherColor = (teacherId) => { + const colors = ["#4f46e5", "#0284c7", "#d97706", "#059669", "#db2777", "#7c3aed"]; + return colors[teacherId % colors.length]; + }; + + return ( +
+
+
+

Vista Semanal

+

Horario general

+
+ + + Agregar Clase + +
+ +
+
+ +
+ +
+ + {query && ( +

+ Mostrando {filtered.length} clases +

+ )} + +
+
+ + + + + {DAYS.map(d => ( + + ))} + + + + {TIME_SLOTS.map((slot, idx) => ( + + + {DAYS.map(day => { + const lessons = filtered.filter(l => l.day === day && l.startSlot === slot.id); + if (isCont(day, slot.id) && lessons.length === 0) return + ); + })} + + ))} + +
Hora{d}
+ {slot.start} + ; + return ( + + {lessons.map(lesson => { + const teacher = teachers.find(t => t.id === lesson.teacherId); + const color = getTeacherColor(lesson.teacherId); + const span = lesson.endSlot - lesson.startSlot; + return ( + + ); + })} +
+
+
+ +
+ {teachers.map((t, idx) => ( +
+
+ {t.name.split(" ").slice(0, 2).join(" ")} +
+ ))} +
+ + setModal(null)} /> +
+ ); +} diff --git a/src/pages/Clock.jsx b/src/pages/Clock.jsx index fb6ae2a..15dd3ac 100644 --- a/src/pages/Clock.jsx +++ b/src/pages/Clock.jsx @@ -1,53 +1,868 @@ -function Clock() { - return ( -
- {/*contenedor principal*/} -
-

- Reloj Checador -

-

- Sistema de Registro de Asistencia Docente -

- - {/*Horario Actual*/} -
-

- 08:45:23 AM -

-

- Hora Actual -

-
- - {/*Estado*/} -
- - Sistema Activo - -
- - {/*Acciones*/} -
- - -
- - {/*Info adicional*/} -
- Universidad UNID Playa del Carmen * Admin -
+import { useState, useEffect } from "react"; + +const IMPORTANT_MESSAGES = [ + "📢 Reunión de facultad: Lunes 10:00 AM — Sala de Juntas", + "⚠️ Mantenimiento del sistema: Viernes 11 PM - 1 AM", + "📅 Período de evaluaciones: 26 - 30 de Mayo", + "🎓 Ceremonia de graduación: 15 de Junio, 11:00 AM", +]; + +export default function Clock() { + const [time, setTime] = useState(new Date()); + const [lastAction, setLastAction] = useState(null); + const [actionMessage, setActionMessage] = useState(""); + const [showMessage, setShowMessage] = useState(false); + const [msgIndex, setMsgIndex] = useState(0); + const [fade, setFade] = useState(true); + const [pulse, setPulse] = useState(false); + const [showAllMsgs, setShowAllMsgs] = useState(false); + const [matricula, setMatricula] = useState(""); + const [selectedMatricula, setSelectedMatricula] = useState(""); + const [teachers, setTeachers] = useState([]); + + useEffect(() => { + const timer = setInterval(() => setTime(new Date()), 1000); + return () => clearInterval(timer); + }, []); + + useEffect(() => { + const rotateMsg = setInterval(() => { + setFade(false); + setTimeout(() => { + setMsgIndex((i) => (i + 1) % IMPORTANT_MESSAGES.length); + setFade(true); + }, 400); + }, 5000); + return () => clearInterval(rotateMsg); + }, []); + + useEffect(() => { + const loadTeachers = async () => { + try { + const response = await fetch("https://ontimeclock.onrender.com/api/teachers"); + if (!response.ok) return; + const data = await response.json(); + if (Array.isArray(data)) { + setTeachers( + [...data].sort((a, b) => + String(a?.nombre || "").localeCompare(String(b?.nombre || ""), "es", { + sensitivity: "base", + }) + ) + ); + } + } catch { + setTeachers([]); + } + }; + + loadTeachers(); + }, []); + + const formatTime = (date) => { + return date.toLocaleTimeString("es-MX", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: true, + }); + }; + + const formatDate = (date) => { + return date.toLocaleDateString("es-MX", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); + }; + + const getHourDegrees = () => (time.getHours() % 12) * 30 + time.getMinutes() * 0.5; + const getMinuteDegrees = () => time.getMinutes() * 6 + time.getSeconds() * 0.1; + const getSecondDegrees = () => time.getSeconds() * 6; + + const normalizeMatricula = (value) => { + const raw = String(value ?? "").trim(); + if (!raw) return ""; + if (!/^\d+$/.test(raw)) return raw; + const normalized = raw.replace(/^0+/, ""); + return normalized || "0"; + }; + + const handleAction = async (type) => { + const matriculaActiva = (selectedMatricula || matricula).trim(); + if (!matriculaActiva.trim()) { + setActionMessage("⚠️ Escribe o selecciona una matrícula antes de registrar"); + setShowMessage(true); + setTimeout(() => setShowMessage(false), 3000); + return; + } + + const existeMatricula = teachers.some( + (teacher) => + normalizeMatricula(teacher.matricula) === normalizeMatricula(matriculaActiva) + ); + + if (!existeMatricula) { + setActionMessage(`❌ La matrícula ${matriculaActiva} no existe en la base de datos`); + setShowMessage(true); + setTimeout(() => setShowMessage(false), 3500); + return; + } + + try { + const response = await fetch("https://ontimeclock.onrender.com/api/records", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + matricula: matriculaActiva, + tipo_registro: type, + }), + }); + + const result = await response.json().catch(() => ({})); + + if (!response.ok) { + setActionMessage(`❌ ${result.error || "No se pudo registrar"}`); + setShowMessage(true); + setTimeout(() => setShowMessage(false), 3500); + return; + } + + const now = new Date(); + const timeStr = now.toLocaleTimeString("es-MX", { + hour: "2-digit", + minute: "2-digit", + hour12: true, + }); + + setLastAction(type); + setActionMessage( + type === "entrada" + ? `✅ Entrada registrada — ${timeStr} | Matrícula: ${matriculaActiva}` + : `🔴 Salida aplicada — ${timeStr} | Matrícula: ${matriculaActiva}` + ); + setShowMessage(true); + setPulse(true); + setTimeout(() => setPulse(false), 600); + setTimeout(() => setShowMessage(false), 4000); + } catch { + setActionMessage("❌ No se pudo conectar con el servidor"); + setShowMessage(true); + setTimeout(() => setShowMessage(false), 3500); + } + }; + + const dateStr = formatDate(time); + const capitalizedDate = dateStr.charAt(0).toUpperCase() + dateStr.slice(1); + + return ( +
+ {/* Background grid pattern */} +
+ + {/* Top bar */} +
+
+
🎓
+
+
Sistema de Control
+
Universidad — Asistencia Docente
+
+
+ + {/* Important messages box */} +
setShowAllMsgs(true)}> +
+ + AVISOS IMPORTANTES + Ver todos → +
+
+ {IMPORTANT_MESSAGES[msgIndex]} +
+
+ {IMPORTANT_MESSAGES.map((_, i) => ( +
+ ))} +
+
+
+ + {/* Main clock area */} +
+ {/* Analog clock */} +
+
+ {/* Hour markers */} + {[...Array(12)].map((_, i) => ( +
+
+
+ ))} + + {/* Hour numbers */} + {[12, 3, 6, 9].map((num, i) => { + const angle = i * 90 - 90; + const rad = (angle * Math.PI) / 180; + const r = 53; + const x = 50 + r * Math.cos(rad); + const y = 50 + r * Math.sin(rad); + return ( +
+ {num} +
+ ); + })} + + {/* Hour hand */} +
+ {/* Minute hand */} +
+ {/* Second hand */} +
+ {/* Center dot */} +
+
+
+ +
+ { + const value = e.target.value; + setMatricula(value); + const existe = teachers.some( + (teacher) => + normalizeMatricula(teacher.matricula) === normalizeMatricula(value) + ); + setSelectedMatricula(existe ? value.trim() : ""); + }} + style={styles.matriculaInput} + /> + + +
+ +
+
+ {actionMessage} +
+
+ + {/* Buttons */} +
+ + + +
+ +
+ Último registro:{" "} + + {lastAction === "entrada" ? "Entrada" : "Salida"} + +
+
+ + {/* Footer */} +
+ + © {time.getFullYear()} — Sistema de Asistencia Universitaria + +
+ + {/* All messages modal */} + {showAllMsgs && ( +
setShowAllMsgs(false)}> +
e.stopPropagation()}> +
+
+ + TABLERO DE AVISOS +
+ +
+
+ {IMPORTANT_MESSAGES.map((msg, i) => ( +
+
{String(i + 1).padStart(2, "0")}
+
{msg}
+ ))}
- ) - } +
+ {IMPORTANT_MESSAGES.length} avisos activos +
+
+
+ )} +
+ ); +} -export default Clock; \ No newline at end of file +const styles = { + root: { + minHeight: "100vh", + background: "#1a1a32", + display: "flex", + flexDirection: "column", + alignItems: "center", + fontFamily: "'Segoe UI', 'Trebuchet MS', sans-serif", + position: "relative", + overflow: "hidden", + color: "#fff", + }, + gridPattern: { + position: "absolute", + inset: 0, + backgroundImage: ` + linear-gradient(rgba(240,192,47,0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(240,192,47,0.04) 1px, transparent 1px) + `, + backgroundSize: "40px 40px", + pointerEvents: "none", + }, + topBar: { + width: "100%", + maxWidth: "1200px", + display: "flex", + justifyContent: "space-between", + alignItems: "flex-start", + padding: "24px 32px 0", + boxSizing: "border-box", + zIndex: 1, + }, + logoArea: { + display: "flex", + alignItems: "center", + gap: "12px", + }, + logoIcon: { + fontSize: "2rem", + lineHeight: 1, + }, + logoTitle: { + fontSize: "1rem", + fontWeight: 700, + color: "#f0c02f", + letterSpacing: "0.08em", + textTransform: "uppercase", + }, + logoSub: { + fontSize: "0.72rem", + color: "rgba(255,255,255,0.45)", + letterSpacing: "0.05em", + marginTop: "2px", + }, + msgBox: { + background: "rgba(255,255,255,0.04)", + border: "1px solid rgba(240,192,47,0.25)", + borderRadius: "12px", + padding: "14px 18px", + maxWidth: "360px", + minWidth: "280px", + backdropFilter: "blur(10px)", + boxShadow: "0 4px 24px rgba(0,0,0,0.3)", + }, + msgHeader: { + display: "flex", + alignItems: "center", + gap: "8px", + marginBottom: "8px", + }, + msgDot: { + width: "8px", + height: "8px", + borderRadius: "50%", + background: "#f0c02f", + boxShadow: "0 0 8px #f0c02f", + animation: "none", + display: "inline-block", + }, + msgLabel: { + fontSize: "0.65rem", + fontWeight: 700, + color: "#f0c02f", + letterSpacing: "0.12em", + textTransform: "uppercase", + }, + msgText: { + fontSize: "0.82rem", + color: "rgba(255,255,255,0.85)", + lineHeight: 1.5, + minHeight: "38px", + }, + msgDots: { + display: "flex", + gap: "5px", + marginTop: "10px", + }, + dotIndicator: { + width: "6px", + height: "6px", + borderRadius: "50%", + transition: "background 0.3s", + }, + centerArea: { + flex: 1, + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + zIndex: 1, + paddingBottom: "20px", + gap: "0px", + }, + clockRing: { + width: "260px", + height: "260px", + borderRadius: "50%", + background: "linear-gradient(135deg, rgba(240,192,47,0.12), rgba(26,26,50,0.8))", + border: "3px solid rgba(240,192,47,0.35)", + boxShadow: ` + 0 0 0 1px rgba(240,192,47,0.1), + 0 0 40px rgba(240,192,47,0.1), + inset 0 0 60px rgba(26,26,50,0.6) + `, + display: "flex", + alignItems: "center", + justifyContent: "center", + transition: "box-shadow 0.3s ease", + marginBottom: "24px", + }, + clockRingPulse: { + boxShadow: ` + 0 0 0 4px rgba(240,192,47,0.3), + 0 0 60px rgba(240,192,47,0.3), + inset 0 0 60px rgba(26,26,50,0.6) + `, + }, + clockFace: { + width: "230px", + height: "230px", + borderRadius: "50%", + background: "radial-gradient(circle at 40% 35%, #23234a, #1a1a32 70%)", + position: "relative", + border: "1px solid rgba(240,192,47,0.15)", + }, + hourMarker: { + position: "absolute", + top: "50%", + left: "50%", + width: "2px", + height: "115px", + marginLeft: "-1px", + transformOrigin: "top center", + display: "flex", + justifyContent: "center", + }, + markerTick: { + width: "2px", + borderRadius: "1px", + position: "absolute", + top: "6px", + }, + hourNum: { + position: "absolute", + fontSize: "0.7rem", + fontWeight: 700, + color: "rgba(240,192,47,0.7)", + letterSpacing: "0.02em", + }, + hand: { + position: "absolute", + bottom: "50%", + left: "50%", + transformOrigin: "bottom center", + borderRadius: "4px", + }, + hourHand: { + width: "5px", + height: "62px", + background: "linear-gradient(to top, #f0c02f, rgba(240,192,47,0.6))", + marginLeft: "-2.5px", + boxShadow: "0 0 6px rgba(240,192,47,0.5)", + }, + minuteHand: { + width: "3px", + height: "85px", + background: "linear-gradient(to top, #fff, rgba(255,255,255,0.5))", + marginLeft: "-1.5px", + boxShadow: "0 0 4px rgba(255,255,255,0.3)", + }, + secondHand: { + width: "1.5px", + height: "95px", + background: "linear-gradient(to top, #f0c02f, #ff6060)", + marginLeft: "-0.75px", + boxShadow: "0 0 4px rgba(255,96,96,0.6)", + }, + centerDot: { + position: "absolute", + top: "50%", + left: "50%", + width: "10px", + height: "10px", + borderRadius: "50%", + background: "#f0c02f", + transform: "translate(-50%, -50%)", + boxShadow: "0 0 10px rgba(240,192,47,0.8)", + zIndex: 10, + }, + digitalTime: { + fontSize: "3.8rem", + fontWeight: 300, + letterSpacing: "0.06em", + color: "#ffffff", + fontVariantNumeric: "tabular-nums", + textShadow: "0 0 30px rgba(240,192,47,0.3)", + lineHeight: 1, + marginBottom: "8px", + }, + dateText: { + fontSize: "0.9rem", + color: "rgba(240,192,47,0.75)", + letterSpacing: "0.1em", + textTransform: "uppercase", + marginBottom: "28px", + }, + feedbackBanner: { + background: "rgba(240,192,47,0.12)", + border: "1px solid rgba(240,192,47,0.4)", + borderRadius: "10px", + padding: "12px 28px", + color: "#fff", + marginBottom: "20px", + transition: "all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)", + backdropFilter: "blur(8px)", + minHeight: "46px", + display: "flex", + alignItems: "center", + justifyContent: "center", + minWidth: "320px", + }, + messageSlot: { + width: "420px", + maxWidth: "90vw", + height: "56px", + marginBottom: "10px", + display: "flex", + alignItems: "center", + justifyContent: "center", + flexShrink: 0, + }, + inlineToast: { + background: "rgba(240,192,47,0.14)", + border: "1px solid rgba(240,192,47,0.45)", + borderRadius: "10px", + padding: "10px 16px", + color: "#fff", + backdropFilter: "blur(10px)", + minHeight: "44px", + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "100%", + height: "44px", + boxShadow: "0 10px 28px rgba(0,0,0,0.35)", + transition: "opacity 0.35s ease, transform 0.35s ease", + pointerEvents: "none", + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + }, + inputBlock: { + display: "flex", + flexDirection: "column", + gap: "10px", + width: "420px", + maxWidth: "90vw", + marginBottom: "16px", + flexShrink: 0, + }, + matriculaInput: { + width: "100%", + background: "rgba(255,255,255,0.06)", + border: "1px solid rgba(240,192,47,0.35)", + borderRadius: "10px", + color: "#fff", + padding: "11px 12px", + fontSize: "0.88rem", + outline: "none", + }, + matriculaSelect: { + width: "100%", + background: "rgba(255,255,255,0.06)", + border: "1px solid rgba(240,192,47,0.35)", + borderRadius: "10px", + color: "#fff", + padding: "11px 12px", + fontSize: "0.88rem", + outline: "none", + }, + matriculaOption: { + color: "#1a1a32", + background: "#ffffff", + }, + buttonRow: { + display: "flex", + gap: "20px", + width: "420px", + maxWidth: "90vw", + marginBottom: "16px", + flexShrink: 0, + }, + btnEntrada: { + padding: "16px 44px", + fontSize: "0.9rem", + fontWeight: 700, + letterSpacing: "0.12em", + background: "#f0c02f", + color: "#1a1a32", + border: "none", + borderRadius: "10px", + cursor: "pointer", + boxShadow: "0 4px 20px rgba(240,192,47,0.25)", + transition: "transform 0.2s ease, box-shadow 0.2s ease", + display: "flex", + alignItems: "center", + gap: "10px", + textTransform: "uppercase", + }, + btnSalida: { + padding: "16px 44px", + fontSize: "0.9rem", + fontWeight: 700, + letterSpacing: "0.12em", + background: "transparent", + color: "#fff", + border: "2px solid rgba(255,255,255,0.3)", + borderRadius: "10px", + cursor: "pointer", + boxShadow: "0 4px 20px rgba(255,80,80,0.2)", + transition: "transform 0.2s ease, box-shadow 0.2s ease", + display: "flex", + alignItems: "center", + gap: "10px", + textTransform: "uppercase", + }, + btnIcon: { + fontSize: "0.75rem", + }, + lastActionInfo: { + fontSize: "0.78rem", + color: "rgba(255,255,255,0.35)", + letterSpacing: "0.05em", + minHeight: "18px", + }, + footer: { + padding: "16px", + zIndex: 1, + }, + footerText: { + fontSize: "0.7rem", + color: "rgba(255,255,255,0.2)", + letterSpacing: "0.08em", + textTransform: "uppercase", + }, + msgViewAll: { + marginLeft: "auto", + fontSize: "0.62rem", + color: "rgba(240,192,47,0.55)", + letterSpacing: "0.06em", + fontWeight: 600, + }, + modalOverlay: { + position: "fixed", + inset: 0, + background: "rgba(10,10,25,0.75)", + backdropFilter: "blur(6px)", + display: "flex", + alignItems: "center", + justifyContent: "center", + zIndex: 100, + }, + modalBox: { + background: "#1e1e3a", + border: "1px solid rgba(240,192,47,0.3)", + borderRadius: "16px", + width: "480px", + maxWidth: "90vw", + boxShadow: "0 24px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(240,192,47,0.1)", + overflow: "hidden", + }, + modalHeader: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "18px 22px", + borderBottom: "1px solid rgba(240,192,47,0.15)", + background: "rgba(240,192,47,0.05)", + }, + modalTitleRow: { + display: "flex", + alignItems: "center", + gap: "10px", + }, + modalTitle: { + fontSize: "0.75rem", + fontWeight: 700, + color: "#f0c02f", + letterSpacing: "0.14em", + }, + modalClose: { + background: "rgba(255,255,255,0.08)", + border: "1px solid rgba(255,255,255,0.12)", + borderRadius: "6px", + color: "rgba(255,255,255,0.6)", + cursor: "pointer", + fontSize: "0.85rem", + width: "28px", + height: "28px", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: 0, + }, + modalList: { + padding: "12px 0", + }, + modalItem: { + display: "flex", + alignItems: "flex-start", + gap: "14px", + padding: "14px 22px", + borderBottom: "1px solid rgba(255,255,255,0.05)", + transition: "background 0.2s", + }, + modalItemNum: { + fontSize: "0.65rem", + fontWeight: 700, + color: "rgba(240,192,47,0.45)", + letterSpacing: "0.08em", + paddingTop: "2px", + minWidth: "22px", + }, + modalItemText: { + fontSize: "0.88rem", + color: "rgba(255,255,255,0.85)", + lineHeight: 1.55, + }, + modalFooter: { + padding: "12px 22px", + fontSize: "0.7rem", + color: "rgba(255,255,255,0.25)", + letterSpacing: "0.06em", + borderTop: "1px solid rgba(240,192,47,0.1)", + textAlign: "right", + }, +}; \ No newline at end of file diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index a2476e4..b33af0d 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -1,31 +1,721 @@ -function Dashboard() { - return ( -
-

Panel de Control

- {/* Cards */} -
-
-

Maestros Activos

-

60

+import { useState, useEffect } from "react"; + +// ── Paleta oficial ────────────────────────────────────────────── +const C = { + blue: "#1a3a8f", + blueMid: "#2252c8", + blueLight: "#dde9ff", + yellow: "#f5c400", + yellowSoft: "#fff8d6", + white: "#ffffff", + bg: "#f0f4ff", + text: "#0d1f4e", + muted: "#64748b", +}; + +// ── Mock data ─────────────────────────────────────────────────── +const dailyData = [ + { hora: "7am", asistencias: 4 }, + { hora: "8am", asistencias: 12 }, + { hora: "9am", asistencias: 18 }, + { hora: "10am", asistencias: 10 }, + { hora: "11am", asistencias: 7 }, + { hora: "12pm", asistencias: 5 }, + { hora: "1pm", asistencias: 3 }, +]; + +const monthlyData = [ + { mes: "Ene", asistencias: 92, faltas: 8 }, + { mes: "Feb", asistencias: 88, faltas: 12 }, + { mes: "Mar", asistencias: 95, faltas: 5 }, + { mes: "Abr", asistencias: 90, faltas: 10 }, + { mes: "May", asistencias: 85, faltas: 15 }, + { mes: "Jun", asistencias: 78, faltas: 22 }, +]; + +const recentActivity = [ + { nombre: "Leydi Xequeb", materia: "Matemáticas", hora: "08:02 AM", estado: "Entrada", tipo: "entrada" }, + { nombre: "Irvin Chan", materia: "Programación", hora: "08:15 AM", estado: "Entrada", tipo: "entrada" }, + { nombre: "Marcos Riviera", materia: "Fe y Mundo", hora: "09:05 AM", estado: "Retardo", tipo: "retardo" }, + { nombre: "Ana Pérez", materia: "Inglés", hora: "09:30 AM", estado: "Entrada", tipo: "entrada" }, + { nombre: "Luis Méndez", materia: "Historia", hora: "10:00 AM", estado: "Falta", tipo: "falta" }, +]; + +const teachers = [ + { nombre: "Ana Pérez", materia: "Inglés", asistencia: 99, avatar: "AP" }, + { nombre: "Leydi Xequeb", materia: "Matemáticas", asistencia: 97, avatar: "LX" }, + { nombre: "Irvin Chan", materia: "Programación", asistencia: 94, avatar: "IC" }, + { nombre: "Marcos Riviera", materia: "Fe y Mundo", asistencia: 88, avatar: "MR" }, +]; + +// ── Contador animado ──────────────────────────────────────────── +function Counter({ target }) { + const [count, setCount] = useState(0); + useEffect(() => { + let v = 0; + const step = target / 40; + const t = setInterval(() => { + v += step; + if (v >= target) { setCount(target); clearInterval(t); } + else setCount(Math.floor(v)); + }, 28); + return () => clearInterval(t); + }, [target]); + return <>{count}; +} + +// ── Reloj digital ─────────────────────────────────────────────── +function Clock() { + const [time, setTime] = useState(new Date()); + useEffect(() => { + const interval = setInterval(() => setTime(new Date()), 1000); + return () => clearInterval(interval); + }, []); + + const hours = String(time.getHours()).padStart(2, '0'); + const minutes = String(time.getMinutes()).padStart(2, '0'); + const seconds = String(time.getSeconds()).padStart(2, '0'); + + return ( +
+
+ {hours} + : + {minutes} + : + {seconds} +
+
+ ); +} + +// ── Line chart SVG ────────────────────────────────────────────── +function LineChart({ data, valueKey, labelKey, height = 130 }) { + const [hovered, setHovered] = useState(null); + const W = 560, H = height - 24; + const pad = { t: 16, r: 16, b: 8, l: 28 }; + const innerW = W - pad.l - pad.r; + const innerH = H - pad.t - pad.b; + const max = Math.max(...data.map((d) => d[valueKey])); + const min = 0; + + const pts = data.map((d, i) => ({ + x: pad.l + (i / (data.length - 1)) * innerW, + y: pad.t + (1 - (d[valueKey] - min) / (max - min)) * innerH, + val: d[valueKey], + label: d[labelKey], + })); + + // Smooth polyline path + const linePath = pts + .map((p, i) => (i === 0 ? `M ${p.x},${p.y}` : `L ${p.x},${p.y}`)) + .join(" "); + + // Area fill path + const areaPath = + linePath + + ` L ${pts[pts.length - 1].x},${pad.t + innerH} L ${pts[0].x},${pad.t + innerH} Z`; + + // Unique gradient id + const gradId = "lineGrad"; + const areaId = "areaGrad"; + + return ( +
+ + + + + + + + + + + + + {/* Grid lines */} + {[0, 0.25, 0.5, 0.75, 1].map((r, i) => { + const y = pad.t + r * innerH; + return ( + + ); + })} + + {/* Area fill */} + + + {/* Line */} + + + {/* Points + hover */} + {pts.map((p, i) => ( + + {/* Invisible hit area */} + setHovered(i)} + onMouseLeave={() => setHovered(null)} + /> + + {/* Dot */} + + + {/* Tooltip */} + {hovered === i && ( + + + + {p.val} + + {/* Vertical dashed line */} + + + )} + + {/* X label */} + + {p.label} + + + ))} + +
+ ); +} + +// ── Donut chart ───────────────────────────────────────────────── +function DonutChart({ percent, color, size = 72 }) { + const r = 28; + const circ = 2 * Math.PI * r; + const dash = (percent / 100) * circ; + return ( + + + + + {percent}% + + + ); +} + +// ── KPI Card con hover ────────────────────────────────────────── +function KpiCard({ label, value, icon, sub, accent }) { + const [hover, setHover] = useState(false); + return ( +
setHover(true)} + onMouseLeave={() => setHover(false)} + className="rounded-2xl p-5 relative overflow-hidden cursor-default" + style={{ + background: hover + ? `linear-gradient(135deg, ${C.blue} 0%, ${C.blueMid} 100%)` + : C.white, + border: `2px solid ${hover ? C.yellow : C.blueLight}`, + boxShadow: hover + ? `0 8px 32px ${C.blue}40` + : "0 2px 8px rgba(0,0,0,0.06)", + transition: "all 0.25s ease", + }} + > +
+
+
+

+ {label} +

+ + {icon} + +
+

+ +

+

+ {sub} +

+
+
+ ); +} + +// ── DASHBOARD ─────────────────────────────────────────────────── +function Dashboard() { + const [activeTab, setActiveTab] = useState("diario"); + + const now = new Date(); + const dateStr = now.toLocaleDateString("es-MX", { + weekday: "long", year: "numeric", month: "long", day: "numeric", + }); + + return ( +
+ + {/* ── MAIN ── */} +
+ + {/* Header */} +
+
+

+ Panel de Control +

+

{dateStr}

+
+
+
+ + Sistema Activo +
+
+ UNID Playa del Carmen +
+
+
+ + {/* KPI CARDS */} +
+ + + + +
+ + {/* CHARTS */} +
+ + {/* Bar chart */} +
+
+
+

Registros del Día

+

Entradas por hora — hoy

+
+
+ {["diario", "mensual"].map((tab) => ( + + ))} +
+
+ + {activeTab === "diario" ? ( + + ) : ( + + )} + +
+
+
+ Asistencias +
+ {activeTab === "mensual" && ( +
+
+ Faltas +
+ )} +
+
+ + {/* Donuts */} +
+

Indicadores Clave

+

Porcentaje del periodo actual

+
+ {[ + { label: "Tasa de Asistencia", percent: 87, color: C.blueMid }, + { label: "Puntualidad", percent: 94, color: C.yellow }, + { label: "Cobertura Docente", percent: 100, color: "#16a34a" }, + ].map((item, i) => ( +
+ +
+

{item.label}

+

Ene — Jun 2025

+
+ ))} +
+
+
-
-

Asistencias

-

40

+ {/* BOTTOM ROW */} +
+ {/* Reloj */} +
+

Hora Actual

+ +

Tiempo real del sistema

+
+ + {/* Actividad reciente */} +
+
+
+

Actividad Reciente

+

Últimos registros de asistencia

+
+ +
+
+ {recentActivity.map((item, i) => ( +
+
+ {item.nombre.split(" ").map((n) => n[0]).join("").slice(0, 2)} +
+
+

{item.nombre}

+

{item.materia}

+
+
+

{item.hora}

+ + {item.estado} + +
+ ))} +
+
+ + {/* Ranking */} +
+

Ranking Docentes

+

Por tasa de asistencia

-
-

Retardos

-

3

+
+ {teachers.map((t, i) => ( +
+ + #{i + 1} + +
+ {t.avatar} +
+
+
+

{t.nombre}

+

{t.asistencia}%

+
+
+
+
+
+ ))} +
-
-

Faltas

-

1

+ {/* Resumen cuatrimestral */} +
+
+
+

Resumen Cuatrimestral

+
+

Enero — Abril 2025

+
+
+

91%

+

Asistencia

+
+
+

4.2

+

Promedio

-
-
- ) +
+

6

+

Meses

+
+
+
+
+ +
+
+
+ ); } export default Dashboard; \ No newline at end of file diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index a43bb4a..614b9b7 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -2,7 +2,8 @@ function Home(){ return(

Inicio

-

Bienvenido a nuestra app web con React

+

Bienvenido a nuestra app web con

+
) } diff --git a/src/pages/Reports.jsx b/src/pages/Reports.jsx index 98f60fd..a996edb 100644 --- a/src/pages/Reports.jsx +++ b/src/pages/Reports.jsx @@ -1,83 +1,82 @@ - /*function Reports() { - return ( -
-

reportes de incidentes

-
-
-

- Reporte mensual

-

- asistencia general del mes -

-
- -
-

- Reporte por Docente

-

- historial individual por docente -

-
-
-
- ); -} -export default Reports; */ -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { jsPDF } from "jspdf"; import autoTable from "jspdf-autotable"; function Reports() { - - // =============================== - // DATOS DUMMY - // =============================== - - const dummyData = [ - { profesor: "Irvin Ac Chan", fecha: "2026-02-01", asistencias: 1, retardos: 0, faltas: 0 }, - { profesor: "Leydi", fecha: "2026-02-02", asistencias: 1, retardos: 1, faltas: 0 }, - { profesor: "Trejo Rocha", fecha: "2026-02-03", asistencias: 0, retardos: 0, faltas: 1 }, - { profesor: "Mari Eugenia", fecha: "2026-02-04", asistencias: 1, retardos: 0, faltas: 0 }, - { profesor: "Jessica", fecha: "2026-02-05", asistencias: 1, retardos: 1, faltas: 0 }, - { profesor: "Grecia", fecha: "2026-02-06", asistencias: 0, retardos: 0, faltas: 1 }, - { profesor: "Irvin Ac Chan", fecha: "2026-02-07", asistencias: 1, retardos: 0, faltas: 0 }, - { profesor: "Jessica", fecha: "2026-02-08", asistencias: 1, retardos: 0, faltas: 0 }, - ]; - - // =============================== - // ESTADOS - // =============================== - const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); + const [allData, setAllData] = useState([]); const [filteredData, setFilteredData] = useState([]); const [selectedTeacher, setSelectedTeacher] = useState("Todos"); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const [showDetailModal, setShowDetailModal] = useState(false); + const [detailLoading, setDetailLoading] = useState(false); + const [detailError, setDetailError] = useState(""); + const [detailRows, setDetailRows] = useState([]); + const [detailTeacher, setDetailTeacher] = useState(null); + + const teachers = useMemo( + () => ["Todos", ...new Set(allData.map((d) => d.profesor))], + [allData] + ); - const teachers = ["Todos", ...new Set(dummyData.map(d => d.profesor))]; + const handleSearch = async () => { + if (!startDate || !endDate) { + setError("Por favor selecciona ambas fechas"); + return; + } - // =============================== - // FILTRAR - // =============================== + if (startDate > endDate) { + setError("La fecha de inicio no puede ser mayor a la fecha final"); + return; + } - const handleSearch = () => { - if (!startDate || !endDate) return; + setLoading(true); + setError(""); + + try { + const response = await fetch( + `https://ontimeclock.onrender.com/api/reports?startDate=${startDate}&endDate=${endDate}` + ); + + const payload = await response.json().catch(() => []); + + if (!response.ok) { + throw new Error(payload.error || "Error al obtener reporte del servidor"); + } + + const data = Array.isArray(payload) ? payload : []; + setAllData(data); + + let result = data; + if (selectedTeacher !== "Todos") { + result = result.filter((item) => item.profesor === selectedTeacher); + } + + setFilteredData(result); + } catch (err) { + console.error("Error:", err); + setError( + err.message || + "No se pudo conectar con el servidor. Verifica que esté corriendo en https://ontimeclock.onrender.com" + ); + } finally { + setLoading(false); + } + }; - let result = dummyData.filter(item => - item.fecha >= startDate && item.fecha <= endDate - ); + useEffect(() => { + if (allData.length === 0) return; + let result = allData; if (selectedTeacher !== "Todos") { - result = result.filter(item => item.profesor === selectedTeacher); + result = result.filter((item) => item.profesor === selectedTeacher); } setFilteredData(result); - }; - - // =============================== - // EXPORTAR PDF - // =============================== + }, [selectedTeacher, allData]); const exportPDF = () => { if (filteredData.length === 0) return; @@ -85,58 +84,78 @@ function Reports() { const doc = new jsPDF(); doc.setFontSize(16); - doc.text("Reporte de Incidencias de los Docentes", 14, 15); + doc.text("Reporte de Asistencia de los Docentes", 14, 15); + doc.setFontSize(10); + doc.text(`Periodo: ${startDate} al ${endDate}`, 14, 22); + doc.text(`Profesor: ${selectedTeacher}`, 14, 29); autoTable(doc, { - startY: 25, - head: [["Profesor", "Fecha", "Asistencias", "Retardos", "Faltas"]], - body: filteredData.map(item => [ + startY: 35, + head: [["Matricula", "Profesor", "Asistencias", "Faltas", "Dias Evaluados"]], + body: filteredData.map((item) => [ + item.matricula, item.profesor, - item.fecha, item.asistencias, - item.retardos, - item.faltas - ]) + item.faltas, + item.totalDias, + ]), }); - doc.save(`reporte_${startDate}_${endDate}.pdf`); + doc.save(`reporte_asistencia_${startDate}_${endDate}.pdf`); }; - // =============================== - // RENDER - // =============================== + const openTeacherDetail = async (teacherRow) => { + if (!startDate || !endDate) { + setError("Selecciona primero un rango de fechas"); + return; + } + + setShowDetailModal(true); + setDetailLoading(true); + setDetailError(""); + setDetailRows([]); + setDetailTeacher(teacherRow); + + try { + const response = await fetch( + `https://ontimeclock.onrender.com/api/reports/${teacherRow.matricula}/details?startDate=${startDate}&endDate=${endDate}` + ); + const payload = await response.json().catch(() => []); + + if (!response.ok) { + throw new Error(payload.error || "No se pudo obtener el detalle del docente"); + } + + setDetailRows(Array.isArray(payload) ? payload : []); + } catch (err) { + setDetailError(err.message || "Error al cargar detalle del docente"); + } finally { + setDetailLoading(false); + } + }; return (
- - {/* HEADER */}
-

- Reporte de Incidencias de los Docentes -

+

Reporte de Asistencia de Docentes

{filteredData.length > 0 && ( )}
- - {/* FILTROS */}
-
- +
- +
- + { setForm({ ...form, nombre: e.target.value }); setError(""); }} + autoFocus + /> + {error &&

{error}

} +
+
+ + +
+
+ + +
+
+
+ ); +} + +// ─── Toggle ─────────────────────────────────────────────────────────────────── +function Toggle({ checked, onChange }) { + return ( +
onChange(!checked)} + style={{ + width: "42px", height: "22px", borderRadius: "999px", + background: checked ? "rgba(59,130,246,0.8)" : "rgba(209,213,219,0.8)", + border: checked ? "1px solid rgba(59,130,246,0.6)" : "1px solid rgba(0,0,0,0.12)", + position: "relative", cursor: "pointer", transition: "background 0.2s", flexShrink: 0, + }} + > +
+
+ ); +} + +// ─── SectionCard ────────────────────────────────────────────────────────────── +function SectionCard({ icon, title, desc, color = "#3b82f6", children }) { + return ( +
+
+
+ {icon} +
+
+

{title}

+

{desc}

+
+
+ {children} +
+ ); +} + +// ─── RolBadge ───────────────────────────────────────────────────────────────── +function RolBadge({ permisos }) { + if (permisos === "Total") return {permisos}; + if (permisos === "Parcial") return {permisos}; + return {permisos}; +} + +// ─── COMPONENTE PRINCIPAL ───────────────────────────────────────────────────── +export default function SystemConfig() { + + // ── Toasts ── + const [toasts, setToasts] = useState([]); + const addToast = (message, type = "success") => { + const id = Date.now(); + setToasts((prev) => [...prev, { id, message, type }]); + setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), 3500); + }; + + // ── ID del registro en BD ── + const [configId, setConfigId] = useState(null); + + // ── Estado: Ajustes Institucionales ── + const INITIAL_INSTITUCION = { + nombre: "", + codigo: "", + zona: "America/Mexico_City", + idioma: "es", + }; + const [institucion, setInstitucion] = useState(INITIAL_INSTITUCION); + const [savedInstitucion, setSavedInstitucion] = useState(INITIAL_INSTITUCION); + + // ── Estado: Tolerancia ── + const INITIAL_TOLERANCIA = { entrada: 10, salida: 5, inasistencias: 3 }; + const [tolerancia, setTolerancia] = useState(INITIAL_TOLERANCIA); + const [savedTolerancia, setSavedTolerancia] = useState(INITIAL_TOLERANCIA); + + // ── Estado: Notificaciones ── + const INITIAL_NOTIF = { + emailAsistencia: true, + emailReportes: true, + pushAlertas: false, + resumenDiario: true, + alertasFaltas: true, + notifDocentes: false, + }; + const [notificaciones, setNotificaciones] = useState(INITIAL_NOTIF); + const [savedNotificaciones, setSavedNotificaciones] = useState(INITIAL_NOTIF); + + // ── Estado: Horario base ── + const INITIAL_HORARIO = { + 0: { activo: true, inicio: "07:00", fin: "18:00", id: null }, + 1: { activo: true, inicio: "07:00", fin: "18:00", id: null }, + 2: { activo: true, inicio: "07:00", fin: "18:00", id: null }, + 3: { activo: true, inicio: "07:00", fin: "18:00", id: null }, + 4: { activo: true, inicio: "07:00", fin: "18:00", id: null }, + 5: { activo: false, inicio: "08:00", fin: "14:00", id: null }, + 6: { activo: false, inicio: "08:00", fin: "14:00", id: null }, + }; + const [horarioPorDia, setHorarioPorDia] = useState(INITIAL_HORARIO); + const [savedHorario, setSavedHorario] = useState(INITIAL_HORARIO); + const [diaSeleccionado, setDiaSeleccionado] = useState(0); + + // ── Estado: Roles ── + const [roles, setRoles] = useState(MOCK_ROLES); + const [showModalRol, setShowModalRol] = useState(false); + + // ───────────────────────────────────────────────────────────────────────────── + // CARGAR DATOS DE LA BD AL INICIAR + // ───────────────────────────────────────────────────────────────────────────── + useEffect(() => { + // Cargar configuración general + fetch(API) + .then((r) => r.json()) + .then((data) => { + if (data && data.length > 0) { + const c = data[0]; + setConfigId(c.id); + + const inst = { + nombre: c.nombre_institucion || "", + codigo: c.codigo_institucional || "", + zona: c.zona_horaria || "America/Mexico_City", + idioma: c.idioma || "es", + }; + setInstitucion(inst); + setSavedInstitucion(inst); + + const tol = { + entrada: c.tolerancia_entrada ?? 10, + salida: c.tolerancia_salida ?? 5, + inasistencias: c.inasistencias_permitidas ?? 3, + }; + setTolerancia(tol); + setSavedTolerancia(tol); + + const notif = { + emailAsistencia: c.email_asistencia ?? true, + emailReportes: c.email_reportes ?? true, + pushAlertas: c.push_alertas ?? false, + resumenDiario: c.resumen_diario ?? true, + alertasFaltas: c.alertas_faltas ?? true, + notifDocentes: c.notif_docentes ?? false, + }; + setNotificaciones(notif); + setSavedNotificaciones(notif); + } + }) + .catch(() => addToast("No se pudo conectar con el servidor.", "error")); + + // Cargar días laborales + fetch(`${API}/work-days`) + .then((r) => r.json()) + .then((data) => { + if (data && data.length > 0) { + const nuevo = { ...INITIAL_HORARIO }; + data.forEach((d) => { + const idx = d.dia; + if (idx >= 0 && idx <= 6) { + nuevo[idx] = { + activo: d.activo, + inicio: d.hora_inicio ? d.hora_inicio.slice(0, 5) : "07:00", + fin: d.hora_fin ? d.hora_fin.slice(0, 5) : "18:00", + id: d.id, + }; + } + }); + setHorarioPorDia(nuevo); + setSavedHorario(JSON.parse(JSON.stringify(nuevo))); + } + }) + .catch(() => addToast("No se pudieron cargar los días laborales.", "error")); + }, []); + + // ───────────────────────────────────────────────────────────────────────────── + // HELPERS + // ───────────────────────────────────────────────────────────────────────────── + const buildConfigBody = (instOv, tolOv, notifOv) => { + const inst = instOv || institucion; + const tol = tolOv || tolerancia; + const notif = notifOv || notificaciones; + return { + nombre_institucion: inst.nombre, + codigo_institucional: inst.codigo, + idioma: inst.idioma, + zona_horaria: inst.zona, + tolerancia_entrada: tol.entrada, + tolerancia_salida: tol.salida, + inasistencias_permitidas: tol.inasistencias, + email_asistencia: notif.emailAsistencia, + email_reportes: notif.emailReportes, + push_alertas: notif.pushAlertas, + resumen_diario: notif.resumenDiario, + alertas_faltas: notif.alertasFaltas, + notif_docentes: notif.notifDocentes, + }; + }; + + const saveConfig = async (body) => { + if (configId) { + const res = await fetch(`${API}/${configId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error("Error al actualizar"); + return res.json(); + } else { + const res = await fetch(API, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error("Error al crear"); + const data = await res.json(); + setConfigId(data.id); + return data; + } + }; + + // ───────────────────────────────────────────────────────────────────────────── + // HANDLERS + // ───────────────────────────────────────────────────────────────────────────── + const toggleDia = (idx) => { + setHorarioPorDia((prev) => ({ ...prev, [idx]: { ...prev[idx], activo: !prev[idx].activo } })); + setDiaSeleccionado(idx); + }; + const actualizarHorarioDia = (idx, campo, valor) => { + setHorarioPorDia((prev) => ({ ...prev, [idx]: { ...prev[idx], [campo]: valor } })); + }; + const toggleRolEstado = (id) => { + setRoles(roles.map((r) => + r.id === id ? { ...r, estado: r.estado === "activo" ? "inactivo" : "activo" } : r + )); + }; + const toggleNotif = (key) => { + setNotificaciones((prev) => ({ ...prev, [key]: !prev[key] })); + }; + + const handleGuardarCambios = async () => { + try { + await saveConfig(buildConfigBody(institucion, null, null)); + setSavedInstitucion({ ...institucion }); + addToast("Ajustes institucionales guardados correctamente.", "success"); + } catch { + addToast("Error al guardar los ajustes institucionales.", "error"); + } + }; + const handleCancelarInstitucion = () => { + setInstitucion({ ...savedInstitucion }); + addToast("Cambios descartados.", "info"); + }; + + const handleAplicarTolerancia = async () => { + try { + await saveConfig(buildConfigBody(null, tolerancia, null)); + setSavedTolerancia({ ...tolerancia }); + addToast(`Tolerancia aplicada: entrada ${tolerancia.entrada} min, salida ${tolerancia.salida} min.`, "success"); + } catch { + addToast("Error al guardar la tolerancia.", "error"); + } + }; + const handleRestablecerTolerancia = () => { + setTolerancia({ ...savedTolerancia }); + addToast("Tolerancia restablecida a los valores guardados.", "info"); + }; + + // ── GUARDAR HORARIO (work_days) ── ✅ Sin duracion_bloque + const handleGuardarHorario = async () => { + try { + const updates = Object.entries(horarioPorDia).map(([idx, dia]) => { + if (dia.id) { + // Registro existente → PUT + return fetch(`${API}/work-days/${dia.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + nombre: DIAS_SEMANA[idx], + dia: Number(idx), + hora_inicio: dia.inicio, + hora_fin: dia.fin, + activo: dia.activo, + }), + }).then((res) => { + if (!res.ok) throw new Error(`Error actualizando día ${idx}`); + return res.json(); + }); + } + // Sin id → POST + return fetch(`${API}/work-days`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + nombre: DIAS_SEMANA[idx], + dia: Number(idx), + hora_inicio: dia.inicio, + hora_fin: dia.fin, + activo: dia.activo, + }), + }).then((res) => { + if (!res.ok) throw new Error(`Error creando día ${idx}`); + return res.json(); + }).then((created) => { + setHorarioPorDia((prev) => ({ + ...prev, + [idx]: { ...prev[idx], id: created.id }, + })); + }); + }); + + await Promise.all(updates); + setSavedHorario(JSON.parse(JSON.stringify(horarioPorDia))); + const activos = DIAS_SEMANA.filter((_, i) => horarioPorDia[i].activo).join(", "); + addToast(`Horario guardado. Días activos: ${activos || "ninguno"}.`, "success"); + } catch { + addToast("Error al guardar el horario.", "error"); + } + }; + const handleCancelarHorario = () => { + setHorarioPorDia(JSON.parse(JSON.stringify(savedHorario))); + addToast("Cambios de horario descartados.", "info"); + }; + + const handleNuevoRol = () => setShowModalRol(true); + const handleGuardarRol = ({ nombre, permisos }) => { + const nuevoRol = { id: Date.now(), nombre: nombre.trim(), permisos, usuarios: 0, estado: "activo" }; + setRoles((prev) => [...prev, nuevoRol]); + setShowModalRol(false); + addToast(`Rol "${nombre.trim()}" creado con permisos: ${permisos}.`, "success"); + }; + + const handleGuardarPreferencias = async () => { + try { + await saveConfig(buildConfigBody(null, null, notificaciones)); + setSavedNotificaciones({ ...notificaciones }); + const activas = Object.values(notificaciones).filter(Boolean).length; + addToast(`Preferencias guardadas. ${activas} notificaciones activas.`, "success"); + } catch { + addToast("Error al guardar las preferencias.", "error"); + } + }; + const handleRestablecerPreferencias = () => { + setNotificaciones({ ...savedNotificaciones }); + addToast("Preferencias restablecidas.", "info"); + }; + + // ─── RENDER ─────────────────────────────────────────────────────────────────── + return ( +
+ + {showModalRol && ( + setShowModalRol(false)} + onGuardar={handleGuardarRol} + /> + )} + +
+

Configuración del Sistema

+

+ Administre los parámetros globales de operación institucional. +

+
+ + {/* ══ SECCIÓN 1 & 2 ══ */} +
+ + +
+ + setInstitucion({ ...institucion, nombre: e.target.value })} /> +
+
+
+ + setInstitucion({ ...institucion, codigo: e.target.value })} /> +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + {tolerancia.entrada} min +
+ setTolerancia({ ...tolerancia, entrada: Number(e.target.value) })} /> +
+ 0 min30 min +
+
+ +
+
+ + {tolerancia.salida} min +
+ setTolerancia({ ...tolerancia, salida: Number(e.target.value) })} /> +
+ 0 min20 min +
+
+ +
+ +
+ + +
+ +
+ ⚠️ +

+ Los cambios en tolerancia afectarán los registros futuros. Los datos históricos no se modificarán. +

+
+ +
+ + +
+ +
+ + {/* ══ SECCIÓN 3: Horario Base ══ */} +
+ +

Selecciona un día para configurar su horario

+
+ {DIAS_SEMANA.map((dia, idx) => ( +
setDiaSeleccionado(idx)} + style={{ + ...styles.dayCell, + ...(horarioPorDia[idx].activo ? styles.dayCellActive : styles.dayCellInactive), + outline: diaSeleccionado === idx ? "2px solid #10b981" : "none", + outlineOffset: "2px", + }}> +
{dia}
+
+ {horarioPorDia[idx].activo ? "✓" : "—"} +
+
+ ))} +
+ +
+ +
+
+

+ {["Lunes","Martes","Miércoles","Jueves","Viernes","Sábado","Domingo"][diaSeleccionado]} +

+
+ + {horarioPorDia[diaSeleccionado].activo ? "Activo" : "Inactivo"} + + toggleDia(diaSeleccionado)} /> +
+
+
+
+ + actualizarHorarioDia(diaSeleccionado, "inicio", e.target.value)} /> +
+
+ + actualizarHorarioDia(diaSeleccionado, "fin", e.target.value)} /> +
+
+
+ +
+

Resumen semanal

+
+ {DIAS_SEMANA.map((dia, idx) => ( + horarioPorDia[idx].activo && ( +
setDiaSeleccionado(idx)} style={{ + display: "flex", justifyContent: "space-between", alignItems: "center", + fontSize: "0.82rem", padding: "0.35rem 0.6rem", + background: diaSeleccionado === idx ? "rgba(16,185,129,0.08)" : "transparent", + borderRadius: "6px", cursor: "pointer", + }}> + + {["Lunes","Martes","Miércoles","Jueves","Viernes","Sábado","Domingo"][idx]} + + + {horarioPorDia[idx].inicio} – {horarioPorDia[idx].fin} + +
+ ) + ))} +
+
+ +
+ + +
+ +
+ + {/* ══ SECCIÓN 4 & 5 ══ */} +
+ + + + + + + + + + + + + + {roles.map((rol) => ( + + + + + + + + ))} + +
RolPermisosUsuariosEstadoAcción
{rol.nombre}{rol.usuarios} + {rol.estado === "activo" + ? Activo + : Inactivo} + + +
+
+ +
+
+ + + {[ + { key: "emailAsistencia", label: "Notificaciones de asistencia", sub: "Email al registrar una asistencia" }, + { key: "emailReportes", label: "Reportes periódicos", sub: "Resúmenes semanales por correo" }, + { key: "pushAlertas", label: "Alertas push", sub: "Notificaciones en el navegador" }, + { key: "resumenDiario", label: "Resumen diario", sub: "Email con el resumen del día" }, + { key: "alertasFaltas", label: "Alertas por faltas excesivas", sub: "Notificar al superar el límite" }, + { key: "notifDocentes", label: "Notificar a docentes", sub: "Avisar al docente sobre ausencias" }, + ].map(({ key, label, sub }) => ( +
+
+
{label}
+
{sub}
+
+ toggleNotif(key)} /> +
+ ))} +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/Teachers.jsx b/src/pages/Teachers.jsx index c6ecaad..17ce82e 100644 --- a/src/pages/Teachers.jsx +++ b/src/pages/Teachers.jsx @@ -1,26 +1,1361 @@ -function Teachers() { - return ( +import { useEffect, useMemo, useState } from "react"; + +const avatarColors = [ + ["#1E3A8A", "#FBBF24"], + ["#1D4ED8", "#F59E0B"], + ["#1E40AF", "#FCD34D"], + ["#1a3270", "#FBBF24"], + ["#2563EB", "#F59E0B"], + ["#1E3A8A", "#FDE68A"], +]; + +const statusConfig = { + Activo: { bg: "#D1FAE5", color: "#065F46", dot: "#10B981" }, + Inactivo: { bg: "#FEE2E2", color: "#991B1B", dot: "#EF4444" }, + Licencia: { bg: "#FEF3C7", color: "#92400E", dot: "#F59E0B" }, +}; + +export default function Teachers() { + const [teachers, setTeachers] = useState([]); + const [search, setSearch] = useState(""); + const [statusFilter, setStatusFilter] = useState("Todos"); + const [showModal, setShowModal] = useState(false); + const [modalMode, setModalMode] = useState("add"); // "add" | "view" | "edit" + const [selectedTeacher, setSelectedTeacher] = useState(null); + const [form, setForm] = useState({ + matricula: "", + name: "", + subject: "", + email: "", + phone: "", + degree: "", + status: "Activo", + }); + const [sortField, setSortField] = useState("name"); + const [sortDir, setSortDir] = useState("asc"); + + //Extraer los datos de la endPoint /api/teachers + useEffect(() => { + const fetchTeachers = async () => { + try { + const response = await fetch( + "https://ontimeclock.onrender.com/api/teachers", + ); + const data = await response.json(); + setTeachers(Array.isArray(data) ? data : data.data || []); + } catch (error) { + console.error("Error al cargar datos", error); + setTeachers([]); + } + }; + fetchTeachers(); + }, []); + + const filtered = teachers + .filter( + (t) => + statusFilter === "Todos" || + t.status?.toLowerCase() === statusFilter.toLowerCase(), + ) + .filter( + (t) => + (t.name || "").toLowerCase().includes(search.toLowerCase()) || + (t.subject || "").toLowerCase().includes(search.toLowerCase()) || + (t.email || "").toLowerCase().includes(search.toLowerCase()), + ) + .sort((a, b) => { + const va = a[sortField] || ""; + const vb = b[sortField] || ""; + return sortDir === "asc" ? va.localeCompare(vb) : vb.localeCompare(va); + }); + + const handleSort = (field) => { + if (sortField === field) setSortDir((d) => (d === "asc" ? "desc" : "asc")); + else { + setSortField(field); + setSortDir("asc"); + } + }; + + const emptyForm = { + matricula: "", + name: "", + subject: "", + email: "", + phone: "", + degree: "", + status: "Activo", + avatar: "", + }; + + const openAdd = () => { + setModalMode("add"); + setForm(emptyForm); + setShowModal(true); + }; + const openView = (t) => { + setModalMode("view"); + setSelectedTeacher(t); + setShowModal(true); + }; + const openEdit = (t) => { + setModalMode("edit"); + setSelectedTeacher(t); + setForm({ + matricula: t.matricula || "", + name: t.name, + subject: t.subject, + email: t.email, + phone: t.phone, + degree: t.degree, + status: t.status, + avatar: t.avatar || "", + }); + setShowModal(true); + }; + + const handleSave = async () => { + if (!form.name.trim()) return; + + // Validar que status tenga valor + const formData = { + ...form, + status: form.status || "Activo", + }; + + try { + if (modalMode === "edit") { + // Actualizar docente + const response = await fetch( + `https://ontimeclock.onrender.com/api/teachers/${selectedTeacher.id}`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + }, + ); + + const result = await response.json(); + if (!response.ok) + throw new Error(result.message || "Error al actualizar"); + + setTeachers((prev) => + prev.map((t) => (t.id === selectedTeacher.id ? result.data : t)), + ); + } else { + // Crear nuevo docente + const initials = form.name + .split(" ") + .map((w) => w[0]) + .join("") + .slice(0, 2) + .toUpperCase(); + + const response = await fetch( + "https://ontimeclock.onrender.com/api/teachers", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ...formData, avatar: initials }), + }, + ); + + const result = await response.json(); + console.log("Response:", result); + if (!response.ok) throw new Error(result.message || "Error al crear"); + + setTeachers((prev) => [...prev, result.data]); + } + setShowModal(false); + } catch (error) { + console.error("Error al guardar:", error); + alert(`Error: ${error.message}`); + } + }; + + const handleDelete = async (id) => { + if (window.confirm("¿Estás seguro de que deseas eliminar este docente?")) { + try { + const response = await fetch( + `https://ontimeclock.onrender.com/api/teachers/${id}`, + { + method: "DELETE", + }, + ); + + if (!response.ok) throw new Error("Error al eliminar"); + + setTeachers((prev) => prev.filter((t) => t.id !== id)); + } catch (error) { + console.error("Error al eliminar:", error); + alert("Error al eliminar el docente"); + } + } + }; + + const SortIcon = ({ field }) => ( + + {sortField === field && sortDir === "desc" ? "▼" : "▲"} + + ); + + return ( +
+ + + {/* Header */} +
-

Maestros

-
-
-

Leydi Xequeb

-

Matemáticas

-

Activo

+
+
+ + + + + + +
+

+ Gestión de Docentes +

+
+

+ Sistema Académico Universitario · {teachers.length} docentes + registrados +

+
+ +
+ + {/* Stats bar */} +
+ {[ + { + label: "Total Docentes", + value: teachers.length, + icon: "👥", + accent: "#1E3A8A", + }, + { + label: "Activos", + value: teachers.filter((t) => t.status === "Activo").length, + icon: "✅", + accent: "#059669", + }, + { + label: "Inactivos", + value: teachers.filter((t) => t.status === "Inactivo").length, + icon: "⛔", + accent: "#DC2626", + }, + { + label: "En Licencia", + value: teachers.filter((t) => t.status === "Licencia").length, + icon: "📋", + accent: "#D97706", + }, + ].map((s, i) => ( +
+
+ {s.label} +
+
+ + {s.value} + + {s.icon} +
+
+ ))} +
+ + {/* Search & Filter bar */} +
+
+ + + + + setSearch(e.target.value)} + placeholder="Buscar por nombre, materia o correo..." + style={{ + width: "100%", + paddingLeft: 38, + paddingRight: 14, + height: 40, + border: "1.5px solid #E2E8F0", + borderRadius: 9, + fontSize: 13.5, + color: "#334155", + background: "#F8FAFC", + }} + /> +
+ +
+ {["Todos", "Activo", "Inactivo", "Licencia"].map((s) => ( + + ))} +
+ +
+ {filtered.length} resultado{filtered.length !== 1 ? "s" : ""} +
+
+ + {/* Table */} +
+
+ + + + {[ + { label: "Docente", field: "name" }, + { label: "Materia", field: "subject" }, + { label: "Correo", field: "email" }, + { label: "Teléfono", field: "phone" }, + { label: "Estado", field: "status" }, + { label: "Ingreso", field: "joined" }, + { label: "Acciones", field: null }, + ].map((col, i) => ( + + ))} + + + + {filtered.length === 0 ? ( + + + + ) : ( + filtered.map((t, idx) => { + const normalizedStatus = + t.status?.charAt(0).toUpperCase() + t.status?.slice(1); + + const sc = + statusConfig[normalizedStatus] || statusConfig.Activo; + const ac = avatarColors[(t.id || 0) % avatarColors.length]; + return ( + + {/* Docente */} + + {/* Materia */} + + {/* Email */} + + {/* Phone */} + + {/* Status */} + + {/* Joined */} + + {/* Actions */} + + + ); + }) + )} + +
col.field && handleSort(col.field)} + className={col.field ? "th-btn" : ""} + style={{ + padding: "13px 16px", + textAlign: "left", + fontSize: 11.5, + fontWeight: 700, + color: "#FBBF24", + letterSpacing: "0.08em", + textTransform: "uppercase", + cursor: col.field ? "pointer" : "default", + userSelect: "none", + borderBottom: "2px solid rgba(251,191,36,0.3)", + whiteSpace: "nowrap", + }} + > + {col.label} + {col.field && } +
+
🔍
+
+ Sin resultados
-
-

Irvin Chan

-

Programacion

-

Activo

+
+ Intenta cambiar el filtro o la búsqueda
-
-

Marcos Riviera

-

Fe y Mundo

-

Activo

-
-
+
+
+
+ {t.avatar} +
+
+
+ {t.name} +
+
+ {t.degree} +
+
+
+
+ + {t.subject} + + + {t.email} + + {t.phone} + + + + {t.status} + + + {t.created_at + ? new Date(t.created_at).toLocaleDateString() + : "—"} + +
+ + + +
+
- ) -} -export default Teachers; \ No newline at end of file + {/* Table footer */} +
+ + Mostrando {filtered.length} de {teachers.length} docentes + +
+ {[1].map((p) => ( + + ))} +
+
+
+ + {/* Modal */} + {showModal && ( +
setShowModal(false)} + style={{ + position: "fixed", + inset: 0, + background: "rgba(15,23,42,0.55)", + backdropFilter: "blur(4px)", + display: "flex", + alignItems: "center", + justifyContent: "center", + zIndex: 1000, + padding: 20, + }} + > +
e.stopPropagation()} + style={{ + background: "white", + borderRadius: 18, + width: "100%", + maxWidth: 520, + maxHeight: "90vh", + overflowY: "auto", + boxShadow: "0 24px 64px rgba(0,0,0,0.22)", + overflow: "hidden", + }} + > + {/* Modal header */} +
+
+
+ + + + +
+
+

+ {modalMode === "add" + ? "Nuevo Docente" + : modalMode === "edit" + ? "Editar Docente" + : selectedTeacher?.name} +

+

+ {modalMode === "add" + ? "Completar información del docente" + : modalMode === "edit" + ? "Actualizar información del docente" + : selectedTeacher?.subject} +

+
+
+ +
+ + {/* Modal body */} +
+ {modalMode === "view" && selectedTeacher ? ( +
+ {/* Avatar grande */} +
+
+ {selectedTeacher.avatar} +
+
+
+ {selectedTeacher.name} +
+
+ {selectedTeacher.degree} +
+ + + {selectedTeacher.status} + +
+
+ + {/* ID y Matrícula en fila */} +
+ {[ + { + icon: "🎓", + label: "Matrícula", + val: selectedTeacher.matricula || "—", + }, + ].map((item, i) => ( +
+
+ {item.icon} {item.label} +
+
+ {item.val} +
+
+ ))} +
+ +
+ {[ + { + icon: "📚", + label: "Materia", + val: selectedTeacher.subject, + }, + { + icon: "📧", + label: "Correo", + val: selectedTeacher.email, + }, + { + icon: "📞", + label: "Teléfono", + val: selectedTeacher.phone, + }, + { + icon: "📅", + label: "Fecha de Ingreso", + val: selectedTeacher.created_at + ? new Date( + selectedTeacher.created_at, + ).toLocaleDateString() + : "—", + }, + ].map((item, i) => ( +
+
+ {item.icon} {item.label} +
+
+ {item.val} +
+
+ ))} +
+
+ ) : ( +
+ {/* ── Matrícula ── */} +
+ + + setForm((f) => ({ ...f, matricula: e.target.value })) + } + style={{ + width: "100%", + height: 40, + border: "1.5px solid #BFDBFE", + borderRadius: 9, + padding: "0 14px", + fontSize: 13.5, + color: "#1D4ED8", + background: "#EFF6FF", + fontFamily: "monospace", + fontWeight: 600, + }} + /> +
+ + {/* ── Resto de campos ── */} + {[ + { + label: "Nombre Completo", + key: "name", + type: "text", + placeholder: "Ej. María García López", + }, + { + label: "Materia que imparte", + key: "subject", + type: "text", + placeholder: "Ej. Cálculo Diferencial", + }, + { + label: "Correo Institucional", + key: "email", + type: "email", + placeholder: "docente@universidad.edu", + }, + { + label: "Teléfono", + key: "phone", + type: "text", + placeholder: "+502 0000-0000", + }, + { + label: "Grado Académico", + key: "degree", + type: "text", + placeholder: "Ej. MSc. Ingeniería de Software", + }, + ].map((field) => ( +
+ + + setForm((f) => ({ + ...f, + [field.key]: e.target.value, + })) + } + style={{ + width: "100%", + height: 40, + border: "1.5px solid #E2E8F0", + borderRadius: 9, + padding: "0 14px", + fontSize: 13.5, + color: "#334155", + background: "#F8FAFC", + }} + /> +
+ ))} +
+ + +
+
+ )} +
+ + {/* Modal footer */} +
+ + {(modalMode === "add" || modalMode === "edit") && ( + + )} +
+
+
+ )} +
+ ); +}