Respaldo automático 2026-06-30 06:13
This commit is contained in:
parent
b073238190
commit
bde75f61f3
|
|
@ -1,216 +1,214 @@
|
|||
# Del Austral — Historial Clínico Digital
|
||||
### Guía de instalación en tu hosting cPanel
|
||||
### Guía de instalación y uso (versión final)
|
||||
|
||||
---
|
||||
|
||||
## ¿Ya tenés el sistema instalado? Empezá acá
|
||||
|
||||
Si ya estabas usando una versión anterior con pacientes cargados, **no necesitás reinstalar todo desde cero**, pero esta actualización es la más grande hasta ahora: agrega sedes, varios profesionales con datos separados entre sí, y un nuevo nivel de acceso ("Desarrollador"). Seguí estos pasos en orden, sin saltar ninguno:
|
||||
Si ya estabas usando una versión anterior con pacientes cargados, **no necesitás reinstalar nada desde cero**. Solo tenés que correr las migraciones de base de datos que todavía no hayas corrido, y reemplazar los archivos por sus versiones nuevas.
|
||||
|
||||
1. **Migrá la base de datos** (una sola vez, en este orden exacto): en phpMyAdmin, entrá a tu base de datos → pestaña **"SQL"** (no "Importar"):
|
||||
- Si nunca corriste `migracion_v2.sql`, pegalo y ejecutalo (agrega citas, archivos adjuntos y plantillas).
|
||||
- Si nunca corriste `migracion_v3.sql`, pegalo y ejecutalo (agrega usuarios con roles e historial de cambios).
|
||||
- Si nunca corriste `migracion_v4.sql`, pegalo y ejecutalo (cambia el acceso de patrón dibujado a PIN numérico).
|
||||
- Por último, pegá y ejecutá **`migracion_v5.sql`** (agrega sedes, separa los pacientes por profesional, agrega el rol Desarrollador y la confirmación de turnos por el paciente). Este script crea una "Sede principal" automática y te asigna ahí todo lo que ya tenías cargado, para no perder nada.
|
||||
- Si nunca corriste `migracion_v6.sql`, pegalo y ejecutalo también (agrega el aviso de confirmaciones/cancelaciones de turno).
|
||||
- Por último, pegá y ejecutá **`migracion_v7.sql`** (agrega a la papelera el profesional y la sede original de cada legajo eliminado, necesario para que el Desarrollador pueda recuperarlos).
|
||||
- Si nunca corriste `migracion_v8.sql`, pegalo y ejecutalo también (agrega el aviso de "legajo recuperado de otro profesional" en la ficha del paciente).
|
||||
- Por último, pegá y ejecutá **`migracion_v9.sql`** (el DNI pasa a ser único por profesional, no global — así dos profesionales distintos pueden tener cada uno un paciente con el mismo número de DNI, sin chocar entre sí).
|
||||
- Ninguno de los cinco scripts borra pacientes, sesiones, citas ni adjuntos.
|
||||
2. **Subí la carpeta `adjuntos/`** completa (si todavía no la tenías) al mismo nivel que `index.html`.
|
||||
3. **Reemplazá** estos archivos por sus versiones nuevas: `index.html`, toda la carpeta `assets/`, toda la carpeta `api/`, `exportar.php`, y agregá el archivo nuevo **`confirmar_turno.php`**. No toques `config/config.php` — ya tiene tus credenciales y no cambió.
|
||||
### 1. Migrá la base de datos
|
||||
|
||||
**Importante — cómo queda tu acceso después de esta migración.** Tu usuario profesional y tu PIN siguen funcionando igual que antes, pero el login cambió de forma: ahora antes de poner el PIN vas a tener que elegir una sede (te va a aparecer "Sede principal", que es la que creó la migración) y después tu nombre en la lista. Es un paso más, pero el PIN es el mismo de siempre.
|
||||
En phpMyAdmin, entrá a tu base de datos → pestaña **"SQL"** (no "Importar"), y corré en orden, una por una, las migraciones que todavía no hayas aplicado:
|
||||
|
||||
**Sobre el nuevo rol Desarrollador.** Es un nivel de acceso por encima de todo, pensado para que solo vos (o quien administre el sistema técnicamente) pueda crear sedes nuevas y dar de alta o baja a profesionales y administrativas — así un médico no puede, por error o sin permiso, crear accesos para otros médicos. La migración **no te crea automáticamente** una clave de Desarrollador: la primera vez que entres después de migrar, vas a ver el botón "Soy el Desarrollador" en la pantalla de selección de sede. Tocalo y vas a poder crear esa clave (también de 4 números) en ese momento. Una vez creada, desde el panel de Desarrollador podés organizar mejor tus sedes y agregar a los demás profesionales si tenés más de uno.
|
||||
| Migración | Qué agrega |
|
||||
|---|---|
|
||||
| `migracion_v2.sql` | Citas, archivos adjuntos y plantillas de evolución |
|
||||
| `migracion_v3.sql` | Usuarios con roles e historial de cambios |
|
||||
| `migracion_v4.sql` | Cambia el acceso de patrón dibujado a PIN numérico |
|
||||
| `migracion_v5.sql` | Sedes, separación de pacientes por profesional, rol Desarrollador, confirmación de turnos por el paciente |
|
||||
| `migracion_v6.sql` | Aviso de confirmaciones/cancelaciones de turno |
|
||||
| `migracion_v7.sql` | Profesional y sede original en la papelera (para poder recuperar legajos) |
|
||||
| `migracion_v8.sql` | Aviso de "legajo recuperado de otro profesional" en la ficha del paciente |
|
||||
| `migracion_v9.sql` | El DNI pasa a ser único por profesional, no global |
|
||||
| `migracion_v10.sql` | Legajos completos de profesionales (datos personales, especialidad) y sistema de licencias |
|
||||
| `migracion_v11.sql` | Número de legajo automático (formato `LG-AAAA-NNN`) |
|
||||
| `migracion_v12.sql` | Firma digital del profesional |
|
||||
|
||||
Si en tu consultorio **solo hay un profesional** (vos) y no necesitás nada de sedes múltiples ni separar pacientes entre médicos, no es obligatorio que uses el rol Desarrollador para el día a día — solo lo vas a necesitar la primera vez para crear la sede y, si querés, agregar una administrativa.
|
||||
Ninguna de estas migraciones borra pacientes, sesiones, citas ni adjuntos existentes. Si no estás seguro de cuáles ya corriste, no pasa nada grave en correr una de nuevo por error — la mayoría usa `ALTER TABLE ... ADD COLUMN`, que falla de forma segura (sin romper nada) si la columna ya existe.
|
||||
|
||||
### 2. Reemplazá los archivos
|
||||
|
||||
Subí las versiones nuevas de: `index.html`, toda la carpeta `assets/`, toda la carpeta `api/`, `exportar.php`, `confirmar_turno.php`, `version.json`, `manifest.json`, `sw.js`. **No toques** `config/config.php` — ya tiene tus credenciales reales y no cambia entre actualizaciones.
|
||||
|
||||
### 3. Confirmá la actualización
|
||||
|
||||
Entrá como Desarrollador (ver más abajo cómo) → pestaña **"Versión del sistema"** → tocá "Revisar ahora". Si todo quedó en verde, la actualización se aplicó correctamente.
|
||||
|
||||
---
|
||||
|
||||
## Instalación desde cero (primera vez)
|
||||
|
||||
## 1. Crear la base de datos en cPanel
|
||||
### Si vas a usar hosting compartido (cPanel)
|
||||
|
||||
1. Entrá a **cPanel** → buscá **"Bases de datos MySQL"**.
|
||||
2. En **"Crear nueva base de datos"**, escribí por ejemplo `legajos` y hacé clic en **Crear base de datos**.
|
||||
- cPanel le va a agregar tu usuario de hosting como prefijo, por ejemplo: `tuusuario_legajos`. Anotá ese nombre completo.
|
||||
3. Bajá hasta **"Usuarios MySQL"** → **"Añadir nuevo usuario"**. Elegí un usuario (ej: `admin`) y una contraseña segura. Anotala. El nombre final va a ser algo como `tuusuario_admin`.
|
||||
4. Bajá hasta **"Añadir usuario a la base de datos"**. Seleccioná el usuario y la base que creaste, hacé clic en **Añadir**, y en la pantalla de privilegios marcá **"ALL PRIVILEGES"**. Guardá.
|
||||
**1. Crear la base de datos**
|
||||
|
||||
Con esto ya tenés: nombre de base de datos, usuario y contraseña. Los vas a necesitar en el paso 3.
|
||||
1. Entrá a cPanel → **"Bases de datos MySQL"**.
|
||||
2. En "Crear nueva base de datos", escribí un nombre (ej: `legajos`) → Crear. cPanel le agrega tu usuario de hosting como prefijo (ej: `tuusuario_legajos`); anotá el nombre completo.
|
||||
3. Bajá a "Usuarios MySQL" → "Añadir nuevo usuario". Elegí usuario y contraseña, anotalos.
|
||||
4. Bajá a "Añadir usuario a la base de datos", asociá el usuario a la base, y marcá **"ALL PRIVILEGES"**.
|
||||
|
||||
---
|
||||
**2. Importar la estructura**
|
||||
|
||||
## 2. Importar la estructura de tablas
|
||||
1. Abrí phpMyAdmin → tu base de datos → pestaña **"Importar"**.
|
||||
2. Elegí el archivo `database.sql` (incluido en este proyecto) → Importar.
|
||||
3. Deberías ver, a la izquierda, todas las tablas: `sedes`, `desarrollador`, `usuarios`, `usuarios_sedes`, `profesionales_legajos`, `obras_sociales`, `pacientes`, `sesiones`, `legajos_eliminados`, `citas`, `archivos_adjuntos`, `plantillas_evolucion`, `historial_cambios`.
|
||||
|
||||
1. En cPanel, abrí **phpMyAdmin**.
|
||||
2. En la columna izquierda, hacé clic en la base de datos que creaste (`tuusuario_legajos`).
|
||||
3. Arriba, hacé clic en la pestaña **"Importar"**.
|
||||
4. Elegí el archivo **`database.sql`** (incluido en este proyecto) y hacé clic en **Continuar / Importar** abajo de todo.
|
||||
5. Deberías ver un mensaje de éxito y, a la izquierda, las tablas: `sedes`, `desarrollador`, `usuarios`, `usuarios_sedes`, `obras_sociales`, `pacientes`, `sesiones`, `legajos_eliminados`, `citas`, `archivos_adjuntos`, `plantillas_evolucion`, `historial_cambios`.
|
||||
**3. Configurar la conexión**
|
||||
|
||||
---
|
||||
|
||||
## 3. Configurar la conexión
|
||||
|
||||
1. Abrí el archivo **`config/config.php`** con el editor de texto que prefieras.
|
||||
2. Completá estas 4 líneas con tus datos reales del paso 1:
|
||||
Abrí `config/config.php` y completá con tus datos reales:
|
||||
|
||||
```php
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'tuusuario_legajos');
|
||||
define('DB_USER', 'tuusuario_admin');
|
||||
define('DB_PASS', 'tu_contraseña_real');
|
||||
define('APP_SECRET', 'escribí-aquí-cualquier-texto-largo-y-random-único');
|
||||
```
|
||||
|
||||
3. Cambiá también esta línea por cualquier texto largo e inventado (solo una vez, antes de subir el sitio):
|
||||
**4. Subir los archivos**
|
||||
|
||||
```php
|
||||
define('APP_SECRET', 'escribí-aquí-cualquier-texto-largo-y-random-unico');
|
||||
Subí todo el contenido del proyecto (manteniendo la estructura) a `public_html` o la subcarpeta donde quieras publicar el sitio. La carpeta `adjuntos/` necesita permisos de escritura (755, o 775 si no alcanza).
|
||||
|
||||
### Si vas a usar un VPS propio (Nginx + PHP-FPM + MariaDB)
|
||||
|
||||
La instalación es la misma en esencia (base de datos, `config.php`, archivos), pero con algunas diferencias de configuración del servidor que vale la pena tener en cuenta:
|
||||
|
||||
- **`DB_HOST`**: en algunos VPS, `'localhost'` falla con el error `SQLSTATE[HY000] [2002] No such file or directory` porque PHP intenta usar un socket Unix que no está donde se espera. Si te pasa esto, cambiá `DB_HOST` a `'127.0.0.1'` — eso fuerza la conexión por TCP y evita el problema.
|
||||
- **PHP-FPM debe estar corriendo**: verificá con `systemctl status php8.2-fpm` (ajustá la versión). Si el socket en `/run/php/` no existe, el sitio va a dar 404 o 502 en cualquier archivo `.php`.
|
||||
- **El virtual host de Nginx** necesita el bloque `location ~ \.php$` apuntando al socket correcto de PHP-FPM, sin comentar. Un ejemplo mínimo:
|
||||
|
||||
```
|
||||
server {
|
||||
server_name tudominio.com;
|
||||
root /var/www/tu-proyecto;
|
||||
index index.php index.html;
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
location ~ \.php$ {
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Guardá el archivo.
|
||||
- **Importar la base de datos** se hace igual que en cPanel, pero desde la terminal: `mysql -u tu_usuario -p tu_base < database.sql`.
|
||||
- Si el sitio carga pero da un error de PHP sobre una tabla que "no existe" (`Table 'tubase.desarrollador' doesn't exist`), es señal de que `database.sql` nunca se importó — la base existe pero está vacía.
|
||||
|
||||
---
|
||||
### Primer ingreso (cPanel o VPS, es igual)
|
||||
|
||||
## 4. Subir los archivos
|
||||
1. Entrá a tu dominio. La primera vez te va a pedir crear la **clave de Desarrollador** (4 números) — guardala en un lugar seguro, separado de los PIN de los profesionales.
|
||||
2. Después, una pantalla te deja crear de una vez tu **primera sede** y tu **primer profesional** (con su legajo completo y PIN).
|
||||
3. Listo. Cerrá sesión y volvé a entrar: ahora vas a ver la pantalla normal — elegís sede, elegís tu nombre, ponés tu PIN.
|
||||
|
||||
1. En cPanel, abrí **"Administrador de archivos"** (File Manager) o usá un cliente FTP (FileZilla, etc.).
|
||||
2. Entrá a la carpeta donde se publica tu sitio (normalmente `public_html`, o una subcarpeta si querés que viva en `tudominio.com/legajos`).
|
||||
3. Subí **todo el contenido** de esta carpeta del proyecto (manteniendo la estructura: `index.html`, `exportar.php`, `confirmar_turno.php`, `manifest.json`, `sw.js`, `version.json`, carpetas `api/`, `assets/` —incluida `assets/icons/`—, `config/`, `adjuntos/`).
|
||||
|
||||
**Importante:** la carpeta `adjuntos/` necesita permisos de escritura para que el sitio pueda guardar los archivos que subas. Si al subir un archivo te da error de permisos, entrá al Administrador de archivos, clic derecho sobre la carpeta `adjuntos` → Permisos → poné **755** (o 775 si 755 no alcanza).
|
||||
|
||||
**Sobre `config/`:** no necesita estar accesible desde el navegador directamente, pero no es grave si lo está porque PHP no expone el código fuente, solo lo ejecuta.
|
||||
|
||||
---
|
||||
|
||||
## 5. Probar — primer ingreso
|
||||
|
||||
1. Entrá a `https://tudominio.com/` (o la ruta donde lo subiste).
|
||||
2. La primera vez te va a pedir crear la **clave de Desarrollador** (4 números). Es la llave maestra para configurar el sistema — guardala en un lugar seguro, separado de los PIN de los profesionales.
|
||||
3. Después de crear la clave, te va a aparecer una pantalla para crear de una vez tu **primera sede** y tu **primer profesional** (con su propio PIN de 4 números). Completá los tres datos y tocá "Crear sede y profesional".
|
||||
4. Listo. Cerrá sesión y volvé a entrar: ahora vas a ver la pantalla normal de acceso — elegís la sede, elegís tu nombre, y poné tu PIN.
|
||||
|
||||
Si en algún momento necesitás agregar otra sede, otro profesional, o una administrativa, entrá con la clave de Desarrollador (botón "Soy el Desarrollador" en la pantalla de selección de sede) y gestionalo desde ahí.
|
||||
|
||||
---
|
||||
|
||||
## ¿Qué hace cada parte del sistema?
|
||||
|
||||
- **Crear legajo**: registra un paciente nuevo con sus datos, obra social y, opcionalmente, las primeras sesiones. El paciente queda asociado automáticamente a tu usuario (solo vos lo vas a poder ver) y a la sede donde iniciaste sesión.
|
||||
- **Acceder a legajos**: buscá por DNI, nombre y apellido, fecha de atención, obra social, o **sede**. Desde la ficha del paciente podés:
|
||||
- **Editar sus datos** en cualquier momento con el botón "Editar datos".
|
||||
- **Cambiarlo de sede** con el botón "Cambiar de sede", si el paciente empezó a atenderse en otro lugar.
|
||||
- **Agregar, editar o eliminar sesiones**: cada sesión de la línea de tiempo tiene sus propios botones de lápiz (editar) y tacho (eliminar), por si te equivocaste al escribir algo.
|
||||
- Usar una **plantilla de evolución** al agregar una sesión (texto reutilizable, propio de cada profesional — no se comparten entre médicos distintos).
|
||||
- **Agendar y gestionar citas** del paciente (marcarlas como atendidas, ausentes o cancelarlas). El sistema no te deja agendar dos turnos a la misma fecha y hora para el mismo profesional, sin importar en qué sede sea.
|
||||
- **Enviar un recordatorio por WhatsApp** con un mensaje pre-armado que incluye un link para que el paciente confirme o cancele el turno con un toque, sin necesidad de loguearse a nada.
|
||||
- **Copiar el link de confirmación** del turno (ícono de "copiar"), por si preferís mandarlo por otro medio que no sea WhatsApp.
|
||||
- **Subir y descargar archivos adjuntos** (PDF o imágenes), hasta 15 MB cada uno.
|
||||
- **Exportar el legajo completo a PDF**, con tu nombre y título como firma al final.
|
||||
- **Agenda**: calendario mensual completo con tus citas. Desde el panel principal también ves un resumen de "próximas citas" de los próximos 7 días.
|
||||
- **"Hoy tenés X consultas"**: en el panel principal, una franja te muestra cuántas consultas de hoy todavía no llegó su hora y a qué hora es la próxima. El número baja automáticamente a medida que pasan las horas agendadas, sin que tengas que tocar nada — no depende de que marques "Atendida".
|
||||
- **Aviso de confirmaciones y cancelaciones**: cuando un paciente confirma o cancela su turno desde el link que le mandaste, en el panel principal te aparece un cartel con la cantidad de novedades. Tocalo para ver el detalle (quién confirmó, quién canceló) y marcarlas como vistas.
|
||||
- **Pacientes sin sesiones recientes** y **próximos cumpleaños**: resúmenes en el panel principal.
|
||||
- **Estadísticas**: pacientes totales, sesiones del mes, citas por estado, distribución por obra social — siempre de **tus propios pacientes**, no de otros profesionales. Desde ahí también podés tocar "Descargar backup de todos mis legajos" para bajar un archivo con todos tus pacientes, sus sesiones, citas y la lista de adjuntos — útil como respaldo propio, fuera de la base de datos.
|
||||
- **Eliminar legajos**: el legajo desaparece de las búsquedas normales, pero queda guardado completo en la base histórica.
|
||||
- **Obras sociales**: catálogo compartido entre todos los profesionales (no es información de un paciente puntual, así que no hace falta separarlo).
|
||||
- **Instalar el sistema como app**: tanto en celular como en computadora, podés "instalar" Del Austral para que tenga su propio ícono y se abra como una app, sin pasar por el navegador cada vez. Ver la sección [Instalar como app](#instalar-como-app) más abajo.
|
||||
- **Verificación de versión** (solo Desarrollador): una pestaña en el panel del Desarrollador que revisa si los archivos del servidor coinciden con la última actualización que te entregamos, para detectar de un vistazo si algo quedó con una versión vieja después de subir archivos nuevos.
|
||||
- **Reportes por sede** (solo Desarrollador): otra pestaña del panel que muestra, sede por sede, cuántos profesionales y administrativas atienden ahí, cuántos pacientes hay en total, y la actividad (sesiones y citas) de este mes. Útil para tener una foto general si el consultorio crece a varias sucursales.
|
||||
Si en algún momento necesitás agregar otra sede, otro profesional o una administrativa, entrá con la clave de Desarrollador (ver la sección siguiente sobre cómo acceder) y gestionalo desde el panel.
|
||||
|
||||
---
|
||||
|
||||
## Sedes, profesionales y roles
|
||||
|
||||
Este sistema soporta un consultorio con **varias sedes** y **varios profesionales**, donde cada profesional ve únicamente sus propios pacientes — ni siquiera otro profesional de la misma sede puede verlos.
|
||||
El sistema soporta varias sedes y varios profesionales, donde cada profesional ve únicamente sus propios pacientes — ni siquiera otro profesional de la misma sede puede verlos.
|
||||
|
||||
### Los tres niveles de acceso
|
||||
|
||||
| | **Desarrollador** | **Profesional** | **Administrativa** |
|
||||
|---|---|---|---|
|
||||
| Crear/desactivar sedes | Sí | No | No |
|
||||
| Crear/desactivar usuarios | Sí | No | No |
|
||||
| Crear/renombrar/desactivar sedes | Sí | No | No |
|
||||
| Crear/editar/desactivar usuarios | Sí | No | No |
|
||||
| Gestionar licencias de profesionales | Sí | No | No |
|
||||
| Ver pacientes y agenda | No | Sí (los propios) | Sí (de un profesional elegido) |
|
||||
| Crear pacientes (contacto) | No | Sí | Sí |
|
||||
| Ver motivo, patología, síntomas, sesiones | No | Sí | **No** |
|
||||
| Ver motivo, patología, síntomas, sesiones | No | Sí | No |
|
||||
| Editar / eliminar legajos, sesiones | No | Sí | No |
|
||||
| Exportar PDF, ver estadísticas e historial | No | Sí | No |
|
||||
|
||||
- **El Desarrollador** entra por una puerta separada (botón "Soy el Desarrollador" en la pantalla de acceso) con su propia clave de 4 números. No ve ningún paciente — su única función es organizar sedes y dar de alta o baja a las personas que sí van a usar el sistema día a día.
|
||||
- **El profesional** entra eligiendo su sede y su nombre, y después su PIN. Ve y gestiona solo los pacientes que él mismo creó (o que el Desarrollador le haya dejado asociados).
|
||||
- **La administrativa** entra de la misma forma, pero además tiene que indicar **a nombre de qué profesional** está trabajando en ese momento (si en la sede hay varios médicos, esto evita que el sistema confunda a quién pertenecen los pacientes que ella gestiona). Ve la agenda y los datos de contacto de los pacientes de ese profesional, pero nunca el contenido clínico. Una vez adentro, en la topbar y en el cartel de bienvenida aparece el nombre del profesional (no el de la administrativa) — así no se "mezclan" visualmente las cuentas. Internamente el sistema sigue sabiendo que fue ella quien hizo cada acción, para que el historial de cambios siga siendo preciso.
|
||||
- **El Desarrollador** entra por un acceso discreto, separado del login normal (ver más abajo), con su propia clave de 4 números. No ve ningún paciente — su función es organizar sedes, dar de alta o baja a las personas que usan el sistema, y gestionar sus licencias de acceso.
|
||||
- **El profesional** entra eligiendo su sede y su nombre, y después su PIN. Ve y gestiona solo los pacientes que él mismo creó (o que el Desarrollador le haya asignado).
|
||||
- **La administrativa** entra igual, pero además indica a nombre de qué profesional está trabajando ese momento. Ve agenda y contacto, nunca contenido clínico.
|
||||
|
||||
### Cómo acceder como Desarrollador
|
||||
|
||||
La pantalla de acceso normal no menciona que existe un rol de Desarrollador, a propósito. En la esquina superior derecha de la pantalla hay un botón pequeño y discreto que dice **"Mantenimiento"** — tocalo para ingresar tu clave de Desarrollador.
|
||||
|
||||
### Gestionar sedes y usuarios (como Desarrollador)
|
||||
|
||||
Desde el panel de Desarrollador (al que entrás con tu clave) tenés varias pestañas:
|
||||
Desde el panel de Desarrollador tenés estas pestañas:
|
||||
|
||||
- **Sedes**: crear sedes nuevas o desactivar las que ya no usás.
|
||||
- **Usuarios**: agregar profesionales o administrativas, elegir en qué sede(s) atienden, y asignarles su PIN. También podés quitarles el acceso cuando haga falta (no se borra su historial, solo deja de poder entrar), o gestionar a qué sedes pertenece cada uno.
|
||||
- **Historial de cambios**: acá el Desarrollador ve **todo** el historial de todos los profesionales (es el único rol con esa vista global; cada profesional, desde su propio panel, solo ve el historial de sus propios pacientes).
|
||||
- **Sedes**: crear sedes nuevas, renombrarlas (el cambio se refleja automáticamente en todos los legajos existentes, sin perder ni mover nada — pacientes y profesionales se vinculan por un identificador interno, no por el texto del nombre), o desactivarlas.
|
||||
- **Usuarios**: agregar profesionales (con su legajo completo: título, nombre, DNI, fecha y lugar de nacimiento, especialidad, contacto, sede y licencia) o administrativas (alta simple). Cada profesional recibe un número de legajo automático con formato `LG-2026-001`. Desde acá también podés editar el legajo de un profesional ya creado, gestionar su licencia (activarla por 7 a 120 días o indeterminada, pausarla o prohibirla), cambiar su PIN sin recrearlo, restaurar el acceso de alguien desactivado, y buscar por nombre, DNI o número de legajo en un único buscador.
|
||||
- **Historial de cambios**: el Desarrollador ve todo el historial de todos los profesionales, con un filtro opcional por tipo de entidad.
|
||||
- **Versión del sistema**: compara los archivos del servidor contra la última actualización entregada.
|
||||
- **Reportes por sede**: resumen de cada sede (profesionales, pacientes, actividad del mes).
|
||||
- **Papelera**: ver los legajos que eliminó cualquier profesional y, si hace falta, recuperarlos asignándolos a otro profesional de la misma sede — por ejemplo, si un profesional deja de trabajar en una sede y otro va a continuar atendiendo a sus pacientes. Para usarla: elegí primero la sede, después el profesional que eliminó el legajo, y vas a ver su papelera. Al tocar "Recuperar" en un paciente, elegís a cuál de los profesionales de esa misma sede se lo querés asignar — el legajo vuelve a aparecer activo, con todas sus sesiones, a nombre del profesional que elegiste. Si lo asignaste a alguien distinto del profesional que lo tenía antes, le va a quedar un aviso permanente en la ficha del paciente, indicando de qué profesional venía ese legajo.
|
||||
- **Legajos huérfanos**: distinto de la papelera — acá ves los pacientes que **siguen activos** (nunca se eliminaron) de un profesional al que le **quitaste el acceso** ("Quitar acceso" en la pestaña Usuarios). Como ese profesional ya no puede entrar al sistema, sus pacientes quedarían sin nadie que los gestione, a menos que los transfieras a otro profesional de la misma sede desde esta pestaña. Funciona igual que la papelera: elegís el profesional desactivado, ves sus pacientes, y tocás "Transferir" para asignárselos a otro.
|
||||
- **Papelera**: recuperar legajos eliminados, asignándolos a otro profesional de la misma sede donde estaban.
|
||||
- **Legajos huérfanos**: transferir pacientes activos de un profesional desactivado a otro de la misma sede.
|
||||
- **Calendario de licencias**: vista de calendario mensual con cada profesional marcado en el día exacto en que vence su licencia, para planificar renovaciones con anticipación. En la pestaña Sedes también aparece un aviso si alguna licencia vence dentro de los próximos 7 días.
|
||||
|
||||
---
|
||||
|
||||
## Instalar como app
|
||||
## ¿Qué hace cada parte del sistema (vista del profesional)?
|
||||
|
||||
Del Austral se puede "instalar" tanto en celular como en computadora, para que tenga su propio ícono y se abra como una aplicación, sin las barras del navegador. No es una app de tienda (no pasa por Google Play ni App Store) — se instala directo desde el navegador, y funciona exactamente igual que la versión web (necesita conexión a internet, no funciona sin ella).
|
||||
- **Crear legajo**: registra un paciente con sus datos, obra social y, opcionalmente, las primeras sesiones.
|
||||
- **Acceder a legajos**: buscá por DNI, nombre, fecha, obra social o sede. Desde la ficha podés editar datos, cambiar de sede, agregar/editar/eliminar sesiones, usar plantillas de evolución, agendar citas, mandar recordatorio de turno por WhatsApp, subir/descargar adjuntos, y exportar a PDF.
|
||||
- **Firma digital**: desde "Mi legajo" → "Mi firma", podés dibujarla con el mouse o el dedo, o subir una foto de tu firma real. Una vez guardada, se inserta automáticamente al pie de cada legajo que exportes a PDF.
|
||||
- **Agenda**: calendario mensual con tus citas, más un resumen de próximas citas en el panel principal.
|
||||
- **"Hoy tenés X consultas"**: franja en el panel principal con la cantidad de consultas de hoy que todavía no llegó su hora.
|
||||
- **Aviso de confirmaciones y cancelaciones**: cartel con la cantidad de novedades cuando un paciente confirma o cancela desde el link de WhatsApp.
|
||||
- **Pacientes que podrían estar abandonando el seguimiento**: en "Estadísticas", una sección compara cuánto pasó desde la última sesión de cada paciente contra su propio ritmo habitual de visitas — si un paciente que solía venir cada 2 semanas ya lleva más de un mes sin sesión, aparece marcado ahí. Para pacientes con poca historia, usa una regla de respaldo de 60 días sin actividad.
|
||||
- **Pacientes sin sesiones recientes** y **próximos cumpleaños**: resúmenes en el panel principal.
|
||||
- **Estadísticas**: pacientes totales, sesiones del mes, citas por estado, distribución por obra social, y un botón para descargar un backup completo de todos tus legajos.
|
||||
- **Eliminar legajos**: queda guardado en la papelera, recuperable más adelante.
|
||||
- **Mi legajo**: tus propios datos profesionales en modo de solo lectura, con tu número de legajo y el estado de tu licencia.
|
||||
|
||||
**En Android (Chrome):**
|
||||
1. Entrá al sitio normalmente.
|
||||
2. Va a aparecer un cartelito abajo ofreciendo "Instalar app", o podés tocar los tres puntos (⋮) arriba a la derecha → **"Instalar app"** o **"Agregar a pantalla principal"**.
|
||||
3. Confirmá. Va a aparecer el ícono de Del Austral entre tus apps, como cualquier otra.
|
||||
---
|
||||
|
||||
**En iPhone/iPad (Safari):**
|
||||
1. Entrá al sitio normalmente.
|
||||
2. Tocá el botón de compartir (el cuadrado con la flecha hacia arriba).
|
||||
3. Buscá **"Agregar a pantalla de inicio"** y confirmá.
|
||||
## Instalar como app (PWA)
|
||||
|
||||
**En computadora (Chrome o Edge):**
|
||||
1. Entrá al sitio.
|
||||
2. En la barra de direcciones, a la derecha, va a aparecer un ícono de instalación (una pantalla con una flecha). Hacé clic ahí, o desde el menú (⋮) buscá **"Instalar Del Austral"**.
|
||||
3. Se va a abrir en su propia ventana, con su propio ícono en el escritorio o en la barra de tareas.
|
||||
Del Austral se puede "instalar" en celular o computadora para que tenga su propio ícono y se abra sin las barras del navegador. No pasa por ninguna tienda de apps.
|
||||
|
||||
Si no te aparece la opción de instalar, puede ser que el sitio no esté funcionando bajo HTTPS (revisá que tengas el candadito en la barra de direcciones; si no, activá el AutoSSL de cPanel mencionado en la sección de seguridad).
|
||||
- **Android (Chrome)**: menú → "Instalar app" o "Agregar a pantalla principal".
|
||||
- **iPhone/iPad (Safari)**: botón de compartir → "Agregar a pantalla de inicio".
|
||||
- **Computadora (Chrome/Edge)**: ícono de instalación en la barra de direcciones, o menú → "Instalar Del Austral".
|
||||
|
||||
Si no aparece la opción de instalar, confirmá que el sitio funcione con HTTPS.
|
||||
|
||||
---
|
||||
|
||||
## Verificación de versión (para el Desarrollador)
|
||||
|
||||
Cada vez que te entreguemos una actualización, vas a recibir junto con los archivos un **`version.json`** nuevo. Subilo siempre junto con el resto — es el que le dice al sistema "esta es la versión correcta esperada".
|
||||
Cada actualización viene con un `version.json` nuevo — subilo siempre junto con el resto. Para revisar si todo quedó bien actualizado: entrá como Desarrollador → pestaña "Versión del sistema" → "Revisar ahora". Un cartel verde confirma que todo coincide; uno rojo señala exactamente qué archivo quedó con una versión vieja.
|
||||
|
||||
Para revisar si todo quedó bien actualizado:
|
||||
|
||||
1. Entrá como Desarrollador.
|
||||
2. En el panel, pestaña **"Versión del sistema"**.
|
||||
3. Vas a ver un cartel verde si todo coincide con la última actualización, o uno rojo si algún archivo quedó con una versión vieja — con el detalle de cuál archivo específico tiene el problema.
|
||||
|
||||
Esto compara el contenido real de los archivos del servidor contra lo que te entregamos, así que es la forma más confiable de confirmar una actualización sin tener que ir abriendo archivo por archivo en el editor de cPanel (como tuvimos que hacer alguna vez antes de tener esta pantalla).
|
||||
Un problema común: si pegás el contenido de un archivo a mano dentro del editor de texto del hosting (en vez de subir el archivo real), a veces se cambian detalles invisibles que alteran el archivo sin que se note, y la verificación marca "versión vieja" aunque se vea idéntico. Si pasa esto, borrá el archivo del servidor y subilo de nuevo con el botón de "Cargar/Subir", en vez de copiar y pegar texto.
|
||||
|
||||
---
|
||||
|
||||
## Si falla la subida de archivos adjuntos
|
||||
|
||||
El sistema permite adjuntar PDF o imágenes de hasta 15 MB por archivo, pero ese límite también depende de la configuración propia de PHP en tu hosting (`upload_max_filesize` y `post_max_size`). Si tu hosting tiene un límite más bajo (algo común en planes básicos, donde el default suele ser 2 MB u 8 MB), vas a ver un mensaje claro indicando el límite real del servidor.
|
||||
El sistema permite adjuntar PDF o imágenes de hasta 15 MB, pero ese límite también depende de la configuración de PHP en tu hosting (`upload_max_filesize` y `post_max_size`). Si tu hosting tiene un límite más bajo, vas a ver un mensaje claro con el límite real del servidor. Para subirlo: en cPanel, "MultiPHP INI Editor"; en VPS, editá el `php.ini` y reiniciá PHP-FPM.
|
||||
|
||||
Para subirlo, en cPanel buscá **"Seleccionar versión de PHP"** o **"MultiPHP INI Editor"** → ahí podés aumentar `upload_max_filesize` y `post_max_size` a, por ejemplo, 20M cada uno (siempre poné `post_max_size` igual o más grande que `upload_max_filesize`). Si no encontrás esa opción, contactá al soporte de tu hosting y pedíselo directamente.
|
||||
---
|
||||
|
||||
## Sincronizar el servidor con un repositorio Git (opcional, para VPS)
|
||||
|
||||
Si tenés tu propio Gitea, GitHub u otro servidor Git, podés mantener un respaldo automático con historial de cada cambio en el servidor:
|
||||
|
||||
1. Convertí la carpeta del proyecto en un repositorio git y conectalo con tu servidor remoto.
|
||||
2. Asegurate de que `config/config.php` esté en el `.gitignore` (ya viene así) para no subir tus credenciales reales nunca.
|
||||
3. Usá un token de acceso personal en vez de tu contraseña real.
|
||||
4. Un script simple en cron, corriendo periódicamente, puede hacer commit y push solo si detecta cambios reales.
|
||||
|
||||
Esto es opcional y pensado para quien ya tiene experiencia con git.
|
||||
|
||||
---
|
||||
|
||||
## Sobre la seguridad de los datos
|
||||
|
||||
Esto maneja datos clínicos de pacientes, así que algunas recomendaciones:
|
||||
|
||||
- Activá el **certificado SSL gratuito** de tu cPanel (sección "SSL/TLS Status" → "Run AutoSSL") para que el sitio funcione con `https://` y no `http://`.
|
||||
- Hacé backups periódicos de la base de datos desde phpMyAdmin, y también de la carpeta `adjuntos/` (los archivos subidos no están en la base de datos).
|
||||
- Los PIN y la clave de Desarrollador se guardan encriptados (hash), nunca en texto plano.
|
||||
- El aislamiento entre profesionales no depende solo de la pantalla: está aplicado en el servidor, así que aunque alguien intente forzar una URL con el ID de un paciente de otro profesional, el sistema no lo va a mostrar.
|
||||
- La carpeta `adjuntos/` tiene un archivo `.htaccess` que impide que cualquier archivo subido se ejecute como código.
|
||||
- El link de confirmación de turno (`confirmar_turno.php`) es público a propósito —es lo que permite que el paciente confirme sin loguearse— pero solo expone los datos de esa cita puntual (fecha, hora, motivo), nunca información clínica. Cada link es único e impredecible (un código largo generado al azar), así que no se puede adivinar el de otro paciente.
|
||||
- Activá el certificado SSL (AutoSSL en cPanel, o Certbot en VPS).
|
||||
- Hacé backups periódicos de la base de datos y de la carpeta `adjuntos/`.
|
||||
- Los PIN y la clave de Desarrollador se guardan encriptados, nunca en texto plano.
|
||||
- El aislamiento entre profesionales está aplicado en el servidor, no solo en la pantalla.
|
||||
- La carpeta `adjuntos/` tiene un `.htaccess` que impide ejecutar archivos subidos como código.
|
||||
- El link de confirmación de turno es público a propósito, pero solo expone datos de esa cita puntual, y cada link es único e impredecible.
|
||||
- El acceso de Desarrollador no se anuncia en la pantalla de login.
|
||||
|
||||
Cualquier ajuste que necesites lo podemos ir sumando.
|
||||
|
|
|
|||
357
README.md
357
README.md
|
|
@ -1,172 +1,185 @@
|
|||
# Del Austral
|
||||
|
||||
[](./LICENSE)
|
||||
|
||||
**Historial clínico digital multi-sede para consultorios de salud.** Sistema web autoalojado (PHP + MySQL) pensado para profesionales de la salud —médicos, fonoaudiólogos, nutricionistas, psicólogos y afines— que necesitan reemplazar las fichas de papel sin depender de un SaaS de terceros ni pagar licencias mensuales. Soporta varias sedes y varios profesionales con aislamiento total de datos entre ellos.
|
||||
|
||||
Pensado para correr en hosting compartido tipo cPanel, sin necesidad de acceso SSH, Composer ni extensiones especiales del servidor.
|
||||
|
||||
---
|
||||
|
||||
## Índice
|
||||
|
||||
- [Características](#características)
|
||||
- [Stack técnico](#stack-técnico)
|
||||
- [Estructura del proyecto](#estructura-del-proyecto)
|
||||
- [Instalación](#instalación)
|
||||
- [Sedes y roles de usuario](#sedes-y-roles-de-usuario)
|
||||
- [Modelo de datos](#modelo-de-datos)
|
||||
- [Seguridad](#seguridad)
|
||||
- [Hoja de ruta](#hoja-de-ruta)
|
||||
- [Licencia](#licencia)
|
||||
|
||||
---
|
||||
|
||||
## Características
|
||||
|
||||
- 🔐 **Acceso por PIN numérico** (4 dígitos) en vez de usuario/contraseña tradicional.
|
||||
- 🏥 **Multi-sede**: un consultorio puede tener varias sucursales; cada paciente pertenece a una sede y se puede migrar a otra.
|
||||
- 👥 **Multi-profesional con aislamiento total**: cada profesional ve únicamente sus propios pacientes, incluso compartiendo sede con otros médicos. El filtrado se aplica en el servidor, no solo en la interfaz.
|
||||
- 🛠️ **Rol Desarrollador**: nivel de acceso separado, sin visibilidad de pacientes, dedicado exclusivamente a crear sedes y dar de alta/baja profesionales y administrativas.
|
||||
- 🧑💼 **Rol Administrativa**: gestiona agenda y contacto de los pacientes de un profesional elegido al iniciar sesión, sin acceso a contenido clínico.
|
||||
- 📋 **Legajos de pacientes** completos: datos personales, obra social (catálogo editable), motivo de consulta, patología, síntomas, observaciones.
|
||||
- 🗓️ **Sesiones clínicas** editables y eliminables, con historial cronológico, y **plantillas de evolución** propias de cada profesional.
|
||||
- 📅 **Agenda con calendario mensual** y **detección de choque de horario** (no se puede agendar dos turnos a la misma fecha/hora para el mismo profesional, sin importar la sede).
|
||||
- ✅ **Confirmación de turno por el paciente**: link público único (sin login) donde el paciente confirma o cancela su cita.
|
||||
- 💬 **Recordatorio de turnos por WhatsApp**, con el link de confirmación incluido en el mensaje.
|
||||
- 📎 **Archivos adjuntos** por paciente (PDF e imágenes, hasta 15 MB), servidos desde una carpeta protegida.
|
||||
- 📄 **Exportación de legajo a PDF** vía vista de impresión del navegador.
|
||||
- 🎂 Resumen de **próximos cumpleaños** de pacientes.
|
||||
- 📊 **Dashboard de estadísticas** por profesional: pacientes activos, sesiones por mes, distribución por obra social, citas por estado.
|
||||
- 🧾 **Historial de cambios (auditoría)**: quién creó, editó o eliminó cada legajo, y cuándo — acotado a los propios pacientes de cada profesional.
|
||||
- 🗑️ **Papelera / base histórica**: los legajos eliminados quedan archivados como JSON y son consultables.
|
||||
- ⚖️ Aviso de protección de datos personales conforme a la **Ley 25.326** (Argentina).
|
||||
- 📱 **App instalable (PWA)**: se puede instalar en celular o computadora con su propio ícono, sin pasar por una tienda de aplicaciones.
|
||||
- 🔍 **Verificación de versión**: el Desarrollador puede comparar los archivos del servidor contra la última actualización entregada, para detectar archivos desactualizados tras una subida incompleta.
|
||||
- 📊 **Reportes por sede**: el Desarrollador ve, por cada sede activa, cuántos profesionales/administrativas atienden ahí, pacientes totales y actividad del mes.
|
||||
- 💾 **Exportación masiva**: cada profesional puede descargar un backup propio en JSON con todos sus pacientes, sesiones, citas y metadata de adjuntos.
|
||||
- ♻️ **Papelera global** (Desarrollador): ver y recuperar legajos eliminados por cualquier profesional, asignándolos a otro profesional de la misma sede — útil cuando un profesional deja la sede y otro continúa con sus pacientes.
|
||||
|
||||
## Stack técnico
|
||||
|
||||
| Capa | Tecnología |
|
||||
|---|---|
|
||||
| Backend | PHP 7.4+ puro (sin frameworks, sin Composer) |
|
||||
| Base de datos | MySQL / MariaDB (InnoDB, utf8mb4) |
|
||||
| Frontend | HTML, CSS y JavaScript vanilla (sin build step, sin npm) |
|
||||
| Autenticación | PIN hasheado con `password_hash` (bcrypt) + sesiones nativas de PHP |
|
||||
| Exportación PDF | Vista HTML de impresión (`window.print()`), sin librerías externas |
|
||||
|
||||
No requiere `composer install`, `npm install` ni proceso de build. Se sube por FTP o el Administrador de archivos de cPanel y funciona.
|
||||
|
||||
## Estructura del proyecto
|
||||
|
||||
```
|
||||
del-austral/
|
||||
├── index.html # SPA: toda la interfaz vive acá (templates + vistas)
|
||||
├── exportar.php # Vista de impresión/exportación de legajo a PDF
|
||||
├── confirmar_turno.php # Página pública de confirmación de turno (sin login)
|
||||
├── manifest.json # Metadata de la PWA (nombre, iconos, colores)
|
||||
├── sw.js # Service worker mínimo, solo para habilitar la instalación
|
||||
├── version.json # Hashes de referencia para la verificación de versión
|
||||
├── database.sql # Esquema completo (instalación nueva, desde cero)
|
||||
├── migracion_v2.sql # Agrega: citas, archivos adjuntos, plantillas
|
||||
├── migracion_v3.sql # Agrega: usuarios con roles, historial de cambios
|
||||
├── migracion_v4.sql # Cambia el acceso de patrón dibujado a PIN numérico
|
||||
├── migracion_v5.sql # Agrega: sedes, aislamiento por profesional, rol Desarrollador
|
||||
├── migracion_v6.sql # Agrega: aviso de confirmaciones/cancelaciones de turno
|
||||
├── migracion_v7.sql # Agrega: profesional/sede original a la papelera (recuperación cruzada)
|
||||
├── config/
|
||||
│ └── config.php # Credenciales de BD + helpers de sesión/rol/auditoría
|
||||
├── api/
|
||||
│ ├── auth.php # Login multi-paso, Desarrollador, sedes, usuarios
|
||||
│ ├── pacientes.php # CRUD de legajos, sesiones, búsqueda, migración de sede
|
||||
│ ├── citas.php # Agenda, choque de horario, cumpleaños, inactivos
|
||||
│ ├── adjuntos.php # Subida/descarga de archivos por paciente
|
||||
│ ├── plantillas.php # CRUD de plantillas de evolución (por profesional)
|
||||
│ ├── obras_sociales.php # Catálogo de obras sociales
|
||||
│ └── admin.php # Estadísticas, historial de cambios, verificación de versión
|
||||
├── assets/
|
||||
│ ├── css/estilos.css # Toda la hoja de estilos
|
||||
│ ├── js/app.js # Toda la lógica de frontend
|
||||
│ └── icons/ # Íconos de la PWA en distintos tamaños
|
||||
└── adjuntos/ # Carpeta de archivos subidos (protegida con .htaccess)
|
||||
```
|
||||
|
||||
## Instalación
|
||||
|
||||
Guía completa paso a paso (creación de base de datos en cPanel, configuración de `config.php`, subida de archivos, migración desde versiones anteriores) en **[`INSTRUCCIONES_INSTALACION.md`](./INSTRUCCIONES_INSTALACION.md)**.
|
||||
|
||||
Resumen rápido para una instalación nueva:
|
||||
|
||||
1. Creá una base de datos MySQL en tu hosting e importá `database.sql` desde phpMyAdmin.
|
||||
2. Completá `config/config.php` con tus credenciales de base de datos y un `APP_SECRET` propio.
|
||||
3. Subí todo el proyecto a tu hosting (FTP o Administrador de archivos de cPanel), incluyendo la carpeta `adjuntos/`.
|
||||
4. Abrí el sitio: la primera vez te va a pedir crear la clave de Desarrollador, y luego tu primera sede y profesional.
|
||||
|
||||
Si venís de una versión anterior con pacientes ya cargados, corré las migraciones en orden (`migracion_v2.sql` → `migracion_v3.sql` → `migracion_v4.sql` → `migracion_v5.sql`), sin saltarte ninguna. Ningún script borra pacientes, sesiones, citas ni adjuntos.
|
||||
|
||||
## Sedes y roles de usuario
|
||||
|
||||
| | Desarrollador | Profesional | Administrativa |
|
||||
|---|---|---|---|
|
||||
| Crear/desactivar sedes y usuarios | ✅ | ❌ | ❌ |
|
||||
| Ver pacientes y agenda | ❌ | ✅ (propios) | ✅ (de un profesional elegido) |
|
||||
| Crear paciente (datos de contacto) | ❌ | ✅ | ✅ |
|
||||
| Motivo, patología, síntomas, sesiones | ❌ | ✅ | ❌ |
|
||||
| Editar / eliminar legajo o sesión | ❌ | ✅ | ❌ |
|
||||
| Exportar PDF, estadísticas, historial | ❌ | ✅ | ❌ |
|
||||
|
||||
- El **Desarrollador** entra con una clave separada (no elige sede ni aparece en el login normal) y solo gestiona sedes y altas/bajas de usuarios.
|
||||
- Cada **profesional** ve exclusivamente los pacientes que le pertenecen (`pacientes.profesional_id`), incluso si comparte sede con otros profesionales.
|
||||
- La **administrativa**, al iniciar sesión, además de elegir sede y PIN debe indicar a qué profesional representa — eso determina qué pacientes ve (filtrados, sin contenido clínico).
|
||||
|
||||
El filtrado por rol y por dueño de cada paciente se aplica en cada endpoint de `api/`, no solo ocultando elementos del HTML — un profesional no puede ver ni manipular los pacientes de otro aunque conozca o adivine su ID.
|
||||
|
||||
## Modelo de datos
|
||||
|
||||
Tablas principales (ver `database.sql` para el esquema completo con índices y claves foráneas):
|
||||
|
||||
- `sedes` — sucursales del consultorio
|
||||
- `desarrollador` — clave única del rol Desarrollador
|
||||
- `usuarios` — accesos al sistema (PIN hasheado, rol, estado activo/inactivo)
|
||||
- `usuarios_sedes` — relación N a N entre usuarios y las sedes donde atienden
|
||||
- `pacientes` — legajo principal, con `profesional_id` (dueño) y `sede_id`
|
||||
- `sesiones` — historial clínico cronológico por paciente, editable
|
||||
- `citas` — agenda, con `profesional_id`, token de confirmación pública y detección de choque de horario
|
||||
- `archivos_adjuntos` — metadata de archivos subidos (PDF/imágenes)
|
||||
- `plantillas_evolucion` — textos reutilizables por profesional (no compartidos)
|
||||
- `obras_sociales` — catálogo editable de coberturas de salud, compartido
|
||||
- `legajos_eliminados` — papelera / base histórica (JSON del legajo completo al eliminarlo)
|
||||
- `historial_cambios` — auditoría de acciones (quién, qué, cuándo)
|
||||
|
||||
## Seguridad
|
||||
|
||||
- Los PIN y la clave de Desarrollador se almacenan con `password_hash()` (bcrypt) combinados con un secreto de aplicación (`APP_SECRET`); nunca en texto plano.
|
||||
- Protección básica contra fuerza bruta: bloqueo temporal tras 5 intentos fallidos, tanto para el login normal como para el de Desarrollador.
|
||||
- Todos los endpoints en `api/` requieren sesión autenticada; los que exponen datos clínicos exigen además rol *profesional*, y siempre filtran por el profesional dueño de cada registro.
|
||||
- El link público de confirmación de turno usa un token aleatorio de 20 bytes — no expone contenido clínico, solo fecha/hora/motivo de la cita puntual.
|
||||
- La carpeta `adjuntos/` incluye un `.htaccess` que impide la ejecución de scripts, aunque se suba un archivo con extensión engañosa.
|
||||
- Validación de tipo MIME real (no solo por extensión) al subir archivos.
|
||||
- Se recomienda servir el sitio bajo HTTPS (AutoSSL de cPanel es gratuito) dado que se transmiten datos de salud.
|
||||
|
||||
> Este proyecto no ha pasado por una auditoría de seguridad profesional ni un pentest formal. Es razonablemente seguro para el caso de uso de un consultorio pequeño o mediano, pero quien lo despliegue es responsable de evaluar si cumple los requisitos normativos de su jurisdicción (en Argentina, Ley 25.326 de Protección de Datos Personales) antes de usarlo con datos reales de pacientes.
|
||||
|
||||
## Hoja de ruta
|
||||
|
||||
Ideas pendientes, sin compromiso de implementación:
|
||||
|
||||
- [ ] Firma con imagen escaneada en el PDF exportado (hoy es solo nombre y título en texto)
|
||||
- [ ] Recuperación de PIN olvidado sin pasar por phpMyAdmin
|
||||
- [ ] Buscador único en el panel principal (DNI/nombre → resultado directo)
|
||||
- [ ] Notificaciones push o por email para recordatorios de turno
|
||||
- [ ] Exportación de estadísticas a Excel/CSV
|
||||
- [ ] Internacionalización (hoy todos los textos están en español rioplatense)
|
||||
|
||||
## Licencia
|
||||
|
||||
[PolyForm Noncommercial 1.0.0](./LICENSE) — código fuente disponible para cualquier uso **no comercial**: estudio personal, proyectos propios sin fines de lucro, instituciones educativas, organizaciones de salud pública, ONGs y organismos de gobierno. Para uso comercial (ofrecerlo como servicio, instalarlo a terceros a cambio de un pago, integrarlo en un producto comercial) se necesita una licencia aparte — abrí un issue o contactá a quien mantiene este repositorio.
|
||||
|
||||
---
|
||||
|
||||
Construido de forma iterativa junto a [Claude](https://claude.ai) (Anthropic).
|
||||
# Del Austral
|
||||
|
||||
[](./LICENSE)
|
||||
|
||||
**Historial clínico digital multi-sede para consultorios de salud.** Sistema web autoalojado (PHP + MySQL) pensado para profesionales de la salud —médicos, fonoaudiólogos, nutricionistas, psicólogos y afines— que necesitan reemplazar las fichas de papel sin depender de un SaaS de terceros ni pagar licencias mensuales. Soporta varias sedes y varios profesionales con aislamiento total de datos entre ellos.
|
||||
|
||||
Corre tanto en hosting compartido tipo cPanel (sin SSH, Composer ni extensiones especiales) como en un VPS propio con Nginx + PHP-FPM + MariaDB.
|
||||
|
||||
---
|
||||
|
||||
## Índice
|
||||
|
||||
- [Características](#características)
|
||||
- [Stack técnico](#stack-técnico)
|
||||
- [Estructura del proyecto](#estructura-del-proyecto)
|
||||
- [Instalación](#instalación)
|
||||
- [Sedes, profesionales y roles](#sedes-profesionales-y-roles)
|
||||
- [Modelo de datos](#modelo-de-datos)
|
||||
- [Seguridad](#seguridad)
|
||||
- [Hoja de ruta](#hoja-de-ruta)
|
||||
- [Licencia](#licencia)
|
||||
|
||||
---
|
||||
|
||||
## Características
|
||||
|
||||
- 🔐 **Acceso por PIN numérico** (4 dígitos) en vez de usuario/contraseña tradicional.
|
||||
- 🏥 **Multi-sede**: un consultorio puede tener varias sucursales, renombrables en cualquier momento sin perder ni mover ningún legajo existente.
|
||||
- 👥 **Multi-profesional con aislamiento total**: cada profesional ve únicamente sus propios pacientes, incluso compartiendo sede con otros. El filtrado se aplica en el servidor, no solo en la interfaz.
|
||||
- 🛠️ **Rol Desarrollador**: nivel de acceso separado y discreto (sin mención en la pantalla de login pública), dedicado a crear/renombrar sedes y gestionar profesionales y administrativas.
|
||||
- 🪪 **Legajos completos de profesionales**: título, nombre, DNI, fecha y lugar de nacimiento, especialidad, contacto, y un número de legajo automático con formato `LG-2026-001`.
|
||||
- ⏳ **Sistema de licencias por profesional**: activa por 7/15/30/60/90/120 días o indeterminada, pausada o prohibida. Si vence, el acceso se bloquea solo, con un mensaje claro al intentar entrar.
|
||||
- 📅 **Calendario de vencimientos de licencia** y aviso anticipado de los que vencen en la próxima semana, para el Desarrollador.
|
||||
- 🧑💼 **Rol Administrativa**: gestiona agenda y contacto de los pacientes de un profesional elegido al iniciar sesión, sin acceso a contenido clínico.
|
||||
- 📋 **Legajos de pacientes** completos: datos personales, obra social, motivo de consulta, patología, síntomas, observaciones.
|
||||
- 🗓️ **Sesiones clínicas** editables y eliminables, con historial cronológico, y plantillas de evolución propias de cada profesional.
|
||||
- 📅 **Agenda con calendario mensual** y detección de choque de horario.
|
||||
- ✅ **Confirmación de turno por el paciente**: link público único (sin login) para confirmar o cancelar.
|
||||
- 💬 **Recordatorio de turnos por WhatsApp**, con el link de confirmación incluido.
|
||||
- 📎 **Archivos adjuntos** por paciente (PDF e imágenes, hasta 15 MB), con validación real de tipo MIME.
|
||||
- ✍️ **Firma digital del profesional**: dibujada con mouse/dedo o subida como imagen, se inserta automáticamente al pie de cada legajo exportado a PDF.
|
||||
- 📄 **Exportación de legajo a PDF** vía vista de impresión del navegador.
|
||||
- ⚠️ **Detección de pacientes en riesgo de abandono**, comparando cuánto pasó desde la última sesión contra el ritmo histórico propio de cada paciente (no una regla fija igual para todos).
|
||||
- 🎂 Resumen de próximos cumpleaños y pacientes sin sesiones recientes.
|
||||
- 📊 **Dashboard de estadísticas** por profesional: pacientes activos, sesiones por mes, distribución por obra social, citas por estado.
|
||||
- 🧾 **Historial de cambios (auditoría)**: acotado a los propios pacientes de cada profesional; el Desarrollador ve el historial completo de todos, con filtro por tipo de entidad.
|
||||
- 🗑️ **Papelera** y **legajos huérfanos**: recuperar legajos eliminados, o transferir pacientes activos de un profesional desactivado, asignándolos a otro de la misma sede.
|
||||
- ⚖️ Aviso de protección de datos personales conforme a la Ley 25.326 (Argentina).
|
||||
- 📱 **App instalable (PWA)**, sin pasar por ninguna tienda de aplicaciones.
|
||||
- 🔍 **Verificación de versión**: compara los archivos del servidor contra la última actualización entregada.
|
||||
- 📊 **Reportes por sede**, y **exportación masiva** de todos los legajos propios a un único archivo de respaldo.
|
||||
|
||||
## Stack técnico
|
||||
|
||||
| Capa | Tecnología |
|
||||
|---|---|
|
||||
| Backend | PHP 7.4+ puro (sin frameworks, sin Composer) |
|
||||
| Base de datos | MySQL / MariaDB (InnoDB, utf8mb4) |
|
||||
| Frontend | HTML, CSS y JavaScript vanilla (sin build step, sin npm) |
|
||||
| Autenticación | PIN hasheado con `password_hash` (bcrypt) + sesiones nativas de PHP |
|
||||
| Exportación PDF | Vista HTML de impresión (`window.print()`), sin librerías externas |
|
||||
| Firma digital | `canvas` nativo del navegador, guardada como PNG en base64 |
|
||||
|
||||
No requiere `composer install`, `npm install` ni proceso de build. Se sube por FTP, el Administrador de archivos de cPanel, o se clona por git en un VPS.
|
||||
|
||||
## Estructura del proyecto
|
||||
|
||||
```
|
||||
del-austral/
|
||||
├── index.html # SPA: toda la interfaz vive acá (templates + vistas)
|
||||
├── exportar.php # Vista de impresión/exportación de legajo a PDF
|
||||
├── confirmar_turno.php # Página pública de confirmación de turno (sin login)
|
||||
├── manifest.json # Metadata de la PWA (nombre, iconos, colores)
|
||||
├── sw.js # Service worker mínimo, para habilitar la instalación
|
||||
├── version.json # Hashes de referencia para la verificación de versión
|
||||
├── database.sql # Esquema completo (instalación nueva, desde cero)
|
||||
├── migracion_v2.sql # Citas, archivos adjuntos, plantillas
|
||||
├── migracion_v3.sql # Usuarios con roles, historial de cambios
|
||||
├── migracion_v4.sql # Acceso por PIN numérico (en vez de patrón dibujado)
|
||||
├── migracion_v5.sql # Sedes, aislamiento por profesional, rol Desarrollador
|
||||
├── migracion_v6.sql # Aviso de confirmaciones/cancelaciones de turno
|
||||
├── migracion_v7.sql # Profesional/sede original en la papelera
|
||||
├── migracion_v8.sql # Aviso de "legajo recuperado de otro profesional"
|
||||
├── migracion_v9.sql # DNI único por profesional, no global
|
||||
├── migracion_v10.sql # Legajos completos de profesionales + sistema de licencias
|
||||
├── migracion_v11.sql # Número de legajo automático (LG-AAAA-NNN)
|
||||
├── migracion_v12.sql # Firma digital del profesional
|
||||
├── config/
|
||||
│ └── config.php # Credenciales de BD + helpers de sesión/rol/auditoría
|
||||
├── api/
|
||||
│ ├── auth.php # Login multi-paso, Desarrollador, sedes, usuarios, legajos
|
||||
│ ├── pacientes.php # CRUD de legajos, sesiones, búsqueda, migración de sede
|
||||
│ ├── citas.php # Agenda, choque de horario, cumpleaños, inactivos
|
||||
│ ├── adjuntos.php # Subida/descarga de archivos por paciente
|
||||
│ ├── plantillas.php # CRUD de plantillas de evolución (por profesional)
|
||||
│ ├── obras_sociales.php # Catálogo de obras sociales
|
||||
│ └── admin.php # Estadísticas, historial, versión, riesgo de abandono, calendario de licencias
|
||||
├── assets/
|
||||
│ ├── css/estilos.css # Toda la hoja de estilos
|
||||
│ ├── js/app.js # Toda la lógica de frontend
|
||||
│ └── icons/ # Íconos de la PWA en distintos tamaños
|
||||
└── adjuntos/ # Carpeta de archivos subidos (protegida con .htaccess)
|
||||
```
|
||||
|
||||
## Instalación
|
||||
|
||||
Guía completa paso a paso (cPanel y VPS, configuración de `config.php`, migración desde versiones anteriores) en **[`INSTRUCCIONES_INSTALACION.md`](./INSTRUCCIONES_INSTALACION.md)**.
|
||||
|
||||
Resumen rápido para una instalación nueva:
|
||||
|
||||
1. Creá una base de datos MySQL/MariaDB e importá `database.sql`.
|
||||
2. Completá `config/config.php` con tus credenciales y un `APP_SECRET` propio.
|
||||
3. Subí todo el proyecto a tu hosting o VPS, incluyendo la carpeta `adjuntos/`.
|
||||
4. Abrí el sitio: la primera vez te va a pedir crear la clave de Desarrollador, y luego tu primera sede y profesional.
|
||||
|
||||
Si venís de una versión anterior con pacientes ya cargados, corré las migraciones en orden (`migracion_v2.sql` hasta `migracion_v12.sql`), sin saltarte ninguna. Ningún script borra pacientes, sesiones, citas ni adjuntos.
|
||||
|
||||
## Sedes, profesionales y roles
|
||||
|
||||
| | Desarrollador | Profesional | Administrativa |
|
||||
|---|---|---|---|
|
||||
| Crear/renombrar/desactivar sedes y usuarios | ✅ | ❌ | ❌ |
|
||||
| Gestionar licencias de acceso | ✅ | ❌ | ❌ |
|
||||
| Ver pacientes y agenda | ❌ | ✅ (propios) | ✅ (de un profesional elegido) |
|
||||
| Crear paciente (datos de contacto) | ❌ | ✅ | ✅ |
|
||||
| Motivo, patología, síntomas, sesiones | ❌ | ✅ | ❌ |
|
||||
| Editar / eliminar legajo o sesión | ❌ | ✅ | ❌ |
|
||||
| Exportar PDF, estadísticas, historial | ❌ | ✅ | ❌ |
|
||||
|
||||
- El **Desarrollador** entra por un acceso discreto que no se menciona en la pantalla de login pública (un botón pequeño que dice "Mantenimiento"), con su propia clave separada. No ve pacientes; gestiona sedes, profesionales, administrativas y licencias.
|
||||
- Cada **profesional** ve exclusivamente los pacientes que le pertenecen (`pacientes.profesional_id`), incluso si comparte sede con otros profesionales. Su acceso puede estar activo (por tiempo determinado o indeterminado), pausado, prohibido o suspendido automáticamente al vencer la licencia.
|
||||
- La **administrativa**, al iniciar sesión, además de elegir sede y PIN debe indicar a qué profesional representa — eso determina qué pacientes ve, sin contenido clínico.
|
||||
|
||||
El filtrado por rol y por dueño de cada paciente se aplica en cada endpoint de `api/`, no solo ocultando elementos del HTML.
|
||||
|
||||
## Modelo de datos
|
||||
|
||||
Tablas principales (ver `database.sql` para el esquema completo con índices y claves foráneas):
|
||||
|
||||
- `sedes` — sucursales del consultorio
|
||||
- `desarrollador` — clave única del rol Desarrollador
|
||||
- `usuarios` — accesos al sistema (PIN hasheado, rol, estado activo/inactivo, estado y duración de licencia)
|
||||
- `usuarios_sedes` — relación N a N entre usuarios y las sedes donde atienden
|
||||
- `profesionales_legajos` — datos personales completos de cada profesional, número de legajo y firma digital
|
||||
- `pacientes` — legajo principal, con `profesional_id` (dueño), `sede_id` y rastro de procedencia si fue recuperado de otro profesional
|
||||
- `sesiones` — historial clínico cronológico por paciente, editable
|
||||
- `citas` — agenda, con `profesional_id`, token de confirmación pública y detección de choque de horario
|
||||
- `archivos_adjuntos` — metadata de archivos subidos (PDF/imágenes)
|
||||
- `plantillas_evolucion` — textos reutilizables por profesional (no compartidos)
|
||||
- `obras_sociales` — catálogo editable de coberturas de salud, compartido
|
||||
- `legajos_eliminados` — papelera, con el profesional y la sede original para poder recuperarlos
|
||||
- `historial_cambios` — auditoría de acciones (quién, qué, cuándo)
|
||||
|
||||
## Seguridad
|
||||
|
||||
- Los PIN y la clave de Desarrollador se almacenan con `password_hash()` (bcrypt) combinados con un secreto de aplicación (`APP_SECRET`); nunca en texto plano.
|
||||
- Protección básica contra fuerza bruta: bloqueo temporal tras 5 intentos fallidos.
|
||||
- Todos los endpoints en `api/` requieren sesión autenticada; los que exponen datos clínicos exigen además rol *profesional*, y siempre filtran por el profesional dueño de cada registro.
|
||||
- El link público de confirmación de turno usa un token aleatorio — no expone contenido clínico, solo fecha/hora/motivo de la cita puntual.
|
||||
- La carpeta `adjuntos/` incluye un `.htaccess` que impide la ejecución de scripts.
|
||||
- Validación de tipo MIME real (no solo por extensión) al subir archivos y al guardar la firma digital.
|
||||
- El acceso de Desarrollador no se anuncia en la pantalla de login pública.
|
||||
- Se recomienda servir el sitio bajo HTTPS dado que se transmiten datos de salud.
|
||||
|
||||
> Este proyecto no ha pasado por una auditoría de seguridad profesional ni un pentest formal. Es razonablemente seguro para el caso de uso de un consultorio pequeño o mediano, pero quien lo despliegue es responsable de evaluar si cumple los requisitos normativos de su jurisdicción (en Argentina, Ley 25.326 de Protección de Datos Personales) antes de usarlo con datos reales de pacientes.
|
||||
|
||||
## Hoja de ruta
|
||||
|
||||
Ideas pendientes, sin compromiso de implementación:
|
||||
|
||||
- [ ] Recuperación de PIN olvidado sin pasar por phpMyAdmin (hoy existe el cambio de PIN por el Desarrollador, pero no un flujo de autorrecuperación)
|
||||
- [ ] Buscador único en el panel principal del profesional (DNI/nombre → resultado directo, sin entrar primero a "Acceder a legajos")
|
||||
- [ ] Recordatorio de turno disparado automáticamente por cron (hoy el mensaje de WhatsApp se manda a mano)
|
||||
- [ ] Notificaciones push reales (que avisen aunque la app no esté abierta), más allá del aviso dentro del sistema
|
||||
- [ ] Notas privadas del profesional, separadas de la historia clínica formal
|
||||
- [ ] Importación de pacientes desde Excel/CSV
|
||||
- [ ] Modo offline básico para la app instalada (consulta de solo lectura sin conexión)
|
||||
- [ ] Exportación de estadísticas a Excel/CSV
|
||||
- [ ] Internacionalización (hoy todos los textos están en español rioplatense)
|
||||
|
||||
## Licencia
|
||||
|
||||
[PolyForm Noncommercial 1.0.0](./LICENSE) — código fuente disponible para cualquier uso **no comercial**: estudio personal, proyectos propios sin fines de lucro, instituciones educativas, organizaciones de salud pública, ONGs y organismos de gobierno. Para uso comercial (ofrecerlo como servicio, instalarlo a terceros a cambio de un pago, integrarlo en un producto comercial) se necesita una licencia aparte — abrí un issue o contactá a quien mantiene este repositorio.
|
||||
|
||||
---
|
||||
|
|
|
|||
108
api/admin.php
108
api/admin.php
|
|
@ -80,6 +80,85 @@ if ($accion === 'historial') {
|
|||
// DASHBOARD DE ESTADÍSTICAS (GET ?accion=estadisticas)
|
||||
// Acotado siempre al profesional activo.
|
||||
// ------------------------------------------------------------
|
||||
// ------------------------------------------------------------
|
||||
// PACIENTES EN RIESGO DE ABANDONO (GET ?accion=riesgo_abandono)
|
||||
// Para cada paciente con 2+ sesiones, calcula su propio ritmo
|
||||
// habitual (promedio de días entre sesiones) y compara contra
|
||||
// cuánto pasó desde la última. Si ya pasó más del doble de su
|
||||
// ritmo normal, se marca en riesgo — el umbral se ajusta solo
|
||||
// a cada paciente, no es un número fijo igual para todos.
|
||||
//
|
||||
// Los pacientes con 0 o 1 sesión no tienen ritmo propio para
|
||||
// comparar, así que usan una regla de respaldo simple: si pasó
|
||||
// más de 60 días desde que se creó el legajo (o desde su única
|
||||
// sesión) sin ninguna sesión nueva, también se marcan.
|
||||
// ------------------------------------------------------------
|
||||
if ($accion === 'riesgo_abandono') {
|
||||
requiereRolProfesional();
|
||||
$profesionalActivoId = idProfesionalActivo();
|
||||
|
||||
$stmtPacientes = $pdo->prepare('SELECT id, nombre, apellido, creado_en FROM pacientes WHERE profesional_id = ?');
|
||||
$stmtPacientes->execute([$profesionalActivoId]);
|
||||
$pacientes = $stmtPacientes->fetchAll();
|
||||
|
||||
$stmtSesiones = $pdo->prepare('SELECT fecha_sesion FROM sesiones WHERE paciente_id = ? ORDER BY fecha_sesion ASC');
|
||||
|
||||
$hoy = new DateTime();
|
||||
$enRiesgo = [];
|
||||
|
||||
foreach ($pacientes as $p) {
|
||||
$stmtSesiones->execute([$p['id']]);
|
||||
$fechas = array_column($stmtSesiones->fetchAll(), 'fecha_sesion');
|
||||
$totalSesiones = count($fechas);
|
||||
|
||||
if ($totalSesiones >= 2) {
|
||||
// Promedio de días entre sesiones consecutivas (ritmo propio).
|
||||
$intervalos = [];
|
||||
for ($i = 1; $i < $totalSesiones; $i++) {
|
||||
$a = new DateTime($fechas[$i - 1]);
|
||||
$b = new DateTime($fechas[$i]);
|
||||
$intervalos[] = (int) $a->diff($b)->days;
|
||||
}
|
||||
$promedio = array_sum($intervalos) / count($intervalos);
|
||||
if ($promedio < 1) $promedio = 1; // evita falsos positivos con sesiones el mismo día
|
||||
|
||||
$ultimaSesion = new DateTime($fechas[$totalSesiones - 1]);
|
||||
$diasSinVerlo = (int) $ultimaSesion->diff($hoy)->days;
|
||||
|
||||
if ($diasSinVerlo > $promedio * 2) {
|
||||
$enRiesgo[] = [
|
||||
'id' => $p['id'],
|
||||
'nombre' => $p['nombre'],
|
||||
'apellido' => $p['apellido'],
|
||||
'dias_sin_verlo' => $diasSinVerlo,
|
||||
'ritmo_habitual' => round($promedio),
|
||||
'motivo' => "Venía cada " . round($promedio) . " días aprox., y ya pasaron $diasSinVerlo sin una sesión nueva.",
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// Sin suficiente historia propia: regla de respaldo fija.
|
||||
$referencia = $totalSesiones === 1 ? new DateTime($fechas[0]) : new DateTime($p['creado_en']);
|
||||
$diasSinVerlo = (int) $referencia->diff($hoy)->days;
|
||||
if ($diasSinVerlo > 60) {
|
||||
$enRiesgo[] = [
|
||||
'id' => $p['id'],
|
||||
'nombre' => $p['nombre'],
|
||||
'apellido' => $p['apellido'],
|
||||
'dias_sin_verlo' => $diasSinVerlo,
|
||||
'ritmo_habitual' => null,
|
||||
'motivo' => $totalSesiones === 1
|
||||
? "Una sola sesión registrada, hace $diasSinVerlo días."
|
||||
: "Legajo creado hace $diasSinVerlo días, sin ninguna sesión registrada.",
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usort($enRiesgo, fn($a, $b) => $b['dias_sin_verlo'] - $a['dias_sin_verlo']);
|
||||
echo json_encode(['ok' => true, 'datos' => $enRiesgo]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($accion === 'estadisticas') {
|
||||
requiereRolProfesional();
|
||||
$profesionalActivoId = idProfesionalActivo();
|
||||
|
|
@ -170,6 +249,35 @@ if ($accion === 'estadisticas') {
|
|||
// vence dentro de los próximos 7 días, para tener el aviso a
|
||||
// tiempo y no que la suspensión los agarre de sorpresa.
|
||||
// ------------------------------------------------------------
|
||||
// ------------------------------------------------------------
|
||||
// CALENDARIO DE LICENCIAS (GET ?accion=calendario_licencias)
|
||||
// Todos los profesionales activos con licencia por días que
|
||||
// vencen dentro del mes/año pedido, para mostrar en una vista
|
||||
// de calendario (no solo los que vencen en 7 días, como el
|
||||
// aviso del panel principal).
|
||||
// ------------------------------------------------------------
|
||||
if ($accion === 'calendario_licencias') {
|
||||
requiereDesarrollador();
|
||||
$anio = (int) ($_GET['anio'] ?? date('Y'));
|
||||
$mes = (int) ($_GET['mes'] ?? date('n'));
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT u.id, u.nombre_completo,
|
||||
DATE_ADD(u.licencia_inicio, INTERVAL u.licencia_dias DAY) AS vencimiento,
|
||||
pl.titulo, pl.especialidad
|
||||
FROM usuarios u
|
||||
LEFT JOIN profesionales_legajos pl ON pl.usuario_id = u.id
|
||||
WHERE u.rol = 'profesional' AND u.activo = 1
|
||||
AND u.licencia_dias IS NOT NULL AND u.licencia_inicio IS NOT NULL
|
||||
AND YEAR(DATE_ADD(u.licencia_inicio, INTERVAL u.licencia_dias DAY)) = ?
|
||||
AND MONTH(DATE_ADD(u.licencia_inicio, INTERVAL u.licencia_dias DAY)) = ?
|
||||
ORDER BY vencimiento ASC
|
||||
");
|
||||
$stmt->execute([$anio, $mes]);
|
||||
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($accion === 'licencias_por_vencer') {
|
||||
requiereDesarrollador();
|
||||
$stmt = $pdo->query("
|
||||
|
|
|
|||
42
api/auth.php
42
api/auth.php
|
|
@ -828,6 +828,48 @@ if ($accion === 'crear_usuario') {
|
|||
// los datos personales y mantiene sincronizado el nombre que
|
||||
// se muestra en el login (nombre_completo de usuarios).
|
||||
// ------------------------------------------------------------
|
||||
// ------------------------------------------------------------
|
||||
// GUARDAR FIRMA DIGITAL — el propio profesional sube o dibuja
|
||||
// su firma (PNG en base64), que se inserta automáticamente al
|
||||
// pie de cada PDF que exporte de sus pacientes.
|
||||
// ------------------------------------------------------------
|
||||
if ($accion === 'guardar_firma_digital') {
|
||||
requiereSesion();
|
||||
if (($_SESSION['rol'] ?? '') !== 'profesional') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['ok' => false, 'error' => 'Solo un profesional puede guardar su propia firma.']);
|
||||
exit;
|
||||
}
|
||||
$usuarioId = (int) $_SESSION['usuario_id'];
|
||||
$firmaBase64 = $input['firma'] ?? '';
|
||||
|
||||
if ($firmaBase64 === '') {
|
||||
// Permite borrar la firma actual mandando un valor vacío.
|
||||
$pdo->prepare('UPDATE profesionales_legajos SET firma_digital = NULL WHERE usuario_id = ?')->execute([$usuarioId]);
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!preg_match('/^data:image\/(png|jpeg|jpg);base64,([A-Za-z0-9+\/=]+)$/', $firmaBase64, $coincidencia)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'El archivo tiene que ser una imagen PNG o JPEG válida.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Límite de ~2MB en base64 (≈1.5MB de imagen real), de sobra
|
||||
// para una firma y evita que alguien guarde algo gigante.
|
||||
if (strlen($firmaBase64) > 2 * 1024 * 1024) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'La imagen de la firma es demasiado grande.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo->prepare('UPDATE profesionales_legajos SET firma_digital = ? WHERE usuario_id = ?')->execute([$firmaBase64, $usuarioId]);
|
||||
registrarAuditoria($pdo, 'editar', 'usuario', $usuarioId, 'Actualizó su firma digital.');
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($accion === 'editar_legajo_profesional') {
|
||||
requiereDesarrollador();
|
||||
$usuarioId = (int) ($input['usuario_id'] ?? 0);
|
||||
|
|
|
|||
|
|
@ -1560,6 +1560,119 @@ a { color: var(--salvia); text-decoration: none; }
|
|||
/* ------------------------------------------------------------
|
||||
Aviso de licencias por vencer (panel Desarrollador)
|
||||
------------------------------------------------------------ */
|
||||
/* ------------------------------------------------------------
|
||||
Firma digital (panel Mi legajo)
|
||||
------------------------------------------------------------ */
|
||||
.canvas-firma {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
height: 160px;
|
||||
background: var(--crema);
|
||||
border: 1.5px dashed var(--linea);
|
||||
border-radius: var(--radio-chico);
|
||||
touch-action: none;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.vista-firma-actual {
|
||||
display: inline-block;
|
||||
background: var(--crema);
|
||||
border: 1.5px solid var(--linea);
|
||||
border-radius: var(--radio-chico);
|
||||
padding: 12px 18px;
|
||||
}
|
||||
.vista-firma-actual img {
|
||||
max-width: 220px;
|
||||
max-height: 80px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fila-riesgo-abandono {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--linea);
|
||||
}
|
||||
.fila-riesgo-abandono:last-child { border-bottom: none; }
|
||||
.fila-riesgo-abandono:hover .nombre { color: var(--salvia-oscuro); }
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
Calendario de vencimientos de licencia
|
||||
------------------------------------------------------------ */
|
||||
.encabezado-calendario-licencias {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.titulo-mes-calendario {
|
||||
font-family: var(--fuente-display);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--tinta);
|
||||
min-width: 180px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grilla-dias-semana {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
text-align: center;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
color: var(--tinta-suave);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.grilla-calendario-licencias {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.celda-calendario {
|
||||
min-height: 76px;
|
||||
border-radius: var(--radio-chico);
|
||||
background: var(--crema);
|
||||
border: 1px solid var(--linea);
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.celda-calendario.vacia {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
.celda-calendario.hoy {
|
||||
border-color: var(--salvia);
|
||||
background: var(--salvia-claro);
|
||||
}
|
||||
.numero-dia {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: var(--tinta-suave);
|
||||
}
|
||||
.chip-vencimiento {
|
||||
background: var(--coral-claro);
|
||||
color: var(--coral);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.celda-calendario { min-height: 56px; padding: 4px; }
|
||||
.grilla-dias-semana { font-size: 0.62rem; }
|
||||
.chip-vencimiento { font-size: 0.6rem; }
|
||||
}
|
||||
|
||||
.aviso-licencias-vencer {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
|
|
|||
240
assets/js/app.js
240
assets/js/app.js
|
|
@ -2043,6 +2043,36 @@ async function montarVistaDashboard(contenido) {
|
|||
}
|
||||
}
|
||||
|
||||
async function cargarRiesgoAbandono() {
|
||||
const cont = document.getElementById('lista-riesgo-abandono');
|
||||
if (!cont) return;
|
||||
try {
|
||||
const res = await llamarApi(`${API.admin}?accion=riesgo_abandono`, { method: 'GET' });
|
||||
if (!res.datos.length) {
|
||||
cont.innerHTML = '<p class="resumen-vacio">Ningún paciente parece estar abandonando el seguimiento por ahora.</p>';
|
||||
return;
|
||||
}
|
||||
cont.innerHTML = '';
|
||||
res.datos.forEach(p => {
|
||||
const fila = document.createElement('div');
|
||||
fila.className = 'fila-riesgo-abandono';
|
||||
fila.innerHTML = `
|
||||
<div class="info-principal" style="cursor:pointer;">
|
||||
<div class="avatar-iniciales" style="background:#F5E3DC; color:#C4654A;">${(p.nombre[0] + p.apellido[0]).toUpperCase()}</div>
|
||||
<div>
|
||||
<div class="nombre">${escaparHtml(p.nombre)} ${escaparHtml(p.apellido)}</div>
|
||||
<div class="meta">${escaparHtml(p.motivo)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
fila.querySelector('.info-principal').addEventListener('click', () => irAVista('detalle', { id: p.id }));
|
||||
cont.appendChild(fila);
|
||||
});
|
||||
} catch (e) {
|
||||
cont.innerHTML = '<p class="resumen-vacio">No se pudo calcular esta sección.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderizarDashboard(cont, d) {
|
||||
cont.innerHTML = '';
|
||||
|
||||
|
|
@ -2087,6 +2117,18 @@ function renderizarDashboard(cont, d) {
|
|||
`;
|
||||
cont.appendChild(panelObras);
|
||||
|
||||
// Panel de pacientes en riesgo de abandono (carga aparte, async)
|
||||
const panelRiesgo = document.createElement('div');
|
||||
panelRiesgo.className = 'panel';
|
||||
panelRiesgo.style.marginTop = '18px';
|
||||
panelRiesgo.innerHTML = `
|
||||
<div class="panel-titulo">Pacientes que podrían estar abandonando el seguimiento</div>
|
||||
<p class="panel-subtitulo">Comparamos cuánto pasó desde la última sesión de cada paciente contra su propio ritmo habitual de visitas.</p>
|
||||
<div id="lista-riesgo-abandono"><div class="cargando-pagina chico"><span class="spinner"></span></div></div>
|
||||
`;
|
||||
cont.appendChild(panelRiesgo);
|
||||
cargarRiesgoAbandono();
|
||||
|
||||
const contBarras = panelObras.querySelector('#barras-obras-sociales');
|
||||
const maximoObra = Math.max(...d.por_obra_social.map(o => o.total), 1);
|
||||
d.por_obra_social.forEach(o => {
|
||||
|
|
@ -2171,7 +2213,7 @@ async function montarVistaConfiguracion(contenido) {
|
|||
}
|
||||
|
||||
const tabs = contenido.querySelectorAll('[data-tab-config]');
|
||||
const paneles = { sedes: 'panel-config-sedes', usuarios: 'panel-config-usuarios', historial: 'panel-config-historial', version: 'panel-config-version', reportes: 'panel-config-reportes', papelera: 'panel-config-papelera', huerfanos: 'panel-config-huerfanos' };
|
||||
const paneles = { sedes: 'panel-config-sedes', usuarios: 'panel-config-usuarios', historial: 'panel-config-historial', version: 'panel-config-version', reportes: 'panel-config-reportes', papelera: 'panel-config-papelera', huerfanos: 'panel-config-huerfanos', calendario: 'panel-config-calendario' };
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
tabs.forEach(t => t.classList.remove('activo'));
|
||||
|
|
@ -2186,6 +2228,7 @@ async function montarVistaConfiguracion(contenido) {
|
|||
if (destino === 'reportes') cargarReportesSede();
|
||||
if (destino === 'papelera') inicializarPapeleraDev();
|
||||
if (destino === 'huerfanos') inicializarLegajosHuerfanos();
|
||||
if (destino === 'calendario') inicializarCalendarioLicencias();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -2485,6 +2528,67 @@ async function abrirModalTransferirHuerfano(p) {
|
|||
});
|
||||
}
|
||||
|
||||
let MES_CALENDARIO_LICENCIAS = new Date().getMonth() + 1;
|
||||
let ANIO_CALENDARIO_LICENCIAS = new Date().getFullYear();
|
||||
|
||||
function inicializarCalendarioLicencias() {
|
||||
document.getElementById('btn-mes-anterior-licencias').onclick = () => {
|
||||
MES_CALENDARIO_LICENCIAS--;
|
||||
if (MES_CALENDARIO_LICENCIAS < 1) { MES_CALENDARIO_LICENCIAS = 12; ANIO_CALENDARIO_LICENCIAS--; }
|
||||
cargarCalendarioLicencias();
|
||||
};
|
||||
document.getElementById('btn-mes-siguiente-licencias').onclick = () => {
|
||||
MES_CALENDARIO_LICENCIAS++;
|
||||
if (MES_CALENDARIO_LICENCIAS > 12) { MES_CALENDARIO_LICENCIAS = 1; ANIO_CALENDARIO_LICENCIAS++; }
|
||||
cargarCalendarioLicencias();
|
||||
};
|
||||
cargarCalendarioLicencias();
|
||||
}
|
||||
|
||||
async function cargarCalendarioLicencias() {
|
||||
const nombresMes = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
|
||||
document.getElementById('titulo-mes-licencias').textContent = `${nombresMes[MES_CALENDARIO_LICENCIAS - 1]} ${ANIO_CALENDARIO_LICENCIAS}`;
|
||||
|
||||
const grilla = document.getElementById('grilla-calendario-licencias');
|
||||
grilla.innerHTML = '<div class="cargando-pagina chico"><span class="spinner"></span></div>';
|
||||
|
||||
let vencimientosPorDia = {};
|
||||
try {
|
||||
const res = await llamarApi(`${API.admin}?accion=calendario_licencias&anio=${ANIO_CALENDARIO_LICENCIAS}&mes=${MES_CALENDARIO_LICENCIAS}`, { method: 'GET' });
|
||||
res.datos.forEach(p => {
|
||||
const dia = parseInt(p.vencimiento.split('-')[2], 10);
|
||||
if (!vencimientosPorDia[dia]) vencimientosPorDia[dia] = [];
|
||||
vencimientosPorDia[dia].push(p);
|
||||
});
|
||||
} catch (e) {
|
||||
grilla.innerHTML = '<p class="resumen-vacio">No se pudo cargar el calendario.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
grilla.innerHTML = '';
|
||||
const primerDiaSemana = new Date(ANIO_CALENDARIO_LICENCIAS, MES_CALENDARIO_LICENCIAS - 1, 1).getDay();
|
||||
const diasEnMes = new Date(ANIO_CALENDARIO_LICENCIAS, MES_CALENDARIO_LICENCIAS, 0).getDate();
|
||||
const hoy = new Date();
|
||||
const esMesActual = hoy.getFullYear() === ANIO_CALENDARIO_LICENCIAS && hoy.getMonth() + 1 === MES_CALENDARIO_LICENCIAS;
|
||||
|
||||
for (let i = 0; i < primerDiaSemana; i++) {
|
||||
const celdaVacia = document.createElement('div');
|
||||
celdaVacia.className = 'celda-calendario vacia';
|
||||
grilla.appendChild(celdaVacia);
|
||||
}
|
||||
|
||||
for (let dia = 1; dia <= diasEnMes; dia++) {
|
||||
const celda = document.createElement('div');
|
||||
celda.className = 'celda-calendario' + (esMesActual && dia === hoy.getDate() ? ' hoy' : '');
|
||||
const profesionalesDia = vencimientosPorDia[dia] || [];
|
||||
celda.innerHTML = `
|
||||
<span class="numero-dia">${dia}</span>
|
||||
${profesionalesDia.map(p => `<div class="chip-vencimiento" title="${escaparHtml(p.nombre_completo)}">${escaparHtml((p.titulo || '') + ' ' + p.nombre_completo.split(' ').slice(-1)[0])}</div>`).join('')}
|
||||
`;
|
||||
grilla.appendChild(celda);
|
||||
}
|
||||
}
|
||||
|
||||
async function cargarReportesSede() {
|
||||
const cont = document.getElementById('lista-reportes-sede');
|
||||
cont.innerHTML = '<div class="cargando-pagina chico"><span class="spinner"></span></div>';
|
||||
|
|
@ -3169,12 +3273,146 @@ async function montarVistaMiLegajo(contenido) {
|
|||
<div class="dato-box" style="grid-column: span 2;"><div class="etiqueta-dato">Licencia</div><div class="valor-dato">${venc}</div></div>
|
||||
</div>
|
||||
<p style="font-size:0.82rem; color:var(--tinta-suave); margin-top:8px;">Si necesitás corregir algún dato, comunicate con el administrador del sistema.</p>
|
||||
</div>
|
||||
|
||||
<div class="panel" style="margin-top:18px;">
|
||||
<div class="panel-titulo">Mi firma</div>
|
||||
<p class="panel-subtitulo">Se inserta automáticamente al pie de cada legajo que exportes a PDF.</p>
|
||||
<div id="contenedor-firma-actual" style="margin-bottom:16px;"></div>
|
||||
<div class="tabs-firma" style="display:flex; gap:8px; margin-bottom:14px;">
|
||||
<button class="btn btn-secundario btn-chico" id="tab-dibujar-firma" type="button">Dibujar</button>
|
||||
<button class="btn btn-secundario btn-chico" id="tab-subir-firma" type="button">Subir imagen</button>
|
||||
</div>
|
||||
<div id="panel-dibujar-firma">
|
||||
<canvas id="canvas-firma" width="500" height="160" class="canvas-firma"></canvas>
|
||||
<div style="display:flex; gap:10px; margin-top:10px;">
|
||||
<button class="btn btn-secundario btn-chico" id="btn-limpiar-firma" type="button">Limpiar</button>
|
||||
<button class="btn btn-primario btn-chico" id="btn-guardar-firma-dibujada" type="button">Guardar firma</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="panel-subir-firma" class="oculto">
|
||||
<input type="file" id="input-subir-firma" accept="image/png,image/jpeg" style="margin-bottom:10px;">
|
||||
<div>
|
||||
<button class="btn btn-primario btn-chico" id="btn-guardar-firma-subida" type="button">Guardar firma</button>
|
||||
</div>
|
||||
</div>
|
||||
${l.firma_digital ? '<button class="btn btn-texto" id="btn-quitar-firma" type="button" style="margin-top:12px; color:var(--coral);">Quitar firma actual</button>' : ''}
|
||||
</div>`;
|
||||
|
||||
inicializarModuloFirma(l.firma_digital);
|
||||
} catch (e) {
|
||||
cont.innerHTML = '<p class="resumen-vacio">No se encontró tu legajo. Comunicate con el administrador del sistema.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function inicializarModuloFirma(firmaActual) {
|
||||
const contActual = document.getElementById('contenedor-firma-actual');
|
||||
if (firmaActual) {
|
||||
contActual.innerHTML = `<div class="vista-firma-actual"><img src="${firmaActual}" alt="Tu firma actual"></div>`;
|
||||
} else {
|
||||
contActual.innerHTML = '<p class="resumen-vacio" style="margin:0;">Todavía no guardaste una firma.</p>';
|
||||
}
|
||||
|
||||
const tabDibujar = document.getElementById('tab-dibujar-firma');
|
||||
const tabSubir = document.getElementById('tab-subir-firma');
|
||||
const panelDibujar = document.getElementById('panel-dibujar-firma');
|
||||
const panelSubir = document.getElementById('panel-subir-firma');
|
||||
|
||||
tabDibujar.addEventListener('click', () => {
|
||||
tabDibujar.classList.replace('btn-secundario', 'btn-primario');
|
||||
tabSubir.classList.replace('btn-primario', 'btn-secundario');
|
||||
panelDibujar.classList.remove('oculto');
|
||||
panelSubir.classList.add('oculto');
|
||||
});
|
||||
tabSubir.addEventListener('click', () => {
|
||||
tabSubir.classList.replace('btn-secundario', 'btn-primario');
|
||||
tabDibujar.classList.replace('btn-primario', 'btn-secundario');
|
||||
panelSubir.classList.remove('oculto');
|
||||
panelDibujar.classList.add('oculto');
|
||||
});
|
||||
tabDibujar.classList.replace('btn-secundario', 'btn-primario');
|
||||
|
||||
// --- Dibujar con mouse o dedo ---
|
||||
const canvas = document.getElementById('canvas-firma');
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.lineWidth = 2.4;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = '#1C2421';
|
||||
let dibujando = false;
|
||||
let huboTrazo = false;
|
||||
|
||||
function posicionDesdeEvento(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const escalaX = canvas.width / rect.width;
|
||||
const escalaY = canvas.height / rect.height;
|
||||
const punto = e.touches ? e.touches[0] : e;
|
||||
return { x: (punto.clientX - rect.left) * escalaX, y: (punto.clientY - rect.top) * escalaY };
|
||||
}
|
||||
function empezarTrazo(e) {
|
||||
e.preventDefault();
|
||||
dibujando = true;
|
||||
huboTrazo = true;
|
||||
const p = posicionDesdeEvento(e);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
}
|
||||
function seguirTrazo(e) {
|
||||
if (!dibujando) return;
|
||||
e.preventDefault();
|
||||
const p = posicionDesdeEvento(e);
|
||||
ctx.lineTo(p.x, p.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
function terminarTrazo() { dibujando = false; }
|
||||
|
||||
canvas.addEventListener('mousedown', empezarTrazo);
|
||||
canvas.addEventListener('mousemove', seguirTrazo);
|
||||
canvas.addEventListener('mouseup', terminarTrazo);
|
||||
canvas.addEventListener('mouseleave', terminarTrazo);
|
||||
canvas.addEventListener('touchstart', empezarTrazo, { passive: false });
|
||||
canvas.addEventListener('touchmove', seguirTrazo, { passive: false });
|
||||
canvas.addEventListener('touchend', terminarTrazo);
|
||||
|
||||
document.getElementById('btn-limpiar-firma').addEventListener('click', () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
huboTrazo = false;
|
||||
});
|
||||
|
||||
document.getElementById('btn-guardar-firma-dibujada').addEventListener('click', async () => {
|
||||
if (!huboTrazo) { mostrarToast('Dibujá tu firma antes de guardar.', 'error'); return; }
|
||||
await guardarFirma(canvas.toDataURL('image/png'));
|
||||
});
|
||||
|
||||
// --- Subir imagen ---
|
||||
document.getElementById('btn-guardar-firma-subida').addEventListener('click', async () => {
|
||||
const input = document.getElementById('input-subir-firma');
|
||||
const archivo = input.files && input.files[0];
|
||||
if (!archivo) { mostrarToast('Elegí una imagen de tu firma.', 'error'); return; }
|
||||
if (archivo.size > 1.5 * 1024 * 1024) { mostrarToast('La imagen pesa demasiado. Probá con una más liviana.', 'error'); return; }
|
||||
const lector = new FileReader();
|
||||
lector.onload = () => guardarFirma(lector.result);
|
||||
lector.readAsDataURL(archivo);
|
||||
});
|
||||
|
||||
const btnQuitar = document.getElementById('btn-quitar-firma');
|
||||
if (btnQuitar) {
|
||||
btnQuitar.addEventListener('click', async () => {
|
||||
if (!confirm('¿Quitar tu firma actual? Los próximos PDF que exportes no la van a incluir.')) return;
|
||||
await guardarFirma('');
|
||||
});
|
||||
}
|
||||
|
||||
async function guardarFirma(dataUrl) {
|
||||
try {
|
||||
await llamarApi(API.auth, { method: 'POST', body: JSON.stringify({ accion: 'guardar_firma_digital', firma: dataUrl }) });
|
||||
mostrarToast(dataUrl ? 'Firma guardada correctamente.' : 'Firma eliminada.', 'exito');
|
||||
irAVista('mi-legajo');
|
||||
} catch (e) {
|
||||
mostrarToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function abrirModalGestionarSedesUsuario(usuario) {
|
||||
const modalEnv = clonarPlantilla('tpl-modal-gestionar-sedes-usuario');
|
||||
document.body.appendChild(modalEnv);
|
||||
|
|
|
|||
17
exportar.php
17
exportar.php
|
|
@ -44,10 +44,16 @@ $stmtSesiones = $pdo->prepare('SELECT * FROM sesiones WHERE paciente_id = ? ORDE
|
|||
$stmtSesiones->execute([$id]);
|
||||
$sesiones = $stmtSesiones->fetchAll();
|
||||
|
||||
$stmtNombreProf = $pdo->prepare('SELECT nombre_completo FROM usuarios WHERE id = ?');
|
||||
$stmtNombreProf = $pdo->prepare('
|
||||
SELECT u.nombre_completo, pl.firma_digital
|
||||
FROM usuarios u
|
||||
LEFT JOIN profesionales_legajos pl ON pl.usuario_id = u.id
|
||||
WHERE u.id = ?
|
||||
');
|
||||
$stmtNombreProf->execute([$profesionalActivoId]);
|
||||
$filaProf = $stmtNombreProf->fetch();
|
||||
$nombreProfesional = $filaProf ? $filaProf['nombre_completo'] : '';
|
||||
$firmaDigital = $filaProf ? ($filaProf['firma_digital'] ?? null) : null;
|
||||
|
||||
function e($texto) {
|
||||
return htmlspecialchars($texto ?? '', ENT_QUOTES, 'UTF-8');
|
||||
|
|
@ -163,6 +169,12 @@ $edad = calcularEdadExport($paciente['fecha_nacimiento']);
|
|||
width: 260px;
|
||||
text-align: center;
|
||||
}
|
||||
.imagen-firma {
|
||||
max-width: 220px;
|
||||
max-height: 80px;
|
||||
margin: 0 auto 4px;
|
||||
display: block;
|
||||
}
|
||||
.linea-firma { border-top: 1.5px solid #1C2421; margin-bottom: 8px; }
|
||||
.nombre-firma { font-weight: 700; font-size: 0.92rem; }
|
||||
.aclaracion-firma { font-size: 0.75rem; color: #4A5650; margin-top: 2px; }
|
||||
|
|
@ -237,6 +249,9 @@ $edad = calcularEdadExport($paciente['fecha_nacimiento']);
|
|||
|
||||
<?php if ($nombreProfesional): ?>
|
||||
<div class="bloque-firma">
|
||||
<?php if ($firmaDigital): ?>
|
||||
<img src="<?= e($firmaDigital) ?>" alt="Firma" class="imagen-firma">
|
||||
<?php endif; ?>
|
||||
<div class="linea-firma"></div>
|
||||
<div class="nombre-firma"><?= e($nombreProfesional) ?></div>
|
||||
<div class="aclaracion-firma">Profesional responsable del seguimiento clínico</div>
|
||||
|
|
|
|||
21
index.html
21
index.html
|
|
@ -690,6 +690,7 @@
|
|||
<button class="tab-busqueda" data-tab-config="reportes">Reportes por sede</button>
|
||||
<button class="tab-busqueda" data-tab-config="papelera">Papelera</button>
|
||||
<button class="tab-busqueda" data-tab-config="huerfanos">Legajos huérfanos</button>
|
||||
<button class="tab-busqueda" data-tab-config="calendario">Calendario de licencias</button>
|
||||
</div>
|
||||
|
||||
<div id="panel-config-sedes">
|
||||
|
|
@ -791,6 +792,26 @@
|
|||
<div id="lista-huerfanos-dev" class="lista-resultados"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panel-config-calendario" class="oculto">
|
||||
<div class="panel">
|
||||
<div class="panel-titulo">Calendario de vencimientos de licencia</div>
|
||||
<p class="panel-subtitulo">Cada día marcado muestra qué profesionales vencen ese día, para planificar renovaciones con anticipación.</p>
|
||||
<div class="encabezado-calendario-licencias">
|
||||
<button class="btn-icono" id="btn-mes-anterior-licencias" type="button">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
|
||||
</button>
|
||||
<span id="titulo-mes-licencias" class="titulo-mes-calendario"></span>
|
||||
<button class="btn-icono" id="btn-mes-siguiente-licencias" type="button">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grilla-dias-semana">
|
||||
<span>Dom</span><span>Lun</span><span>Mar</span><span>Mié</span><span>Jue</span><span>Vie</span><span>Sáb</span>
|
||||
</div>
|
||||
<div id="grilla-calendario-licencias" class="grilla-calendario-licencias"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-modal-recuperar-legajo">
|
||||
|
|
|
|||
19
migracion_v12.sql
Normal file
19
migracion_v12.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-- ============================================================
|
||||
-- Del Austral — Migración de base de datos (versión 12)
|
||||
-- ============================================================
|
||||
-- Agrega a "profesionales_legajos" una columna para guardar la
|
||||
-- firma digital del profesional (como imagen PNG en base64),
|
||||
-- ya sea dibujada en pantalla o subida como foto escaneada.
|
||||
-- Se inserta automáticamente al pie de cada PDF exportado.
|
||||
--
|
||||
-- No borra ni modifica pacientes, sesiones, citas ni adjuntos.
|
||||
--
|
||||
-- Cómo aplicarlo:
|
||||
-- 1. Entrá a phpMyAdmin → tu base de datos.
|
||||
-- 2. Pestaña "SQL" (no "Importar").
|
||||
-- 3. Pegá todo este archivo y ejecutá.
|
||||
-- ============================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE profesionales_legajos ADD COLUMN firma_digital MEDIUMTEXT NULL AFTER telefono;
|
||||
16
version.json
16
version.json
|
|
@ -1,17 +1,17 @@
|
|||
{
|
||||
"version": "v22",
|
||||
"version": "v23",
|
||||
"fecha": "2026-06-30",
|
||||
"descripcion": "Arregla el texto de licencia en Mi legajo, que se cortaba mal cuando decía 'Sin vencimiento (indeterminada)'",
|
||||
"descripcion": "Entrega final: firma digital (dibujada o subida) en el PDF exportado, detección de pacientes en riesgo de abandono ajustada por su propio ritmo de visitas, y calendario mensual de vencimientos de licencia para el Desarrollador",
|
||||
"archivos_criticos": {
|
||||
"index.html": "e3c7585d7aeeffa597d277247d77e07e",
|
||||
"assets/js/app.js": "3c5ba080ea3c22a6a69a73f3ae30c69d",
|
||||
"assets/css/estilos.css": "60de9e4c4073cd4a57c0c081577d0d25",
|
||||
"api/auth.php": "702ce82cdca08bef95a10e0821c4dc52",
|
||||
"index.html": "5477853e2519ab620dbd61f81e60a537",
|
||||
"assets/js/app.js": "ea6de159fd836d8b09fc4c424e51966a",
|
||||
"assets/css/estilos.css": "9091b4865695cfa01fcbfd9758ad954d",
|
||||
"api/auth.php": "6f7e7013eec4f93a482981668ff6e754",
|
||||
"api/citas.php": "7622c3facffd9fb671fa63494be56124",
|
||||
"api/pacientes.php": "df7de793317e8229120119a5050c44c2",
|
||||
"api/adjuntos.php": "8da3f85e26239072953298c60dcb5540",
|
||||
"api/admin.php": "15086c71878647214eedfc18757a4dd1",
|
||||
"api/admin.php": "b0f9dbf5039e62dc5c0692268ce46613",
|
||||
"confirmar_turno.php": "ad6a798a41a594bb5dcc797295222cce",
|
||||
"exportar.php": "ae88278f3ea445afb0394ef514fe1d83"
|
||||
"exportar.php": "a10f27743c3af92dcf8fa3f364efdae9"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user