0, 'msgs' => 0, 'media' => 0, 'statuses' => 0];
$pending = 0;
$companies = [];
$logs = [];
$total = 0;
}
$pages = $total > 0 ? (int)ceil($total / self::PER_PAGE) : 1;
$companyCount = count($companies);
self::render(compact('stats', 'pending', 'companies', 'companyCount', 'companyId', 'logs', 'total', 'page', 'pages', 'filter', 'search', 'date', 'user'));
}
// ─── GET /admin/live ────────────────────────────────────────────────────
public static function live(): void
{
SessionAuth::require();
$user = SessionAuth::user();
$userName = htmlspecialchars($user['name'] ?? 'Admin', ENT_QUOTES, 'UTF-8');
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
echo <<
';
if (empty($items)) {
echo '
✅ No hay mensajes pendientes de aprobación.
';
} else {
echo '
| ID | Empresa | De | Mensaje recibido | Respuesta | Estado | Acción |
';
foreach ($items as $item) {
$id = (int)$item['id'];
$company = htmlspecialchars($item['company_name'] ?? '-', ENT_QUOTES, 'UTF-8');
$from = htmlspecialchars($item['from_phone'] ?? '-', ENT_QUOTES, 'UTF-8');
$inMsg = htmlspecialchars(mb_substr($item['incoming_message'] ?? '', 0, 60), ENT_QUOTES, 'UTF-8');
$reply = htmlspecialchars(mb_substr($item['reply_body'] ?? '', 0, 60), ENT_QUOTES, 'UTF-8');
$status = $item['status'] ?? 'pending';
echo "
{$id} |
{$company} |
{$from} |
{$inMsg} |
{$reply} |
{$status} |
|
";
}
echo '
';
}
echo <<
HTML;
exit;
}
// ─── GET /admin/companies ──────────────────────────────────────────────
public static function companies(): void
{
SessionAuth::require();
$user = SessionAuth::user();
$userName = self::h($user['name'] ?? 'Admin');
$companies = CompanyRepository::findAll(true);
$companyCount = count($companies);
$msg = $_GET['msg'] ?? '';
$toastHtml = '';
if ($msg !== '') {
$map = [
'created' => 'Empresa creada exitosamente.',
'updated' => 'Empresa actualizada exitosamente.',
'deleted' => 'Empresa eliminada.',
'error' => 'Ocurrió un error. Intente de nuevo.',
];
$text = self::h($map[$msg] ?? '');
$cls = in_array($msg, ['created', 'updated']) ? 'toast-success' : 'toast-error';
if ($text !== '') {
$toastHtml = '
' . $text . '
';
}
}
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
echo <<
Empresas — Palmas360
Empresas
{$toastHtml}
HTML;
if (empty($companies)) {
echo '
';
} else {
echo '
| ID | Nombre | WhatsApp Phone ID | Teléfono | Bot Type | Aprueba | Activo | API URL | Acciones |
';
foreach ($companies as $c) {
$id = (int)$c['id'];
$name = self::h($c['name'] ?? '');
$dname = self::h($c['display_name'] ?? '');
$pid = self::h((string)($c['phone_number_id'] ?? ''));
$phone = self::h($c['display_phone'] ?? '');
$bot = $c['bot_type'] ?? 'normal';
$apr = !empty($c['requires_approval']);
$act = !empty($c['is_active']);
$url = self::h($c['api_base_url'] ?? '');
$btag = match($bot) { 'hybrid' => 'hybrid', 'ai' => 'ai', default => 'normal' };
$eName = self::h($c['name'] ?? '');
echo "
{$id} |
{$name} {$dname} |
{$pid} |
{$phone} |
{$bot} |
' . ($apr ? 'Sí' : 'No') . " |
' . ($act ? 'Sí' : 'No') . " |
{$url} |
✎ Editar
🗑 Eliminar
|
";
}
echo '
';
}
echo <<
Total: {$companyCount} empresa(s)
HTML;
exit;
}
// ─── GET /admin/company/edit ─────────────────────────────────────────────
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;
$cv = fn(string $k, string $d = '') => self::h($company[$k] ?? $d);
$name = $cv('name');
$display_name = $cv('display_name');
$phone_number_id = $cv('phone_number_id');
$display_phone = $cv('display_phone');
$api_base_url = $cv('api_base_url');
$api_key = $cv('api_key');
$config_json = $cv('config_json');
$bot_type = $company['bot_type'] ?? 'normal';
$requires_approval = !empty($company['requires_approval']);
$is_active = !empty($company['is_active']);
$reqAprChecked = $requires_approval ? 'checked' : '';
$isActChecked = $is_active ? 'checked' : '';
$botOptions = '';
foreach (['normal' => 'Normal', 'ai' => 'AI', 'hybrid' => 'Híbrido'] as $val => $label) {
$sel = $bot_type === $val ? 'selected' : '';
$botOptions .= "
\n";
}
$pageTitle = $isEdit ? 'Editar Empresa' : 'Nueva Empresa';
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
echo <<
{$pageTitle} — Palmas360
{$pageTitle}
HTML;
exit;
}
// ─── GET /admin/sync-companies ──────────────────────────────────────────
public static function syncCompanies(): void
{
SessionAuth::require();
$user = SessionAuth::user();
$userName = self::h($user['name'] ?? 'Admin');
$result = ErpSync::sync();
$hasError = isset($result['error']);
$icon = $hasError ? '❌' : '✅';
$title = $hasError ? 'Error en Sincronización' : 'Sincronización Exitosa';
$titleClass = $hasError ? 'error' : 'success';
$resultJson = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
echo <<
Sincronizar — Palmas360
Sincronizar Empresas
{$icon}
{$title}
{$resultJson}
HTML;
exit;
}
// ─── GET /admin/process-queue ───────────────────────────────────────────
public static function processQueue(): void
{
SessionAuth::require();
$user = SessionAuth::user();
$userName = self::h($user['name'] ?? 'Admin');
$result = OutboundWorker::processQueue();
$hasError = !empty($result['errors']);
$icon = $hasError ? '⚠' : '✅';
$title = $hasError ? 'Cola Procesada con Advertencias' : 'Cola Procesada Exitosamente';
$titleClass = $hasError ? 'error' : 'success';
$resultJson = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
echo <<
Procesar Cola — Palmas360
Procesar Cola de Mensajes
{$icon}
{$title}
{$resultJson}
HTML;
exit;
}
// ─── GET /admin/settings ────────────────────────────────────────────────
public static function settings(): void
{
SessionAuth::require();
$user = SessionAuth::user();
$userName = self::h($user['name'] ?? 'Admin');
$settings = Settings::all();
$msg = $_GET['msg'] ?? '';
$toastHtml = '';
if ($msg === 'saved') {
$toastHtml = '
Configuración guardada exitosamente.
';
}
$fields = [
'WhatsApp Cloud API' => [
['key' => 'whatsapp_access_token', 'label' => 'Access Token', 'type' => 'password', 'placeholder' => 'EAAMzB...'],
['key' => 'whatsapp_app_secret', 'label' => 'App Secret', 'type' => 'password', 'placeholder' => 'a4f82c1d...'],
['key' => 'whatsapp_verify_token', 'label' => 'Verify Token', 'type' => 'text', 'placeholder' => 'PLM360_WH_...'],
['key' => 'whatsapp_business_account_id', 'label' => 'Business Account ID', 'type' => 'text', 'placeholder' => '920641783052817'],
['key' => 'whatsapp_default_phone_number_id', 'label' => 'Phone Number ID (default)', 'type' => 'text', 'placeholder' => '384710295638401'],
],
'Inteligencia Artificial' => [
['key' => 'ai_provider', 'label' => 'Proveedor', 'type' => 'select', 'options' => ['mock' => 'Mock (simulado)', 'openai' => 'OpenAI']],
['key' => 'openai_api_key', 'label' => 'OpenAI API Key', 'type' => 'password', 'placeholder' => 'sk-...'],
['key' => 'openai_model', 'label' => 'Modelo', 'type' => 'select', 'options' => ['gpt-4o-mini' => 'GPT-4o Mini', 'gpt-4o' => 'GPT-4o', 'gpt-3.5-turbo' => 'GPT-3.5 Turbo']],
['key' => 'ai_max_tokens', 'label' => 'Máximo de tokens', 'type' => 'number', 'placeholder' => '500'],
['key' => 'ai_default_prompt', 'label' => 'System Prompt por defecto', 'type' => 'textarea', 'placeholder' => 'Eres un asistente...'],
],
];
$versionHash = substr(sha1_file(__DIR__ . '/../.env'), 0, 8);
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
echo <<
Configuración — Palmas360
Configuración
Los cambios se aplican inmediatamente. Los valores se almacenan en la base de datos.
HTML;
exit;
}
// ─── Chat ────────────────────────────────────────────────────────────────
public static function chat(): void
{
SessionAuth::require();
$user = SessionAuth::user();
$userName = self::h($user['name'] ?? 'Admin');
$companies = CompanyRepository::findAll();
http_response_code(200);
header('Content-Type: text/html; charset=utf-8');
echo <<