src/Controller/AdminController.php line 35

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. /*
  4.  * To change this license header, choose License Headers in Project Properties.
  5.  * To change this template file, choose Tools | Templates
  6.  * and open the template in the editor.
  7.  */
  8. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  9. use Symfony\Component\HttpFoundation\Session\Session;
  10. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  11. use Symfony\Component\HttpFoundation\JsonResponse;
  12. use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
  13. use Symfony\Component\HttpFoundation\Request;
  14. use Doctrine\ORM\EntityManagerInterface;
  15. use App\Entity\Empresa;
  16. use App\Entity\Usuario;
  17. use App\Entity\ConexionBD;
  18. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  19. use Symfony\Component\HttpFoundation\File\UploadedFile;
  20. /**
  21.  * Description of AdminController
  22.  *
  23.  * @author joseangelparra
  24.  */
  25. class AdminController extends AbstractController{
  26.     //put your code here
  27.     private $params;
  28.     public function __construct(ParameterBagInterface $params){
  29.          
  30.          $this->session = new Session();
  31.          $this->params $params;
  32.     }
  33.     
  34.     #[Route('/'name'login')]
  35.     public function Login(AuthenticationUtils $authenticationUtils){
  36.          $error $authenticationUtils->getLastAuthenticationError();
  37.         
  38.         $lastUsername $authenticationUtils->getLastUSername();
  39.         
  40.         
  41.         return $this->render('base.html.twig',array(
  42.             'error'=>$error,
  43.             'last_username'=>$lastUsername
  44.         ));
  45.     }
  46.     
  47.     public function changePWD(UserPasswordHasherInterface $encoderEntityManagerInterface $em){
  48.         $user $em->getRepository(Usuario::class)->find(10);
  49.         $encoded $encoder->hashPassword($user,'docuManager2025');
  50.         $user->setPassword($encoded);
  51.         $em->persist($user);
  52.         $flush=$em->flush();
  53.         die();
  54.     }
  55.     
  56.     public function checkUserExists(Request $requestEntityManagerInterface $em){
  57.         $email $request->request->get("user");
  58.         
  59.         $exists $em->getRepository(Usuario::class)->findBy(array("email"=>$email));
  60.         
  61.         if($exists){
  62.             return new JsonResponse(array("exists"=>true));
  63.         }else{
  64.             return new JsonResponse(array("exists"=>false));
  65.         }
  66.     }   
  67.     
  68.     private function azurePortForTenant(int $tenantIdint $base 12000int $max 20999): int {
  69.         $port $base $tenantId;
  70.         if ($port $max) {
  71.             $range max(1$max $base);
  72.             $port  $base + ($tenantId $range);
  73.         }
  74.         return $port;
  75.     }
  76.     
  77.     #[Route('/list'name'list')]
  78.     public function List( EntityManagerInterface $entityManager){
  79.          if (!$this->getUser() || !is_object($this->getUser())) {         
  80.             return $this->redirectToRoute('logout');
  81.         }
  82.         $empresas $entityManager->getRepository(Empresa::class)->findAll();
  83.          
  84.          return $this->render('listusers.html.twig',array(
  85.              'empresas' => $empresas,
  86.          ));
  87.     }
  88.     
  89.     public function addEmpresa(Request $reqEntityManagerInterface $em)
  90.     {
  91.         //dd(\shell_exec('whoami'));
  92.          if (!$this->getUser() || !is_object($this->getUser())) {         
  93.             return $this->redirectToRoute('logout');
  94.         }
  95.         if ($req->request->get("submit") != "") {
  96.             $data $req->request->all();
  97.             // Crear nombre de base de datos y credenciales aleatorios
  98.             $dbName 'doc_' bin2hex(random_bytes(3));
  99.             $dbUser 'doc_' bin2hex(random_bytes(2));
  100.             $dbPass bin2hex(random_bytes(8));
  101.             $dbHost 'localhost';
  102.             $dbPort '3306';
  103.             // Credenciales de HestiaCP desde variables de entorno
  104.             $hestiaApiUrl $_ENV['HESTIA_API_URL'];
  105.             $hestiaApiUser $_ENV['HESTIA_API_USER'];
  106.             $hestiaApiPass $_ENV['HESTIA_API_PASS'];
  107.             $hestiaOwner   $_ENV['HESTIA_OWNER'];
  108.             $accessKeyId   $_ENV['HESTIA_ACCESS_KEY_ID'] ?? '';
  109.             $secretKey     $_ENV['HESTIA_SECRET_KEY'] ?? '';
  110.             // Variables para el script de bash
  111.             $ocrBinary  $_ENV['OCR_BINARY'];
  112.             $filesPath  $_ENV['FILES_PATH'];                    
  113.             $owner $hestiaOwner// o el dueño del hosting
  114.             $postFields http_build_query([
  115.                 'user' => $hestiaApiUser,
  116.                 'password' => $hestiaApiPass,
  117.                 'returncode' => 'yes',
  118.                 'cmd' => 'v-add-database',
  119.                 'arg1' => $owner,
  120.                 'arg2' => $dbName,
  121.                 'arg3' => $dbUser,
  122.                 'arg4' => $dbPass,
  123.                 'arg5' => 'mysql'
  124.             ]);
  125.             //dd($postFields);            
  126.             $headers = [
  127.                 'Authorization: Bearer ' $accessKeyId ':' $secretKey
  128.             ];
  129.             $ch curl_init();
  130.             curl_setopt($chCURLOPT_URL$hestiaApiUrl);
  131.             curl_setopt($chCURLOPT_HTTPHEADER$headers);
  132.             curl_setopt($chCURLOPT_RETURNTRANSFERtrue);
  133.             curl_setopt($chCURLOPT_SSL_VERIFYHOSTfalse);
  134.             curl_setopt($chCURLOPT_POSTtrue);
  135.             curl_setopt($chCURLOPT_POSTFIELDS$postFields);
  136.             curl_setopt($chCURLOPT_SSL_VERIFYPEERfalse); // Solo si usas certificados autofirmados
  137.             
  138.             $response curl_exec($ch);
  139.             $error curl_error($ch);
  140.             curl_close($ch);
  141.            
  142.             if ($error || trim($response) !== '0') {
  143.                 $this->addFlash('danger''Error al crear la base de datos en HestiaCP: ' . ($error ?: $response));
  144.                 return $this->redirectToRoute('app_empresa_new');
  145.             }
  146.             
  147.             //Añadir sql
  148.             $sqlFile __DIR__ '/../../db/db_base.sql'// Ajusta la ruta si está en otro sitio
  149.             if (!file_exists($sqlFile)) {
  150.                 $this->addFlash('danger''Archivo db_base.sql no encontrado.');
  151.                 return $this->redirectToRoute('app_empresa_new');
  152.             }
  153.            
  154.             $mysqli = new \mysqli($dbHost"{$owner}_{$dbUser}"$dbPass"{$owner}_{$dbName}", (int)$dbPort);
  155.             if ($mysqli->connect_error) {
  156.                 $this->addFlash('danger''Error al conectar a la base de datos: ' $mysqli->connect_error);
  157.                 return $this->redirectToRoute('app_empresa_new');
  158.             }
  159.             $sql file_get_contents($sqlFile);
  160.             // Eliminar líneas con DELIMITER
  161.             $sql preg_replace('/DELIMITER\s+\$\$/'''$sql);
  162.             $sql preg_replace('/DELIMITER\s+;/'''$sql);
  163.             // Separar por ';;' si los triggers usan ese delimitador (ajusta si es $$)
  164.             $statements explode('$$'$sql);
  165.             foreach ($statements as $stmt) {
  166.                 $stmt trim($stmt);
  167.                 if ($stmt) {
  168.                     if (!$mysqli->multi_query($stmt)) {
  169.                         $this->addFlash('danger''Error ejecutando SQL: ' $mysqli->error);
  170.                         return $this->redirectToRoute('app_empresa_new');
  171.                     }
  172.                     // Limpiar cualquier resultado intermedio
  173.                     while ($mysqli->more_results() && $mysqli->next_result()) {
  174.                         $mysqli->use_result();
  175.                     }
  176.                 }
  177.             }
  178.             
  179.             $updateSql "UPDATE users SET email = '".$data["user"]."' WHERE id = 1";
  180.             if (!$mysqli->query($updateSql)) {
  181.                 $this->addFlash('danger''Error al actualizar usuario: ' $mysqli->error);
  182.                 return $this->redirectToRoute('app_empresa_new');
  183.             }
  184.             // Guardar parámetros (activeUsers + modulos_*) en la BD del cliente
  185.             $this->saveEmpresaParametros($mysqli$data);
  186.             // SUBIR LOGO PERSONALIZADO (SI LO HAY) ===
  187.             $customLogoFile null;
  188.             try {
  189.                 $customLogoFile $this->uploadEmpresaLogo($req);
  190.             } catch (\Throwable $e) {
  191.                 // Aquí decides si quieres que esto sea fatal o solo un aviso
  192.                 $this->addFlash('warning''El logo personalizado no se pudo subir: ' $e->getMessage());
  193.                 // Si quieres abortar todo el proceso por fallo de logo, haz return+redirect aquí.
  194.             }
  195.             // Guardar logo en la BD del cliente
  196.             $this->saveEmpresaLogo($mysqli$data$customLogoFile);
  197.             // Actualizar/insertar license (capacityGb y users) en la BD del cliente
  198.             $this->upsertLicense(
  199.                 $mysqli,
  200.                 $data,
  201.                 isset($data['maxDiskQuota']) && $data['maxDiskQuota'] !== '' ? (int)$data['maxDiskQuota'] : 200,
  202.                 isset($data['maxActiveUsers']) && $data['maxActiveUsers'] !== '' ? (int)$data['maxActiveUsers'] : 3
  203.             );
  204.             // Obtener modelo seleccionado del formulario (0 si placeholder)
  205.             $modelId = isset($data['extraction_model']) && $data['extraction_model'] !== '' ? (int)$data['extraction_model'] : 0;
  206.             if ((int)($data['modulo_extraccion'] ?? 0) === && $modelId 0) {
  207.                 try {
  208.                     $this->syncExtractionModelToClientDb($mysqli$em$modelId);
  209.                 } catch (\Throwable $e) {
  210.                     // log opcional
  211.                 }
  212.             }
  213.            
  214.             // Crear y persistir la empresa
  215.             $emp = new Empresa();
  216.             $emp->setName($data["name"]);
  217.             $emp->setMaxDiskQuota((int)$data["maxDiskQuota"]);
  218.             
  219.             
  220.             $em->persist($emp);
  221.             $em->flush();
  222.             
  223.             $conexionBD = new ConexionBD();
  224.             $conexionBD->setDbName($owner."_".$dbName);
  225.             $conexionBD->setDbUser($owner."_".$dbUser);
  226.             $conexionBD->setDbPassword($dbPass);
  227.             $conexionBD->setDbUrl($dbHost);
  228.             $conexionBD->setDbPort($dbPort);
  229.             $em->persist($conexionBD);
  230.             $em->flush();
  231.             
  232.             
  233.             $emp->setConexionBD($conexionBD);
  234.             $em->persist($emp);
  235.             $em->flush();
  236.             
  237.             //crear usuario
  238.             $user = new \App\Entity\Usuario();
  239.             $user->setEmail($data["user"]);
  240.             $user->setEmpresa($emp);
  241.             $user->setPassword("dscsdcsno2234dwvw");
  242.             $user->setStatus(1);
  243.             $user->setIsAdmin(2);
  244.             $user->setConnection($conexionBD->getId());
  245.             
  246.             $em->persist($user);
  247.             $em->flush();
  248.             
  249.             //crear el script de bash
  250.             $company_name $emp->getId();
  251.             // "DOCU_MAX_THREADS=" por defecto 4
  252.             // "NO" al final es para desactivar FTP
  253.             // Crear archivo .service
  254.             $serviceContent "<<<EOT
  255.             [Unit]
  256.             Description=".$data["name"]." DocuManager OCR
  257.             Requires=mariadb.service
  258.             After=mariadb.service
  259.             [Service]
  260.             Type=simple            
  261.             ExecStart=$ocrBinary localhost/".$owner."_".$dbName." ".$owner."_".$dbUser." ".$dbPass.$filesPath/".$company_name."
  262.             Restart=always
  263.             User=root
  264.             [Install]
  265.             WantedBy=multi-user.target
  266.             EOT";
  267.            
  268.             
  269.             // Guardar contenido temporal en un archivo dentro de /tmp
  270.             $serviceName $company_name."-documanager.service";
  271.             $tmpServicePath "/tmp/$serviceName";
  272.             file_put_contents($tmpServicePath$serviceContent);
  273.             \chmod($tmpServicePath0644);
  274.             
  275.             // Mover el archivo y habilitar el servicio desde PHP con shell_exec
  276.             $commands = [
  277.                 "sudo /bin/mv /tmp/$serviceName /etc/systemd/system/$serviceName",
  278.                 "sudo /bin/systemctl daemon-reload",
  279.                 "sudo /bin/systemctl enable $serviceName",
  280.                 "sudo /bin/systemctl start $serviceName",
  281.             ];
  282.             $errors = [];
  283.             foreach ($commands as $cmd) {
  284.                 $output \shell_exec($cmd " 2>&1");
  285.                 if ($output !== null) {
  286.                     // Puedes loguearlo si quieres para ver errores
  287.                     error_log("CMD OUTPUT: $cmd\n$output");
  288.                     $errors[] = "CMD OUTPUT: $cmd\n$output";
  289.                 }
  290.             }
  291.             // === Crear servicio AZURE DI por tenant ===
  292.             $azureBasePort 12000;
  293.             $tenantId      = (int)$emp->getId();
  294.             $port          $this->azurePortForTenant($tenantId$azureBasePort20999);
  295.             $serviceName   $tenantId "-azuredi.service";
  296.             $workdir       "/home/docunecta/web/platform.documanager.es/public_html/newdocu/services/azure-di";
  297.             $logsDir       $workdir "/logs/" $tenantId;
  298.             // Asegura carpeta de logs
  299.             @mkdir($logsDir0775true);
  300.             $serviceContent = <<<EOT
  301.             [Unit]
  302.             Description=DocuManager Azure DI (tenant {$tenantId})
  303.             After=network.target
  304.             [Service]
  305.             User=root
  306.             WorkingDirectory={$workdir}
  307.             Environment=APP_HOST=127.0.0.1
  308.             Environment=APP_PORT={$port}
  309.             Environment=LOG_DIR={$logsDir}
  310.             Environment=PYTHONUNBUFFERED=1
  311.             Environment=MAX_CONCURRENT=20
  312.             Environment="PATH=/opt/azure-di/.venv/bin:/usr/local/bin:/usr/bin"
  313.             EnvironmentFile=-{$workdir}/.env
  314.             ExecStart=/opt/azure-di/.venv/bin/python -m uvicorn app.main:app --host 127.0.0.1 --port {$port} --proxy-headers --workers 2
  315.             Restart=always
  316.             RestartSec=2
  317.             StandardOutput=journal
  318.             StandardError=journal
  319.             [Install]
  320.             WantedBy=multi-user.target
  321.             EOT;
  322.             $tmpServicePath "/tmp/{$serviceName}";
  323.             file_put_contents($tmpServicePath$serviceContent);
  324.             @chmod($tmpServicePath0644);
  325.             $cmds = [
  326.                 "sudo /bin/mv {$tmpServicePath} /etc/systemd/system/{$serviceName}",
  327.                 "sudo /bin/systemctl daemon-reload",
  328.                 "sudo /bin/systemctl enable {$serviceName}",
  329.                 "sudo /bin/systemctl start {$serviceName}",
  330.             ];
  331.             foreach ($cmds as $cmd) {
  332.                 $out \shell_exec($cmd " 2>&1");
  333.                 if ($out !== null) { error_log("AZURE-DI CMD: $cmd\n$out"); }
  334.             }
  335.             
  336.             if(count($errors)>0){
  337.                  $this->addFlash('success''Empresa y base de datos creadas correctamente: '.implode(" | ",$errors));
  338.             }
  339.             
  340.             
  341.             
  342.             $this->addFlash('success''Empresa y base de datos creadas correctamente.');
  343.             return $this->redirectToRoute('list');
  344.         } else {
  345.             // Lee los modelos usando la conexión principal de Doctrine
  346.             $extractionModels = [];
  347.             try {
  348.                 $conn $em->getConnection();
  349.                 $sql 'SELECT id, model_id FROM extraction_models ORDER BY model_id';
  350.                 $extractionModels $conn->executeQuery($sql)->fetchAllAssociative(); // ['id' => '1', 'model_name' => '...']
  351.                 // Opcional: castear id a int
  352.                 foreach ($extractionModels as &$m) { $m['id'] = (int)$m['id']; }
  353.             } catch (\Throwable $e) {
  354.                 $extractionModels = [];
  355.             }
  356.             return $this->render('empresa/_add.html.twig', [
  357.                 'extraction_models' => $extractionModels,
  358.                 'modulos' => [
  359.                     'extraction_model' => 0,
  360.                     'limite_archivos' => 500,
  361.                 ],
  362.             ]);
  363.         }
  364.     }    
  365.     private function saveEmpresaParametros(\mysqli $mysqli, array $data): void
  366.     {
  367.         // Helpers
  368.         $getInt  = fn(array $astring $kint $d) => (isset($a[$k]) && $a[$k] !== '') ? (int)$a[$k] : $d;
  369.         $getFlag = fn(array $astring $k) => (isset($a[$k]) && (int)$a[$k] === 1) ? 0;
  370.         // Keys y valores tal y como quieres guardarlos
  371.         $paramMap = [
  372.             'activeUsers'        => $getInt($data'maxActiveUsers'3),
  373.             'modulo_etiquetas'   => $getFlag($data'modulo_etiquetas'),
  374.             'modulo_calendario'  => $getFlag($data'modulo_calendario'),
  375.             'modulo_calExt'      => $getFlag($data'modulo_calendarioExterno'),            
  376.             'modulo_estados'     => $getFlag($data'modulo_estados'),
  377.             'modulo_subida'      => $getFlag($data'modulo_subida'),
  378.             'modulo_extraccion'  => $getFlag($data'modulo_extraccion'),
  379.             'modulo_lineas'      => $getFlag($data'modulo_lineas'),
  380.             'limite_archivos'  => $getInt($data'limite_archivos'500),
  381.             'extraction_model'  => $getInt($data'extraction_model'0),
  382.         ];
  383.         // RECOMENDADO en tu SQL base:
  384.         // ALTER TABLE parametros ADD UNIQUE KEY uniq_nombre (nombre);
  385.         $mysqli->begin_transaction();
  386.         try {
  387.             $stmt $mysqli->prepare("
  388.                 INSERT INTO parametros (nombre, valor)
  389.                 VALUES (?, ?)
  390.                 ON DUPLICATE KEY UPDATE valor = VALUES(valor)
  391.             ");
  392.             if (!$stmt) {
  393.                 throw new \RuntimeException('Prepare parametros: ' $mysqli->error);
  394.             }
  395.             foreach ($paramMap as $nombre => $valor) {
  396.                 // valor es TEXT en tu esquema: bindeamos como string
  397.                 $v = (string)$valor;
  398.                 $stmt->bind_param('ss'$nombre$v);
  399.                 if (!$stmt->execute()) {
  400.                     throw new \RuntimeException("Guardar parámetro $nombre: " $stmt->error);
  401.                 }
  402.             }
  403.             $stmt->close();
  404.             $mysqli->commit();
  405.         } catch (\Throwable $e) {
  406.             $mysqli->rollback();
  407.             // Si NO puedes añadir UNIQUE(nombre), usa fallback DELETE+INSERT:
  408.             // $this->saveParametrosFallback($mysqli, $paramMap);
  409.             throw $e;
  410.         }
  411.     }
  412.     private function saveEmpresaLogo(\mysqli $mysqli, array $data, ?string $customLogoFile null): void
  413.     {
  414.         $this->logLogo('empresa_logo_save.log''--- NUEVA LLAMADA saveEmpresaLogo ---');
  415.         $this->logLogo('empresa_logo_save.log''customLogoFile = '.var_export($customLogoFiletrue));
  416.         $this->logLogo('empresa_logo_save.log''data[empresa_vendor] = '.var_export($data['empresa_vendor'] ?? nulltrue));
  417.         // 1) Decidir qué logo vamos a guardar
  418.         $logoFile null;
  419.         // --- PRIORIDAD: LOGO PERSONALIZADO ---
  420.         if ($customLogoFile !== null && $customLogoFile !== '') {
  421.             $logoFile $customLogoFile;
  422.             $this->logLogo('empresa_logo_save.log''Usando logo personalizado: '.$logoFile);
  423.         } else {
  424.             // --- SI NO HAY PERSONALIZADO, USAMOS EL SELECT DE EMPRESA ---
  425.             if (!isset($data['empresa_vendor']) || $data['empresa_vendor'] === '') {
  426.                 $this->logLogo('empresa_logo_save.log''No hay empresa_vendor y no hay logo custom. No hago nada.');
  427.                 return;
  428.             }
  429.             $empresa $data['empresa_vendor'];
  430.             $logoMap = [
  431.                 'docunecta'  => 'DocuManager_transparente.png',
  432.                 'docuindexa' => 'DocuIndexa.png',
  433.             ];
  434.             if (!isset($logoMap[$empresa])) {
  435.                 $this->logLogo('empresa_logo_save.log'"empresa_vendor $empresa no está en logoMap. No hago nada.");
  436.                 return;
  437.             }
  438.             $logoFile $logoMap[$empresa];
  439.             $this->logLogo('empresa_logo_save.log''Usando logo por vendor: '.$logoFile);
  440.         }
  441.         if ($logoFile === null || $logoFile === '') {
  442.             $this->logLogo('empresa_logo_save.log''logoFile está vacío. No hago nada.');
  443.             return;
  444.         }
  445.         $this->logLogo('empresa_logo_save.log''Voy a guardar en parametros.logo: '.$logoFile);
  446.         $mysqli->begin_transaction();
  447.         try {
  448.             $stmt $mysqli->prepare("
  449.                 INSERT INTO parametros (nombre, valor)
  450.                 VALUES ('logo', ?)
  451.                 ON DUPLICATE KEY UPDATE valor = VALUES(valor)
  452.             ");
  453.             if (!$stmt) {
  454.                 $this->logLogo('empresa_logo_save.log''Error prepare: '.$mysqli->error);
  455.                 throw new \RuntimeException('Prepare logo: ' $mysqli->error);
  456.             }
  457.             $stmt->bind_param('s'$logoFile);
  458.             if (!$stmt->execute()) {
  459.                 $this->logLogo('empresa_logo_save.log''Error execute: '.$stmt->error);
  460.                 throw new \RuntimeException('Guardar parámetro logo: ' $stmt->error);
  461.             }
  462.             $stmt->close();
  463.             $mysqli->commit();
  464.             $this->logLogo('empresa_logo_save.log''Logo guardado correctamente en BD.');
  465.         } catch (\Throwable $e) {
  466.             $mysqli->rollback();
  467.             $this->logLogo('empresa_logo_save.log''EXCEPCIÓN: '.$e->getMessage());
  468.             throw $e;
  469.         }
  470.     }
  471.     private function uploadEmpresaLogo(Request $req): ?string
  472.     {
  473.         $this->logLogo('empresa_logo_upload.log''--- NUEVA LLAMADA uploadEmpresaLogo ---');
  474.         // name del checkbox en el formulario (ajústalo si usas otro)
  475.         $useCustomLogo $req->request->get('customLogoCheck');
  476.         $this->logLogo('empresa_logo_upload.log''customLogoCheck = '.var_export($useCustomLogotrue));
  477.         // Si no marcaron "usar logo personalizado", no hacemos nada
  478.         if (!$useCustomLogo) {
  479.             $this->logLogo('empresa_logo_upload.log''No se ha marcado customLogoCheck. Salgo sin subir.');
  480.             return null;
  481.         }
  482.         /** @var UploadedFile|null $file */
  483.         $file $req->files->get('logo_personalizado'); // name="logo_personalizado" en el input file
  484.         $this->logLogo('empresa_logo_upload.log''FILES[logo_personalizado] = '.print_r($filetrue));
  485.         if (!$file instanceof UploadedFile || !$file->isValid()) {
  486.             $this->logLogo('empresa_logo_upload.log''File no es UploadedFile válido. Salgo sin subir.');
  487.             return null;
  488.         }
  489.         // VALIDACIONES BÁSICAS
  490.         $maxSize 1024 1024// 2 MB por ejemplo
  491.         if ($file->getSize() > $maxSize) {
  492.             $this->logLogo('empresa_logo_upload.log''Tamaño demasiado grande: '.$file->getSize());
  493.             throw new \RuntimeException('El logo personalizado es demasiado grande (máx 2MB).');
  494.         }
  495.         $mime $file->getMimeType();
  496.         $allowedMimeTypes = ['image/png''image/jpeg''image/webp''image/svg+xml'];
  497.         $this->logLogo('empresa_logo_upload.log''MIME = '.$mime);
  498.         if (!in_array($mime$allowedMimeTypestrue)) {
  499.             $this->logLogo('empresa_logo_upload.log''MIME no permitido.');
  500.             throw new \RuntimeException('Formato de logo no permitido. Usa PNG, JPG, WEBP o SVG.');
  501.         }
  502.         // Directorio destino según entorno (configurado en .env/.env.local)
  503.         $targetDir $_ENV['APP_LOGO_DIR'] ?? null;
  504.         $this->logLogo('empresa_logo_upload.log''APP_LOGO_DIR = '.var_export($targetDirtrue));
  505.         if (!$targetDir) {
  506.             throw new \RuntimeException('APP_LOGO_DIR no está configurado en el entorno.');
  507.         }
  508.         if (!is_dir($targetDir)) {
  509.             $this->logLogo('empresa_logo_upload.log'"El directorio no existe: $targetDir");
  510.             throw new \RuntimeException("El directorio de logos no existe: $targetDir");
  511.         }
  512.         if (!is_writable($targetDir)) {
  513.             $this->logLogo('empresa_logo_upload.log'"El directorio no es escribible: $targetDir");
  514.             throw new \RuntimeException("El directorio de logos no es escribible: $targetDir");
  515.         }
  516.         // Nombre de archivo "seguro" y único
  517.         $ext $file->guessExtension() ?: 'png';
  518.         $fileName 'logo_empresa_' bin2hex(random_bytes(6)) . '.' $ext;
  519.         $this->logLogo('empresa_logo_upload.log'"Voy a mover archivo como: $fileName");
  520.         // Mover físicamente el archivo
  521.         $file->move($targetDir$fileName);
  522.         $this->logLogo('empresa_logo_upload.log'"Fichero movido OK a $targetDir/$fileName");
  523.         // Devolvemos SOLO el nombre, que es lo que se guardará en parametros.logo
  524.         return $fileName;
  525.     }
  526.     private function logLogo(string $fileNamestring $message): void
  527.     {
  528.         // Directorio de logs de Symfony (donde está dev.log/prod.log)
  529.         $logDir $this->getParameter('kernel.logs_dir');
  530.         $fullPath rtrim($logDir'/').'/'.$fileName;
  531.         $line sprintf(
  532.             "[%s] %s\n",
  533.             date('Y-m-d H:i:s'),
  534.             $message
  535.         );
  536.         file_put_contents($fullPath$lineFILE_APPEND);
  537.     }
  538.     private function loadEmpresaLogo(\mysqli $mysqli): ?string
  539.     {
  540.         $sql "SELECT valor FROM parametros WHERE nombre = 'logo' LIMIT 1";
  541.         $res $mysqli->query($sql);
  542.         if (!$res) {
  543.             return null;
  544.         }
  545.         if ($row $res->fetch_assoc()) {
  546.             return $row['valor'] ?? null;
  547.         }
  548.         return null;
  549.     }
  550.     private function upsertLicense(\mysqli $mysqli, array $dataint $capacityGb 200int $activeUsers 3): void
  551.     {
  552.         $clientName = (string)($data['name'] ?? '');
  553.         $licenseStr  'Documanager 1.0';
  554.         $initialDate date('Y-m-d');
  555.         $price       0;
  556.         $emailSender 'Documanager.es';
  557.         $emailFrom   'no-reply@documanager.es';
  558.         $emailName   'Documanager';
  559.         $smtpHost    'documanager.es';
  560.         $smtpUser    'no-reply@documanager.es';
  561.         $smtpPort    587;
  562.         $smtpPass    'Documanager1!';
  563.         $ins $mysqli->prepare("
  564.             INSERT INTO license
  565.                 (client, license, initialDate, capacityGb, users, price, emailSender, emailFrom, emailName, smtpHost, smtpUser, smtpPort, smtpPass)
  566.             VALUES
  567.                 (?,      ?,       ?,          ?,          ?,     ?,     ?,           ?,         ?,         ?,        ?,        ?,        ?)
  568.         ");
  569.         if (!$ins) {
  570.             throw new \RuntimeException('Prepare INSERT license: ' $mysqli->error);
  571.         }
  572.         $ins->bind_param(
  573.             'sssiiisssssis',
  574.             $clientName$licenseStr$initialDate$capacityGb$activeUsers$price,
  575.             $emailSender$emailFrom$emailName$smtpHost$smtpUser$smtpPort$smtpPass
  576.         );
  577.         if (!$ins->execute()) {
  578.             $ins->close();
  579.             throw new \RuntimeException('Execute INSERT license: ' $ins->error);
  580.         }
  581.         $ins->close();
  582.     }
  583.     private function syncExtractionModelToClientDb(\mysqli $clientMysqliEntityManagerInterface $emint $modelId): void
  584.     {
  585.         if ($modelId <= 0) {
  586.             return; // no hay selección
  587.         }
  588.         // Leer MODELO + DEFINICIONES (cabecera y líneas) de la BD principal
  589.         $conn $em->getConnection();
  590.         // 1) Modelo
  591.         $rowModel $conn->executeQuery(
  592.             'SELECT id, provider, model_id, endpoint, api_key, type, show_confidence_badges 
  593.             FROM extraction_models 
  594.             WHERE id = ?',
  595.             [$modelId]
  596.         )->fetchAssociative();
  597.         if (!$rowModel) {
  598.             return; // id inexistente
  599.         }
  600.         // 2) Campos de cabecera
  601.         $headers $conn->executeQuery(
  602.             'SELECT id, model_id, field_key, label, value_type, 
  603.                     order_index, visibility, order_index_table, visibility_table
  604.             FROM definitions_header
  605.             WHERE model_id = ?',
  606.             [$modelId]
  607.         )->fetchAllAssociative();
  608.         // 3) Campos de líneas
  609.         $lines $conn->executeQuery(
  610.             'SELECT id, model_id, field_key, label, value_type,
  611.                     order_index, visibility
  612.             FROM definitions_lines
  613.             WHERE model_id = ?',
  614.             [$modelId]
  615.         )->fetchAllAssociative();
  616.         // ----- TRANSACCIÓN EN BD DEL CLIENTE -----
  617.         $clientMysqli->begin_transaction();
  618.         try {
  619.             // ------------------------------------------------
  620.             // 1) UPSERT extraction_models (cliente)
  621.             // ------------------------------------------------
  622.             $sqlModel 'INSERT INTO extraction_models 
  623.                             (id, provider, model_id, endpoint, api_key, type, show_confidence_badges)
  624.                         VALUES (?, ?, ?, ?, ?, ?, ?)
  625.                         ON DUPLICATE KEY UPDATE
  626.                             provider = VALUES(provider),
  627.                             model_id = VALUES(model_id),
  628.                             endpoint = VALUES(endpoint),
  629.                             api_key = VALUES(api_key),
  630.                             type = VALUES(type),
  631.                             show_confidence_badges = VALUES(show_confidence_badges)';
  632.             $stmtModel $clientMysqli->prepare($sqlModel);
  633.             if (!$stmtModel) {
  634.                 throw new \RuntimeException('Prepare INSERT extraction_models (cliente): ' $clientMysqli->error);
  635.             }
  636.             $id       = (int)$rowModel['id'];
  637.             $provider = (string)$rowModel['provider'];
  638.             $modelIdStr = (string)$rowModel['model_id'];
  639.             $endpoint = (string)$rowModel['endpoint'];
  640.             $apiKey   = (string)$rowModel['api_key'];
  641.             $type     = (string)$rowModel['type'];
  642.             $badges   = (int)$rowModel['show_confidence_badges'];
  643.             if (
  644.                 !$stmtModel->bind_param(
  645.                     'isssssi',
  646.                     $id,
  647.                     $provider,
  648.                     $modelIdStr,
  649.                     $endpoint,
  650.                     $apiKey,
  651.                     $type,
  652.                     $badges
  653.                 )
  654.                 || !$stmtModel->execute()
  655.             ) {
  656.                 $stmtModel->close();
  657.                 throw new \RuntimeException('Execute INSERT/UPSERT extraction_models (cliente): ' $clientMysqli->error);
  658.             }
  659.             $stmtModel->close();
  660.             // ------------------------------------------------
  661.             // 2) Sincronizar definitions_header (cliente)
  662.             //    - Primero borramos las filas del modelo
  663.             //    - Luego insertamos las de la central
  664.             // ------------------------------------------------
  665.             $stmtDelHeaders $clientMysqli->prepare(
  666.                 'DELETE FROM definitions_header WHERE model_id = ?'
  667.             );
  668.             if (!$stmtDelHeaders) {
  669.                 throw new \RuntimeException('Prepare DELETE definitions_header (cliente): ' $clientMysqli->error);
  670.             }
  671.             $stmtDelHeaders->bind_param('i'$id);
  672.             if (!$stmtDelHeaders->execute()) {
  673.                 $stmtDelHeaders->close();
  674.                 throw new \RuntimeException('Execute DELETE definitions_header (cliente): ' $clientMysqli->error);
  675.             }
  676.             $stmtDelHeaders->close();
  677.             if (!empty($headers)) {
  678.                 $stmtInsHeaders $clientMysqli->prepare(
  679.                     'INSERT INTO definitions_header 
  680.                         (id, model_id, field_key, label, value_type, 
  681.                         order_index, visibility, order_index_table, visibility_table)
  682.                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
  683.                 );
  684.                 if (!$stmtInsHeaders) {
  685.                     throw new \RuntimeException('Prepare INSERT definitions_header (cliente): ' $clientMysqli->error);
  686.                 }
  687.                 foreach ($headers as $h) {
  688.                     $hId        = (int)$h['id'];
  689.                     $hModelId   = (int)$h['model_id'];
  690.                     $fieldKey   = (string)$h['field_key'];
  691.                     // si quieres respetar NULL tal cual, quita el (string) y pasa directamente $h['label']
  692.                     $label      $h['label'] !== null ? (string)$h['label'] : null;
  693.                     $valueType  = (string)$h['value_type'];
  694.                     $orderIndex = (int)$h['order_index'];
  695.                     $visibility = (int)$h['visibility'];
  696.                     $orderTable = (int)$h['order_index_table'];
  697.                     $visTable   = (int)$h['visibility_table'];
  698.                     if (
  699.                         !$stmtInsHeaders->bind_param(
  700.                             'iisssiiii',
  701.                             $hId,
  702.                             $hModelId,
  703.                             $fieldKey,
  704.                             $label,
  705.                             $valueType,
  706.                             $orderIndex,
  707.                             $visibility,
  708.                             $orderTable,
  709.                             $visTable
  710.                         )
  711.                         || !$stmtInsHeaders->execute()
  712.                     ) {
  713.                         $stmtInsHeaders->close();
  714.                         throw new \RuntimeException('Execute INSERT definitions_header (cliente): ' $clientMysqli->error);
  715.                     }
  716.                 }
  717.                 $stmtInsHeaders->close();
  718.             }
  719.             // ------------------------------------------------
  720.             // 3) Sincronizar definitions_lines (cliente)
  721.             // ------------------------------------------------
  722.             $stmtDelLines $clientMysqli->prepare(
  723.                 'DELETE FROM definitions_lines WHERE model_id = ?'
  724.             );
  725.             if (!$stmtDelLines) {
  726.                 throw new \RuntimeException('Prepare DELETE definitions_lines (cliente): ' $clientMysqli->error);
  727.             }
  728.             $stmtDelLines->bind_param('i'$id);
  729.             if (!$stmtDelLines->execute()) {
  730.                 $stmtDelLines->close();
  731.                 throw new \RuntimeException('Execute DELETE definitions_lines (cliente): ' $clientMysqli->error);
  732.             }
  733.             $stmtDelLines->close();
  734.             if (!empty($lines)) {
  735.                 $stmtInsLines $clientMysqli->prepare(
  736.                     'INSERT INTO definitions_lines
  737.                         (id, model_id, field_key, label, value_type, 
  738.                         order_index, visibility)
  739.                     VALUES (?, ?, ?, ?, ?, ?, ?)'
  740.                 );
  741.                 if (!$stmtInsLines) {
  742.                     throw new \RuntimeException('Prepare INSERT definitions_lines (cliente): ' $clientMysqli->error);
  743.                 }
  744.                 foreach ($lines as $l) {
  745.                     $lId        = (int)$l['id'];
  746.                     $lModelId   = (int)$l['model_id'];
  747.                     $lFieldKey  = (string)$l['field_key'];
  748.                     $lLabel     $l['label'] !== null ? (string)$l['label'] : null;
  749.                     $lType      = (string)$l['value_type'];
  750.                     $lOrder     = (int)$l['order_index'];
  751.                     $lVis       = (int)$l['visibility'];
  752.                     if (
  753.                         !$stmtInsLines->bind_param(
  754.                             'iisssii',
  755.                             $lId,
  756.                             $lModelId,
  757.                             $lFieldKey,
  758.                             $lLabel,
  759.                             $lType,
  760.                             $lOrder,
  761.                             $lVis
  762.                         )
  763.                         || !$stmtInsLines->execute()
  764.                     ) {
  765.                         $stmtInsLines->close();
  766.                         throw new \RuntimeException('Execute INSERT definitions_lines (cliente): ' $clientMysqli->error);
  767.                     }
  768.                 }
  769.                 $stmtInsLines->close();
  770.             }
  771.             // Si todo ha ido bien
  772.             $clientMysqli->commit();
  773.         } catch (\Throwable $e) {
  774.             $clientMysqli->rollback();
  775.             throw $e;
  776.         }
  777.     }
  778.     
  779.     public function Empresa(Request $reqEntityManagerInterface $em)
  780.     {
  781.         if (!$this->getUser() || !is_object($this->getUser())) {         
  782.             return $this->redirectToRoute('logout');
  783.         }
  784.         $id = (int)$req->get("id");
  785.         if (!$id) {
  786.             $this->addFlash('warning''Empresa no encontrada.');
  787.             return $this->redirectToRoute("list");
  788.         }
  789.         $empresa $em->getRepository(Empresa::class)->find($id);
  790.         if (!$empresa) {
  791.             $this->addFlash('warning''Empresa no encontrada.');
  792.             return $this->redirectToRoute("list");
  793.         }
  794.         $users $em->getRepository(Usuario::class)->findBy([
  795.             "empresa" => $empresa->getId()
  796.         ]);
  797.         // Valores por defecto por si algo falla al conectar con la BD del cliente
  798.         $activeUsers            null;
  799.         $modulos                = [];
  800.         $empresaLogo            null;
  801.         $extractionModelLabel   null;
  802.         $diskUsedBytes          null;
  803.         $diskUsedGb             null;
  804.         try {
  805.             $cx $empresa->getConexionBD();
  806.             if ($cx) {
  807.                 $mysqli = @new \mysqli(
  808.                     $cx->getDbUrl(),
  809.                     $cx->getDbUser(),
  810.                     $cx->getDbPassword(),
  811.                     $cx->getDbName(),
  812.                     (int)$cx->getDbPort()
  813.                 );
  814.                 if (!$mysqli->connect_error) {
  815.                     // parámetros (modulos, límites, etc.)
  816.                     [$activeUsers$modulos] = $this->loadEmpresaParametros($mysqli);
  817.                     // logo guardado en la BD del cliente
  818.                     $empresaLogo $this->loadEmpresaLogo($mysqli);
  819.                     $diskUsedBytes $this->loadEmpresaDiskUsageBytes($mysqli);
  820.                     $diskUsedGb = ($diskUsedBytes !== null)
  821.                         ? round($diskUsedBytes 1024 1024 10242)
  822.                         : null;
  823.                     // Si tiene extracción y modelo seleccionado, buscamos el ID legible del modelo
  824.                     if (
  825.                         !empty($modulos['modulo_extraccion']) &&
  826.                         !empty($modulos['extraction_model'])
  827.                     ) {
  828.                         try {
  829.                             $conn $em->getConnection();
  830.                             $row $conn->fetchAssociative(
  831.                                 'SELECT model_id FROM extraction_models WHERE id = :id',
  832.                                 ['id' => (int)$modulos['extraction_model']]
  833.                             );
  834.                             if ($row && isset($row['model_id'])) {
  835.                                 $extractionModelLabel $row['model_id'];
  836.                             }
  837.                         } catch (\Throwable $e) {
  838.                             // Si falla, simplemente no mostramos el texto bonito del modelo
  839.                             $extractionModelLabel null;
  840.                         }
  841.                     }
  842.                     $mysqli->close();
  843.                 }
  844.             }
  845.         } catch (\Throwable $e) {
  846.             // Aquí podrías loguear el error si quieres, pero no rompemos la pantalla
  847.         }
  848.         return $this->render('empresa_detail.html.twig', [
  849.             'empresa'              => $empresa,
  850.             'users'                => $users,
  851.             'activeUsers'          => $activeUsers,
  852.             'modulos'              => $modulos,
  853.             'empresaLogo'          => $empresaLogo,
  854.             'extractionModelLabel' => $extractionModelLabel,
  855.             'diskUsedGb' => $diskUsedGb,
  856.         ]);
  857.     } 
  858.     
  859.     public function deleteEmpresa(Request $requestEntityManagerInterface $em){
  860.         $id $request->get("id");
  861.         
  862.         $empresa $em->getRepository(Empresa::class)->find($id);
  863.         
  864.         $conexion $empresa->getConexionBD();
  865.         $usuarios $em->getRepository(Usuario::class)->findBy(array("empresa"=>$empresa->getId()));
  866.         
  867.         $hestiaApiUrl 'https://200.234.237.107:8083/api/';
  868.         $owner 'docunecta'// o el dueño del hosting
  869.         
  870.         $postFields http_build_query([
  871.             'user' => 'admin',
  872.             'password' => 'i9iQiSmxb2EpvgLq',
  873.             'returncode' => 'yes',
  874.             'cmd' => 'v-delete-database',
  875.             'arg1' => 'admin',
  876.             'arg2' => $conexion->getDbName(),
  877.             
  878.         ]);
  879.         
  880.         $accessKeyId 'cWYbt9ShyFQ3yVRsUE8u';
  881.         $secretKey 'e2M_5wk2_jUAlPorF7V8zfwo3_0ihu90WoLPMKwj';
  882.         $headers = [
  883.             'Authorization: Bearer ' $accessKeyId ':' $secretKey
  884.         ];
  885.         $ch curl_init();
  886.         curl_setopt($chCURLOPT_URL$hestiaApiUrl);
  887.         curl_setopt($chCURLOPT_HTTPHEADER$headers);
  888.         curl_setopt($chCURLOPT_RETURNTRANSFERtrue);
  889.         curl_setopt($chCURLOPT_SSL_VERIFYHOSTfalse);
  890.         curl_setopt($chCURLOPT_POSTtrue);
  891.         curl_setopt($chCURLOPT_POSTFIELDS$postFields);
  892.         curl_setopt($chCURLOPT_SSL_VERIFYPEERfalse); // Solo si usas certificados autofirmados
  893.         $response curl_exec($ch);
  894.         $error curl_error($ch);
  895.         curl_close($ch);
  896.         if (($error || trim($response) !== '0') && trim($response) !== '3') {
  897.             $this->addFlash('danger''Error al eliminar la base de datos en HestiaCP: ' . ($error ?: $response));
  898.             return $this->redirectToRoute('list');
  899.         }
  900.         // Eliminar el servicio systemd asociado a la empresa
  901.         $company_name $empresa->getId();
  902.         $serviceName $company_name "-documanager.service";
  903.         $servicePath "/etc/systemd/system/$serviceName";
  904.         $cmds = [
  905.             "sudo /bin/systemctl stop $serviceName",
  906.             "sudo /bin/systemctl disable $serviceName",
  907.             "sudo /bin/rm $servicePath",
  908.             "sudo /bin/systemctl daemon-reload"
  909.         ];
  910.         $serviceErrors = [];
  911.         foreach ($cmds as $cmd) {
  912.             $output = @\shell_exec($cmd " 2>&1");
  913.             if ($output !== null && trim($output) !== '') {
  914.                 $serviceErrors[] = "CMD OUTPUT: $cmd\n$output";
  915.             }
  916.         }
  917.         $azureService $company_name "-azuredi.service";
  918.         $servicePathAzure "/etc/systemd/system/$azureService";
  919.         $cmdsAzure = [
  920.             "sudo /bin/systemctl stop $azureService",
  921.             "sudo /bin/systemctl disable $azureService",
  922.             "sudo /bin/rm $servicePathAzure",
  923.             "sudo /bin/systemctl daemon-reload",
  924.         ];
  925.         foreach ($cmdsAzure as $cmd) {
  926.             $output = @\shell_exec($cmd " 2>&1");
  927.             if ($output) {
  928.                 $serviceErrors[] = "CMD OUTPUT: $cmd\n$output";
  929.             }
  930.         }
  931.         
  932.         //eliminamos usuarios
  933.         foreach($usuarios as $user){
  934.             $em->remove($user);
  935.             $em->flush();
  936.         }
  937.         
  938.         //eliminamos conexión
  939.         $em->remove($conexion);
  940.         $em->flush();
  941.         
  942.         //eliminamos empresa
  943.         $em->remove($empresa);
  944.         $em->flush();
  945.         $msg 'Empresa y base de datos eliminadas correctamente.';
  946.         if (count($serviceErrors) > 0) {
  947.             $msg .= ' ' implode('<br>'$serviceErrors);
  948.         }
  949.         $this->addFlash('success'$msg);
  950.         return $this->redirectToRoute('list');
  951.         
  952.     }
  953.     public function editEmpresa(Request $requestEntityManagerInterface $em)
  954.     {
  955.         $id = (int)$request->get('id');
  956.         $empresa $em->getRepository(Empresa::class)->find($id);
  957.         if (!$empresa) {
  958.             throw $this->createNotFoundException('Empresa no encontrada');
  959.         }
  960.         // 1) Conectar a la BD del cliente con las credenciales de la central
  961.         $cx $empresa->getConexionBD();
  962.         $mysqli = @new \mysqli(
  963.             $cx->getDbUrl(),
  964.             $cx->getDbUser(),
  965.             $cx->getDbPassword(),
  966.             $cx->getDbName(),
  967.             (int)$cx->getDbPort()
  968.         );
  969.         if ($mysqli->connect_error) {
  970.             $this->addFlash('danger''No se puede conectar a la BD del cliente: ' $mysqli->connect_error);
  971.             return $this->redirectToRoute('list');
  972.         }
  973.         if ($request->isMethod('POST') && $request->request->get('submit') !== null) {
  974.             $data $request->request->all();
  975.             // 2) Actualizar SOLO central
  976.             $empresa->setName((string)($data['name'] ?? $empresa->getName()));
  977.             $empresa->setMaxDiskQuota(
  978.                 isset($data['maxDiskQuota']) && $data['maxDiskQuota'] !== ''
  979.                     ? (int)$data['maxDiskQuota']
  980.                     : $empresa->getMaxDiskQuota()
  981.             );
  982.             $em->persist($empresa);
  983.             // ---- Normalización de POST (checkboxes / select) ----
  984.             $toBool = fn($v) => in_array(strtolower((string)$v), ['1','on','true','yes'], true);
  985.             $extractionModel 0;
  986.             if (isset($data['extraction_model']) && $data['extraction_model'] !== '') {
  987.                 $extractionModel = (int)$data['extraction_model'];
  988.             }
  989.             $data['extraction_model'] = $extractionModel;
  990.             $data['modulo_extraccion']       = isset($data['modulo_extraccion'])       && $toBool($data['modulo_extraccion'])       ? 0;
  991.             $data['modulo_etiquetas']        = isset($data['modulo_etiquetas'])        && $toBool($data['modulo_etiquetas'])        ? 0;
  992.             $data['modulo_calendario']       = isset($data['modulo_calendario'])       && $toBool($data['modulo_calendario'])       ? 0;
  993.             $data['modulo_calendarioExterno']= isset($data['modulo_calendarioExterno'])&& $toBool($data['modulo_calendarioExterno'])? 0;
  994.             $data['modulo_estados']          = isset($data['modulo_estados'])          && $toBool($data['modulo_estados'])          ? 0;
  995.             $data['modulo_lineas']           = isset($data['modulo_lineas'])           && $toBool($data['modulo_lineas'])           ? 0;
  996.             // Dependencias
  997.             if (!$data['modulo_calendario']) {
  998.                 $data['modulo_calendarioExterno'] = 0;
  999.             }
  1000.             if (!$data['modulo_extraccion']) {
  1001.                 $data['modulo_lineas']   = 0;
  1002.                 $data['extraction_model'] = 0;
  1003.             }
  1004.             // 3) Guardar en BD del cliente: parametros + license
  1005.             $this->updateEmpresaParametros($mysqli$data);
  1006.             // 3.1) Si extracción activa y hay modelo, sincroniza modelo a BD del cliente
  1007.             if ($data['modulo_extraccion'] === && $data['extraction_model'] > 0) {
  1008.                 try {
  1009.                     $this->syncExtractionModelToClientDb($mysqli$em, (int)$data['extraction_model']);
  1010.                 } catch (\Throwable $e) {
  1011.                     // log opcional
  1012.                 }
  1013.             }
  1014.             // si prefieres solo UPDATE en license, usa una función updateLicense(); si no, upsert/insert:
  1015.             $this->updateLicense(
  1016.                 $mysqli,
  1017.                 $data,
  1018.                 $empresa->getName()
  1019.             );
  1020.             $em->flush();
  1021.             $mysqli->close();
  1022.             // Mensaje de éxito
  1023.             $this->addFlash('success''Empresa editada correctamente.');
  1024.             return $this->redirectToRoute('app_empresa_show', ['id' => $id]);
  1025.         }
  1026.         // 4) GET: precargar desde BD del cliente
  1027.         [$activeUsers$modulos] = $this->loadEmpresaParametros($mysqli);
  1028.         $license $this->loadLicense($mysqli$empresa->getName()); // por si quieres mostrarlo
  1029.         $extractionModels = [];
  1030.         try {
  1031.             $conn $em->getConnection(); // principal
  1032.             $sql 'SELECT id, model_id FROM extraction_models ORDER BY model_id';
  1033.             $extractionModels $conn->executeQuery($sql)->fetchAllAssociative();
  1034.             foreach ($extractionModels as &$m) { $m['id'] = (int)$m['id']; }
  1035.         } catch (\Throwable $e) {
  1036.             $extractionModels = [];
  1037.         }
  1038.         $mysqli->close();
  1039.         return $this->render('empresa/_edit.html.twig', [
  1040.             'empresa'      => $empresa,   // central: name + maxDiskQuota
  1041.             'id'           => $id,
  1042.             'activeUsers'  => $activeUsers// cliente
  1043.             'modulos'      => $modulos,     // cliente
  1044.             'license'      => $license,     // opcional
  1045.             'extraction_models' => $extractionModels,
  1046.         ]);
  1047.     }
  1048.     private function loadEmpresaParametros(\mysqli $mysqli): array
  1049.     {
  1050.         // claves que nos interesan en la tabla parametros
  1051.         $keys = [
  1052.             'activeUsers',
  1053.             'modulo_etiquetas','modulo_calendario','modulo_calExt',
  1054.             'modulo_estados''modulo_subida''modulo_extraccion','modulo_lineas','limite_archivos','extraction_model',
  1055.         ];
  1056.         $placeholders implode(','array_fill(0count($keys), '?'));
  1057.         $sql "SELECT nombre, valor FROM parametros WHERE nombre IN ($placeholders)";
  1058.         $stmt $mysqli->prepare($sql);
  1059.         if (!$stmt) {
  1060.             throw new \RuntimeException('Prepare SELECT parametros: ' $mysqli->error);
  1061.         }
  1062.         // bind dinámico
  1063.         $types str_repeat('s'count($keys));
  1064.         $stmt->bind_param($types, ...$keys);
  1065.         if (!$stmt->execute()) {
  1066.             $stmt->close();
  1067.             throw new \RuntimeException('Execute SELECT parametros: ' $stmt->error);
  1068.         }
  1069.         $res $stmt->get_result();
  1070.         $map = [];
  1071.         while ($row $res->fetch_assoc()) {
  1072.             $map[$row['nombre']] = $row['valor'];
  1073.         }
  1074.         $stmt->close();
  1075.         // defaults seguros
  1076.         $activeUsers = isset($map['activeUsers']) ? (int)$map['activeUsers'] : 3;
  1077.         $flags = [
  1078.             'modulo_etiquetas'  => isset($map['modulo_etiquetas'])  ? (int)$map['modulo_etiquetas']  : 0,
  1079.             'modulo_calendario' => isset($map['modulo_calendario']) ? (int)$map['modulo_calendario'] : 0,
  1080.             'modulo_calExt'     => isset($map['modulo_calExt'])     ? (int)$map['modulo_calExt']     : 0,            
  1081.             'modulo_estados'    => isset($map['modulo_estados'])    ? (int)$map['modulo_estados']    : 0,
  1082.             'modulo_subida'     => isset($map['modulo_subida'])     ? (int)$map['modulo_subida']     : 0,
  1083.             'modulo_extraccion' => isset($map['modulo_extraccion']) ? (int)$map['modulo_extraccion'] : 0,
  1084.             'modulo_lineas'     => isset($map['modulo_lineas'])     ? (int)$map['modulo_lineas']     : 0,
  1085.         ];
  1086.         $limiteArchivos = isset($map['limite_archivos']) && $map['limite_archivos'] !== ''
  1087.             ? (int)$map['limite_archivos']
  1088.             : 500;
  1089.             
  1090.         // Lo añadimos a $flags para no cambiar la firma del return
  1091.         $flags['limite_archivos'] = $limiteArchivos;
  1092.         // extraction_model: default 0 (sin modelo)
  1093.         $flags['extraction_model'] = isset($map['extraction_model']) && $map['extraction_model'] !== '' ? (int)$map['extraction_model'] : 0;
  1094.         return [$activeUsers$flags];
  1095.     }
  1096.     private function updateEmpresaParametros(\mysqli $mysqli, array $data): void
  1097.     {
  1098.         $toBool = fn($v) => in_array(strtolower((string)$v), ['1','on','true','yes'], true);
  1099.         $getInt  = fn(array $astring $kint $d) => (isset($a[$k]) && $a[$k] !== '') ? (int)$a[$k] : $d;
  1100.         $getFlag = fn(array $astring $k) => (isset($a[$k]) && (int)$a[$k] === 1) ? 0;
  1101.         $paramMap = [
  1102.             'activeUsers'        => $getInt($data'maxActiveUsers'3),
  1103.             'modulo_etiquetas'   => $getFlag($data'modulo_etiquetas'),
  1104.             'modulo_calendario'  => $getFlag($data'modulo_calendario'),
  1105.             'modulo_calExt'      => $getFlag($data'modulo_calendarioExterno'),            
  1106.             'modulo_estados'     => $getFlag($data'modulo_estados'),
  1107.             'modulo_subida'      => $getFlag($data'modulo_subida'),
  1108.             'modulo_extraccion'  => $getFlag($data'modulo_extraccion'),
  1109.             'modulo_lineas'      => $getFlag($data'modulo_lineas'),
  1110.             'limite_archivos'    => $getInt($data'limite_archivos'500),
  1111.             'extraction_model'   => $getInt($data'extraction_model'0),
  1112.         ];
  1113.         if ($paramMap['modulo_extraccion'] === 0) {
  1114.             $paramMap['modulo_lineas'] = 0;
  1115.             $paramMap['extraction_model'] = 0;
  1116.         }
  1117.         if ($paramMap['modulo_calendario'] === 0) {
  1118.             $paramMap['modulo_calExt'] = 0;
  1119.         }
  1120.         
  1121.         $mysqli->begin_transaction();
  1122.         try {
  1123.             $stmt $mysqli->prepare("
  1124.                 INSERT INTO parametros (nombre, valor)
  1125.                 VALUES (?, ?)
  1126.                 ON DUPLICATE KEY UPDATE valor = VALUES(valor)
  1127.             ");
  1128.             if (!$stmt) {
  1129.                 throw new \RuntimeException('Prepare UPSERT parametros: ' $mysqli->error);
  1130.             }
  1131.             foreach ($paramMap as $nombre => $valor) {
  1132.                 $v = (string)$valor;
  1133.                 if (!$stmt->bind_param('ss'$nombre$v) || !$stmt->execute()) {
  1134.                     $stmt->close();
  1135.                     throw new \RuntimeException("Guardar parámetro {$nombre}{$stmt->error}");
  1136.                 }
  1137.             }
  1138.             $stmt->close();
  1139.             $mysqli->commit();
  1140.         } catch (\Throwable $e) {
  1141.             $mysqli->rollback();
  1142.             throw $e;
  1143.         }
  1144.     }
  1145.     private function loadLicense(\mysqli $mysqli): array
  1146.     {
  1147.         // Carga opcional para mostrar en la edición: capacityGb/users (u otros)
  1148.         $sql "SELECT client, capacityGb, users FROM license LIMIT 1";
  1149.         $res $mysqli->query($sql);
  1150.         if ($res && $row $res->fetch_assoc()) {
  1151.             return $row;
  1152.         }
  1153.         return [];
  1154.     }
  1155.     private function updateLicense(\mysqli $mysqli, array $datastring $clientName): void
  1156.     {
  1157.         $capacityGb  = isset($data['maxDiskQuota']) ? (int)$data['maxDiskQuota'] : 200;
  1158.         $activeUsers = isset($data['maxActiveUsers']) ? (int)$data['maxActiveUsers'] : 3;
  1159.         $stmt $mysqli->prepare("UPDATE license SET capacityGb = ?, users = ? WHERE client = ?");
  1160.         if (!$stmt) {
  1161.             throw new \RuntimeException('Prepare UPDATE license: ' $mysqli->error);
  1162.         }
  1163.         $stmt->bind_param('iis'$capacityGb$activeUsers$clientName);
  1164.         $stmt->execute();
  1165.         $stmt->close();
  1166.     }
  1167.     
  1168.     public function usersListEmpresa(Request $request,EntityManagerInterface $em){
  1169.         $id $request->get("id");
  1170.         $users_empresa $em->getRepository(Usuario::class)->findBy(array("empresa"=>$id));
  1171.         
  1172.          return $this->render('empresa/usersList.html.twig',array(
  1173.              'users' => $users_empresa,
  1174.              'id'=>$id
  1175.          ));
  1176.     }
  1177.     private function loadEmpresaDiskUsageBytes(\mysqli $mysqli): ?int
  1178.     {
  1179.         // Si la tabla no existe o hay error, devolvemos null para no romper la vista
  1180.         $sql "SELECT COALESCE(SUM(size_bytes), 0) AS total_bytes FROM files";
  1181.         $res $mysqli->query($sql);
  1182.         if (!$res) {
  1183.             return null;
  1184.         }
  1185.         $row $res->fetch_assoc();
  1186.         $res->free();
  1187.         return isset($row['total_bytes']) ? (int)$row['total_bytes'] : 0;
  1188.     }
  1189. }