feat: módulo de empresa con números WhatsApp y endpoints API por empresa

- DB: company_phones (3 tipos de permiso, límites por usuarios ERP)
- DB: company_endpoints (11 endpoints upload/download configurables)
- DB: erp_active_users en companies (base para calcular límites)
- Company edit: 3 pestañas — General / Números WhatsApp / Endpoints API
- Límites: tipo 1 = usuarios_erp×10, tipo 2+3 = usuarios_erp
- Endpoints: URL configurable + botón Probar + preview de respuesta JSON
- Layout::close() agregado

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-19 21:57:31 -05:00
co-authored by Claude Sonnet 4.6
parent b083d49769
commit 860e93a20b
4 changed files with 517 additions and 82 deletions
+464 -82
View File
@@ -945,11 +945,32 @@ HTML;
// ─── GET /admin/company/edit ─────────────────────────────────────────────
// ── Catálogo de endpoints (fijo para todas las empresas) ─────────────────
private static function endpointCatalog(): array
{
return [
'upload' => [
'cosecha_up' => 'Ingreso ciclos cosecha, sanidad, polinización',
'bascula_up' => 'Ingreso tiquetes de báscula',
'ausentismo_up' => 'Reporte de ausentismos',
'pluviometria_up' => 'Reporte de Pluviometría',
'subproductos_up' => 'Reporte de Salida subproductos',
],
'download' => [
'cosecha_dn' => 'Informe ciclos cosecha, sanidad, polinización',
'produccion_dn' => 'Informe de producción',
'ausentismo_dn' => 'Informe de ausentismos',
'pluviometria_dn' => 'Informe de Pluviometría',
'subproductos_dn' => 'Informe de Salida subproductos de extractora',
'numeros_dn' => 'Informe de números activos',
],
];
}
public static function companyEdit(): void
{
SessionAuth::require();
$user = SessionAuth::user();
$userName = self::h($user['name'] ?? 'Admin');
$id = (int)($_GET['id'] ?? 0);
$isEdit = $id > 0;
$company = $isEdit ? CompanyRepository::findById($id) : null;
@@ -965,104 +986,465 @@ HTML;
$bot_type = $company['bot_type'] ?? 'normal';
$requires_approval = !empty($company['requires_approval']);
$is_active = !empty($company['is_active']);
$erp_active_users = (int)($company['erp_active_users'] ?? 0);
$reqAprChecked = $requires_approval ? 'checked' : '';
$isActChecked = $is_active ? 'checked' : '';
$botOptions = '';
foreach (['normal' => 'Normal', 'ai' => 'AI', 'hybrid' => 'Híbrido'] as $val => $label) {
foreach (['normal' => 'Normal', 'ai' => 'AI', 'hybrid' => 'Híbrido'] as $val => $lbl) {
$sel = $bot_type === $val ? 'selected' : '';
$botOptions .= "<option value=\"{$val}\" {$sel}>{$label}</option>\n";
$botOptions .= "<option value=\"{$val}\" {$sel}>{$lbl}</option>\n";
}
$pageTitle = $isEdit ? 'Editar Empresa' : 'Nueva Empresa';
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
echo Layout::open($pageTitle, 'companies', $user['name'] ?? 'Admin');
echo <<<HTML
<style>
.wrap{padding:24px;max-width:800px;margin:0 auto}
.card h2{font-size:18px;margin-bottom:22px;color:#0b3d91;font-weight:700}
.form-group textarea{min-height:120px;resize:vertical;font-family:'SF Mono',Monaco,monospace;font-size:12px;line-height:1.6}
.form-group .hint{font-size:11px;color:#7a8291;margin-top:4px}
.btn-row{display:flex;gap:12px;margin-top:28px;padding-top:20px;border-top:1px solid #eef1f5}
@media(max-width:768px){.wrap{padding:12px}.card{padding:16px}.form-row .form-group{min-width:100%}}
</style>
<div class="wrap">
<div class="card">
<h2>{$pageTitle}</h2>
<form method="POST" action="/admin/company/save">
HTML;
// ── Tab: Números WhatsApp ─────────────────────────────────────────────
$phonesHtml = '';
$epHtml = '';
if ($isEdit) {
echo '<input type="hidden" name="id" value="' . $id . '">';
}
echo <<<HTML
<div class="form-row">
<div class="form-group">
<label for="name">Nombre *</label>
<input type="text" id="name" name="name" value="{$name}" required placeholder="Ej: Comercializadora ABC">
<div class="hint">Nombre interno de la empresa</div>
</div>
<div class="form-group">
<label for="display_name">Nombre mostrado</label>
<input type="text" id="display_name" name="display_name" value="{$display_name}" placeholder="Ej: ABC S.A. de C.V.">
<div class="hint">Nombre visible en reportes (opcional)</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="phone_number_id">WhatsApp Phone Number ID *</label>
<input type="text" id="phone_number_id" name="phone_number_id" value="{$phone_number_id}" required placeholder="Ej: 123456789012345">
<div class="hint">ID num&eacute;rico del n&uacute;mero de tel&eacute;fono de WhatsApp Business</div>
</div>
<div class="form-group">
<label for="display_phone">Tel&eacute;fono mostrado</label>
<input type="text" id="display_phone" name="display_phone" value="{$display_phone}" placeholder="Ej: +521234567890">
<div class="hint">N&uacute;mero telef&oacute;nico visible (opcional)</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="api_base_url">API Base URL *</label>
<input type="text" id="api_base_url" name="api_base_url" value="{$api_base_url}" required placeholder="Ej: https://erp.ejemplo.com/api/">
<div class="hint">Endpoint base del ERP para esta empresa</div>
</div>
<div class="form-group">
<label for="api_key">API Key</label>
<input type="text" id="api_key" name="api_key" value="{$api_key}" placeholder="Token de autenticaci&oacute;n">
<div class="hint">Clave API para autenticaci&oacute;n ERP &harr; Bot (opcional)</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="bot_type">Tipo de Bot</label>
<select id="bot_type" name="bot_type">{$botOptions}</select>
<div class="hint">Comportamiento del bot: normal (reglas), AI (inteligente), h&iacute;brido (combinado)</div>
</div>
<div class="form-group checkbox">
<input type="checkbox" id="requires_approval" name="requires_approval" value="1" {$reqAprChecked}>
<label for="requires_approval">Requiere aprobaci&oacute;n manual</label>
</div>
<div class="form-group checkbox">
<input type="checkbox" id="is_active" name="is_active" value="1" {$isActChecked}>
<label for="is_active">Activo</label>
</div>
// Phones
$phones = db()->prepare("SELECT * FROM company_phones WHERE company_id=? ORDER BY permission_type, wa_number");
$phones->execute([$id]);
$phones = $phones->fetchAll(\PDO::FETCH_ASSOC);
$limitType1 = max(1, $erp_active_users * 10);
$limitType23 = max(1, $erp_active_users);
$countT1 = count(array_filter($phones, fn($p) => (int)$p['permission_type'] === 1));
$countT23 = count(array_filter($phones, fn($p) => (int)$p['permission_type'] !== 1));
$pTypeName = ['1' => 'Solo reporta', '2' => 'Solo recibe', '3' => 'Reporta y recibe'];
$pTypeCls = ['1' => 'badge-blue', '2' => 'badge-green', '3' => 'badge-teal'];
$rows = '';
foreach ($phones as $ph) {
$t = (string)$ph['permission_type'];
$tc = $pTypeCls[$t] ?? 'badge-gray';
$tn = $pTypeName[$t] ?? '?';
$lbl = self::h($ph['label'] ?? '');
$num = self::h($ph['wa_number']);
$act = $ph['is_active'] ? '<span class="badge badge-green">Activo</span>' : '<span class="badge badge-gray">Inactivo</span>';
$rows .= "<tr>
<td style='font-family:monospace;font-size:13px'>+{$num}</td>
<td>{$lbl}</td>
<td><span class='badge {$tc}'>{$tn}</span></td>
<td>{$act}</td>
<td><button class='btn-danger-sm' onclick='deletePhone({$ph['id']},this)'>Eliminar</button></td>
</tr>";
}
if (!$rows) $rows = '<tr><td colspan="5" class="empty">Sin números registrados</td></tr>';
$warnERP = $erp_active_users === 0
? '<div class="toast toast-error" style="margin-bottom:12px">⚠ Usuarios activos ERP = 0. Configura el campo en la pestaña General o ejecuta sincronización.</div>'
: '';
$pTypeOpts = '';
foreach ($pTypeName as $v => $n) $pTypeOpts .= "<option value='{$v}'>{$n}</option>";
$phonesHtml = <<<HTML
{$warnERP}
<div style="display:flex;align-items:center;gap:16px;margin-bottom:14px;flex-wrap:wrap">
<span class="badge badge-blue">Tipo 1 (Solo reporta): {$countT1} / {$limitType1}</span>
<span class="badge badge-teal">Tipo 2+3 (Reciben): {$countT23} / {$limitType23}</span>
</div>
<div class="card" style="margin-bottom:16px">
<table>
<thead><tr><th>Número WA</th><th>Etiqueta</th><th>Permiso</th><th>Estado</th><th></th></tr></thead>
<tbody id="phoneRows">{$rows}</tbody>
</table>
</div>
<div class="card">
<div class="card-h">Agregar número</div>
<div class="card-b">
<div class="form-row">
<div class="form-group">
<label>Número (sin +, ej: 573001234567)</label>
<input type="text" id="newPhone" placeholder="573001234567" pattern="[0-9]+" style="max-width:220px">
</div>
<div class="form-group">
<label for="config_json">Configuraci&oacute;n JSON</label>
<textarea id="config_json" name="config_json" placeholder='{"key": "value", ...}'>{$config_json}</textarea>
<div class="hint">Configuraci&oacute;n adicional del bot en formato JSON</div>
<label>Etiqueta</label>
<input type="text" id="newPhoneLabel" placeholder="Ej: Supervisor finca A">
</div>
<div class="btn-row">
<button type="submit" class="btn-primary">&#128190; Guardar</button>
<a href="/admin/companies" class="btn-secondary">Cancelar</a>
<div class="form-group">
<label>Tipo de permiso</label>
<select id="newPhoneType">{$pTypeOpts}</select>
</div>
</form>
</div>
<div id="phoneMsg"></div>
<button class="btn-primary" onclick="addPhone({$id})">Agregar número</button>
</div>
</div>
</body>
</html>
HTML;
// Endpoints
$epRows = db()->prepare("SELECT endpoint_key, url, method, last_response, last_called_at FROM company_endpoints WHERE company_id=?");
$epRows->execute([$id]);
$epMap = [];
foreach ($epRows->fetchAll(\PDO::FETCH_ASSOC) as $r) $epMap[$r['endpoint_key']] = $r;
$catalog = self::endpointCatalog();
$renderEpSection = function(string $dir, string $dirLabel, string $badge) use ($catalog, $epMap, $id): string {
$html = "<h3 style='font-size:13px;font-weight:600;color:#111827;margin:0 0 12px'>{$badge} {$dirLabel}</h3>";
foreach ($catalog[$dir] as $key => $label) {
$saved = $epMap[$key] ?? [];
$url = self::h($saved['url'] ?? '');
$meth = $saved['method'] ?? 'GET';
$lastAt = $saved['last_called_at'] ? '<span style="font-size:11px;color:#9ca3af">' . self::h($saved['last_called_at']) . '</span>' : '';
$mSel = fn($v) => $meth === $v ? 'selected' : '';
$html .= <<<EP
<div style="border:1px solid #e5e7eb;border-radius:8px;padding:14px;margin-bottom:10px">
<div style="font-size:13px;font-weight:500;color:#111827;margin-bottom:8px">{$label} {$lastAt}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end">
<select id="m_{$key}" style="width:90px;padding:7px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px">
<option {$mSel('GET')}>GET</option><option {$mSel('POST')}>POST</option>
</select>
<input type="text" id="u_{$key}" value="{$url}" placeholder="https://..." style="flex:1;min-width:200px;padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px">
<button class="btn-secondary" onclick="saveEp({$id},'{$key}','{$dir}')">Guardar</button>
<button class="btn-sm" onclick="testEp({$id},'{$key}')">Probar</button>
</div>
<div id="ep_resp_{$key}" style="margin-top:8px;display:none"></div>
</div>
EP;
}
return $html;
};
$epHtml = $renderEpSection('upload', 'Subir información (WhatsApp → ERP)', '<span class="badge badge-blue">↑ Upload</span>')
. '<div style="margin:20px 0;border-top:1px solid #e5e7eb"></div>'
. $renderEpSection('download', 'Bajar información (ERP → WhatsApp)', '<span class="badge badge-green">↓ Download</span>');
}
$tabsHtml = $isEdit ? <<<HTML
<div style="display:flex;gap:0;border-bottom:1px solid #e5e7eb;margin-bottom:20px">
<button class="tab-btn" id="tb-general" onclick="showTab('general')">General</button>
<button class="tab-btn" id="tb-phones" onclick="showTab('phones')">Números WhatsApp</button>
<button class="tab-btn" id="tb-endpoints" onclick="showTab('endpoints')">Endpoints API</button>
</div>
HTML : '';
echo <<<HTML
<style>
.tab-btn{background:none;border:none;border-bottom:2px solid transparent;padding:10px 18px;font-size:13px;font-weight:500;color:#6b7280;cursor:pointer;transition:color .12s,border-color .12s;margin-bottom:-1px}
.tab-btn.active{color:#111827;border-bottom-color:#111827}
.tab-panel{display:none}.tab-panel.active{display:block}
</style>
<div class="wrap" style="max-width:860px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
<h1 style="font-size:18px;font-weight:700;color:#111827">{$pageTitle}</h1>
<a href="/admin/companies" class="btn-secondary" style="font-size:12px">← Empresas</a>
</div>
{$tabsHtml}
<!-- Tab General -->
<div id="panel-general" class="tab-panel">
<div class="card">
<div class="card-b">
<form method="POST" action="/admin/company/save">
HTML;
if ($isEdit) echo '<input type="hidden" name="id" value="' . $id . '">';
echo <<<HTML
<div class="form-row">
<div class="form-group">
<label>Nombre *</label>
<input type="text" name="name" value="{$name}" required placeholder="Comercializadora ABC">
</div>
<div class="form-group">
<label>Nombre mostrado</label>
<input type="text" name="display_name" value="{$display_name}" placeholder="ABC S.A.">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>WhatsApp Phone Number ID *</label>
<input type="text" name="phone_number_id" value="{$phone_number_id}" required placeholder="123456789012345">
<div class="hint">ID del n&uacute;mero WA Business API (para enviar mensajes)</div>
</div>
<div class="form-group">
<label>Tel&eacute;fono mostrado</label>
<input type="text" name="display_phone" value="{$display_phone}" placeholder="+573001234567">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>API Base URL *</label>
<input type="text" name="api_base_url" value="{$api_base_url}" required placeholder="https://erp.empresa.com/api/">
<div class="hint">Base URL del ERP de esta empresa</div>
</div>
<div class="form-group">
<label>API Key</label>
<input type="text" name="api_key" value="{$api_key}" placeholder="Token Bearer">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Usuarios activos en ERP</label>
<input type="number" name="erp_active_users" value="{$erp_active_users}" min="0" placeholder="0">
<div class="hint">Define los l&iacute;mites de n&uacute;meros WA. Se actualiza con sincronizaci&oacute;n ERP.</div>
</div>
<div class="form-group">
<label>Tipo de Bot</label>
<select name="bot_type">{$botOptions}</select>
</div>
</div>
<div style="display:flex;gap:20px;margin-bottom:14px;flex-wrap:wrap">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer">
<input type="checkbox" name="requires_approval" value="1" {$reqAprChecked}> Requiere aprobaci&oacute;n manual
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer">
<input type="checkbox" name="is_active" value="1" {$isActChecked}> Activa
</label>
</div>
<div class="form-group">
<label>Configuraci&oacute;n JSON adicional</label>
<textarea name="config_json" rows="5" style="font-family:monospace;font-size:12px">{$config_json}</textarea>
</div>
<div style="display:flex;gap:10px;padding-top:16px;border-top:1px solid #e5e7eb">
<button type="submit" class="btn-primary">Guardar</button>
<a href="/admin/companies" class="btn-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
HTML;
if ($isEdit) {
echo <<<HTML
<!-- Tab Números WhatsApp -->
<div id="panel-phones" class="tab-panel">
{$phonesHtml}
</div>
<!-- Tab Endpoints API -->
<div id="panel-endpoints" class="tab-panel">
{$epHtml}
</div>
<script>
function showTab(name) {
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById('panel-' + name).classList.add('active');
document.getElementById('tb-' + name).classList.add('active');
history.replaceState(null,'',location.pathname+'?id={$id}&tab='+name);
}
const initTab = new URLSearchParams(location.search).get('tab') || 'general';
showTab(initTab);
async function addPhone(cid) {
const num = document.getElementById('newPhone').value.trim().replace(/\D/g,'');
const lbl = document.getElementById('newPhoneLabel').value.trim();
const typ = document.getElementById('newPhoneType').value;
const msg = document.getElementById('phoneMsg');
if (!num) { msg.innerHTML='<div class="toast toast-error">Ingresa un número válido</div>'; return; }
const fd = new FormData();
fd.append('company_id', cid); fd.append('wa_number', num);
fd.append('label', lbl); fd.append('permission_type', typ);
const r = await fetch('/admin/company/phone/save', {method:'POST',body:fd});
const j = await r.json();
if (j.ok) {
msg.innerHTML='<div class="toast toast-success">'+j.message+'</div>';
setTimeout(()=>location.reload(),800);
} else {
msg.innerHTML='<div class="toast toast-error">'+j.error+'</div>';
}
}
async function deletePhone(pid, btn) {
if (!confirm('¿Eliminar este número?')) return;
btn.disabled=true;
const fd=new FormData(); fd.append('id',pid);
const r=await fetch('/admin/company/phone/delete',{method:'POST',body:fd});
const j=await r.json();
if (j.ok) btn.closest('tr').remove();
else { alert(j.error); btn.disabled=false; }
}
async function saveEp(cid, key, dir) {
const url = document.getElementById('u_'+key).value.trim();
const meth = document.getElementById('m_'+key).value;
const fd=new FormData();
fd.append('company_id',cid); fd.append('endpoint_key',key);
fd.append('direction',dir); fd.append('url',url); fd.append('method',meth);
const r=await fetch('/admin/company/endpoint/save',{method:'POST',body:fd});
const j=await r.json();
const box=document.getElementById('ep_resp_'+key);
box.style.display='';
box.innerHTML=j.ok ? '<span style="color:#166534;font-size:12px">✓ Guardado</span>' : '<span style="color:#991b1b;font-size:12px">'+j.error+'</span>';
setTimeout(()=>box.style.display='none', 2000);
}
async function testEp(cid, key) {
const url=document.getElementById('u_'+key).value.trim();
const meth=document.getElementById('m_'+key).value;
const box=document.getElementById('ep_resp_'+key);
box.style.display=''; box.innerHTML='<span style="color:#6b7280;font-size:12px">Probando...</span>';
const fd=new FormData();
fd.append('company_id',cid); fd.append('endpoint_key',key);
fd.append('url',url); fd.append('method',meth);
const r=await fetch('/admin/company/endpoint/test',{method:'POST',body:fd});
const j=await r.json();
if (j.ok) {
const preview=JSON.stringify(j.response,null,2).substring(0,800);
box.innerHTML='<pre style="font-size:11px;max-height:200px;overflow-y:auto;margin:0">'+preview+'</pre>';
} else {
box.innerHTML='<div class="toast toast-error" style="margin:0;font-size:12px">'+j.error+'</div>';
}
}
</script>
HTML;
}
echo '</div>';
echo Layout::close();
}
// ─── POST /admin/company/phone/save ──────────────────────────────────────
public static function companyPhoneSave(): void
{
SessionAuth::require();
header('Content-Type: application/json; charset=utf-8');
$companyId = (int)($_POST['company_id'] ?? 0);
$waNumber = preg_replace('/\D/', '', $_POST['wa_number'] ?? '');
$label = trim($_POST['label'] ?? '');
$permissionType = (int)($_POST['permission_type'] ?? 1);
if ($companyId <= 0 || strlen($waNumber) < 7) {
echo json_encode(['ok' => false, 'error' => 'Datos inválidos']);
exit;
}
if (!in_array($permissionType, [1, 2, 3])) {
echo json_encode(['ok' => false, 'error' => 'Tipo de permiso inválido']);
exit;
}
$company = CompanyRepository::findById($companyId);
if (!$company) {
echo json_encode(['ok' => false, 'error' => 'Empresa no encontrada']);
exit;
}
$erpUsers = (int)($company['erp_active_users'] ?? 0);
$limitT1 = max(1, $erpUsers * 10);
$limitT23 = max(1, $erpUsers);
// Contar números actuales
$counts = db()->prepare("SELECT permission_type, COUNT(*) as cnt FROM company_phones WHERE company_id=? AND is_active=1 GROUP BY permission_type");
$counts->execute([$companyId]);
$cntMap = [];
foreach ($counts->fetchAll(\PDO::FETCH_ASSOC) as $r) $cntMap[(int)$r['permission_type']] = (int)$r['cnt'];
$cntT1 = $cntMap[1] ?? 0;
$cntT23 = ($cntMap[2] ?? 0) + ($cntMap[3] ?? 0);
if ($permissionType === 1 && $cntT1 >= $limitT1) {
echo json_encode(['ok' => false, 'error' => "Límite alcanzado: máximo {$limitT1} números de tipo 1 (usuarios ERP × 10)"]);
exit;
}
if ($permissionType !== 1 && $cntT23 >= $limitT23) {
echo json_encode(['ok' => false, 'error' => "Límite alcanzado: máximo {$limitT23} números de tipo 2/3 (igual a usuarios ERP)"]);
exit;
}
try {
db()->prepare("INSERT INTO company_phones (company_id, wa_number, label, permission_type) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE label=VALUES(label), permission_type=VALUES(permission_type), is_active=1")
->execute([$companyId, $waNumber, $label, $permissionType]);
echo json_encode(['ok' => true, 'message' => "Número +{$waNumber} agregado"]);
} catch (\PDOException $e) {
echo json_encode(['ok' => false, 'error' => 'Error DB: ' . $e->getMessage()]);
}
exit;
}
// ─── POST /admin/company/phone/delete ────────────────────────────────────
public static function companyPhoneDelete(): void
{
SessionAuth::require();
header('Content-Type: application/json; charset=utf-8');
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) { echo json_encode(['ok' => false, 'error' => 'ID inválido']); exit; }
db()->prepare("DELETE FROM company_phones WHERE id=?")->execute([$id]);
echo json_encode(['ok' => true]);
exit;
}
// ─── POST /admin/company/endpoint/save ───────────────────────────────────
public static function companyEndpointSave(): void
{
SessionAuth::require();
header('Content-Type: application/json; charset=utf-8');
$companyId = (int)($_POST['company_id'] ?? 0);
$key = preg_replace('/[^a-z0-9_]/', '', $_POST['endpoint_key'] ?? '');
$direction = in_array($_POST['direction'] ?? '', ['upload','download']) ? $_POST['direction'] : null;
$url = trim($_POST['url'] ?? '');
$method = in_array(strtoupper($_POST['method'] ?? 'GET'), ['GET','POST']) ? strtoupper($_POST['method']) : 'GET';
if ($companyId <= 0 || $key === '' || $direction === null) {
echo json_encode(['ok' => false, 'error' => 'Datos inválidos']);
exit;
}
try {
db()->prepare("INSERT INTO company_endpoints (company_id, endpoint_key, direction, url, method) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE url=VALUES(url), method=VALUES(method)")
->execute([$companyId, $key, $direction, $url, $method]);
echo json_encode(['ok' => true]);
} catch (\PDOException $e) {
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
}
exit;
}
// ─── POST /admin/company/endpoint/test ───────────────────────────────────
public static function companyEndpointTest(): void
{
SessionAuth::require();
header('Content-Type: application/json; charset=utf-8');
$companyId = (int)($_POST['company_id'] ?? 0);
$key = preg_replace('/[^a-z0-9_]/', '', $_POST['endpoint_key'] ?? '');
$url = trim($_POST['url'] ?? '');
$method = in_array(strtoupper($_POST['method'] ?? 'GET'), ['GET','POST']) ? strtoupper($_POST['method']) : 'GET';
if ($url === '') { echo json_encode(['ok' => false, 'error' => 'URL vacía']); exit; }
$company = CompanyRepository::findById($companyId);
$apiKey = $company['api_key'] ?? '';
$ch = curl_init($url);
$opts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json'],
];
if ($apiKey !== '') $opts[CURLOPT_HTTPHEADER][] = 'Authorization: Bearer ' . $apiKey;
if ($method === 'POST') { $opts[CURLOPT_POST] = true; $opts[CURLOPT_POSTFIELDS] = '{}'; }
curl_setopt_array($ch, $opts);
$resp = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($err) { echo json_encode(['ok' => false, 'error' => 'cURL: ' . $err]); exit; }
$decoded = json_decode($resp, true);
$preview = is_array($decoded) ? $decoded : (string)$resp;
// Guardar última respuesta
if ($key !== '') {
db()->prepare("UPDATE company_endpoints SET last_response=?, last_called_at=NOW() WHERE company_id=? AND endpoint_key=?")
->execute([is_string($resp) ? substr($resp, 0, 2000) : '', $companyId, $key]);
}
if ($httpCode >= 200 && $httpCode < 300) {
echo json_encode(['ok' => true, 'http_code' => $httpCode, 'response' => $preview]);
} else {
echo json_encode(['ok' => false, 'error' => "HTTP {$httpCode}", 'response' => $preview]);
}
exit;
}