first commit
This commit is contained in:
commit
19b9cdba59
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
.git
|
||||
node_modules
|
||||
vendor
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
README.md
|
13
.env.example
Normal file
13
.env.example
Normal file
@ -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
|
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal file
@ -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
|
15
000-default.conf
Normal file
15
000-default.conf
Normal file
@ -0,0 +1,15 @@
|
||||
<VirtualHost *:80>
|
||||
ServerAdmin webmaster@localhost
|
||||
ServerName localhost
|
||||
|
||||
DocumentRoot /var/www/html/public
|
||||
|
||||
<Directory /var/www/html/public>
|
||||
Options Indexes FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
</VirtualHost>
|
51
Dockerfile
Normal file
51
Dockerfile
Normal file
@ -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"]
|
144
README.md
Normal file
144
README.md
Normal file
@ -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.
|
0
app/Adapters/Interfaces/.gitkeep
Normal file
0
app/Adapters/Interfaces/.gitkeep
Normal file
179
app/Config/Modulos.php
Normal file
179
app/Config/Modulos.php
Normal file
@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
// No eliminar este módulo, se utilizara para almacenar las notificaciones del sistema
|
||||
$trash = [
|
||||
"nombre" => "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",
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
380
app/Config/Routes.php
Normal file
380
app/Config/Routes.php
Normal file
@ -0,0 +1,380 @@
|
||||
<?php
|
||||
|
||||
use App\Controllers\AuthController;
|
||||
use App\Controllers\HomeController;
|
||||
use App\Controllers\KanbanController;
|
||||
use App\Controllers\NotificationController;
|
||||
use App\Controllers\SettingController;
|
||||
use App\Controllers\FinanzasController;
|
||||
use App\Controllers\FitnessController;
|
||||
use App\Controllers\EventosController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
return [
|
||||
/**
|
||||
* Rutas de la aplicación
|
||||
*/
|
||||
[
|
||||
"method" => "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,
|
||||
],
|
||||
];
|
174
app/Controllers/AuthController.php
Normal file
174
app/Controllers/AuthController.php
Normal file
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\HttpHelper;
|
||||
|
||||
use App\Repositories\UserRepository;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AuthController extends BaseController
|
||||
{
|
||||
protected $userRepository;
|
||||
private $jwtSecret;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$database = new Database();
|
||||
$this->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");
|
||||
}
|
||||
}
|
60
app/Controllers/BaseController.php
Normal file
60
app/Controllers/BaseController.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Repositories\BaseRepository;
|
||||
|
||||
class BaseController
|
||||
{
|
||||
protected $repository;
|
||||
|
||||
public function __construct(BaseRepository $repository)
|
||||
{
|
||||
$this->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}.";
|
||||
}
|
||||
}
|
||||
}
|
193
app/Controllers/ClienteController.php
Normal file
193
app/Controllers/ClienteController.php
Normal file
@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\HttpHelper;
|
||||
use App\Repositories\ClienteRepository;
|
||||
|
||||
class ClienteController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$clienteRepository = new ClienteRepository(new \App\Core\Database());
|
||||
parent::__construct($clienteRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra la lista de clientes
|
||||
*/
|
||||
public function showIndex()
|
||||
{
|
||||
$request = HttpHelper::getRequest();
|
||||
$page = HttpHelper::getParam($request, "page", 1); // Por defecto, página 1
|
||||
|
||||
$perPage = 8; // Número de clientes por página
|
||||
$totalClientes = $this->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
|
||||
);
|
||||
}
|
||||
}
|
48
app/Controllers/ErrorController.php
Normal file
48
app/Controllers/ErrorController.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
class ErrorController extends ViewController
|
||||
{
|
||||
private function sendErrorHeader($statusCode, $view)
|
||||
{
|
||||
http_response_code($statusCode);
|
||||
$this->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');
|
||||
}
|
||||
}
|
8
app/Controllers/EventosController.php
Normal file
8
app/Controllers/EventosController.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
class EventosController extends BaseController
|
||||
{
|
||||
// Add your controller methods here
|
||||
}
|
27
app/Controllers/ExerciseController.php
Normal file
27
app/Controllers/ExerciseController.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Entities\Exercise;
|
||||
use App\Repositories\ExerciseRepository;
|
||||
use App\Core\Database;
|
||||
use App\Core\HttpHelper;
|
||||
use App\Helpers\AuthHelper;
|
||||
|
||||
class ExerciseController extends BaseController
|
||||
{
|
||||
protected ExerciseRepository $exerciseRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$database = new Database();
|
||||
$this->exerciseRepository = new ExerciseRepository($database);
|
||||
parent::__construct($this->exerciseRepository);
|
||||
}
|
||||
|
||||
public function showExercises()
|
||||
{
|
||||
$exercises = $this->exerciseRepository->getAll();
|
||||
$this->render('modules/fitness/exercises', ['exercises' => $exercises]);
|
||||
}
|
||||
}
|
8
app/Controllers/FinanzasController.php
Normal file
8
app/Controllers/FinanzasController.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
class FinanzasController extends BaseController
|
||||
{
|
||||
// Add your controller methods here
|
||||
}
|
12
app/Controllers/FitnessController.php
Normal file
12
app/Controllers/FitnessController.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
class FitnessController extends ViewController
|
||||
{
|
||||
// Add your controller methods here
|
||||
public function showRutinas()
|
||||
{
|
||||
$this->render('modules/fitness/rutinas');
|
||||
}
|
||||
}
|
11
app/Controllers/HomeController.php
Normal file
11
app/Controllers/HomeController.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
class HomeController extends ViewController
|
||||
{
|
||||
public function showHome()
|
||||
{
|
||||
$this->render('home/home');
|
||||
}
|
||||
}
|
144
app/Controllers/KanbanController.php
Normal file
144
app/Controllers/KanbanController.php
Normal file
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Entities\Task;
|
||||
use App\Repositories\TaskRepository;
|
||||
use App\Core\Database;
|
||||
use App\Core\HttpHelper;
|
||||
|
||||
use App\Helpers\AuthHelper;
|
||||
|
||||
class KanbanController extends BaseController
|
||||
{
|
||||
protected TaskRepository $taskRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$database = new Database();
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
8
app/Controllers/NotificationController.php
Normal file
8
app/Controllers/NotificationController.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
class NotificationController extends BaseController
|
||||
{
|
||||
// Add your controller methods here
|
||||
}
|
8
app/Controllers/SettingController.php
Normal file
8
app/Controllers/SettingController.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
class SettingController extends BaseController
|
||||
{
|
||||
// Add your controller methods here
|
||||
}
|
31
app/Controllers/ViewController.php
Normal file
31
app/Controllers/ViewController.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
/**
|
||||
*
|
||||
* Este controlador se utiliza cuando quieres mostrar una vista en la aplicación.
|
||||
* Se encarga de renderizar la vista y pasarle los datos necesarios. Fue creada para
|
||||
* vistas que solo muestran información y no requieren de lógica del repositorio. Pero
|
||||
* puedes usar logica de PHP para precargar datos en la vista.
|
||||
*
|
||||
* Author: David Vargas
|
||||
* Last Modified: 22/09/2024 13:30
|
||||
*
|
||||
*/
|
||||
|
||||
class ViewController
|
||||
{
|
||||
public 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}.";
|
||||
}
|
||||
}
|
||||
}
|
42
app/Core/Database.php
Normal file
42
app/Core/Database.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Medoo\Medoo;
|
||||
use Dotenv\Dotenv;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Exception;
|
||||
|
||||
class Database
|
||||
{
|
||||
protected $connection;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
try {
|
||||
$dotenv = Dotenv::createImmutable(__DIR__ . "/../../");
|
||||
$dotenv->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;
|
||||
}
|
||||
}
|
118
app/Core/HttpHelper.php
Normal file
118
app/Core/HttpHelper.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
||||
class HttpHelper
|
||||
{
|
||||
/**
|
||||
* Obtiene una solicitud desde las variables globales ($_GET, $_POST, etc.)
|
||||
*
|
||||
* @return Request
|
||||
*/
|
||||
public static function getRequest(): Request
|
||||
{
|
||||
return Request::createFromGlobals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea una respuesta HTML estándar.
|
||||
*
|
||||
* @param string $content
|
||||
* @param int $statusCode
|
||||
* @param array $headers
|
||||
* @return Response
|
||||
*/
|
||||
public static function createResponse(
|
||||
string $content,
|
||||
int $statusCode = 200,
|
||||
array $headers = []
|
||||
): Response {
|
||||
return new Response($content, $statusCode, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea una respuesta JSON.
|
||||
*
|
||||
* @param array $data
|
||||
* @param int $statusCode
|
||||
* @param array $headers
|
||||
* @return void
|
||||
*/
|
||||
public static function createJsonResponse(
|
||||
array $data,
|
||||
int $statusCode = 200,
|
||||
array $headers = []
|
||||
) {
|
||||
http_response_code($statusCode);
|
||||
header("Content-Type: application/json");
|
||||
|
||||
foreach ($headers as $key => $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();
|
||||
}
|
||||
}
|
13
app/Entities/BaseEntity.php
Normal file
13
app/Entities/BaseEntity.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
abstract class BaseEntity
|
||||
{
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public static function getPrimaryKey(): string;
|
||||
}
|
31
app/Entities/EmailStatus.php
Normal file
31
app/Entities/EmailStatus.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class EmailStatus extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único del estado de correo.
|
||||
*/
|
||||
public int $status_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre del estado de correo.
|
||||
*/
|
||||
public string $status_name;
|
||||
|
||||
/**
|
||||
* @var string|null Descripción del estado de correo.
|
||||
*/
|
||||
public ?string $description;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "status_id";
|
||||
}
|
||||
}
|
41
app/Entities/EmailTemplate.php
Normal file
41
app/Entities/EmailTemplate.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class EmailTemplate extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único de la plantilla de correo.
|
||||
*/
|
||||
public int $template_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre de la plantilla.
|
||||
*/
|
||||
public string $template_name;
|
||||
|
||||
/**
|
||||
* @var string Contenido HTML de la plantilla.
|
||||
*/
|
||||
public string $html_content;
|
||||
|
||||
/**
|
||||
* @var string Fecha de creación de la plantilla.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* @var string|null Fecha de última actualización de la plantilla.
|
||||
*/
|
||||
public ?string $updated_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "template_id";
|
||||
}
|
||||
}
|
63
app/Entities/Exercise.php
Normal file
63
app/Entities/Exercise.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
use App\Entities\BaseEntity;
|
||||
|
||||
class Exercise extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único de Exercise.
|
||||
*/
|
||||
public int $exercise_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre del ejercicio.
|
||||
*/
|
||||
public string $name;
|
||||
|
||||
/**
|
||||
* @var string tipo de ejercicio.
|
||||
*/
|
||||
public string $type;
|
||||
|
||||
/**
|
||||
* @var string musculo principal.
|
||||
*/
|
||||
public string $muscle;
|
||||
|
||||
/**
|
||||
* @var string Equipamiento necesario.
|
||||
*/
|
||||
public string $equipment;
|
||||
|
||||
/**
|
||||
* @var string Dificultad del ejercicio.
|
||||
*/
|
||||
public string $difficulty;
|
||||
|
||||
/**
|
||||
* @var string instrucciones del ejercicio.
|
||||
*/
|
||||
public string $instructions;
|
||||
|
||||
/**
|
||||
* @var string Fecha de creación de la tarea.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* @var string|null Fecha de última actualización de la tarea.
|
||||
*/
|
||||
public ?string $updated_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return 'exercise_id';
|
||||
}
|
||||
}
|
56
app/Entities/Expense.php
Normal file
56
app/Entities/Expense.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class Expense extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único del gasto.
|
||||
*/
|
||||
public int $expense_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador del usuario asociado al gasto.
|
||||
*/
|
||||
public int $user_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador de la categoría de gasto.
|
||||
*/
|
||||
public int $category_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre del gasto.
|
||||
*/
|
||||
public string $expense_name;
|
||||
|
||||
/**
|
||||
* @var float Monto del gasto.
|
||||
*/
|
||||
public float $amount;
|
||||
|
||||
/**
|
||||
* @var int Identificador de la frecuencia del gasto.
|
||||
*/
|
||||
public int $frequency_id;
|
||||
|
||||
/**
|
||||
* @var string Fecha de creación del gasto.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* @var string|null Fecha de última actualización del gasto.
|
||||
*/
|
||||
public ?string $updated_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "expense_id";
|
||||
}
|
||||
}
|
41
app/Entities/ExpenseCategory.php
Normal file
41
app/Entities/ExpenseCategory.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class ExpenseCategory extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único de la categoría de gasto.
|
||||
*/
|
||||
public int $category_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre de la categoría de gasto.
|
||||
*/
|
||||
public string $category_name;
|
||||
|
||||
/**
|
||||
* @var string|null Descripción de la categoría de gasto.
|
||||
*/
|
||||
public ?string $description;
|
||||
|
||||
/**
|
||||
* @var string Fecha de creación de la categoría.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* @var string|null Fecha de última actualización de la categoría.
|
||||
*/
|
||||
public ?string $updated_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "category_id";
|
||||
}
|
||||
}
|
31
app/Entities/Frequency.php
Normal file
31
app/Entities/Frequency.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class Frequency extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único de la frecuencia.
|
||||
*/
|
||||
public int $frequency_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre de la frecuencia.
|
||||
*/
|
||||
public string $frequency_name;
|
||||
|
||||
/**
|
||||
* @var string|null Descripción de la frecuencia.
|
||||
*/
|
||||
public ?string $description;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "frequency_id";
|
||||
}
|
||||
}
|
61
app/Entities/HealthyRecipe.php
Normal file
61
app/Entities/HealthyRecipe.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class HealthyRecipe extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único de la receta.
|
||||
*/
|
||||
public int $recipe_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador del usuario asociado a la receta.
|
||||
*/
|
||||
public int $user_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre de la receta.
|
||||
*/
|
||||
public string $recipe_name;
|
||||
|
||||
/**
|
||||
* @var string|null Descripción de la receta.
|
||||
*/
|
||||
public ?string $description;
|
||||
|
||||
/**
|
||||
* @var array Lista de ingredientes de la receta.
|
||||
*/
|
||||
public array $ingredients;
|
||||
|
||||
/**
|
||||
* @var string Instrucciones para preparar la receta.
|
||||
*/
|
||||
public string $instructions;
|
||||
|
||||
/**
|
||||
* @var int Número de calorías de la receta.
|
||||
*/
|
||||
public int $calories;
|
||||
|
||||
/**
|
||||
* @var string Fecha de creación de la receta.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* @var string|null Fecha de última actualización de la receta.
|
||||
*/
|
||||
public ?string $updated_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "recipe_id";
|
||||
}
|
||||
}
|
51
app/Entities/ImportantEvent.php
Normal file
51
app/Entities/ImportantEvent.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class ImportantEvent extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único del evento importante.
|
||||
*/
|
||||
public int $event_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador del usuario asociado al evento.
|
||||
*/
|
||||
public int $user_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre del evento.
|
||||
*/
|
||||
public string $event_name;
|
||||
|
||||
/**
|
||||
* @var string Fecha del evento.
|
||||
*/
|
||||
public string $event_date;
|
||||
|
||||
/**
|
||||
* @var string|null Descripción del evento.
|
||||
*/
|
||||
public ?string $description;
|
||||
|
||||
/**
|
||||
* @var string Fecha de creación del evento.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* @var string|null Fecha de última actualización del evento.
|
||||
*/
|
||||
public ?string $updated_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "event_id";
|
||||
}
|
||||
}
|
51
app/Entities/Income.php
Normal file
51
app/Entities/Income.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class Income extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único del ingreso.
|
||||
*/
|
||||
public int $income_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador del usuario asociado al ingreso.
|
||||
*/
|
||||
public int $user_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre del ingreso.
|
||||
*/
|
||||
public string $income_name;
|
||||
|
||||
/**
|
||||
* @var float Monto del ingreso.
|
||||
*/
|
||||
public float $amount;
|
||||
|
||||
/**
|
||||
* @var int Identificador de la frecuencia del ingreso.
|
||||
*/
|
||||
public int $frequency_id;
|
||||
|
||||
/**
|
||||
* @var string Fecha de creación del ingreso.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* @var string|null Fecha de última actualización del ingreso.
|
||||
*/
|
||||
public ?string $updated_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "income_id";
|
||||
}
|
||||
}
|
46
app/Entities/Notification.php
Normal file
46
app/Entities/Notification.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class Notification extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único de la notificación.
|
||||
*/
|
||||
public int $notification_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador del usuario asociado a la notificación.
|
||||
*/
|
||||
public int $user_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador del tipo de notificación.
|
||||
*/
|
||||
public int $type_id;
|
||||
|
||||
/**
|
||||
* @var string Mensaje de la notificación.
|
||||
*/
|
||||
public string $message;
|
||||
|
||||
/**
|
||||
* @var bool Indica si la notificación ha sido leída.
|
||||
*/
|
||||
public bool $is_read;
|
||||
|
||||
/**
|
||||
* @var string Fecha de creación de la notificación.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "notification_id";
|
||||
}
|
||||
}
|
31
app/Entities/NotificationType.php
Normal file
31
app/Entities/NotificationType.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class NotificationType extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único del tipo de notificación.
|
||||
*/
|
||||
public int $type_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre del tipo de notificación.
|
||||
*/
|
||||
public string $type_name;
|
||||
|
||||
/**
|
||||
* @var string|null Descripción del tipo de notificación.
|
||||
*/
|
||||
public ?string $description;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "type_id";
|
||||
}
|
||||
}
|
51
app/Entities/SentEmail.php
Normal file
51
app/Entities/SentEmail.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class SentEmail extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único del correo enviado.
|
||||
*/
|
||||
public int $email_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador del usuario que envió el correo.
|
||||
*/
|
||||
public int $user_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador de la plantilla del correo.
|
||||
*/
|
||||
public int $template_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador del estado del correo.
|
||||
*/
|
||||
public int $status_id;
|
||||
|
||||
/**
|
||||
* @var string Asunto del correo.
|
||||
*/
|
||||
public string $subject;
|
||||
|
||||
/**
|
||||
* @var string Cuerpo del mensaje del correo.
|
||||
*/
|
||||
public string $message;
|
||||
|
||||
/**
|
||||
* @var string Fecha en la que se envió el correo.
|
||||
*/
|
||||
public string $sent_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "email_id";
|
||||
}
|
||||
}
|
41
app/Entities/Setting.php
Normal file
41
app/Entities/Setting.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class Setting extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único de la configuración.
|
||||
*/
|
||||
public int $setting_id;
|
||||
|
||||
/**
|
||||
* @var string Clave de la configuración.
|
||||
*/
|
||||
public string $setting_key;
|
||||
|
||||
/**
|
||||
* @var string Valor de la configuración.
|
||||
*/
|
||||
public string $setting_value;
|
||||
|
||||
/**
|
||||
* @var string|null Descripción de la configuración.
|
||||
*/
|
||||
public ?string $description;
|
||||
|
||||
/**
|
||||
* @var string Fecha de creación de la configuración.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "setting_id";
|
||||
}
|
||||
}
|
61
app/Entities/Task.php
Normal file
61
app/Entities/Task.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class Task extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único de la tarea.
|
||||
*/
|
||||
public int $task_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador del usuario que creó la tarea.
|
||||
*/
|
||||
public int $user_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador del estado de la tarea.
|
||||
*/
|
||||
public int $status_id;
|
||||
|
||||
/**
|
||||
* @var string Título de la tarea.
|
||||
*/
|
||||
public string $task_title;
|
||||
|
||||
/**
|
||||
* @var string Color de la tarea.
|
||||
*/
|
||||
public string $color;
|
||||
|
||||
/**
|
||||
* @var string|null Descripción de la tarea.
|
||||
*/
|
||||
public ?string $description;
|
||||
|
||||
/**
|
||||
* @var string Fecha de vencimiento de la tarea.
|
||||
*/
|
||||
public string $due_date;
|
||||
|
||||
/**
|
||||
* @var string Fecha de creación de la tarea.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* @var string|null Fecha de última actualización de la tarea.
|
||||
*/
|
||||
public ?string $updated_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "task_id";
|
||||
}
|
||||
}
|
31
app/Entities/TaskStatus.php
Normal file
31
app/Entities/TaskStatus.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class TaskStatus extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único del estado de la tarea.
|
||||
*/
|
||||
public int $status_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre del estado.
|
||||
*/
|
||||
public string $status_name;
|
||||
|
||||
/**
|
||||
* @var string|null Descripción del estado.
|
||||
*/
|
||||
public ?string $description;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "status_id";
|
||||
}
|
||||
}
|
46
app/Entities/User.php
Normal file
46
app/Entities/User.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class User extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único del usuario.
|
||||
*/
|
||||
public int $user_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre de usuario.
|
||||
*/
|
||||
public string $username;
|
||||
|
||||
/**
|
||||
* @var string Correo electrónico del usuario.
|
||||
*/
|
||||
public string $email;
|
||||
|
||||
/**
|
||||
* @var string Hash de la contraseña.
|
||||
*/
|
||||
public string $password_hash;
|
||||
|
||||
/**
|
||||
* @var string Fecha de creación.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* @var string|null Fecha de última actualización.
|
||||
*/
|
||||
public ?string $updated_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "user_id";
|
||||
}
|
||||
}
|
61
app/Entities/WorkoutRoutine.php
Normal file
61
app/Entities/WorkoutRoutine.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
class WorkoutRoutine extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único de la rutina.
|
||||
*/
|
||||
public int $routine_id;
|
||||
|
||||
/**
|
||||
* @var int Identificador del usuario asociado.
|
||||
*/
|
||||
public int $user_id;
|
||||
|
||||
/**
|
||||
* @var string Nombre de la rutina.
|
||||
*/
|
||||
public string $routine_name;
|
||||
|
||||
/**
|
||||
* @var string|null Descripción de la rutina.
|
||||
*/
|
||||
public ?string $description;
|
||||
|
||||
/**
|
||||
* @var string Tipo de rutina.
|
||||
*/
|
||||
public string $routine_type;
|
||||
|
||||
/**
|
||||
* @var array array de ejercicios asociados a la rutina.
|
||||
*/
|
||||
public array $exercises;
|
||||
|
||||
/**
|
||||
* @var int tiempo en minutos de la rutina.
|
||||
*/
|
||||
public int $duration;
|
||||
|
||||
/**
|
||||
* @var string fecha de creación de la rutina.
|
||||
*/
|
||||
public string $created_at;
|
||||
|
||||
/**
|
||||
* @var string|null fecha de última actualización de la rutina.
|
||||
*/
|
||||
public ?string $updated_at;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return "routine_id";
|
||||
}
|
||||
}
|
25
app/Helpers/AuthHelper.php
Normal file
25
app/Helpers/AuthHelper.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
|
||||
class AuthHelper
|
||||
{
|
||||
public static function getUserIdFromToken(): ?int
|
||||
{
|
||||
$jwt = $_COOKIE["jwt"] ?? null;
|
||||
|
||||
if (!$jwt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = JWT::decode($jwt, new Key($_ENV["JWT_SECRET"], "HS256"));
|
||||
return $decoded->user_id ?? null;
|
||||
} catch (\Throwable $th) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
315
app/Helpers/SerializeHelper.php
Normal file
315
app/Helpers/SerializeHelper.php
Normal file
@ -0,0 +1,315 @@
|
||||
<?php
|
||||
namespace App\Helpers;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use JsonException;
|
||||
use RuntimeException;
|
||||
use WeakMap;
|
||||
use Stringable;
|
||||
use ValueError;
|
||||
|
||||
class SerializeHelper
|
||||
{
|
||||
private const JSON_OPTIONS =
|
||||
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
|
||||
private const XML_VERSION = "1.0";
|
||||
private const XML_ENCODING = "UTF-8";
|
||||
|
||||
/**
|
||||
* Caché usando WeakMap para mejor gestión de memoria
|
||||
*/
|
||||
private static WeakMap $cache;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
self::$cache = new WeakMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte datos a JSON
|
||||
*
|
||||
* @template T
|
||||
* @param T $data
|
||||
* @return string
|
||||
* @throws JsonException
|
||||
*/
|
||||
public static function toJson(mixed $data): string
|
||||
{
|
||||
try {
|
||||
return json_encode(
|
||||
self::normalize($data),
|
||||
self::JSON_OPTIONS | JSON_THROW_ON_ERROR
|
||||
);
|
||||
} catch (JsonException $e) {
|
||||
throw new JsonException(
|
||||
"Error en la serialización JSON: {$e->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();
|
||||
}
|
||||
}
|
304
app/Helpers/StringHelper.php
Normal file
304
app/Helpers/StringHelper.php
Normal file
@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class StringHelper
|
||||
{
|
||||
private $string;
|
||||
|
||||
public function __construct(?string $string = "")
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
65
app/Middleware/AuthorizationMiddleware.php
Normal file
65
app/Middleware/AuthorizationMiddleware.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use App\Controllers\ViewController;
|
||||
|
||||
class AuthorizationMiddleware
|
||||
{
|
||||
private $jwtSecret;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
196
app/Repositories/BaseRepository.php
Normal file
196
app/Repositories/BaseRepository.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Entities\BaseEntity;
|
||||
use Exception;
|
||||
use PDOStatement;
|
||||
|
||||
class BaseRepository
|
||||
{
|
||||
protected $db;
|
||||
protected string $table;
|
||||
protected string $primaryKey;
|
||||
protected string $entityClass;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Database $database Conexión a la base de datos.
|
||||
* @param string $table Nombre de la tabla en la base de datos.
|
||||
* @param string $entityClass Clase de la entidad (debe extender BaseEntity).
|
||||
*
|
||||
* @throws Exception Si la clase de entidad proporcionada no es válida.
|
||||
*/
|
||||
public function __construct(Database $database, string $table, string $entityClass)
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
||||
}
|
14
app/Repositories/EmailStatusRepository.php
Normal file
14
app/Repositories/EmailStatusRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Entities\EmailStatus;
|
||||
use App\Core\Database;
|
||||
|
||||
class EmailStatusRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "email_status", EmailStatus::class);
|
||||
}
|
||||
}
|
14
app/Repositories/EmailTemplateRepository.php
Normal file
14
app/Repositories/EmailTemplateRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Entities\EmailTemplate;
|
||||
use App\Core\Database;
|
||||
class EmailTemplateRepository extends BaseRepository
|
||||
{
|
||||
// Add your repository methods here
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "email_templates", EmailTemplate::class);
|
||||
}
|
||||
}
|
14
app/Repositories/ExerciseRepository.php
Normal file
14
app/Repositories/ExerciseRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Entities\Exercise;
|
||||
|
||||
class ExerciseRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, 'exercises', Exercise::class);
|
||||
}
|
||||
}
|
13
app/Repositories/ExpenseCategoryRepository.php
Normal file
13
app/Repositories/ExpenseCategoryRepository.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
use App\Entities\ExpenseCategory;
|
||||
use App\Core\Database;
|
||||
|
||||
class ExpenseCategoryRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "expense_categories", ExpenseCategory::class);
|
||||
}
|
||||
}
|
14
app/Repositories/ExpenseRepository.php
Normal file
14
app/Repositories/ExpenseRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Entities\Expense;
|
||||
use App\Core\Database;
|
||||
|
||||
class ExpenseRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "expenses", Expense::class);
|
||||
}
|
||||
}
|
14
app/Repositories/FrequencyRepository.php
Normal file
14
app/Repositories/FrequencyRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Entities\Frequency;
|
||||
use App\Core\Database;
|
||||
|
||||
class FrequencyRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "frequencies", Frequency::class);
|
||||
}
|
||||
}
|
14
app/Repositories/HealthyRecipeRepository.php
Normal file
14
app/Repositories/HealthyRecipeRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Entities\HealthyRecipe;
|
||||
use App\Core\Database;
|
||||
|
||||
class HealthyRecipeRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "healthy_recipes", HealthyRecipe::class);
|
||||
}
|
||||
}
|
14
app/Repositories/ImportantEventRepository.php
Normal file
14
app/Repositories/ImportantEventRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Entities\ImportantEvent;
|
||||
use App\Core\Database;
|
||||
|
||||
class ImportantEventRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "important_events", ImportantEvent::class);
|
||||
}
|
||||
}
|
14
app/Repositories/IncomeRepository.php
Normal file
14
app/Repositories/IncomeRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Entities\Income;
|
||||
use App\Core\Database;
|
||||
|
||||
class IncomeRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "incomes", Income::class);
|
||||
}
|
||||
}
|
15
app/Repositories/NotificationRepository.php
Normal file
15
app/Repositories/NotificationRepository.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Entities\Notification;
|
||||
|
||||
class NotificationRepository extends BaseRepository
|
||||
{
|
||||
// Add your repository methods here
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "notifications", Notification::class);
|
||||
}
|
||||
}
|
14
app/Repositories/NotificationTypeRepository.php
Normal file
14
app/Repositories/NotificationTypeRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Entities\NotificationType;
|
||||
|
||||
class NotificationTypeRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "notification_types", NotificationType::class);
|
||||
}
|
||||
}
|
14
app/Repositories/SentEmailRepository.php
Normal file
14
app/Repositories/SentEmailRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Entities\SentEmail;
|
||||
use App\Core\Database;
|
||||
|
||||
class SentEmailRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "sent_emails", SentEmail::class);
|
||||
}
|
||||
}
|
15
app/Repositories/SettingRepository.php
Normal file
15
app/Repositories/SettingRepository.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Entities\Setting;
|
||||
|
||||
class SettingRepository extends BaseRepository
|
||||
{
|
||||
// Add your repository methods here
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "settings", Setting::class);
|
||||
}
|
||||
}
|
14
app/Repositories/TaskRepository.php
Normal file
14
app/Repositories/TaskRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Entities\Task;
|
||||
|
||||
class TaskRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "tasks", Task::class);
|
||||
}
|
||||
}
|
14
app/Repositories/TaskStatusRepository.php
Normal file
14
app/Repositories/TaskStatusRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Entities\TaskStatus;
|
||||
|
||||
class TaskStatusRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "task_status", TaskStatus::class);
|
||||
}
|
||||
}
|
14
app/Repositories/UserRepository.php
Normal file
14
app/Repositories/UserRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Entities\User;
|
||||
use App\Core\Database;
|
||||
|
||||
class UserRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "users", User::class);
|
||||
}
|
||||
}
|
14
app/Repositories/WorkoutRoutineRepository.php
Normal file
14
app/Repositories/WorkoutRoutineRepository.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Entities\WorkoutRoutine;
|
||||
|
||||
class WorkoutRoutineRepository extends BaseRepository
|
||||
{
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
parent::__construct($database, "workout_routines", WorkoutRoutine::class);
|
||||
}
|
||||
}
|
0
app/Views/auth/.gitkeep
Normal file
0
app/Views/auth/.gitkeep
Normal file
157
app/Views/auth/login.php
Normal file
157
app/Views/auth/login.php
Normal file
@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Iniciar Sesión</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
/* Estilos comunes */
|
||||
body, html {
|
||||
height: 100%;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.text-muted a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.text-muted a:hover {
|
||||
text-decoration: underline;
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fw-bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Ajustes para pantallas pequeñas */
|
||||
@media (max-width: 576px) {
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.text-muted a {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ajustes para pantallas medianas */
|
||||
@media (min-width: 768px) and (max-width: 992px) {
|
||||
.card {
|
||||
padding: 2.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.text-muted a {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ajustes para pantallas grandes */
|
||||
@media (min-width: 992px) {
|
||||
.card {
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.text-muted a {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<div class="card p-4">
|
||||
<h1 class="text-center fw-bold mb-2">GTD Assistant</h1>
|
||||
<p class="text-center mb-4">Dale un respiro a tu mente organizándote con este sistema.</p>
|
||||
<?php if (isset($error)): ?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
text: '<?php echo htmlspecialchars(
|
||||
$error,
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
); ?>',
|
||||
confirmButtonColor: '#d33',
|
||||
confirmButtonText: 'Aceptar'
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<form action="/login" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Correo Electrónico</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Contraseña</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Iniciar Sesión</button>
|
||||
</form>
|
||||
<p class="mt-3 text-center text-muted">¿No tienes una cuenta? <a href="/register">Regístrate</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
221
app/Views/auth/register.php
Normal file
221
app/Views/auth/register.php
Normal file
@ -0,0 +1,221 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Registrarse</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900&display=swap"
|
||||
rel="stylesheet">
|
||||
<style>
|
||||
/* Estilos comunes */
|
||||
body, html {
|
||||
height: 100%;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.text-muted a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.text-muted a:hover {
|
||||
text-decoration: underline;
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fw-bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Ajustes para pantallas pequeñas */
|
||||
@media (max-width: 576px) {
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.text-muted a {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ajustes para pantallas medianas */
|
||||
@media (min-width: 768px) and (max-width: 992px) {
|
||||
.card {
|
||||
padding: 2.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.text-muted a {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ajustes para pantallas grandes */
|
||||
@media (min-width: 992px) {
|
||||
.card {
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.text-muted a {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<div class="card p-4">
|
||||
<h3 class="text-center fw-bold mb-4">Nuevo Usuario</h3>
|
||||
<form action="/register" method="POST" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Nombre de Usuario</label>
|
||||
<input type="text" class="form-control <?php echo isset(
|
||||
$errors["username"]
|
||||
)
|
||||
? "is-invalid"
|
||||
: ""; ?>" id="username" name="username" value="<?php echo htmlspecialchars(
|
||||
$old["username"] ?? "",
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
); ?>" required>
|
||||
<?php if (isset($errors["username"])): ?>
|
||||
<div class="invalid-feedback">
|
||||
<?php echo htmlspecialchars(
|
||||
$errors["username"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Correo Electrónico</label>
|
||||
<input type="email" class="form-control <?php echo isset(
|
||||
$errors["email"]
|
||||
)
|
||||
? "is-invalid"
|
||||
: ""; ?>" id="email" name="email" value="<?php echo htmlspecialchars(
|
||||
$old["email"] ?? "",
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
); ?>" required>
|
||||
<?php if (isset($errors["email"])): ?>
|
||||
<div class="invalid-feedback">
|
||||
<?php echo htmlspecialchars(
|
||||
$errors["email"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Contraseña</label>
|
||||
<input type="password" class="form-control <?php echo isset(
|
||||
$errors["password"]
|
||||
)
|
||||
? "is-invalid"
|
||||
: ""; ?>" id="password" name="password" required>
|
||||
<?php if (isset($errors["password"])): ?>
|
||||
<div class="invalid-feedback">
|
||||
<?php echo htmlspecialchars(
|
||||
$errors["password"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Registrarse</button>
|
||||
</form>
|
||||
<p class="mt-3 text-center text-muted">¿Ya tienes una cuenta? <a href="/login">Iniciar Sesión</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<?php if (isset($success)): ?>
|
||||
<script>
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: '¡Registro Exitoso!',
|
||||
text: '<?php echo htmlspecialchars(
|
||||
$success,
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
); ?>',
|
||||
confirmButtonColor: '#3085d6',
|
||||
confirmButtonText: 'Aceptar'
|
||||
}).then(() => {
|
||||
window.location.href = "/login";
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($error)): ?>
|
||||
<script>
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
text: '<?php echo htmlspecialchars(
|
||||
$error,
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
); ?>',
|
||||
confirmButtonColor: '#d33',
|
||||
confirmButtonText: 'Aceptar'
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
</html>
|
31
app/Views/components/breadcrumb.php
Normal file
31
app/Views/components/breadcrumb.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
$modulos = include __DIR__ . '/../../Config/Modulos.php';
|
||||
$uriActual = $_SERVER['REQUEST_URI'];
|
||||
|
||||
$moduloActual = null;
|
||||
|
||||
foreach ($modulos as $moduloPadre) {
|
||||
if (!empty($moduloPadre['hijos'])) {
|
||||
foreach ($moduloPadre['hijos'] as $moduloHijo) {
|
||||
if ($moduloHijo['ruta'] === $uriActual) {
|
||||
// Asignar el módulo actual
|
||||
$moduloActual = [
|
||||
'nombre' => $moduloHijo['nombre'],
|
||||
'icono' => $moduloHijo['icono']
|
||||
];
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if ($moduloActual): ?>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
<i class="<?= $moduloActual['icono'] ?>"></i> <?= $moduloActual['nombre'] ?>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<?php endif; ?>
|
155
app/Views/components/navbar.php
Normal file
155
app/Views/components/navbar.php
Normal file
@ -0,0 +1,155 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<!-- Logo -->
|
||||
<a class="navbar-brand" href="/home">
|
||||
<?php echo $_ENV["APP_NAME"]; ?>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<?php
|
||||
$modulos = include __DIR__ . "/../../Config/Modulos.php";
|
||||
|
||||
foreach ($modulos as $moduloPadre) {
|
||||
// Módulo "Inicio" sin hijos
|
||||
if ($moduloPadre["nombre"] === "Inicio") {
|
||||
echo '<li class="nav-item">';
|
||||
echo '<a class="nav-link" href="' .
|
||||
htmlspecialchars(
|
||||
$moduloPadre["ruta"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
) .
|
||||
'">';
|
||||
echo '<i class="' .
|
||||
htmlspecialchars(
|
||||
$moduloPadre["icono"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
) .
|
||||
' me-2"></i>' .
|
||||
htmlspecialchars(
|
||||
$moduloPadre["nombre"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
);
|
||||
echo "</a>";
|
||||
echo "</li>";
|
||||
} elseif (
|
||||
isset($moduloPadre["hijos"]) &&
|
||||
!empty($moduloPadre["hijos"])
|
||||
) {
|
||||
// Módulos con submódulos
|
||||
echo '<li class="nav-item dropdown">';
|
||||
echo '<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">';
|
||||
echo '<i class="' .
|
||||
htmlspecialchars(
|
||||
$moduloPadre["icono"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
) .
|
||||
' me-2"></i>' .
|
||||
htmlspecialchars(
|
||||
$moduloPadre["nombre"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
);
|
||||
echo "</a>";
|
||||
echo '<ul class="dropdown-menu" aria-labelledby="navbarDropdown">';
|
||||
|
||||
foreach ($moduloPadre["hijos"] as $moduloHijo) {
|
||||
echo '<li><a class="dropdown-item" href="' .
|
||||
htmlspecialchars(
|
||||
$moduloHijo["ruta"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
) .
|
||||
'">';
|
||||
echo '<i class="' .
|
||||
htmlspecialchars(
|
||||
$moduloHijo["icono"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
) .
|
||||
' me-2"></i>';
|
||||
echo htmlspecialchars(
|
||||
$moduloHijo["nombre"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
);
|
||||
echo "</a></li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
echo "</li>";
|
||||
} else {
|
||||
// Módulos sin submódulos
|
||||
echo '<li class="nav-item">';
|
||||
echo '<a class="nav-link" href="' .
|
||||
htmlspecialchars(
|
||||
$moduloPadre["ruta"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
) .
|
||||
'">';
|
||||
echo '<i class="' .
|
||||
htmlspecialchars(
|
||||
$moduloPadre["icono"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
) .
|
||||
' me-2"></i>' .
|
||||
htmlspecialchars(
|
||||
$moduloPadre["nombre"],
|
||||
ENT_QUOTES,
|
||||
"UTF-8"
|
||||
);
|
||||
echo "</a>";
|
||||
echo "</li>";
|
||||
}
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<form action="/logout" method="GET">
|
||||
<button class="btn btn-danger" type="submit">Cerrar sesión</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
/* Estilos para la navbar */
|
||||
.navbar-nav .nav-link {
|
||||
transition: background-color 0.3s ease, color 0.3s ease; /* Transición suave */
|
||||
}
|
||||
|
||||
/* Efecto hover para los enlaces de la navbar */
|
||||
.navbar-nav .nav-link:hover {
|
||||
background-color: #007bff; /* Cambia a un azul más oscuro */
|
||||
color: white; /* Cambia el color del texto a blanco */
|
||||
border-radius: 0.25rem; /* Añadir bordes redondeados */
|
||||
}
|
||||
|
||||
/* Efecto hover para los dropdowns */
|
||||
.dropdown-menu {
|
||||
transition: opacity 0.3s ease; /* Transición suave para el dropdown */
|
||||
}
|
||||
|
||||
.dropdown-menu .dropdown-item:hover {
|
||||
background-color: #f8f9fa; /* Cambia el fondo en hover */
|
||||
color: #007bff; /* Cambia el color del texto */
|
||||
}
|
||||
|
||||
/* Sombra sutil para la navbar */
|
||||
.navbar {
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* Sombra sutil */
|
||||
}
|
||||
</style>
|
0
app/Views/error/.gitkeep
Normal file
0
app/Views/error/.gitkeep
Normal file
56
app/Views/error/400.php
Normal file
56
app/Views/error/400.php
Normal file
@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error 400 - Solicitud Incorrecta</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body, html {
|
||||
height: 100%;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<div class="card p-4">
|
||||
<h4 class="text-center fw-bold mb-4">Solicitud Incorrecta</h4>
|
||||
<div class="image-placeholder">
|
||||
[Placeholder para imagen de error 400]
|
||||
</div>
|
||||
<h2 class="text-center mb-3">Error 400</h2>
|
||||
<p class="text-center mb-4">La solicitud enviada no es válida o está mal estructurada.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
56
app/Views/error/401.php
Normal file
56
app/Views/error/401.php
Normal file
@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error 401 - No Autorizado</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body, html {
|
||||
height: 100%;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<div class="card p-4">
|
||||
<h4 class="text-center fw-bold mb-4">No Autorizado</h4>
|
||||
<div class="image-placeholder">
|
||||
[Placeholder para imagen de error 401]
|
||||
</div>
|
||||
<h2 class="text-center mb-3">Error 401</h2>
|
||||
<p class="text-center mb-4">No tienes autorización para acceder a esta página.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
60
app/Views/error/403.php
Normal file
60
app/Views/error/403.php
Normal file
@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error 403 - Prohibido</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<div class="card p-4">
|
||||
<h4 class="text-center fw-bold mb-4">Acceso Prohibido</h4>
|
||||
<div class="image-placeholder">
|
||||
[Placeholder para imagen de error 403]
|
||||
</div>
|
||||
<h2 class="text-center mb-3">Error 403</h2>
|
||||
<p class="text-center mb-4">No tienes permisos para acceder a esta página.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
66
app/Views/error/404.php
Normal file
66
app/Views/error/404.php
Normal file
@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error 404 - Página no encontrada</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<div class="card p-4">
|
||||
<h4 class="text-center fw-bold mb-4">Oops! Página no encontrada</h4>
|
||||
<div class="image-placeholder">
|
||||
[Placeholder para imagen de error 404]
|
||||
</div>
|
||||
<h2 class="text-center mb-3">Error 404</h2>
|
||||
<p class="text-center mb-4">Lo sentimos, la página que estás buscando no existe o ha sido movida.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
56
app/Views/error/408.php
Normal file
56
app/Views/error/408.php
Normal file
@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error 408 - Tiempo de Espera Agotado</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body, html {
|
||||
height: 100%;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<div class="card p-4">
|
||||
<h4 class="text-center fw-bold mb-4">Tiempo de Espera Agotado</h4>
|
||||
<div class="image-placeholder">
|
||||
[Placeholder para imagen de error 408]
|
||||
</div>
|
||||
<h2 class="text-center mb-3">Error 408</h2>
|
||||
<p class="text-center mb-4">El servidor no pudo procesar tu solicitud en el tiempo esperado.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
60
app/Views/error/500.php
Normal file
60
app/Views/error/500.php
Normal file
@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error 500 - Error Interno del Servidor</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<div class="card p-4">
|
||||
<h4 class="text-center fw-bold mb-4">Error Interno del Servidor</h4>
|
||||
<div class="image-placeholder">
|
||||
[Placeholder para imagen de error 500]
|
||||
</div>
|
||||
<h2 class="text-center mb-3">Error 500</h2>
|
||||
<p class="text-center mb-4">Ocurrió un problema inesperado en el servidor.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
56
app/Views/error/503.php
Normal file
56
app/Views/error/503.php
Normal file
@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error 503 - Servicio No Disponible</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body, html {
|
||||
height: 100%;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="col-md-6">
|
||||
<div class="card p-4">
|
||||
<h4 class="text-center fw-bold mb-4">Servicio No Disponible</h4>
|
||||
<div class="image-placeholder">
|
||||
[Placeholder para imagen de error 503]
|
||||
</div>
|
||||
<h2 class="text-center mb-3">Error 503</h2>
|
||||
<p class="text-center mb-4">El servidor no está disponible temporalmente.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
440
app/Views/home/home.php
Normal file
440
app/Views/home/home.php
Normal file
@ -0,0 +1,440 @@
|
||||
<?php include __DIR__ . "/../layouts/header.php"; ?>
|
||||
|
||||
<style>
|
||||
.task-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.task-form__input-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-form__input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-form__badge {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
|
||||
#calendar {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.fc-header-toolbar {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row g-3">
|
||||
<!-- Título del Dashboard -->
|
||||
<div class="col-12 text-center my-4">
|
||||
<h1 class="display-4 text-dark"><?php echo $_ENV["APP_NAME"]; ?></h1>
|
||||
<p class="lead text-muted">Automatiza tus tareas diarias y mejora tu productividad con
|
||||
<b><?php echo $_ENV["APP_NAME"]; ?></b></p>
|
||||
</div>
|
||||
|
||||
<!-- Tarjetas de Resumen -->
|
||||
<div class="row g-3 mb-4">
|
||||
<!-- Balance General de Dinero -->
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="card border-start border-success border-4 shadow h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="row align-items-center g-0">
|
||||
<div class="col-8">
|
||||
<div class="small fw-bold text-success text-uppercase mb-1">
|
||||
Balance General</div>
|
||||
<div class="h5 mb-0 fw-bold text-gray-800" id="balanceGeneralAmount">Q 0</div>
|
||||
</div>
|
||||
<div class="col-4 text-end">
|
||||
<i class="fas fa-wallet fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Eventos Próximos -->
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="card border-start border-warning border-4 shadow h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="row align-items-center g-0">
|
||||
<div class="col-8">
|
||||
<div class="small fw-bold text-warning text-uppercase mb-1">
|
||||
Eventos Próximos</div>
|
||||
<div class="h5 mb-0 fw-bold text-gray-800" id="upcomingEventsCount">0</div>
|
||||
</div>
|
||||
<div class="col-4 text-end">
|
||||
<i class="fas fa-calendar-alt fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tareas En Proceso -->
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="card border-start border-info border-4 shadow h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="row align-items-center g-0">
|
||||
<div class="col-8">
|
||||
<div class="small fw-bold text-info text-uppercase mb-1">
|
||||
Tareas En Proceso</div>
|
||||
<div class="h5 mb-0 fw-bold text-gray-800" id="inProgressTasksCount">0</div>
|
||||
</div>
|
||||
<div class="col-4 text-end">
|
||||
<i class="fas fa-tasks fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tareas Completadas -->
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="card border-start border-primary border-4 shadow h-100">
|
||||
<div class="card-body py-3">
|
||||
<div class="row align-items-center g-0">
|
||||
<div class="col-8">
|
||||
<div class="small fw-bold text-primary text-uppercase mb-1">
|
||||
Tareas Completadas</div>
|
||||
<div class="h5 mb-0 fw-bold text-gray-800" id="completedTasksCount">0</div>
|
||||
</div>
|
||||
<div class="col-4 text-end">
|
||||
<i class="fas fa-check-circle fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendario y Gráficos -->
|
||||
<div class="row mb-5 g-3">
|
||||
<!-- Calendario -->
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="my-0">Calendario de Eventos</h6>
|
||||
<button class="btn btn-sm btn-light" id="newEventBtn">
|
||||
<i class="fas fa-plus"></i> Nuevo Evento
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panel Lateral -->
|
||||
<div class="col-12 col-lg-4">
|
||||
<!-- Gráfico de Actividades Fitness -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h6 class="my-0">Actividad Física Últimos 7 Días</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="fitnessActivityChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de Próximos Eventos -->
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<h6 class="my-0">Próximos Eventos</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="upcomingEventsList" class="list-group list-group-flush">
|
||||
<!-- Los eventos se cargarán dinámicamente aquí -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css' rel='stylesheet' />
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js'></script>
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/locales-all.min.js'></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Datos simulados
|
||||
const data = {
|
||||
balanceGeneral: 3500,
|
||||
upcomingEvents: 5,
|
||||
inProgressTasks: 8,
|
||||
completedTasks: 15,
|
||||
fitnessActivities: [10000, 6000, 6500, 5000, 12000, 5500, 9500],
|
||||
events: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Reunión de equipo',
|
||||
start: '2024-10-28T10:00:00',
|
||||
end: '2024-10-28T11:30:00',
|
||||
backgroundColor: '#0275d8',
|
||||
description: 'Reunión semanal de seguimiento'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Entrenamiento',
|
||||
start: '2024-10-29T15:00:00',
|
||||
end: '2024-10-29T16:00:00',
|
||||
backgroundColor: '#5cb85c',
|
||||
description: 'Sesión de ejercicio'
|
||||
}
|
||||
// ... agrega más eventos si lo deseas
|
||||
]
|
||||
};
|
||||
|
||||
// Actualizar tarjetas de resumen
|
||||
document.getElementById('balanceGeneralAmount').textContent = `Q ${data.balanceGeneral}`;
|
||||
document.getElementById('upcomingEventsCount').textContent = data.upcomingEvents;
|
||||
document.getElementById('inProgressTasksCount').textContent = data.inProgressTasks;
|
||||
document.getElementById('completedTasksCount').textContent = data.completedTasks;
|
||||
|
||||
// Inicializar FullCalendar
|
||||
const calendarEl = document.getElementById('calendar');
|
||||
const calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||
},
|
||||
locale: 'es',
|
||||
events: data.events,
|
||||
editable: true,
|
||||
selectable: true,
|
||||
selectMirror: true,
|
||||
dayMaxEvents: true,
|
||||
eventClick: function (info) {
|
||||
showEventDetails(info.event);
|
||||
},
|
||||
select: function (info) {
|
||||
showEventForm(null, info);
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
|
||||
// Gráfico de Actividades Fitness
|
||||
const fitnessActivityCtx = document.getElementById('fitnessActivityChart').getContext('2d');
|
||||
new Chart(fitnessActivityCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Hace 6 días', 'Hace 5 días', 'Hace 4 días', 'Hace 3 días', 'Hace 2 días', 'Ayer', 'Hoy'],
|
||||
datasets: [{
|
||||
label: 'Pasos diarios',
|
||||
data: data.fitnessActivities,
|
||||
backgroundColor: 'rgba(0, 123, 255, 0.7)',
|
||||
borderColor: 'rgba(0, 123, 255, 1)',
|
||||
borderWidth: 1,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Actualizar lista de próximos eventos
|
||||
updateUpcomingEventsList();
|
||||
|
||||
// Manejadores de eventos
|
||||
document.getElementById('newEventBtn').addEventListener('click', () => showEventForm());
|
||||
|
||||
// Funciones auxiliares
|
||||
function showEventForm(event = null, selectInfo = null) {
|
||||
let eventData = {
|
||||
id: '',
|
||||
title: '',
|
||||
start: '',
|
||||
end: '',
|
||||
color: '#0275d8',
|
||||
description: ''
|
||||
};
|
||||
|
||||
if (event) {
|
||||
// Editar evento existente
|
||||
eventData = {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
start: event.startStr,
|
||||
end: event.endStr,
|
||||
color: event.backgroundColor,
|
||||
description: event.extendedProps.description
|
||||
};
|
||||
} else if (selectInfo) {
|
||||
// Nuevo evento con fechas seleccionadas
|
||||
eventData.start = selectInfo.startStr;
|
||||
eventData.end = selectInfo.endStr;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: event ? 'Editar Evento' : 'Nuevo Evento',
|
||||
html: `
|
||||
<div class="task-form">
|
||||
<input type="text" id="swalEvtTitle" class="swal2-input" placeholder="Título" value="${escapeHtml(eventData.title)}">
|
||||
<input type="datetime-local" id="swalEvtStart" class="swal2-input" value="${formatDateTimeInput(eventData.start)}">
|
||||
<input type="datetime-local" id="swalEvtEnd" class="swal2-input" value="${formatDateTimeInput(eventData.end)}">
|
||||
<div class="task-form__input-group">
|
||||
<label for="swalEvtColor">Color:</label>
|
||||
<select id="swalEvtColor" class="swal2-input task-form__input">
|
||||
<option value="#0275d8" ${eventData.color === '#0275d8' ? 'selected' : ''}>Azul</option>
|
||||
<option value="#5cb85c" ${eventData.color === '#5cb85c' ? 'selected' : ''}>Verde</option>
|
||||
<option value="#f0ad4e" ${eventData.color === '#f0ad4e' ? 'selected' : ''}>Amarillo</option>
|
||||
<option value="#d9534f" ${eventData.color === '#d9534f' ? 'selected' : ''}>Rojo</option>
|
||||
</select>
|
||||
</div>
|
||||
<textarea id="swalEvtDesc" class="swal2-textarea" placeholder="Descripción">${escapeHtml(eventData.description)}</textarea>
|
||||
</div>
|
||||
`,
|
||||
focusConfirm: false,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Guardar',
|
||||
cancelButtonText: 'Cancelar',
|
||||
preConfirm: () => {
|
||||
const title = document.getElementById('swalEvtTitle').value;
|
||||
const start = document.getElementById('swalEvtStart').value;
|
||||
const end = document.getElementById('swalEvtEnd').value;
|
||||
const color = document.getElementById('swalEvtColor').value;
|
||||
const description = document.getElementById('swalEvtDesc').value;
|
||||
|
||||
if (!title || !start || !end) {
|
||||
Swal.showValidationMessage('Por favor completa todos los campos obligatorios');
|
||||
return false;
|
||||
}
|
||||
|
||||
return { title, start, end, color, description };
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
const formData = result.value;
|
||||
if (event) {
|
||||
// Actualizar evento existente
|
||||
event.setProp('title', formData.title);
|
||||
event.setStart(formData.start);
|
||||
event.setEnd(formData.end);
|
||||
event.setProp('backgroundColor', formData.color);
|
||||
event.setExtendedProp('description', formData.description);
|
||||
} else {
|
||||
// Crear nuevo evento
|
||||
calendar.addEvent({
|
||||
id: String(Date.now()), // Generar un ID único
|
||||
title: formData.title,
|
||||
start: formData.start,
|
||||
end: formData.end,
|
||||
backgroundColor: formData.color,
|
||||
description: formData.description
|
||||
});
|
||||
}
|
||||
updateUpcomingEventsList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showEventDetails(event) {
|
||||
Swal.fire({
|
||||
title: event.title,
|
||||
html: `
|
||||
<div class="task-form">
|
||||
<p><strong>Inicio:</strong> ${formatDateTimeDisplay(event.start)}</p>
|
||||
<p><strong>Fin:</strong> ${formatDateTimeDisplay(event.end)}</p>
|
||||
<p><strong>Descripción:</strong> ${escapeHtml(event.extendedProps.description || 'Sin descripción')}</p>
|
||||
</div>
|
||||
`,
|
||||
showCancelButton: true,
|
||||
showDenyButton: true,
|
||||
confirmButtonText: 'Editar',
|
||||
denyButtonText: 'Eliminar',
|
||||
cancelButtonText: 'Cerrar',
|
||||
confirmButtonColor: '#0d6efd',
|
||||
denyButtonColor: '#dc3545',
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
// Editar evento
|
||||
showEventForm(event);
|
||||
} else if (result.isDenied) {
|
||||
// Eliminar evento
|
||||
Swal.fire({
|
||||
title: '¿Estás seguro?',
|
||||
text: 'Esta acción no se puede deshacer',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Sí, eliminar',
|
||||
cancelButtonText: 'Cancelar',
|
||||
confirmButtonColor: '#dc3545',
|
||||
}).then((res) => {
|
||||
if (res.isConfirmed) {
|
||||
event.remove();
|
||||
updateUpcomingEventsList();
|
||||
Swal.fire('Eliminado', 'El evento ha sido eliminado', 'success');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateUpcomingEventsList() {
|
||||
const events = calendar.getEvents();
|
||||
const upcomingEvents = events
|
||||
.filter(event => event.start >= new Date())
|
||||
.sort((a, b) => a.start - b.start)
|
||||
.slice(0, 5);
|
||||
|
||||
const listContainer = document.getElementById('upcomingEventsList');
|
||||
listContainer.innerHTML = '';
|
||||
|
||||
upcomingEvents.forEach(event => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'list-group-item';
|
||||
item.innerHTML = `
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1">${event.title}</h6>
|
||||
<small>${formatDateTimeDisplay(event.start)}</small>
|
||||
</div>
|
||||
<p class="mb-1">${event.extendedProps.description || ''}</p>
|
||||
`;
|
||||
listContainer.appendChild(item);
|
||||
});
|
||||
|
||||
// Actualizar el contador de eventos próximos
|
||||
document.getElementById('upcomingEventsCount').textContent = upcomingEvents.length;
|
||||
}
|
||||
|
||||
function formatDateTimeInput(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
const isoString = date.toISOString();
|
||||
return isoString.slice(0, 16);
|
||||
}
|
||||
|
||||
function formatDateTimeDisplay(date) {
|
||||
if (!date) return '';
|
||||
const options = { dateStyle: 'medium', timeStyle: 'short' };
|
||||
return new Intl.DateTimeFormat('es-ES', options).format(new Date(date));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include __DIR__ . "/../layouts/footer.php"; ?>
|
8
app/Views/layouts/footer.php
Normal file
8
app/Views/layouts/footer.php
Normal file
@ -0,0 +1,8 @@
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
201
app/Views/layouts/header.php
Normal file
201
app/Views/layouts/header.php
Normal file
@ -0,0 +1,201 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo $_ENV['APP_NAME']; ?></title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<!-- FontAwesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/main.css">
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<script>
|
||||
// Precharged JS code for the app
|
||||
|
||||
/**
|
||||
* @function showMessage
|
||||
* @description Muestra un mensaje usando SweetAlert2
|
||||
* @param {string} message - Mensaje a mostrar
|
||||
* @param {string} [type='success'] - Tipo de mensaje ('success', 'error', etc.)
|
||||
*/
|
||||
const showMessage = (message, type = 'success') => {
|
||||
Swal.fire({
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
icon: type,
|
||||
title: message,
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
timerProgressBar: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @function formatDate
|
||||
* @description Formatea una fecha en formato corto
|
||||
* @param {string} dateStr - Fecha en formato ISO
|
||||
* @returns {string} Fecha formateada
|
||||
*/
|
||||
const formatDate = (dateStr) => {
|
||||
return new Date(dateStr).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
};
|
||||
|
||||
/**
|
||||
* @function escapeHtml
|
||||
* @description Escapa caracteres HTML para prevenir XSS
|
||||
* @param {string} text - Texto a escapar
|
||||
* @returns {string} Texto escapado
|
||||
*/
|
||||
const escapeHtml = (text) => {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, (m) => map[m]);
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} StateManager
|
||||
* @description Gestor de estado que maneja el almacenamiento y persistencia de datos de la aplicación
|
||||
*/
|
||||
const StateManager = (() => {
|
||||
const state = {};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @function initialize
|
||||
* @description Inicializa el estado desde localStorage si existen datos persistidos
|
||||
* @returns {void}
|
||||
*/
|
||||
const initialize = () => {
|
||||
const persistedState = JSON.parse(localStorage.getItem('appState'));
|
||||
if (persistedState) {
|
||||
Object.assign(state, persistedState);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @function persistState
|
||||
* @description Persiste el estado actual en localStorage
|
||||
* @returns {void}
|
||||
*/
|
||||
const persistState = () => {
|
||||
localStorage.setItem('appState', JSON.stringify(state));
|
||||
};
|
||||
|
||||
/**
|
||||
* @function get
|
||||
* @description Obtiene un valor del estado
|
||||
* @param {string} key - Clave del valor a obtener
|
||||
* @param {*} [defaultValue=null] - Valor por defecto si la clave no existe
|
||||
* @returns {*} El valor almacenado o el valor por defecto
|
||||
*/
|
||||
const get = (key, defaultValue = null) => {
|
||||
return state[key] !== undefined ? state[key] : defaultValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* @function set
|
||||
* @description Establece un valor en el estado y lo persiste
|
||||
* @param {string} key - Clave bajo la cual almacenar el valor
|
||||
* @param {*} value - Valor a almacenar
|
||||
* @returns {void}
|
||||
*/
|
||||
const set = (key, value) => {
|
||||
state[key] = value;
|
||||
persistState();
|
||||
};
|
||||
|
||||
/**
|
||||
* @function update
|
||||
* @description Actualiza propiedades específicas de un objeto en el estado
|
||||
* @param {string} key - Clave del objeto a actualizar
|
||||
* @param {Object} updates - Objeto con las actualizaciones a realizar
|
||||
* @returns {void}
|
||||
* @throws {Warning} Si el valor no es un objeto o no existe
|
||||
*/
|
||||
const update = (key, updates) => {
|
||||
if (typeof state[key] === 'object' && !Array.isArray(state[key])) {
|
||||
Object.assign(state[key], updates);
|
||||
persistState();
|
||||
} else {
|
||||
console.warn(`El valor de ${key} no es un objeto o no existe.`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @function remove
|
||||
* @description Elimina una clave y su valor del estado
|
||||
* @param {string} key - Clave a eliminar
|
||||
* @returns {void}
|
||||
*/
|
||||
const remove = (key) => {
|
||||
delete state[key];
|
||||
persistState();
|
||||
};
|
||||
|
||||
/**
|
||||
* @function clear
|
||||
* @description Limpia todo el estado y el almacenamiento persistente
|
||||
* @returns {void}
|
||||
*/
|
||||
const clear = () => {
|
||||
for (let key in state) {
|
||||
delete state[key];
|
||||
}
|
||||
localStorage.removeItem('appState');
|
||||
};
|
||||
|
||||
/**
|
||||
* @function has
|
||||
* @description Verifica si una clave existe en el estado
|
||||
* @param {string} key - Clave a verificar
|
||||
* @returns {boolean} True si la clave existe, false en caso contrario
|
||||
*/
|
||||
const has = (key) => {
|
||||
return state.hasOwnProperty(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* @function getState
|
||||
* @description Obtiene una copia del estado completo
|
||||
* @returns {Object} Copia del estado actual
|
||||
*/
|
||||
const getState = () => {
|
||||
return { ...state };
|
||||
};
|
||||
|
||||
// Inicializamos la store
|
||||
initialize();
|
||||
|
||||
return {
|
||||
get,
|
||||
set,
|
||||
update,
|
||||
remove,
|
||||
clear,
|
||||
has,
|
||||
getState,
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body class="bg-body-secondary">
|
||||
|
||||
<?php include __DIR__ . "/../components/navbar.php"; ?>
|
0
app/Views/modules/.gitkeep
Normal file
0
app/Views/modules/.gitkeep
Normal file
446
app/Views/modules/clientes/index.php
Normal file
446
app/Views/modules/clientes/index.php
Normal file
@ -0,0 +1,446 @@
|
||||
<?php include __DIR__ . "/../../layouts/header.php";
|
||||
|
||||
function truncate($text, $length)
|
||||
{
|
||||
return strlen($text) > $length ? substr($text, 0, $length) . "..." : $text;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-2">
|
||||
<?php include __DIR__ . "/../../components/breadcrumb.php"; ?>
|
||||
</div>
|
||||
<div class="col-md-2 mb-2">
|
||||
</div>
|
||||
<div class="col-md-4 mb-2 text-end">
|
||||
<!-- Botón para agregar nuevo cliente -->
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addClienteModal">
|
||||
<i class="fas fa-plus"></i> Agregar Cliente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m-1 bg-white rounded">
|
||||
<div class="col-md-12 p-4 table-responsive">
|
||||
<table class="table table-striped table-hover table-bordered" id="clientesTable">
|
||||
<thead>
|
||||
<tr class="table-dark">
|
||||
<th>Nombre</th>
|
||||
<th>Dirección</th>
|
||||
<th>Teléfono</th>
|
||||
<th>NIT</th>
|
||||
<th class="text-center">Activo</th>
|
||||
<th class="text-center">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="clientesBody">
|
||||
<?php foreach ($clientes as $cliente): ?>
|
||||
<tr data-id="<?php echo $cliente["id"]; ?>">
|
||||
<td><?php echo truncate(
|
||||
$cliente["nombre"],
|
||||
20
|
||||
); ?></td>
|
||||
<td><?php echo truncate(
|
||||
$cliente["direccion"],
|
||||
40
|
||||
); ?>
|
||||
<td><?php echo $cliente["telefono"]; ?></td>
|
||||
<td><?php echo $cliente["nit"]; ?></td>
|
||||
<td class="text-center"><?php echo $cliente[
|
||||
"active"
|
||||
]
|
||||
? "✅"
|
||||
: "❌"; ?></td>
|
||||
<td class="text-center">
|
||||
<!-- Botón para editar -->
|
||||
<button class="btn btn-warning btn-sm edit-btn" data-id="<?php echo $cliente[
|
||||
"id"
|
||||
]; ?>"
|
||||
data-bs-toggle="modal" data-bs-target="#editClienteModal">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<!-- Botón para eliminar -->
|
||||
<button class="btn btn-danger btn-sm delete-btn" data-id="<?php echo $cliente[
|
||||
"id"
|
||||
]; ?>"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteClienteModal" <?php echo $cliente[
|
||||
"active"
|
||||
]
|
||||
? ""
|
||||
: "disabled"; ?>>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Paginación -->
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||
<li class="page-item <?php echo $i == $currentPage
|
||||
? "active"
|
||||
: ""; ?>">
|
||||
<a class="page-link" href="#" data-page="<?php echo $i; ?>"><?php echo $i; ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal para agregar cliente -->
|
||||
<div class="modal fade" id="addClienteModal" tabindex="-1" aria-labelledby="addClienteLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" id="addClienteLabel">
|
||||
<i class="fas fa-user-plus me-2"></i>Agregar Cliente
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="addClienteForm" class="needs-validation" novalidate>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="nombre" class="form-label">Nombre <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="nombre" required>
|
||||
<div class="invalid-feedback">Por favor, ingrese un nombre.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="email" class="form-label">Email <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control" id="email" required>
|
||||
<div class="invalid-feedback">Por favor, ingrese un email válido.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="telefono" class="form-label">Teléfono</label>
|
||||
<input type="tel" class="form-control" id="telefono" pattern="[0-9]{8,}">
|
||||
<div class="invalid-feedback">Por favor, ingrese un número de teléfono válido.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="direccion" class="form-label">Dirección</label>
|
||||
<input type="text" class="form-control" id="direccion">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="nit" class="form-label">NIT</label>
|
||||
<input type="text" class="form-control" id="nit">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="cui" class="form-label">CUI</label>
|
||||
<input type="text" class="form-control" id="cui">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="fecha_nacimiento" class="form-label">Fecha de Nacimiento</label>
|
||||
<input type="date" class="form-control" id="fecha_nacimiento">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="activoSwitch" class="form-label d-block">Estado</label>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="activoSwitch" checked>
|
||||
<label class="form-check-label" for="activoSwitch">Activo</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-end">
|
||||
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-2"></i>Guardar Cliente
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal para editar cliente -->
|
||||
<div class="modal fade" id="editClienteModal" tabindex="-1" aria-labelledby="editClienteLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-warning text-dark">
|
||||
<h5 class="modal-title" id="editClienteLabel">
|
||||
<i class="fas fa-user-edit me-2"></i>Editar Cliente
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editClienteForm" class="needs-validation" novalidate>
|
||||
<input type="hidden" id="edit-id">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="edit-nombre" class="form-label">Nombre <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="edit-nombre" required>
|
||||
<div class="invalid-feedback">Por favor, ingrese un nombre.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit-email" class="form-label">Email <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control" id="edit-email" required>
|
||||
<div class="invalid-feedback">Por favor, ingrese un email válido.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit-telefono" class="form-label">Teléfono</label>
|
||||
<input type="tel" class="form-control" id="edit-telefono" pattern="[0-9]{8,}">
|
||||
<div class="invalid-feedback">Por favor, ingrese un número de teléfono válido.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit-direccion" class="form-label">Dirección</label>
|
||||
<input type="text" class="form-control" id="edit-direccion">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit-nit" class="form-label">NIT</label>
|
||||
<input type="text" class="form-control" id="edit-nit">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit-cui" class="form-label">CUI</label>
|
||||
<input type="text" class="form-control" id="edit-cui">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit-fecha_nacimiento" class="form-label">Fecha de Nacimiento</label>
|
||||
<input type="date" class="form-control" id="edit-fecha_nacimiento">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="edit-activoSwitch" class="form-label d-block">Estado</label>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="edit-activoSwitch">
|
||||
<label class="form-check-label" for="edit-activoSwitch">Activo</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-end">
|
||||
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="fas fa-save me-2"></i>Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
addPaginationListeners();
|
||||
addModalListeners();
|
||||
resetFormOnModalClose();
|
||||
});
|
||||
|
||||
const resetFormOnModalClose = () => {
|
||||
const addClienteModal = document.getElementById('addClienteModal');
|
||||
addClienteModal.addEventListener('show.bs.modal', () => {
|
||||
resetForm('addClienteForm');
|
||||
});
|
||||
|
||||
const editClienteModal = document.getElementById('editClienteModal');
|
||||
editClienteModal.addEventListener('hide.bs.modal', () => {
|
||||
resetForm('editClienteForm');
|
||||
});
|
||||
}
|
||||
|
||||
const resetForm = (formId) => {
|
||||
const form = document.getElementById(formId);
|
||||
form.reset(); // Esto limpia todos los inputs del formulario
|
||||
if (formId === 'editClienteForm') {
|
||||
document.getElementById('edit-activoSwitch').checked = false; // Asegurar que el checkbox también se limpie
|
||||
}
|
||||
}
|
||||
|
||||
const addPaginationListeners = () => {
|
||||
document.querySelectorAll('.page-link').forEach((link) => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const page = link.getAttribute('data-page');
|
||||
|
||||
fetch(`/clientes?page=${page}`)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
document.getElementById('clientesBody').innerHTML = doc.querySelector('#clientesBody').innerHTML;
|
||||
document.getElementById('pagination').innerHTML = doc.querySelector('#pagination').innerHTML;
|
||||
|
||||
addPaginationListeners();
|
||||
addModalListeners(); // Re-inicializar modales para los nuevos elementos
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error al cambiar de página:', error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Crear cliente
|
||||
document.getElementById('addClienteForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const nombre = document.getElementById('nombre').value;
|
||||
const direccion = document.getElementById('direccion').value;
|
||||
const telefono = document.getElementById('telefono').value;
|
||||
const email = document.getElementById('email').value;
|
||||
const nit = document.getElementById('nit').value;
|
||||
const cui = document.getElementById('cui').value;
|
||||
const fecha_nacimiento = document.getElementById('fecha_nacimiento').value;
|
||||
const active = document.getElementById('activoSwitch').checked ? 1 : 0;
|
||||
|
||||
fetch('/cliente/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ nombre, direccion, telefono, email, nit, cui, fecha_nacimiento, active })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Cliente agregado',
|
||||
text: 'El cliente fue agregado exitosamente.',
|
||||
showConfirmButton: false,
|
||||
timer: 1500
|
||||
}).then(() => {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Error', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error al agregar cliente:', error);
|
||||
Swal.fire('Error', 'Ocurrió un error al intentar agregar el cliente.', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Función para volver a inicializar los modales
|
||||
const addModalListeners = () => {
|
||||
document.querySelectorAll('.edit-btn').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const id = button.getAttribute('data-id'); // Obtener el ID del cliente
|
||||
|
||||
fetch(`/cliente/get/${id}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Llenar el formulario de edición con los datos recibidos
|
||||
document.getElementById('edit-id').value = data.cliente.id;
|
||||
document.getElementById('edit-nombre').value = data.cliente.nombre;
|
||||
document.getElementById('edit-direccion').value = data.cliente.direccion;
|
||||
document.getElementById('edit-telefono').value = data.cliente.telefono;
|
||||
document.getElementById('edit-email').value = data.cliente.email;
|
||||
document.getElementById('edit-nit').value = data.cliente.nit;
|
||||
document.getElementById('edit-cui').value = data.cliente.cui;
|
||||
document.getElementById('edit-fecha_nacimiento').value = data.cliente.fecha_nacimiento;
|
||||
document.getElementById('edit-activoSwitch').checked = data.cliente.active == 1;
|
||||
} else {
|
||||
Swal.fire('Error', 'No se pudo obtener los datos del cliente', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error al obtener los datos del cliente:', error);
|
||||
Swal.fire('Error', 'Ocurrió un error al obtener los datos del cliente.', 'error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Editar cliente (enviar cambios al servidor)
|
||||
document.getElementById('editClienteForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('edit-id').value;
|
||||
const nombre = document.getElementById('edit-nombre').value;
|
||||
const direccion = document.getElementById('edit-direccion').value;
|
||||
const telefono = document.getElementById('edit-telefono').value;
|
||||
const email = document.getElementById('edit-email').value;
|
||||
const nit = document.getElementById('edit-nit').value;
|
||||
const cui = document.getElementById('edit-cui').value;
|
||||
const fecha_nacimiento = document.getElementById('edit-fecha_nacimiento').value;
|
||||
const active = document.getElementById('edit-activoSwitch').checked ? 1 : 0;
|
||||
|
||||
fetch('/cliente/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id, nombre, direccion, telefono, email, nit, cui, fecha_nacimiento, active })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Cliente actualizado',
|
||||
text: 'El cliente fue actualizado exitosamente.',
|
||||
showConfirmButton: false,
|
||||
timer: 1000
|
||||
}).then(() => {
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 600);
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Error', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error al actualizar cliente:', error);
|
||||
Swal.fire('Error', 'Ocurrió un error al intentar actualizar el cliente.', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Eliminar cliente
|
||||
document.querySelectorAll('.delete-btn').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const id = button.getAttribute('data-id'); // Obtener el ID del cliente a eliminar
|
||||
|
||||
Swal.fire({
|
||||
title: '¿Estás seguro?',
|
||||
text: 'No podrás revertir esta acción',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Sí, eliminar',
|
||||
cancelButtonText: 'Cancelar'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
fetch('/cliente/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id }) // Enviar el ID del cliente en la solicitud
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Cliente eliminado',
|
||||
text: 'El cliente fue eliminado exitosamente.',
|
||||
showConfirmButton: false,
|
||||
timer: 1000
|
||||
}).then(() => {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Error', data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error al eliminar cliente:', error);
|
||||
Swal.fire('Error', 'Ocurrió un error al intentar eliminar el cliente.', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php include __DIR__ . "/../../layouts/footer.php"; ?>
|
0
app/Views/modules/fitness/exercises.php
Normal file
0
app/Views/modules/fitness/exercises.php
Normal file
483
app/Views/modules/fitness/rutinas.php
Normal file
483
app/Views/modules/fitness/rutinas.php
Normal file
@ -0,0 +1,483 @@
|
||||
<?php include __DIR__ . "/../../layouts/header.php"; ?>
|
||||
|
||||
<style>
|
||||
/* --- Diseño adaptable (Responsive) y estilos de juego --- */
|
||||
body {
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.achievement {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.achievement:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 25px;
|
||||
font-size: 0.8rem;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4e73df;
|
||||
border-color: #4e73df;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #2e59d9;
|
||||
border-color: #2653d4;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
border-left: 5px solid #4e73df;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4em 0.6em;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #4e73df !important;
|
||||
}
|
||||
|
||||
/* Animaciones */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.075);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* Breakpoint para tablets y pantallas medianas (≥ 768px) */
|
||||
@media (max-width: 767.98px) {
|
||||
.achievement-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Breakpoint para teléfonos y pantallas pequeñas (≤ 576px) */
|
||||
@media (max-width: 576px) {
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Block: profile-card */
|
||||
.profile-card {
|
||||
background: linear-gradient(145deg, #ffffff, #f5f5f5);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
padding: 1.5rem;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* Elements */
|
||||
.profile-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.profile-card__icon {
|
||||
background: linear-gradient(45deg, #4e73df, #2e59d9);
|
||||
color: white;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.profile-card__title {
|
||||
color: #1a1a1a;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-card__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.profile-card__stat-item {
|
||||
background: rgba(78, 115, 223, 0.1);
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-card__stat-label {
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.profile-card__stat-value {
|
||||
color: #1a1a1a;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-card__progress {
|
||||
background: #e0e0e0;
|
||||
border-radius: 8px;
|
||||
height: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.profile-card__progress-bar {
|
||||
background: linear-gradient(90deg, #4e73df, #2e59d9); /* Colores ajustados */
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Modifiers */
|
||||
.profile-card__progress-bar--low {
|
||||
background: linear-gradient(90deg, #FFA726, #F57C00);
|
||||
}
|
||||
|
||||
.profile-card__progress-bar--high {
|
||||
background: linear-gradient(90deg, #66BB6A, #388E3C);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid px-3 px-md-4 mt-4">
|
||||
<div class="game-container">
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<h1 class="text-center fw-bold h2 text-dark">
|
||||
<i class="fas fa-dumbbell mr-2"></i> Rutinas Personalizadas
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="profile-card">
|
||||
<div class="profile-card__header">
|
||||
<div class="profile-card__icon">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
</div>
|
||||
<h2 class="profile-card__title">Perfil de Gymbro</h2>
|
||||
</div>
|
||||
|
||||
<div class="profile-card__stats">
|
||||
<div class="profile-card__stat-item">
|
||||
<div class="profile-card__stat-label">Nivel</div>
|
||||
<div class="profile-card__stat-value" id="userLevel">1</div>
|
||||
</div>
|
||||
<div class="profile-card__stat-item">
|
||||
<div class="profile-card__stat-label">Puntos XP</div>
|
||||
<div class="profile-card__stat-value" id="userPoints">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-card__progress">
|
||||
<div class="profile-card__progress-bar" id="expProgress" role="progressbar" aria-valuenow="0"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="fas fa-tasks mr-2"></i> Misión Diaria</h5>
|
||||
<select id="routineType" class="form-select mb-3">
|
||||
<option value="fullBody">Full Body</option>
|
||||
<option value="upperLower">Torso/Pierna</option>
|
||||
</select>
|
||||
<ul id="exerciseList" class="list-group mb-3">
|
||||
<!-- Los ejercicios se agregarán dinámicamente aquí -->
|
||||
</ul>
|
||||
<button id="completeRoutine" class="btn btn-primary btn-lg btn-block">
|
||||
<i class="fas fa-check-circle mr-2"></i> Completar Misión
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h3 class="text-center mb-3 text-primary"><i class="fas fa-trophy mr-2"></i> Logros Desbloqueados</h3>
|
||||
<div id="achievements" class="row">
|
||||
<!-- Los logros se agregarán dinámicamente aquí -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SweetAlert2 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const exercisesByType = {
|
||||
fullBody: [
|
||||
{ name: 'Sentadillas', points: 15, icon: 'fa-dumbbell' },
|
||||
{ name: 'Flexiones', points: 10, icon: 'fa-child' },
|
||||
{ name: 'Peso muerto', points: 20, icon: 'fa-dumbbell' },
|
||||
{ name: 'Dominadas', points: 15, icon: 'fa-child' },
|
||||
{ name: 'Burpees', points: 20, icon: 'fa-running' }
|
||||
],
|
||||
upperLower: {
|
||||
upper: [
|
||||
{ name: 'Press de banca', points: 15, icon: 'fa-dumbbell' },
|
||||
{ name: 'Remo con barra', points: 15, icon: 'fa-dumbbell' },
|
||||
{ name: 'Press militar', points: 12, icon: 'fa-dumbbell' },
|
||||
{ name: 'Curl de bíceps', points: 10, icon: 'fa-dumbbell' },
|
||||
{ name: 'Tríceps en polea', points: 10, icon: 'fa-dumbbell' }
|
||||
],
|
||||
lower: [
|
||||
{ name: 'Sentadillas', points: 15, icon: 'fa-dumbbell' },
|
||||
{ name: 'Peso muerto', points: 20, icon: 'fa-dumbbell' },
|
||||
{ name: 'Prensa de piernas', points: 15, icon: 'fa-dumbbell' },
|
||||
{ name: 'Elevaciones de pantorrilla', points: 10, icon: 'fa-dumbbell' },
|
||||
{ name: 'Hip thrust', points: 12, icon: 'fa-dumbbell' }
|
||||
]
|
||||
},
|
||||
pushPullLegs: {
|
||||
push: [
|
||||
{ name: 'Press de banca', points: 15, icon: 'fa-dumbbell' },
|
||||
{ name: 'Press militar', points: 12, icon: 'fa-dumbbell' },
|
||||
{ name: 'Fondos en paralelas', points: 15, icon: 'fa-child' },
|
||||
{ name: 'Elevaciones laterales', points: 10, icon: 'fa-dumbbell' },
|
||||
{ name: 'Extensiones de tríceps', points: 10, icon: 'fa-dumbbell' }
|
||||
],
|
||||
pull: [
|
||||
{ name: 'Dominadas', points: 15, icon: 'fa-child' },
|
||||
{ name: 'Remo con barra', points: 15, icon: 'fa-dumbbell' },
|
||||
{ name: 'Curl de bíceps', points: 10, icon: 'fa-dumbbell' },
|
||||
{ name: 'Face pull', points: 12, icon: 'fa-dumbbell' },
|
||||
{ name: 'Remo en T', points: 12, icon: 'fa-dumbbell' }
|
||||
],
|
||||
legs: [
|
||||
{ name: 'Sentadillas', points: 15, icon: 'fa-dumbbell' },
|
||||
{ name: 'Peso muerto', points: 20, icon: 'fa-dumbbell' },
|
||||
{ name: 'Prensa de piernas', points: 15, icon: 'fa-dumbbell' },
|
||||
{ name: 'Extensiones de cuádriceps', points: 10, icon: 'fa-dumbbell' },
|
||||
{ name: 'Curl de isquiotibiales', points: 10, icon: 'fa-dumbbell' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const achievements = [
|
||||
{ name: 'Principiante', description: 'Completa tu primera misión', icon: 'fa-star', unlocked: false },
|
||||
{ name: 'Constante', description: 'Completa 5 misiones', icon: 'fa-fire', unlocked: false },
|
||||
{ name: 'Maestro del Fitness', description: 'Alcanza el nivel 5', icon: 'fa-fist-raised', unlocked: false },
|
||||
{ name: 'Imparable', description: 'Acumula 1000 puntos', icon: 'fa-trophy', unlocked: false }
|
||||
];
|
||||
|
||||
let userPoints = 0;
|
||||
let userLevel = 1;
|
||||
let routinesCompleted = 0;
|
||||
|
||||
function updateUserInterface() {
|
||||
document.getElementById('userPoints').textContent = userPoints;
|
||||
document.getElementById('userLevel').textContent = userLevel;
|
||||
const expProgress = (userPoints % 100) / 100 * 100;
|
||||
const progressBar = document.getElementById('expProgress');
|
||||
progressBar.style.width = `${expProgress}%`;
|
||||
progressBar.setAttribute('aria-valuenow', expProgress);
|
||||
// Eliminado: progressBar.textContent
|
||||
}
|
||||
|
||||
function populateExerciseList() {
|
||||
const exerciseList = document.getElementById('exerciseList');
|
||||
exerciseList.innerHTML = '';
|
||||
const routineType = document.getElementById('routineType').value;
|
||||
let exercises;
|
||||
|
||||
if (routineType === 'fullBody') {
|
||||
exercises = exercisesByType.fullBody;
|
||||
} else if (routineType === 'upperLower') {
|
||||
exercises = Math.random() < 0.5 ? exercisesByType.upperLower.upper : exercisesByType.upperLower.lower;
|
||||
} else {
|
||||
const pplTypes = ['push', 'pull', 'legs'];
|
||||
const randomType = pplTypes[Math.floor(Math.random() * pplTypes.length)];
|
||||
exercises = exercisesByType.pushPullLegs[randomType];
|
||||
}
|
||||
|
||||
exercises.forEach((exercise, index) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'list-group-item d-flex justify-content-between align-items-center';
|
||||
li.innerHTML = `
|
||||
<span><i class="fas ${exercise.icon} mr-2"></i> ${exercise.name}</span>
|
||||
<span class="badge bg-primary rounded-pill">${exercise.points} XP</span>
|
||||
`;
|
||||
exerciseList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function completeRoutine() {
|
||||
const routineType = document.getElementById('routineType').value;
|
||||
let exercises;
|
||||
|
||||
if (routineType === 'fullBody') {
|
||||
exercises = exercisesByType.fullBody;
|
||||
} else if (routineType === 'upperLower') {
|
||||
exercises = Math.random() < 0.5 ? exercisesByType.upperLower.upper : exercisesByType.upperLower.lower;
|
||||
} else {
|
||||
const pplTypes = ['push', 'pull', 'legs'];
|
||||
const randomType = pplTypes[Math.floor(Math.random() * pplTypes.length)];
|
||||
exercises = exercisesByType.pushPullLegs[randomType];
|
||||
}
|
||||
|
||||
const earnedPoints = exercises.reduce((sum, exercise) => sum + exercise.points, 0);
|
||||
userPoints += earnedPoints;
|
||||
routinesCompleted++;
|
||||
|
||||
while (userPoints >= userLevel * 100) {
|
||||
userPoints -= userLevel * 100;
|
||||
userLevel++;
|
||||
showLevelUpMessage(userLevel);
|
||||
}
|
||||
|
||||
updateUserInterface();
|
||||
checkAchievements();
|
||||
|
||||
showMessageToast(`¡Misión cumplida! Has ganado ${earnedPoints} puntos de experiencia.`);
|
||||
}
|
||||
|
||||
function showLevelUpMessage(newLevel) {
|
||||
Swal.fire({
|
||||
title: '¡Subiste de nivel!',
|
||||
text: `Has alcanzado el nivel ${newLevel}. ¡Sigue así, campeón!`,
|
||||
icon: 'success',
|
||||
confirmButtonText: '¡Genial!'
|
||||
});
|
||||
}
|
||||
|
||||
function checkAchievements() {
|
||||
achievements.forEach((achievement, index) => {
|
||||
if (!achievement.unlocked) {
|
||||
if (
|
||||
(achievement.name === 'Principiante' && routinesCompleted >= 1) ||
|
||||
(achievement.name === 'Constante' && routinesCompleted >= 5) ||
|
||||
(achievement.name === 'Maestro del Fitness' && userLevel >= 5) ||
|
||||
(achievement.name === 'Imparable' && userPoints + (userLevel - 1) * 100 >= 1000)
|
||||
) {
|
||||
achievement.unlocked = true;
|
||||
updateAchievementDisplay(index);
|
||||
showAchievementUnlockedMessage(achievement);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAchievementDisplay(index) {
|
||||
const achievementElement = document.getElementById(`achievement-${index}`);
|
||||
if (achievementElement) {
|
||||
achievementElement.classList.remove('bg-secondary');
|
||||
achievementElement.classList.add('bg-success', 'pulse');
|
||||
}
|
||||
}
|
||||
|
||||
function showAchievementUnlockedMessage(achievement) {
|
||||
Swal.fire({
|
||||
title: '¡Logro Desbloqueado!',
|
||||
text: `Has conseguido el logro "${achievement.name}": ${achievement.description}`,
|
||||
icon: 'success',
|
||||
confirmButtonText: '¡Increíble!'
|
||||
});
|
||||
}
|
||||
|
||||
function populateAchievements() {
|
||||
const achievementsContainer = document.getElementById('achievements');
|
||||
achievements.forEach((achievement, index) => {
|
||||
const achievementElement = document.createElement('div');
|
||||
achievementElement.className = 'col-md-3 mb-3';
|
||||
achievementElement.innerHTML = `
|
||||
<div id="achievement-${index}" class="card text-white ${achievement.unlocked ? 'bg-success' : 'bg-secondary'} achievement">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title"><i class="fas ${achievement.icon} mr-2"></i> ${achievement.name}</h5>
|
||||
<p class="card-text">${achievement.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
achievementsContainer.appendChild(achievementElement);
|
||||
});
|
||||
}
|
||||
|
||||
function showMessageToast(message, type = 'success') {
|
||||
Swal.fire({
|
||||
text: message,
|
||||
icon: type,
|
||||
showConfirmButton: false,
|
||||
timer: 2000,
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
background: '#f8f9fa',
|
||||
color: '#000',
|
||||
iconColor: type === 'error' ? '#dc3545' : type === 'warning' ? '#ffc107' : '#28a745',
|
||||
customClass: {
|
||||
popup: 'shadow-sm'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('routineType').addEventListener('change', populateExerciseList);
|
||||
document.getElementById('completeRoutine').addEventListener('click', completeRoutine);
|
||||
|
||||
populateExerciseList();
|
||||
populateAchievements();
|
||||
updateUserInterface();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include __DIR__ . "/../../layouts/footer.php"; ?>
|
635
app/Views/modules/tareas/kanban.php
Normal file
635
app/Views/modules/tareas/kanban.php
Normal file
@ -0,0 +1,635 @@
|
||||
<?php include __DIR__ . "/../../layouts/header.php"; ?>
|
||||
|
||||
<style>
|
||||
.kanban-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.kanban-column {
|
||||
flex: 1;
|
||||
margin: 0 1rem;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
transition: box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.kanban-column:hover {
|
||||
box-shadow: 0 0 10px rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
|
||||
.kanban-header {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.kanban-task-container {
|
||||
min-height: 40vh;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.kanban-task-item {
|
||||
background-color: #e9ecef;
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: background-color 0.3s;
|
||||
cursor: move;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.kanban-task-item:hover {
|
||||
background-color: #d3d3d3;
|
||||
}
|
||||
|
||||
.kanban-task-actions button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.task-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.task-form__input-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-form__input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-form__badge {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.color-container-html {
|
||||
min-width: 5rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 2px solid #ccc;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
transition: border 0.3s ease, box-shadow 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-container-html:hover {
|
||||
border-color: #33d17a;
|
||||
}
|
||||
|
||||
.color-container-html:focus {
|
||||
border-color: #33d17a;
|
||||
box-shadow: 0 0 5px rgba(51, 209, 122, 0.5);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.priority-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.priority-high {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.priority-medium {
|
||||
background-color: #ffc107;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.priority-low {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Estilo para la descripción en la tarjeta */
|
||||
.task-description {
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.5rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* Estilo para la fecha de vencimiento */
|
||||
.due-date {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Estilos para el formulario de tareas */
|
||||
.task-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.swal2-input,
|
||||
.swal2-textarea,
|
||||
.swal2-select {
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.3rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.swal2-input:focus,
|
||||
.swal2-textarea:focus,
|
||||
.swal2-select:focus {
|
||||
border-color: #007bff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#taskColor,
|
||||
#editTaskColor {
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.task-form {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.swal2-input,
|
||||
.swal2-textarea,
|
||||
.swal2-select {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.kanban-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.kanban-column {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid px-3 px-md-4 mt-4">
|
||||
<div class="text-center mb-4">
|
||||
<h1 class="fw-bold">Kanban Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div id="alertMessages" class="container mb-3"></div>
|
||||
|
||||
<div class="d-flex justify-content-center mb-3">
|
||||
<button id="addTaskButton" class="btn btn-success">
|
||||
<i class="fas fa-plus"></i> Añadir Tarea
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="kanban-container">
|
||||
<div class="kanban-column" id="pendingList" data-status="1">
|
||||
<div class="kanban-header">Pendiente</div>
|
||||
<div class="kanban-task-container"></div>
|
||||
</div>
|
||||
<div class="kanban-column" id="inProgressList" data-status="2">
|
||||
<div class="kanban-header">En Proceso</div>
|
||||
<div class="kanban-task-container"></div>
|
||||
</div>
|
||||
<div class="kanban-column" id="completedList" data-status="3">
|
||||
<div class="kanban-header">Completada</div>
|
||||
<div class="kanban-task-container"></div>
|
||||
</div>
|
||||
<div class="kanban-column" id="canceledList" data-status="4">
|
||||
<div class="kanban-header">Cancelada</div>
|
||||
<div class="kanban-task-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts necesarios -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.4.2/dist/tinycolor-min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const apiEndpoints = {
|
||||
fetch: '/tareas',
|
||||
create: '/tarea/create',
|
||||
update: '/tarea/update',
|
||||
updateStatus: '/tarea/update-status',
|
||||
delete: '/tarea/delete',
|
||||
get: (id) => `/tarea/get/${id}`
|
||||
};
|
||||
|
||||
const columns = Array.from(document.querySelectorAll('.kanban-column'));
|
||||
const addTaskButton = document.getElementById('addTaskButton');
|
||||
|
||||
/**
|
||||
* @function fetchTasks
|
||||
* @description Obtiene las tareas desde el servidor y las carga en la UI y el StateManager
|
||||
*/
|
||||
const fetchTasks = async () => {
|
||||
try {
|
||||
const response = await fetch(apiEndpoints.fetch);
|
||||
const { tasks } = await response.json();
|
||||
|
||||
const taskMap = tasks.reduce((acc, task) => {
|
||||
acc[task.task_id] = task;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
StateManager.set('tasks', taskMap);
|
||||
|
||||
tasks.forEach(task => addTaskToColumn(task));
|
||||
} catch (error) {
|
||||
console.error('Error al cargar las tareas:', error);
|
||||
showMessage('Error al cargar las tareas', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @function buildTaskHTML
|
||||
* @description Crea el HTML para una tarea
|
||||
* @param {Object} task - Objeto de tarea
|
||||
* @returns {HTMLElement} Elemento DOM de la tarea
|
||||
*/
|
||||
const buildTaskHTML = (task) => {
|
||||
const taskElement = document.createElement('div');
|
||||
taskElement.className = 'kanban-task-item';
|
||||
taskElement.setAttribute('data-task-id', task.task_id);
|
||||
taskElement.style.backgroundColor = task.color;
|
||||
taskElement.style.color = tinycolor(task.color).isLight() ? '#000000' : '#ffffff';
|
||||
|
||||
taskElement.innerHTML = `
|
||||
<div>
|
||||
<h5>${task.task_title}</h5>
|
||||
<p class="task-description">${task.description || ''}</p>
|
||||
<small class="due-date">Vence: ${formatDate(task.due_date)}</small>
|
||||
</div>
|
||||
<div class="kanban-task-actions">
|
||||
<button class="btn btn-sm btn-outline-${tinycolor(task.color).isLight() ? 'dark' : 'light'} edit-task-btn">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-${tinycolor(task.color).isLight() ? 'danger' : 'light'} delete-task-btn">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
taskElement.querySelector('.edit-task-btn').addEventListener('click', () => handleEditTask(task.task_id));
|
||||
taskElement.querySelector('.delete-task-btn').addEventListener('click', () => handleDeleteTask(task.task_id));
|
||||
|
||||
return taskElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* @function addTaskToColumn
|
||||
* @description Agrega una tarea a la columna correspondiente en la UI
|
||||
* @param {Object} task - Objeto de tarea
|
||||
*/
|
||||
const addTaskToColumn = (task) => {
|
||||
const column = document.querySelector(`[data-status="${task.status_id}"] .kanban-task-container`);
|
||||
if (column) {
|
||||
column.appendChild(buildTaskHTML(task));
|
||||
} else {
|
||||
console.warn(`Columna con status_id ${task.status_id} no encontrada.`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @function updateTaskStatus
|
||||
* @description Actualiza el estado de una tarea si ha cambiado
|
||||
* @param {number} taskId - ID de la tarea
|
||||
* @param {number} newStatus - Nuevo estado de la tarea
|
||||
*/
|
||||
const updateTaskStatus = async (taskId, newStatus) => {
|
||||
const currentTasks = StateManager.get('tasks', {});
|
||||
const currentTask = currentTasks[taskId];
|
||||
|
||||
if (currentTask && currentTask.status_id === newStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
StateManager.update('tasks', {
|
||||
[taskId]: { ...currentTask, status_id: newStatus }
|
||||
});
|
||||
|
||||
const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
|
||||
if (taskElement) {
|
||||
const newColumn = document.querySelector(`[data-status="${newStatus}"] .kanban-task-container`);
|
||||
if (newColumn) {
|
||||
newColumn.appendChild(taskElement);
|
||||
} else {
|
||||
console.warn(`Columna con status_id ${newStatus} no encontrada.`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(apiEndpoints.updateStatus, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ task_id: taskId, status_id: newStatus })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.message) {
|
||||
showMessage(data.message, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
StateManager.update('tasks', {
|
||||
[taskId]: { ...currentTask, status_id: currentTask.status_id }
|
||||
});
|
||||
if (taskElement) {
|
||||
const originalColumn = document.querySelector(`[data-status="${currentTask.status_id}"] .kanban-task-container`);
|
||||
if (originalColumn) {
|
||||
originalColumn.appendChild(taskElement);
|
||||
}
|
||||
}
|
||||
showMessage('Error al actualizar el estado de la tarea', 'error');
|
||||
console.error('Error al actualizar el estado de la tarea:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @function handleCreateTask
|
||||
* @description Maneja la creación de una nueva tarea
|
||||
*/
|
||||
const handleCreateTask = () => {
|
||||
Swal.fire({
|
||||
title: 'Crear Nueva Tarea',
|
||||
html: `
|
||||
<div class="task-form">
|
||||
<input type="text" id="taskTitle" class="swal2-input" placeholder="Título de la Tarea">
|
||||
<textarea id="taskDescription" class="swal2-textarea" placeholder="Descripción"></textarea>
|
||||
<input type="date" id="taskDueDate" class="swal2-input">
|
||||
<div class="color-picker">
|
||||
<input type="color" id="taskColor" class="swal2-input" value="#e9ecef">
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Crear',
|
||||
cancelButtonText: 'Cancelar',
|
||||
preConfirm: () => {
|
||||
const title = document.getElementById('taskTitle').value.trim();
|
||||
if (!title) {
|
||||
Swal.showValidationMessage('El título de la tarea es obligatorio');
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
task_title: title,
|
||||
description: document.getElementById('taskDescription').value.trim(),
|
||||
due_date: document.getElementById('taskDueDate').value,
|
||||
color: document.getElementById('taskColor').value,
|
||||
status_id: 1
|
||||
};
|
||||
}
|
||||
}).then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await fetch(apiEndpoints.create, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(result.value)
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.task_id) {
|
||||
const newTask = {
|
||||
task_id: data.task_id,
|
||||
...result.value
|
||||
};
|
||||
addTaskToColumn(newTask);
|
||||
|
||||
const currentTasks = StateManager.get('tasks', {});
|
||||
StateManager.update('tasks', {
|
||||
[data.task_id]: newTask
|
||||
});
|
||||
|
||||
showMessage('Tarea creada exitosamente', 'success');
|
||||
} else {
|
||||
showMessage('Error al crear la tarea', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al crear la tarea:', error);
|
||||
showMessage('Error al crear la tarea', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @function handleEditTask
|
||||
* @description Maneja la edición de una tarea existente
|
||||
* @param {number} taskId - ID de la tarea a editar
|
||||
*/
|
||||
const handleEditTask = async (taskId) => {
|
||||
try {
|
||||
const response = await fetch(apiEndpoints.get(taskId));
|
||||
const task = await response.json();
|
||||
|
||||
if (!task) {
|
||||
showMessage('Tarea no encontrada', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const { task_title, description, due_date, color } = task;
|
||||
Swal.fire({
|
||||
title: 'Editar Tarea',
|
||||
html: `
|
||||
<div class="task-form">
|
||||
<input type="text" id="editTaskTitle" class="swal2-input" placeholder="Título de la Tarea" value="${escapeHtml(task_title)}">
|
||||
<textarea id="editTaskDescription" class="swal2-textarea" placeholder="Descripción">${escapeHtml(description)}</textarea>
|
||||
<input type="date" id="editTaskDueDate" class="swal2-input" value="${due_date ? due_date.split('T')[0] : ''}">
|
||||
<div class="color-picker">
|
||||
<input type="color" id="editTaskColor" class="swal2-input" value="${color}">
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Guardar',
|
||||
cancelButtonText: 'Cancelar',
|
||||
preConfirm: () => {
|
||||
const title = document.getElementById('editTaskTitle').value.trim();
|
||||
if (!title) {
|
||||
Swal.showValidationMessage('El título de la tarea es obligatorio');
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
task_title: title,
|
||||
description: document.getElementById('editTaskDescription').value.trim(),
|
||||
due_date: document.getElementById('editTaskDueDate').value,
|
||||
color: document.getElementById('editTaskColor').value
|
||||
};
|
||||
}
|
||||
}).then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
const updatedTask = {
|
||||
task_id: taskId,
|
||||
...result.value
|
||||
};
|
||||
|
||||
try {
|
||||
const updateResponse = await fetch(apiEndpoints.update, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(updatedTask)
|
||||
});
|
||||
|
||||
const updateData = await updateResponse.json();
|
||||
if (updateResponse.ok) {
|
||||
const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
|
||||
if (taskElement) {
|
||||
taskElement.querySelector('h5').textContent = updatedTask.task_title;
|
||||
taskElement.querySelector('.task-description').textContent = updatedTask.description;
|
||||
taskElement.querySelector('.due-date').textContent = `Vence: ${formatDate(updatedTask.due_date)}`;
|
||||
taskElement.style.backgroundColor = updatedTask.color;
|
||||
const textColor = tinycolor(updatedTask.color).isLight() ? '#000000' : '#ffffff';
|
||||
taskElement.style.color = textColor;
|
||||
}
|
||||
|
||||
StateManager.update('tasks', {
|
||||
[taskId]: updatedTask
|
||||
});
|
||||
|
||||
showMessage(updateData.message, 'success');
|
||||
} else {
|
||||
showMessage(updateData.message || 'Error al actualizar la tarea', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al actualizar la tarea:', error);
|
||||
showMessage('Error al actualizar la tarea', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error al obtener los datos de la tarea:', error);
|
||||
showMessage('Error al cargar la tarea para editar', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @function handleDeleteTask
|
||||
* @description Maneja la eliminación de una tarea
|
||||
* @param {number} taskId - ID de la tarea a eliminar
|
||||
*/
|
||||
const handleDeleteTask = async (taskId) => {
|
||||
try {
|
||||
const result = await Swal.fire({
|
||||
title: '¿Estás seguro?',
|
||||
text: 'Esta acción no se puede deshacer',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#dc3545',
|
||||
cancelButtonColor: '#6c757d',
|
||||
confirmButtonText: 'Sí, eliminar',
|
||||
cancelButtonText: 'Cancelar',
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await fetch(apiEndpoints.delete, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ task_id: taskId })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.message) {
|
||||
const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
|
||||
if (taskElement) {
|
||||
taskElement.remove();
|
||||
}
|
||||
|
||||
const currentTasks = StateManager.get('tasks', {});
|
||||
if (currentTasks[taskId]) {
|
||||
delete currentTasks[taskId];
|
||||
StateManager.set('tasks', currentTasks);
|
||||
}
|
||||
|
||||
showMessage(data.message, 'success');
|
||||
} else {
|
||||
showMessage('Error al eliminar la tarea', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al eliminar la tarea:', error);
|
||||
showMessage('Error al eliminar la tarea', 'error');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al eliminar la tarea:', error);
|
||||
showMessage('Error al eliminar la tarea', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @function setupSortable
|
||||
* @description Configura SortableJS para las columnas
|
||||
* @param {HTMLElement} column - Elemento de columna
|
||||
*/
|
||||
const setupSortable = (column) => {
|
||||
new Sortable(column.querySelector('.kanban-task-container'), {
|
||||
group: 'tasks',
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
onEnd: ({ item, to }) => {
|
||||
const taskId = item.getAttribute('data-task-id');
|
||||
const newStatus = parseInt(to.closest('.kanban-column').getAttribute('data-status'), 10);
|
||||
updateTaskStatus(taskId, newStatus);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!StateManager.has('tasks')) {
|
||||
StateManager.set('tasks', {});
|
||||
}
|
||||
|
||||
columns.forEach(column => {
|
||||
setupSortable(column);
|
||||
});
|
||||
|
||||
addTaskButton.addEventListener('click', handleCreateTask);
|
||||
fetchTasks();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<?php include __DIR__ . "/../../layouts/footer.php"; ?>
|
229
bin/Crafter.php
Normal file
229
bin/Crafter.php
Normal file
@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace Console;
|
||||
|
||||
use Exception;
|
||||
|
||||
class Crafter
|
||||
{
|
||||
private $config;
|
||||
|
||||
public function run($command, $arguments)
|
||||
{
|
||||
if (is_null($command)) {
|
||||
$this->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 = "<?php\n\nnamespace App\Controllers;\n\nclass {$controllerName} extends BaseController\n{\n // Add your controller methods here\n}\n";
|
||||
|
||||
try {
|
||||
$this->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 = "<?php\n\nnamespace App\Repositories;\n\nclass {$repositoryName} extends BaseRepository\n{\n // Add your repository methods here\n}\n";
|
||||
|
||||
try {
|
||||
$this->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 = <<<PHP
|
||||
<?php
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
use App\Entities\BaseEntity;
|
||||
|
||||
class {$entityName} extends BaseEntity
|
||||
{
|
||||
/**
|
||||
* @var int Identificador único de {$entityName}.
|
||||
*/
|
||||
public int \${$primaryKey};
|
||||
|
||||
/**
|
||||
* Obtiene el nombre de la clave primaria de la entidad.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getPrimaryKey(): string
|
||||
{
|
||||
return '{$primaryKey}';
|
||||
}
|
||||
}
|
||||
|
||||
PHP;
|
||||
|
||||
try {
|
||||
$this->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 <command> [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";
|
||||
}
|
||||
}
|
0
bin/SysTask/.gitkeep
Normal file
0
bin/SysTask/.gitkeep
Normal file
23
composer.json
Normal file
23
composer.json
Normal file
@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
11
crafter
Normal file
11
crafter
Normal file
@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Console\Crafter;
|
||||
|
||||
$command = $_SERVER['argv'][1] ?? null;
|
||||
|
||||
$handler = new Crafter();
|
||||
$handler->run($command, array_slice($_SERVER['argv'], 2));
|
43
docker-compose.yml
Normal file
43
docker-compose.yml
Normal file
@ -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:
|
52
docker/init_scripts/check_php_extensions.sh
Executable file
52
docker/init_scripts/check_php_extensions.sh
Executable file
@ -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
|
63
docker/init_scripts/update_composer.sh
Executable file
63
docker/init_scripts/update_composer.sh
Executable file
@ -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 <package>'${NC}"
|
||||
else
|
||||
echo -e "\n${GREEN}Todas las dependencias están actualizadas.${NC}"
|
||||
fi
|
||||
|
||||
print_message "Proceso Completado"
|
7
public/.htaccess
Normal file
7
public/.htaccess
Normal file
@ -0,0 +1,7 @@
|
||||
ReWriteEngine On
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
RewriteRule ^ index.php [L]
|
192
public/assets/css/main.css
Normal file
192
public/assets/css/main.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
0
public/assets/img/.gitkeep
Normal file
0
public/assets/img/.gitkeep
Normal file
38
public/index.php
Normal file
38
public/index.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
require '../vendor/autoload.php';
|
||||
date_default_timezone_set('America/Guatemala');
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
|
||||
$dotenv->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();
|
||||
}
|
222
schema/schema.sql
Normal file
222
schema/schema.sql
Normal file
@ -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');
|
0
storage/.gitkeep
Normal file
0
storage/.gitkeep
Normal file
8
xdebug.ini
Normal file
8
xdebug.ini
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user