Migración a la versión más estable de Gitea en Del Austral
Signed-off-by: Keiko <keiko@delaustral.com>
This commit is contained in:
parent
25fd0859f4
commit
78f46fa0c6
270
api/adjuntos.php
Normal file
270
api/adjuntos.php
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config/config.php';
|
||||||
|
requiereSesion();
|
||||||
|
requiereProfesionalActivo();
|
||||||
|
|
||||||
|
$pdo = obtenerConexion();
|
||||||
|
$metodo = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$accion = $_GET['accion'] ?? '';
|
||||||
|
$profesionalActivoId = idProfesionalActivo();
|
||||||
|
|
||||||
|
define('CARPETA_ADJUNTOS', __DIR__ . '/../adjuntos/');
|
||||||
|
define('TIPOS_PERMITIDOS', [
|
||||||
|
'application/pdf' => 'pdf',
|
||||||
|
'image/jpeg' => 'jpg',
|
||||||
|
'image/png' => 'png',
|
||||||
|
'image/webp' => 'webp',
|
||||||
|
]);
|
||||||
|
define('TAMANIO_MAXIMO', 15 * 1024 * 1024); // 15 MB
|
||||||
|
|
||||||
|
function pacienteEsDelProfesional($pdo, $pacienteId, $profesionalActivoId) {
|
||||||
|
$stmt = $pdo->prepare('SELECT 1 FROM pacientes WHERE id = ? AND profesional_id = ?');
|
||||||
|
$stmt->execute([$pacienteId, $profesionalActivoId]);
|
||||||
|
return (bool) $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte un valor de php.ini como "8M", "2G" o "512K" a bytes.
|
||||||
|
* Si ya es un número plano, lo devuelve tal cual.
|
||||||
|
*/
|
||||||
|
function convertirAByte($valor) {
|
||||||
|
$valor = trim((string) $valor);
|
||||||
|
if ($valor === '' || $valor === '-1') return 0; // sin límite configurado
|
||||||
|
$unidad = strtoupper(substr($valor, -1));
|
||||||
|
$numero = (float) $valor;
|
||||||
|
switch ($unidad) {
|
||||||
|
case 'G': return (int) ($numero * 1024 * 1024 * 1024);
|
||||||
|
case 'M': return (int) ($numero * 1024 * 1024);
|
||||||
|
case 'K': return (int) ($numero * 1024);
|
||||||
|
default: return (int) $valor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Da un texto legible para humanos a partir de una cantidad de bytes.
|
||||||
|
*/
|
||||||
|
function formatearBytesLegible($bytes) {
|
||||||
|
if ($bytes <= 0) return 'sin límite definido';
|
||||||
|
if ($bytes >= 1024 * 1024 * 1024) return round($bytes / (1024 * 1024 * 1024), 1) . ' GB';
|
||||||
|
if ($bytes >= 1024 * 1024) return round($bytes / (1024 * 1024), 1) . ' MB';
|
||||||
|
return round($bytes / 1024, 1) . ' KB';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// SUBIR ARCHIVO (POST ?accion=subir) — multipart/form-data
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'POST' && $accion === 'subir') {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
requiereRolProfesional();
|
||||||
|
|
||||||
|
// Si el archivo (o el POST completo) superó los límites del
|
||||||
|
// propio servidor (php.ini: upload_max_filesize / post_max_size),
|
||||||
|
// PHP vacía $_FILES sin avisar el motivo real. Lo detectamos
|
||||||
|
// comparando contra esos límites para poder dar un mensaje claro,
|
||||||
|
// en vez de un error confuso de "no se recibió ningún archivo".
|
||||||
|
if (empty($_FILES) && empty($_POST) && (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
|
||||||
|
$limitePost = convertirAByte(ini_get('post_max_size'));
|
||||||
|
http_response_code(413);
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'El archivo es demasiado grande para este servidor (el límite actual es ' . formatearBytesLegible($limitePost) . '). Probá con un archivo más chico, o pedile a quien administre el hosting que aumente el límite de subida en PHP.',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_FILES['archivo'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No se recibió ningún archivo.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$pacienteId = $_POST['paciente_id'] ?? 0;
|
||||||
|
$sesionId = $_POST['sesion_id'] ?? null;
|
||||||
|
$descripcion = trim($_POST['descripcion'] ?? '');
|
||||||
|
|
||||||
|
if (!$pacienteId) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Falta el paciente al que pertenece el archivo.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pacienteEsDelProfesional($pdo, $pacienteId, $profesionalActivoId)) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Paciente no encontrado.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$archivo = $_FILES['archivo'];
|
||||||
|
if ($archivo['error'] === UPLOAD_ERR_INI_SIZE || $archivo['error'] === UPLOAD_ERR_FORM_SIZE) {
|
||||||
|
$limiteUpload = convertirAByte(ini_get('upload_max_filesize'));
|
||||||
|
http_response_code(413);
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'El archivo supera el límite de subida configurado en este servidor (' . formatearBytesLegible($limiteUpload) . '). Probá con un archivo más chico.',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($archivo['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Error al subir el archivo. Intentá nuevamente.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($archivo['size'] > TAMANIO_MAXIMO) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'El archivo supera el tamaño máximo permitido (15 MB).']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('finfo_open')) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'El servidor no tiene habilitada la extensión PHP "fileinfo", necesaria para verificar el tipo de archivo. Pedile a tu hosting que la active (suele estar en "Seleccionar versión de PHP" → extensiones).',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
if ($finfo === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No se pudo verificar el tipo de archivo en este servidor. Intentá nuevamente o avisá a soporte.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$mimeReal = finfo_file($finfo, $archivo['tmp_name']);
|
||||||
|
finfo_close($finfo);
|
||||||
|
|
||||||
|
if ($mimeReal === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No se pudo leer el archivo subido. Probá nuevamente.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset(TIPOS_PERMITIDOS[$mimeReal])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Tipo de archivo no permitido. Solo se aceptan PDF, JPG, PNG o WEBP.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = TIPOS_PERMITIDOS[$mimeReal];
|
||||||
|
$nombreArchivo = bin2hex(random_bytes(16)) . '.' . $extension;
|
||||||
|
$rutaDestino = CARPETA_ADJUNTOS . $nombreArchivo;
|
||||||
|
|
||||||
|
if (!is_dir(CARPETA_ADJUNTOS)) {
|
||||||
|
mkdir(CARPETA_ADJUNTOS, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!move_uploaded_file($archivo['tmp_name'], $rutaDestino)) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No se pudo guardar el archivo en el servidor.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
INSERT INTO archivos_adjuntos (paciente_id, sesion_id, nombre_original, nombre_archivo, tipo_mime, tamanio_bytes, descripcion)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
');
|
||||||
|
$stmt->execute([
|
||||||
|
$pacienteId,
|
||||||
|
$sesionId ?: null,
|
||||||
|
$archivo['name'],
|
||||||
|
$nombreArchivo,
|
||||||
|
$mimeReal,
|
||||||
|
$archivo['size'],
|
||||||
|
$descripcion ?: null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'id' => $pdo->lastInsertId(), 'nombre_archivo' => $nombreArchivo]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// LISTAR ADJUNTOS DE UN PACIENTE (GET ?accion=listar&paciente_id=X)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'GET' && $accion === 'listar') {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
requiereRolProfesional();
|
||||||
|
$pacienteId = $_GET['paciente_id'] ?? 0;
|
||||||
|
|
||||||
|
if (!pacienteEsDelProfesional($pdo, $pacienteId, $profesionalActivoId)) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Paciente no encontrado.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM archivos_adjuntos WHERE paciente_id = ? ORDER BY subido_en DESC');
|
||||||
|
$stmt->execute([$pacienteId]);
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// DESCARGAR / VER ARCHIVO (GET ?accion=ver&id=X)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'GET' && $accion === 'ver') {
|
||||||
|
requiereRolProfesional();
|
||||||
|
$id = $_GET['id'] ?? 0;
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT a.* FROM archivos_adjuntos a
|
||||||
|
INNER JOIN pacientes p ON p.id = a.paciente_id
|
||||||
|
WHERE a.id = ? AND p.profesional_id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$id, $profesionalActivoId]);
|
||||||
|
$archivo = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$archivo) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo 'Archivo no encontrado.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ruta = CARPETA_ADJUNTOS . $archivo['nombre_archivo'];
|
||||||
|
if (!file_exists($ruta)) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo 'El archivo ya no está disponible en el servidor.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: ' . $archivo['tipo_mime']);
|
||||||
|
header('Content-Disposition: inline; filename="' . basename($archivo['nombre_original']) . '"');
|
||||||
|
header('Content-Length: ' . filesize($ruta));
|
||||||
|
readfile($ruta);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// ELIMINAR ADJUNTO (POST ?accion=eliminar)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'POST' && $accion === 'eliminar') {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
requiereRolProfesional();
|
||||||
|
$d = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $d['id'] ?? 0;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT a.* FROM archivos_adjuntos a
|
||||||
|
INNER JOIN pacientes p ON p.id = a.paciente_id
|
||||||
|
WHERE a.id = ? AND p.profesional_id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$id, $profesionalActivoId]);
|
||||||
|
$archivo = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$archivo) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Archivo no encontrado.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ruta = CARPETA_ADJUNTOS . $archivo['nombre_archivo'];
|
||||||
|
if (file_exists($ruta)) {
|
||||||
|
unlink($ruta);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtBorrar = $pdo->prepare('DELETE FROM archivos_adjuntos WHERE id = ?');
|
||||||
|
$stmtBorrar->execute([$id]);
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Solicitud no válida.']);
|
||||||
243
api/admin.php
Normal file
243
api/admin.php
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config/config.php';
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
requiereSesion();
|
||||||
|
|
||||||
|
$pdo = obtenerConexion();
|
||||||
|
$accion = $_GET['accion'] ?? '';
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// HISTORIAL DE CAMBIOS (GET ?accion=historial&pagina=1)
|
||||||
|
// Solo se muestran cambios sobre pacientes/sesiones que
|
||||||
|
// pertenecen al profesional activo.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'historial') {
|
||||||
|
requiereRolProfesional();
|
||||||
|
$profesionalActivoId = idProfesionalActivo();
|
||||||
|
$pagina = max(1, (int) ($_GET['pagina'] ?? 1));
|
||||||
|
$porPagina = 40;
|
||||||
|
$offset = ($pagina - 1) * $porPagina;
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT h.* FROM historial_cambios h
|
||||||
|
WHERE
|
||||||
|
(h.entidad = 'paciente' AND h.entidad_id IN (SELECT id FROM pacientes WHERE profesional_id = ?))
|
||||||
|
OR (h.entidad = 'sesion' AND h.entidad_id IN (
|
||||||
|
SELECT s.id FROM sesiones s INNER JOIN pacientes p ON p.id = s.paciente_id WHERE p.profesional_id = ?
|
||||||
|
))
|
||||||
|
OR h.usuario_id = ?
|
||||||
|
ORDER BY h.creado_en DESC
|
||||||
|
LIMIT $porPagina OFFSET $offset
|
||||||
|
";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$profesionalActivoId, $profesionalActivoId, $_SESSION['usuario_id']]);
|
||||||
|
$datos = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$sqlTotal = "
|
||||||
|
SELECT COUNT(*) AS total FROM historial_cambios h
|
||||||
|
WHERE
|
||||||
|
(h.entidad = 'paciente' AND h.entidad_id IN (SELECT id FROM pacientes WHERE profesional_id = ?))
|
||||||
|
OR (h.entidad = 'sesion' AND h.entidad_id IN (
|
||||||
|
SELECT s.id FROM sesiones s INNER JOIN pacientes p ON p.id = s.paciente_id WHERE p.profesional_id = ?
|
||||||
|
))
|
||||||
|
OR h.usuario_id = ?
|
||||||
|
";
|
||||||
|
$stmtTotal = $pdo->prepare($sqlTotal);
|
||||||
|
$stmtTotal->execute([$profesionalActivoId, $profesionalActivoId, $_SESSION['usuario_id']]);
|
||||||
|
$total = $stmtTotal->fetch()['total'];
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $datos, 'total' => (int)$total, 'pagina' => $pagina]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// DASHBOARD DE ESTADÍSTICAS (GET ?accion=estadisticas)
|
||||||
|
// Acotado siempre al profesional activo.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'estadisticas') {
|
||||||
|
requiereRolProfesional();
|
||||||
|
$profesionalActivoId = idProfesionalActivo();
|
||||||
|
$stmtTotal = $pdo->prepare('SELECT COUNT(*) AS t FROM pacientes WHERE profesional_id = ?');
|
||||||
|
$stmtTotal->execute([$profesionalActivoId]);
|
||||||
|
$totalPacientes = $stmtTotal->fetch()['t'];
|
||||||
|
|
||||||
|
$stmtObras = $pdo->prepare('
|
||||||
|
SELECT COALESCE(o.nombre, "Sin especificar") AS nombre, COUNT(*) AS total
|
||||||
|
FROM pacientes p
|
||||||
|
LEFT JOIN obras_sociales o ON o.id = p.obra_social_id
|
||||||
|
WHERE p.profesional_id = ?
|
||||||
|
GROUP BY o.id
|
||||||
|
ORDER BY total DESC
|
||||||
|
LIMIT 8
|
||||||
|
');
|
||||||
|
$stmtObras->execute([$profesionalActivoId]);
|
||||||
|
$porObraSocial = $stmtObras->fetchAll();
|
||||||
|
|
||||||
|
$stmtSesionesMes = $pdo->prepare("
|
||||||
|
SELECT COUNT(*) AS t FROM sesiones s
|
||||||
|
INNER JOIN pacientes p ON p.id = s.paciente_id
|
||||||
|
WHERE p.profesional_id = ? AND s.fecha_sesion BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())
|
||||||
|
");
|
||||||
|
$stmtSesionesMes->execute([$profesionalActivoId]);
|
||||||
|
$sesionesEsteMes = $stmtSesionesMes->fetch()['t'];
|
||||||
|
|
||||||
|
$stmtSesionesMesAnterior = $pdo->prepare("
|
||||||
|
SELECT COUNT(*) AS t FROM sesiones s
|
||||||
|
INNER JOIN pacientes p ON p.id = s.paciente_id
|
||||||
|
WHERE p.profesional_id = ?
|
||||||
|
AND s.fecha_sesion BETWEEN DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')
|
||||||
|
AND LAST_DAY(DATE_SUB(CURDATE(), INTERVAL 1 MONTH))
|
||||||
|
");
|
||||||
|
$stmtSesionesMesAnterior->execute([$profesionalActivoId]);
|
||||||
|
$sesionesMesAnterior = $stmtSesionesMesAnterior->fetch()['t'];
|
||||||
|
|
||||||
|
$stmtCitasEstado = $pdo->prepare("
|
||||||
|
SELECT estado, COUNT(*) AS total FROM citas
|
||||||
|
WHERE profesional_id = ? AND fecha BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())
|
||||||
|
GROUP BY estado
|
||||||
|
");
|
||||||
|
$stmtCitasEstado->execute([$profesionalActivoId]);
|
||||||
|
$citasPorEstadoEsteMes = $stmtCitasEstado->fetchAll();
|
||||||
|
|
||||||
|
$stmtNuevos = $pdo->prepare("
|
||||||
|
SELECT COUNT(*) AS t FROM pacientes
|
||||||
|
WHERE profesional_id = ? AND creado_en BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())
|
||||||
|
");
|
||||||
|
$stmtNuevos->execute([$profesionalActivoId]);
|
||||||
|
$pacientesNuevosEsteMes = $stmtNuevos->fetch()['t'];
|
||||||
|
|
||||||
|
$stmtUltimos6 = $pdo->prepare("
|
||||||
|
SELECT DATE_FORMAT(s.fecha_sesion, '%Y-%m') AS mes, COUNT(*) AS total
|
||||||
|
FROM sesiones s
|
||||||
|
INNER JOIN pacientes p ON p.id = s.paciente_id
|
||||||
|
WHERE p.profesional_id = ? AND s.fecha_sesion >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
|
||||||
|
GROUP BY mes
|
||||||
|
ORDER BY mes ASC
|
||||||
|
");
|
||||||
|
$stmtUltimos6->execute([$profesionalActivoId]);
|
||||||
|
$sesionesUltimos6Meses = $stmtUltimos6->fetchAll();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'datos' => [
|
||||||
|
'total_pacientes' => (int) $totalPacientes,
|
||||||
|
'pacientes_nuevos_mes' => (int) $pacientesNuevosEsteMes,
|
||||||
|
'por_obra_social' => $porObraSocial,
|
||||||
|
'sesiones_este_mes' => (int) $sesionesEsteMes,
|
||||||
|
'sesiones_mes_anterior' => (int) $sesionesMesAnterior,
|
||||||
|
'citas_por_estado_mes' => $citasPorEstadoEsteMes,
|
||||||
|
'sesiones_ultimos_6_meses' => $sesionesUltimos6Meses,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// REPORTES POR SEDE (GET ?accion=reporte_sedes) — solo
|
||||||
|
// desarrollador. Resumen agregado de cada sede activa: cuántos
|
||||||
|
// profesionales atienden ahí, total de pacientes, sesiones de
|
||||||
|
// este mes y citas por estado de este mes.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'reporte_sedes') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
|
||||||
|
$stmtSedes = $pdo->query('SELECT id, nombre FROM sedes WHERE activa = 1 ORDER BY nombre ASC');
|
||||||
|
$sedes = $stmtSedes->fetchAll();
|
||||||
|
|
||||||
|
$stmtProfesionales = $pdo->prepare("
|
||||||
|
SELECT COUNT(DISTINCT u.id) AS t FROM usuarios u
|
||||||
|
INNER JOIN usuarios_sedes us ON us.usuario_id = u.id
|
||||||
|
WHERE us.sede_id = ? AND u.rol = 'profesional' AND u.activo = 1
|
||||||
|
");
|
||||||
|
$stmtAdministrativas = $pdo->prepare("
|
||||||
|
SELECT COUNT(DISTINCT u.id) AS t FROM usuarios u
|
||||||
|
INNER JOIN usuarios_sedes us ON us.usuario_id = u.id
|
||||||
|
WHERE us.sede_id = ? AND u.rol = 'administrativa' AND u.activo = 1
|
||||||
|
");
|
||||||
|
$stmtPacientes = $pdo->prepare('SELECT COUNT(*) AS t FROM pacientes WHERE sede_id = ?');
|
||||||
|
$stmtSesionesMes = $pdo->prepare("
|
||||||
|
SELECT COUNT(*) AS t FROM sesiones s
|
||||||
|
INNER JOIN pacientes p ON p.id = s.paciente_id
|
||||||
|
WHERE p.sede_id = ? AND s.fecha_sesion BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())
|
||||||
|
");
|
||||||
|
$stmtCitasMes = $pdo->prepare("
|
||||||
|
SELECT c.estado, COUNT(*) AS total FROM citas c
|
||||||
|
INNER JOIN pacientes p ON p.id = c.paciente_id
|
||||||
|
WHERE p.sede_id = ? AND c.fecha BETWEEN DATE_FORMAT(CURDATE(), '%Y-%m-01') AND LAST_DAY(CURDATE())
|
||||||
|
GROUP BY c.estado
|
||||||
|
");
|
||||||
|
|
||||||
|
$resultado = [];
|
||||||
|
foreach ($sedes as $sede) {
|
||||||
|
$stmtProfesionales->execute([$sede['id']]);
|
||||||
|
$stmtAdministrativas->execute([$sede['id']]);
|
||||||
|
$stmtPacientes->execute([$sede['id']]);
|
||||||
|
$stmtSesionesMes->execute([$sede['id']]);
|
||||||
|
$stmtCitasMes->execute([$sede['id']]);
|
||||||
|
|
||||||
|
$resultado[] = [
|
||||||
|
'id' => $sede['id'],
|
||||||
|
'nombre' => $sede['nombre'],
|
||||||
|
'profesionales' => (int) $stmtProfesionales->fetch()['t'],
|
||||||
|
'administrativas' => (int) $stmtAdministrativas->fetch()['t'],
|
||||||
|
'pacientes' => (int) $stmtPacientes->fetch()['t'],
|
||||||
|
'sesiones_mes' => (int) $stmtSesionesMes->fetch()['t'],
|
||||||
|
'citas_por_estado_mes' => $stmtCitasMes->fetchAll(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $resultado]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// VERIFICAR VERSIÓN (GET ?accion=verificar_version) — solo
|
||||||
|
// desarrollador. Compara el contenido real de los archivos
|
||||||
|
// críticos del servidor contra los hashes de referencia que
|
||||||
|
// vinieron en la última entrega (version.json), para detectar
|
||||||
|
// de un vistazo si algún archivo quedó con una versión vieja
|
||||||
|
// después de una actualización a medio subir.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'verificar_version') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$rutaVersion = __DIR__ . '/../version.json';
|
||||||
|
if (!file_exists($rutaVersion)) {
|
||||||
|
echo json_encode(['ok' => true, 'sin_version_json' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$referencia = json_decode(file_get_contents($rutaVersion), true);
|
||||||
|
$resultado = [];
|
||||||
|
$hayDesactualizados = false;
|
||||||
|
|
||||||
|
foreach ($referencia['archivos_criticos'] as $rutaRelativa => $hashEsperado) {
|
||||||
|
$rutaAbsoluta = __DIR__ . '/../' . $rutaRelativa;
|
||||||
|
if (!file_exists($rutaAbsoluta)) {
|
||||||
|
$resultado[] = ['archivo' => $rutaRelativa, 'estado' => 'falta', 'hash_esperado' => $hashEsperado, 'hash_real' => null];
|
||||||
|
$hayDesactualizados = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$hashReal = md5_file($rutaAbsoluta);
|
||||||
|
$actualizado = ($hashReal === $hashEsperado);
|
||||||
|
if (!$actualizado) $hayDesactualizados = true;
|
||||||
|
$resultado[] = [
|
||||||
|
'archivo' => $rutaRelativa,
|
||||||
|
'estado' => $actualizado ? 'actualizado' : 'desactualizado',
|
||||||
|
'hash_esperado' => $hashEsperado,
|
||||||
|
'hash_real' => $hashReal,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'version' => $referencia['version'] ?? '?',
|
||||||
|
'fecha' => $referencia['fecha'] ?? null,
|
||||||
|
'descripcion' => $referencia['descripcion'] ?? null,
|
||||||
|
'hay_desactualizados' => $hayDesactualizados,
|
||||||
|
'archivos' => $resultado,
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Solicitud no válida.']);
|
||||||
998
api/auth.php
Normal file
998
api/auth.php
Normal file
|
|
@ -0,0 +1,998 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config/config.php';
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$pdo = obtenerConexion();
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$accion = $input['accion'] ?? '';
|
||||||
|
|
||||||
|
const LARGO_PIN = 4;
|
||||||
|
|
||||||
|
function obtenerUsuariosActivosPorSede($pdo, $sedeId) {
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT u.* FROM usuarios u
|
||||||
|
INNER JOIN usuarios_sedes us ON us.usuario_id = u.id
|
||||||
|
WHERE us.sede_id = ? AND u.activo = 1 AND u.rol IN ("profesional", "administrativa")
|
||||||
|
ORDER BY u.rol ASC, u.nombre_completo ASC
|
||||||
|
');
|
||||||
|
$stmt->execute([$sedeId]);
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pinValido($pin) {
|
||||||
|
return is_string($pin) && preg_match('/^\d{' . LARGO_PIN . '}$/', $pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function aplicarBloqueoFuerzaBruta($claveSesionIntentos, $claveSesionBloqueo) {
|
||||||
|
if (!isset($_SESSION[$claveSesionIntentos])) $_SESSION[$claveSesionIntentos] = 0;
|
||||||
|
if (!isset($_SESSION[$claveSesionBloqueo])) $_SESSION[$claveSesionBloqueo] = 0;
|
||||||
|
|
||||||
|
if (time() < $_SESSION[$claveSesionBloqueo]) {
|
||||||
|
$espera = $_SESSION[$claveSesionBloqueo] - time();
|
||||||
|
http_response_code(429);
|
||||||
|
echo json_encode(['ok' => false, 'error' => "Demasiados intentos. Esperá $espera segundos."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registrarIntentoFallido($claveSesionIntentos, $claveSesionBloqueo) {
|
||||||
|
$_SESSION[$claveSesionIntentos]++;
|
||||||
|
if ($_SESSION[$claveSesionIntentos] >= 5) {
|
||||||
|
$_SESSION[$claveSesionBloqueo] = time() + 30;
|
||||||
|
$_SESSION[$claveSesionIntentos] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// ESTADO INICIAL: en qué etapa de configuración está el sistema.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'estado') {
|
||||||
|
$hayDesarrollador = $pdo->query('SELECT COUNT(*) AS t FROM desarrollador')->fetch()['t'] > 0;
|
||||||
|
if (!$hayDesarrollador) {
|
||||||
|
echo json_encode(['ok' => true, 'etapa' => 'sin_desarrollador', 'largo_pin' => LARGO_PIN]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sedesConUsuarios = $pdo->query('
|
||||||
|
SELECT COUNT(*) AS t FROM sedes s
|
||||||
|
INNER JOIN usuarios_sedes us ON us.sede_id = s.id
|
||||||
|
INNER JOIN usuarios u ON u.id = us.usuario_id AND u.activo = 1
|
||||||
|
WHERE s.activa = 1
|
||||||
|
')->fetch()['t'];
|
||||||
|
|
||||||
|
if ($sedesConUsuarios == 0) {
|
||||||
|
echo json_encode(['ok' => true, 'etapa' => 'sin_sedes_o_usuarios', 'largo_pin' => LARGO_PIN]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'etapa' => 'listo', 'largo_pin' => LARGO_PIN]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// LISTAR SEDES ACTIVAS (paso 1 del login normal).
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'listar_sedes_login') {
|
||||||
|
$stmt = $pdo->query('
|
||||||
|
SELECT s.id, s.nombre FROM sedes s
|
||||||
|
INNER JOIN usuarios_sedes us ON us.sede_id = s.id
|
||||||
|
INNER JOIN usuarios u ON u.id = us.usuario_id AND u.activo = 1
|
||||||
|
WHERE s.activa = 1
|
||||||
|
GROUP BY s.id
|
||||||
|
ORDER BY s.nombre ASC
|
||||||
|
');
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// LISTAR USUARIOS DE UNA SEDE (paso 2 del login normal).
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'listar_usuarios_sede_login') {
|
||||||
|
$sedeId = $input['sede_id'] ?? 0;
|
||||||
|
$usuarios = obtenerUsuariosActivosPorSede($pdo, $sedeId);
|
||||||
|
$datos = array_map(function ($u) {
|
||||||
|
return ['id' => $u['id'], 'nombre_completo' => $u['nombre_completo'], 'rol' => $u['rol']];
|
||||||
|
}, $usuarios);
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $datos]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// LISTAR PROFESIONALES DE UNA SEDE (para que la administrativa
|
||||||
|
// elija a quién representa, dentro del paso 3 del login).
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'listar_profesionales_sede_login') {
|
||||||
|
$sedeId = $input['sede_id'] ?? 0;
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT u.id, u.nombre_completo FROM usuarios u
|
||||||
|
INNER JOIN usuarios_sedes us ON us.usuario_id = u.id
|
||||||
|
WHERE u.rol = "profesional" AND u.activo = 1 AND us.sede_id = ?
|
||||||
|
ORDER BY u.nombre_completo ASC
|
||||||
|
');
|
||||||
|
$stmt->execute([$sedeId]);
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// VERIFICAR PIN (paso 3 del login normal).
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'verificar') {
|
||||||
|
aplicarBloqueoFuerzaBruta('intentos_fallidos', 'bloqueado_hasta');
|
||||||
|
|
||||||
|
$sedeId = $input['sede_id'] ?? 0;
|
||||||
|
$usuarioId = $input['usuario_id'] ?? 0;
|
||||||
|
$pin = trim($input['pin'] ?? '');
|
||||||
|
$profesionalActivoId = $input['profesional_activo_id'] ?? null;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE id = ? AND activo = 1');
|
||||||
|
$stmt->execute([$usuarioId]);
|
||||||
|
$usuario = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$usuario || !password_verify($pin . APP_SECRET, $usuario['patron_hash'])) {
|
||||||
|
registrarIntentoFallido('intentos_fallidos', 'bloqueado_hasta');
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'PIN incorrecto.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtSede = $pdo->prepare('SELECT 1 FROM usuarios_sedes WHERE usuario_id = ? AND sede_id = ?');
|
||||||
|
$stmtSede->execute([$usuarioId, $sedeId]);
|
||||||
|
if (!$stmtSede->fetch()) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Este usuario no pertenece a la sede elegida.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar licencia para profesionales: si venció el período
|
||||||
|
// contratado, bloqueamos el acceso y marcamos como suspendido.
|
||||||
|
if ($usuario['rol'] === 'profesional') {
|
||||||
|
$estadoLicencia = $usuario['estado_licencia'] ?? 'activo';
|
||||||
|
|
||||||
|
// Si tiene días definidos, chequear si el período venció.
|
||||||
|
if ($estadoLicencia === 'activo' && !empty($usuario['licencia_dias']) && !empty($usuario['licencia_inicio'])) {
|
||||||
|
$inicio = new DateTime($usuario['licencia_inicio']);
|
||||||
|
$vencimiento = clone $inicio;
|
||||||
|
$vencimiento->modify('+' . (int) $usuario['licencia_dias'] . ' days');
|
||||||
|
if (new DateTime() > $vencimiento) {
|
||||||
|
$estadoLicencia = 'suspendido';
|
||||||
|
$pdo->prepare('UPDATE usuarios SET estado_licencia = "suspendido" WHERE id = ?')->execute([$usuarioId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($estadoLicencia !== 'activo') {
|
||||||
|
$mensajes = [
|
||||||
|
'suspendido' => 'Tu licencia de acceso venció. Comunicarte con el administrador del sistema para renovarla.',
|
||||||
|
'pausado' => 'Tu cuenta está pausada temporalmente. Comunicarte con el administrador del sistema.',
|
||||||
|
'prohibido' => 'Tu cuenta está inhabilitada. Comunicarte con el administrador del sistema.',
|
||||||
|
];
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['ok' => false, 'error' => $mensajes[$estadoLicencia] ?? 'No podés acceder al sistema en este momento.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['autenticado'] = true;
|
||||||
|
$_SESSION['usuario_id'] = $usuario['id'];
|
||||||
|
$_SESSION['nombre_usuario'] = $usuario['nombre_completo'];
|
||||||
|
$_SESSION['rol'] = $usuario['rol'];
|
||||||
|
$_SESSION['sede_id'] = (int) $sedeId;
|
||||||
|
$_SESSION['intentos_fallidos'] = 0;
|
||||||
|
|
||||||
|
$nombreParaMostrar = $usuario['nombre_completo'];
|
||||||
|
|
||||||
|
if ($usuario['rol'] === 'administrativa') {
|
||||||
|
if (!$profesionalActivoId) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Falta indicar a qué profesional representa esta administrativa.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$stmtProf = $pdo->prepare('
|
||||||
|
SELECT u.nombre_completo FROM usuarios u
|
||||||
|
INNER JOIN usuarios_sedes us ON us.usuario_id = u.id
|
||||||
|
WHERE u.id = ? AND u.rol = "profesional" AND u.activo = 1 AND us.sede_id = ?
|
||||||
|
');
|
||||||
|
$stmtProf->execute([$profesionalActivoId, $sedeId]);
|
||||||
|
$profesionalRepresentado = $stmtProf->fetch();
|
||||||
|
if (!$profesionalRepresentado) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'El profesional elegido no pertenece a esta sede.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$_SESSION['profesional_activo_id'] = (int) $profesionalActivoId;
|
||||||
|
// En pantalla se muestra el nombre del profesional, nunca el
|
||||||
|
// de la administrativa, para que no se "mezclen" las cuentas
|
||||||
|
// visualmente. Internamente (auditoría, etc.) se sigue
|
||||||
|
// registrando que fue la administrativa quien actuó.
|
||||||
|
$nombreParaMostrar = $profesionalRepresentado['nombre_completo'];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'nombre_usuario' => $nombreParaMostrar,
|
||||||
|
'rol' => $usuario['rol'],
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// DESARROLLADOR
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
if ($accion === 'crear_desarrollador') {
|
||||||
|
$hay = $pdo->query('SELECT COUNT(*) AS t FROM desarrollador')->fetch()['t'];
|
||||||
|
if ($hay > 0) {
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Ya existe una clave de desarrollador configurada.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$clave = trim($input['clave'] ?? '');
|
||||||
|
if (!pinValido($clave)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'La clave debe tener exactamente ' . LARGO_PIN . ' números.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$hash = password_hash($clave . APP_SECRET, PASSWORD_BCRYPT);
|
||||||
|
$pdo->prepare('INSERT INTO desarrollador (clave_hash) VALUES (?)')->execute([$hash]);
|
||||||
|
|
||||||
|
$_SESSION['autenticado'] = true;
|
||||||
|
$_SESSION['usuario_id'] = null;
|
||||||
|
$_SESSION['nombre_usuario'] = 'Desarrollador';
|
||||||
|
$_SESSION['rol'] = 'desarrollador';
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'rol' => 'desarrollador', 'nombre_usuario' => 'Desarrollador']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($accion === 'verificar_desarrollador') {
|
||||||
|
aplicarBloqueoFuerzaBruta('dev_intentos_fallidos', 'dev_bloqueado_hasta');
|
||||||
|
|
||||||
|
$clave = trim($input['clave'] ?? '');
|
||||||
|
$fila = $pdo->query('SELECT * FROM desarrollador ORDER BY id ASC LIMIT 1')->fetch();
|
||||||
|
|
||||||
|
if (!$fila || !password_verify($clave . APP_SECRET, $fila['clave_hash'])) {
|
||||||
|
registrarIntentoFallido('dev_intentos_fallidos', 'dev_bloqueado_hasta');
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Clave incorrecta.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['autenticado'] = true;
|
||||||
|
$_SESSION['usuario_id'] = null;
|
||||||
|
$_SESSION['nombre_usuario'] = 'Desarrollador';
|
||||||
|
$_SESSION['rol'] = 'desarrollador';
|
||||||
|
$_SESSION['dev_intentos_fallidos'] = 0;
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'rol' => 'desarrollador', 'nombre_usuario' => 'Desarrollador']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// GESTIÓN DE SEDES (solo desarrollador)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'crear_sede') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$nombre = trim($input['nombre'] ?? '');
|
||||||
|
if ($nombre === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Ingresá el nombre de la sede.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si ya existe una sede con ese nombre pero está desactivada,
|
||||||
|
// la reactivamos en vez de fallar por el nombre duplicado.
|
||||||
|
$stmtExistente = $pdo->prepare('SELECT id, activa FROM sedes WHERE nombre = ?');
|
||||||
|
$stmtExistente->execute([$nombre]);
|
||||||
|
$existente = $stmtExistente->fetch();
|
||||||
|
|
||||||
|
if ($existente && $existente['activa']) {
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Ya existe una sede activa con ese nombre.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($existente && !$existente['activa']) {
|
||||||
|
$pdo->prepare('UPDATE sedes SET activa = 1 WHERE id = ?')->execute([$existente['id']]);
|
||||||
|
registrarAuditoria($pdo, 'crear', 'sede', $existente['id'], "Se reactivó la sede \"$nombre\".");
|
||||||
|
echo json_encode(['ok' => true, 'id' => $existente['id']]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->prepare('INSERT INTO sedes (nombre) VALUES (?)')->execute([$nombre]);
|
||||||
|
registrarAuditoria($pdo, 'crear', 'sede', $pdo->lastInsertId(), "Se creó la sede \"$nombre\".");
|
||||||
|
echo json_encode(['ok' => true, 'id' => $pdo->lastInsertId()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($accion === 'listar_sedes') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$stmt = $pdo->query('SELECT * FROM sedes WHERE activa = 1 ORDER BY nombre ASC');
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($accion === 'desactivar_sede') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$id = $input['id'] ?? 0;
|
||||||
|
$pdo->prepare('UPDATE sedes SET activa = 0 WHERE id = ?')->execute([$id]);
|
||||||
|
registrarAuditoria($pdo, 'desactivar', 'sede', $id, 'Se desactivó una sede.');
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// PAPELERA (DESARROLLADOR): listar profesionales de una sede,
|
||||||
|
// para elegir de quién ver la papelera y a quién reasignar.
|
||||||
|
// (Reutiliza la misma idea que listar_profesionales_sede_login,
|
||||||
|
// pero accesible solo para el Desarrollador.)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'listar_profesionales_sede_dev') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$sedeId = $input['sede_id'] ?? 0;
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT u.id, u.nombre_completo FROM usuarios u
|
||||||
|
INNER JOIN usuarios_sedes us ON us.usuario_id = u.id
|
||||||
|
WHERE u.rol = "profesional" AND u.activo = 1 AND us.sede_id = ?
|
||||||
|
ORDER BY u.nombre_completo ASC
|
||||||
|
');
|
||||||
|
$stmt->execute([$sedeId]);
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// PAPELERA (DESARROLLADOR): ver los legajos eliminados de un
|
||||||
|
// profesional específico (GET vía POST con accion en el body,
|
||||||
|
// igual que el resto de este archivo).
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'listar_papelera_dev') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$profesionalId = $input['profesional_id'] ?? 0;
|
||||||
|
$sedeId = $input['sede_id'] ?? 0;
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT id, paciente_id_original, nombre_completo, dni, eliminado_en, sede_id_original
|
||||||
|
FROM legajos_eliminados
|
||||||
|
WHERE profesional_id_original = ? AND sede_id_original = ?
|
||||||
|
ORDER BY eliminado_en DESC
|
||||||
|
');
|
||||||
|
$stmt->execute([$profesionalId, $sedeId]);
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// PAPELERA (DESARROLLADOR): recuperar un legajo eliminado,
|
||||||
|
// asignándolo a un profesional de la MISMA sede donde estaba
|
||||||
|
// originalmente (no se permite recuperar hacia otra sede, para
|
||||||
|
// evitar que un paciente "viaje" de sede sin que el Desarrollador
|
||||||
|
// lo haga a propósito desde la ficha del paciente ya recuperado).
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'recuperar_legajo_dev') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$idPapelera = $input['id'] ?? 0;
|
||||||
|
$nuevoProfesionalId = $input['profesional_id'] ?? 0;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM legajos_eliminados WHERE id = ?');
|
||||||
|
$stmt->execute([$idPapelera]);
|
||||||
|
$registro = $stmt->fetch();
|
||||||
|
if (!$registro) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Ese registro de la papelera no existe.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sedeOriginalId = $registro['sede_id_original'];
|
||||||
|
|
||||||
|
// Confirmar que el nuevo profesional realmente atienda en esa misma sede.
|
||||||
|
$stmtCheck = $pdo->prepare('
|
||||||
|
SELECT u.nombre_completo FROM usuarios u
|
||||||
|
INNER JOIN usuarios_sedes us ON us.usuario_id = u.id
|
||||||
|
WHERE u.id = ? AND u.rol = "profesional" AND u.activo = 1 AND us.sede_id = ?
|
||||||
|
');
|
||||||
|
$stmtCheck->execute([$nuevoProfesionalId, $sedeOriginalId]);
|
||||||
|
$nuevoProfesional = $stmtCheck->fetch();
|
||||||
|
if (!$nuevoProfesional) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Ese profesional no atiende en la sede donde estaba este legajo.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$datos = json_decode($registro['datos_json'], true);
|
||||||
|
$sesionesGuardadas = $datos['sesiones'] ?? [];
|
||||||
|
unset($datos['sesiones'], $datos['edad']); // no son columnas de la tabla pacientes
|
||||||
|
|
||||||
|
// Si el legajo se está asignando a un profesional DISTINTO del
|
||||||
|
// que lo tenía antes de eliminarlo, guardamos el nombre del
|
||||||
|
// profesional original para que quede visible en la ficha.
|
||||||
|
$nombreProfesionalAnterior = null;
|
||||||
|
$profesionalOriginalId = $registro['profesional_id_original'];
|
||||||
|
if ($profesionalOriginalId && (int) $profesionalOriginalId !== (int) $nuevoProfesionalId) {
|
||||||
|
$stmtAnterior = $pdo->prepare('SELECT nombre_completo FROM usuarios WHERE id = ?');
|
||||||
|
$stmtAnterior->execute([$profesionalOriginalId]);
|
||||||
|
$filaAnterior = $stmtAnterior->fetch();
|
||||||
|
if ($filaAnterior) {
|
||||||
|
$nombreProfesionalAnterior = $filaAnterior['nombre_completo'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
INSERT INTO pacientes
|
||||||
|
(profesional_id, sede_id, recuperado_de_profesional, nombre, apellido, dni, fecha_nacimiento, sexo, obra_social_id, numero_afiliado,
|
||||||
|
telefono, email, direccion, motivo_consulta, patologia, sintomas, observaciones_generales)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
');
|
||||||
|
$stmt->execute([
|
||||||
|
$nuevoProfesionalId,
|
||||||
|
$sedeOriginalId,
|
||||||
|
$nombreProfesionalAnterior,
|
||||||
|
$datos['nombre'] ?? '',
|
||||||
|
$datos['apellido'] ?? '',
|
||||||
|
$datos['dni'] ?? '',
|
||||||
|
$datos['fecha_nacimiento'] ?? null,
|
||||||
|
$datos['sexo'] ?? 'Otro',
|
||||||
|
$datos['obra_social_id'] ?? null,
|
||||||
|
$datos['numero_afiliado'] ?? null,
|
||||||
|
$datos['telefono'] ?? null,
|
||||||
|
$datos['email'] ?? null,
|
||||||
|
$datos['direccion'] ?? null,
|
||||||
|
$datos['motivo_consulta'] ?? null,
|
||||||
|
$datos['patologia'] ?? null,
|
||||||
|
$datos['sintomas'] ?? null,
|
||||||
|
$datos['observaciones_generales'] ?? null,
|
||||||
|
]);
|
||||||
|
$nuevoPacienteId = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
if (!empty($sesionesGuardadas) && is_array($sesionesGuardadas)) {
|
||||||
|
$stmtSesion = $pdo->prepare('INSERT INTO sesiones (paciente_id, fecha_sesion, descripcion, evolucion, proxima_cita) VALUES (?, ?, ?, ?, ?)');
|
||||||
|
foreach ($sesionesGuardadas as $s) {
|
||||||
|
$stmtSesion->execute([
|
||||||
|
$nuevoPacienteId,
|
||||||
|
$s['fecha_sesion'] ?? date('Y-m-d'),
|
||||||
|
$s['descripcion'] ?? '',
|
||||||
|
$s['evolucion'] ?? null,
|
||||||
|
$s['proxima_cita'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->prepare('DELETE FROM legajos_eliminados WHERE id = ?')->execute([$idPapelera]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
registrarAuditoria(
|
||||||
|
$pdo, 'crear', 'paciente', $nuevoPacienteId,
|
||||||
|
"Se recuperó de la papelera el legajo de {$datos['nombre']} {$datos['apellido']}, asignado a {$nuevoProfesional['nombre_completo']}."
|
||||||
|
);
|
||||||
|
echo json_encode(['ok' => true, 'id' => $nuevoPacienteId]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No se pudo recuperar el legajo.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// PROFESIONALES DESACTIVADOS (DESARROLLADOR): listar usuarios
|
||||||
|
// con rol profesional que ya no tienen acceso (activo = 0), para
|
||||||
|
// poder ver y reasignar los legajos que les quedaron a cargo.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'listar_profesionales_desactivados') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$stmt = $pdo->query("
|
||||||
|
SELECT u.id, u.nombre_completo,
|
||||||
|
(SELECT COUNT(*) FROM pacientes p WHERE p.profesional_id = u.id) AS total_pacientes
|
||||||
|
FROM usuarios u
|
||||||
|
WHERE u.rol = 'profesional' AND u.activo = 0
|
||||||
|
HAVING total_pacientes > 0
|
||||||
|
ORDER BY u.nombre_completo ASC
|
||||||
|
");
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// LEGAJOS ACTIVOS DE UN PROFESIONAL DESACTIVADO (DESARROLLADOR)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'listar_legajos_huerfanos') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$profesionalId = $input['profesional_id'] ?? 0;
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT p.id, p.nombre, p.apellido, p.dni, p.sede_id, s.nombre AS sede_nombre
|
||||||
|
FROM pacientes p
|
||||||
|
LEFT JOIN sedes s ON s.id = p.sede_id
|
||||||
|
WHERE p.profesional_id = ?
|
||||||
|
ORDER BY p.apellido ASC, p.nombre ASC
|
||||||
|
');
|
||||||
|
$stmt->execute([$profesionalId]);
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// REASIGNAR LEGAJO HUÉRFANO (DESARROLLADOR): transfiere un
|
||||||
|
// paciente activo a otro profesional de la MISMA sede donde
|
||||||
|
// está el paciente (misma regla que la papelera: no se permite
|
||||||
|
// cambiar de sede de paso, solo de profesional).
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'reasignar_legajo_huerfano') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$pacienteId = $input['paciente_id'] ?? 0;
|
||||||
|
$nuevoProfesionalId = $input['profesional_id'] ?? 0;
|
||||||
|
|
||||||
|
$stmtPaciente = $pdo->prepare('SELECT * FROM pacientes WHERE id = ?');
|
||||||
|
$stmtPaciente->execute([$pacienteId]);
|
||||||
|
$paciente = $stmtPaciente->fetch();
|
||||||
|
if (!$paciente) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Ese paciente no existe.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtCheck = $pdo->prepare('
|
||||||
|
SELECT u.nombre_completo FROM usuarios u
|
||||||
|
INNER JOIN usuarios_sedes us ON us.usuario_id = u.id
|
||||||
|
WHERE u.id = ? AND u.rol = "profesional" AND u.activo = 1 AND us.sede_id = ?
|
||||||
|
');
|
||||||
|
$stmtCheck->execute([$nuevoProfesionalId, $paciente['sede_id']]);
|
||||||
|
$nuevoProfesional = $stmtCheck->fetch();
|
||||||
|
if (!$nuevoProfesional) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Ese profesional no atiende en la sede de este paciente.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si el profesional anterior queda registrado, lo guardamos
|
||||||
|
// como "recuperado_de_profesional" igual que en la papelera,
|
||||||
|
// para que quede el mismo aviso en la ficha del paciente.
|
||||||
|
$nombreProfesionalAnterior = null;
|
||||||
|
if ($paciente['profesional_id'] && (int) $paciente['profesional_id'] !== (int) $nuevoProfesionalId) {
|
||||||
|
$stmtAnterior = $pdo->prepare('SELECT nombre_completo FROM usuarios WHERE id = ?');
|
||||||
|
$stmtAnterior->execute([$paciente['profesional_id']]);
|
||||||
|
$filaAnterior = $stmtAnterior->fetch();
|
||||||
|
if ($filaAnterior) $nombreProfesionalAnterior = $filaAnterior['nombre_completo'];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->prepare('UPDATE pacientes SET profesional_id = ?, recuperado_de_profesional = ? WHERE id = ?')
|
||||||
|
->execute([$nuevoProfesionalId, $nombreProfesionalAnterior, $pacienteId]);
|
||||||
|
|
||||||
|
registrarAuditoria(
|
||||||
|
$pdo, 'editar', 'paciente', $pacienteId,
|
||||||
|
"Se transfirió el legajo de {$paciente['nombre']} {$paciente['apellido']} a {$nuevoProfesional['nombre_completo']} (profesional anterior desactivado)."
|
||||||
|
);
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
if ($e->getCode() === '23000') {
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'El profesional elegido ya tiene un paciente con ese mismo DNI.']);
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No se pudo transferir el legajo.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// CONFIGURACIÓN INICIAL: crea la primera sede y el primer
|
||||||
|
// profesional en un solo paso, justo después de crear la
|
||||||
|
// clave de Desarrollador.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'crear_setup_inicial') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$nombreSede = trim($input['nombre_sede'] ?? '');
|
||||||
|
$nombreProfesional = trim($input['nombre_profesional'] ?? '');
|
||||||
|
$pin = trim($input['pin'] ?? '');
|
||||||
|
|
||||||
|
if ($nombreSede === '' || $nombreProfesional === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Completá el nombre de la sede y del profesional.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (!pinValido($pin)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'El PIN debe tener exactamente ' . LARGO_PIN . ' números.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
$pdo->prepare('INSERT INTO sedes (nombre) VALUES (?)')->execute([$nombreSede]);
|
||||||
|
$sedeId = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
$hash = password_hash($pin . APP_SECRET, PASSWORD_BCRYPT);
|
||||||
|
$pdo->prepare('INSERT INTO usuarios (nombre_completo, rol, patron_hash) VALUES (?, "profesional", ?)')
|
||||||
|
->execute([$nombreProfesional, $hash]);
|
||||||
|
$usuarioId = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
$pdo->prepare('INSERT INTO usuarios_sedes (usuario_id, sede_id) VALUES (?, ?)')->execute([$usuarioId, $sedeId]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
registrarAuditoria($pdo, 'crear', 'sede', $sedeId, "Configuración inicial: sede \"$nombreSede\" y profesional \"$nombreProfesional\".");
|
||||||
|
echo json_encode(['ok' => true, 'sede_id' => $sedeId, 'usuario_id' => $usuarioId]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No se pudo completar la configuración inicial.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// CREAR USUARIO (profesional o administrativa) — solo desarrollador
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// CREAR PROFESIONAL (legajo completo) — solo desarrollador.
|
||||||
|
// Reemplaza el alta simple anterior: ahora incluye datos
|
||||||
|
// personales, especialidad, sede(s), PIN y tipo de licencia.
|
||||||
|
// Para administrativas se mantiene el alta simple de antes.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'crear_usuario') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$rol = $input['rol'] ?? '';
|
||||||
|
$pin = trim($input['pin'] ?? '');
|
||||||
|
$sedeIds = $input['sede_ids'] ?? [];
|
||||||
|
|
||||||
|
if (!in_array($rol, ['profesional', 'administrativa'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Rol no válido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (!pinValido($pin)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'El PIN debe tener exactamente ' . LARGO_PIN . ' números.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (empty($sedeIds) || !is_array($sedeIds)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Elegí al menos una sede.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rol === 'profesional') {
|
||||||
|
$titulo = $input['titulo'] ?? 'Dr.';
|
||||||
|
$nombre = trim($input['nombre'] ?? '');
|
||||||
|
$apellido = trim($input['apellido'] ?? '');
|
||||||
|
$dni = trim($input['dni'] ?? '') ?: null;
|
||||||
|
$fechaNac = $input['fecha_nacimiento'] ?? null ?: null;
|
||||||
|
$lugarNac = trim($input['lugar_nacimiento'] ?? '') ?: null;
|
||||||
|
$especialidad = trim($input['especialidad'] ?? '') ?: null;
|
||||||
|
$email = trim($input['email'] ?? '') ?: null;
|
||||||
|
$telefono = trim($input['telefono'] ?? '') ?: null;
|
||||||
|
$licenciaDias = $input['licencia_dias'] !== '' ? (int) $input['licencia_dias'] : null;
|
||||||
|
|
||||||
|
if ($nombre === '' || $apellido === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Ingresá el nombre y apellido del profesional.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$titulos = ['Dr.', 'Dra.', 'Lic.', 'Tec.', 'Mg.', 'Prof.', 'Otro'];
|
||||||
|
if (!in_array($titulo, $titulos)) $titulo = 'Dr.';
|
||||||
|
|
||||||
|
$nombreCompleto = "$titulo $nombre $apellido";
|
||||||
|
$hash = password_hash($pin . APP_SECRET, PASSWORD_BCRYPT);
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO usuarios (nombre_completo, rol, patron_hash, estado_licencia, licencia_dias, licencia_inicio) VALUES (?, "profesional", ?, "activo", ?, CURDATE())');
|
||||||
|
$stmt->execute([$nombreCompleto, $hash, $licenciaDias]);
|
||||||
|
$nuevoId = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
$stmtLegajo = $pdo->prepare('INSERT INTO profesionales_legajos (usuario_id, titulo, nombre, apellido, dni, fecha_nacimiento, lugar_nacimiento, especialidad, email, telefono) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||||
|
$stmtLegajo->execute([$nuevoId, $titulo, $nombre, $apellido, $dni, $fechaNac, $lugarNac, $especialidad, $email, $telefono]);
|
||||||
|
|
||||||
|
$stmtSede = $pdo->prepare('INSERT INTO usuarios_sedes (usuario_id, sede_id) VALUES (?, ?)');
|
||||||
|
foreach ($sedeIds as $sid) $stmtSede->execute([$nuevoId, $sid]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
registrarAuditoria($pdo, 'crear', 'usuario', $nuevoId, "Se creó el legajo del profesional \"$nombreCompleto\".");
|
||||||
|
echo json_encode(['ok' => true, 'id' => $nuevoId]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No se pudo crear el legajo.']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Administrativa: alta simple como antes
|
||||||
|
$nombre = trim($input['nombre_completo'] ?? '');
|
||||||
|
if ($nombre === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Ingresá el nombre de la administrativa.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$hash = password_hash($pin . APP_SECRET, PASSWORD_BCRYPT);
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO usuarios (nombre_completo, rol, patron_hash) VALUES (?, "administrativa", ?)');
|
||||||
|
$stmt->execute([$nombre, $hash]);
|
||||||
|
$nuevoId = $pdo->lastInsertId();
|
||||||
|
$stmtSede = $pdo->prepare('INSERT INTO usuarios_sedes (usuario_id, sede_id) VALUES (?, ?)');
|
||||||
|
foreach ($sedeIds as $sid) $stmtSede->execute([$nuevoId, $sid]);
|
||||||
|
$pdo->commit();
|
||||||
|
registrarAuditoria($pdo, 'crear', 'usuario', $nuevoId, "Se creó la administrativa \"$nombre\".");
|
||||||
|
echo json_encode(['ok' => true, 'id' => $nuevoId]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No se pudo crear la administrativa.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// OBTENER LEGAJO DE UN PROFESIONAL — Desarrollador y el propio
|
||||||
|
// profesional pueden consultar.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'obtener_legajo_profesional') {
|
||||||
|
requiereSesion();
|
||||||
|
$rol = $_SESSION['rol'] ?? '';
|
||||||
|
$usuarioId = (int) ($input['usuario_id'] ?? 0);
|
||||||
|
|
||||||
|
// El profesional solo puede ver su propio legajo.
|
||||||
|
if ($rol === 'profesional') {
|
||||||
|
$usuarioId = (int) $_SESSION['usuario_id'];
|
||||||
|
} elseif ($rol !== 'desarrollador') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Sin permiso.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT pl.*, u.estado_licencia, u.licencia_dias, u.licencia_inicio, u.activo,
|
||||||
|
CASE
|
||||||
|
WHEN u.licencia_dias IS NULL THEN NULL
|
||||||
|
ELSE DATE_ADD(u.licencia_inicio, INTERVAL u.licencia_dias DAY)
|
||||||
|
END AS licencia_vencimiento
|
||||||
|
FROM profesionales_legajos pl
|
||||||
|
INNER JOIN usuarios u ON u.id = pl.usuario_id
|
||||||
|
WHERE pl.usuario_id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$usuarioId]);
|
||||||
|
$legajo = $stmt->fetch();
|
||||||
|
if (!$legajo) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Legajo no encontrado.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $legajo]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// ACTUALIZAR LICENCIA — solo desarrollador.
|
||||||
|
// Permite activar (con duración), pausar, prohibir el acceso.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'actualizar_licencia') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$usuarioId = $input['usuario_id'] ?? 0;
|
||||||
|
$nuevoEstado = $input['estado'] ?? '';
|
||||||
|
$diasLicencia = isset($input['dias']) && $input['dias'] !== '' ? (int) $input['dias'] : null;
|
||||||
|
|
||||||
|
$estadosValidos = ['activo', 'pausado', 'prohibido'];
|
||||||
|
if (!in_array($nuevoEstado, $estadosValidos)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Estado no válido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$licenciaInicio = $nuevoEstado === 'activo' ? date('Y-m-d') : null;
|
||||||
|
$pdo->prepare('UPDATE usuarios SET estado_licencia = ?, licencia_dias = ?, licencia_inicio = ? WHERE id = ?')
|
||||||
|
->execute([$nuevoEstado, $nuevoEstado === 'activo' ? $diasLicencia : null, $licenciaInicio, $usuarioId]);
|
||||||
|
|
||||||
|
registrarAuditoria($pdo, 'editar', 'usuario', $usuarioId, "Se cambió el estado de licencia a \"$nuevoEstado\"" . ($licenciaInicio && $diasLicencia ? " por $diasLicencia días." : "."));
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// LISTAR USUARIOS (con sus sedes) — solo desarrollador.
|
||||||
|
// Solo se muestran los activos: los desactivados quedan
|
||||||
|
// fuera de este listado (pero siguen existiendo en la base,
|
||||||
|
// así que el historial de cambios sigue siendo legible).
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'listar_usuarios') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$stmt = $pdo->query("
|
||||||
|
SELECT u.id, u.nombre_completo, u.rol, u.activo, u.creado_en,
|
||||||
|
u.estado_licencia, u.licencia_dias, u.licencia_inicio,
|
||||||
|
CASE
|
||||||
|
WHEN u.licencia_dias IS NULL THEN NULL
|
||||||
|
ELSE DATE_ADD(u.licencia_inicio, INTERVAL u.licencia_dias DAY)
|
||||||
|
END AS licencia_vencimiento,
|
||||||
|
pl.titulo, pl.especialidad
|
||||||
|
FROM usuarios u
|
||||||
|
LEFT JOIN profesionales_legajos pl ON pl.usuario_id = u.id
|
||||||
|
WHERE u.activo = 1
|
||||||
|
ORDER BY u.creado_en ASC
|
||||||
|
");
|
||||||
|
$usuarios = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$stmtSedes = $pdo->prepare('SELECT s.id, s.nombre FROM sedes s INNER JOIN usuarios_sedes us ON us.sede_id = s.id WHERE us.usuario_id = ?');
|
||||||
|
foreach ($usuarios as &$u) {
|
||||||
|
$stmtSedes->execute([$u['id']]);
|
||||||
|
$u['sedes'] = $stmtSedes->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $usuarios]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// DESACTIVAR USUARIO — solo desarrollador
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'desactivar_usuario') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$idDesactivar = $input['id'] ?? 0;
|
||||||
|
$pdo->prepare('UPDATE usuarios SET activo = 0 WHERE id = ?')->execute([$idDesactivar]);
|
||||||
|
registrarAuditoria($pdo, 'desactivar', 'usuario', $idDesactivar, 'Se desactivó el acceso de un usuario.');
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// PREVISUALIZAR CAMBIO DE SEDES — solo desarrollador.
|
||||||
|
// Antes de aplicar el cambio real, le dice al Desarrollador
|
||||||
|
// cuántos legajos se van a borrar definitivamente por cada
|
||||||
|
// sede que se le esté quitando al usuario.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'previsualizar_cambio_sedes') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$usuarioId = $input['usuario_id'] ?? 0;
|
||||||
|
$sedeIdsNuevas = $input['sede_ids'] ?? [];
|
||||||
|
|
||||||
|
$stmtActuales = $pdo->prepare('SELECT sede_id FROM usuarios_sedes WHERE usuario_id = ?');
|
||||||
|
$stmtActuales->execute([$usuarioId]);
|
||||||
|
$sedeIdsActuales = array_column($stmtActuales->fetchAll(), 'sede_id');
|
||||||
|
|
||||||
|
$sedeIdsQueSeQuitan = array_diff($sedeIdsActuales, $sedeIdsNuevas);
|
||||||
|
|
||||||
|
$detalle = [];
|
||||||
|
$totalPacientesABorrar = 0;
|
||||||
|
if (!empty($sedeIdsQueSeQuitan)) {
|
||||||
|
$stmtSede = $pdo->prepare('SELECT nombre FROM sedes WHERE id = ?');
|
||||||
|
$stmtConteo = $pdo->prepare('SELECT COUNT(*) AS t FROM pacientes WHERE profesional_id = ? AND sede_id = ?');
|
||||||
|
foreach ($sedeIdsQueSeQuitan as $sedeId) {
|
||||||
|
$stmtSede->execute([$sedeId]);
|
||||||
|
$nombreSede = $stmtSede->fetch()['nombre'] ?? 'Sede desconocida';
|
||||||
|
$stmtConteo->execute([$usuarioId, $sedeId]);
|
||||||
|
$cantidad = (int) $stmtConteo->fetch()['t'];
|
||||||
|
$totalPacientesABorrar += $cantidad;
|
||||||
|
$detalle[] = ['sede_id' => (int) $sedeId, 'nombre' => $nombreSede, 'pacientes' => $cantidad];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'sedes_que_se_quitan' => $detalle, 'total_pacientes_a_borrar' => $totalPacientesABorrar]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// ACTUALIZAR SEDES DE UN USUARIO — solo desarrollador.
|
||||||
|
// Aplica el nuevo conjunto de sedes. Si se le quita una sede
|
||||||
|
// donde el usuario es profesional y tenía pacientes, esos
|
||||||
|
// legajos (con sus sesiones, citas y adjuntos) se eliminan
|
||||||
|
// definitivamente, sin pasar por la papelera.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'actualizar_sedes_usuario') {
|
||||||
|
requiereDesarrollador();
|
||||||
|
$usuarioId = $input['usuario_id'] ?? 0;
|
||||||
|
$sedeIdsNuevas = $input['sede_ids'] ?? [];
|
||||||
|
|
||||||
|
if (empty($sedeIdsNuevas) || !is_array($sedeIdsNuevas)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Elegí al menos una sede para esta persona.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtUsuario = $pdo->prepare('SELECT nombre_completo, rol FROM usuarios WHERE id = ?');
|
||||||
|
$stmtUsuario->execute([$usuarioId]);
|
||||||
|
$usuario = $stmtUsuario->fetch();
|
||||||
|
if (!$usuario) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Usuario no encontrado.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtActuales = $pdo->prepare('SELECT sede_id FROM usuarios_sedes WHERE usuario_id = ?');
|
||||||
|
$stmtActuales->execute([$usuarioId]);
|
||||||
|
$sedeIdsActuales = array_column($stmtActuales->fetchAll(), 'sede_id');
|
||||||
|
$sedeIdsQueSeQuitan = array_diff($sedeIdsActuales, $sedeIdsNuevas);
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
// Si es profesional, borrar definitivamente sus legajos
|
||||||
|
// de cada sede que se le esté quitando.
|
||||||
|
$pacientesBorrados = 0;
|
||||||
|
if ($usuario['rol'] === 'profesional' && !empty($sedeIdsQueSeQuitan)) {
|
||||||
|
$stmtPacientesSede = $pdo->prepare('SELECT id FROM pacientes WHERE profesional_id = ? AND sede_id = ?');
|
||||||
|
$stmtBorrarPaciente = $pdo->prepare('DELETE FROM pacientes WHERE id = ?');
|
||||||
|
foreach ($sedeIdsQueSeQuitan as $sedeId) {
|
||||||
|
$stmtPacientesSede->execute([$usuarioId, $sedeId]);
|
||||||
|
$pacientesDeEstaSede = $stmtPacientesSede->fetchAll();
|
||||||
|
foreach ($pacientesDeEstaSede as $p) {
|
||||||
|
// Las sesiones, citas y adjuntos de cada paciente se
|
||||||
|
// borran en cascada por las claves foráneas (ON DELETE CASCADE).
|
||||||
|
$stmtBorrarPaciente->execute([$p['id']]);
|
||||||
|
$pacientesBorrados++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->prepare('DELETE FROM usuarios_sedes WHERE usuario_id = ?')->execute([$usuarioId]);
|
||||||
|
$stmtInsertSede = $pdo->prepare('INSERT INTO usuarios_sedes (usuario_id, sede_id) VALUES (?, ?)');
|
||||||
|
foreach ($sedeIdsNuevas as $sedeId) {
|
||||||
|
$stmtInsertSede->execute([$usuarioId, $sedeId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
$descripcion = "Se actualizaron las sedes de \"{$usuario['nombre_completo']}\".";
|
||||||
|
if ($pacientesBorrados > 0) {
|
||||||
|
$descripcion .= " Se eliminaron $pacientesBorrados legajo(s) de forma definitiva por quitarle acceso a su sede.";
|
||||||
|
}
|
||||||
|
registrarAuditoria($pdo, 'editar', 'usuario', $usuarioId, $descripcion);
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'pacientes_borrados' => $pacientesBorrados]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'No se pudo actualizar las sedes del usuario.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// CAMBIAR MI PROPIO PIN (cualquier usuario logueado, incluido
|
||||||
|
// el desarrollador)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'cambiar') {
|
||||||
|
requiereSesion();
|
||||||
|
$pinNuevo = trim($input['pin_nuevo'] ?? '');
|
||||||
|
if (!pinValido($pinNuevo)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'El PIN debe tener exactamente ' . LARGO_PIN . ' números.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$hash = password_hash($pinNuevo . APP_SECRET, PASSWORD_BCRYPT);
|
||||||
|
if (esDesarrollador()) {
|
||||||
|
$pdo->prepare('UPDATE desarrollador SET clave_hash = ?')->execute([$hash]);
|
||||||
|
} else {
|
||||||
|
$pdo->prepare('UPDATE usuarios SET patron_hash = ? WHERE id = ?')->execute([$hash, $_SESSION['usuario_id']]);
|
||||||
|
}
|
||||||
|
echo json_encode(['ok' => true, 'mensaje' => 'PIN actualizado.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// QUIÉN SOY
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($accion === 'quien_soy') {
|
||||||
|
requiereSesion();
|
||||||
|
$nombreParaMostrar = $_SESSION['nombre_usuario'] ?? '';
|
||||||
|
if (($_SESSION['rol'] ?? '') === 'administrativa' && !empty($_SESSION['profesional_activo_id'])) {
|
||||||
|
$stmt = $pdo->prepare('SELECT nombre_completo FROM usuarios WHERE id = ?');
|
||||||
|
$stmt->execute([$_SESSION['profesional_activo_id']]);
|
||||||
|
$fila = $stmt->fetch();
|
||||||
|
if ($fila) $nombreParaMostrar = $fila['nombre_completo'];
|
||||||
|
}
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'nombre_usuario' => $nombreParaMostrar,
|
||||||
|
'rol' => $_SESSION['rol'] ?? '',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($accion === 'cerrar_sesion') {
|
||||||
|
$_SESSION = [];
|
||||||
|
session_destroy();
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Acción no reconocida.']);
|
||||||
362
api/citas.php
Normal file
362
api/citas.php
Normal file
|
|
@ -0,0 +1,362 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config/config.php';
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
requiereSesion();
|
||||||
|
requiereProfesionalActivo();
|
||||||
|
|
||||||
|
$pdo = obtenerConexion();
|
||||||
|
$metodo = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$accion = $_GET['accion'] ?? '';
|
||||||
|
$profesionalActivoId = idProfesionalActivo();
|
||||||
|
|
||||||
|
function calcularEdadCita($fechaNacimiento) {
|
||||||
|
try {
|
||||||
|
$nacimiento = new DateTime($fechaNacimiento);
|
||||||
|
$hoy = new DateTime();
|
||||||
|
return $hoy->diff($nacimiento)->y;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pacientePerteneceAlProfesional($pdo, $pacienteId, $profesionalActivoId) {
|
||||||
|
$stmt = $pdo->prepare('SELECT 1 FROM pacientes WHERE id = ? AND profesional_id = ?');
|
||||||
|
$stmt->execute([$pacienteId, $profesionalActivoId]);
|
||||||
|
return (bool) $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve la cita en choque (si existe) para ese profesional,
|
||||||
|
* fecha y hora exactas, entre citas pendientes. Si se pasa
|
||||||
|
* $excluirCitaId, esa cita no se cuenta (permite editar sin
|
||||||
|
* chocar contra sí misma).
|
||||||
|
*/
|
||||||
|
function hayChoqueDeHorario($pdo, $profesionalId, $fecha, $hora, $excluirCitaId = null) {
|
||||||
|
if (!$hora) return false;
|
||||||
|
$sql = '
|
||||||
|
SELECT c.id, p.nombre, p.apellido FROM citas c
|
||||||
|
INNER JOIN pacientes p ON p.id = c.paciente_id
|
||||||
|
WHERE c.profesional_id = ? AND c.fecha = ? AND c.hora = ? AND c.estado = "pendiente"
|
||||||
|
';
|
||||||
|
$params = [$profesionalId, $fecha, $hora];
|
||||||
|
if ($excluirCitaId) {
|
||||||
|
$sql .= ' AND c.id != ?';
|
||||||
|
$params[] = $excluirCitaId;
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
return $stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// CREAR CITA (POST ?accion=crear)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'POST' && $accion === 'crear') {
|
||||||
|
$d = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (empty($d['paciente_id']) || empty($d['fecha'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Faltan datos de la cita (paciente y fecha son obligatorios).']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pacientePerteneceAlProfesional($pdo, $d['paciente_id'], $profesionalActivoId)) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Paciente no encontrado.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hora = $d['hora'] ?: null;
|
||||||
|
$choque = hayChoqueDeHorario($pdo, $profesionalActivoId, $d['fecha'], $hora);
|
||||||
|
if ($choque) {
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode(['ok' => false, 'error' => "Ese horario ya está ocupado con {$choque['nombre']} {$choque['apellido']}. Elegí otro horario."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = bin2hex(random_bytes(20));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
INSERT INTO citas (paciente_id, profesional_id, fecha, hora, motivo, notas, estado, token_confirmacion)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, "pendiente", ?)
|
||||||
|
');
|
||||||
|
$stmt->execute([
|
||||||
|
$d['paciente_id'],
|
||||||
|
$profesionalActivoId,
|
||||||
|
$d['fecha'],
|
||||||
|
$hora,
|
||||||
|
$d['motivo'] ?? null,
|
||||||
|
$d['notas'] ?? null,
|
||||||
|
$token,
|
||||||
|
]);
|
||||||
|
echo json_encode(['ok' => true, 'id' => $pdo->lastInsertId(), 'token_confirmacion' => $token]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// ACTUALIZAR CITA (POST ?accion=actualizar)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'POST' && $accion === 'actualizar') {
|
||||||
|
$d = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (empty($d['id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Falta el ID de la cita.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtCheck = $pdo->prepare('SELECT * FROM citas WHERE id = ? AND profesional_id = ?');
|
||||||
|
$stmtCheck->execute([$d['id'], $profesionalActivoId]);
|
||||||
|
if (!$stmtCheck->fetch()) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Cita no encontrada.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hora = $d['hora'] ?: null;
|
||||||
|
$choque = hayChoqueDeHorario($pdo, $profesionalActivoId, $d['fecha'], $hora, $d['id']);
|
||||||
|
if ($choque) {
|
||||||
|
http_response_code(409);
|
||||||
|
echo json_encode(['ok' => false, 'error' => "Ese horario ya está ocupado con {$choque['nombre']} {$choque['apellido']}. Elegí otro horario."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
UPDATE citas SET fecha = ?, hora = ?, motivo = ?, notas = ?, estado = ?
|
||||||
|
WHERE id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([
|
||||||
|
$d['fecha'],
|
||||||
|
$hora,
|
||||||
|
$d['motivo'] ?? null,
|
||||||
|
$d['notas'] ?? null,
|
||||||
|
$d['estado'] ?? 'pendiente',
|
||||||
|
$d['id'],
|
||||||
|
]);
|
||||||
|
echo json_encode(['ok' => true, 'mensaje' => 'Cita actualizada.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// CAMBIAR SOLO EL ESTADO (POST ?accion=cambiar_estado)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'POST' && $accion === 'cambiar_estado') {
|
||||||
|
$d = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (empty($d['id']) || empty($d['estado'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Faltan datos.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (!in_array($d['estado'], ['pendiente', 'atendida', 'cancelada', 'ausente'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Estado no válido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtCheck = $pdo->prepare('SELECT 1 FROM citas WHERE id = ? AND profesional_id = ?');
|
||||||
|
$stmtCheck->execute([$d['id'], $profesionalActivoId]);
|
||||||
|
if (!$stmtCheck->fetch()) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Cita no encontrada.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('UPDATE citas SET estado = ? WHERE id = ?');
|
||||||
|
$stmt->execute([$d['estado'], $d['id']]);
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// ELIMINAR CITA (POST ?accion=eliminar)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'POST' && $accion === 'eliminar') {
|
||||||
|
$d = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = $d['id'] ?? 0;
|
||||||
|
|
||||||
|
$stmtCheck = $pdo->prepare('SELECT 1 FROM citas WHERE id = ? AND profesional_id = ?');
|
||||||
|
$stmtCheck->execute([$id, $profesionalActivoId]);
|
||||||
|
if (!$stmtCheck->fetch()) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Cita no encontrada.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->prepare('DELETE FROM citas WHERE id = ?')->execute([$id]);
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// LISTAR PRÓXIMAS CITAS (GET ?accion=proximas&dias=7)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'GET' && $accion === 'proximas') {
|
||||||
|
$dias = (int) ($_GET['dias'] ?? 7);
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT c.*, p.nombre, p.apellido, p.telefono
|
||||||
|
FROM citas c
|
||||||
|
INNER JOIN pacientes p ON p.id = c.paciente_id
|
||||||
|
WHERE c.profesional_id = ?
|
||||||
|
AND c.fecha BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
|
||||||
|
AND c.estado = 'pendiente'
|
||||||
|
ORDER BY c.fecha ASC, c.hora ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$profesionalActivoId, $dias]);
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// LISTAR CITAS POR RANGO (GET ?accion=rango&desde=&hasta=)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'GET' && $accion === 'rango') {
|
||||||
|
$desde = $_GET['desde'] ?? date('Y-m-01');
|
||||||
|
$hasta = $_GET['hasta'] ?? date('Y-m-t');
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT c.*, p.nombre, p.apellido, p.telefono
|
||||||
|
FROM citas c
|
||||||
|
INNER JOIN pacientes p ON p.id = c.paciente_id
|
||||||
|
WHERE c.profesional_id = ? AND c.fecha BETWEEN ? AND ?
|
||||||
|
ORDER BY c.fecha ASC, c.hora ASC
|
||||||
|
');
|
||||||
|
$stmt->execute([$profesionalActivoId, $desde, $hasta]);
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// CITAS DE UN PACIENTE (GET ?accion=por_paciente&paciente_id=X)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'GET' && $accion === 'por_paciente') {
|
||||||
|
$id = $_GET['paciente_id'] ?? 0;
|
||||||
|
|
||||||
|
if (!pacientePerteneceAlProfesional($pdo, $id, $profesionalActivoId)) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Paciente no encontrado.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT * FROM citas WHERE paciente_id = ? ORDER BY fecha DESC, hora DESC');
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// PACIENTES INACTIVOS (GET ?accion=inactivos&dias=30)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'GET' && $accion === 'inactivos') {
|
||||||
|
requiereRolProfesional();
|
||||||
|
$dias = (int) ($_GET['dias'] ?? 30);
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT p.id, p.nombre, p.apellido, p.dni, p.telefono,
|
||||||
|
MAX(s.fecha_sesion) AS ultima_sesion,
|
||||||
|
p.creado_en
|
||||||
|
FROM pacientes p
|
||||||
|
LEFT JOIN sesiones s ON s.paciente_id = p.id
|
||||||
|
WHERE p.profesional_id = ?
|
||||||
|
GROUP BY p.id
|
||||||
|
HAVING (ultima_sesion IS NOT NULL AND ultima_sesion < DATE_SUB(CURDATE(), INTERVAL ? DAY))
|
||||||
|
OR (ultima_sesion IS NULL AND p.creado_en < DATE_SUB(NOW(), INTERVAL ? DAY))
|
||||||
|
ORDER BY ultima_sesion ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$profesionalActivoId, $dias, $dias]);
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// CUMPLEAÑOS PRÓXIMOS (GET ?accion=cumpleanios&dias=14)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'GET' && $accion === 'cumpleanios') {
|
||||||
|
$dias = (int) ($_GET['dias'] ?? 14);
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT id, nombre, apellido, telefono, fecha_nacimiento,
|
||||||
|
DATE_FORMAT(fecha_nacimiento, '%m-%d') AS mes_dia
|
||||||
|
FROM pacientes
|
||||||
|
WHERE profesional_id = ?
|
||||||
|
HAVING mes_dia BETWEEN DATE_FORMAT(CURDATE(), '%m-%d') AND DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL ? DAY), '%m-%d')
|
||||||
|
OR (DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL ? DAY), '%m-%d') < DATE_FORMAT(CURDATE(), '%m-%d')
|
||||||
|
AND (mes_dia >= DATE_FORMAT(CURDATE(), '%m-%d') OR mes_dia <= DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL ? DAY), '%m-%d')))
|
||||||
|
ORDER BY mes_dia ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$profesionalActivoId, $dias, $dias, $dias]);
|
||||||
|
$resultados = $stmt->fetchAll();
|
||||||
|
foreach ($resultados as &$r) {
|
||||||
|
$r['edad_que_cumple'] = calcularEdadCita($r['fecha_nacimiento']) + 1;
|
||||||
|
}
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $resultados]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// AVISOS SIN REVISAR (GET ?accion=avisos_pendientes)
|
||||||
|
// Cuenta cuántas citas tienen un cambio del paciente (confirmó
|
||||||
|
// o canceló) que el profesional todavía no vio.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'GET' && $accion === 'avisos_pendientes') {
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
SELECT COUNT(*) AS total FROM citas
|
||||||
|
WHERE profesional_id = ? AND revisada_por_profesional = 0
|
||||||
|
');
|
||||||
|
$stmt->execute([$profesionalActivoId]);
|
||||||
|
echo json_encode(['ok' => true, 'total' => (int) $stmt->fetch()['total']]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// DETALLE DE AVISOS SIN REVISAR (GET ?accion=listar_avisos)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'GET' && $accion === 'listar_avisos') {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT c.id, c.fecha, c.hora, c.estado, c.confirmada_por_paciente, p.nombre, p.apellido
|
||||||
|
FROM citas c
|
||||||
|
INNER JOIN pacientes p ON p.id = c.paciente_id
|
||||||
|
WHERE c.profesional_id = ? AND c.revisada_por_profesional = 0
|
||||||
|
ORDER BY c.fecha ASC, c.hora ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$profesionalActivoId]);
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// MARCAR AVISOS COMO VISTOS (POST ?accion=marcar_avisos_vistos)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'POST' && $accion === 'marcar_avisos_vistos') {
|
||||||
|
$stmt = $pdo->prepare('UPDATE citas SET revisada_por_profesional = 1 WHERE profesional_id = ?');
|
||||||
|
$stmt->execute([$profesionalActivoId]);
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// RESUMEN DE HOY (GET ?accion=resumen_hoy)
|
||||||
|
// Cuántas consultas quedan por pasar hoy (la hora de la cita
|
||||||
|
// todavía no llegó) y a qué hora es la próxima.
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
if ($metodo === 'GET' && $accion === 'resumen_hoy') {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT hora FROM citas
|
||||||
|
WHERE profesional_id = ? AND fecha = CURDATE() AND estado = 'pendiente'
|
||||||
|
ORDER BY hora ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$profesionalActivoId]);
|
||||||
|
$todasHoy = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$horaActual = date('H:i:s');
|
||||||
|
$restantes = array_filter($todasHoy, function ($c) use ($horaActual) {
|
||||||
|
// Una cita sin hora especificada se cuenta como "restante"
|
||||||
|
// todo el día, ya que no hay forma de saber si ya pasó.
|
||||||
|
return $c['hora'] === null || $c['hora'] >= $horaActual;
|
||||||
|
});
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'total_hoy' => count($todasHoy),
|
||||||
|
'restantes_hoy' => count($restantes),
|
||||||
|
'proxima_hora' => !empty($restantes) ? reset($restantes)['hora'] : null,
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Solicitud no válida.']);
|
||||||
43
api/obras_sociales.php
Normal file
43
api/obras_sociales.php
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config/config.php';
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
requiereSesion();
|
||||||
|
|
||||||
|
$pdo = obtenerConexion();
|
||||||
|
$metodo = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
if ($metodo === 'GET') {
|
||||||
|
$stmt = $pdo->query('SELECT id, nombre FROM obras_sociales ORDER BY es_predefinida DESC, nombre ASC');
|
||||||
|
echo json_encode(['ok' => true, 'datos' => $stmt->fetchAll()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($metodo === 'POST') {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$nombre = trim($input['nombre'] ?? '');
|
||||||
|
if ($nombre === '') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'El nombre de la obra social no puede estar vacío.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare('INSERT INTO obras_sociales (nombre, es_predefinida) VALUES (?, 0)');
|
||||||
|
$stmt->execute([$nombre]);
|
||||||
|
echo json_encode(['ok' => true, 'id' => $pdo->lastInsertId(), 'nombre' => $nombre]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
if ($e->getCode() === '23000') {
|
||||||
|
// Ya existe: la devolvemos igual para que el front la pueda usar
|
||||||
|
$stmt = $pdo->prepare('SELECT id, nombre FROM obras_sociales WHERE nombre = ?');
|
||||||
|
$stmt->execute([$nombre]);
|
||||||
|
$fila = $stmt->fetch();
|
||||||
|
echo json_encode(['ok' => true, 'id' => $fila['id'], 'nombre' => $fila['nombre'], 'ya_existia' => true]);
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Error al guardar la obra social.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Método no permitido.']);
|
||||||
Loading…
Reference in New Issue
Block a user