Respaldo automático 2026-06-30 06:55
This commit is contained in:
parent
3bf93fa635
commit
55461b3ac9
|
|
@ -24,6 +24,7 @@ En phpMyAdmin, entrá a tu base de datos → pestaña **"SQL"** (no "Importar"),
|
||||||
| `migracion_v10.sql` | Legajos completos de profesionales (datos personales, especialidad) y sistema de licencias |
|
| `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_v11.sql` | Número de legajo automático (formato `LG-AAAA-NNN`) |
|
||||||
| `migracion_v12.sql` | Firma digital del profesional |
|
| `migracion_v12.sql` | Firma digital del profesional |
|
||||||
|
| `migracion_v13.sql` | Matrícula nacional/provincial y sello automático generado al crear el legajo |
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
@ -136,7 +137,7 @@ La pantalla de acceso normal no menciona que existe un rol de Desarrollador, a p
|
||||||
Desde el panel de Desarrollador tenés estas pestañas:
|
Desde el panel de Desarrollador tenés estas pestañas:
|
||||||
|
|
||||||
- **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.
|
- **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.
|
- **Usuarios**: agregar profesionales (con su legajo completo: título, nombre, DNI, fecha y lugar de nacimiento, especialidad, matrícula nacional y/o provincial —ambas opcionales—, contacto, sede y licencia) o administrativas (alta simple). Cada profesional recibe un número de legajo automático con formato `LG-2026-001`, y un sello/firma de partida generado automáticamente con sus datos (nombre, título y matrícula si la cargaste). Desde acá también podés editar el legajo de un profesional ya creado (si todavía tiene el sello automático sin reemplazar, se regenera solo con los datos nuevos al guardar), 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.
|
- **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.
|
- **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).
|
- **Reportes por sede**: resumen de cada sede (profesionales, pacientes, actividad del mes).
|
||||||
|
|
@ -150,7 +151,7 @@ Desde el panel de Desarrollador tenés estas pestañas:
|
||||||
|
|
||||||
- **Crear legajo**: registra un paciente con sus datos, obra social y, opcionalmente, las primeras sesiones.
|
- **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.
|
- **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.
|
- **Firma digital**: cuando el Desarrollador crea el legajo, el sistema genera automáticamente un sello de partida (título junto al nombre, especialidad y matrícula si la cargaste, en el estilo de un sello real). Desde "Mi legajo" → "Mi firma", el profesional ve ese sello ya precargado en el recuadro de dibujo y puede firmar directamente encima con el mouse o el dedo, reemplazarlo subiendo una imagen de su propio sello escaneado (firmando igual encima), o limpiar todo y dibujar una firma libre. Sea cual sea el resultado, se inserta automáticamente al pie de cada legajo que exporte a PDF.
|
||||||
- **Agenda**: calendario mensual con tus citas, más un resumen de próximas citas en el panel principal.
|
- **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.
|
- **"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.
|
- **Aviso de confirmaciones y cancelaciones**: cartel con la cantidad de novedades cuando un paciente confirma o cancela desde el link de WhatsApp.
|
||||||
|
|
|
||||||
373
README.md
373
README.md
|
|
@ -1,187 +1,186 @@
|
||||||
# Del Austral
|
# Del Austral
|
||||||
|
|
||||||
[](./LICENSE)
|
[](./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.
|
**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.
|
Corre tanto en hosting compartido tipo cPanel (sin SSH, Composer ni extensiones especiales) como en un VPS propio con Nginx + PHP-FPM + MariaDB.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Índice
|
## Índice
|
||||||
|
|
||||||
- [Características](#características)
|
- [Características](#características)
|
||||||
- [Stack técnico](#stack-técnico)
|
- [Stack técnico](#stack-técnico)
|
||||||
- [Estructura del proyecto](#estructura-del-proyecto)
|
- [Estructura del proyecto](#estructura-del-proyecto)
|
||||||
- [Instalación](#instalación)
|
- [Instalación](#instalación)
|
||||||
- [Sedes, profesionales y roles](#sedes-profesionales-y-roles)
|
- [Sedes, profesionales y roles](#sedes-profesionales-y-roles)
|
||||||
- [Modelo de datos](#modelo-de-datos)
|
- [Modelo de datos](#modelo-de-datos)
|
||||||
- [Seguridad](#seguridad)
|
- [Seguridad](#seguridad)
|
||||||
- [Hoja de ruta](#hoja-de-ruta)
|
- [Hoja de ruta](#hoja-de-ruta)
|
||||||
- [Licencia](#licencia)
|
- [Licencia](#licencia)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Características
|
## Características
|
||||||
|
|
||||||
- 🔐 **Acceso por PIN numérico** (4 dígitos) en vez de usuario/contraseña tradicional.
|
- 🔐 **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-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.
|
- 👥 **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.
|
- 🛠️ **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`.
|
- 🪪 **Legajos completos de profesionales**: título, nombre, DNI, fecha y lugar de nacimiento, especialidad, matrícula nacional/provincial (ambas opcionales), 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.
|
- ⏳ **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.
|
- 📅 **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.
|
- 🧑💼 **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.
|
- 📋 **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.
|
- 🗓️ **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.
|
- 📅 **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.
|
- ✅ **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.
|
- 💬 **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.
|
- 📎 **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.
|
- ✍️ **Firma digital del profesional**: generada automáticamente al crear el legajo (sello con nombre, título, especialidad y matrícula, en el estilo de un sello real). El profesional la ve precargada en "Mi legajo" y puede firmar encima con mouse o dedo, reemplazarla subiendo una imagen de su propio sello escaneado, o dibujar una firma libre. 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.
|
- 📄 **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).
|
- ⚠️ **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.
|
- 🎂 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.
|
- 📊 **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.
|
- 🧾 **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.
|
- 🗑️ **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).
|
- ⚖️ Aviso de protección de datos personales conforme a la Ley 25.326 (Argentina).
|
||||||
- 📱 **App instalable (PWA)**, sin pasar por ninguna tienda de aplicaciones.
|
- 📱 **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.
|
- 🔍 **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.
|
- 📊 **Reportes por sede**, y **exportación masiva** de todos los legajos propios a un único archivo de respaldo.
|
||||||
|
|
||||||
## Stack técnico
|
## Stack técnico
|
||||||
|
|
||||||
| Capa | Tecnología |
|
| Capa | Tecnología |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Backend | PHP 7.4+ puro (sin frameworks, sin Composer) |
|
| Backend | PHP 7.4+ puro (sin frameworks, sin Composer) |
|
||||||
| Base de datos | MySQL / MariaDB (InnoDB, utf8mb4) |
|
| Base de datos | MySQL / MariaDB (InnoDB, utf8mb4) |
|
||||||
| Frontend | HTML, CSS y JavaScript vanilla (sin build step, sin npm) |
|
| Frontend | HTML, CSS y JavaScript vanilla (sin build step, sin npm) |
|
||||||
| Autenticación | PIN hasheado con `password_hash` (bcrypt) + sesiones nativas de PHP |
|
| 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 |
|
| 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 |
|
| 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.
|
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
|
## Estructura del proyecto
|
||||||
|
|
||||||
```
|
```
|
||||||
del-austral/
|
del-austral/
|
||||||
├── index.html # SPA: toda la interfaz vive acá (templates + vistas)
|
├── index.html # SPA: toda la interfaz vive acá (templates + vistas)
|
||||||
├── exportar.php # Vista de impresión/exportación de legajo a PDF
|
├── 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)
|
├── confirmar_turno.php # Página pública de confirmación de turno (sin login)
|
||||||
├── manifest.json # Metadata de la PWA (nombre, iconos, colores)
|
├── manifest.json # Metadata de la PWA (nombre, iconos, colores)
|
||||||
├── sw.js # Service worker mínimo, para habilitar la instalación
|
├── sw.js # Service worker mínimo, para habilitar la instalación
|
||||||
├── version.json # Hashes de referencia para la verificación de versión
|
├── version.json # Hashes de referencia para la verificación de versión
|
||||||
├── database.sql # Esquema completo (instalación nueva, desde cero)
|
├── database.sql # Esquema completo (instalación nueva, desde cero)
|
||||||
├── migracion_v2.sql # Citas, archivos adjuntos, plantillas
|
├── migracion_v2.sql # Citas, archivos adjuntos, plantillas
|
||||||
├── migracion_v3.sql # Usuarios con roles, historial de cambios
|
├── migracion_v3.sql # Usuarios con roles, historial de cambios
|
||||||
├── migracion_v4.sql # Acceso por PIN numérico (en vez de patrón dibujado)
|
├── migracion_v4.sql # Acceso por PIN numérico (en vez de patrón dibujado)
|
||||||
├── migracion_v5.sql # Sedes, aislamiento por profesional, rol Desarrollador
|
├── migracion_v5.sql # Sedes, aislamiento por profesional, rol Desarrollador
|
||||||
├── migracion_v6.sql # Aviso de confirmaciones/cancelaciones de turno
|
├── migracion_v6.sql # Aviso de confirmaciones/cancelaciones de turno
|
||||||
├── migracion_v7.sql # Profesional/sede original en la papelera
|
├── migracion_v7.sql # Profesional/sede original en la papelera
|
||||||
├── migracion_v8.sql # Aviso de "legajo recuperado de otro profesional"
|
├── migracion_v8.sql # Aviso de "legajo recuperado de otro profesional"
|
||||||
├── migracion_v9.sql # DNI único por profesional, no global
|
├── migracion_v9.sql # DNI único por profesional, no global
|
||||||
├── migracion_v10.sql # Legajos completos de profesionales + sistema de licencias
|
├── migracion_v10.sql # Legajos completos de profesionales + sistema de licencias
|
||||||
├── migracion_v11.sql # Número de legajo automático (LG-AAAA-NNN)
|
├── migracion_v11.sql # Número de legajo automático (LG-AAAA-NNN)
|
||||||
├── migracion_v12.sql # Firma digital del profesional
|
├── migracion_v12.sql # Firma digital del profesional
|
||||||
├── config/
|
├── migracion_v13.sql # Matrícula nacional/provincial y sello automático
|
||||||
│ └── config.php # Credenciales de BD + helpers de sesión/rol/auditoría
|
├── config/
|
||||||
├── api/
|
│ └── config.php # Credenciales de BD + helpers de sesión/rol/auditoría
|
||||||
│ ├── auth.php # Login multi-paso, Desarrollador, sedes, usuarios, legajos
|
├── api/
|
||||||
│ ├── pacientes.php # CRUD de legajos, sesiones, búsqueda, migración de sede
|
│ ├── auth.php # Login multi-paso, Desarrollador, sedes, usuarios, legajos
|
||||||
│ ├── citas.php # Agenda, choque de horario, cumpleaños, inactivos
|
│ ├── pacientes.php # CRUD de legajos, sesiones, búsqueda, migración de sede
|
||||||
│ ├── adjuntos.php # Subida/descarga de archivos por paciente
|
│ ├── citas.php # Agenda, choque de horario, cumpleaños, inactivos
|
||||||
│ ├── plantillas.php # CRUD de plantillas de evolución (por profesional)
|
│ ├── adjuntos.php # Subida/descarga de archivos por paciente
|
||||||
│ ├── obras_sociales.php # Catálogo de obras sociales
|
│ ├── plantillas.php # CRUD de plantillas de evolución (por profesional)
|
||||||
│ └── admin.php # Estadísticas, historial, versión, riesgo de abandono, calendario de licencias
|
│ ├── obras_sociales.php # Catálogo de obras sociales
|
||||||
├── assets/
|
│ └── admin.php # Estadísticas, historial, versión, riesgo de abandono, calendario de licencias
|
||||||
│ ├── css/estilos.css # Toda la hoja de estilos
|
├── assets/
|
||||||
│ ├── js/app.js # Toda la lógica de frontend
|
│ ├── css/estilos.css # Toda la hoja de estilos
|
||||||
│ └── icons/ # Íconos de la PWA en distintos tamaños
|
│ ├── js/app.js # Toda la lógica de frontend
|
||||||
└── adjuntos/ # Carpeta de archivos subidos (protegida con .htaccess)
|
│ └── icons/ # Íconos de la PWA en distintos tamaños
|
||||||
```
|
└── adjuntos/ # Carpeta de archivos subidos (protegida con .htaccess)
|
||||||
|
```
|
||||||
## Instalación
|
|
||||||
|
## 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)**.
|
|
||||||
|
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:
|
|
||||||
|
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.
|
1. Creá una base de datos MySQL/MariaDB e importá `database.sql`.
|
||||||
3. Subí todo el proyecto a tu hosting o VPS, incluyendo la carpeta `adjuntos/`.
|
2. Completá `config/config.php` con tus credenciales y un `APP_SECRET` propio.
|
||||||
4. Abrí el sitio: la primera vez te va a pedir crear la clave de Desarrollador, y luego tu primera sede y profesional.
|
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.
|
|
||||||
|
Si venís de una versión anterior con pacientes ya cargados, corré las migraciones en orden (`migracion_v2.sql` hasta `migracion_v13.sql`), sin saltarte ninguna. Ningún script borra pacientes, sesiones, citas ni adjuntos.
|
||||||
## Sedes, profesionales y roles
|
|
||||||
|
## Sedes, profesionales y roles
|
||||||
| | Desarrollador | Profesional | Administrativa |
|
|
||||||
|---|---|---|---|
|
| | Desarrollador | Profesional | Administrativa |
|
||||||
| Crear/renombrar/desactivar sedes y usuarios | ✅ | ❌ | ❌ |
|
|---|---|---|---|
|
||||||
| Gestionar licencias de acceso | ✅ | ❌ | ❌ |
|
| Crear/renombrar/desactivar sedes y usuarios | ✅ | ❌ | ❌ |
|
||||||
| Ver pacientes y agenda | ❌ | ✅ (propios) | ✅ (de un profesional elegido) |
|
| Gestionar licencias de acceso | ✅ | ❌ | ❌ |
|
||||||
| Crear paciente (datos de contacto) | ❌ | ✅ | ✅ |
|
| Ver pacientes y agenda | ❌ | ✅ (propios) | ✅ (de un profesional elegido) |
|
||||||
| Motivo, patología, síntomas, sesiones | ❌ | ✅ | ❌ |
|
| Crear paciente (datos de contacto) | ❌ | ✅ | ✅ |
|
||||||
| Editar / eliminar legajo o sesión | ❌ | ✅ | ❌ |
|
| Motivo, patología, síntomas, sesiones | ❌ | ✅ | ❌ |
|
||||||
| Exportar PDF, estadísticas, historial | ❌ | ✅ | ❌ |
|
| 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.
|
- 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.
|
||||||
- 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.
|
- 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.
|
|
||||||
|
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
|
|
||||||
|
## Modelo de datos
|
||||||
Tablas principales (ver `database.sql` para el esquema completo con índices y claves foráneas):
|
|
||||||
|
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
|
- `sedes` — sucursales del consultorio
|
||||||
- `usuarios` — accesos al sistema (PIN hasheado, rol, estado activo/inactivo, estado y duración de licencia)
|
- `desarrollador` — clave única del rol Desarrollador
|
||||||
- `usuarios_sedes` — relación N a N entre usuarios y las sedes donde atienden
|
- `usuarios` — accesos al sistema (PIN hasheado, rol, estado activo/inactivo, estado y duración de licencia)
|
||||||
- `profesionales_legajos` — datos personales completos de cada profesional, número de legajo y firma digital
|
- `usuarios_sedes` — relación N a N entre usuarios y las sedes donde atienden
|
||||||
- `pacientes` — legajo principal, con `profesional_id` (dueño), `sede_id` y rastro de procedencia si fue recuperado de otro profesional
|
- `profesionales_legajos` — datos personales completos de cada profesional, número de legajo y firma digital
|
||||||
- `sesiones` — historial clínico cronológico por paciente, editable
|
- `pacientes` — legajo principal, con `profesional_id` (dueño), `sede_id` y rastro de procedencia si fue recuperado de otro profesional
|
||||||
- `citas` — agenda, con `profesional_id`, token de confirmación pública y detección de choque de horario
|
- `sesiones` — historial clínico cronológico por paciente, editable
|
||||||
- `archivos_adjuntos` — metadata de archivos subidos (PDF/imágenes)
|
- `citas` — agenda, con `profesional_id`, token de confirmación pública y detección de choque de horario
|
||||||
- `plantillas_evolucion` — textos reutilizables por profesional (no compartidos)
|
- `archivos_adjuntos` — metadata de archivos subidos (PDF/imágenes)
|
||||||
- `obras_sociales` — catálogo editable de coberturas de salud, compartido
|
- `plantillas_evolucion` — textos reutilizables por profesional (no compartidos)
|
||||||
- `legajos_eliminados` — papelera, con el profesional y la sede original para poder recuperarlos
|
- `obras_sociales` — catálogo editable de coberturas de salud, compartido
|
||||||
- `historial_cambios` — auditoría de acciones (quién, qué, cuándo)
|
- `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
|
|
||||||
|
## 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.
|
- 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.
|
||||||
- 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.
|
- Protección básica contra fuerza bruta: bloqueo temporal tras 5 intentos fallidos.
|
||||||
- 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.
|
- 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.
|
||||||
- La carpeta `adjuntos/` incluye un `.htaccess` que impide la ejecución de scripts.
|
- 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.
|
||||||
- Validación de tipo MIME real (no solo por extensión) al subir archivos y al guardar la firma digital.
|
- La carpeta `adjuntos/` incluye un `.htaccess` que impide la ejecución de scripts.
|
||||||
- El acceso de Desarrollador no se anuncia en la pantalla de login pública.
|
- Validación de tipo MIME real (no solo por extensión) al subir archivos y al guardar la firma digital.
|
||||||
- Se recomienda servir el sitio bajo HTTPS dado que se transmiten datos de salud.
|
- 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.
|
|
||||||
|
> 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
|
|
||||||
|
## Hoja de ruta
|
||||||
Ideas pendientes, sin compromiso de implementación:
|
|
||||||
|
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")
|
- [ ] 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)
|
||||||
- [ ] Recordatorio de turno disparado automáticamente por cron (hoy el mensaje de WhatsApp se manda a mano)
|
- [ ] Buscador único en el panel principal del profesional (DNI/nombre → resultado directo, sin entrar primero a "Acceder a legajos")
|
||||||
- [ ] Notificaciones push reales (que avisen aunque la app no esté abierta), más allá del aviso dentro del sistema
|
- [ ] Recordatorio de turno disparado automáticamente por cron (hoy el mensaje de WhatsApp se manda a mano)
|
||||||
- [ ] Notas privadas del profesional, separadas de la historia clínica formal
|
- [ ] Notificaciones push reales (que avisen aunque la app no esté abierta), más allá del aviso dentro del sistema
|
||||||
- [ ] Importación de pacientes desde Excel/CSV
|
- [ ] Notas privadas del profesional, separadas de la historia clínica formal
|
||||||
- [ ] Modo offline básico para la app instalada (consulta de solo lectura sin conexión)
|
- [ ] Importación de pacientes desde Excel/CSV
|
||||||
- [ ] Exportación de estadísticas a Excel/CSV
|
- [ ] Modo offline básico para la app instalada (consulta de solo lectura sin conexión)
|
||||||
- [ ] Internacionalización (hoy todos los textos están en español rioplatense)
|
- [ ] Exportación de estadísticas a Excel/CSV
|
||||||
|
- [ ] Internacionalización (hoy todos los textos están en español rioplatense)
|
||||||
## Licencia
|
|
||||||
|
## 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.
|
|
||||||
|
[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).
|
|
||||||
99
api/auth.php
99
api/auth.php
|
|
@ -31,6 +31,49 @@ function generarNumeroLegajo($pdo) {
|
||||||
return "LG-$anio-" . str_pad($total + 1, 3, '0', STR_PAD_LEFT);
|
return "LG-$anio-" . str_pad($total + 1, 3, '0', STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genera un sello automático tipo texto (nombre + título +
|
||||||
|
* matrícula) como imagen SVG en base64, similar a un sello de
|
||||||
|
* profesional escaneado pero generado al instante. Sirve como
|
||||||
|
* firma de partida; el profesional puede reemplazarla después
|
||||||
|
* dibujando o subiendo su propia firma/sello real.
|
||||||
|
*/
|
||||||
|
function generarSelloAutomatico($titulo, $nombre, $apellido, $especialidad, $matriculaNacional, $matriculaProvincial) {
|
||||||
|
// mb_strtoupper respeta tildes y caracteres UTF-8 (strtoupper
|
||||||
|
// normal corrompe letras como "ó" o "í" porque procesa byte
|
||||||
|
// por byte). Si por algún motivo mbstring no está disponible
|
||||||
|
// en el hosting, usamos el texto tal cual en vez de romper.
|
||||||
|
$aMayusculas = function_exists('mb_strtoupper')
|
||||||
|
? fn($t) => mb_strtoupper($t, 'UTF-8')
|
||||||
|
: fn($t) => strtoupper($t);
|
||||||
|
|
||||||
|
$nombreCompleto = htmlspecialchars("$titulo $nombre $apellido", ENT_QUOTES);
|
||||||
|
$tituloLimpio = trim(str_replace('.', '', $titulo));
|
||||||
|
$tituloEspecialidad = $especialidad ? $especialidad : ($tituloLimpio . (str_contains($titulo, 'Dr') ? ' MÉDICO' : ''));
|
||||||
|
$tituloEspecialidad = trim($aMayusculas($tituloEspecialidad));
|
||||||
|
$tituloEspecialidad = htmlspecialchars($tituloEspecialidad, ENT_QUOTES);
|
||||||
|
|
||||||
|
$lineaMatricula = '';
|
||||||
|
if ($matriculaNacional && $matriculaProvincial) {
|
||||||
|
$lineaMatricula = "M.N. $matriculaNacional - M.P. $matriculaProvincial";
|
||||||
|
} elseif ($matriculaNacional) {
|
||||||
|
$lineaMatricula = "M.N. $matriculaNacional";
|
||||||
|
} elseif ($matriculaProvincial) {
|
||||||
|
$lineaMatricula = "M.P. $matriculaProvincial";
|
||||||
|
}
|
||||||
|
$lineaMatricula = htmlspecialchars($lineaMatricula, ENT_QUOTES);
|
||||||
|
|
||||||
|
$alturaTotal = $lineaMatricula ? 130 : 100;
|
||||||
|
|
||||||
|
$svg = '<svg xmlns="http://www.w3.org/2000/svg" width="460" height="' . $alturaTotal . '" viewBox="0 0 460 ' . $alturaTotal . '"><!--sello-automatico-->'
|
||||||
|
. '<text x="230" y="44" text-anchor="middle" font-family="Georgia, \'Times New Roman\', serif" font-style="italic" font-size="34" fill="#1C2421">' . $nombreCompleto . '</text>'
|
||||||
|
. ($tituloEspecialidad ? '<text x="230" y="74" text-anchor="middle" font-family="Arial, sans-serif" font-size="17" letter-spacing="1" fill="#1C2421">' . $tituloEspecialidad . '</text>' : '')
|
||||||
|
. ($lineaMatricula ? '<text x="230" y="102" text-anchor="middle" font-family="Arial, sans-serif" font-size="17" letter-spacing="1" fill="#1C2421">' . $lineaMatricula . '</text>' : '')
|
||||||
|
. '</svg>';
|
||||||
|
|
||||||
|
return 'data:image/svg+xml;base64,' . base64_encode($svg);
|
||||||
|
}
|
||||||
|
|
||||||
function aplicarBloqueoFuerzaBruta($claveSesionIntentos, $claveSesionBloqueo) {
|
function aplicarBloqueoFuerzaBruta($claveSesionIntentos, $claveSesionBloqueo) {
|
||||||
if (!isset($_SESSION[$claveSesionIntentos])) $_SESSION[$claveSesionIntentos] = 0;
|
if (!isset($_SESSION[$claveSesionIntentos])) $_SESSION[$claveSesionIntentos] = 0;
|
||||||
if (!isset($_SESSION[$claveSesionBloqueo])) $_SESSION[$claveSesionBloqueo] = 0;
|
if (!isset($_SESSION[$claveSesionBloqueo])) $_SESSION[$claveSesionBloqueo] = 0;
|
||||||
|
|
@ -754,6 +797,8 @@ if ($accion === 'crear_usuario') {
|
||||||
$fechaNac = $input['fecha_nacimiento'] ?? null ?: null;
|
$fechaNac = $input['fecha_nacimiento'] ?? null ?: null;
|
||||||
$lugarNac = trim($input['lugar_nacimiento'] ?? '') ?: null;
|
$lugarNac = trim($input['lugar_nacimiento'] ?? '') ?: null;
|
||||||
$especialidad = trim($input['especialidad'] ?? '') ?: null;
|
$especialidad = trim($input['especialidad'] ?? '') ?: null;
|
||||||
|
$matriculaNacional = trim($input['matricula_nacional'] ?? '') ?: null;
|
||||||
|
$matriculaProvincial = trim($input['matricula_provincial'] ?? '') ?: null;
|
||||||
$email = trim($input['email'] ?? '') ?: null;
|
$email = trim($input['email'] ?? '') ?: null;
|
||||||
$telefono = trim($input['telefono'] ?? '') ?: null;
|
$telefono = trim($input['telefono'] ?? '') ?: null;
|
||||||
$licenciaDias = $input['licencia_dias'] !== '' ? (int) $input['licencia_dias'] : null;
|
$licenciaDias = $input['licencia_dias'] !== '' ? (int) $input['licencia_dias'] : null;
|
||||||
|
|
@ -769,6 +814,7 @@ if ($accion === 'crear_usuario') {
|
||||||
|
|
||||||
$nombreCompleto = "$titulo $nombre $apellido";
|
$nombreCompleto = "$titulo $nombre $apellido";
|
||||||
$hash = password_hash($pin . APP_SECRET, PASSWORD_BCRYPT);
|
$hash = password_hash($pin . APP_SECRET, PASSWORD_BCRYPT);
|
||||||
|
$selloAutomatico = generarSelloAutomatico($titulo, $nombre, $apellido, $especialidad, $matriculaNacional, $matriculaProvincial);
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
try {
|
try {
|
||||||
|
|
@ -777,8 +823,12 @@ if ($accion === 'crear_usuario') {
|
||||||
$nuevoId = $pdo->lastInsertId();
|
$nuevoId = $pdo->lastInsertId();
|
||||||
|
|
||||||
$numeroLegajo = generarNumeroLegajo($pdo);
|
$numeroLegajo = generarNumeroLegajo($pdo);
|
||||||
$stmtLegajo = $pdo->prepare('INSERT INTO profesionales_legajos (usuario_id, numero_legajo, titulo, nombre, apellido, dni, fecha_nacimiento, lugar_nacimiento, especialidad, email, telefono) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
$stmtLegajo = $pdo->prepare('
|
||||||
$stmtLegajo->execute([$nuevoId, $numeroLegajo, $titulo, $nombre, $apellido, $dni, $fechaNac, $lugarNac, $especialidad, $email, $telefono]);
|
INSERT INTO profesionales_legajos
|
||||||
|
(usuario_id, numero_legajo, titulo, nombre, apellido, dni, fecha_nacimiento, lugar_nacimiento, especialidad, matricula_nacional, matricula_provincial, email, telefono, firma_digital)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
');
|
||||||
|
$stmtLegajo->execute([$nuevoId, $numeroLegajo, $titulo, $nombre, $apellido, $dni, $fechaNac, $lugarNac, $especialidad, $matriculaNacional, $matriculaProvincial, $email, $telefono, $selloAutomatico]);
|
||||||
|
|
||||||
$stmtSede = $pdo->prepare('INSERT INTO usuarios_sedes (usuario_id, sede_id) VALUES (?, ?)');
|
$stmtSede = $pdo->prepare('INSERT INTO usuarios_sedes (usuario_id, sede_id) VALUES (?, ?)');
|
||||||
foreach ($sedeIds as $sid) $stmtSede->execute([$nuevoId, $sid]);
|
foreach ($sedeIds as $sid) $stmtSede->execute([$nuevoId, $sid]);
|
||||||
|
|
@ -880,6 +930,8 @@ if ($accion === 'editar_legajo_profesional') {
|
||||||
$fechaNac = $input['fecha_nacimiento'] ?? null ?: null;
|
$fechaNac = $input['fecha_nacimiento'] ?? null ?: null;
|
||||||
$lugarNac = trim($input['lugar_nacimiento'] ?? '') ?: null;
|
$lugarNac = trim($input['lugar_nacimiento'] ?? '') ?: null;
|
||||||
$especialidad = trim($input['especialidad'] ?? '') ?: null;
|
$especialidad = trim($input['especialidad'] ?? '') ?: null;
|
||||||
|
$matriculaNacional = trim($input['matricula_nacional'] ?? '') ?: null;
|
||||||
|
$matriculaProvincial = trim($input['matricula_provincial'] ?? '') ?: null;
|
||||||
$email = trim($input['email'] ?? '') ?: null;
|
$email = trim($input['email'] ?? '') ?: null;
|
||||||
$telefono = trim($input['telefono'] ?? '') ?: null;
|
$telefono = trim($input['telefono'] ?? '') ?: null;
|
||||||
|
|
||||||
|
|
@ -892,23 +944,48 @@ if ($accion === 'editar_legajo_profesional') {
|
||||||
$titulos = ['Dr.', 'Dra.', 'Lic.', 'Tec.', 'Mg.', 'Prof.', 'Otro'];
|
$titulos = ['Dr.', 'Dra.', 'Lic.', 'Tec.', 'Mg.', 'Prof.', 'Otro'];
|
||||||
if (!in_array($titulo, $titulos)) $titulo = 'Dr.';
|
if (!in_array($titulo, $titulos)) $titulo = 'Dr.';
|
||||||
|
|
||||||
$stmtCheck = $pdo->prepare('SELECT id FROM profesionales_legajos WHERE usuario_id = ?');
|
$stmtCheck = $pdo->prepare('SELECT id, firma_digital FROM profesionales_legajos WHERE usuario_id = ?');
|
||||||
$stmtCheck->execute([$usuarioId]);
|
$stmtCheck->execute([$usuarioId]);
|
||||||
if (!$stmtCheck->fetch()) {
|
$legajoActual = $stmtCheck->fetch();
|
||||||
|
if (!$legajoActual) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo json_encode(['ok' => false, 'error' => 'Ese legajo no existe.']);
|
echo json_encode(['ok' => false, 'error' => 'Ese legajo no existe.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Solo regeneramos el sello automático si la firma actual es
|
||||||
|
// VACÍA o sigue siendo el sello automático (no si el profesional
|
||||||
|
// ya cargó su propia firma manual/escaneada, para no pisarla).
|
||||||
|
// El sello automático siempre es un SVG (las firmas dibujadas o
|
||||||
|
// subidas son PNG/JPEG), así que primero confirmamos que sea un
|
||||||
|
// SVG antes de decodificarlo y buscar la marca interna.
|
||||||
|
$firmaActual = $legajoActual['firma_digital'] ?? '';
|
||||||
|
$esSvg = is_string($firmaActual) && str_starts_with($firmaActual, 'data:image/svg+xml;base64,');
|
||||||
|
$esSelloAutomaticoOVacio = $firmaActual === '' || $firmaActual === null
|
||||||
|
|| ($esSvg && str_contains(base64_decode(substr($firmaActual, strlen('data:image/svg+xml;base64,'))) ?: '', 'sello-automatico'));
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
try {
|
try {
|
||||||
$stmt = $pdo->prepare('
|
if ($esSelloAutomaticoOVacio) {
|
||||||
UPDATE profesionales_legajos
|
$selloNuevo = generarSelloAutomatico($titulo, $nombre, $apellido, $especialidad, $matriculaNacional, $matriculaProvincial);
|
||||||
SET titulo = ?, nombre = ?, apellido = ?, dni = ?, fecha_nacimiento = ?,
|
$stmt = $pdo->prepare('
|
||||||
lugar_nacimiento = ?, especialidad = ?, email = ?, telefono = ?
|
UPDATE profesionales_legajos
|
||||||
WHERE usuario_id = ?
|
SET titulo = ?, nombre = ?, apellido = ?, dni = ?, fecha_nacimiento = ?,
|
||||||
');
|
lugar_nacimiento = ?, especialidad = ?, matricula_nacional = ?, matricula_provincial = ?,
|
||||||
$stmt->execute([$titulo, $nombre, $apellido, $dni, $fechaNac, $lugarNac, $especialidad, $email, $telefono, $usuarioId]);
|
email = ?, telefono = ?, firma_digital = ?
|
||||||
|
WHERE usuario_id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$titulo, $nombre, $apellido, $dni, $fechaNac, $lugarNac, $especialidad, $matriculaNacional, $matriculaProvincial, $email, $telefono, $selloNuevo, $usuarioId]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
UPDATE profesionales_legajos
|
||||||
|
SET titulo = ?, nombre = ?, apellido = ?, dni = ?, fecha_nacimiento = ?,
|
||||||
|
lugar_nacimiento = ?, especialidad = ?, matricula_nacional = ?, matricula_provincial = ?,
|
||||||
|
email = ?, telefono = ?
|
||||||
|
WHERE usuario_id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$titulo, $nombre, $apellido, $dni, $fechaNac, $lugarNac, $especialidad, $matriculaNacional, $matriculaProvincial, $email, $telefono, $usuarioId]);
|
||||||
|
}
|
||||||
|
|
||||||
$nombreCompleto = "$titulo $nombre $apellido";
|
$nombreCompleto = "$titulo $nombre $apellido";
|
||||||
$pdo->prepare('UPDATE usuarios SET nombre_completo = ? WHERE id = ?')->execute([$nombreCompleto, $usuarioId]);
|
$pdo->prepare('UPDATE usuarios SET nombre_completo = ? WHERE id = ?')->execute([$nombreCompleto, $usuarioId]);
|
||||||
|
|
|
||||||
|
|
@ -1566,7 +1566,7 @@ a { color: var(--salvia); text-decoration: none; }
|
||||||
.canvas-firma {
|
.canvas-firma {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
height: 160px;
|
height: 170px;
|
||||||
background: var(--crema);
|
background: var(--crema);
|
||||||
border: 1.5px dashed var(--linea);
|
border: 1.5px dashed var(--linea);
|
||||||
border-radius: var(--radio-chico);
|
border-radius: var(--radio-chico);
|
||||||
|
|
|
||||||
136
assets/js/app.js
136
assets/js/app.js
|
|
@ -3026,6 +3026,8 @@ async function abrirModalNuevoProfesional() {
|
||||||
const fechaNac = document.getElementById('input-fechanac-prof').value;
|
const fechaNac = document.getElementById('input-fechanac-prof').value;
|
||||||
const lugarNac = document.getElementById('input-lugarnac-prof').value.trim();
|
const lugarNac = document.getElementById('input-lugarnac-prof').value.trim();
|
||||||
const especialidad = document.getElementById('input-especialidad-prof').value.trim();
|
const especialidad = document.getElementById('input-especialidad-prof').value.trim();
|
||||||
|
const matriculaNacional = document.getElementById('input-mn-prof').value.trim();
|
||||||
|
const matriculaProvincial = document.getElementById('input-mp-prof').value.trim();
|
||||||
const email = document.getElementById('input-email-prof').value.trim();
|
const email = document.getElementById('input-email-prof').value.trim();
|
||||||
const tel = document.getElementById('input-tel-prof').value.trim();
|
const tel = document.getElementById('input-tel-prof').value.trim();
|
||||||
const pin = inputPin.value.trim();
|
const pin = inputPin.value.trim();
|
||||||
|
|
@ -3045,6 +3047,7 @@ async function abrirModalNuevoProfesional() {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
accion: 'crear_usuario', rol: 'profesional', titulo, nombre, apellido,
|
accion: 'crear_usuario', rol: 'profesional', titulo, nombre, apellido,
|
||||||
dni, fecha_nacimiento: fechaNac, lugar_nacimiento: lugarNac, especialidad,
|
dni, fecha_nacimiento: fechaNac, lugar_nacimiento: lugarNac, especialidad,
|
||||||
|
matricula_nacional: matriculaNacional, matricula_provincial: matriculaProvincial,
|
||||||
email, telefono: tel, pin, sede_ids: sedeIds, licencia_dias: licenciaDias,
|
email, telefono: tel, pin, sede_ids: sedeIds, licencia_dias: licenciaDias,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -3102,7 +3105,7 @@ async function abrirModalEditarLegajoProfesional(usuario) {
|
||||||
const modalEnv = clonarPlantilla('tpl-modal-editar-legajo-profesional');
|
const modalEnv = clonarPlantilla('tpl-modal-editar-legajo-profesional');
|
||||||
document.body.appendChild(modalEnv);
|
document.body.appendChild(modalEnv);
|
||||||
|
|
||||||
const campos = ['titulo', 'nombre', 'apellido', 'dni', 'fechanac', 'lugarnac', 'especialidad', 'email', 'tel'];
|
const campos = ['titulo', 'nombre', 'apellido', 'dni', 'fechanac', 'lugarnac', 'especialidad', 'mn', 'mp', 'email', 'tel'];
|
||||||
campos.forEach(c => { document.getElementById(`input-${c}-editar-prof`).disabled = true; });
|
campos.forEach(c => { document.getElementById(`input-${c}-editar-prof`).disabled = true; });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -3118,6 +3121,8 @@ async function abrirModalEditarLegajoProfesional(usuario) {
|
||||||
document.getElementById('input-fechanac-editar-prof').value = l.fecha_nacimiento || '';
|
document.getElementById('input-fechanac-editar-prof').value = l.fecha_nacimiento || '';
|
||||||
document.getElementById('input-lugarnac-editar-prof').value = l.lugar_nacimiento || '';
|
document.getElementById('input-lugarnac-editar-prof').value = l.lugar_nacimiento || '';
|
||||||
document.getElementById('input-especialidad-editar-prof').value = l.especialidad || '';
|
document.getElementById('input-especialidad-editar-prof').value = l.especialidad || '';
|
||||||
|
document.getElementById('input-mn-editar-prof').value = l.matricula_nacional || '';
|
||||||
|
document.getElementById('input-mp-editar-prof').value = l.matricula_provincial || '';
|
||||||
document.getElementById('input-email-editar-prof').value = l.email || '';
|
document.getElementById('input-email-editar-prof').value = l.email || '';
|
||||||
document.getElementById('input-tel-editar-prof').value = l.telefono || '';
|
document.getElementById('input-tel-editar-prof').value = l.telefono || '';
|
||||||
campos.forEach(c => { document.getElementById(`input-${c}-editar-prof`).disabled = false; });
|
campos.forEach(c => { document.getElementById(`input-${c}-editar-prof`).disabled = false; });
|
||||||
|
|
@ -3141,6 +3146,8 @@ async function abrirModalEditarLegajoProfesional(usuario) {
|
||||||
fecha_nacimiento: document.getElementById('input-fechanac-editar-prof').value,
|
fecha_nacimiento: document.getElementById('input-fechanac-editar-prof').value,
|
||||||
lugar_nacimiento: document.getElementById('input-lugarnac-editar-prof').value.trim(),
|
lugar_nacimiento: document.getElementById('input-lugarnac-editar-prof').value.trim(),
|
||||||
especialidad: document.getElementById('input-especialidad-editar-prof').value.trim(),
|
especialidad: document.getElementById('input-especialidad-editar-prof').value.trim(),
|
||||||
|
matricula_nacional: document.getElementById('input-mn-editar-prof').value.trim(),
|
||||||
|
matricula_provincial: document.getElementById('input-mp-editar-prof').value.trim(),
|
||||||
email: document.getElementById('input-email-editar-prof').value.trim(),
|
email: document.getElementById('input-email-editar-prof').value.trim(),
|
||||||
telefono: document.getElementById('input-tel-editar-prof').value.trim(),
|
telefono: document.getElementById('input-tel-editar-prof').value.trim(),
|
||||||
};
|
};
|
||||||
|
|
@ -3277,25 +3284,22 @@ async function montarVistaMiLegajo(contenido) {
|
||||||
|
|
||||||
<div class="panel" style="margin-top:18px;">
|
<div class="panel" style="margin-top:18px;">
|
||||||
<div class="panel-titulo">Mi firma</div>
|
<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>
|
<p class="panel-subtitulo">Se inserta automáticamente al pie de cada legajo que exportes a PDF. Tu sello actual ya está cargado en el recuadro de abajo: firmá encima con el mouse o el dedo, o subí otra imagen de sello para reemplazarlo.</p>
|
||||||
<div id="contenedor-firma-actual" style="margin-bottom:16px;"></div>
|
<div id="contenedor-firma-actual" style="margin-bottom:12px;"></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>
|
<div class="campo" style="margin-bottom:12px; max-width: 360px;">
|
||||||
<button class="btn btn-secundario btn-chico" id="tab-subir-firma" type="button">Subir imagen</button>
|
<label>Reemplazar el sello de fondo (opcional)</label>
|
||||||
|
<input type="file" id="input-imagen-fondo-firma" accept="image/png,image/jpeg">
|
||||||
|
<span class="ayuda">Subí una foto de tu propio sello si querés cambiar el que ya está.</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="panel-dibujar-firma">
|
|
||||||
<canvas id="canvas-firma" width="500" height="160" class="canvas-firma"></canvas>
|
<canvas id="canvas-firma" width="500" height="170" class="canvas-firma"></canvas>
|
||||||
<div style="display:flex; gap:10px; margin-top:10px;">
|
<div style="display:flex; gap:10px; margin-top:10px; flex-wrap:wrap;">
|
||||||
<button class="btn btn-secundario btn-chico" id="btn-limpiar-firma" type="button">Limpiar</button>
|
<button class="btn btn-secundario btn-chico" id="btn-quitar-imagen-fondo" type="button">Quitar imagen de fondo</button>
|
||||||
<button class="btn btn-primario btn-chico" id="btn-guardar-firma-dibujada" type="button">Guardar firma</button>
|
<button class="btn btn-secundario btn-chico" id="btn-limpiar-firma" type="button">Limpiar todo</button>
|
||||||
</div>
|
<button class="btn btn-primario btn-chico" id="btn-guardar-firma-dibujada" type="button">Guardar firma</button>
|
||||||
</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>
|
</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>' : ''}
|
${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>`;
|
</div>`;
|
||||||
|
|
||||||
|
|
@ -3307,32 +3311,10 @@ async function montarVistaMiLegajo(contenido) {
|
||||||
|
|
||||||
function inicializarModuloFirma(firmaActual) {
|
function inicializarModuloFirma(firmaActual) {
|
||||||
const contActual = document.getElementById('contenedor-firma-actual');
|
const contActual = document.getElementById('contenedor-firma-actual');
|
||||||
if (firmaActual) {
|
contActual.innerHTML = firmaActual
|
||||||
contActual.innerHTML = `<div class="vista-firma-actual"><img src="${firmaActual}" alt="Tu firma actual"></div>`;
|
? '<p class="ayuda" style="margin:0 0 4px;">Tu sello/firma actual ya está cargado abajo. Podés firmar encima, agrandar el sello reemplazándolo, o limpiar todo y empezar de nuevo.</p>'
|
||||||
} else {
|
: '<p class="resumen-vacio" style="margin:0;">Todavía no tenés ninguna firma guardada.</p>';
|
||||||
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 canvas = document.getElementById('canvas-firma');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.lineWidth = 2.4;
|
ctx.lineWidth = 2.4;
|
||||||
|
|
@ -3340,6 +3322,29 @@ function inicializarModuloFirma(firmaActual) {
|
||||||
ctx.strokeStyle = '#1C2421';
|
ctx.strokeStyle = '#1C2421';
|
||||||
let dibujando = false;
|
let dibujando = false;
|
||||||
let huboTrazo = false;
|
let huboTrazo = false;
|
||||||
|
let imagenFondo = null; // Image cargada como fondo, o null si no hay
|
||||||
|
|
||||||
|
function redibujarFondo() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
if (imagenFondo) {
|
||||||
|
// Centramos la imagen de fondo manteniendo su proporción.
|
||||||
|
const escala = Math.min(canvas.width / imagenFondo.width, canvas.height / imagenFondo.height);
|
||||||
|
const w = imagenFondo.width * escala;
|
||||||
|
const h = imagenFondo.height * escala;
|
||||||
|
ctx.drawImage(imagenFondo, (canvas.width - w) / 2, (canvas.height - h) / 2, w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precargamos la firma/sello actual como fondo, para que el
|
||||||
|
// profesional pueda firmar directamente encima sin perderla.
|
||||||
|
if (firmaActual) {
|
||||||
|
const imgPrevia = new Image();
|
||||||
|
imgPrevia.onload = () => {
|
||||||
|
imagenFondo = imgPrevia;
|
||||||
|
redibujarFondo();
|
||||||
|
};
|
||||||
|
imgPrevia.src = firmaActual;
|
||||||
|
}
|
||||||
|
|
||||||
function posicionDesdeEvento(e) {
|
function posicionDesdeEvento(e) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
@ -3373,27 +3378,48 @@ function inicializarModuloFirma(firmaActual) {
|
||||||
canvas.addEventListener('touchmove', seguirTrazo, { passive: false });
|
canvas.addEventListener('touchmove', seguirTrazo, { passive: false });
|
||||||
canvas.addEventListener('touchend', terminarTrazo);
|
canvas.addEventListener('touchend', terminarTrazo);
|
||||||
|
|
||||||
|
// --- Cargar imagen de fondo (sello escaneado) ---
|
||||||
|
document.getElementById('input-imagen-fondo-firma').addEventListener('change', (e) => {
|
||||||
|
const archivo = e.target.files && e.target.files[0];
|
||||||
|
if (!archivo) return;
|
||||||
|
if (archivo.size > 1.5 * 1024 * 1024) {
|
||||||
|
mostrarToast('La imagen pesa demasiado. Probá con una más liviana.', 'error');
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lector = new FileReader();
|
||||||
|
lector.onload = () => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
imagenFondo = img;
|
||||||
|
redibujarFondo();
|
||||||
|
};
|
||||||
|
img.src = lector.result;
|
||||||
|
};
|
||||||
|
lector.readAsDataURL(archivo);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-quitar-imagen-fondo').addEventListener('click', () => {
|
||||||
|
imagenFondo = null;
|
||||||
|
document.getElementById('input-imagen-fondo-firma').value = '';
|
||||||
|
redibujarFondo();
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('btn-limpiar-firma').addEventListener('click', () => {
|
document.getElementById('btn-limpiar-firma').addEventListener('click', () => {
|
||||||
|
imagenFondo = null;
|
||||||
|
document.getElementById('input-imagen-fondo-firma').value = '';
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
huboTrazo = false;
|
huboTrazo = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-guardar-firma-dibujada').addEventListener('click', async () => {
|
document.getElementById('btn-guardar-firma-dibujada').addEventListener('click', async () => {
|
||||||
if (!huboTrazo) { mostrarToast('Dibujá tu firma antes de guardar.', 'error'); return; }
|
if (!huboTrazo && !imagenFondo) {
|
||||||
|
mostrarToast('Dibujá tu firma, o subí una imagen de tu sello, antes de guardar.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await guardarFirma(canvas.toDataURL('image/png'));
|
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');
|
const btnQuitar = document.getElementById('btn-quitar-firma');
|
||||||
if (btnQuitar) {
|
if (btnQuitar) {
|
||||||
btnQuitar.addEventListener('click', async () => {
|
btnQuitar.addEventListener('click', async () => {
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ CREATE TABLE IF NOT EXISTS profesionales_legajos (
|
||||||
fecha_nacimiento DATE NULL,
|
fecha_nacimiento DATE NULL,
|
||||||
lugar_nacimiento VARCHAR(150) NULL,
|
lugar_nacimiento VARCHAR(150) NULL,
|
||||||
especialidad VARCHAR(150) NULL,
|
especialidad VARCHAR(150) NULL,
|
||||||
|
matricula_nacional VARCHAR(30) NULL,
|
||||||
|
matricula_provincial VARCHAR(30) NULL,
|
||||||
email VARCHAR(150) NULL,
|
email VARCHAR(150) NULL,
|
||||||
telefono VARCHAR(40) NULL,
|
telefono VARCHAR(40) NULL,
|
||||||
firma_digital MEDIUMTEXT NULL,
|
firma_digital MEDIUMTEXT NULL,
|
||||||
|
|
|
||||||
|
|
@ -166,12 +166,12 @@ $edad = calcularEdadExport($paciente['fecha_nacimiento']);
|
||||||
|
|
||||||
.bloque-firma {
|
.bloque-firma {
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
width: 260px;
|
width: 300px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.imagen-firma {
|
.imagen-firma {
|
||||||
max-width: 220px;
|
max-width: 280px;
|
||||||
max-height: 80px;
|
max-height: 110px;
|
||||||
margin: 0 auto 4px;
|
margin: 0 auto 4px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
index.html
16
index.html
|
|
@ -910,6 +910,14 @@
|
||||||
<label>Especialidad</label>
|
<label>Especialidad</label>
|
||||||
<input type="text" id="input-especialidad-prof" placeholder="Ej: Fonoaudiología, Traumatología…">
|
<input type="text" id="input-especialidad-prof" placeholder="Ej: Fonoaudiología, Traumatología…">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="campo">
|
||||||
|
<label>Matrícula nacional (M.N.)</label>
|
||||||
|
<input type="text" id="input-mn-prof" placeholder="Opcional, ej: 2020">
|
||||||
|
</div>
|
||||||
|
<div class="campo">
|
||||||
|
<label>Matrícula provincial (M.P.)</label>
|
||||||
|
<input type="text" id="input-mp-prof" placeholder="Opcional, ej: 2080">
|
||||||
|
</div>
|
||||||
<div class="campo">
|
<div class="campo">
|
||||||
<label>Correo electrónico</label>
|
<label>Correo electrónico</label>
|
||||||
<input type="email" id="input-email-prof" placeholder="Ej: arian@clinica.com">
|
<input type="email" id="input-email-prof" placeholder="Ej: arian@clinica.com">
|
||||||
|
|
@ -995,6 +1003,14 @@
|
||||||
<label>Especialidad</label>
|
<label>Especialidad</label>
|
||||||
<input type="text" id="input-especialidad-editar-prof">
|
<input type="text" id="input-especialidad-editar-prof">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="campo">
|
||||||
|
<label>Matrícula nacional (M.N.)</label>
|
||||||
|
<input type="text" id="input-mn-editar-prof" placeholder="Opcional">
|
||||||
|
</div>
|
||||||
|
<div class="campo">
|
||||||
|
<label>Matrícula provincial (M.P.)</label>
|
||||||
|
<input type="text" id="input-mp-editar-prof" placeholder="Opcional">
|
||||||
|
</div>
|
||||||
<div class="campo">
|
<div class="campo">
|
||||||
<label>Correo electrónico</label>
|
<label>Correo electrónico</label>
|
||||||
<input type="email" id="input-email-editar-prof">
|
<input type="email" id="input-email-editar-prof">
|
||||||
|
|
|
||||||
20
migracion_v13.sql
Normal file
20
migracion_v13.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
-- ============================================================
|
||||||
|
-- Del Austral — Migración de base de datos (versión 13)
|
||||||
|
-- ============================================================
|
||||||
|
-- Agrega a "profesionales_legajos" las columnas de matrícula
|
||||||
|
-- nacional (M.N.) y provincial (M.P.), ambas opcionales. Se
|
||||||
|
-- usan para armar el sello/firma automática del profesional
|
||||||
|
-- (ej: "M.N. 2020 - M.P. 2080" si tiene las dos).
|
||||||
|
--
|
||||||
|
-- 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 matricula_nacional VARCHAR(30) NULL AFTER especialidad;
|
||||||
|
ALTER TABLE profesionales_legajos ADD COLUMN matricula_provincial VARCHAR(30) NULL AFTER matricula_nacional;
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
-- Del Austral — REINICIO TOTAL DEL SISTEMA
|
-- Del Austral — REINICIO TOTAL DEL SISTEMA
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Este script borra TODO: usuarios, sedes, clave de Desarrollador,
|
-- Este script borra TODO: usuarios, sedes, clave de Desarrollador,
|
||||||
-- pacientes, sesiones, citas, archivos adjuntos (los registros en
|
-- legajos completos de profesionales (datos personales, matrícula,
|
||||||
-- la base, no los archivos físicos en el servidor), plantillas de
|
-- firma digital), pacientes, sesiones, citas, archivos adjuntos
|
||||||
-- evolución, papelera, e historial de cambios.
|
-- (los registros en la base, no los archivos físicos en el
|
||||||
|
-- servidor), plantillas de evolución, papelera, e historial de
|
||||||
|
-- cambios.
|
||||||
--
|
--
|
||||||
-- Después de correr esto, el sistema vuelve a quedar como recién
|
-- Después de correr esto, el sistema vuelve a quedar como recién
|
||||||
-- instalado: la primera vez que entres te va a pedir crear la
|
-- instalado: la primera vez que entres te va a pedir crear la
|
||||||
|
|
@ -35,6 +37,7 @@ DELETE FROM citas;
|
||||||
DELETE FROM sesiones;
|
DELETE FROM sesiones;
|
||||||
DELETE FROM pacientes;
|
DELETE FROM pacientes;
|
||||||
DELETE FROM plantillas_evolucion;
|
DELETE FROM plantillas_evolucion;
|
||||||
|
DELETE FROM profesionales_legajos;
|
||||||
DELETE FROM usuarios_sedes;
|
DELETE FROM usuarios_sedes;
|
||||||
DELETE FROM usuarios;
|
DELETE FROM usuarios;
|
||||||
DELETE FROM sedes;
|
DELETE FROM sedes;
|
||||||
|
|
@ -50,6 +53,7 @@ ALTER TABLE citas AUTO_INCREMENT = 1;
|
||||||
ALTER TABLE sesiones AUTO_INCREMENT = 1;
|
ALTER TABLE sesiones AUTO_INCREMENT = 1;
|
||||||
ALTER TABLE pacientes AUTO_INCREMENT = 1;
|
ALTER TABLE pacientes AUTO_INCREMENT = 1;
|
||||||
ALTER TABLE plantillas_evolucion AUTO_INCREMENT = 1;
|
ALTER TABLE plantillas_evolucion AUTO_INCREMENT = 1;
|
||||||
|
ALTER TABLE profesionales_legajos AUTO_INCREMENT = 1;
|
||||||
ALTER TABLE usuarios AUTO_INCREMENT = 1;
|
ALTER TABLE usuarios AUTO_INCREMENT = 1;
|
||||||
ALTER TABLE sedes AUTO_INCREMENT = 1;
|
ALTER TABLE sedes AUTO_INCREMENT = 1;
|
||||||
ALTER TABLE desarrollador AUTO_INCREMENT = 1;
|
ALTER TABLE desarrollador AUTO_INCREMENT = 1;
|
||||||
|
|
|
||||||
14
version.json
14
version.json
|
|
@ -1,17 +1,17 @@
|
||||||
{
|
{
|
||||||
"version": "v23",
|
"version": "v28",
|
||||||
"fecha": "2026-06-30",
|
"fecha": "2026-06-30",
|
||||||
"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",
|
"descripcion": "El sello automático ahora muestra el título junto al nombre (ej: 'Dr. Juan Pérez'), con la especialidad sola debajo, como en un sello real",
|
||||||
"archivos_criticos": {
|
"archivos_criticos": {
|
||||||
"index.html": "5477853e2519ab620dbd61f81e60a537",
|
"index.html": "36c702a69103dc0a12acfa0ab669c0c8",
|
||||||
"assets/js/app.js": "ea6de159fd836d8b09fc4c424e51966a",
|
"assets/js/app.js": "d07d9460be9f1998061979aa5e96f8ea",
|
||||||
"assets/css/estilos.css": "9091b4865695cfa01fcbfd9758ad954d",
|
"assets/css/estilos.css": "b4b8ff61d69a0782528ccdc3d4ad4e78",
|
||||||
"api/auth.php": "6f7e7013eec4f93a482981668ff6e754",
|
"api/auth.php": "ae4c6fa7926cce80520b6ccb02f98ce0",
|
||||||
"api/citas.php": "7622c3facffd9fb671fa63494be56124",
|
"api/citas.php": "7622c3facffd9fb671fa63494be56124",
|
||||||
"api/pacientes.php": "df7de793317e8229120119a5050c44c2",
|
"api/pacientes.php": "df7de793317e8229120119a5050c44c2",
|
||||||
"api/adjuntos.php": "8da3f85e26239072953298c60dcb5540",
|
"api/adjuntos.php": "8da3f85e26239072953298c60dcb5540",
|
||||||
"api/admin.php": "b0f9dbf5039e62dc5c0692268ce46613",
|
"api/admin.php": "b0f9dbf5039e62dc5c0692268ce46613",
|
||||||
"confirmar_turno.php": "ad6a798a41a594bb5dcc797295222cce",
|
"confirmar_turno.php": "ad6a798a41a594bb5dcc797295222cce",
|
||||||
"exportar.php": "a10f27743c3af92dcf8fa3f364efdae9"
|
"exportar.php": "1f6661903954229762d858d2f179da94"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user