diff --git a/ci4/app/Config/Paths.php b/ci4/app/Config/Paths.php index d0035fbc..221145b2 100755 --- a/ci4/app/Config/Paths.php +++ b/ci4/app/Config/Paths.php @@ -72,4 +72,21 @@ class Paths * is used when no value is provided to `Services::renderer()`. */ public string $viewDirectory = __DIR__ . '/../Views'; + + /** + * Ruta base relativa dentro de WRITEPATH donde se almacenan + * los archivos asociados a presupuestos. + * + * Esta ruta se utiliza como base para componer las rutas + * completas tanto locales como remotas (SFTP) de ficheros + * relacionados con cada presupuesto. + * + * Ejemplo: + * Si el ID del presupuesto es 123 y el nombre del archivo es "documento.pdf", + * la ruta final será: storage/presupuestos/123/documento.pdf + * + * Se recomienda mantener esta ruta fuera de `public/` por razones de seguridad + * y utilizar controladores para servir los archivos si se desea acceso web. + */ + public string $presupuestosPath = 'storage/presupuestos'; } diff --git a/ci4/app/Config/PresupuestoSFTP.php b/ci4/app/Config/PresupuestoSFTP.php index a5dd8b1a..4e1919ec 100644 --- a/ci4/app/Config/PresupuestoSFTP.php +++ b/ci4/app/Config/PresupuestoSFTP.php @@ -11,6 +11,7 @@ class PresupuestoSFTP extends BaseConfig public string $username; public string $password; public string $base_dir; + public string $remote_base_dir = 'ficheros'; // subcarpeta específica para presupuestos public int $id_offset; public function __construct() @@ -21,7 +22,17 @@ class PresupuestoSFTP extends BaseConfig $this->username = env("HIDRIVE_FILES_USER"); $this->password = env("HIDRIVE_FILES_PASS"); $this->id_offset = (int) env("BUDGET_FILES_OFFSET_ID", 1000000); + + // Directorio base remoto: /users/usuario/dominio $domain = parse_url(env("app.baseURL"), PHP_URL_HOST); $this->base_dir = "/users/{$this->username}/{$domain}"; } + + /** + * Devuelve la ruta completa del directorio remoto para un presupuesto + */ + public function getRemoteDirForPresupuesto(int $presupuestoId): string + { + return "{$this->base_dir}/{$this->remote_base_dir}/" . ($presupuestoId + $this->id_offset); + } } diff --git a/ci4/app/Config/Routes/PresupuestosRoutes.php b/ci4/app/Config/Routes/PresupuestosRoutes.php index 16929334..a4976457 100755 --- a/ci4/app/Config/Routes/PresupuestosRoutes.php +++ b/ci4/app/Config/Routes/PresupuestosRoutes.php @@ -69,4 +69,10 @@ $routes->group('importador', ['namespace' => 'App\Controllers\Presupuestos'], fu $routes->get('getencuadernacion', 'Importadorpresupuestos::getEncuadernacionList'); $routes->get('getpresupuestodata', 'Importadorpresupuestos::getPresupuesto', ['as' => 'getPresupuesto']); $routes->post('importar', 'Importadorpresupuestos::importarPresupuesto'); +}); + +$routes->group('files', ['namespace' => 'App\Controllers\Presupuestos'], function($routes) { + $routes->post('get_files', 'PresupuestoFicheroController::get_files', ['as' => 'getFiles']); + $routes->post('upload_files', 'PresupuestoFicheroController::upload_files', ['as' => 'uploadFiles']); + $routes->post('download_zip', 'PresupuestoFicheroController::download_zip', ['as' => 'downloadFilesZipped']); }); \ No newline at end of file diff --git a/ci4/app/Controllers/Presupuestos/PresupuestoFicheroController.php b/ci4/app/Controllers/Presupuestos/PresupuestoFicheroController.php new file mode 100644 index 00000000..4b27c8c5 --- /dev/null +++ b/ci4/app/Controllers/Presupuestos/PresupuestoFicheroController.php @@ -0,0 +1,184 @@ +request->getMethod(true) !== 'POST') { + return $this->response->setStatusCode(405)->setJSON(['message' => 'Método no permitido']); + } + + $presupuesto_id = $this->request->getPost('presupuesto_id') ?? 0; + + $model = model('App\Models\Presupuestos\PresupuestoFicheroModel'); + $files = $model->getFiles($presupuesto_id); + + $result = []; + + foreach ($files as $file) { + $relativePath = $file->file_path; + $fullPath = WRITEPATH . ltrim($relativePath, '/'); + + $relativePath = $file->file_path; + $basename = basename($relativePath); // solo el nombre del archivo + + $result[] = (object) [ + 'name' => $file->nombre, + 'size' => file_exists(WRITEPATH . $relativePath) ? filesize(WRITEPATH . $relativePath) : 0, + 'hash' => $basename + ]; + } + + return $this->response->setJSON($result); + } + + + public function upload_files() + { + $request = service('request'); + $model = model('App\Models\Presupuestos\PresupuestoFicheroModel'); + $files = $request->getFiles()['file'] ?? []; + $presupuesto_id = $request->getPost('presupuesto_id'); + $old_files = json_decode($request->getPost('oldFiles') ?? '[]'); + + if (!is_array($files)) { + $files = [$files]; + } + + // Servicio de subida (con SFTP) + $service = new \App\Services\PresupuestoUploaderService( + new \App\Libraries\SftpClientWrapper(config('PresupuestoSFTP')), + $model, + config('PresupuestoSFTP') + ); + + // Borrar ficheros eliminados por el usuario (BD y remoto) + $service->removeFromRemote($presupuesto_id); + $numDeleted = $model->deleteMissingFiles($presupuesto_id, $old_files); + + $results = []; + foreach ($files as $file) { + if (!$file->isValid()) { + $results[] = [ + 'name' => $file->getName(), + 'status' => 'invalid', + 'message' => $file->getErrorString() + ]; + continue; + } + + $newName = $model->saveFileInBBDD( + $presupuesto_id, + $file->getClientName(), + $file->getClientExtension(), + auth()->id() + ); + + // Crear directorio si no existe + $uploadDir = dirname($model->getAbsolutePath($presupuesto_id, $newName)); + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0755, true); + } + + $file->move($uploadDir, $newName); + + $results[] = [ + 'name' => $file->getClientName(), + 'status' => 'uploaded' + ]; + } + + // Subida al SFTP + $sftpResult = $service->uploadToRemote($presupuesto_id); + + // ✅ Contar totales para mostrar en el frontend + $numUploaded = count(array_filter($results, fn($f) => $f['status'] === 'uploaded')); + $numErrores = count(array_filter($results, fn($f) => $f['status'] !== 'uploaded')); + + if (!$sftpResult['success']) { + return $this->response->setJSON([ + 'message' => 'Error en la subida de algunos archivos.', + 'summary' => [ + 'subidos_ok' => $numUploaded, + 'errores_locales' => $numErrores, + 'errores_remotos' => count(array_filter($sftpResult['files'], fn($f) => !$f['success'])), + 'borrados' => $numDeleted, + ], + 'details' => [ + 'local' => $results, + 'sftp' => $sftpResult['files'] + ] + ])->setStatusCode(500); + } + + return $this->response->setJSON([ + 'message' => 'Archivos subidos correctamente.', + 'summary' => [ + 'subidos_ok' => $numUploaded, + 'errores_locales' => $numErrores, + 'errores_remotos' => 0, + 'borrados' => $numDeleted + ], + 'details' => [ + 'local' => $results, + 'sftp' => $sftpResult['files'] + ] + ]); + } + + public function download_zip() + { + $presupuesto_id = $this->request->getPost('presupuesto_id'); + $ot_id = $this->request->getPost('ot_id'); + + if (!$presupuesto_id) { + return $this->response->setStatusCode(400)->setBody('Presupuesto ID requerido'); + } + + $prefijo = (!empty($ot_id) && is_numeric($ot_id)) ? "OT_{$ot_id}" : null; + + $service = new \App\Services\PresupuestoUploaderService( + new \App\Libraries\SftpClientWrapper(config('PresupuestoSFTP')), + model('App\Models\Presupuestos\PresupuestoFicheroModel'), + config('PresupuestoSFTP') + ); + + $result = $service->downloadZip((int) $presupuesto_id, $prefijo); + + if (!$result['success'] || empty($result['zipPath'])) { + return $this->response + ->setStatusCode(500) + ->setJSON(['error' => $result['message']]); + } + + $zipPath = $result['zipPath']; + + // Definir nombre final del ZIP para el cliente + $nombreArchivo = $prefijo + ? "{$prefijo}_PRESUPUESTO_{$presupuesto_id}.zip" + : "archivos_presupuesto_{$presupuesto_id}.zip"; + + // Eliminar archivo ZIP tras terminar la descarga (una vez enviada la respuesta) + register_shutdown_function(function () use ($zipPath) { + if (file_exists($zipPath)) { + unlink($zipPath); + } + }); + + // Descargar el archivo al cliente + return $this->response + ->download($zipPath, null) + ->setFileName($nombreArchivo); + } +} diff --git a/ci4/app/Controllers/Presupuestos/Presupuestocliente.php b/ci4/app/Controllers/Presupuestos/Presupuestocliente.php index 007759fe..30cdb4f2 100755 --- a/ci4/app/Controllers/Presupuestos/Presupuestocliente.php +++ b/ci4/app/Controllers/Presupuestos/Presupuestocliente.php @@ -1747,110 +1747,7 @@ class Presupuestocliente extends \App\Controllers\BaseResourceController } } - public function get_files() - { - // Aceptar solo POST (puedes cambiar a GET si lo necesitas) - if ($this->request->getMethod(true) !== 'POST') { - return $this->response->setStatusCode(405)->setJSON(['message' => 'Método no permitido']); - } - - $presupuesto_id = $this->request->getPost('presupuesto_id') ?? 0; - - $model = model('App\Models\Presupuestos\PresupuestoFicheroModel'); - $files = $model->getFiles($presupuesto_id); - - $result = []; - - foreach ($files as $file) { - $relativePath = $file->file_path; - $fullPath = WRITEPATH . ltrim($relativePath, '/'); - - $relativePath = $file->file_path; - $basename = basename($relativePath); // solo el nombre del archivo - - $result[] = (object) [ - 'name' => $file->nombre, - 'size' => file_exists(WRITEPATH . $relativePath) ? filesize(WRITEPATH . $relativePath) : 0, - 'hash' => $basename - ]; - } - - return $this->response->setJSON($result); - } - - - - public function upload_files() - { - $request = service('request'); - $model = model('App\Models\Presupuestos\PresupuestoFicheroModel'); - $files = $request->getFiles()['file'] ?? []; - $presupuesto_id = $request->getPost('presupuesto_id'); - $old_files = json_decode($request->getPost('oldFiles') ?? '[]'); - - if (!is_array($files)) { - $files = [$files]; - } - - // Instanciar servicio con dependencias inyectadas manualmente - $service = new \App\Services\PresupuestoUploaderService( - new \App\Libraries\SftpClientWrapper(config('PresupuestoSFTP')), - $model, - config('PresupuestoSFTP') - ); - - // Borrar antiguos del SFTP y de la BD (pero no del disco local) - $service->removeFromRemote($presupuesto_id); - $model->deleteMissingFiles($presupuesto_id, $old_files); - - $results = []; - foreach ($files as $file) { - if (!$file->isValid()) { - $results[] = ['name' => $file->getName(), 'status' => 'invalid']; - continue; - } - - $newName = $model->saveFileInBBDD( - $presupuesto_id, - $file->getClientName(), - $file->getClientExtension(), - auth()->id() - ); - - $uploadPath = WRITEPATH . 'uploads/presupuestos/' . $newName; - $file->move(dirname($uploadPath), basename($uploadPath)); - - $results[] = ['name' => $file->getClientName(), 'status' => 'uploaded']; - } - - // Subida al SFTP - $sftpResult = $service->uploadToRemote($presupuesto_id); - - // Preparar notificación - if (!$sftpResult['success']) { - return $this->response->setJSON([ - 'message' => 'Error al subir uno o más archivos al SFTP.', - 'details' => [ - 'local' => $results, - 'sftp' => $sftpResult['files'] - ] - ])->setStatusCode(500); - } - - return $this->response->setJSON([ - 'message' => 'Archivos subidos correctamente al sistema y al SFTP.', - 'details' => [ - 'local' => $results, - 'sftp' => $sftpResult['files'] - ] - ]); - } - - - - - - + /*********************** * @@ -3657,37 +3554,5 @@ class Presupuestocliente extends \App\Controllers\BaseResourceController return $servicios; } - - public function download_zip() - { - $presupuesto_id = $this->request->getPost('presupuesto_id'); - $ot_id = $this->request->getPost('ot_id'); - - if (!$presupuesto_id) { - return $this->response->setStatusCode(400)->setBody('Presupuesto ID requerido'); - } - - // Definir prefijo si se recibió un ot_id válido - $prefijo = (!empty($ot_id) && is_numeric($ot_id)) ? "OT_{$ot_id}" : null; - - $ftpClient = new \App\Libraries\SafekatFtpClient(); - try { - $zipPath = $ftpClient->downloadZipPresupuesto((int) $presupuesto_id, $prefijo); - - if ($zipPath === null || !file_exists($zipPath)) { - return $this->response->setStatusCode(404)->setBody('No se encontraron archivos'); - } - - $nombreArchivo = $prefijo - ? "{$prefijo}_PRESUPUESTO_{$presupuesto_id}.zip" - : "archivos_presupuesto_{$presupuesto_id}.zip"; - - return $this->response - ->download($zipPath, null) - ->setFileName($nombreArchivo); - } catch (\Throwable $e) { - log_message('error', $e->getMessage()); - return $this->response->setStatusCode(500)->setBody('Error interno'); - } - } + } diff --git a/ci4/app/Controllers/Sistema/Intranet.php b/ci4/app/Controllers/Sistema/Intranet.php index c01d9b96..aa3141d4 100755 --- a/ci4/app/Controllers/Sistema/Intranet.php +++ b/ci4/app/Controllers/Sistema/Intranet.php @@ -3,6 +3,7 @@ namespace App\Controllers\Sistema; use CodeIgniter\Controller; +use App\Models\Presupuestos\PresupuestoFicheroModel; class Intranet extends Controller { @@ -11,25 +12,24 @@ class Intranet extends Controller { helper('file'); - $resource_path = WRITEPATH . 'uploads/presupuestos/' . $resource_name; + $model = new PresupuestoFicheroModel(); + $file = $model->where('file_path LIKE', "%{$resource_name}")->first(); - if (file_exists($resource_path)) { - // Get the mime type of the file - $mime_type = mime_content_type($resource_path); - - // Get an instance of the Response class - $response = service('response'); - - // Set the content type - $response->setContentType($mime_type); - - // Set the output - $response->setBody(file_get_contents($resource_path)); - - // Send the response to the browser - $response->send(); + if (!$file) { + return service('response')->setStatusCode(404)->setBody("Archivo no encontrado"); } + $resource_path = WRITEPATH . $file->file_path; + + if (file_exists($resource_path)) { + $mime_type = mime_content_type($resource_path); + $response = service('response'); + $response->setContentType($mime_type); + $response->setBody(file_get_contents($resource_path)); + $response->send(); + } else { + return service('response')->setStatusCode(404)->setBody("Archivo no encontrado"); + } } function tickets($resource_name) @@ -54,7 +54,6 @@ class Intranet extends Controller // Send the response to the browser $response->send(); } - } function orden_trabajo($ot_id, $resource_name) { @@ -76,7 +75,6 @@ class Intranet extends Controller // Send the response to the browser $response->send(); } - } function catalogo($catalogo_id, $resource_name) @@ -99,7 +97,5 @@ class Intranet extends Controller // Send the response to the browser $response->send(); } - } - -} \ No newline at end of file +} diff --git a/ci4/app/Libraries/SftpClientWrapper.php b/ci4/app/Libraries/SftpClientWrapper.php index 42e12702..96897451 100644 --- a/ci4/app/Libraries/SftpClientWrapper.php +++ b/ci4/app/Libraries/SftpClientWrapper.php @@ -40,4 +40,8 @@ class SftpClientWrapper return $this->client->chmod($permissions, $path); } + public function get(string $remotePath, string $localPath): bool + { + return $this->client->get($remotePath, $localPath); + } } diff --git a/ci4/app/Models/Presupuestos/PresupuestoFicheroModel.php b/ci4/app/Models/Presupuestos/PresupuestoFicheroModel.php index 8ddae06d..9b8f6a2b 100755 --- a/ci4/app/Models/Presupuestos/PresupuestoFicheroModel.php +++ b/ci4/app/Models/Presupuestos/PresupuestoFicheroModel.php @@ -2,6 +2,8 @@ namespace App\Models\Presupuestos; +use Config\Paths; + class PresupuestoFicheroModel extends \App\Models\BaseModel { protected $table = "presupuesto_ficheros"; @@ -23,16 +25,42 @@ class PresupuestoFicheroModel extends \App\Models\BaseModel public static $labelField = "nombre"; + /** + * Devuelve la ruta relativa del archivo dentro de WRITEPATH. + * + * @param int $presupuesto_id + * @param string $filename + * @return string + */ + public function getRelativePath(int $presupuesto_id, string $filename): string + { + return config(Paths::class)->presupuestosPath . '/' . $presupuesto_id . '/' . $filename; + } + + /** + * Devuelve la ruta absoluta en el sistema de archivos del servidor. + * + * @param int $presupuesto_id + * @param string $filename + * @return string + */ + public function getAbsolutePath(int $presupuesto_id, string $filename): string + { + return WRITEPATH . $this->getRelativePath($presupuesto_id, $filename); + } + public function saveFileInBBDD($presupuesto_id, $filename, $extension, $user_id) { try { + $new_filename = $this->generateFileHash($filename) . '.' . $extension; + $relativePath = $this->getRelativePath($presupuesto_id, $new_filename); $this->db->table($this->table . " t1") ->set('presupuesto_id', $presupuesto_id) ->set('nombre', $filename) - ->set('file_path', 'uploads/presupuestos/' . $new_filename) + ->set('file_path', $relativePath) ->set('upload_by', $user_id) ->set('upload_at', date('Y-m-d H:i:s')) ->insert(); @@ -112,6 +140,7 @@ class PresupuestoFicheroModel extends \App\Models\BaseModel public function deleteMissingFiles(int $presupuesto_id, array $keepNames = []) { $files = $this->getFiles($presupuesto_id); + $deletedCount = 0; foreach ($files as $file) { if (!in_array($file->nombre, $keepNames)) { @@ -123,8 +152,12 @@ class PresupuestoFicheroModel extends \App\Models\BaseModel ->where('presupuesto_id', $presupuesto_id) ->where('nombre', $file->nombre) ->delete(); + + $deletedCount++; } } + + return $deletedCount; } diff --git a/ci4/app/Services/PresupuestoUploaderService.php b/ci4/app/Services/PresupuestoUploaderService.php index d8df6f21..04c4cfe7 100644 --- a/ci4/app/Services/PresupuestoUploaderService.php +++ b/ci4/app/Services/PresupuestoUploaderService.php @@ -3,7 +3,6 @@ namespace App\Services; use App\Models\Presupuestos\PresupuestoFicheroModel; -use App\Models\Pedidos\PedidoLineaModel; use App\Libraries\SftpClientWrapper; use Config\PresupuestoSFTP; @@ -15,9 +14,12 @@ class PresupuestoUploaderService protected PresupuestoSFTP $config ) {} + /** + * Sube todos los archivos asociados a un presupuesto al SFTP. + */ public function uploadToRemote(int $presupuestoId): array { - $remoteDir = "{$this->config->base_dir}/ficheros/" . ($presupuestoId + $this->config->id_offset); + $remoteDir = $this->config->getRemoteDirForPresupuesto($presupuestoId); if (!$this->ftp->exists($remoteDir)) { if (!$this->ftp->mkdir($remoteDir, true)) { @@ -33,10 +35,21 @@ class PresupuestoUploaderService $results = []; foreach ($files as $file) { - $remotePath = $remoteDir . '/' . basename($file->file_path); + $filename = basename($file->file_path); $localPath = WRITEPATH . $file->file_path; - $ok = $this->ftp->upload($localPath, $remotePath); + $remotePath = $remoteDir . '/' . $filename; + if (!file_exists($localPath)) { + $results[] = [ + 'file' => $file->nombre, + 'remotePath' => $remotePath, + 'success' => false, + 'error' => 'Archivo local no encontrado' + ]; + continue; + } + + $ok = $this->ftp->upload($localPath, $remotePath); $results[] = [ 'file' => $file->nombre, 'remotePath' => $remotePath, @@ -52,25 +65,149 @@ class PresupuestoUploaderService ]; } - public function removeFromRemote(int $presupuestoId): void + /** + * Elimina todos los archivos actuales del presupuesto del SFTP. + */ + public function removeFromRemote(int $presupuestoId): array { - $remoteDir = "{$this->config->base_dir}/pedidos_files/" . ($presupuestoId + $this->config->id_offset); + $remoteDir = $this->config->getRemoteDirForPresupuesto($presupuestoId); $files = $this->fileModel->getFiles($presupuestoId); + $results = []; + foreach ($files as $file) { - $this->ftp->delete($remoteDir . '/' . basename($file->file_path)); + $filename = basename($file->file_path); + $remotePath = $remoteDir . '/' . $filename; + + if ($this->ftp->exists($remotePath)) { + $deleted = $this->ftp->delete($remotePath); + $results[] = [ + 'file' => $file->nombre, + 'remotePath' => $remotePath, + 'success' => $deleted, + 'message' => $deleted ? 'Eliminado correctamente' : 'Falló al eliminar' + ]; + } else { + $results[] = [ + 'file' => $file->nombre, + 'remotePath' => $remotePath, + 'success' => false, + 'message' => 'Archivo no encontrado en el SFTP' + ]; + } } + + return $results; } + /** + * Elimina del SFTP los archivos que ya no existen en la lista permitida. + */ public function removeMissingFromRemote(int $presupuestoId, array $keepFileNames): void { - $remoteDir = "{$this->config->base_dir}/pedidos_files/" . ($presupuestoId + $this->config->id_offset); + $remoteDir = $this->config->getRemoteDirForPresupuesto($presupuestoId); $files = $this->fileModel->getFiles($presupuestoId); foreach ($files as $file) { if (!in_array($file->nombre, $keepFileNames)) { - $this->ftp->delete($remoteDir . '/' . basename($file->file_path)); + $remotePath = $remoteDir . '/' . basename($file->file_path); + if ($this->ftp->exists($remotePath)) { + $this->ftp->delete($remotePath); + } } } } + + + /** + * Descarga archivos de SFTP y genera un ZIP temporal para el presupuesto dado. + * + * @param int $presupuestoId ID del presupuesto. + * @param string|null $prefijo Prefijo para los nombres de los archivos. + * @return array Estructura: ['success' => bool, 'message' => string, 'zipPath' => string|null] + */ + public function downloadZip(int $presupuestoId, ?string $prefijo = null): array + { + $files = $this->fileModel->getFiles($presupuestoId); + if (empty($files)) { + return [ + 'success' => false, + 'message' => "No hay archivos en la base de datos para el presupuesto ID {$presupuestoId}.", + 'zipPath' => null + ]; + } + + $remoteDir = $this->config->getRemoteDirForPresupuesto($presupuestoId); + if (!$this->ftp->exists($remoteDir)) { + return [ + 'success' => false, + 'message' => "El directorio remoto no existe: {$remoteDir}", + 'zipPath' => null + ]; + } + + $localTempDir = WRITEPATH . 'zip_presupuestos/' . uniqid("presupuesto_"); + if (!is_dir($localTempDir) && !mkdir($localTempDir, 0777, true)) { + return [ + 'success' => false, + 'message' => "No se pudo crear el directorio temporal en: {$localTempDir}", + 'zipPath' => null + ]; + } + + $erroresDescarga = []; + foreach ($files as $file) { + $originalName = $file->nombre ?? basename($file->file_path ?? ''); + $prefixedName = $prefijo ? $prefijo . '_' . $originalName : $originalName; + + $localFile = $localTempDir . '/' . $prefixedName; + $remoteFile = $remoteDir . '/' . basename($file->file_path ?? ''); + + if (!$this->ftp->get($remoteFile, $localFile)) { + $erroresDescarga[] = "Error al descargar: {$remoteFile}"; + } + } + + if (count($erroresDescarga) === count($files)) { + return [ + 'success' => false, + 'message' => "Fallo al descargar todos los archivos:\n" . implode("\n", $erroresDescarga), + 'zipPath' => null + ]; + } + + $zipPath = $localTempDir . '.zip'; + $zip = new \ZipArchive(); + if (!$zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE)) { + return [ + 'success' => false, + 'message' => "No se pudo crear el archivo ZIP: {$zipPath}", + 'zipPath' => null + ]; + } + + foreach (glob($localTempDir . '/*') as $localFile) { + $zip->addFile($localFile, basename($localFile)); + } + $zip->close(); + + foreach (glob($localTempDir . '/*') as $localFile) { + unlink($localFile); + } + rmdir($localTempDir); + + if (!file_exists($zipPath)) { + return [ + 'success' => false, + 'message' => "El ZIP no fue generado correctamente.", + 'zipPath' => null + ]; + } + + return [ + 'success' => true, + 'message' => "ZIP generado correctamente.", + 'zipPath' => $zipPath + ]; + } } diff --git a/ci4/writable/storage/index.html b/ci4/writable/storage/index.html new file mode 100644 index 00000000..b702fbc3 --- /dev/null +++ b/ci4/writable/storage/index.html @@ -0,0 +1,11 @@ + + +
+Directory access is forbidden.
+ + + diff --git a/httpdocs/assets/js/safekat/components/alerts/sweetAlert.js b/httpdocs/assets/js/safekat/components/alerts/sweetAlert.js index 40d15a0a..68230f62 100644 --- a/httpdocs/assets/js/safekat/components/alerts/sweetAlert.js +++ b/httpdocs/assets/js/safekat/components/alerts/sweetAlert.js @@ -130,4 +130,19 @@ export const alertWarning = (value, target = 'body',options = {}) => { timerProgressBar: true, ...options }) -} \ No newline at end of file +} + +export const alertFileUploadSuccess = (message = "Archivos subidos correctamente.") => { + return Swal.fire({ + icon: "success", + title: "Carga finalizada", + text: message, + showConfirmButton: false, + timer: 3000, + customClass: { + popup: 'p-4', + title: 'fs-4 fw-bold text-success', + htmlContainer: 'text-muted', + } + }); +}; diff --git a/httpdocs/assets/js/safekat/components/forms/fileUploadDropzone.js b/httpdocs/assets/js/safekat/components/forms/fileUploadDropzone.js index 792134ae..94d7cd07 100644 --- a/httpdocs/assets/js/safekat/components/forms/fileUploadDropzone.js +++ b/httpdocs/assets/js/safekat/components/forms/fileUploadDropzone.js @@ -1,6 +1,6 @@ // Importación de utilidades AJAX y alertas personalizadas import Ajax from '../ajax.js'; -import { alertSuccessMessage, alertWarningMessage } from '../alerts/sweetAlert.js' +import { alertFileUploadSuccess, alertWarningMessage } from '../alerts/sweetAlert.js' // Template HTML para la vista previa de cada archivo en Dropzone const PREVIEW_TEMPLATE = ` @@ -142,11 +142,42 @@ class FileUploadDropzone { // Éxito tras subir archivos _handleUploadFilesSuccess(response) { - this.dropZoneClean(); // Limpia visualmente - this._handleGetFiles(); // Recarga archivos desde backend - alertSuccessMessage(response?.message ?? "Archivos subidos correctamente"); + this.dropZoneClean(); + this._handleGetFiles(); + + const summary = response?.summary || {}; + const numOk = summary.subidos_ok || 0; + const numErrLocal = summary.errores_locales || 0; + const numErrRemote = summary.errores_remotos || 0; + const numDeleted = summary.borrados || 0; + + const partes = []; + + if (numOk > 0) { + partes.push(`Se subió${numOk === 1 ? '' : 'ron'} ${numOk} archivo${numOk === 1 ? '' : 's'} correctamente.`); + } + + if (numDeleted > 0) { + partes.push(`Se eliminaron ${numDeleted} archivo${numDeleted === 1 ? '' : 's'} obsoleto${numDeleted === 1 ? '' : 's'}.`); + } + + if (numErrLocal > 0) { + partes.push(`${numErrLocal} archivo${numErrLocal === 1 ? '' : 's'} falló${numErrLocal === 1 ? '' : 'n'} en el sistema.`); + } + + if (numErrRemote > 0) { + partes.push(`${numErrRemote} fallo${numErrRemote === 1 ? '' : 's'} en la transferencia.`); + } + + const mensaje = partes.length > 0 + ? partes.join(' ') + : response?.message ?? "Archivos actualizados correctamente."; + + alertFileUploadSuccess(mensaje); } + + _handleUploadFilesError(errors) { // No implementado aún } @@ -185,7 +216,7 @@ class FileUploadDropzone { $("#loader").modal('show'); $.ajax({ - url: `/presupuestoadmin/download_zip`, + url: `/files/download_zip`, type: 'POST', data: { [this.nameId]: this.modelId, @@ -225,7 +256,7 @@ class FileUploadDropzone { // Carga archivos simulados (mock) al Dropzone visual dropZoneUpdateFiles(files) { files.forEach(file => { - console.log("Iterando archivo:", file.name); + //console.log("Iterando archivo:", file.name); this.dropZoneAddFile(file); }); } diff --git a/httpdocs/assets/js/safekat/pages/presupuestoAdmin/presupuestoAdminEdit.js b/httpdocs/assets/js/safekat/pages/presupuestoAdmin/presupuestoAdminEdit.js index b36b7843..76f632ce 100644 --- a/httpdocs/assets/js/safekat/pages/presupuestoAdmin/presupuestoAdminEdit.js +++ b/httpdocs/assets/js/safekat/pages/presupuestoAdmin/presupuestoAdminEdit.js @@ -65,8 +65,8 @@ class PresupuestoAdminEdit { this.configUploadDropzone = { domElement: '#dropzone-presupuesto-admin-files', nameId: "presupuesto_id", - getUri: '/presupuestos/presupuestocliente/get_files', - postUri: '/presupuestos/presupuestocliente/upload_files' + getUri: '/files/get_files', + postUri: '/files/upload_files' } if ($(this.configUploadDropzone.domElement).length > 0) { this.fileUploadDropzone = new FileUploadDropzone(this.configUploadDropzone) diff --git a/httpdocs/assets/js/safekat/pages/presupuestoCliente/resumen.js b/httpdocs/assets/js/safekat/pages/presupuestoCliente/resumen.js index aab86616..1253f0df 100644 --- a/httpdocs/assets/js/safekat/pages/presupuestoCliente/resumen.js +++ b/httpdocs/assets/js/safekat/pages/presupuestoCliente/resumen.js @@ -115,7 +115,7 @@ class Resumen { $('#loader').show(); $.ajax({ - url: "/presupuestos/presupuestocliente/get_files", + url: "/files/get_files", type: 'POST', data: { presupuesto_id: id } @@ -192,7 +192,7 @@ class Resumen { $('#loader').show(); $.ajax({ - url: "/presupuestocliente/download_zip", + url: "/files/download_zip", type: 'POST', data: { presupuesto_id: this.presupuesto_id }, xhrFields: { diff --git a/httpdocs/assets/js/safekat/pages/produccion/ot.js b/httpdocs/assets/js/safekat/pages/produccion/ot.js index b4909d85..1f057e1d 100644 --- a/httpdocs/assets/js/safekat/pages/produccion/ot.js +++ b/httpdocs/assets/js/safekat/pages/produccion/ot.js @@ -72,8 +72,8 @@ class OrdenTrabajo { domElement: '#dropzone-ot-files', nameId: "presupuesto_id", otId: this.otId, - getUri: '/presupuestos/presupuestocliente/get_files', - postUri: '/presupuestos/presupuestocliente/upload_files' + getUri: '/files/get_files', + postUri: '/files/upload_files' } if ($(this.configUploadDropzone.domElement).length > 0) { this.fileUploadDropzone = new FileUploadDropzone(this.configUploadDropzone)