commit 19b9cdba5993c18390bbcba8f5cd27ab771b160f Author: DavidDevGt Date: Sun Oct 27 12:50:51 2024 -0600 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..15d02b3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +node_modules +vendor +docker-compose.yml +Dockerfile +README.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..499828b --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +APP_NAME=appname + +DB_HOST=localhost +DB_NAME=dbname +DB_USER=user +DB_PASS=passwordsecure +DB_DRIVER=mysql +DB_CHARSET=utf8 + +EMAIL_HOST=smtp.youremail.com +EMAIL_PORT=587 + +JWT_SECRET=secretkey \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb22705 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# Ignore Composer's vendor directory +/vendor/ + +# Ignore Composer lock file +/composer.lock + +# Ignore environment variables +/.env + +# Ignore log files +/log/* +!log/.gitkeep + +# Ignore temporary and cache files +/tmp/ +*.temp +*.cache + +# Ignore system files +.DS_Store +Thumbs.db + +# Ignore IDE files +/.vscode/ +/.idea/ +*.iml + +# Ignore system file configurations +*.bak +*.swp + +# Ignore Robo task cache +/.robo/ + +# Ignore Node.js dependencies and build files (if you use Node for assets) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Ignore build artifacts (for assets) +dist/ +build/ + +# Ignore compiled files +/public/js/*.map +/public/css/*.map +/public/js/*.min.* +/public/css/*.min.* + +# Ignore backups or old config +*.bak +*.old + +# Custom +/public/phpinfo.php \ No newline at end of file diff --git a/000-default.conf b/000-default.conf new file mode 100644 index 0000000..2b26af6 --- /dev/null +++ b/000-default.conf @@ -0,0 +1,15 @@ + + ServerAdmin webmaster@localhost + ServerName localhost + + DocumentRoot /var/www/html/public + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b496aa7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,51 @@ +FROM php:8.3-apache AS build + +RUN apt-get update && apt-get install -y \ + libzip-dev \ + unzip \ + libonig-dev \ + libxml2-dev \ + libpng-dev \ + libjpeg-dev \ + libfreetype6-dev \ + libicu-dev \ + libgmp-dev \ + libsodium-dev \ + && docker-php-ext-install \ + zip \ + mbstring \ + xml \ + bcmath \ + intl \ + gmp \ + sodium \ + opcache \ + && docker-php-ext-configure gd --with-jpeg --with-freetype \ + && docker-php-ext-install gd \ + && docker-php-ext-install pdo_mysql + +# Xdebug config +RUN pecl install xdebug \ + && docker-php-ext-enable xdebug + +COPY xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini + +RUN a2enmod rewrite + +COPY 000-default.conf /etc/apache2/sites-available/000-default.conf + +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +COPY . /var/www/html + +RUN composer install --no-dev --optimize-autoloader + +RUN chown -R www-data:www-data /var/www/html \ + && chmod -R 755 /var/www/html/storage + +EXPOSE 80 +EXPOSE 9003 + +CMD ["apache2-foreground"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ff1f7c --- /dev/null +++ b/README.md @@ -0,0 +1,144 @@ +# ERP System - FerreToolsApp + +Este proyecto es un **sistema ERP** desarrollado con **PHP 8**, que sigue el patrón de diseño **Adapters** y está diseñado para ser modular y extensible. El sistema incluye módulos de gestión de clientes, productos, pedidos, facturación e inventario. El sistema está pensado para facilitar la gestión de las operaciones diarias en una ferretería o negocio similar, ofreciendo un panel administrativo y una estructura robusta para futuras expansiones. + +## Características Principales + +- **Módulos** de Ventas, Inventario, Facturación, Clientes, Pedidos. +- **Autenticación** con inicio de sesión y registro de usuarios. +- **JWT para autenticación**: Gestión de roles y sesiones de usuario. +- **Roles de Usuario** para controlar el acceso a las diferentes secciones del sistema. +- **Rutas protegidas** que permiten controlar qué usuarios pueden acceder a cada módulo. +- **Sistema de manejo de errores** HTTP con respuestas claras y detalladas (404, 500, etc.). +- **Reportes en PDF y Excel** para la gestión de informes de ventas, inventario y otros módulos. +- **Diseño limpio y responsive** utilizando Bootstrap 5. +- **Manejo de formularios** con sanitización de datos para evitar ataques de inyección. + +## Requisitos + +- PHP >= 8.3 +- Composer +- Servidor web (Apache, Nginx) +- MySQL (también puedes usar PostgreSQL, Oracle, SQL Server, etc.) +- Extensiones PHP: + - PDO + - cURL + - mbstring + - openssl + +## Librerías Utilizadas + +El proyecto se construyó utilizando las siguientes librerías: + +- **[Medoo](https://medoo.in/)**: ORM ligero para la interacción con la base de datos. +- **[PHPMailer](https://github.com/PHPMailer/PHPMailer)**: Para el envío de correos electrónicos. +- **[Monolog](https://github.com/Seldaek/monolog)**: Para el registro y manejo de logs. +- **[phpdotenv](https://github.com/vlucas/phpdotenv)**: Para la gestión de variables de entorno. +- **[AltoRouter](https://github.com/dannyvankooten/AltoRouter)**: Router simple y rápido para la gestión de rutas. +- **[Symfony HttpFoundation](https://github.com/symfony/http-foundation)**: Para manejar las peticiones y respuestas HTTP. +- **[DOMPDF](https://github.com/dompdf/dompdf)**: Para generar reportes y documentos en formato PDF. +- **[PhpSpreadsheet](https://phpspreadsheet.readthedocs.io/en/latest/)**: Para la generación y manejo de archivos Excel. +- **[MoneyPHP](https://github.com/moneyphp/money)**: Para manejar transacciones monetarias de manera segura. +- **[Firebase JWT](https://github.com/firebase/php-jwt)**: Para la autenticación con JWT (JSON Web Tokens). + +## Instalación + +### 1. Clona el repositorio + +```bash +git clone https://github.com/daviddevgt/proyecto-erp.git +cd proyecto-erp +``` + +### 2. Instala las dependencias de Composer + +Asegúrate de que tienes Composer instalado. Luego, ejecuta el siguiente comando: + +```bash +composer install +``` + +### 3. Configuración del entorno + +Crea un archivo `.env` en la raíz del proyecto basado en el archivo `.env.example`. Actualiza las variables con la configuración de tu base de datos y otros ajustes: + +```dotenv +DB_DRIVER=mysql +DB_HOST=127.0.0.1 +DB_NAME=erp_database +DB_USER=root +DB_PASS=password +DB_CHARSET=utf8mb4 + +JWT_SECRET=tu_secreto_jwt +``` + +### 4. Configuración de la base de datos + +Crea una base de datos para el sistema e importa el archivo `schema.sql` ubicado en la carpeta `schema`. + +```bash +mysql -u root -p erp_database < schema/schema.sql +``` + +### 5. Iniciar el servidor + +Puedes usar el servidor de desarrollo de PHP para correr la aplicación en local: + +```bash +php -S localhost:8000 -t public +``` + +Luego, abre el navegador y ve a `http://localhost:8000`. + +## Estructura del Proyecto + +```plaintext +. +├── app +│ ├── Config # Configuraciones del sistema, rutas y módulos +│ ├── Controllers # Controladores del sistema +│ ├── Core # Clases de core (ej. conexión a la base de datos, helpers) +│ ├── Entities # Entidades que representan las tablas de la BD +│ ├── Repositories # Repositorios para la gestión de entidades +│ └── Views # Vistas y componentes de la aplicación +├── public +│ ├── assets # Archivos estáticos (CSS, JS, imágenes) +│ └── index.php # Punto de entrada del sistema +├── schema +│ └── schema.sql # Esquema de la base de datos +└── vendor # Dependencias instaladas por Composer +``` + +## Uso + +### 1. Autenticación y Roles + +El sistema cuenta con **autenticación JWT**. Los usuarios registrados pueden acceder a las diferentes secciones del ERP según el rol que se les haya asignado. + +- **Roles soportados**: + - `ADMIN` + - `GERENCIA` + - `VENTAS` + - `BODEGA` + - `INVITADO` + +### 2. Módulos + +El sistema está estructurado en módulos, los cuales pueden ser gestionados desde la interfaz de administración. Algunos de los módulos principales son: + +- **Clientes**: Gestión de los datos de los clientes. +- **Pedidos**: Gestión y seguimiento de los pedidos. +- **Facturación**: Generación y gestión de facturas. +- **Productos**: Gestión del inventario de productos. +- **Reportes PDF y Excel**: Generación de reportes en PDF y Excel para el manejo de información de clientes, ventas e inventarios. + +## Desarrollo + +### 1. Agregar Nuevas Rutas + +Las rutas del sistema están gestionadas a través de **AltoRouter**. Puedes añadir nuevas rutas editando el archivo `app/Config/Routes.php`. Asegúrate de mapear correctamente los controladores y métodos para cada ruta. + +### 2. Estructura de las Vistas + +Las vistas están organizadas en la carpeta `app/Views`. Si deseas agregar nuevas vistas, simplemente crea los archivos correspondientes en esta carpeta y asegúrate de referenciarlos correctamente en los controladores. diff --git a/app/Adapters/Interfaces/.gitkeep b/app/Adapters/Interfaces/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/Config/Modulos.php b/app/Config/Modulos.php new file mode 100644 index 0000000..c213cb7 --- /dev/null +++ b/app/Config/Modulos.php @@ -0,0 +1,179 @@ + "Notificaciones", + "descripcion" => "Módulos relacionados con las notificaciones del sistema", + "icono" => "fas fa-bell", + "hijos" => [ + [ + "nombre" => "Ver Notificaciones", + "descripcion" => "Lista de notificaciones del sistema", + "ruta" => "/notificaciones", + "vista" => "modules/notificaciones/index.php", + "icono" => "fa-solid fa-bell", + ], + ], +]; + +// Módulos del sistema ordenados por prioridad/importancia +return [ + // Core - Módulo principal + [ + "nombre" => "Inicio", + "descripcion" => "Página principal del sistema GTD", + "icono" => "fas fa-home", + "ruta" => "/home", // Ruta del módulo sin hijos + ], + + // Gestión de tareas + [ + "nombre" => "Tareas", + "descripcion" => "Módulos relacionados con la gestión de tareas", + "icono" => "fas fa-tasks", + "hijos" => [ + [ + "nombre" => "Tablero Kanban", + "descripcion" => "Tablero visual de tareas", + "ruta" => "/tareas/kanban", + "vista" => "modules/tareas/kanban.php", + "icono" => "fa-solid fa-th-large", + ], + ], + ], + + [ + "nombre" => "Fitness", + "descripcion" => "Módulos relacionados con la salud y el bienestar físico", + "icono" => "fas fa-dumbbell", + "hijos" => [ + [ + "nombre" => "Rutinas de Ejercicio", + "descripcion" => "Crear y gestionar rutinas de entrenamiento", + "ruta" => "/fitness/rutinas", + "vista" => "modules/fitness/rutinas.php", + "icono" => "fa-solid fa-running", + ], + [ + "nombre" => "Recetas Saludables", + "descripcion" => "Almacenar y visualizar recetas nutritivas", + "ruta" => "/fitness/recetas", + "vista" => "modules/fitness/recetas.php", + "icono" => "fa-solid fa-utensils", + ], + [ + "nombre" => "Seguimiento de Actividades", + "descripcion" => "Registrar y monitorear actividades físicas realizadas", + "ruta" => "/fitness/seguimiento", + "vista" => "modules/fitness/seguimiento.php", + "icono" => "fa-solid fa-chart-line", + ], + ], + ], + + [ + "nombre" => "Finanzas", + "descripcion" => "Módulos para gestionar tus finanzas personales", + "icono" => "fas fa-wallet", + "hijos" => [ + [ + "nombre" => "Ingresos", + "descripcion" => "Registrar y gestionar fuentes de ingresos", + "ruta" => "/finanzas/ingresos", + "vista" => "modules/finanzas/ingresos.php", + "icono" => "fa-solid fa-plus-circle", + ], + [ + "nombre" => "Gastos", + "descripcion" => "Registrar y categorizar gastos", + "ruta" => "/finanzas/gastos", + "vista" => "modules/finanzas/gastos.php", + "icono" => "fa-solid fa-minus-circle", + ], + [ + "nombre" => "Presupuestos", + "descripcion" => "Crear y monitorear presupuestos", + "ruta" => "/finanzas/presupuestos", + "vista" => "modules/finanzas/presupuestos.php", + "icono" => "fa-solid fa-chart-pie", + ], + [ + "nombre" => "Reportes Financieros", + "descripcion" => "Visualizar gráficos y reportes de finanzas personales", + "ruta" => "/finanzas/reportes", + "vista" => "modules/finanzas/reportes.php", + "icono" => "fa-solid fa-chart-line", + ], + ], + ], + + [ + "nombre" => "Eventos", + "descripcion" => "Módulos para planificar y gestionar tus eventos importantes", + "icono" => "fas fa-calendar-alt", + "hijos" => [ + [ + "nombre" => "Crear Evento", + "descripcion" => "Crear y programar nuevos eventos", + "ruta" => "/eventos/crear", + "vista" => "modules/eventos/crear.php", + "icono" => "fa-solid fa-plus", + ], + [ + "nombre" => "Calendario de Eventos", + "descripcion" => "Visualizar eventos en un calendario interactivo", + "ruta" => "/eventos/calendario", + "vista" => "modules/eventos/calendario.php", + "icono" => "fa-solid fa-calendar", + ], + [ + "nombre" => "Recordatorios de Eventos", + "descripcion" => "Configurar recordatorios para eventos próximos", + "ruta" => "/eventos/recordatorios", + "vista" => "modules/eventos/recordatorios.php", + "icono" => "fa-solid fa-bell", + ], + ], + ], + + + // Configuración del sistema + [ + "nombre" => "Configuración", + "descripcion" => + "Módulos relacionados con la configuración del sistema", + "icono" => "fas fa-cogs", + "hijos" => [ + [ + "nombre" => "Preferencias", + "descripcion" => "Gestión de configuraciones del sistema", + "ruta" => "/configuracion", + "vista" => "modules/configuracion/index.php", + "icono" => "fa-solid fa-sliders-h", + ], + [ + "nombre" => "Plantillas de Correo", + "descripcion" => "Crear y editar plantillas para diferentes tipos de correos", + "ruta" => "/configuracion/correos/plantillas", + "vista" => "modules/correos/plantillas.php", + "icono" => "fa-solid fa-file-alt", + ], + [ + "nombre" => "Estados de Correo", + "descripcion" => "Gestionar y visualizar los estados de los correos enviados", + "ruta" => "/configuracion/correos/estados", + "vista" => "modules/correos/estados.php", + "icono" => "fa-solid fa-info-circle", + ], + [ + "nombre" => "Historial de Correos Enviados", + "descripcion" => "Revisar el historial de correos enviados por el sistema", + "ruta" => "/configuracion/correos/historial", + "vista" => "modules/correos/historial.php", + "icono" => "fa-solid fa-history", + ], + ], + ], +]; diff --git a/app/Config/Routes.php b/app/Config/Routes.php new file mode 100644 index 0000000..dfbf80f --- /dev/null +++ b/app/Config/Routes.php @@ -0,0 +1,380 @@ + "GET", + "uri" => "/home", + "target" => [HomeController::class, "showHome"], + "protected" => true, + ], + + /* + * Rutas del Módulo Fitness + */ + [ + "method" => "GET", + "uri" => "/fitness/rutinas", + "target" => [FitnessController::class, "showRutinas"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/fitness/rutinas/crear", + "target" => [FitnessController::class, "showCrearRutina"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/fitness/rutinas/crear", + "target" => [FitnessController::class, "createRutina"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/fitness/rutinas/editar/[i:id]", + "target" => [FitnessController::class, "showEditarRutina"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/fitness/rutinas/editar/[i:id]", + "target" => [FitnessController::class, "editRutina"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/fitness/rutinas/eliminar", + "target" => [FitnessController::class, "deleteRutina"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/fitness/recetas", + "target" => [FitnessController::class, "showRecetas"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/fitness/recetas/crear", + "target" => [FitnessController::class, "showCrearReceta"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/fitness/recetas/crear", + "target" => [FitnessController::class, "createReceta"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/fitness/recetas/editar/[i:id]", + "target" => [FitnessController::class, "showEditarReceta"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/fitness/recetas/editar/[i:id]", + "target" => [FitnessController::class, "editReceta"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/fitness/recetas/eliminar", + "target" => [FitnessController::class, "deleteReceta"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/fitness/seguimiento", + "target" => [FitnessController::class, "showSeguimiento"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/fitness/seguimiento/registrar", + "target" => [FitnessController::class, "registrarActividad"], + "protected" => true, + ], + + /* + * Rutas de Tareas + */ + [ + "method" => "GET", + "uri" => "/tareas", + "target" => [KanbanController::class, "getAllTasks"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/tarea/get/[i:id]", + "target" => [KanbanController::class, "getTask"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/tarea/create", + "target" => [KanbanController::class, "createTask"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/tarea/update", + "target" => [KanbanController::class, "updateTask"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/tarea/update-status", + "target" => [KanbanController::class, "updateTaskStatus"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/tarea/delete", + "target" => [KanbanController::class, "deleteTask"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/tareas/kanban", + "target" => [KanbanController::class, "showIndex"], + "protected" => true, + ], + + + /* + * Rutas del Módulo Finanzas + */ + [ + "method" => "GET", + "uri" => "/finanzas/ingresos", + "target" => [FinanzasController::class, "showIngresos"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/finanzas/ingresos/crear", + "target" => [FinanzasController::class, "createIngreso"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/finanzas/ingresos/eliminar", + "target" => [FinanzasController::class, "deleteIngreso"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/finanzas/gastos", + "target" => [FinanzasController::class, "showGastos"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/finanzas/gastos/crear", + "target" => [FinanzasController::class, "createGasto"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/finanzas/gastos/eliminar", + "target" => [FinanzasController::class, "deleteGasto"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/finanzas/presupuestos", + "target" => [FinanzasController::class, "showPresupuestos"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/finanzas/presupuestos/crear", + "target" => [FinanzasController::class, "createPresupuesto"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/finanzas/presupuestos/eliminar", + "target" => [FinanzasController::class, "deletePresupuesto"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/finanzas/reportes", + "target" => [FinanzasController::class, "showReportes"], + "protected" => true, + ], + + /* + * Rutas del Módulo Eventos + */ + [ + "method" => "GET", + "uri" => "/eventos/crear", + "target" => [EventosController::class, "showCrearEvento"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/eventos/crear", + "target" => [EventosController::class, "createEvento"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/eventos/calendario", + "target" => [EventosController::class, "showCalendario"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/eventos/recordatorios", + "target" => [EventosController::class, "showRecordatorios"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/eventos/recordatorios/crear", + "target" => [EventosController::class, "createRecordatorio"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/eventos/recordatorios/eliminar", + "target" => [EventosController::class, "deleteRecordatorio"], + "protected" => true, + ], + + /* + * Rutas de Notificaciones + */ + [ + "method" => "GET", + "uri" => "/notificaciones", + "target" => [NotificationController::class, "showIndex"], + "protected" => true, + ], + /* + * Rutas de Configuraciones + */ + [ + "method" => "GET", + "uri" => "/configuracion", + "target" => [SettingController::class, "showSettings"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/configuracion/update", + "target" => [SettingController::class, "updateSettings"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/configuracion/correos/plantillas", + "target" => [SettingController::class, "showPlantillas"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/configuracion/correos/plantillas/crear", + "target" => [SettingController::class, "showCrearPlantilla"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/configuracion/correos/plantillas/crear", + "target" => [SettingController::class, "createPlantilla"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/configuracion/correos/plantillas/editar/[i:id]", + "target" => [SettingController::class, "showEditarPlantilla"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/configuracion/correos/plantillas/editar/[i:id]", + "target" => [SettingController::class, "editPlantilla"], + "protected" => true, + ], + [ + "method" => "POST", + "uri" => "/configuracion/correos/plantillas/eliminar", + "target" => [SettingController::class, "deletePlantilla"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/configuracion/correos/estados", + "target" => [SettingController::class, "showEstados"], + "protected" => true, + ], + [ + "method" => "GET", + "uri" => "/configuracion/correos/historial", + "target" => [SettingController::class, "showHistorial"], + "protected" => true, + ], + /* + * Rutas de autenticación + */ + [ + "method" => "GET", + "uri" => "/login", + "target" => function () { + (new AuthController())->showLogin(); + }, + "protected" => false, + ], + [ + "method" => "POST", + "uri" => "/login", + "target" => function () { + $request = Request::createFromGlobals(); + (new AuthController())->login($request); + }, + "protected" => false, + ], + [ + "method" => "GET", + "uri" => "/register", + "target" => function () { + (new AuthController())->showRegister(); + }, + "protected" => false, + ], + [ + "method" => "POST", + "uri" => "/register", + "target" => function () { + $request = Request::createFromGlobals(); + (new AuthController())->register($request); + }, + "protected" => false, + ], + [ + "method" => "GET", + "uri" => "/logout", + "target" => function () { + (new AuthController())->logout(); + }, + "protected" => true, + ], +]; diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php new file mode 100644 index 0000000..2c3e916 --- /dev/null +++ b/app/Controllers/AuthController.php @@ -0,0 +1,174 @@ +userRepository = new UserRepository($database); + $this->jwtSecret = $_ENV["JWT_SECRET"]; + } + + public function showLogin() + { + $this->render("auth/login"); + } + + public function login(Request $request) + { + $email = HttpHelper::getParam( + $request, + "email", + null, + FILTER_SANITIZE_EMAIL + ); + $password = HttpHelper::getParam($request, "password"); + + $user = $this->userRepository->findBy("email", $email); + + if (!$user) { + $this->render("auth/login", [ + "error" => "Credenciales incorrectas", + ]); + return; + } + + if (!password_verify($password, $user["password_hash"])) { + $this->render("auth/login", ["error" => "Contraseña incorrecta"]); + return; + } + + $payload = [ + "user_id" => $user["user_id"], + "email" => $user["email"], + "exp" => time() + 60 * 60, // Expiración de 1 hora + ]; + + $jwt = JWT::encode($payload, $this->jwtSecret, "HS256"); + + setcookie("jwt", $jwt, time() + 60 * 60, "/", "", false, true); + + header("Location: /home"); + } + + public function showRegister() + { + $this->render("auth/register"); + } + + public function register(Request $request) + { + $username = HttpHelper::getParam($request, "username"); + $email = HttpHelper::getParam( + $request, + "email", + null, + FILTER_SANITIZE_EMAIL + ); + $passwordPlain = HttpHelper::getParam($request, "password"); + $errors = []; // Guardar errores + + $validations = [ + "username" => [ + "required" => true, + "message" => "El nombre de usuario es obligatorio.", + ], + "email" => [ + "required" => true, + "message" => "El correo electrónico es obligatorio.", + "validate" => function ($value) { + return filter_var($value, FILTER_VALIDATE_EMAIL) + ? null + : "El correo electrónico no es válido."; + }, + ], + "password" => [ + "required" => true, + "message" => "La contraseña es obligatoria.", + "minLength" => 8, + "lengthMessage" => + "La contraseña debe tener al menos 8 caracteres.", + ], + ]; + + $inputData = [ + "username" => $username, + "email" => $email, + "password" => $passwordPlain, + ]; + + foreach ($validations as $key => $rules) { + $value = $inputData[$key] ?? null; + + if ($rules["required"] && empty($value)) { + $errors[$key] = $rules["message"]; + continue; + } + + if ( + isset($rules["minLength"]) && + !is_null($value) && + strlen($value) < $rules["minLength"] + ) { + $errors[$key] = $rules["lengthMessage"]; + } + + if (isset($rules["validate"]) && empty($errors[$key])) { + $customError = $rules["validate"]($value); + if ($customError) { + $errors[$key] = $customError; + } + } + + if ($key === "email" && empty($errors[$key])) { + $existingUser = $this->userRepository->findBy("email", $email); + if ($existingUser) { + $errors[$key] = + "El correo electrónico ya está registrado. Por favor, utiliza otro correo o inicia sesión."; + } + } + } + + if (!empty($errors)) { + $this->render("auth/register", [ + "errors" => $errors, + "old" => ["username" => $username, "email" => $email], + ]); + return; + } + + // Continuar con el registro + $password = password_hash($passwordPlain, PASSWORD_DEFAULT); + + $userData = [ + "username" => $username, + "email" => $email, + "password_hash" => $password, + ]; + + $this->userRepository->insert($userData); + $this->render("auth/register", [ + "success" => "Usuario registrado con éxito.", + ]); + } + + public function logout() + { + setcookie("jwt", "", time() - 3600, "/"); // Eliminar la cookie JWT + header("Location: /login"); + } +} diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php new file mode 100644 index 0000000..36bf868 --- /dev/null +++ b/app/Controllers/BaseController.php @@ -0,0 +1,60 @@ +repository = $repository; + } + + // Obtener todos los registros + public function index() + { + return $this->repository->getAll(); + } + + // Crear un nuevo registro + public function create(array $data) + { + return $this->repository->insert($data); + } + + // Actualizar un registro + public function update($id, array $data) + { + return $this->repository->update($id, $data); + } + + // Eliminar un registro + public function delete($id) + { + return $this->repository->delete($id); + } + + // Eliminar un registro activo + public function deleteActive($id) + { + return $this->repository->deleteActive($id); + } + + // Renderizar una vista + protected function render($view, $data = []) + { + extract($data); + + $viewPath = realpath(__DIR__ . "/../Views/{$view}.php"); + + if (file_exists($viewPath)) { + include $viewPath; + } else { + echo "Error: La vista '{$view}' no se encontró en {$viewPath}."; + } + } +} diff --git a/app/Controllers/ClienteController.php b/app/Controllers/ClienteController.php new file mode 100644 index 0000000..d0800eb --- /dev/null +++ b/app/Controllers/ClienteController.php @@ -0,0 +1,193 @@ +repository->countAll(); + $clientes = $this->repository->getPaginated($page, $perPage); + + $totalPages = ceil($totalClientes / $perPage); + + $this->render("modules/clientes/index", [ + "clientes" => $clientes, + "currentPage" => $page, + "totalPages" => $totalPages, + ]); + } + + /** + * Obtener datos de un cliente por ID + */ + public function getCliente($id) + { + $cliente = $this->repository->findById($id); + + if ($cliente) { + return HttpHelper::createJsonResponse([ + "status" => "success", + "cliente" => $cliente, + ]); + } + + return HttpHelper::createJsonResponse( + [ + "status" => "error", + "message" => "Cliente no encontrado", + ], + 404 + ); + } + + /** + * Crea un nuevo cliente + */ + public function createCliente() + { + $request = HttpHelper::getRequest(); + + $nombre = HttpHelper::getParam($request, "nombre"); + $direccion = HttpHelper::getParam($request, "direccion"); + $telefono = HttpHelper::getParam($request, "telefono"); + $email = HttpHelper::getParam($request, "email"); + $nit = HttpHelper::getParam($request, "nit"); + $cui = HttpHelper::getParam($request, "cui") ?: null; + $fecha_nacimiento = HttpHelper::getParam($request, "fecha_nacimiento"); + + if (!$nombre || !$nit) { + return HttpHelper::createJsonResponse( + [ + "status" => "error", + "message" => "Nombre y NIT son obligatorios", + ], + 400 + ); + } + + $data = [ + "nombre" => $nombre, + "direccion" => $direccion, + "telefono" => $telefono, + "email" => $email, + "nit" => $nit, + "cui" => $cui, + "fecha_nacimiento" => $fecha_nacimiento, + ]; + + $result = $this->repository->insert($data); + + if ($result) { + return HttpHelper::createJsonResponse([ + "status" => "success", + "message" => "Cliente agregado exitosamente", + ]); + } + + return HttpHelper::createJsonResponse( + ["status" => "error", "message" => "Error al agregar cliente"], + 500 + ); + } + + /** + * Actualiza un cliente existente + */ + public function updateCliente() + { + $requestBody = file_get_contents("php://input"); + $requestData = json_decode($requestBody, true); + + $id = $requestData["id"] ?? null; + $nombre = $requestData["nombre"] ?? null; + $direccion = $requestData["direccion"] ?? null; + $telefono = $requestData["telefono"] ?? null; + $email = $requestData["email"] ?? null; + $nit = $requestData["nit"] ?? null; + $cui = $requestData["cui"] ?? null; + $fecha_nacimiento = $requestData["fecha_nacimiento"] ?? null; + $active = $requestData["active"] ?? 0; + + if (!$id || !$nombre || !$nit) { + return HttpHelper::createJsonResponse( + [ + "status" => "error", + "message" => "ID, Nombre y NIT son obligatorios", + ], + 400 + ); + } + + $data = [ + "nombre" => $nombre, + "direccion" => $direccion, + "telefono" => $telefono, + "email" => $email, + "nit" => $nit, + "cui" => $cui, + "fecha_nacimiento" => $fecha_nacimiento, + "active" => $active, + ]; + + $result = $this->repository->update($id, $data); + + if ($result) { + return HttpHelper::createJsonResponse([ + "status" => "success", + "message" => "Cliente actualizado exitosamente", + ]); + } + + return HttpHelper::createJsonResponse( + ["status" => "error", "message" => "Error al actualizar cliente"], + 500 + ); + } + + /** + * Elimina un cliente + */ + public function deleteCliente() + { + $request = HttpHelper::getRequest(); + $id = HttpHelper::getParam($request, "id"); + + if (!$id) { + return HttpHelper::createJsonResponse( + ["status" => "error", "message" => "El ID es obligatorio"], + 400 + ); + } + + $result = $this->repository->deleteActive($id); + + if ($result) { + return HttpHelper::createJsonResponse([ + "status" => "success", + "message" => "Cliente eliminado", + ]); + } + + return HttpHelper::createJsonResponse( + ["status" => "error", "message" => "Error al eliminar cliente"], + 500 + ); + } +} diff --git a/app/Controllers/ErrorController.php b/app/Controllers/ErrorController.php new file mode 100644 index 0000000..9ddd18b --- /dev/null +++ b/app/Controllers/ErrorController.php @@ -0,0 +1,48 @@ +render($view); + exit; + } + + public function notFound() + { + $this->sendErrorHeader(404, 'error/404'); + } + + public function forbidden() + { + $this->sendErrorHeader(403, 'error/403'); + } + + public function internalServerError() + { + $this->sendErrorHeader(500, 'error/500'); + } + + public function badRequest() + { + $this->sendErrorHeader(400, 'error/400'); + } + + public function unauthorized() + { + $this->sendErrorHeader(401, 'error/401'); + } + + public function requestTimeout() + { + $this->sendErrorHeader(408, 'error/408'); + } + + public function serviceUnavailable() + { + $this->sendErrorHeader(503, 'error/503'); + } +} diff --git a/app/Controllers/EventosController.php b/app/Controllers/EventosController.php new file mode 100644 index 0000000..88938bd --- /dev/null +++ b/app/Controllers/EventosController.php @@ -0,0 +1,8 @@ +exerciseRepository = new ExerciseRepository($database); + parent::__construct($this->exerciseRepository); + } + + public function showExercises() + { + $exercises = $this->exerciseRepository->getAll(); + $this->render('modules/fitness/exercises', ['exercises' => $exercises]); + } +} diff --git a/app/Controllers/FinanzasController.php b/app/Controllers/FinanzasController.php new file mode 100644 index 0000000..47ee7ec --- /dev/null +++ b/app/Controllers/FinanzasController.php @@ -0,0 +1,8 @@ +render('modules/fitness/rutinas'); + } +} diff --git a/app/Controllers/HomeController.php b/app/Controllers/HomeController.php new file mode 100644 index 0000000..43d4720 --- /dev/null +++ b/app/Controllers/HomeController.php @@ -0,0 +1,11 @@ +render('home/home'); + } +} diff --git a/app/Controllers/KanbanController.php b/app/Controllers/KanbanController.php new file mode 100644 index 0000000..38a3927 --- /dev/null +++ b/app/Controllers/KanbanController.php @@ -0,0 +1,144 @@ +taskRepository = new TaskRepository($database); + parent::__construct($this->taskRepository); + } + + public function showIndex() + { + $tasks = $this->taskRepository->getAll(); + $this->render("modules/tareas/kanban", compact('tasks')); + } + + public function getAllTasks() + { + $tasks = $this->taskRepository->getAll(); + HttpHelper::createJsonResponse(['tasks' => $tasks], 200); + } + + public function getTask($id) + { + $task = $this->taskRepository->findById($id); + + if ($task) { + HttpHelper::createJsonResponse($task); + } else { + HttpHelper::createJsonResponse(["message" => "No se encontró la tarea"], 404); + } + } + + public function createTask() + { + $request = HttpHelper::getRequest(); + $userId = AuthHelper::getUserIdFromToken(); + + if (!$userId) { + HttpHelper::createJsonResponse(["message" => "Usuario no autenticado."], 401); + return; + } + + $data = [ + 'task_title' => HttpHelper::getParam($request, 'task_title', '', FILTER_SANITIZE_STRING), + 'description' => HttpHelper::getParam($request, 'description', null, FILTER_SANITIZE_STRING), + 'due_date' => HttpHelper::getParam($request, 'due_date', null, FILTER_SANITIZE_STRING), + 'color' => HttpHelper::getParam($request, 'color', '#e9ecef', FILTER_SANITIZE_STRING), + 'status_id' => HttpHelper::getParam($request, 'status_id', 1, FILTER_VALIDATE_INT), + 'user_id' => $userId, + ]; + + error_log("Datos recibidos para la creación de la tarea: " . print_r($data, true)); + + $task_id = $this->taskRepository->insert($data); + + if ($task_id) { + HttpHelper::createJsonResponse(["task_id" => $task_id], 201); + } else { + HttpHelper::createJsonResponse(["message" => "No se pudo crear la tarea"], 400); + } + } + + + public function updateTask() + { + $request = HttpHelper::getRequest(); + $taskId = HttpHelper::getParam($request, 'task_id', null, FILTER_VALIDATE_INT); + if (!$taskId) { + HttpHelper::createJsonResponse(["message" => "El ID de la tarea es obligatorio."], 400); + return; + } + + $data = [ + 'task_title' => HttpHelper::getParam($request, 'task_title', '', FILTER_SANITIZE_STRING), + 'description' => HttpHelper::getParam($request, 'description', null, FILTER_SANITIZE_STRING), + 'due_date' => HttpHelper::getParam($request, 'due_date', null, FILTER_SANITIZE_STRING), + 'color' => HttpHelper::getParam($request, 'color', '#e9ecef', FILTER_SANITIZE_STRING), + 'status_id' => HttpHelper::getParam($request, 'status_id', 1, FILTER_VALIDATE_INT) + ]; + + if ($this->taskRepository->update($taskId, $data)) { + HttpHelper::createJsonResponse(["message" => "Tarea actualizada exitosamente."]); + } else { + HttpHelper::createJsonResponse(["message" => "Error al actualizar la tarea."], 500); + } + } + + public function updateTaskStatus() + { + $request = HttpHelper::getRequest(); + $taskId = HttpHelper::getParam($request, 'task_id', null, FILTER_VALIDATE_INT); + if (!$taskId) { + HttpHelper::createJsonResponse(["message" => "El ID de la tarea es obligatorio."], 400); + return; + } + + $statusId = HttpHelper::getParam($request, 'status_id', null, FILTER_VALIDATE_INT); + if (!$statusId) { + HttpHelper::createJsonResponse(["message" => "El ID de estado es obligatorio."], 400); + return; + } + + $data = [ + 'status_id' => $statusId, + 'updated_at' => date('Y-m-d H:i:s') + ]; + + if ($this->taskRepository->update($taskId, $data)) { + HttpHelper::createJsonResponse(["message" => "Estado de la tarea actualizado exitosamente."]); + } else { + HttpHelper::createJsonResponse(["message" => "Error al actualizar el estado de la tarea."], 500); + } + } + + public function deleteTask() + { + $request = HttpHelper::getRequest(); + $taskId = HttpHelper::getParam($request, 'task_id', null, FILTER_VALIDATE_INT); + if (!$taskId) { + HttpHelper::createJsonResponse(["message" => "El ID de la tarea es obligatorio."], 400); + return; + } + + if ($this->taskRepository->delete($taskId)) { + HttpHelper::createJsonResponse(["message" => "Tarea eliminada exitosamente."]); + } else { + HttpHelper::createJsonResponse(["message" => "Error al eliminar la tarea."], 500); + } + } + +} diff --git a/app/Controllers/NotificationController.php b/app/Controllers/NotificationController.php new file mode 100644 index 0000000..c9f770c --- /dev/null +++ b/app/Controllers/NotificationController.php @@ -0,0 +1,8 @@ +load(); + + $this->connection = new Medoo([ + 'type' => $_ENV['DB_DRIVER'], + 'host' => $_ENV['DB_HOST'], + 'database' => $_ENV['DB_NAME'], + 'username' => $_ENV['DB_USER'], + 'password' => $_ENV['DB_PASS'], + 'charset' => $_ENV['DB_CHARSET'], + 'error' => PDO::ERRMODE_EXCEPTION, + 'timeout' => 15, + ]); + } catch (PDOException $e) { + throw new Exception("Error al conectar con la base de datos: " . $e->getMessage()); + } catch (Exception $e) { + throw new Exception("Error configuracion base de datos: " . $e->getMessage()); + } + } + + public function getConnection() + { + return $this->connection; + } +} diff --git a/app/Core/HttpHelper.php b/app/Core/HttpHelper.php new file mode 100644 index 0000000..ec2f357 --- /dev/null +++ b/app/Core/HttpHelper.php @@ -0,0 +1,118 @@ + $value) { + header("{$key}: {$value}"); + } + + echo json_encode($data); + exit(); + } + + /** + * Redirige a una URL específica. + * + * @param string $url + * @param int $statusCode + * @return RedirectResponse + */ + public static function redirect( + string $url, + int $statusCode = 302 + ): RedirectResponse { + return new RedirectResponse($url, $statusCode); + } + + /** + * Extrae un parámetro de la solicitud (GET, POST, etc.) con saneamiento. + * Si es una solicitud JSON, extrae los datos del cuerpo JSON. + * + * @param Request $request + * @param string $key + * @param mixed $default + * @param int|null $filter + * @return mixed + */ + public static function getParam( + Request $request, + string $key, + $default = null, + int $filter = null + ) { + $contentType = $request->headers->get("Content-Type", ""); + + if (strpos($contentType, "application/json") === 0) { + $data = json_decode($request->getContent(), true); + $value = $data[$key] ?? $default; + } else { + $value = $request->get($key, $default); + } + + if ($filter !== null) { + return filter_var($value, $filter); + } + + return $value; + } + + /** + * Obtiene la IP del cliente de manera segura. + * + * @param Request $request + * @return string|null + */ + public static function getClientIp(Request $request): ?string + { + return $request->getClientIp(); + } +} diff --git a/app/Entities/BaseEntity.php b/app/Entities/BaseEntity.php new file mode 100644 index 0000000..a2e06aa --- /dev/null +++ b/app/Entities/BaseEntity.php @@ -0,0 +1,13 @@ +user_id ?? null; + } catch (\Throwable $th) { + return null; + } + } +} diff --git a/app/Helpers/SerializeHelper.php b/app/Helpers/SerializeHelper.php new file mode 100644 index 0000000..f298cf1 --- /dev/null +++ b/app/Helpers/SerializeHelper.php @@ -0,0 +1,315 @@ +getMessage()}" + ); + } + } + + /** + * Convierte datos a XML + */ + public static function toXml( + mixed $data, + string $rootElement = "root" + ): string { + $dom = new DOMDocument(self::XML_VERSION, self::XML_ENCODING); + $dom->formatOutput = true; + + $root = $dom->createElement($rootElement); + $dom->appendChild($root); + + self::arrayToDomElement($data, $root, $dom); + + return $dom->saveXML() ?: + throw new RuntimeException("Error generando XML"); + } + + /** + * Método auxiliar para convertir array a elementos DOM + */ + private static function arrayToDomElement( + mixed $data, + DOMElement $parent, + DOMDocument $dom + ): void { + $data = match (true) { + $data instanceof Stringable => (string) $data, + is_object($data) => (array) $data, + default => $data, + }; + + foreach ((array) $data as $key => $value) { + $key = preg_replace("/[^a-z0-9_]/i", "_", (string) $key) ?: "item"; + + $child = match (true) { + is_array($value), + is_object($value) + => self::createComplexElement($dom, $key, $value), + default => self::createSimpleElement($dom, $key, $value), + }; + + $parent->appendChild($child); + } + } + + /** + * Crea un elemento XML complejo + */ + private static function createComplexElement( + DOMDocument $dom, + string $key, + mixed $value + ): DOMElement { + $element = $dom->createElement($key); + self::arrayToDomElement($value, $element, $dom); + return $element; + } + + /** + * Crea un elemento XML simple + */ + private static function createSimpleElement( + DOMDocument $dom, + string $key, + mixed $value + ): DOMElement { + $element = $dom->createElement($key); + $element->appendChild($dom->createTextNode((string) $value)); + return $element; + } + + /** + * Serializa datos de manera optimizada + */ + public static function serialize(mixed $data): string + { + return match (true) { + extension_loaded("igbinary") => igbinary_serialize($data), + default => serialize($data), + }; + } + + /** + * Deserializa datos + */ + public static function unserialize(string $data): mixed + { + return match (true) { + extension_loaded("igbinary") && + !str_starts_with($data, "a:") && + !str_starts_with($data, "O:") + => igbinary_unserialize($data), + default => unserialize($data, ["allowed_classes" => true]), + }; + } + + /** + * Compara objetos o arrays + */ + public static function compare(mixed $obj1, mixed $obj2): array + { + return self::calculateDifferences( + self::normalize($obj1), + self::normalize($obj2) + ); + } + + /** + * Calcula diferencias entre arrays + */ + private static function calculateDifferences( + array $array1, + array $array2, + string $path = "" + ): array { + $differences = []; + + foreach ($array1 as $key => $value) { + $currentPath = $path ? "{$path}.{$key}" : $key; + + if (!array_key_exists($key, $array2)) { + $differences[$currentPath] = [ + "type" => "removed", + "value" => $value, + ]; + continue; + } + + $differences = match (true) { + is_array($value) && is_array($array2[$key]) => [ + ...$differences, + ...self::calculateDifferences( + $value, + $array2[$key], + $currentPath + ), + ], + $value !== $array2[$key] => [ + ...$differences, + $currentPath => [ + "type" => "modified", + "old" => $value, + "new" => $array2[$key], + ], + ], + default => $differences, + }; + } + + // Verificar elementos adicionales + foreach ($array2 as $key => $value) { + if (!array_key_exists($key, $array1)) { + $currentPath = $path ? "{$path}.{$key}" : $key; + $differences[$currentPath] = [ + "type" => "added", + "value" => $value, + ]; + } + } + + return $differences; + } + + /** + * Normaliza datos para comparación + */ + public static function normalize(mixed $data): array + { + return match (true) { + is_object($data) => self::normalizeObject($data), + is_array($data) => self::normalizeArray($data), + default => [$data], + }; + } + + /** + * Normaliza un objeto + */ + private static function normalizeObject(object $object): array + { + return match (true) { + method_exists($object, "toArray") => $object->toArray(), + default => self::normalizeArray(get_object_vars($object)), + }; + } + + /** + * Normaliza un array + */ + private static function normalizeArray(array $array): array + { + return array_map( + fn($value) => match (true) { + is_object($value), is_array($value) => self::normalize($value), + default => $value, + }, + $array + ); + } + + /** + * Convierte a array + */ + public static function toArray(mixed $data): array + { + return match (true) { + is_object($data) && method_exists($data, "toArray") + => $data->toArray(), + is_object($data) => self::normalize($data), + is_array($data) => array_map([self::class, "toArray"], $data), + default => [$data], + }; + } + + /** + * Clonación profunda + * + * @template T of object + * @param T $object + * @return T + */ + public static function deepClone(object $object): object + { + if (self::$cache->offsetExists($object)) { + return self::$cache->offsetGet($object); + } + + $clone = match (true) { + extension_loaded("igbinary") => igbinary_unserialize( + igbinary_serialize($object) + ), + method_exists($object, "__clone") => self::cloneWithProperties( + $object + ), + default => unserialize(serialize($object)), + }; + + self::$cache->offsetSet($object, $clone); + return $clone; + } + + /** + * Clona un objeto y sus propiedades + */ + private static function cloneWithProperties(object $object): object + { + $clone = clone $object; + foreach (get_object_vars($clone) as $property => $value) { + if (is_object($value)) { + $clone->$property = self::deepClone($value); + } + } + return $clone; + } + + /** + * Limpia la caché + */ + public static function clearCache(): void + { + self::$cache = new WeakMap(); + } +} diff --git a/app/Helpers/StringHelper.php b/app/Helpers/StringHelper.php new file mode 100644 index 0000000..5959f2f --- /dev/null +++ b/app/Helpers/StringHelper.php @@ -0,0 +1,304 @@ +string = $string; + } + + /** + * Establece el string a manipular + */ + public function setString(string $string): self + { + $this->string = $string; + return $this; + } + + /** + * Obtiene el string actual + */ + public function getString(): string + { + return $this->string; + } + + /** + * Convierte el string a mayúsculas + */ + public function toUpper(): self + { + $this->string = mb_strtoupper($this->string); + return $this; + } + + /** + * Convierte el string a minúsculas + */ + public function toLower(): self + { + $this->string = mb_strtolower($this->string); + return $this; + } + + /** + * Capitaliza la primera letra + */ + public function capitalize(): self + { + $this->string = ucfirst($this->string); + return $this; + } + + /** + * Capitaliza cada palabra + */ + public function capitalizeWords(): self + { + $this->string = ucwords($this->string); + return $this; + } + + /** + * Elimina espacios en blanco al inicio y final + */ + public function trim(): self + { + $this->string = trim($this->string); + return $this; + } + + /** + * Remueve caracteres especiales y acentos + */ + public function removeSpecialChars(): self + { + $unwanted_array = [ + "Š" => "S", + "š" => "s", + "Ž" => "Z", + "ž" => "z", + "À" => "A", + "Á" => "A", + "Â" => "A", + "Ã" => "A", + "Ä" => "A", + "Å" => "A", + "Æ" => "A", + "Ç" => "C", + "È" => "E", + "É" => "E", + "Ê" => "E", + "Ë" => "E", + "Ì" => "I", + "Í" => "I", + "Î" => "I", + "Ï" => "I", + "Ñ" => "N", + "Ò" => "O", + "Ó" => "O", + "Ô" => "O", + "Õ" => "O", + "Ö" => "O", + "Ø" => "O", + "Ù" => "U", + "Ú" => "U", + "Û" => "U", + "Ü" => "U", + "Ý" => "Y", + "Þ" => "B", + "ß" => "ss", + "à" => "a", + "á" => "a", + "â" => "a", + "ã" => "a", + "ä" => "a", + "å" => "a", + "æ" => "a", + "ç" => "c", + "è" => "e", + "é" => "e", + "ê" => "e", + "ë" => "e", + "ì" => "i", + "í" => "i", + "î" => "i", + "ï" => "i", + "ð" => "o", + "ñ" => "n", + "ò" => "o", + "ó" => "o", + "ô" => "o", + "õ" => "o", + "ö" => "o", + "ø" => "o", + "ù" => "u", + "ú" => "u", + "û" => "u", + "ý" => "y", + "ý" => "y", + "þ" => "b", + "ÿ" => "y", + ]; + $this->string = strtr($this->string, $unwanted_array); + return $this; + } + + /** + * Genera un slug a partir del string + */ + public function slug(): self + { + $this->removeSpecialChars(); + $this->string = strtolower( + trim(preg_replace("/[^A-Za-z0-9-]+/", "-", $this->string)) + ); + return $this; + } + + /** + * Trunca el string a una longitud específica + */ + public function truncate(int $length, string $append = "..."): self + { + if (strlen($this->string) > $length) { + $this->string = substr($this->string, 0, $length) . $append; + } + return $this; + } + + /** + * Cuenta las palabras en el string + */ + public function wordCount(): int + { + return str_word_count($this->string); + } + + /** + * Revierte el string + */ + public function reverse(): self + { + $this->string = strrev($this->string); + return $this; + } + + /** + * Reemplaza todas las ocurrencias de un string por otro + */ + public function replace(string $search, string $replace): self + { + $this->string = str_replace($search, $replace, $this->string); + return $this; + } + + /** + * Extrae una substring + */ + public function substring(int $start, ?int $length = null): self + { + $this->string = substr($this->string, $start, $length); + return $this; + } + + /** + * Convierte el string a camelCase + */ + public function toCamelCase(): self + { + $this->string = lcfirst( + str_replace( + " ", + "", + ucwords(str_replace(["_", "-"], " ", $this->string)) + ) + ); + return $this; + } + + /** + * Convierte el string a snake_case + */ + public function toSnakeCase(): self + { + $this->string = strtolower( + preg_replace( + ["/([a-z\d])([A-Z])/", "/([^_])([A-Z][a-z])/"], + '$1_$2', + $this->string + ) + ); + return $this; + } + + /** + * Verifica si el string contiene una substring + */ + public function contains(string $needle): bool + { + return strpos($this->string, $needle) !== false; + } + + /** + * Verifica si el string empieza con una substring + */ + public function startsWith(string $needle): bool + { + return strpos($this->string, $needle) === 0; + } + + /** + * Verifica si el string termina con una substring + */ + public function endsWith(string $needle): bool + { + return substr($this->string, -strlen($needle)) === $needle; + } + + /** + * Limpia el string de HTML y PHP tags + */ + public function stripTags(): self + { + $this->string = strip_tags($this->string); + return $this; + } + + /** + * Convierte caracteres especiales a entidades HTML + */ + public function htmlEntities(): self + { + $this->string = htmlentities($this->string, ENT_QUOTES, "UTF-8"); + return $this; + } + + /** + * Genera un hash MD5 del string + */ + public function toMd5(): self + { + $this->string = md5($this->string); + return $this; + } + + /** + * Genera un string aleatorio + */ + public function random(int $length = 10): self + { + $characters = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + $this->string = ""; + for ($i = 0; $i < $length; $i++) { + $this->string .= $characters[rand(0, strlen($characters) - 1)]; + } + return $this; + } +} diff --git a/app/Middleware/AuthorizationMiddleware.php b/app/Middleware/AuthorizationMiddleware.php new file mode 100644 index 0000000..eae20b9 --- /dev/null +++ b/app/Middleware/AuthorizationMiddleware.php @@ -0,0 +1,65 @@ +jwtSecret = $_ENV["JWT_SECRET"]; + } + + public function handle($requestUri, $requestMethod) + { + $routes = include __DIR__ . "/../Config/Routes.php"; + $isProtected = $this->isProtectedRoute( + $routes, + $requestUri, + $requestMethod + ); + + if (!$isProtected) { + // La ruta es pública, permitir acceso + return true; + } + + $jwt = $_COOKIE["jwt"] ?? null; + + if (!$jwt) { + // El usuario no está logueado, redirigir al login + header("Location: /login"); + exit(); + } + + try { + $decoded = JWT::decode($jwt, new Key($this->jwtSecret, "HS256")); + } catch (\Exception $e) { + // Token inválido, redirigir al login + header("Location: /login"); + exit(); + } + + // Token válido, continuar + return true; + } + + private function isProtectedRoute($routes, $requestUri, $requestMethod) + { + foreach ($routes as $route) { + if ( + $route["uri"] === $requestUri && + $route["method"] === $requestMethod + ) { + return $route["protected"]; + } + } + // Si la ruta no se encuentra, se considera protegida por defecto + return true; + } +} diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php new file mode 100644 index 0000000..a5adb98 --- /dev/null +++ b/app/Repositories/BaseRepository.php @@ -0,0 +1,196 @@ +db = $database->getConnection(); + $this->table = $table; + + if (!is_subclass_of($entityClass, BaseEntity::class)) { + throw new Exception("La clase de entidad debe extender de BaseEntity."); + } + + $this->entityClass = $entityClass; + $this->primaryKey = $entityClass::getPrimaryKey(); + } + + /** + * Obtiene todas las filas de la tabla. + * + * @return array + */ + public function getAll(): array + { + return $this->db->select($this->table, "*"); + } + + /** + * Encuentra una fila por un campo específico. + * + * @param string $field Campo por el cual buscar. + * @param mixed $value Valor del campo. + * + * @return array|null + */ + public function findBy(string $field, $value): ?array + { + return $this->db->get($this->table, "*", [$field => $value]); + } + + /** + * Encuentra una fila por su ID. + * + * @param mixed $id Identificador de la fila. + * + * @return array|null + */ + public function findById($id): ?array + { + return $this->db->get($this->table, "*", [$this->primaryKey => $id]); + } + + /** + * Inserta una fila en la tabla. + * + * @param array $data Datos a insertar. + * + * @return bool|int + */ + public function insert(array $data): int|false + { + try { + $this->db->insert($this->table, $data); + + $lastInsertId = (int) $this->db->id(); + + if ($lastInsertId) { + return $lastInsertId; + } else { + error_log("Error al insertar en la tabla {$this->table}: " . print_r($this->db->error(), true)); + return false; + } + } catch (Exception $e) { + // Registra el error de la excepción + error_log("Excepción al insertar en la tabla {$this->table}: " . $e->getMessage()); + return false; + } + } + + /** + * Actualiza una fila por su ID. + * + * @param mixed $id Identificador de la fila a actualizar. + * @param array $data Datos a actualizar. + * + * @return bool + */ + public function update($id, array $data): bool + { + return $this->db->update($this->table, $data, [ + $this->primaryKey => $id, + ])->rowCount() > 0; + } + + /** + * Elimina una fila por su ID. + * + * @param mixed $id Identificador de la fila a eliminar. + * + * @return bool + */ + public function delete($id): bool + { + return $this->db->delete($this->table, [$this->primaryKey => $id])->rowCount() > 0; + } + + /** + * Marca una fila como inactiva (soft delete). + * + * @param mixed $id Identificador de la fila. + * + * @return bool + */ + public function deleteActive($id): bool + { + return $this->db->update( + $this->table, + ["active" => false], + [$this->primaryKey => $id] + )->rowCount() > 0; + } + + /** + * Obtiene una lista paginada de filas. + * + * @param int $page Número de página. + * @param int $perPage Número de filas por página. + * + * @return array + */ + public function getPaginated(int $page, int $perPage): array + { + $perPage = min($perPage, 100); + $offset = ($page - 1) * $perPage; + return $this->db->select($this->table, "*", [ + "LIMIT" => [$offset, $perPage], + ]); + } + + /** + * Cuenta el total de filas en la tabla. + * + * @return int + */ + public function countAll(): int + { + return $this->db->count($this->table); + } + + /** + * Inicia una transacción. + */ + public function beginTransaction() + { + $this->db->pdo->beginTransaction(); + } + + /** + * Confirma la transacción. + */ + public function commit() + { + $this->db->pdo->commit(); + } + + /** + * Revierte la transacción. + */ + public function rollBack() + { + if ($this->db->pdo->inTransaction()) { + $this->db->pdo->rollBack(); + } + } +} diff --git a/app/Repositories/EmailStatusRepository.php b/app/Repositories/EmailStatusRepository.php new file mode 100644 index 0000000..5268e05 --- /dev/null +++ b/app/Repositories/EmailStatusRepository.php @@ -0,0 +1,14 @@ + + + + + + + Iniciar Sesión + + + + + + + + +
+
+
+

GTD Assistant

+

Dale un respiro a tu mente organizándote con este sistema.

+ + + + +
+
+ + +
+
+ + +
+ +
+

¿No tienes una cuenta? Regístrate

+
+
+
+ + + diff --git a/app/Views/auth/register.php b/app/Views/auth/register.php new file mode 100644 index 0000000..c0207e5 --- /dev/null +++ b/app/Views/auth/register.php @@ -0,0 +1,221 @@ + + + + + + Registrarse + + + + + + + +
+
+
+

Nuevo Usuario

+
+
+ + " id="username" name="username" value="" required> + +
+ +
+ +
+
+ + " id="email" name="email" value="" required> + +
+ +
+ +
+
+ + " id="password" name="password" required> + +
+ +
+ +
+ +
+

¿Ya tienes una cuenta? Iniciar Sesión

+
+
+
+ + + + + + + + + diff --git a/app/Views/components/breadcrumb.php b/app/Views/components/breadcrumb.php new file mode 100644 index 0000000..cdbfc70 --- /dev/null +++ b/app/Views/components/breadcrumb.php @@ -0,0 +1,31 @@ + $moduloHijo['nombre'], + 'icono' => $moduloHijo['icono'] + ]; + break 2; + } + } + } +} +?> + + + + diff --git a/app/Views/components/navbar.php b/app/Views/components/navbar.php new file mode 100644 index 0000000..2b64e3a --- /dev/null +++ b/app/Views/components/navbar.php @@ -0,0 +1,155 @@ + + + diff --git a/app/Views/error/.gitkeep b/app/Views/error/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/Views/error/400.php b/app/Views/error/400.php new file mode 100644 index 0000000..eac4bce --- /dev/null +++ b/app/Views/error/400.php @@ -0,0 +1,56 @@ + + + + + + + Error 400 - Solicitud Incorrecta + + + + + +
+
+
+

Solicitud Incorrecta

+
+ [Placeholder para imagen de error 400] +
+

Error 400

+

La solicitud enviada no es válida o está mal estructurada.

+
+
+
+ + + diff --git a/app/Views/error/401.php b/app/Views/error/401.php new file mode 100644 index 0000000..a6109dc --- /dev/null +++ b/app/Views/error/401.php @@ -0,0 +1,56 @@ + + + + + + + Error 401 - No Autorizado + + + + + +
+
+
+

No Autorizado

+
+ [Placeholder para imagen de error 401] +
+

Error 401

+

No tienes autorización para acceder a esta página.

+
+
+
+ + + diff --git a/app/Views/error/403.php b/app/Views/error/403.php new file mode 100644 index 0000000..a662801 --- /dev/null +++ b/app/Views/error/403.php @@ -0,0 +1,60 @@ + + + + + + + Error 403 - Prohibido + + + + + + + + +
+
+
+

Acceso Prohibido

+
+ [Placeholder para imagen de error 403] +
+

Error 403

+

No tienes permisos para acceder a esta página.

+
+
+
+ + + \ No newline at end of file diff --git a/app/Views/error/404.php b/app/Views/error/404.php new file mode 100644 index 0000000..3aa44af --- /dev/null +++ b/app/Views/error/404.php @@ -0,0 +1,66 @@ + + + + + + + Error 404 - Página no encontrada + + + + + + + + +
+
+
+

Oops! Página no encontrada

+
+ [Placeholder para imagen de error 404] +
+

Error 404

+

Lo sentimos, la página que estás buscando no existe o ha sido movida.

+
+
+
+ + + \ No newline at end of file diff --git a/app/Views/error/408.php b/app/Views/error/408.php new file mode 100644 index 0000000..c5386d7 --- /dev/null +++ b/app/Views/error/408.php @@ -0,0 +1,56 @@ + + + + + + + Error 408 - Tiempo de Espera Agotado + + + + + +
+
+
+

Tiempo de Espera Agotado

+
+ [Placeholder para imagen de error 408] +
+

Error 408

+

El servidor no pudo procesar tu solicitud en el tiempo esperado.

+
+
+
+ + + diff --git a/app/Views/error/500.php b/app/Views/error/500.php new file mode 100644 index 0000000..cc9b8ef --- /dev/null +++ b/app/Views/error/500.php @@ -0,0 +1,60 @@ + + + + + + + Error 500 - Error Interno del Servidor + + + + + + + + +
+
+
+

Error Interno del Servidor

+
+ [Placeholder para imagen de error 500] +
+

Error 500

+

Ocurrió un problema inesperado en el servidor.

+
+
+
+ + + \ No newline at end of file diff --git a/app/Views/error/503.php b/app/Views/error/503.php new file mode 100644 index 0000000..2b4ab1b --- /dev/null +++ b/app/Views/error/503.php @@ -0,0 +1,56 @@ + + + + + + + Error 503 - Servicio No Disponible + + + + + +
+
+
+

Servicio No Disponible

+
+ [Placeholder para imagen de error 503] +
+

Error 503

+

El servidor no está disponible temporalmente.

+
+
+
+ + + diff --git a/app/Views/home/home.php b/app/Views/home/home.php new file mode 100644 index 0000000..a4b33a1 --- /dev/null +++ b/app/Views/home/home.php @@ -0,0 +1,440 @@ + + + + +
+
+ +
+

+

Automatiza tus tareas diarias y mejora tu productividad con +

+
+ + +
+ +
+
+
+
+
+
+ Balance General
+
Q 0
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+ Eventos Próximos
+
0
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+ Tareas En Proceso
+
0
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+ Tareas Completadas
+
0
+
+
+ +
+
+
+
+
+
+ + +
+ +
+
+
+
Calendario de Eventos
+ +
+
+
+
+
+
+ + +
+ +
+
+
Actividad Física Últimos 7 Días
+
+
+ +
+
+ + +
+
+
Próximos Eventos
+
+
+
+ +
+
+
+
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/app/Views/layouts/footer.php b/app/Views/layouts/footer.php new file mode 100644 index 0000000..a5a5492 --- /dev/null +++ b/app/Views/layouts/footer.php @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/Views/layouts/header.php b/app/Views/layouts/header.php new file mode 100644 index 0000000..b5a4ffa --- /dev/null +++ b/app/Views/layouts/header.php @@ -0,0 +1,201 @@ + + + + + + + <?php echo $_ENV['APP_NAME']; ?> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/Views/modules/.gitkeep b/app/Views/modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/Views/modules/clientes/index.php b/app/Views/modules/clientes/index.php new file mode 100644 index 0000000..12e7edd --- /dev/null +++ b/app/Views/modules/clientes/index.php @@ -0,0 +1,446 @@ + $length ? substr($text, 0, $length) . "..." : $text; +} +?> + +
+
+
+ +
+
+
+
+ + +
+
+ +
+
+ + + + + + + + + + + + + + "> + + + + + + + + +
NombreDirecciónTeléfonoNITActivoAcciones
+ + + + + + +
+ + + +
+
+
+ + + + + + + + + + + + diff --git a/app/Views/modules/fitness/exercises.php b/app/Views/modules/fitness/exercises.php new file mode 100644 index 0000000..e69de29 diff --git a/app/Views/modules/fitness/rutinas.php b/app/Views/modules/fitness/rutinas.php new file mode 100644 index 0000000..39f69e9 --- /dev/null +++ b/app/Views/modules/fitness/rutinas.php @@ -0,0 +1,483 @@ + + + + +
+
+
+
+

+ Rutinas Personalizadas +

+
+
+ +
+
+
+
+
+ +
+

Perfil de Gymbro

+
+ +
+
+
Nivel
+
1
+
+
+
Puntos XP
+
0
+
+
+ +
+
+
+
+
+
+
+
+
Misión Diaria
+ +
    + +
+ +
+
+
+
+ +
+
+

Logros Desbloqueados

+
+ +
+
+
+
+
+ + + + + + + + diff --git a/app/Views/modules/tareas/kanban.php b/app/Views/modules/tareas/kanban.php new file mode 100644 index 0000000..43f20da --- /dev/null +++ b/app/Views/modules/tareas/kanban.php @@ -0,0 +1,635 @@ + + + + +
+
+

Kanban Dashboard

+
+ +
+ +
+ +
+ +
+
+
Pendiente
+
+
+
+
En Proceso
+
+
+
+
Completada
+
+
+
+
Cancelada
+
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/bin/Crafter.php b/bin/Crafter.php new file mode 100644 index 0000000..308cde3 --- /dev/null +++ b/bin/Crafter.php @@ -0,0 +1,229 @@ +printHelp(); + return; + } + + switch ($command) { + case "create:controller": + $this->createController($arguments); + break; + case "create:repository": + $this->createRepository($arguments); + break; + case "create:entity": + $this->createEntity($arguments); + break; + case "create:config": + $this->createNewConfig($arguments); + break; + default: + echo "Unknown command: $command\n"; + $this->printHelp(); + break; + } + } + + /** + * Create a new file with the given content. + * This method ensures that the directory exists and the file does not already exist. + * + * @param string $filePath The path to the file. + * @param string $content The content to write to the file. + * @throws Exception If the file already exists or if it cannot be created. + */ + private function createFile(string $filePath, string $content): void + { + $directory = dirname($filePath); + if (!is_dir($directory)) { + if (!mkdir($directory, 0755, true)) { + throw new Exception("Failed to create directory: $directory"); + } + } + + if (file_exists($filePath)) { + throw new Exception("File already exists: $filePath"); + } + + if (file_put_contents($filePath, $content) === false) { + throw new Exception("Failed to write file: $filePath"); + } + + echo "File created successfully: $filePath\n"; + } + + /** + * Creates a new controller class file. + * This function generates a new PHP class file for a controller in the 'app/Controllers' directory. + * It uses the provided name or defaults to 'NewController' if no name is given. The controller + * class is created with a basic structure including the namespace and an empty class body. + * + * @param array $arguments The arguments passed to the command. The first + * argument is the controller name. + * + * @throws Exception If the file already exists or if it cannot be created. + * + * @return void + * + * Usage example: + * $this->createController(['User']); // Creates a UserController + */ + private function createController($arguments) + { + $controllerName = $arguments[0] ?? "NewController"; + $controllerName = ucfirst($arguments[0] ?? "New") . "Controller"; + $filePath = "app/Controllers/{$controllerName}.php"; + + $controllerContent = "createFile($filePath, $controllerContent); + } catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + } + } + + /** + * Creates a new repository class file. + * + * This function generates a new PHP class file for a repository in the 'app/Repositories' directory. + * It uses the provided name or defaults to 'NewRepository' if no name is given. The repository + * class is created with a basic structure including the namespace and an empty class body + * that extends BaseRepository. + * + * @param array $arguments The arguments passed to the command. The first element, if present, + * is used as the repository name. + * + * @throws Exception If there's an error in file creation, which is caught and its message displayed. + * + * @return void + * + * Usage example: + * $this->createRepository(['User']); // Creates a UserRepository + */ + private function createRepository($arguments) + { + $repositoryName = $arguments[0] ?? "NewRepository"; + $repositoryName = ucfirst($arguments[0] ?? "New") . "Repository"; + $filePath = "app/Repositories/{$repositoryName}.php"; + + $repositoryContent = "createFile($filePath, $repositoryContent); + } catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + } + } + + private function createNewConfig($arguments) + { + $configName = $arguments[0] ?? "NewConfig"; + echo "Creating new config file: $configName\n"; + echo "Arguments: " . implode(", ", $arguments) . "\n"; + } + + /** + * Creates a new entity class file. + * + * This function generates a new PHP class file for an entity in the 'app/Entities' directory. + * It uses the provided name or defaults to 'NewEntity' if no name is given. The entity + * class is created with a basic structure including the namespace and an empty class body. + * + * @param array $arguments An array of command-line arguments. The first element, if present, + * is used as the entity name. + * + * @throws Exception If there's an error in file creation, which is caught and its message displayed. + * + * @return void + * + * Usage example: + * $this->createEntity(['User']); // Creates a User entity + */ + private function createEntity($arguments) + { + $entityName = $arguments[0] ?? "NewEntity"; + $entityName = ucfirst($entityName); + + $primaryKey = 'id'; // Valor por defecto + foreach ($arguments as $index => $arg) { + if ($arg === '--pk' && isset($arguments[$index + 1])) { + $primaryKey = $arguments[$index + 1]; + break; + } + } + + $filePath = "app/Entities/{$entityName}.php"; + + $entityContent = <<createFile($filePath, $entityContent); + } catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + } + } + + protected function printHelp() + { + echo " + ( ( + ( ( ) ( )\ ) )\ ) + )\ ( ) )\ ) ( /( ( ( )\ (()/( (()/( + (((_) )( ( /( (()/( )\()) ))\ )( (((_) /(_)) /(_)) + )\___ (()\ )(_)) /(_)) (_))/ /((_) (()\ )\___ (_)) (_)) + ((/ __| ((_) ((_)_ (_) _| | |_ (_)) ((_) ___ ((/ __| | | |_ _| + | (__ | '_| / _` | | _| | _| / -_) | '_| |___| | (__ | |__ | | + \___| |_| \__,_| |_| \__| \___| |_| \___| |____| |___| + + \n\n"; + echo "Available Commands:\n"; + echo " create:controller Create a new controller\n"; + echo " create:repository Create a new repository\n"; + echo " create:entity Create a new entity\n"; + echo " create:config Create a new config\n"; + echo "\nUsage:\n"; + echo " crafter [arguments]\n"; + echo "\nExamples:\n"; + echo " crafter create:controller User\n"; + echo " crafter create:repository User\n"; + echo " crafter create:entity User\n"; + echo " crafter create:config database\n"; + } +} diff --git a/bin/SysTask/.gitkeep b/bin/SysTask/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6c87454 --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "require": { + "catfan/medoo": "^2.1", + "phpmailer/phpmailer": "^6.9", + "monolog/monolog": "^3.7", + "vlucas/phpdotenv": "^5.6", + "consolidation/robo": "^5.0", + "altorouter/altorouter": "^2.0", + "symfony/http-foundation": "^7.1", + "dompdf/dompdf": "^3.0", + "phpoffice/phpspreadsheet": "^2.2", + "moneyphp/money": "^4.5", + "firebase/php-jwt": "^6.10", + "paragonie/sodium_compat": "^2.1", + "jms/serializer": "^3.30" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Console\\": "bin/" + } + } +} diff --git a/crafter b/crafter new file mode 100644 index 0000000..946c75b --- /dev/null +++ b/crafter @@ -0,0 +1,11 @@ +#!/usr/bin/env php +run($command, array_slice($_SERVER['argv'], 2)); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3a30db5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +services: + app: + build: . + container_name: gtd_app + ports: + - "8000:80" + #- "9003:9003" # Xdebug + volumes: + - .:/var/www/html + environment: + - APP_ENV=local + - DB_HOST=${DB_HOST} + - DB_NAME=${DB_NAME} + - DB_USER=${DB_USER} + - DB_PASS=${DB_PASS} + - DB_DRIVER=${DB_DRIVER} + - DB_CHARSET=${DB_CHARSET} + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + - db + command: > + bash -c " + /var/www/html/docker/init_scripts/update_composer.sh && + /var/www/html/docker/init_scripts/check_php_extensions.sh && + apache2-foreground + " + + db: + image: mysql:8.0 + container_name: gtd_db + restart: always + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASS} + MYSQL_DATABASE: ${DB_NAME} + volumes: + - db_data:/var/lib/mysql + - ./schema:/docker-entrypoint-initdb.d + ports: + - "3310:3306" + +volumes: + db_data: diff --git a/docker/init_scripts/check_php_extensions.sh b/docker/init_scripts/check_php_extensions.sh new file mode 100755 index 0000000..db3cbbd --- /dev/null +++ b/docker/init_scripts/check_php_extensions.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}=======================================${NC}" +echo -e "${BLUE} Verificación de Extensiones de PHP ${NC}" +echo -e "${BLUE}=======================================${NC}" + +REQUIRED_EXTENSIONS=( + "xdebug" + "zip" + "mbstring" + "xml" + "intl" + "gmp" + "sodium" + "pdo_mysql" +) + +echo -e "\nVerificando extensiones requeridas:\n" + +ENABLED_COUNT=0 +DISABLED_COUNT=0 + +for extension in "${REQUIRED_EXTENSIONS[@]}"; do + if php -m | grep -q "$extension"; then + echo -e " ${GREEN}✔ $extension${NC}" + ((ENABLED_COUNT++)) + else + echo -e " ${RED}✘ $extension${NC}" + ((DISABLED_COUNT++)) + fi +done + +echo -e "\n${BLUE}=======================================${NC}" +echo -e "${BLUE} Resumen ${NC}" +echo -e "${BLUE}=======================================${NC}" +echo -e " Extensiones habilitadas: ${GREEN}$ENABLED_COUNT${NC}" +echo -e " Extensiones faltantes: ${RED}$DISABLED_COUNT${NC}" +echo -e "${BLUE}=======================================${NC}" + +if [ $DISABLED_COUNT -eq 0 ]; then + echo -e "\n${GREEN}¡Todas las extensiones requeridas están habilitadas!${NC}" +else + echo -e "\n${RED}Algunas extensiones requeridas no están habilitadas.${NC}" + echo -e "Por favor, instale las extensiones faltantes." +fi + +#php -m \ No newline at end of file diff --git a/docker/init_scripts/update_composer.sh b/docker/init_scripts/update_composer.sh new file mode 100755 index 0000000..709cb82 --- /dev/null +++ b/docker/init_scripts/update_composer.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +print_message() { + echo -e "${BLUE}=======================================${NC}" + echo -e "${BLUE} $1${NC}" + echo -e "${BLUE}=======================================${NC}" +} + +PROJECT_DIR="/var/www/html" + +print_message "Actualización de Dependencias de Composer" + +[ ! -d "$PROJECT_DIR" ] && { echo -e "${RED}Error: El directorio $PROJECT_DIR no existe.${NC}"; exit 1; } + +cd "$PROJECT_DIR" || { echo -e "${RED}Error: No se pudo acceder al directorio $PROJECT_DIR${NC}"; exit 1; } + +[ ! -f composer.json ] && { echo -e "${RED}Error: No se encontró el archivo composer.json en $PROJECT_DIR${NC}"; exit 1; } + +echo -e "\n${YELLOW}Actualizando dependencias...${NC}" +COMPOSER_OUTPUT=$(composer install --no-dev --optimize-autoloader 2>&1) + +if echo "$COMPOSER_OUTPUT" | grep -q "Xdebug: \[Step Debug\] Could not connect to debugging client"; then + echo -e "\n${CYAN}Nota sobre Xdebug:${NC}" + echo -e "Se detectó un mensaje de Xdebug intentando conectarse a un cliente de depuración." + echo -e "Esto es normal si no estás depurando activamente y no afecta la actualización de dependencias." + echo -e "\nPara desactivar Xdebug temporalmente, puedes usar:" + echo -e "${YELLOW}php -dxdebug.mode=off /usr/local/bin/composer [comando]${NC}" + echo -e "\nPara desactivar Xdebug permanentemente:" + echo -e "1. Localiza tu php.ini (usa 'php --ini' para encontrarlo)" + echo -e "2. Comenta la línea que carga Xdebug añadiendo un ';' al principio" + echo -e "3. O cambia 'xdebug.mode' a 'off' en la configuración de Xdebug" + echo -e "\nPara evitar intentos de conexión automáticos, añade esta línea a tu php.ini:" + echo -e "${YELLOW}xdebug.start_with_request = trigger${NC}" +fi + +echo -e "\n$COMPOSER_OUTPUT" + +if [ $? -eq 0 ]; then + echo -e "\n${GREEN}¡Todas las dependencias han sido actualizadas exitosamente!${NC}" +else + echo -e "\n${RED}Error: Hubo un problema al actualizar las dependencias.${NC}" + exit 1 +fi + +echo -e "\n${YELLOW}Verificando actualizaciones disponibles...${NC}" +OUTDATED=$(composer outdated --direct) + +if [ -n "$OUTDATED" ]; then + echo -e "\n${YELLOW}Las siguientes dependencias tienen actualizaciones disponibles:${NC}" + echo "$OUTDATED" + echo -e "\n${YELLOW}Considere actualizarlas manualmente con 'composer update '${NC}" +else + echo -e "\n${GREEN}Todas las dependencias están actualizadas.${NC}" +fi + +print_message "Proceso Completado" \ No newline at end of file diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..7373f57 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,7 @@ +ReWriteEngine On + +RewriteCond %{REQUEST_FILENAME} -f [OR] +RewriteCond %{REQUEST_FILENAME} -d +RewriteRule ^ - [L] + +RewriteRule ^ index.php [L] diff --git a/public/assets/css/main.css b/public/assets/css/main.css new file mode 100644 index 0000000..4c638e9 --- /dev/null +++ b/public/assets/css/main.css @@ -0,0 +1,192 @@ +/* Fuente base */ +* { + font-family: "Poppins", sans-serif; +} + +/* Configuración de la barra de navegación */ +.navbar { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background-color: #f8f9fa; +} + +/* Logo de la barra de navegación */ +.navbar-brand { + padding: 0.4rem 1rem; + font-size: 1.6rem; + font-weight: 800; + color: #1a1a1a; + transition: + color 0.3s ease, + transform 0.3s ease; +} + +.navbar-brand:hover { + color: hsl(0, 0%, 30%); + transform: scale(1.1); +} + +/* Elementos de la barra de navegación */ +.nav-item { + margin-right: 0.65rem; +} + +.nav-link { + color: #495057; + font-weight: 500; + font-size: 1.1rem; + padding: 12px 20px; + transition: all 0.3s ease; +} + +.nav-link:hover, +.nav-link:focus { + color: #007bff; + background-color: rgba(0, 123, 255, 0.1); +} + +.nav-link.dropdown-toggle { + font-weight: 600; +} + +/* Menú desplegable */ +.dropdown-menu { + background-color: #ffffff; + border: 1px solid #dee2e6; + border-radius: 0.5rem; + padding: 0.5rem 0; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transition: + opacity 0.3s ease, + transform 0.3s ease; + transform: translateY(10px); + opacity: 0; +} + +.dropdown-menu.show { + opacity: 1; + transform: translateY(0); +} + +.dropdown-item { + display: flex; + align-items: center; + padding: 10px 20px; + font-size: 1rem; + color: #495057; + transition: + background-color 0.3s ease, + color 0.3s ease; +} + +.dropdown-item-content i { + margin-right: 10px; + font-size: 1.2rem; +} + +.dropdown-item:hover, +.dropdown-item:focus { + background-color: #e9ecef; + color: #000; +} + +/* Estilo de breadcrumb */ +.breadcrumb { + background-color: #eeeeee; + border-radius: 0.5rem; + padding: 0.5rem 0.85rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + display: inline-block; +} + +.breadcrumb-item a { + color: #007bff; + text-decoration: none; + font-size: 1rem; +} + +.breadcrumb-item a:hover { + text-decoration: underline; +} + +.breadcrumb-item.active { + color: #6c757d; + font-weight: 600; +} + +.breadcrumb i { + margin-right: 5px; +} + +/* --- Diseño adaptable (Responsive) --- */ + +/* Breakpoint para tablets y pantallas medianas (≥ 768px) */ +@media (max-width: 767.98px) { + .navbar-brand { + font-size: 1.4rem; + } + + .nav-link { + font-size: 1rem; + padding: 10px 15px; + } + + .dropdown-item { + padding: 8px 15px; + } + + .breadcrumb-item a { + font-size: 0.9rem; + } + + .breadcrumb { + padding: 0.4rem 0.75rem; + } +} + +/* Breakpoint para teléfonos y pantallas pequeñas (≤ 576px) */ +@media (max-width: 576px) { + .navbar-brand { + font-size: 1.2rem; + } + + .nav-item { + margin-right: 0.4rem; + } + + .nav-link { + padding: 8px 12px; + font-size: 0.9rem; + } + + .dropdown-item { + padding: 7px 12px; + } + + .breadcrumb-item a { + font-size: 0.85rem; + } + + .breadcrumb { + padding: 0.3rem 0.65rem; + } +} + +/* Breakpoint para pantallas grandes (≥ 1200px) */ +@media (min-width: 1200px) { + .navbar-brand { + font-size: 1.7rem; + } + + .nav-link { + font-size: 1.2rem; + padding: 15px 25px; + } + + .breadcrumb-item a { + font-size: 1.1rem; + } + + .breadcrumb { + padding: 0.6rem 1rem; + } +} diff --git a/public/assets/img/.gitkeep b/public/assets/img/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..955b702 --- /dev/null +++ b/public/index.php @@ -0,0 +1,38 @@ +load(); + +$router = new AltoRouter(); +$routes = include __DIR__ . '/../app/Config/Routes.php'; + +foreach ($routes as $route) { + $router->map($route['method'], $route['uri'], $route['target']); +} + +$match = $router->match(); + +if ($match) { + try { + if (is_callable($match['target'])) { + call_user_func_array($match['target'], $match['params']); + } else { + if (isset($match['target']) && is_array($match['target'])) { + list($controller, $method) = $match['target']; + $controller = new $controller(); + call_user_func_array([$controller, $method], $match['params']); + } else { + (new \App\Controllers\ErrorController())->notFound(); + } + } + } catch (Exception $e) { + (new \App\Controllers\ErrorController())->internalServerError(); + } +} else { + (new \App\Controllers\ErrorController())->notFound(); +} diff --git a/schema/schema.sql b/schema/schema.sql new file mode 100644 index 0000000..c6464b6 --- /dev/null +++ b/schema/schema.sql @@ -0,0 +1,222 @@ +-- Base de Datos Personal Assistant +CREATE DATABASE IF NOT EXISTS personal_assistant_db; +USE personal_assistant_db; + +-- @table: users +-- @description: Tabla de usuarios que contiene la información básica del usuario +CREATE TABLE IF NOT EXISTS users ( + user_id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- @table: task_status +-- @description: Tabla de estados de las tareas +CREATE TABLE IF NOT EXISTS task_status ( + status_id INT AUTO_INCREMENT PRIMARY KEY, + status_name VARCHAR(50) NOT NULL UNIQUE, + description TEXT +); + +-- @table: tasks +-- @description: Tabla de tareas que almacena las tareas creadas por los usuarios +CREATE TABLE IF NOT EXISTS tasks ( + task_id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + status_id INT NOT NULL, + task_title VARCHAR(150) NOT NULL, + description TEXT, + color VARCHAR(10), + due_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- @table: notification_types +-- @description: Tabla de tipos de notificación +CREATE TABLE IF NOT EXISTS notification_types ( + type_id INT AUTO_INCREMENT PRIMARY KEY, + type_name VARCHAR(50) NOT NULL UNIQUE, + description TEXT +); + +-- @table: notifications +-- @description: Tabla de notificaciones que almacena las notificaciones enviadas a los usuarios +CREATE TABLE IF NOT EXISTS notifications ( + notification_id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + type_id INT NOT NULL, + message TEXT NOT NULL, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- @table: settings +-- @description: Tabla de configuraciones del sistema +CREATE TABLE IF NOT EXISTS settings ( + setting_id INT AUTO_INCREMENT PRIMARY KEY, + setting_key VARCHAR(50) NOT NULL UNIQUE, + setting_value VARCHAR(255) NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- @table: frequencies +-- @description: Tabla de frecuencias utilizadas para ingresos y gastos +CREATE TABLE IF NOT EXISTS frequencies ( + frequency_id INT AUTO_INCREMENT PRIMARY KEY, + frequency_name VARCHAR(50) NOT NULL UNIQUE, + description TEXT +); + +-- @table: incomes +-- @description: Tabla de ingresos de los usuarios +CREATE TABLE IF NOT EXISTS incomes ( + income_id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + income_name VARCHAR(100) NOT NULL, + amount DECIMAL(10, 2) NOT NULL, + frequency_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- @table: expense_categories +-- @description: Tabla de categorías de gastos +CREATE TABLE IF NOT EXISTS expense_categories ( + category_id INT AUTO_INCREMENT PRIMARY KEY, + category_name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- @table: expenses +-- @description: Tabla de gastos de los usuarios +CREATE TABLE IF NOT EXISTS expenses ( + expense_id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + category_id INT NOT NULL, + expense_name VARCHAR(100) NOT NULL, + amount DECIMAL(10, 2) NOT NULL, + frequency_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- @table: email_templates +-- @description: Tabla de plantillas de correo +CREATE TABLE IF NOT EXISTS email_templates ( + template_id INT AUTO_INCREMENT PRIMARY KEY, + template_name VARCHAR(100) NOT NULL UNIQUE, + html_content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- @table: email_status +-- @description: Tabla de estados de correo +CREATE TABLE IF NOT EXISTS email_status ( + status_id INT AUTO_INCREMENT PRIMARY KEY, + status_name VARCHAR(50) NOT NULL UNIQUE, + description TEXT +); + +-- @table: sent_emails +-- @description: Tabla de correos enviados por el sistema +CREATE TABLE IF NOT EXISTS sent_emails ( + email_id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + template_id INT NOT NULL, + status_id INT NOT NULL, + subject VARCHAR(150) NOT NULL, + message TEXT NOT NULL, + sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- @table: healthy_recipes +-- @description: Tabla de recetas saludables +CREATE TABLE IF NOT EXISTS healthy_recipes ( + recipe_id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + recipe_name VARCHAR(100) NOT NULL, + description TEXT, + ingredients TEXT, + instructions TEXT, + calories DECIMAL(6, 2), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- @table: workout_routines +-- @description: Tabla de rutinas de gimnasio o calistenia +CREATE TABLE IF NOT EXISTS workout_routines ( + routine_id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + routine_name VARCHAR(100) NOT NULL, + description TEXT, + routine_type VARCHAR(50) NOT NULL, + exercises TEXT, + duration INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- @table: exercises +-- @description: Tabla de ejercicios para rutinas de gimnasio o calistenia +CREATE TABLE IF NOT EXISTS exercises ( + exercise_id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + type VARCHAR(50) NOT NULL, + muscle VARCHAR(50) NOT NULL, + equipment VARCHAR(50), + difficulty VARCHAR(20) NOT NULL, + instructions TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- @table: important_events +-- @description: Tabla de eventos importantes +CREATE TABLE IF NOT EXISTS important_events ( + event_id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + event_name VARCHAR(100) NOT NULL, + event_date DATE NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- @insert: task_status +-- @description: Inserta los estados iniciales de las tareas +INSERT INTO task_status (status_name, description) VALUES +('Pendiente', 'Tarea pendiente de realizar'), +('En Proceso', 'Tarea en ejecución'), +('Completada', 'Tarea completada'), +('Cancelada', 'Tarea cancelada'); + +-- @insert: notification_types +-- @description: Inserta los tipos de notificación iniciales +INSERT INTO notification_types (type_name, description) VALUES +('Alerta', 'Notificación de alerta de alta prioridad'), +('Recordatorio', 'Notificación para recordar tareas o eventos'), +('Información', 'Notificación de información general'); + +-- @insert: settings +-- @description: Inserta configuraciones iniciales del sistema +INSERT INTO settings (setting_key, setting_value, description) VALUES +('theme', 'dark', 'Tema predeterminado de la aplicación'), +('language', 'es', 'Idioma predeterminado de la aplicación'), +('timezone', 'UTC-6', 'Zona horaria predeterminada de la aplicación'); + +-- @insert: frequencies +-- @description: Inserta las frecuencias de ingresos y gastos +INSERT INTO frequencies (frequency_name, description) VALUES +('Diario', 'Frecuencia diaria'), +('Semanal', 'Frecuencia semanal'), +('Mensual', 'Frecuencia mensual'), +('Anual', 'Frecuencia anual'); diff --git a/storage/.gitkeep b/storage/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/xdebug.ini b/xdebug.ini new file mode 100644 index 0000000..67d4dd6 --- /dev/null +++ b/xdebug.ini @@ -0,0 +1,8 @@ +; zend_extension=xdebug.so + +[xdebug] +xdebug.mode=debug +xdebug.start_with_request=yes +xdebug.client_host=host.docker.internal +xdebug.client_port=9003 +xdebug.idekey=VSCODE