feat: clonar endpoints desde otra empresa en pestaña Endpoints API

Agrega panel de clonación en la pestaña Endpoints API del editor de empresa.
Permite copiar los endpoints de otra empresa con dos modos: sobreescribir todo
o solo añadir los endpoints que aún no tienen URL configurada.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-04 13:50:39 -05:00
co-authored by Claude Sonnet 4.6
parent 34322ae3dc
commit bfbc70451d
2 changed files with 110 additions and 0 deletions
+109
View File
@@ -1298,6 +1298,34 @@ ROW;
</div>
</div>
EP;
// Prepend clone panel
$otherCompanies = array_filter(CompanyRepository::findAll(), fn($c) => (int)$c['id'] !== $id);
$cloneOpts = '<option value="">— Selecciona empresa origen —</option>';
foreach ($otherCompanies as $oc) {
$cloneOpts .= '<option value="' . (int)$oc['id'] . '">' . self::h($oc['name'] ?? $oc['display_name'] ?? '') . '</option>';
}
$clonePanel = <<<CLONE
<div style="background:#f0f4ff;border:1px solid #c7d7ff;border-radius:10px;padding:14px 16px;margin-bottom:20px">
<div style="font-size:13px;font-weight:600;color:#1e40af;margin-bottom:10px">📋 Clonar endpoints desde otra empresa</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<select id="cloneSrc" style="flex:1;min-width:200px;padding:7px 10px;border:1px solid #c7d7ff;border-radius:6px;font-size:13px">
{$cloneOpts}
</select>
<button class="btn-secondary" onclick="showCloneOptions()">Clonar </button>
</div>
<div id="cloneOptions" style="display:none;margin-top:10px">
<div style="font-size:12px;color:#374151;margin-bottom:8px;font-weight:500">¿Cómo quieres clonar los endpoints existentes en esta empresa?</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn-primary" onclick="doClone('overwrite')" style="font-size:12px">Sobreescribir todo</button>
<button class="btn-secondary" onclick="doClone('add_only')" style="font-size:12px">Solo añadir faltantes</button>
<button onclick="cancelClone()" style="background:none;border:none;font-size:12px;color:#6b7280;cursor:pointer;padding:4px 8px">Cancelar</button>
</div>
</div>
<div id="cloneMsg" style="margin-top:8px"></div>
</div>
CLONE;
$epHtml = $clonePanel . $epHtml;
}
// For "copy from" dropdown on new company form
@@ -1700,6 +1728,38 @@ async function addNewEp(cid) {
msg.innerHTML='<div class="toast toast-error">'+j.error+'</div>';
}
}
function showCloneOptions() {
const src = document.getElementById('cloneSrc').value;
if (!src) { alert('Selecciona una empresa origen'); return; }
document.getElementById('cloneOptions').style.display = 'block';
}
function cancelClone() {
document.getElementById('cloneOptions').style.display = 'none';
}
async function doClone(mode) {
const src = document.getElementById('cloneSrc').value;
if (!src) return;
cancelClone();
const msg = document.getElementById('cloneMsg');
msg.innerHTML = '<span style="font-size:12px;color:#6b7280">Clonando...</span>';
const fd = new FormData();
fd.append('source_company_id', src);
fd.append('target_company_id', {$id});
fd.append('mode', mode);
try {
const r = await fetch('/admin/company/endpoints/clone', {method:'POST', body:fd});
const j = await r.json();
if (j.ok) {
msg.innerHTML = '<div class="toast toast-success" style="margin:0">' + j.message + '</div>';
setTimeout(() => location.reload(), 1200);
} else {
msg.innerHTML = '<div class="toast toast-error" style="margin:0">' + j.error + '</div>';
}
} catch(e) {
msg.innerHTML = '<div class="toast toast-error" style="margin:0">Error de red</div>';
}
}
</script>
HTML;
}
@@ -2033,6 +2093,55 @@ HTML;
exit;
}
// ─── POST /admin/company/endpoints/clone ─────────────────────────────────
public static function companyEndpointsClone(): void
{
SessionAuth::require();
header('Content-Type: application/json; charset=utf-8');
$sourceId = (int)($_POST['source_company_id'] ?? 0);
$targetId = (int)($_POST['target_company_id'] ?? 0);
$mode = ($_POST['mode'] ?? '') === 'add_only' ? 'add_only' : 'overwrite';
if ($sourceId <= 0 || $targetId <= 0 || $sourceId === $targetId) {
echo json_encode(['ok' => false, 'error' => 'IDs inválidos']);
exit;
}
try {
$stmt = db()->prepare("SELECT endpoint_key, direction, url, method FROM company_endpoints WHERE company_id = ?");
$stmt->execute([$sourceId]);
$sourceEps = $stmt->fetchAll(\PDO::FETCH_ASSOC);
if (empty($sourceEps)) {
echo json_encode(['ok' => false, 'error' => 'La empresa origen no tiene endpoints configurados']);
exit;
}
$existingKeys = [];
if ($mode === 'add_only') {
$ex = db()->prepare("SELECT endpoint_key FROM company_endpoints WHERE company_id = ? AND url IS NOT NULL AND url != ''");
$ex->execute([$targetId]);
$existingKeys = array_column($ex->fetchAll(\PDO::FETCH_ASSOC), 'endpoint_key');
}
$upsert = db()->prepare("INSERT INTO company_endpoints (company_id, endpoint_key, direction, url, method) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE url=VALUES(url), method=VALUES(method)");
$count = 0;
foreach ($sourceEps as $ep) {
if ($mode === 'add_only' && in_array($ep['endpoint_key'], $existingKeys, true)) continue;
$upsert->execute([$targetId, $ep['endpoint_key'], $ep['direction'], $ep['url'], $ep['method']]);
$count++;
}
$label = $mode === 'add_only' ? 'añadidos' : 'sobreescritos';
echo json_encode(['ok' => true, 'count' => $count, 'message' => "{$count} endpoint(s) {$label} correctamente"]);
} catch (\PDOException $e) {
echo json_encode(['ok' => false, 'error' => 'Error de base de datos: ' . $e->getMessage()]);
}
exit;
}
// ─── GET /admin/sync-companies ──────────────────────────────────────────
public static function syncCompanies(): void
+1
View File
@@ -282,6 +282,7 @@ $routes = [
['POST', '/admin/company/endpoint/save', fn() => DashboardController::companyEndpointSave()],
['POST', '/admin/company/endpoint/test', fn() => DashboardController::companyEndpointTest()],
['POST', '/admin/company/endpoint/delete', fn() => DashboardController::companyEndpointDelete()],
['POST', '/admin/company/endpoints/clone', fn() => DashboardController::companyEndpointsClone()],
// ─── Admin: listar pendientes de aprobación ─────────────────────────────
['GET', '/admin/pending-list', fn() => DashboardController::pendingList()],