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:
co-authored by
Claude Sonnet 4.6
parent
b083d49769
commit
860e93a20b
+464
-82
@@ -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érico del número de teléfono de WhatsApp Business</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="display_phone">Teléfono mostrado</label>
|
||||
<input type="text" id="display_phone" name="display_phone" value="{$display_phone}" placeholder="Ej: +521234567890">
|
||||
<div class="hint">Número telefó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ón">
|
||||
<div class="hint">Clave API para autenticación ERP ↔ 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í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ó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ón JSON</label>
|
||||
<textarea id="config_json" name="config_json" placeholder='{"key": "value", ...}'>{$config_json}</textarea>
|
||||
<div class="hint">Configuració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">💾 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úmero WA Business API (para enviar mensajes)</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Telé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ímites de números WA. Se actualiza con sincronizació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ó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ó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;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,11 @@ class Layout
|
||||
return $icons[$name] ?? '';
|
||||
}
|
||||
|
||||
public static function close(): string
|
||||
{
|
||||
return '</div></body></html>';
|
||||
}
|
||||
|
||||
public static function open(
|
||||
string $title,
|
||||
string $active,
|
||||
|
||||
@@ -265,6 +265,14 @@ $routes = [
|
||||
['GET', '/admin/test-message', fn() => DashboardController::testMessage()],
|
||||
['POST', '/admin/test-message/send', fn() => DashboardController::testMessageSend()],
|
||||
|
||||
// ─── Números WhatsApp por empresa ──────────────────────────────────────
|
||||
['POST', '/admin/company/phone/save', fn() => DashboardController::companyPhoneSave()],
|
||||
['POST', '/admin/company/phone/delete', fn() => DashboardController::companyPhoneDelete()],
|
||||
|
||||
// ─── Endpoints API por empresa ─────────────────────────────────────────
|
||||
['POST', '/admin/company/endpoint/save', fn() => DashboardController::companyEndpointSave()],
|
||||
['POST', '/admin/company/endpoint/test', fn() => DashboardController::companyEndpointTest()],
|
||||
|
||||
// ─── Admin: listar pendientes de aprobación ─────────────────────────────
|
||||
['GET', '/admin/pending-list', fn() => DashboardController::pendingList()],
|
||||
['GET', '/admin/pending', fn() => DashboardController::pending()],
|
||||
|
||||
@@ -207,6 +207,46 @@ foreach ($seedSettings as $k => $v) {
|
||||
$insertStmt->execute([$k, $v]);
|
||||
}
|
||||
|
||||
// ─── Números WhatsApp por empresa ─────────────────────────────────────────────
|
||||
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS company_phones (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
company_id INT NOT NULL,
|
||||
wa_number VARCHAR(25) NOT NULL,
|
||||
label VARCHAR(100) DEFAULT '',
|
||||
permission_type TINYINT NOT NULL DEFAULT 1 COMMENT '1=solo reporta, 2=solo recibe, 3=ambos',
|
||||
is_active TINYINT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_cp (company_id, wa_number),
|
||||
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
|
||||
// ─── Endpoints API configurables por empresa ───────────────────────────────────
|
||||
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS company_endpoints (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
company_id INT NOT NULL,
|
||||
endpoint_key VARCHAR(50) NOT NULL,
|
||||
direction ENUM('upload','download') NOT NULL,
|
||||
url VARCHAR(500) DEFAULT '',
|
||||
method VARCHAR(10) DEFAULT 'GET',
|
||||
last_response TEXT,
|
||||
last_called_at DATETIME,
|
||||
is_active TINYINT DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_ce (company_id, endpoint_key),
|
||||
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
|
||||
// ─── Campo erp_active_users en companies ──────────────────────────────────────
|
||||
try {
|
||||
$db->exec("ALTER TABLE companies ADD COLUMN erp_active_users INT DEFAULT 0 AFTER is_active");
|
||||
} catch (\PDOException $e) {}
|
||||
|
||||
// ─── Usuario admin por defecto ────────────────────────────────────────────────
|
||||
|
||||
$email = 'admin@palmas360.com';
|
||||
|
||||
Reference in New Issue
Block a user