diff --git a/admin/DashboardController.php b/admin/DashboardController.php
index 5916b42..97bbe90 100644
--- a/admin/DashboardController.php
+++ b/admin/DashboardController.php
@@ -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 .= "\n";
+ $botOptions .= "\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 <<
- .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%}}
-
-
-
-
-
-
+
+
+ | Número WA | Etiqueta | Permiso | Estado | |
+ {$rows}
+
+
+
+
Agregar número
+
-
-
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 = "
{$badge} {$dirLabel}
";
+ foreach ($catalog[$dir] as $key => $label) {
+ $saved = $epMap[$key] ?? [];
+ $url = self::h($saved['url'] ?? '');
+ $meth = $saved['method'] ?? 'GET';
+ $lastAt = $saved['last_called_at'] ? '
' . self::h($saved['last_called_at']) . '' : '';
+ $mSel = fn($v) => $meth === $v ? 'selected' : '';
+ $html .= <<
+ {$label} {$lastAt}
+
+
+
+
+
+
+
+
+EP;
+ }
+ return $html;
+ };
+
+ $epHtml = $renderEpSection('upload', 'Subir información (WhatsApp → ERP)', '
↑ Upload')
+ . '
'
+ . $renderEpSection('download', 'Bajar información (ERP → WhatsApp)', '
↓ Download');
+ }
+
+ $tabsHtml = $isEdit ? <<
+
+
+
+
+HTML : '';
+
+ echo <<
+.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}
+
+
+
+ {$tabsHtml}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+HTML;
+
+ if ($isEdit) {
+ echo <<
+
+ {$phonesHtml}
+
+
+
+
+ {$epHtml}
+
+
+
+HTML;
+ }
+
+ echo '';
+ 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;
}
diff --git a/admin/Layout.php b/admin/Layout.php
index 6a22f02..3acbaa2 100644
--- a/admin/Layout.php
+++ b/admin/Layout.php
@@ -26,6 +26,11 @@ class Layout
return $icons[$name] ?? '';
}
+ public static function close(): string
+ {
+ return '';
+ }
+
public static function open(
string $title,
string $active,
diff --git a/public/index.php b/public/index.php
index afeaf81..cd6137d 100644
--- a/public/index.php
+++ b/public/index.php
@@ -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()],
diff --git a/setup/migrate.php b/setup/migrate.php
index 7c8c5e9..3c1cb1c 100644
--- a/setup/migrate.php
+++ b/setup/migrate.php
@@ -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';