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.
This commit is contained in:
@@ -0,0 +1,291 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/../../src/Auth.php';
|
||||||
|
Auth::requireAuth();
|
||||||
|
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
readfile(__DIR__ . '/../../views/tramites.html');
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Donna — Schema 004: Control de Trámites Legales
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tramites_legales (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
codigo VARCHAR(20) NOT NULL UNIQUE,
|
||||||
|
departamento ENUM('adm','corp','mig','lit') NOT NULL,
|
||||||
|
|
||||||
|
-- Comunes
|
||||||
|
cliente VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
abogado VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
estatus VARCHAR(50) NOT NULL DEFAULT 'Solicitado',
|
||||||
|
instrucciones TEXT NOT NULL,
|
||||||
|
fecha_solicitud DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
-- adm + corp
|
||||||
|
nombre VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
-- adm
|
||||||
|
institucion VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
-- corp
|
||||||
|
tipo_documento VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
-- mig
|
||||||
|
tipo_tramite VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
doc_completa ENUM('Sí','No') NULL,
|
||||||
|
permiso_trabajo ENUM('Sí','No') NULL,
|
||||||
|
dependientes ENUM('Sí','No') NULL,
|
||||||
|
fecha_vencimiento DATE NULL,
|
||||||
|
-- lit
|
||||||
|
esfera ENUM('Civil','Penal') NULL,
|
||||||
|
instancia ENUM('Primera','Segunda','CSJ') NULL,
|
||||||
|
circuito VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
juzgado VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
tipo_proceso VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
incidentes ENUM('Sí','No') NULL,
|
||||||
|
amparo ENUM('Sí','No') NULL,
|
||||||
|
fecha_termino DATE NULL,
|
||||||
|
|
||||||
|
-- Auditoría
|
||||||
|
creado_por VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
actualizado_por VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_dept (departamento),
|
||||||
|
INDEX idx_estatus (estatus),
|
||||||
|
INDEX idx_abogado (abogado),
|
||||||
|
INDEX idx_cliente (cliente),
|
||||||
|
INDEX idx_fecha (fecha_solicitud DESC)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tramites_obs (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tramite_id INT NOT NULL,
|
||||||
|
texto TEXT NOT NULL,
|
||||||
|
abogado VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
autor_email VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
fecha DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_tramite (tramite_id),
|
||||||
|
INDEX idx_fecha (fecha DESC),
|
||||||
|
CONSTRAINT fk_tramites_obs FOREIGN KEY (tramite_id)
|
||||||
|
REFERENCES tramites_legales(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tramites_hours (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tramite_id INT NOT NULL,
|
||||||
|
abogado VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
hrs DECIMAL(6,2) NOT NULL DEFAULT 0,
|
||||||
|
descripcion VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
|
autor_email VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
fecha DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_tramite (tramite_id),
|
||||||
|
INDEX idx_fecha (fecha DESC),
|
||||||
|
CONSTRAINT fk_tramites_hours FOREIGN KEY (tramite_id)
|
||||||
|
REFERENCES tramites_legales(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Contador para generar códigos ADM-001 / CRP-001 / MIG-001 / LIT-001
|
||||||
|
CREATE TABLE IF NOT EXISTS tramites_seq (
|
||||||
|
departamento VARCHAR(10) PRIMARY KEY,
|
||||||
|
next_seq INT NOT NULL DEFAULT 1
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO tramites_seq (departamento, next_seq) VALUES
|
||||||
|
('adm', 1), ('corp', 1), ('mig', 1), ('lit', 1);
|
||||||
+5
-5
@@ -13,12 +13,12 @@ final class Apps {
|
|||||||
'status' => 'live',
|
'status' => 'live',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'slug' => 'casos',
|
'slug' => 'tramites',
|
||||||
'title' => 'Gestión de Casos',
|
'title' => 'Control de Trámites Legales',
|
||||||
'description' => 'Expedientes, plazos procesales y notas de cada asunto.',
|
'description' => 'Administrativo, corporativo, migración y litigios. Estado, observaciones, horas e historial por trámite.',
|
||||||
'icon' => 'briefcase',
|
'icon' => 'briefcase',
|
||||||
'href' => '#',
|
'href' => '/tramites/',
|
||||||
'status' => 'coming-soon',
|
'status' => 'live',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'slug' => 'agenda',
|
'slug' => 'agenda',
|
||||||
|
|||||||
@@ -0,0 +1,647 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Control de Trámites Legales — AALN</title>
|
||||||
|
<style>
|
||||||
|
:root{--navy:#1B3A6B;--gold:#C9A84C;--gold-light:#f5e6c2;--red:#c0392b;--green:#27ae60;--blue:#2980b9;--gray:#6c757d;--bg:#f4f6fa;--white:#fff;--border:#dee2e6;--text:#212529;}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0;}
|
||||||
|
body{font-family:'Segoe UI',Arial,sans-serif;background:var(--bg);color:var(--text);font-size:13px;}
|
||||||
|
header{background:var(--navy);color:#fff;padding:14px 24px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 2px 8px rgba(0,0,0,.3);}
|
||||||
|
header h1{font-size:17px;font-weight:700;letter-spacing:.5px;}
|
||||||
|
header h1 span{color:var(--gold);}
|
||||||
|
.hdr-btns{display:flex;gap:8px;align-items:center;}
|
||||||
|
.hdr-btn{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.25);color:#fff;padding:5px 12px;border-radius:4px;cursor:pointer;font-size:12px;transition:.2s;text-decoration:none;}
|
||||||
|
.hdr-btn:hover{background:var(--gold);border-color:var(--gold);color:var(--navy);}
|
||||||
|
.hdr-user{display:flex;align-items:center;gap:8px;font-size:11.5px;color:rgba(255,255,255,.85);padding:4px 10px;background:rgba(255,255,255,.08);border-radius:14px;}
|
||||||
|
.sync-dot{width:7px;height:7px;border-radius:50%;background:#4CAF50;}
|
||||||
|
.sync-dot.syncing{background:var(--gold);}
|
||||||
|
.sync-dot.error{background:#ef5350;}
|
||||||
|
.tabs{display:flex;background:var(--navy);padding:0 16px;gap:2px;border-bottom:3px solid var(--gold);}
|
||||||
|
.tab{padding:10px 18px;color:rgba(255,255,255,.65);cursor:pointer;font-size:12.5px;font-weight:600;border-radius:4px 4px 0 0;transition:.2s;white-space:nowrap;}
|
||||||
|
.tab:hover{color:#fff;background:rgba(255,255,255,.1);}
|
||||||
|
.tab.active{background:var(--gold);color:var(--navy);}
|
||||||
|
.panel{display:none;padding:16px;}
|
||||||
|
.panel.active{display:block;}
|
||||||
|
.toolbar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:center;}
|
||||||
|
.toolbar input,.toolbar select{padding:6px 10px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:#fff;}
|
||||||
|
.toolbar input{flex:1;min-width:160px;}
|
||||||
|
.btn{padding:6px 14px;border:none;border-radius:4px;cursor:pointer;font-size:12px;font-weight:600;transition:.2s;}
|
||||||
|
.btn-primary{background:var(--navy);color:#fff;}
|
||||||
|
.btn-primary:hover{background:#14305a;}
|
||||||
|
.btn-gold{background:var(--gold);color:var(--navy);}
|
||||||
|
.btn-gold:hover{background:#b8923e;}
|
||||||
|
.btn-sm{padding:3px 9px;font-size:11px;}
|
||||||
|
.btn-danger{background:#e74c3c;color:#fff;}
|
||||||
|
.btn-danger:hover{background:#c0392b;}
|
||||||
|
.tbl-wrap{overflow-x:auto;background:#fff;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.08);}
|
||||||
|
table{width:100%;border-collapse:collapse;font-size:12px;}
|
||||||
|
th{background:var(--navy);color:#fff;padding:8px 10px;text-align:left;font-weight:600;white-space:nowrap;}
|
||||||
|
td{padding:7px 10px;border-bottom:1px solid var(--border);vertical-align:top;}
|
||||||
|
tr:hover>td{background:#f8f9ff;}
|
||||||
|
tr.expanded>td{background:#fffdf5;}
|
||||||
|
.toggle-btn{background:none;border:none;cursor:pointer;font-size:14px;color:var(--navy);font-weight:700;}
|
||||||
|
.badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10.5px;font-weight:700;white-space:nowrap;}
|
||||||
|
.b-solicitado{background:#e8f4fd;color:#1a6898;}
|
||||||
|
.b-recibido{background:#e8f0e8;color:#2e6b2e;}
|
||||||
|
.b-enproceso,.b-borrador,.b-presentado{background:#fff3cd;color:#856404;}
|
||||||
|
.b-notificado,.b-admitido{background:#d1ecf1;color:#0c5460;}
|
||||||
|
.b-recurso,.b-apelacion{background:#f8d7da;color:#721c24;}
|
||||||
|
.b-culminado,.b-firmado,.b-sentencia{background:#d4edda;color:#155724;}
|
||||||
|
.b-facturado{background:#e2e3e5;color:#383d41;}
|
||||||
|
.b-versionfirma{background:#fce4ec;color:#880e4f;}
|
||||||
|
.b-contestado,.b-pruebas{background:#ede7f6;color:#4527a0;}
|
||||||
|
.b-contrapruebas,.b-alegatos{background:#e3f2fd;color:#0d47a1;}
|
||||||
|
.b-civil{background:#bbdefb;color:#0d47a1;}
|
||||||
|
.b-penal{background:#ffcdd2;color:#b71c1c;}
|
||||||
|
.b-primera{background:#fff9c4;color:#f57f17;}
|
||||||
|
.b-segunda{background:#ffe0b2;color:#e65100;}
|
||||||
|
.b-csj{background:#f3e5f5;color:#6a1b9a;}
|
||||||
|
.b-si{background:#d4edda;color:#155724;}
|
||||||
|
.b-no{background:#f8d7da;color:#721c24;}
|
||||||
|
.detail-row td{background:#fafbfc;padding:0;}
|
||||||
|
.detail-inner{padding:14px 16px;border-left:4px solid var(--gold);}
|
||||||
|
.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px;}
|
||||||
|
@media(max-width:700px){.detail-grid{grid-template-columns:1fr;}}
|
||||||
|
.detail-card{background:#fff;border:1px solid var(--border);border-radius:6px;padding:10px 14px;}
|
||||||
|
.detail-card h4{font-size:11px;font-weight:700;color:var(--navy);margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--border);padding-bottom:5px;}
|
||||||
|
.detail-card p{font-size:12px;margin-bottom:4px;line-height:1.5;}
|
||||||
|
.detail-card p b{color:var(--navy);}
|
||||||
|
.instr-banner{background:var(--gold-light);border:1px solid var(--gold);border-radius:5px;padding:8px 12px;font-size:12px;margin-bottom:12px;color:#6b4c00;}
|
||||||
|
.obs-list{list-style:none;max-height:140px;overflow-y:auto;}
|
||||||
|
.obs-list li{font-size:11.5px;padding:5px 0;border-bottom:1px dashed #eee;line-height:1.5;}
|
||||||
|
.obs-list li .obs-meta{font-size:10px;color:var(--gray);}
|
||||||
|
.add-obs{display:flex;gap:6px;margin-top:8px;}
|
||||||
|
.add-obs textarea{flex:1;font-size:12px;border:1px solid var(--border);border-radius:4px;padding:5px 8px;resize:vertical;min-height:48px;}
|
||||||
|
.hours-list{list-style:none;}
|
||||||
|
.hours-list li{font-size:11.5px;padding:3px 0;display:flex;justify-content:space-between;border-bottom:1px dashed #eee;}
|
||||||
|
.hours-list li .hl-who{color:var(--navy);font-weight:600;}
|
||||||
|
.add-hours{display:flex;gap:6px;margin-top:8px;align-items:center;flex-wrap:wrap;}
|
||||||
|
.add-hours select,.add-hours input{font-size:12px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;}
|
||||||
|
.total-hrs{font-size:11px;font-weight:700;color:var(--navy);margin-top:6px;}
|
||||||
|
.progress-wrap{margin:8px 0;}
|
||||||
|
.progress-label{font-size:10px;color:var(--gray);margin-bottom:3px;}
|
||||||
|
.progress-bar{height:8px;background:#e9ecef;border-radius:4px;overflow:hidden;}
|
||||||
|
.progress-fill{height:100%;border-radius:4px;background:var(--gold);transition:.3s;}
|
||||||
|
.vence-alert{color:var(--red);font-weight:700;font-size:10.5px;}
|
||||||
|
.last-upd{font-size:10px;color:var(--gray);font-style:italic;}
|
||||||
|
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:1000;align-items:center;justify-content:center;}
|
||||||
|
.modal-overlay.open{display:flex;}
|
||||||
|
.modal{background:#fff;border-radius:8px;width:96%;max-width:580px;max-height:90vh;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,.25);}
|
||||||
|
.modal-header{background:var(--navy);color:#fff;padding:14px 18px;display:flex;justify-content:space-between;align-items:center;border-radius:8px 8px 0 0;position:sticky;top:0;z-index:1;}
|
||||||
|
.modal-header h3{font-size:14px;}
|
||||||
|
.modal-close{background:none;border:none;color:#fff;font-size:20px;cursor:pointer;line-height:1;}
|
||||||
|
.modal-body{padding:18px;}
|
||||||
|
.form-group{margin-bottom:12px;}
|
||||||
|
.form-group label{display:block;font-size:11.5px;font-weight:700;color:var(--navy);margin-bottom:4px;text-transform:uppercase;letter-spacing:.4px;}
|
||||||
|
.form-group input,.form-group select,.form-group textarea{width:100%;padding:7px 10px;border:1px solid var(--border);border-radius:4px;font-size:12.5px;}
|
||||||
|
.form-group textarea{resize:vertical;min-height:56px;}
|
||||||
|
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
|
||||||
|
.modal-footer{padding:12px 18px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px;}
|
||||||
|
.counts{display:flex;gap:10px;margin-bottom:10px;flex-wrap:wrap;}
|
||||||
|
.count-card{background:#fff;border-radius:6px;padding:8px 14px;border-left:4px solid var(--navy);box-shadow:0 1px 3px rgba(0,0,0,.07);font-size:12px;}
|
||||||
|
.count-card b{font-size:18px;display:block;color:var(--navy);}
|
||||||
|
.empty-state{text-align:center;padding:40px;color:var(--gray);font-size:13px;}
|
||||||
|
.toast{position:fixed;bottom:24px;right:24px;background:#1B3A6B;color:#fff;padding:10px 18px;border-radius:6px;font-size:12px;box-shadow:0 4px 14px rgba(0,0,0,.2);border-left:3px solid var(--gold);transform:translateY(20px);opacity:0;transition:.25s;z-index:2000;}
|
||||||
|
.toast.show{transform:translateY(0);opacity:1;}
|
||||||
|
.toast.error{border-left-color:#ef5350;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>⚖️ <span>AALN</span> — Control de Trámites Legales</h1>
|
||||||
|
<div class="hdr-btns">
|
||||||
|
<span class="hdr-user">
|
||||||
|
<span class="sync-dot" id="sync-dot"></span>
|
||||||
|
<span id="sync-label">Cargando…</span>
|
||||||
|
</span>
|
||||||
|
<a class="hdr-btn" href="/">← Portal</a>
|
||||||
|
<a class="hdr-btn" href="/auth/logout.php" onclick="return confirm('¿Cerrar sesión?')">Salir</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab active" onclick="switchTab('adm')">🏛 Administrativo</div>
|
||||||
|
<div class="tab" onclick="switchTab('corp')">⚖️ Corp · Com · Civil</div>
|
||||||
|
<div class="tab" onclick="switchTab('mig')">✈️ Migración</div>
|
||||||
|
<div class="tab" onclick="switchTab('lit')">⚔️ Litigios</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="panel-adm" class="panel active"></div>
|
||||||
|
<div id="panel-corp" class="panel"></div>
|
||||||
|
<div id="panel-mig" class="panel"></div>
|
||||||
|
<div id="panel-lit" class="panel"></div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="modal">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="modal-title">Nuevo Trámite</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="modal-body"></div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn" onclick="closeModal()">Cancelar</button>
|
||||||
|
<button class="btn btn-primary" onclick="saveModal()">Guardar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast" class="toast"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const ABOGADOS = ['Arias','Abrego','Lopez','Altafulla','Álvarez','Gutiérrez','Cedeño','Mariscal','Ávila','Agudo','Martínez','De Gracia','Góndola','González','Valdés'];
|
||||||
|
const PROVINCIAS = ['Bocas del Toro','Chiriquí','Coclé','Colón','Darién','Herrera','Los Santos','Panamá','Panamá Oeste','Veraguas','Comarca Ngäbe-Buglé','Comarca Guna Yala','Comarca Emberá-Wounaan'];
|
||||||
|
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']
|
||||||
|
};
|
||||||
|
const DEPT_LABELS = {adm:'Trámite Admin',corp:'Corp/Com/Civil',mig:'Migración',lit:'Litigios'};
|
||||||
|
const SI_NO = ['Sí','No'];
|
||||||
|
const API = 'api.php';
|
||||||
|
|
||||||
|
let data = {adm:[],corp:[],mig:[],lit:[]};
|
||||||
|
let currentDept = 'adm';
|
||||||
|
let editingId = null;
|
||||||
|
let authUser = {email:'', name:''};
|
||||||
|
|
||||||
|
function esc(s){return (s==null?'':String(s)).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');}
|
||||||
|
function setSync(state,label){
|
||||||
|
const dot=document.getElementById('sync-dot');
|
||||||
|
dot.className='sync-dot'+(state!=='ok'?' '+state:'');
|
||||||
|
document.getElementById('sync-label').textContent=label;
|
||||||
|
}
|
||||||
|
function toast(msg,type='ok'){
|
||||||
|
const el=document.getElementById('toast');
|
||||||
|
el.textContent=msg;
|
||||||
|
el.className='toast show'+(type==='error'?' error':'');
|
||||||
|
setTimeout(()=>{el.className='toast'+(type==='error'?' error':'');},3000);
|
||||||
|
}
|
||||||
|
function fmtDate(dt){
|
||||||
|
if(!dt) return '';
|
||||||
|
const d=new Date(dt); if(isNaN(d)) return dt;
|
||||||
|
return d.toLocaleDateString('es-PA',{day:'2-digit',month:'2-digit',year:'numeric'})
|
||||||
|
+' '+d.toLocaleTimeString('es-PA',{hour:'2-digit',minute:'2-digit'});
|
||||||
|
}
|
||||||
|
function fmtDateOnly(dt){
|
||||||
|
if(!dt) return '';
|
||||||
|
if(/^\d{4}-\d{2}-\d{2}$/.test(dt)) {
|
||||||
|
const [y,m,d]=dt.split('-');
|
||||||
|
return `${d}/${m}/${y}`;
|
||||||
|
}
|
||||||
|
return dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiCall(method, action, body=null, query=''){
|
||||||
|
const url = API + '?action=' + action + (query?'&'+query:'');
|
||||||
|
const opts = {method, headers:{'Accept':'application/json'}, credentials:'same-origin'};
|
||||||
|
if(body){ opts.headers['Content-Type']='application/json'; opts.body=JSON.stringify(body); }
|
||||||
|
const res = await fetch(url, opts);
|
||||||
|
let j;
|
||||||
|
try { j = await res.json(); } catch(e){ throw new Error('Respuesta inválida del servidor'); }
|
||||||
|
if(res.status===401 && j.auth_required){
|
||||||
|
window.location.href = j.login_url || '/auth/login.php?return_to=' + encodeURIComponent(location.pathname);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if(!res.ok || !j.ok) throw new Error(j.error || ('HTTP '+res.status));
|
||||||
|
if(j.user) authUser = j.user;
|
||||||
|
return j.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptRow(r){
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
_id: r.id,
|
||||||
|
id : r.codigo,
|
||||||
|
fechaSolicitud: fmtDate(r.fecha_solicitud),
|
||||||
|
docCompleta: r.doc_completa,
|
||||||
|
permisoTrabajo: r.permiso_trabajo,
|
||||||
|
dependientes: r.dependientes,
|
||||||
|
fechaVencimiento: r.fecha_vencimiento || '',
|
||||||
|
tipoDocumento: r.tipo_documento,
|
||||||
|
tipoTramite: r.tipo_tramite,
|
||||||
|
tipoProceso: r.tipo_proceso,
|
||||||
|
fechaTermino: r.fecha_termino || '',
|
||||||
|
obs: (r.obs||[]).map(o=>({...o, fecha:fmtDate(o.fecha)})),
|
||||||
|
hours: (r.hours||[]).map(h=>({...h, fecha:fmtDate(h.fecha)})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAll(){
|
||||||
|
setSync('syncing','Cargando…');
|
||||||
|
try {
|
||||||
|
const payload = await apiCall('GET','list');
|
||||||
|
if(payload === null) return;
|
||||||
|
data = {
|
||||||
|
adm: (payload.adm || []).map(adaptRow),
|
||||||
|
corp: (payload.corp || []).map(adaptRow),
|
||||||
|
mig: (payload.mig || []).map(adaptRow),
|
||||||
|
lit: (payload.lit || []).map(adaptRow),
|
||||||
|
};
|
||||||
|
setSync('ok', authUser.email ? authUser.email.split('@')[0] : 'Conectado');
|
||||||
|
renderAll();
|
||||||
|
} catch(e){
|
||||||
|
setSync('error','Sin conexión');
|
||||||
|
toast('Error al cargar: '+e.message,'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(dept){
|
||||||
|
currentDept = dept;
|
||||||
|
document.querySelectorAll('.tab').forEach((t,i)=>{
|
||||||
|
t.classList.toggle('active',['adm','corp','mig','lit'][i]===dept);
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
|
||||||
|
document.getElementById('panel-'+dept).classList.add('active');
|
||||||
|
renderPanel(dept);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAll(){['adm','corp','mig','lit'].forEach(renderPanel);}
|
||||||
|
|
||||||
|
function renderPanel(dept){
|
||||||
|
const panel=document.getElementById('panel-'+dept);
|
||||||
|
const rows=data[dept];
|
||||||
|
const statuses=STATUS_MAP[dept];
|
||||||
|
const search=(panel.querySelector('.search-input')||{value:''}).value.toLowerCase();
|
||||||
|
const fStatus=(panel.querySelector('.f-status')||{value:''}).value;
|
||||||
|
const fAbog=(panel.querySelector('.f-abog')||{value:''}).value;
|
||||||
|
const fEsfera=(panel.querySelector('.f-esfera')||{value:''}).value;
|
||||||
|
|
||||||
|
let filtered=rows.filter(r=>{
|
||||||
|
const txt=((r.nombre||r.tipoTramite||r.tipoProceso||'')+(r.cliente||'')+(r.abogado||'')+(r.id||'')).toLowerCase();
|
||||||
|
if(search && !txt.includes(search)) return false;
|
||||||
|
if(fStatus && r.estatus!==fStatus) return false;
|
||||||
|
if(fAbog && r.abogado!==fAbog) return false;
|
||||||
|
if(dept==='lit' && fEsfera && r.esfera!==fEsfera) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const total=rows.length;
|
||||||
|
const activos=rows.filter(r=>r.estatus!=='Culminado'&&r.estatus!=='Facturado').length;
|
||||||
|
const culminados=rows.filter(r=>r.estatus==='Culminado'||r.estatus==='Facturado').length;
|
||||||
|
|
||||||
|
panel.innerHTML=`
|
||||||
|
<div class="counts">
|
||||||
|
<div class="count-card"><b>${total}</b>Total</div>
|
||||||
|
<div class="count-card"><b>${activos}</b>Activos</div>
|
||||||
|
<div class="count-card"><b>${culminados}</b>Cerrados</div>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar">
|
||||||
|
<input class="search-input" placeholder="Buscar..." oninput="renderPanel('${dept}')" value="${esc(search)}">
|
||||||
|
<select class="f-status" onchange="renderPanel('${dept}')">
|
||||||
|
<option value="">— Estatus —</option>
|
||||||
|
${statuses.map(s=>`<option${fStatus===s?' selected':''}>${s}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
<select class="f-abog" onchange="renderPanel('${dept}')">
|
||||||
|
<option value="">— Abogado —</option>
|
||||||
|
${ABOGADOS.map(a=>`<option${fAbog===a?' selected':''}>${a}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
${dept==='lit'?`<select class="f-esfera" onchange="renderPanel('${dept}')">
|
||||||
|
<option value="">— Esfera —</option>
|
||||||
|
<option${fEsfera==='Civil'?' selected':''}>Civil</option><option${fEsfera==='Penal'?' selected':''}>Penal</option>
|
||||||
|
</select>`:''}
|
||||||
|
<button class="btn btn-primary" onclick="openModal('${dept}',null)">+ Nuevo</button>
|
||||||
|
</div>
|
||||||
|
<div class="tbl-wrap">
|
||||||
|
<table>
|
||||||
|
<thead><tr>${buildHeaders(dept)}</tr></thead>
|
||||||
|
<tbody id="tbody-${dept}">
|
||||||
|
${filtered.length?filtered.map(r=>buildRow(dept,r)).join(''):`<tr><td colspan="20"><div class="empty-state">Sin registros. Haz clic en <b>+ Nuevo</b> para agregar.</div></td></tr>`}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHeaders(dept){
|
||||||
|
const base=`<th></th><th>ID</th>`;
|
||||||
|
if(dept==='adm') return base+`<th>Institución</th><th>Nombre Trámite</th><th>Cliente</th><th>Abogado</th><th>Estatus</th><th>Fecha Solicitud</th><th>Horas</th><th>Acciones</th>`;
|
||||||
|
if(dept==='corp') return base+`<th>Tipo Documento</th><th>Nombre Trámite</th><th>Cliente</th><th>Abogado</th><th>Estatus</th><th>Fecha Solicitud</th><th>Horas</th><th>Acciones</th>`;
|
||||||
|
if(dept==='mig') return base+`<th>Tipo Trámite</th><th>Cliente</th><th>Abogado</th><th>Doc. Completa</th><th>Perm. Trabajo</th><th>Dependientes</th><th>Estatus</th><th>Fecha Solicitud</th><th>Vencimiento</th><th>Horas</th><th>Acciones</th>`;
|
||||||
|
if(dept==='lit') return base+`<th>Proceso / Juzgado</th><th>Cliente</th><th>Abogado</th><th>Esfera</th><th>Instancia</th><th>Doc. Completa</th><th>Incidentes</th><th>Amparo</th><th>Estatus</th><th>Fecha Solicitud</th><th>Término</th><th>Horas</th><th>Acciones</th>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusSlug(s){return (s||'').toLowerCase().replace(/\s+/g,'').replace(/\./g,'').replace(/[éèê]/g,'e').replace(/[áàâ]/g,'a').replace(/[íì]/g,'i').replace(/[óò]/g,'o').replace(/[úù]/g,'u');}
|
||||||
|
function statusBadge(s){return `<span class="badge b-${statusSlug(s)}">${esc(s)}</span>`;}
|
||||||
|
function yesnoBadge(v){return v?`<span class="badge ${v==='Sí'?'b-si':'b-no'}">${esc(v)}</span>`:'—';}
|
||||||
|
|
||||||
|
function totalHours(r){
|
||||||
|
if(!r.hours||!r.hours.length) return 0;
|
||||||
|
return r.hours.reduce((s,h)=>s+(parseFloat(h.hrs)||0),0).toFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function venceAlert(fecha){
|
||||||
|
if(!fecha) return '';
|
||||||
|
const diff=(new Date(fecha)-new Date())/(1000*60*60*24);
|
||||||
|
if(diff<0) return `<br><span class="vence-alert">⚠ Vencido</span>`;
|
||||||
|
if(diff<=30) return `<br><span class="vence-alert">⚠ ${Math.round(diff)}d</span>`;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRow(dept,r){
|
||||||
|
let cells='';
|
||||||
|
if(dept==='adm') cells=`<td>${esc(r.institucion)||'—'}</td><td>${esc(r.nombre)||'—'}</td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td><td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${totalHours(r)} h</td>`;
|
||||||
|
if(dept==='corp') cells=`<td>${esc(r.tipoDocumento)||'—'}</td><td>${esc(r.nombre)||'—'}</td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td><td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${totalHours(r)} h</td>`;
|
||||||
|
if(dept==='mig') cells=`<td>${esc(r.tipoTramite)||'—'}</td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td><td>${yesnoBadge(r.docCompleta)}</td><td>${yesnoBadge(r.permisoTrabajo)}</td><td>${yesnoBadge(r.dependientes)}</td><td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${fmtDateOnly(r.fechaVencimiento)||'—'}${venceAlert(r.fechaVencimiento)}</td><td>${totalHours(r)} h</td>`;
|
||||||
|
if(dept==='lit'){
|
||||||
|
const esfera=r.esfera||'';
|
||||||
|
const inst=r.instancia||'';
|
||||||
|
cells=`<td><b>${esc(r.tipoProceso)||'—'}</b><br><small>${esc(r.juzgado)||'—'} · ${esc(r.circuito)||'—'}</small></td><td>${esc(r.cliente)||'—'}</td><td>${esc(r.abogado)||'—'}</td><td>${esfera?`<span class="badge b-${esfera.toLowerCase()}">${esfera}</span>`:'—'}</td><td>${inst?`<span class="badge b-${statusSlug(inst)}">${inst}</span>`:'—'}</td><td>${yesnoBadge(r.docCompleta)}</td><td>${yesnoBadge(r.incidentes)}</td><td>${yesnoBadge(r.amparo)}</td><td>${statusBadge(r.estatus)}</td><td>${esc(r.fechaSolicitud)||'—'}</td><td>${fmtDateOnly(r.fechaTermino)||'—'}${venceAlert(r.fechaTermino)}</td><td>${totalHours(r)} h</td>`;
|
||||||
|
}
|
||||||
|
const actions=`<td><button class="btn btn-gold btn-sm" onclick="openModal('${dept}','${r.id}');event.stopPropagation()">✎</button> <button class="btn btn-danger btn-sm" onclick="deleteRow('${dept}','${r.id}');event.stopPropagation()">✕</button></td>`;
|
||||||
|
return `<tr id="row-${r.id}" onclick="toggleDetail(event,'${dept}','${r.id}')">
|
||||||
|
<td><button class="toggle-btn" id="tog-${r.id}">▶</button></td>
|
||||||
|
<td><b>${esc(r.id)}</b></td>
|
||||||
|
${cells}${actions}
|
||||||
|
</tr>
|
||||||
|
<tr class="detail-row" id="det-${r.id}" style="display:none">
|
||||||
|
<td colspan="20">${buildDetail(dept,r)}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDetail(dept,r){
|
||||||
|
let progress='';
|
||||||
|
if(dept==='lit'){
|
||||||
|
const steps=STATUS_MAP.lit;
|
||||||
|
const idx=steps.indexOf(r.estatus);
|
||||||
|
const pct=idx>=0?Math.round(((idx+1)/steps.length)*100):0;
|
||||||
|
const col=pct>=70?'#27ae60':pct>=40?'#2980b9':'var(--gold)';
|
||||||
|
progress=`<div class="progress-wrap">
|
||||||
|
<div class="progress-label">Avance procesal: ${pct}% — ${esc(r.estatus)||'—'}</div>
|
||||||
|
<div class="progress-bar"><div class="progress-fill" style="width:${pct}%;background:${col}"></div></div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ficha='';
|
||||||
|
if(dept==='adm') ficha=`<p><b>Institución:</b> ${esc(r.institucion)||'—'}</p><p><b>Nombre:</b> ${esc(r.nombre)||'—'}</p>`;
|
||||||
|
if(dept==='corp') ficha=`<p><b>Tipo Documento:</b> ${esc(r.tipoDocumento)||'—'}</p><p><b>Nombre:</b> ${esc(r.nombre)||'—'}</p>`;
|
||||||
|
if(dept==='mig') ficha=`<p><b>Tipo:</b> ${esc(r.tipoTramite)||'—'}</p><p><b>Doc. Completa:</b> ${esc(r.docCompleta)||'—'}</p><p><b>Permiso Trabajo:</b> ${esc(r.permisoTrabajo)||'—'}</p><p><b>Dependientes:</b> ${esc(r.dependientes)||'—'}</p><p><b>Vencimiento:</b> ${fmtDateOnly(r.fechaVencimiento)||'—'}</p>`;
|
||||||
|
if(dept==='lit') ficha=`<p><b>Esfera:</b> ${esc(r.esfera)||'—'}</p><p><b>Instancia:</b> ${esc(r.instancia)||'—'}</p><p><b>Circuito:</b> ${esc(r.circuito)||'—'}</p><p><b>Juzgado:</b> ${esc(r.juzgado)||'—'}</p><p><b>Tipo Proceso:</b> ${esc(r.tipoProceso)||'—'}</p><p><b>Incidentes:</b> ${esc(r.incidentes)||'—'} | <b>Amparo:</b> ${esc(r.amparo)||'—'}</p><p><b>Término:</b> ${fmtDateOnly(r.fechaTermino)||'—'}</p>`;
|
||||||
|
|
||||||
|
const obs=(r.obs||[]).map(o=>`<li>${esc(o.texto)}<br><span class="obs-meta">${esc(o.abogado)} · ${esc(o.fecha)}${o.autor_email?` · <i>${esc(o.autor_email.split('@')[0])}</i>`:''}</span></li>`).join('');
|
||||||
|
const lastUpd=r.obs&&r.obs.length?`<div class="last-upd">Última actualización: ${esc(r.obs[r.obs.length-1].fecha)}</div>`:'';
|
||||||
|
const hrs=(r.hours||[]).map(h=>`<li><span class="hl-who">${esc(h.abogado)}</span><span>${h.hrs} h · ${esc(h.fecha)}</span></li>`).join('');
|
||||||
|
const totalH=totalHours(r);
|
||||||
|
|
||||||
|
const statusSel=`<select onchange="changeStatus('${dept}','${r.id}',this.value)" onclick="event.stopPropagation()">
|
||||||
|
${STATUS_MAP[dept].map(s=>`<option${r.estatus===s?' selected':''}>${esc(s)}</option>`).join('')}
|
||||||
|
</select>`;
|
||||||
|
|
||||||
|
return `<div class="detail-inner">
|
||||||
|
${progress}
|
||||||
|
${r.instrucciones?`<div class="instr-banner">📌 <b>Instrucciones:</b> ${esc(r.instrucciones)}</div>`:''}
|
||||||
|
<div class="detail-grid">
|
||||||
|
<div class="detail-card">
|
||||||
|
<h4>Ficha del Trámite</h4>
|
||||||
|
${ficha}
|
||||||
|
<p><b>Cliente:</b> ${esc(r.cliente)||'—'}</p>
|
||||||
|
<p><b>Abogado:</b> ${esc(r.abogado)||'—'}</p>
|
||||||
|
<p><b>Fecha Solicitud:</b> ${esc(r.fechaSolicitud)||'—'}</p>
|
||||||
|
<p><b>Estatus:</b> ${statusSel}</p>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card">
|
||||||
|
<h4>Observaciones</h4>
|
||||||
|
${obs?`<ul class="obs-list">${obs}</ul>`:'<p style="color:var(--gray);font-size:11px">Sin observaciones.</p>'}
|
||||||
|
${lastUpd}
|
||||||
|
<div class="add-obs" onclick="event.stopPropagation()">
|
||||||
|
<textarea id="obs-txt-${r.id}" placeholder="Nueva observación..."></textarea>
|
||||||
|
<select id="obs-abog-${r.id}">${ABOGADOS.map(a=>`<option>${a}</option>`).join('')}</select>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="addObs('${dept}','${r.id}')">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-card">
|
||||||
|
<h4>Horas Incurridas</h4>
|
||||||
|
${hrs?`<ul class="hours-list">${hrs}</ul>`:'<p style="color:var(--gray);font-size:11px">Sin horas registradas.</p>'}
|
||||||
|
<div class="total-hrs">Total: ${totalH} h</div>
|
||||||
|
<div class="add-hours" onclick="event.stopPropagation()">
|
||||||
|
<select id="hr-abog-${r.id}">${ABOGADOS.map(a=>`<option>${a}</option>`).join('')}</select>
|
||||||
|
<input type="number" id="hr-val-${r.id}" min="0.1" step="0.1" placeholder="Horas" style="width:70px">
|
||||||
|
<input type="text" id="hr-desc-${r.id}" placeholder="Descripción" style="flex:1">
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="addHours('${dept}','${r.id}')">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findRow(dept,codigo){return data[dept].find(x=>x.id===codigo);}
|
||||||
|
|
||||||
|
function toggleDetail(e,dept,codigo){
|
||||||
|
if(e.target.tagName==='BUTTON'||e.target.tagName==='SELECT'||e.target.tagName==='TEXTAREA'||e.target.tagName==='INPUT') return;
|
||||||
|
const det=document.getElementById('det-'+codigo);
|
||||||
|
const tog=document.getElementById('tog-'+codigo);
|
||||||
|
const row=document.getElementById('row-'+codigo);
|
||||||
|
if(!det) return;
|
||||||
|
const open=det.style.display==='table-row';
|
||||||
|
det.style.display=open?'none':'table-row';
|
||||||
|
tog.textContent=open?'▶':'▼';
|
||||||
|
row.classList.toggle('expanded',!open);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeStatus(dept,codigo,val){
|
||||||
|
const r=findRow(dept,codigo); if(!r) return;
|
||||||
|
const prev=r.estatus;
|
||||||
|
r.estatus=val;
|
||||||
|
try {
|
||||||
|
await apiCall('PATCH','set_status',{_id:r._id, estatus:val});
|
||||||
|
toast('Estatus actualizado');
|
||||||
|
renderPanel(dept);
|
||||||
|
} catch(e){
|
||||||
|
r.estatus=prev;
|
||||||
|
toast('Error: '+e.message,'error');
|
||||||
|
renderPanel(dept);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addObs(dept,codigo){
|
||||||
|
const r=findRow(dept,codigo); if(!r) return;
|
||||||
|
const txt =document.getElementById('obs-txt-' +codigo);
|
||||||
|
const abog=document.getElementById('obs-abog-'+codigo);
|
||||||
|
if(!txt||!txt.value.trim()){toast('Escribe la observación','error');return;}
|
||||||
|
try {
|
||||||
|
const updated = await apiCall('POST','add_obs',{_id:r._id, texto:txt.value.trim(), abogado:abog.value});
|
||||||
|
Object.assign(r, adaptRow(updated));
|
||||||
|
renderPanel(dept);
|
||||||
|
setTimeout(()=>reopenDetail(codigo), 30);
|
||||||
|
toast('Observación añadida');
|
||||||
|
} catch(e){ toast('Error: '+e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addHours(dept,codigo){
|
||||||
|
const r=findRow(dept,codigo); if(!r) return;
|
||||||
|
const abog=document.getElementById('hr-abog-'+codigo);
|
||||||
|
const val =document.getElementById('hr-val-' +codigo);
|
||||||
|
const desc=document.getElementById('hr-desc-'+codigo);
|
||||||
|
if(!val||!val.value||parseFloat(val.value)<=0){toast('Horas debe ser > 0','error');return;}
|
||||||
|
try {
|
||||||
|
const updated = await apiCall('POST','add_hours',{_id:r._id, abogado:abog.value, hrs:parseFloat(val.value), descripcion:desc?desc.value:''});
|
||||||
|
Object.assign(r, adaptRow(updated));
|
||||||
|
renderPanel(dept);
|
||||||
|
setTimeout(()=>reopenDetail(codigo), 30);
|
||||||
|
toast('Horas registradas');
|
||||||
|
} catch(e){ toast('Error: '+e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function reopenDetail(codigo){
|
||||||
|
const det=document.getElementById('det-'+codigo);
|
||||||
|
const tog=document.getElementById('tog-'+codigo);
|
||||||
|
const row=document.getElementById('row-'+codigo);
|
||||||
|
if(det){det.style.display='table-row';if(tog)tog.textContent='▼';if(row)row.classList.add('expanded');}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRow(dept,codigo){
|
||||||
|
const r=findRow(dept,codigo); if(!r) return;
|
||||||
|
if(!confirm('¿Eliminar trámite '+codigo+'? Esta acción no se puede deshacer (incluye observaciones e horas).')) return;
|
||||||
|
try {
|
||||||
|
await apiCall('DELETE','delete',null,'id='+r._id);
|
||||||
|
data[dept]=data[dept].filter(x=>x._id!==r._id);
|
||||||
|
renderPanel(dept);
|
||||||
|
toast('Trámite eliminado');
|
||||||
|
} catch(e){ toast('Error: '+e.message,'error'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
let _modalDept='';
|
||||||
|
function openModal(dept,codigo){
|
||||||
|
_modalDept=dept;
|
||||||
|
const r=codigo?findRow(dept,codigo):null;
|
||||||
|
editingId=r?r._id:null;
|
||||||
|
document.getElementById('modal-title').textContent=r?('Editar '+r.id):('Nuevo Trámite — '+DEPT_LABELS[dept]);
|
||||||
|
document.getElementById('modal-body').innerHTML=buildForm(dept,r);
|
||||||
|
document.getElementById('modal').classList.add('open');
|
||||||
|
}
|
||||||
|
function closeModal(){document.getElementById('modal').classList.remove('open');editingId=null;}
|
||||||
|
|
||||||
|
function sel(arr,val,id){return `<select id="${id}">${arr.map(a=>`<option${a===val?' selected':''}>${a}</option>`).join('')}</select>`;}
|
||||||
|
function selBlank(arr,val,id){
|
||||||
|
const opts = `<option value=""></option>` + arr.map(a=>`<option${a===val?' selected':''}>${a}</option>`).join('');
|
||||||
|
return `<select id="${id}">${opts}</select>`;
|
||||||
|
}
|
||||||
|
function inp(id,val,type='text',ph=''){return `<input type="${type}" id="${id}" value="${esc(val||'')}" placeholder="${esc(ph)}">`;}
|
||||||
|
function fld(label,html){return `<div class="form-group"><label>${label}</label>${html}</div>`;}
|
||||||
|
|
||||||
|
function buildForm(dept,r){
|
||||||
|
const abogSel=sel(ABOGADOS,r?.abogado,'f-abogado');
|
||||||
|
const instrTxt=`<textarea id="f-instrucciones">${esc(r?.instrucciones||'')}</textarea>`;
|
||||||
|
const clienteInp=inp('f-cliente',r?.cliente,'text','Nombre del cliente');
|
||||||
|
const estatusSel=sel(STATUS_MAP[dept],r?.estatus,'f-estatus');
|
||||||
|
|
||||||
|
if(dept==='adm') return `
|
||||||
|
${fld('Institución',inp('f-institucion',r?.institucion,'text','Ej: Municipio de Panamá'))}
|
||||||
|
${fld('Nombre del Trámite',inp('f-nombre',r?.nombre,'text','Nombre descriptivo'))}
|
||||||
|
${fld('Instrucciones Especiales',instrTxt)}
|
||||||
|
<div class="form-row">
|
||||||
|
${fld('Cliente',clienteInp)}
|
||||||
|
${fld('Abogado Solicitante',abogSel)}
|
||||||
|
</div>
|
||||||
|
${fld('Estatus',estatusSel)}`;
|
||||||
|
|
||||||
|
if(dept==='corp') return `
|
||||||
|
${fld('Tipo de Documento',inp('f-tipoDocumento',r?.tipoDocumento,'text','Ej: Contrato, Acta, Estatutos...'))}
|
||||||
|
${fld('Nombre del Trámite',inp('f-nombre',r?.nombre,'text','Nombre descriptivo'))}
|
||||||
|
${fld('Instrucciones Especiales',instrTxt)}
|
||||||
|
<div class="form-row">
|
||||||
|
${fld('Cliente',clienteInp)}
|
||||||
|
${fld('Abogado Solicitante',abogSel)}
|
||||||
|
</div>
|
||||||
|
${fld('Estatus',estatusSel)}`;
|
||||||
|
|
||||||
|
if(dept==='mig') return `
|
||||||
|
${fld('Tipo de Trámite',inp('f-tipoTramite',r?.tipoTramite,'text','Ej: Residencia, Visa, Naturalización...'))}
|
||||||
|
${fld('Instrucciones Especiales',instrTxt)}
|
||||||
|
<div class="form-row">
|
||||||
|
${fld('Cliente',clienteInp)}
|
||||||
|
${fld('Abogado Solicitante',abogSel)}
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
${fld('Documentación Completa',selBlank(SI_NO,r?.docCompleta,'f-docCompleta'))}
|
||||||
|
${fld('Permiso de Trabajo',selBlank(SI_NO,r?.permisoTrabajo,'f-permisoTrabajo'))}
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
${fld('Dependientes',selBlank(SI_NO,r?.dependientes,'f-dependientes'))}
|
||||||
|
${fld('Fecha de Vencimiento',inp('f-fechaVencimiento',r?.fechaVencimiento,'date'))}
|
||||||
|
</div>
|
||||||
|
${fld('Estatus',estatusSel)}`;
|
||||||
|
|
||||||
|
if(dept==='lit') return `
|
||||||
|
<div class="form-row">
|
||||||
|
${fld('Esfera',selBlank(['Civil','Penal'],r?.esfera,'f-esfera'))}
|
||||||
|
${fld('Instancia',selBlank(['Primera','Segunda','CSJ'],r?.instancia,'f-instancia'))}
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
${fld('Circuito (Provincia)',selBlank(PROVINCIAS,r?.circuito,'f-circuito'))}
|
||||||
|
${fld('Juzgado',inp('f-juzgado',r?.juzgado,'text','Ej: 3er Juzgado Civil'))}
|
||||||
|
</div>
|
||||||
|
${fld('Tipo de Proceso',inp('f-tipoProceso',r?.tipoProceso,'text','Ej: Ejecutivo, Ordinario, Penal...'))}
|
||||||
|
${fld('Instrucciones Especiales',instrTxt)}
|
||||||
|
<div class="form-row">
|
||||||
|
${fld('Cliente',clienteInp)}
|
||||||
|
${fld('Abogado Solicitante',abogSel)}
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
${fld('Documentación Completa',selBlank(SI_NO,r?.docCompleta,'f-docCompleta'))}
|
||||||
|
${fld('Incidentes',selBlank(SI_NO,r?.incidentes,'f-incidentes'))}
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
${fld('Amparo',selBlank(SI_NO,r?.amparo,'f-amparo'))}
|
||||||
|
${fld('Fecha de Término',inp('f-fechaTermino',r?.fechaTermino,'date'))}
|
||||||
|
</div>
|
||||||
|
${fld('Estatus',estatusSel)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function g(id){const el=document.getElementById(id);return el?el.value:'';}
|
||||||
|
|
||||||
|
async function saveModal(){
|
||||||
|
const dept=_modalDept;
|
||||||
|
const payload = {
|
||||||
|
departamento: dept,
|
||||||
|
cliente: g('f-cliente'),
|
||||||
|
abogado: g('f-abogado'),
|
||||||
|
estatus: g('f-estatus'),
|
||||||
|
instrucciones: g('f-instrucciones'),
|
||||||
|
};
|
||||||
|
if(dept==='adm'){ payload.institucion=g('f-institucion'); payload.nombre=g('f-nombre'); }
|
||||||
|
if(dept==='corp'){ payload.tipoDocumento=g('f-tipoDocumento'); payload.nombre=g('f-nombre'); }
|
||||||
|
if(dept==='mig'){
|
||||||
|
payload.tipoTramite = g('f-tipoTramite');
|
||||||
|
payload.docCompleta = g('f-docCompleta');
|
||||||
|
payload.permisoTrabajo = g('f-permisoTrabajo');
|
||||||
|
payload.dependientes = g('f-dependientes');
|
||||||
|
payload.fechaVencimiento= g('f-fechaVencimiento');
|
||||||
|
}
|
||||||
|
if(dept==='lit'){
|
||||||
|
payload.esfera = g('f-esfera');
|
||||||
|
payload.instancia = g('f-instancia');
|
||||||
|
payload.circuito = g('f-circuito');
|
||||||
|
payload.juzgado = g('f-juzgado');
|
||||||
|
payload.tipoProceso = g('f-tipoProceso');
|
||||||
|
payload.docCompleta = g('f-docCompleta');
|
||||||
|
payload.incidentes = g('f-incidentes');
|
||||||
|
payload.amparo = g('f-amparo');
|
||||||
|
payload.fechaTermino = g('f-fechaTermino');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let saved;
|
||||||
|
if(editingId){
|
||||||
|
payload._id = editingId;
|
||||||
|
saved = await apiCall('PUT','update',payload);
|
||||||
|
} else {
|
||||||
|
saved = await apiCall('POST','create',payload);
|
||||||
|
}
|
||||||
|
const adapted = adaptRow(saved);
|
||||||
|
const idx = data[dept].findIndex(r => r._id === adapted._id);
|
||||||
|
if(idx >= 0) data[dept][idx] = adapted; else data[dept].unshift(adapted);
|
||||||
|
closeModal();
|
||||||
|
renderPanel(dept);
|
||||||
|
toast(editingId?'Trámite actualizado':'Trámite creado: '+adapted.id);
|
||||||
|
} catch(e){
|
||||||
|
toast('Error al guardar: '+e.message,'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAll();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user