'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()); $inv = $pdo->prepare( 'SELECT u.id, u.email, u.name, u.apellido FROM tramites_involucrados ti JOIN users u ON u.id = ti.user_id WHERE ti.tramite_id = :t ORDER BY u.name ASC, u.email ASC' ); $inv->execute([':t' => $tid]); $row['involucrados'] = array_map(function ($u) { $u['id'] = (int)$u['id']; return $u; }, $inv->fetchAll()); return $row; } /** * ¿El usuario actual puede ver/editar este trámite? * Socio → siempre * Resto → si es creador, o abogado solicitante (apellido), o involucrado */ function canView(PDO $pdo, int $tid): bool { if (Auth::isSocio()) return true; $email = Auth::user()['email']; $uid = Auth::userId(); $stmt = $pdo->prepare( "SELECT 1 FROM tramites_legales t LEFT JOIN users ua ON ua.apellido = t.abogado AND ua.activo = 1 LEFT JOIN tramites_involucrados ti ON ti.tramite_id = t.id AND ti.user_id = :uid WHERE t.id = :tid AND (t.creado_por = :email OR ua.id = :uid2 OR ti.user_id IS NOT NULL) LIMIT 1" ); $stmt->execute([':tid' => $tid, ':email' => $email, ':uid' => $uid, ':uid2' => $uid]); return $stmt->fetchColumn() !== false; } /** Reemplaza la lista de involucrados del trámite por la lista dada (array de user_id). */ function syncInvolucrados(PDO $pdo, int $tid, array $userIds, string $addedBy): void { $pdo->prepare('DELETE FROM tramites_involucrados WHERE tramite_id = :t')->execute([':t' => $tid]); if (empty($userIds)) return; $placeholders = implode(',', array_fill(0, count($userIds), '?')); $check = $pdo->prepare("SELECT id FROM users WHERE activo = 1 AND id IN ($placeholders)"); $check->execute(array_map('intval', $userIds)); $valid = array_map('intval', $check->fetchAll(PDO::FETCH_COLUMN)); if (!$valid) return; $ins = $pdo->prepare('INSERT INTO tramites_involucrados (tramite_id, user_id, added_by) VALUES (?,?,?)'); foreach ($valid as $uid) $ins->execute([$tid, $uid, $addedBy]); } $method = $_SERVER['REQUEST_METHOD']; $action = $_GET['action'] ?? ''; $user = Auth::user(); $myId = Auth::userId(); $pdo = Db::pdo(); try { if ($method === 'GET' && $action === 'list') { if (Auth::isSocio()) { $rows = $pdo->query('SELECT * FROM tramites_legales ORDER BY id DESC')->fetchAll(); } else { $stmt = $pdo->prepare( "SELECT DISTINCT t.* FROM tramites_legales t LEFT JOIN users ua ON ua.apellido = t.abogado AND ua.activo = 1 LEFT JOIN tramites_involucrados ti ON ti.tramite_id = t.id AND ti.user_id = :uid WHERE t.creado_por = :email OR ua.id = :uid2 OR ti.user_id IS NOT NULL ORDER BY t.id DESC" ); $stmt->execute([':email' => $user['email'], ':uid' => $myId, ':uid2' => $myId]); $rows = $stmt->fetchAll(); } $grouped = ['adm' => [], 'corp' => [], 'mig' => [], 'lit' => []]; foreach ($rows as $r) { $grouped[$r['departamento']][] = hydrate($pdo, $r); } ok($grouped); } if ($method === 'POST' && $action === 'create') { if (!Auth::canCreate()) err('Tu rol no permite crear trámites. Solo Socios y Abogados pueden.', 403); $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(); // Involucrados: si vienen del payload, se respetan; si no, se auto-incluye al creador $involIds = array_map('intval', (array)($b['involucrados'] ?? [])); if ($myId > 0 && !in_array($myId, $involIds, true)) $involIds[] = $myId; syncInvolucrados($pdo, $id, $involIds, $user['email']); $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'); if (!canView($pdo, $id)) err('No tienes acceso a este trámite', 403); $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, ]); if (array_key_exists('involucrados', $b) && is_array($b['involucrados'])) { syncInvolucrados($pdo, $id, array_map('intval', $b['involucrados']), $user['email']); } $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'); if (!canView($pdo, $id)) err('No tienes acceso a este trámite', 403); $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'); if (!canView($pdo, $id)) err('No tienes acceso a este trámite', 403); $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'); if (!canView($pdo, $id)) err('No tienes acceso a este trámite', 403); $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') { if (!Auth::canDelete()) err('Tu rol no permite eliminar trámites. Solo Socios y Abogados pueden.', 403); $id = (int) ($_GET['id'] ?? 0); if (!$id) err('id requerido'); if (!canView($pdo, $id)) err('No tienes acceso a este trámite', 403); $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); }