Merge branch 'main' into mod/presupuesto_cliente

This commit is contained in:
2025-06-12 15:32:26 +02:00
14 changed files with 840 additions and 65 deletions

View File

@ -9,7 +9,7 @@ use App\Models\Catalogo\IdentificadorIsknModel;
class CatalogoLibroAsignarIskn extends BaseCommand
{
protected $group = 'custom';
protected $group = 'Safekat';
protected $name = 'catalogo:libro-asignar-iskn';
protected $description = 'Asigna ISKN directamente en la base de datos a los libros que no lo tienen.';

View File

@ -7,7 +7,7 @@ use CodeIgniter\CLI\CLI;
class CatalogoLibroImportar extends BaseCommand
{
protected $group = 'custom';
protected $group = 'Safekat';
protected $name = 'catalogo:libro-importar';
protected $description = 'Importa los registros de catalogo_libro a catalogo_libros para un customer_id dado';

View File

@ -0,0 +1,93 @@
<?php
namespace App\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use ZipArchive;
class RestoreBackup extends BaseCommand
{
protected $group = 'Safekat';
protected $name = 'restore:backup';
protected $description = 'Restaura un backup desde un archivo .zip en writable/backups/.';
protected $usage = 'restore:backup [--dry-run]';
protected $options = [
'--dry-run' => 'Simula el proceso de restauración sin ejecutarlo realmente.',
];
public function run(array $params)
{
$isDryRun = CLI::getOption('dry-run');
$backupDir = WRITEPATH . 'backups/';
$backups = glob($backupDir . '*.zip');
if (empty($backups)) {
CLI::error("No se encontraron backups .zip en: $backupDir");
return;
}
CLI::write("Backups disponibles:", 'blue');
foreach ($backups as $i => $file) {
CLI::write("[" . ($i + 1) . "] " . basename($file));
}
$index = CLI::prompt("Selecciona el número del backup a restaurar", null, 'required');
if (!is_numeric($index) || $index < 1 || $index > count($backups)) {
CLI::error("Selección no válida.");
return;
}
$selectedFile = $backups[$index - 1];
CLI::write("🎯 Seleccionado: " . basename($selectedFile), 'cyan');
if ($isDryRun) {
CLI::write("🔍 Modo simulación activado (--dry-run)", 'yellow');
}
$zip = new ZipArchive();
if ($zip->open($selectedFile) !== TRUE) {
CLI::error("No se pudo abrir el archivo ZIP.");
return;
}
$extractPath = WRITEPATH . 'backups/tmp_restore/';
if (!is_dir($extractPath)) {
mkdir($extractPath, 0775, true);
}
$zip->extractTo($extractPath);
$zip->close();
$sqlFiles = glob($extractPath . '*.sql');
if (count($sqlFiles) === 0) {
CLI::error("No se encontró ningún .sql dentro del backup.");
return;
}
$sqlFile = escapeshellarg($sqlFiles[0]);
$db = config('Database')->default;
$cmd = "mysql -h {$db['hostname']} -u {$db['username']} -p'{$db['password']}' {$db['database']} < {$sqlFile}";
if ($isDryRun) {
CLI::write("📋 Comando que se ejecutaría:", 'yellow');
CLI::write($cmd, 'light_gray');
CLI::write("✅ Simulación completa. No se hizo ninguna modificación.", 'green');
} else {
CLI::write("⏳ Restaurando...", 'yellow');
system($cmd, $retval);
if ($retval !== 0) {
CLI::error("❌ Error al restaurar la base de datos (código $retval).");
} else {
CLI::write("✅ Backup restaurado correctamente.", 'green');
}
}
array_map('unlink', glob($extractPath . '*'));
rmdir($extractPath);
}
}

View File

@ -20,12 +20,6 @@ $routes->get('viewmode/(:alpha)', 'Viewmode::index/$1');
$routes->get('test', 'Test::index');
$routes->group('activity', ['namespace' => 'App\Controllers\Sistema'], function ($routes) {
$routes->get('', 'Actividad::index', ['as' => 'activityList']);
$routes->post('datatable', 'Actividad::datatable', ['as' => 'activityDT']);
});
/*
* --------------------------------------------------------------------
* Route Definitions

View File

@ -0,0 +1,35 @@
<?php
use CodeIgniter\Router\RouteCollection;
/** @var RouteCollection $routes */
/* Rutas para tarifas */
$routes->group('sistema', ['namespace' => 'App\Controllers\Sistema'], function ($routes) {
/* Actividad */
$routes->group('actividad', ['namespace' => 'App\Controllers\Sistema'], function ($routes) {
/**======================
* CRUD
*========================**/
$routes->get('', 'Actividad::index', ['as' => 'activityList']);
$routes->post('datatable', 'Actividad::datatable', ['as' => 'activityDT']);
});
/* Backups */
$routes->group('backups', function ($routes) {
/**======================
* Tool
*========================**/
$routes->get('', 'Backups::index', ['as' => 'backupsList']);
$routes->get('create', 'Backups::create', ['as' => 'backupsCreate']);
$routes->get('create-dev', 'Backups::createDevelopment', ['as' => 'backupsCreateDev']);
$routes->get('backups/download/(:segment)', 'Backups::download/$1', ['as' => 'backupsDownload']);
$routes->get('delete-local/(:num)', 'Backups::deleteLocal/$1', ['as' => 'backupsDeleteLocal']);
$routes->get('delete-local-dev/(:segment)', 'Backups::deleteLocalDevelopment/$1', ['as' => 'backupsDeleteLocalDev']);
$routes->get('restore/(:segment)/local', 'Backups::restoreLocal/$1', ['as' => 'backupsRestoreLocal']);
$routes->get('restore/(:segment)/remote', 'Backups::restoreRemote/$1', ['as' => 'backupsRestoreRemote']);
});
});

View File

@ -0,0 +1,494 @@
<?php
namespace App\Controllers\Sistema;
use App\Controllers\BaseController;
use App\Models\Sistema\BackupModel;
use phpseclib3\Net\SFTP;
use ZipArchive;
class Backups extends BaseController
{
protected $backupModel;
public function __construct()
{
$this->backupModel = new BackupModel();
}
public function index()
{
helper('filesystem');
$entorno = getenv('SK_ENVIRONMENT');
$backups = [];
if ($entorno === 'development') {
// Leer archivos directamente del disco en entorno de desarrollo
$backups = [];
// === 1. Backups locales ===
$localDir = WRITEPATH . 'backups/';
$localFiles = get_filenames($localDir);
foreach ($localFiles as $file) {
if (!str_ends_with($file, '.zip') || !str_starts_with($file, 'backup_dev_')) {
continue;
}
$localPath = $localDir . $file;
$fecha = date('Y-m-d H:i', filemtime($localPath));
$tamano = filesize($localPath);
$tamanoFmt = $tamano > 1048576
? number_format($tamano / 1048576, 2) . ' MB'
: number_format($tamano / 1024, 2) . ' KB';
$backups[$file] = [
'id' => null,
'filename' => $file,
'fecha' => $fecha,
'tamano' => $tamanoFmt,
'local' => true,
'remoto' => false,
];
}
// === 2. Backups remotos en SFTP ===
$sftpHost = getenv('HIDRIVE_HOST');
$sftpUser = getenv('HIDRIVE_USER');
$sftpPass = getenv('HIDRIVE_PASS');
$remoteDir = '/users/erp2019/backups_erp/';
$sftp = new SFTP($sftpHost);
if ($sftp->login($sftpUser, $sftpPass)) {
$remoteFiles = $sftp->nlist($remoteDir);
foreach ($remoteFiles as $file) {
if (!str_ends_with($file, '.zip') || !str_starts_with($file, 'backup_')) {
continue;
}
// Verificar si ya se cargó como local
$alreadyLocal = isset($backups[$file]);
// Obtener info de archivo remoto
$stat = $sftp->stat($remoteDir . $file);
$fecha = isset($stat['mtime']) ? date('Y-m-d H:i', $stat['mtime']) : '-';
$tamano = $stat['size'] ?? null;
$tamanoFmt = $tamano > 1048576
? number_format($tamano / 1048576, 2) . ' MB'
: number_format($tamano / 1024, 2) . ' KB';
$backups[$file] = [
'id' => null,
'filename' => $file,
'fecha' => $fecha,
'tamano' => $tamanoFmt,
'local' => $alreadyLocal,
'remoto' => true,
];
}
} else {
// Opcional: mostrar un error o dejarlo pasar silenciosamente
session()->setFlashdata('warning', 'No se pudo conectar al servidor SFTP para obtener backups remotos.');
}
// Convertir a lista (por si se usó índice por filename)
$backups = array_values($backups);
} else {
// En producción: seguir usando la base de datos
$entries = $this->backupModel->orderBy('created_at', 'DESC')->findAll();
foreach ($entries as $entry) {
$file = $entry['filename'];
if (!str_ends_with($file, '.zip')) {
continue;
}
$localPath = $entry['path_local'];
$remotePath = $entry['path_remote'];
$isLocal = $localPath && file_exists($localPath);
$isRemote = !empty($remotePath);
if ($isLocal) {
$fecha = date('Y-m-d H:i', filemtime($localPath));
$tamano = filesize($localPath);
$tamanoFmt = $tamano > 1048576
? number_format($tamano / 1048576, 2) . ' MB'
: number_format($tamano / 1024, 2) . ' KB';
if ($entry['size'] != $tamano) {
$this->backupModel->update($entry['id'], [
'size' => $tamano,
]);
}
} else {
$fecha = $entry['created_at'] ?? '-';
$tamano = $entry['size'] ?? null;
$tamanoFmt = $tamano > 1048576
? number_format($tamano / 1048576, 2) . ' MB'
: number_format($tamano / 1024, 2) . ' KB';
}
$backups[] = [
'id' => $entry['id'],
'filename' => $file,
'fecha' => $fecha,
'tamano' => $tamanoFmt,
'local' => $isLocal,
'remoto' => $isRemote,
];
}
}
return view('themes/vuexy/form/backups/backupList', ['backups' => $backups]);
}
public function create()
{
if (getenv('SK_ENVIRONMENT') !== 'production') {
return redirect()->to(route_to('backupsList'))->with('error', 'No permitido en entorno de desarrollo.');
}
helper('filesystem');
$timestamp = date('Ymd_His');
$sqlFilename = "backup_{$timestamp}.sql";
$zipFilename = "backup_{$timestamp}.zip";
$sqlPath = WRITEPATH . 'backups/' . $sqlFilename;
$zipPath = WRITEPATH . 'backups/' . $zipFilename;
$dbConfig = config('Database')->default;
$host = $dbConfig['hostname'];
$username = $dbConfig['username'];
$password = $dbConfig['password'];
$database = $dbConfig['database'];
$command = "mysqldump -h {$host} -u" . escapeshellarg($username) . " -p'" . addslashes($password) . "' {$database} > {$sqlPath}";
system($command, $retval);
if ($retval !== 0) {
throw new \RuntimeException("Error al crear el backup.");
}
// Crear el zip
$zip = new ZipArchive();
if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
$zip->addFile($sqlPath, $sqlFilename);
$zip->close();
unlink($sqlPath); // eliminar el .sql original
} else {
throw new \RuntimeException("Error al comprimir el backup.");
}
// Insertar en BD
$backupId = $this->backupModel->insert([
'filename' => $zipFilename,
'type' => 'manual',
'path_local' => $zipPath,
'path_remote' => null,
'size' => filesize($zipPath),
'status' => 'pendiente',
'created_at' => date('Y-m-d H:i:s')
], true);
// Enviar a SFTP
$this->sendToSFTP($zipPath, $zipFilename);
// Actualizar BD
$remotePath = "/users/erp2019/backups_erp/" . $zipFilename;
$this->backupModel->update($backupId, [
'path_remote' => $remotePath,
'status' => 'subido'
]);
return redirect()->to(route_to('backupsList'))->with('message', 'Backup del entorno de produccion creado, comprimido y enviado al remoto.');
}
public function createDevelopment()
{
if (getenv('SK_ENVIRONMENT') !== 'development') {
return redirect()->to(route_to('backupsList'))->with('error', 'Esta acción solo está permitida en desarrollo.');
}
helper('filesystem');
$timestamp = date('Ymd_His');
$sqlFilename = "backup_dev_{$timestamp}.sql";
$zipFilename = "backup_dev_{$timestamp}.zip";
$sqlPath = WRITEPATH . 'backups/' . $sqlFilename;
$zipPath = WRITEPATH . 'backups/' . $zipFilename;
$dbConfig = config('Database')->default;
$host = $dbConfig['hostname'];
$username = $dbConfig['username'];
$password = $dbConfig['password'];
$database = $dbConfig['database'];
$command = "mysqldump -h {$host} -u{$username} -p'{$password}' {$database} > {$sqlPath}";
system($command, $retval);
if ($retval !== 0) {
throw new \RuntimeException("Error al crear el backup local.");
}
$zip = new \ZipArchive();
if ($zip->open($zipPath, \ZipArchive::CREATE) === TRUE) {
$zip->addFile($sqlPath, $sqlFilename);
$zip->close();
unlink($sqlPath);
} else {
throw new \RuntimeException("Error al comprimir el backup local.");
}
// Ya no insertamos en la base de datos; el archivo queda en disco y se listará dinámicamente
return redirect()->to(route_to('backupsList'))->with('message', 'Backup local del entorno de desarrollo creado.');
}
public function download($filename)
{
helper('filesystem');
$entorno = getenv('SK_ENVIRONMENT');
$backup = $this->backupModel->where('filename', $filename)->first();
// 1. Si hay entrada en la base de datos
if ($backup) {
$localPath = $backup['path_local'];
$remotePath = $backup['path_remote'];
if ($localPath && file_exists($localPath)) {
return $this->response->download($localPath, null)->setFileName($filename);
}
if (!empty($remotePath)) {
$sftpHost = getenv('HIDRIVE_HOST');
$sftpUser = getenv('HIDRIVE_USER');
$sftpPass = getenv('HIDRIVE_PASS');
$sftp = new SFTP($sftpHost);
if (!$sftp->login($sftpUser, $sftpPass)) {
return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo conectar al servidor SFTP.');
}
$fileContents = $sftp->get($remotePath);
if ($fileContents === false) {
return redirect()->to(route_to('backupsList'))->with('error', 'Error al descargar desde SFTP.');
}
$newLocalPath = WRITEPATH . 'backups/' . $filename;
write_file($newLocalPath, $fileContents);
// Omitimos update() si estamos en desarrollo sin base de datos
if ($entorno === 'production') {
$this->backupModel->update($backup['id'], ['path_local' => $newLocalPath]);
}
return $this->response->download($newLocalPath, null)->setFileName($filename);
}
return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo encontrar el archivo ni local ni remoto.');
}
// 2. Si NO hay entrada en la BD y estamos en desarrollo
if ($entorno === 'development') {
$localPath = WRITEPATH . 'backups/' . $filename;
if (file_exists($localPath)) {
return $this->response->download($localPath, null)->setFileName($filename);
}
// También se puede intentar buscar en el SFTP si quieres
$sftpHost = getenv('HIDRIVE_HOST');
$sftpUser = getenv('HIDRIVE_USER');
$sftpPass = getenv('HIDRIVE_PASS');
$remotePath = '/users/erp2019/backups_erp/' . $filename;
$sftp = new SFTP($sftpHost);
if ($sftp->login($sftpUser, $sftpPass)) {
$fileContents = $sftp->get($remotePath);
if ($fileContents !== false) {
$newLocalPath = WRITEPATH . 'backups/' . $filename;
write_file($newLocalPath, $fileContents);
return $this->response->download($newLocalPath, null)->setFileName($filename);
}
}
return redirect()->to(route_to('backupsList'))->with('error', 'Archivo no encontrado local ni remoto (sin base de datos).');
}
return redirect()->to(route_to('backupsList'))->with('error', 'Backup no encontrado.');
}
public function deleteLocal($id)
{
$entorno = getenv('SK_ENVIRONMENT');
$backup = $this->backupModel->find($id);
if (!$backup) {
return redirect()->to(route_to('backupsList'))->with('error', 'Backup no encontrado en la base de datos.');
}
$localPath = $backup['path_local'];
// Si existe el archivo, intentamos borrarlo
if ($localPath && file_exists($localPath)) {
unlink($localPath);
}
if ($entorno === 'production') {
// Solo desvincular el archivo local
$this->backupModel->update($id, ['path_local' => null]);
return redirect()->to(route_to('backupsList'))->with('message', 'Archivo local eliminado (registro conservado).');
} else {
// Eliminar completamente en desarrollo
$this->backupModel->delete($id);
return redirect()->to(route_to('backupsList'))->with('message', 'Backup de desarrollo eliminado completamente.');
}
}
public function deleteLocalDevelopment($filename)
{
$entorno = getenv('SK_ENVIRONMENT');
if ($entorno !== 'development') {
return redirect()->to(route_to('backupsList'))->with('error', 'Solo permitido en desarrollo.');
}
$path = WRITEPATH . 'backups/' . $filename;
if (file_exists($path)) {
unlink($path);
return redirect()->to(route_to('backupsList'))->with('message', "Archivo '$filename' eliminado del sistema de archivos.");
}
return redirect()->to(route_to('backupsList'))->with('error', "Archivo '$filename' no encontrado en el sistema.");
}
public function restoreLocal($file)
{
$path = WRITEPATH . 'backups/' . $file;
if (!file_exists($path)) {
throw new \CodeIgniter\Exceptions\PageNotFoundException("Backup no encontrado.");
}
$zip = new \ZipArchive();
if ($zip->open($path) === TRUE) {
$extractPath = WRITEPATH . 'backups/tmp_restore/';
if (!is_dir($extractPath)) {
mkdir($extractPath, 0775, true);
}
$zip->extractTo($extractPath);
$zip->close();
$sqlFiles = glob($extractPath . '*.sql');
if (count($sqlFiles) === 0) {
throw new \RuntimeException("No se encontró ningún archivo .sql en el ZIP");
}
$sqlFile = $sqlFiles[0];
$dbConfig = config('Database')->default;
$host = $dbConfig['hostname'];
$username = $dbConfig['username'];
$password = $dbConfig['password'];
$database = $dbConfig['database'];
$cmd = "mysql -h {$host} -u{$username} -p'{$password}' {$database} < {$sqlFile}";
system($cmd, $retval);
if ($retval !== 0) {
throw new \RuntimeException("Error al restaurar la base de datos. Código: $retval");
}
array_map('unlink', glob($extractPath . '*'));
rmdir($extractPath);
return redirect()->to(route_to('backupsList'))->with('message', 'Backup restaurado correctamente (vía sistema).');
} else {
throw new \RuntimeException("No se pudo abrir el archivo ZIP");
}
}
public function restoreRemote($filename)
{
helper('filesystem');
// Buscar el backup en la base de datos
$backup = $this->backupModel->where('filename', $filename)->first();
if (!$backup || empty($backup['path_remote'])) {
return redirect()->to(route_to('backupsList'))->with('error', 'Backup remoto no encontrado en la base de datos.');
}
// Parámetros SFTP
$sftpHost = getenv('HIDRIVE_HOST');
$sftpUser = getenv('HIDRIVE_USER');
$sftpPass = getenv('HIDRIVE_PASS');
$remotePath = $backup['path_remote'];
$localPath = WRITEPATH . 'backups/' . $filename;
// Conectar al SFTP
$sftp = new SFTP($sftpHost);
if (!$sftp->login($sftpUser, $sftpPass)) {
return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo autenticar en el servidor SFTP.');
}
// Descargar el archivo
$fileContents = $sftp->get($remotePath);
if ($fileContents === false) {
return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo descargar el archivo remoto.');
}
// Guardar localmente
if (write_file($localPath, $fileContents) === false) {
return redirect()->to(route_to('backupsList'))->with('error', 'No se pudo guardar el archivo localmente.');
}
// Actualizar la base de datos para marcar el archivo como local
$this->backupModel->update($backup['id'], [
'path_local' => $localPath,
]);
// Restaurar usando el método local
return $this->restoreLocal($filename);
}
private function sendToSFTP($localPath, $remoteFilename)
{
$sftpHost = getenv('HIDRIVE_HOST');
$sftpUser = getenv('HIDRIVE_USER');
$sftpPass = getenv('HIDRIVE_PASS');
$remotePath = '/users/erp2019/backups_erp/' . $remoteFilename;
$sftp = new SFTP($sftpHost);
if (!$sftp->login($sftpUser, $sftpPass)) {
throw new \RuntimeException('Error de autenticación SFTP');
}
$fileContents = file_get_contents($localPath);
if (!$sftp->put($remotePath, $fileContents)) {
throw new \RuntimeException("No se pudo subir el backup al servidor SFTP.");
}
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateBackupsTable extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true
],
'filename' => ['type' => 'VARCHAR', 'constraint' => 255],
'type' => ['type' => 'ENUM', 'constraint' => ['manual', 'cron']],
'path_local' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
'path_remote' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
'size' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'status' => ['type' => 'ENUM', 'constraint' => ['pendiente', 'subido', 'error'], 'default' => 'pendiente'],
'created_at' => ['type' => 'DATETIME', 'null' => false],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->createTable('backups');
}
public function down()
{
$this->forge->dropTable('backups');
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateRestoresTable extends Migration
{
public function up()
{
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'backup_id' => ['type' => 'INT', 'unsigned' => true],
'restored_by' => ['type' => 'VARCHAR', 'constraint' => 100],
'source' => ['type' => 'ENUM', 'constraint' => ['local', 'remote']],
'restored_at' => ['type' => 'DATETIME'],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('backup_id', 'backups', 'id', 'CASCADE', 'CASCADE');
$this->forge->createTable('backup_restores');
}
public function down()
{
$this->forge->dropTable('backup_restores');
}
}

View File

@ -316,59 +316,7 @@ return [
"group_rules_title_r" => "¡El campo del nombre del grupo es obligatorio!",
"group_rules_dashboard_r" => "¡El campo del panel es obligatorio!",
// GROUP - Rules Name
/* JJO
"group_rules_label_group" => "Permiso de grupo",
"group_rules_label_user" => "Usuario",
"group_rules_label_settings" => "Configuración",
"group_rules_label_index" => "Lista",
"group_rules_label_add" => "Agregar",
"group_rules_label_edit" => "Editar",
"group_rules_label_delete" => "Eliminar",
"group_rules_label_store" => "Guardar",
"group_rules_label_oauth" => "Autenticaciones",
"group_rules_label_template" => "Plantillas",
"group_rules_label_all" => "Ver todo",
"group_rules_label_my" => "Mis notificaciones",
"group_rules_label_view" => "Ver notificación",
"group_rules_label_oauth_store" => "Guardar oAuth",
"group_rules_label_template_store" => "Guardar plantillas",
*/
// AUTH - index
"oauth_title" => "Autenticación oAuth",
"oauth_subtitle" => "Configuración de autenticación de redes sociales",
"oauth_label_id" => "ID de la Cuenta",
"oauth_label_id_ph" => "Escriba su id de la cuenta",
"oauth_label_key" => "Key de la Cuenta",
"oauth_label_key_ph" => "Escriba su key de la cuenta",
"oauth_label_secret" => "Llave Secreta",
"oauth_label_secret_ph" => "Escriba su llave secreta",
"oauth_label_view" => "Mostrar texto",
"oauth_label_active" => "Activar red social",
"oauth_alert_add" => "¡Guardado exitosamente!",
"oauth_alert_error" => "¡Error al guardar!",
// TEMPLATE - index
"template_title" => "Plantillas",
"template_subtitle" => "Configuración de Plantilla",
"template_subtitle_email" => "Plantillas de Correo Electrónico",
"template_label_title" => "Título",
"template_label_title_ph" => "Escriba su título",
"template_label_message" => "Mensaje",
"template_label_forgot_pass" => "Olvido la contraseña",
"template_label_welcome" => "Bienvenida",
"template_label_tfa" => "Autenticación de dos factores (2FA)",
"template_label_tag" => "Ver palabras clave",
"template_alert_add" => "¡Guardado exitosamente!",
"template_alert_error" => "¡Error al guardar!",
"template_modal_title" => "Palabras Clave",
"template_modal_subtitle" => "A continuación, se muestran algunas palabras clave que se pueden incorporar al texto:",
"template_modal_btn_1" => "Cerrar",
"template_modal_copy" => "Copiado!",
"template_modal_copy_msg" => "¡Copiado con éxito!",
"template_label_confirmation_email" => "Confirmación por correo electrónico",
"template_label_notification" => "Notificación de cuentas nuevas",
// SETTINGS - index
"settings_title" => "Configuración",
@ -634,7 +582,6 @@ return [
"permisos_tarifaenvio" => "Envío",
"permisos_tarifaimpresion" => "Impresión",
"permisos_configuracion" => "Configuración",
"permisos_tareasservicio" => "Tareas servicio",
"permisos_formaspago" => "Formas de pago",
"permisos_papelgenerico" => "Papel genérico",
@ -827,11 +774,10 @@ return [
"menu_logout" => "Salir",
"menu_profile" => "Mi Perfil",
"menu_activity" => "Actividad",
"menu_backups" => "Backups",
"menu_notification" => "Notificaciones",
"menu_list" => "Lista",
"menu_add" => "Agregar",
"menu_oauth" => "Autenticaciones",
"menu_template" => "Plantillas",
"menu_soporte" => "Soporte",
"menu_soporte_new_ticket" => "Crear ticket",

View File

@ -62,6 +62,7 @@ return [
'seriesFacturasSection' => 'Series facturas',
'ajustesSection' => 'Ajustes',
'actividadSection' => 'Accesos',
'backupSection' => 'Backups',
'facturasSection' => 'Facturas',
'logisticaSection' => 'Logística',
'albaranesPermission' => 'Albaranes',

View File

@ -0,0 +1,15 @@
<?php
namespace App\Models\Sistema;
use App\Models\BaseModel;
class BackupModel extends BaseModel
{
protected $table = 'backups';
protected $primaryKey = 'id';
protected $allowedFields = [
'filename', 'type', 'path_local', 'path_remote', 'size', 'status', 'created_at', 'updated_at'
];
protected $useTimestamps = true;
}

View File

@ -0,0 +1,111 @@
<?= $this->include("themes/_commonPartialsBs/select2bs5") ?>
<?= $this->include("themes/_commonPartialsBs/datatables") ?>
<?= $this->include("themes/_commonPartialsBs/sweetalert") ?>
<?= $this->extend('themes/vuexy/main/defaultlayout') ?>
<?= $this->section("content") ?>
<div class="row mt-4">
<div class="col-12">
<div class="card card-info">
<div class="card-header">
<h3 class="card-title">Backups disponibles</h3>
</div>
<div class="card-body">
<?= view("themes/_commonPartialsBs/_alertBoxes") ?>
<?php if (getenv('SK_ENVIRONMENT') === 'production'): ?>
<a href="<?= route_to('backupsCreate') ?>" class="btn btn-primary mb-3">Crear backup</a>
<?php else: ?>
<a href="<?= route_to('backupsCreateDev') ?>" class="btn btn-secondary mb-3">Crear backup desarrollo</a>
<?php endif; ?>
<div class="table-responsive">
<table id="tablaBackups" class="table table-striped table-hover w-100">
<thead>
<tr>
<th>Archivo</th>
<th>Fecha</th>
<th>Tamaño</th>
<th>Local</th>
<th>Remoto</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($backups as $b): ?>
<tr>
<td><?= esc($b['filename']) ?></td>
<td><?= esc($b['fecha']) ?></td>
<td><?= esc($b['tamano']) ?></td>
<td>
<span class="badge bg-<?= $b['local'] ? 'success' : 'secondary' ?>">
<?= $b['local'] ? 'Sí' : 'No' ?>
</span>
</td>
<td>
<span class="badge bg-<?= $b['remoto'] ? 'info' : 'secondary' ?>">
<?= $b['remoto'] ? 'Sí' : 'No' ?>
</span>
</td>
<td class="text-nowrap">
<!-- Descargar siempre disponible -->
<a href="<?= route_to('backupsDownload', $b['filename']) ?>"
class="btn btn-sm btn-info" title="Descargar backup">
Descargar
</a>
<!-- Restaurar y eliminar solo si local -->
<?php if ($b['local']): ?>
<?php if (getenv('SK_ENVIRONMENT') === 'production'): ?>
<a href="<?= route_to('backupsRestoreLocal', $b['filename']) ?>"
class="btn btn-sm btn-warning" title="Restaurar backup local">
Restaurar Local
</a>
<?php endif; ?>
<?php if (getenv('SK_ENVIRONMENT') === 'production' && !empty($b['id'])): ?>
<a href="<?= route_to('backupsDeleteLocal', $b['id']) ?>"
class="btn btn-sm btn-danger"
onclick="return confirm('¿Seguro que deseas eliminar este backup?')"
title="Eliminar archivo local (producción)">
Eliminar [PROD]
</a>
<?php elseif (getenv('SK_ENVIRONMENT') === 'development' && !empty($b['filename'])): ?>
<a href="<?= route_to('backupsDeleteLocalDev', $b['filename']) ?>"
class="btn btn-sm btn-danger"
onclick="return confirm('¿Eliminar archivo del sistema de archivos local?')"
title="Eliminar archivo local (DEV)">
Eliminar [DEV]
</a>
<?php endif; ?>
<?php elseif ($b['remoto']): ?>
<!-- Restaurar remoto si solo existe remoto -->
<a href="<?= route_to('backupsRestoreRemote', $b['filename']) ?>"
class="btn btn-sm btn-warning" title="Restaurar desde servidor remoto">
Restaurar Remoto
</a>
<?php else: ?>
<span class="text-muted">No disponible</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div><!-- /.card-body -->
</div><!-- /.card -->
</div><!-- /.col -->
</div><!-- /.row -->
<?= $this->endSection() ?>
<?= $this->section('additionalInlineJs') ?>
$(document).ready(function () {
$('#tablaBackups').DataTable({
order: [[1, 'desc']]
});
});
<?= $this->endSection() ?>

View File

@ -3,7 +3,7 @@
* SEPARADOR Y MENUS DE SISTEMA
*/
if (auth()->user()->can('actividad.menu')) {
if (auth()->user()->can('actividad.menu') || auth()->user()->can('backup.menu')) {
?>
<li class="menu-header small text-uppercase">
<span class="menu-header-text">Sistema</span>
@ -23,4 +23,20 @@ if (auth()->user()->can('actividad.menu')) {
</a>
</li>
<?php } ?>
<?php
/**
* MENU BACKUP
*/
if (auth()->user()->can('backup.menu')) {
?>
<!-- Backups -->
<li class="menu-item">
<a href="<?= route_to("backupsList") ?>" class="menu-link">
<i class="menu-icon tf-icons ti ti-database-import"></i>
<div data-i18n="<?= lang("App.menu_backups") ?>"><?= lang("App.menu_backups") ?></div>
</a>
</li>
<?php } ?>
<?php } ?>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<p>Directory access is forbidden.</p>
</body>
</html>