Files
donna/public/tramites/api.php
T
Soporte ZH Consultores 1bb63f12f6 feat(tramites): app "Control de Trámites Legales" + backend
Migración del frontend localStorage a backend autenticado bajo Donna.

* SQL 004: tramites_legales (todos los campos por dept en columnas
  nullable + ENUMs Sí/No), tramites_obs (bitácora N:1), tramites_hours
  (registro de horas N:1), tramites_seq (contador por departamento).
  IDs server-side tipo ADM-001 / CRP-001 / MIG-001 / LIT-001 con
  SELECT FOR UPDATE para atomicidad.
* public/tramites/api.php: list/create/update/delete + set_status +
  add_obs + add_hours. Valida departamento y estatus contra los enums
  del frontend. Audita creado_por / actualizado_por con el email
  autenticado.
* views/tramites.html: artifact original adaptado. Quitado backup/restore
  localStorage (mysqldump diario ya hace ese trabajo). Cada mutación
  va vía fetch con manejo de 401 → redirect a login. Mapeo
  id<->codigo para preservar el contrato del artifact.
* Apps.php: la card "Gestión de Casos" placeholder se reemplaza por
  "Control de Trámites Legales" en estado live.
2026-05-11 20:03:28 +00:00

292 lines
13 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../../src/Auth.php';
require_once __DIR__ . '/../../src/Db.php';
if (($_GET['action'] ?? '') === 'logout') {
header('Location: /auth/logout.php');
exit;
}
Auth::requireAuth();
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
const DEPTS = ['adm', 'corp', 'mig', 'lit'];
const PREFIX = ['adm' => 'ADM', 'corp' => 'CRP', 'mig' => 'MIG', 'lit' => 'LIT'];
const STATUS_MAP = [
'adm' => ['Solicitado','Recibido','En Proceso','Notificado','Recurso','Culminado','Facturado'],
'corp' => ['Solicitado','Recibido','Borrador','Versión de Firma','Firmado','Culminado','Facturado'],
'mig' => ['Solicitado','Recibido','Presentado','Notificado','Recurso','Culminado','Facturado'],
'lit' => ['Solicitado','Recibido','Presentado','Admitido','Contestado','Pruebas','Contrapruebas','Alegatos','Sentencia','Apelación'],
];
function ok($data = null): void {
echo json_encode(['ok' => true, 'data' => $data, 'user' => Auth::user()], JSON_UNESCAPED_UNICODE);
exit;
}
function err(string $msg, int $code = 400): void {
http_response_code($code);
echo json_encode(['ok' => false, 'error' => $msg], JSON_UNESCAPED_UNICODE);
exit;
}
function body(): array {
return json_decode((string) file_get_contents('php://input'), true) ?? [];
}
function dept_of(array $b): string {
$d = strtolower((string)($b['departamento'] ?? $_GET['dept'] ?? ''));
if (!in_array($d, DEPTS, true)) err('Departamento inválido');
return $d;
}
function clean_yesno(?string $v): ?string {
if ($v === null) return null;
$v = trim($v);
if ($v === '') return null;
return in_array($v, ['Sí','No'], true) ? $v : null;
}
function clean_date(?string $v): ?string {
if (!$v) return null;
$v = trim($v);
if ($v === '') return null;
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : null;
}
function next_codigo(PDO $pdo, string $dept): string {
$pdo->beginTransaction();
try {
$sel = $pdo->prepare('SELECT next_seq FROM tramites_seq WHERE departamento = :d FOR UPDATE');
$sel->execute([':d' => $dept]);
$n = (int) ($sel->fetchColumn() ?: 1);
$upd = $pdo->prepare('UPDATE tramites_seq SET next_seq = :n WHERE departamento = :d');
$upd->execute([':n' => $n + 1, ':d' => $dept]);
$pdo->commit();
return sprintf('%s-%03d', PREFIX[$dept], $n);
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
}
function hydrate(PDO $pdo, array $row): array {
$tid = (int) $row['id'];
$obs = $pdo->prepare('SELECT id, texto, abogado, autor_email, fecha FROM tramites_obs WHERE tramite_id = :t ORDER BY fecha ASC');
$obs->execute([':t' => $tid]);
$row['obs'] = $obs->fetchAll();
$hrs = $pdo->prepare('SELECT id, abogado, hrs, descripcion, autor_email, fecha FROM tramites_hours WHERE tramite_id = :t ORDER BY fecha ASC');
$hrs->execute([':t' => $tid]);
$row['hours'] = array_map(function ($h) {
$h['hrs'] = (float) $h['hrs'];
return $h;
}, $hrs->fetchAll());
return $row;
}
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
$user = Auth::user();
$pdo = Db::pdo();
try {
if ($method === 'GET' && $action === 'list') {
$rows = $pdo->query('SELECT * FROM tramites_legales ORDER BY id DESC')->fetchAll();
$grouped = ['adm' => [], 'corp' => [], 'mig' => [], 'lit' => []];
foreach ($rows as $r) {
$grouped[$r['departamento']][] = hydrate($pdo, $r);
}
ok($grouped);
}
if ($method === 'POST' && $action === 'create') {
$b = body();
$dept = dept_of($b);
$est = (string) ($b['estatus'] ?? 'Solicitado');
if (!in_array($est, STATUS_MAP[$dept], true)) $est = STATUS_MAP[$dept][0];
$codigo = next_codigo($pdo, $dept);
$stmt = $pdo->prepare(
'INSERT INTO tramites_legales
(codigo, departamento, cliente, abogado, estatus, instrucciones,
nombre, institucion, tipo_documento, tipo_tramite,
doc_completa, permiso_trabajo, dependientes, fecha_vencimiento,
esfera, instancia, circuito, juzgado, tipo_proceso,
incidentes, amparo, fecha_termino, creado_por)
VALUES
(:codigo, :dept, :cliente, :abogado, :estatus, :instrucciones,
:nombre, :institucion, :tipo_documento, :tipo_tramite,
:doc_completa, :permiso_trabajo, :dependientes, :fecha_vencimiento,
:esfera, :instancia, :circuito, :juzgado, :tipo_proceso,
:incidentes, :amparo, :fecha_termino, :user)'
);
$stmt->execute([
':codigo' => $codigo,
':dept' => $dept,
':cliente' => (string)($b['cliente'] ?? ''),
':abogado' => (string)($b['abogado'] ?? ''),
':estatus' => $est,
':instrucciones' => (string)($b['instrucciones'] ?? ''),
':nombre' => (string)($b['nombre'] ?? ''),
':institucion' => (string)($b['institucion'] ?? ''),
':tipo_documento'=> (string)($b['tipoDocumento'] ?? ''),
':tipo_tramite' => (string)($b['tipoTramite'] ?? ''),
':doc_completa' => clean_yesno($b['docCompleta'] ?? null),
':permiso_trabajo'=> clean_yesno($b['permisoTrabajo'] ?? null),
':dependientes' => clean_yesno($b['dependientes'] ?? null),
':fecha_vencimiento' => clean_date($b['fechaVencimiento'] ?? null),
':esfera' => in_array($b['esfera'] ?? '', ['Civil','Penal'], true) ? $b['esfera'] : null,
':instancia' => in_array($b['instancia'] ?? '', ['Primera','Segunda','CSJ'], true) ? $b['instancia'] : null,
':circuito' => (string)($b['circuito'] ?? ''),
':juzgado' => (string)($b['juzgado'] ?? ''),
':tipo_proceso' => (string)($b['tipoProceso'] ?? ''),
':incidentes' => clean_yesno($b['incidentes'] ?? null),
':amparo' => clean_yesno($b['amparo'] ?? null),
':fecha_termino' => clean_date($b['fechaTermino'] ?? null),
':user' => $user['email'],
]);
$id = (int) $pdo->lastInsertId();
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
$row->execute([':id' => $id]);
ok(hydrate($pdo, $row->fetch()));
}
if ($method === 'PUT' && $action === 'update') {
$b = body();
$id = (int) ($b['_id'] ?? 0);
if (!$id) err('id requerido');
$cur = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
$cur->execute([':id' => $id]);
$before = $cur->fetch();
if (!$before) err('Trámite no encontrado', 404);
$dept = $before['departamento'];
$est = (string) ($b['estatus'] ?? $before['estatus']);
if (!in_array($est, STATUS_MAP[$dept], true)) $est = $before['estatus'];
$stmt = $pdo->prepare(
'UPDATE tramites_legales SET
cliente=:cliente, abogado=:abogado, estatus=:estatus, instrucciones=:instrucciones,
nombre=:nombre, institucion=:institucion,
tipo_documento=:tipo_documento, tipo_tramite=:tipo_tramite,
doc_completa=:doc_completa, permiso_trabajo=:permiso_trabajo,
dependientes=:dependientes, fecha_vencimiento=:fecha_vencimiento,
esfera=:esfera, instancia=:instancia, circuito=:circuito,
juzgado=:juzgado, tipo_proceso=:tipo_proceso,
incidentes=:incidentes, amparo=:amparo, fecha_termino=:fecha_termino,
actualizado_por=:user
WHERE id=:id'
);
$stmt->execute([
':cliente' => (string)($b['cliente'] ?? $before['cliente']),
':abogado' => (string)($b['abogado'] ?? $before['abogado']),
':estatus' => $est,
':instrucciones' => (string)($b['instrucciones'] ?? $before['instrucciones']),
':nombre' => (string)($b['nombre'] ?? $before['nombre']),
':institucion' => (string)($b['institucion'] ?? $before['institucion']),
':tipo_documento'=> (string)($b['tipoDocumento'] ?? $before['tipo_documento']),
':tipo_tramite' => (string)($b['tipoTramite'] ?? $before['tipo_tramite']),
':doc_completa' => clean_yesno($b['docCompleta'] ?? $before['doc_completa']),
':permiso_trabajo'=> clean_yesno($b['permisoTrabajo'] ?? $before['permiso_trabajo']),
':dependientes' => clean_yesno($b['dependientes'] ?? $before['dependientes']),
':fecha_vencimiento' => clean_date($b['fechaVencimiento'] ?? $before['fecha_vencimiento']),
':esfera' => in_array(($b['esfera'] ?? $before['esfera']), ['Civil','Penal'], true) ? ($b['esfera'] ?? $before['esfera']) : null,
':instancia' => in_array(($b['instancia'] ?? $before['instancia']), ['Primera','Segunda','CSJ'], true) ? ($b['instancia'] ?? $before['instancia']) : null,
':circuito' => (string)($b['circuito'] ?? $before['circuito']),
':juzgado' => (string)($b['juzgado'] ?? $before['juzgado']),
':tipo_proceso' => (string)($b['tipoProceso'] ?? $before['tipo_proceso']),
':incidentes' => clean_yesno($b['incidentes'] ?? $before['incidentes']),
':amparo' => clean_yesno($b['amparo'] ?? $before['amparo']),
':fecha_termino' => clean_date($b['fechaTermino'] ?? $before['fecha_termino']),
':user' => $user['email'],
':id' => $id,
]);
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
$row->execute([':id' => $id]);
ok(hydrate($pdo, $row->fetch()));
}
if ($method === 'PATCH' && $action === 'set_status') {
$b = body();
$id = (int) ($b['_id'] ?? 0);
$val = (string) ($b['estatus'] ?? '');
if (!$id) err('id requerido');
$cur = $pdo->prepare('SELECT departamento FROM tramites_legales WHERE id = :id');
$cur->execute([':id' => $id]);
$dept = $cur->fetchColumn();
if (!$dept) err('Trámite no encontrado', 404);
if (!in_array($val, STATUS_MAP[$dept], true)) err('Estatus inválido para el departamento');
$pdo->prepare('UPDATE tramites_legales SET estatus = :s, actualizado_por = :u WHERE id = :id')
->execute([':s' => $val, ':u' => $user['email'], ':id' => $id]);
ok(['id' => $id, 'estatus' => $val]);
}
if ($method === 'POST' && $action === 'add_obs') {
$b = body();
$id = (int) ($b['_id'] ?? 0);
$texto = trim((string)($b['texto'] ?? ''));
$abog = trim((string)($b['abogado'] ?? ''));
if (!$id) err('id requerido');
if ($texto === '') err('texto requerido');
$pdo->prepare('INSERT INTO tramites_obs (tramite_id, texto, abogado, autor_email) VALUES (?,?,?,?)')
->execute([$id, $texto, $abog, $user['email']]);
$pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id')
->execute([':u' => $user['email'], ':id' => $id]);
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
$row->execute([':id' => $id]);
ok(hydrate($pdo, $row->fetch()));
}
if ($method === 'POST' && $action === 'add_hours') {
$b = body();
$id = (int) ($b['_id'] ?? 0);
$abog = trim((string)($b['abogado'] ?? ''));
$hrs = (float) ($b['hrs'] ?? 0);
$desc = trim((string)($b['descripcion'] ?? ''));
if (!$id) err('id requerido');
if ($hrs <= 0) err('horas debe ser > 0');
$pdo->prepare('INSERT INTO tramites_hours (tramite_id, abogado, hrs, descripcion, autor_email) VALUES (?,?,?,?,?)')
->execute([$id, $abog, $hrs, $desc, $user['email']]);
$pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id')
->execute([':u' => $user['email'], ':id' => $id]);
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
$row->execute([':id' => $id]);
ok(hydrate($pdo, $row->fetch()));
}
if ($method === 'DELETE' && $action === 'delete') {
$id = (int) ($_GET['id'] ?? 0);
if (!$id) err('id requerido');
$pdo->prepare('DELETE FROM tramites_legales WHERE id = :id')->execute([':id' => $id]);
ok(['deleted' => $id]);
}
err('Acción no reconocida', 404);
} catch (PDOException $e) {
error_log('tramites db error: ' . $e->getMessage());
err('Error de base de datos', 500);
} catch (Throwable $e) {
error_log('tramites error: ' . $e->getMessage());
err('Error interno', 500);
}