diff --git a/app/Console/Commands/BackupDatabase.php b/app/Console/Commands/BackupDatabase.php
new file mode 100644
index 0000000..be93ec1
--- /dev/null
+++ b/app/Console/Commands/BackupDatabase.php
@@ -0,0 +1,159 @@
+info('Iniciando copia de seguridad de la base de datos...');
+
+ $dbConfig = config('database.connections.mysql');
+
+ if (!$dbConfig) {
+ $this->error('No se pudo cargar la configuración de la base de datos MySQL.');
+ return 1;
+ }
+
+ $host = $dbConfig['host'] ?? '127.0.0.1';
+ $port = $dbConfig['port'] ?? '3306';
+ $database = $dbConfig['database'] ?? 'lauck';
+ $username = $dbConfig['username'] ?? 'root';
+ $password = $dbConfig['password'] ?? '';
+
+ // Asegurarse de que el directorio de respaldos exista
+ $backupDir = storage_path('app/backups');
+ if (!File::exists($backupDir)) {
+ File::makeDirectory($backupDir, 0755, true);
+ }
+
+ // Nombre de los archivos
+ $timestamp = now()->format('Y-m-d_H-i-s');
+ $sqlFile = "backup-{$database}-{$timestamp}.sql";
+ $sqlPath = $backupDir . '/' . $sqlFile;
+ $zipFile = "backup-{$database}-{$timestamp}.zip";
+ $zipPath = $backupDir . '/' . $zipFile;
+
+ // Buscar el ejecutable mysqldump
+ $mysqldumpPath = 'mysqldump';
+ if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+ // En Windows/XAMPP comúnmente está en C:\xampp\mysql\bin\mysqldump.exe
+ $xamppPath = 'C:\\xampp\\mysql\\bin\\mysqldump.exe';
+ if (File::exists($xamppPath)) {
+ $mysqldumpPath = $xamppPath;
+ }
+ }
+
+ // Ejecutar mysqldump con Process
+ // Usamos variables de entorno para la contraseña por seguridad
+ $command = [
+ $mysqldumpPath,
+ "--host={$host}",
+ "--port={$port}",
+ "--user={$username}",
+ "--no-tablespaces",
+ $database
+ ];
+
+ // Inherit SystemRoot and PATH from the system environment to prevent Winsock 10106 errors on Windows
+ $env = [
+ 'SystemRoot' => getenv('SystemRoot') ?: 'C:\\Windows',
+ 'windir' => getenv('windir') ?: 'C:\\Windows',
+ 'PATH' => getenv('PATH'),
+ ];
+ if ($password !== '') {
+ $env['MYSQL_PWD'] = $password;
+ }
+
+ $this->info("Ejecutando volcado a archivo temporal SQL...");
+
+ $process = new Process($command, null, $env);
+ $process->setTimeout(300); // 5 minutos máximo
+
+ try {
+ $process->run();
+
+ if (!$process->isSuccessful()) {
+ throw new \Exception(trim($process->getErrorOutput()) ?: 'Error desconocido al ejecutar mysqldump.');
+ }
+
+ // Guardar salida en el archivo SQL
+ File::put($sqlPath, $process->getOutput());
+
+ } catch (\Exception $e) {
+ $this->error('Error durante el volcado de la base de datos: ' . $e->getMessage());
+ // Limpieza si quedó el archivo a medio hacer
+ if (File::exists($sqlPath)) {
+ File::delete($sqlPath);
+ }
+ return 1;
+ }
+
+ // Comprimir el archivo SQL a ZIP
+ $this->info('Comprimiendo respaldo...');
+ $zip = new ZipArchive();
+ if ($zip->open($zipPath, ZipArchive::CREATE) === true) {
+ $zip->addFile($sqlPath, $sqlFile);
+ $zip->close();
+
+ // Eliminar el archivo SQL temporal
+ File::delete($sqlPath);
+ $this->info("Copia de seguridad guardada con éxito en: {$zipFile}");
+ } else {
+ $this->error('No se pudo crear el archivo ZIP.');
+ return 1;
+ }
+
+ // Retención de respaldos (limpieza de archivos antiguos)
+ $this->cleanupOldBackups($backupDir);
+
+ return 0;
+ }
+
+ /**
+ * Elimina respaldos antiguos que exceden el límite de días (30 días).
+ */
+ protected function cleanupOldBackups($backupDir)
+ {
+ $this->info('Revisando si hay respaldos antiguos para limpiar...');
+ $days = 30; // Conservar los últimos 30 días de respaldos semanales
+ $files = File::files($backupDir);
+
+ foreach ($files as $file) {
+ // Verificar que sea un archivo de backup
+ if ($file->getExtension() === 'zip' && strpos($file->getFilename(), 'backup-') === 0) {
+ $lastModified = $file->getMTime();
+ $ageInDays = (time() - $lastModified) / (24 * 60 * 60);
+
+ if ($ageInDays > $days) {
+ $this->warn("Eliminando respaldo antiguo: " . $file->getFilename());
+ File::delete($file->getPathname());
+ }
+ }
+ }
+ $this->info('Limpieza finalizada.');
+ }
+}
diff --git a/app/Http/Controllers/Admin/BackupController.php b/app/Http/Controllers/Admin/BackupController.php
new file mode 100644
index 0000000..ada7056
--- /dev/null
+++ b/app/Http/Controllers/Admin/BackupController.php
@@ -0,0 +1,284 @@
+backupDir = storage_path('app/backups');
+ }
+
+ /**
+ * Muestra el panel con el listado de copias de seguridad.
+ */
+ public function index()
+ {
+ // Asegurarse de que el directorio exista
+ if (!File::exists($this->backupDir)) {
+ File::makeDirectory($this->backupDir, 0755, true);
+ }
+
+ $files = File::files($this->backupDir);
+ $backups = [];
+ $totalSize = 0;
+
+ foreach ($files as $file) {
+ if ($file->getExtension() === 'zip' && strpos($file->getFilename(), 'backup-') === 0) {
+ $size = $file->getSize();
+ $totalSize += $size;
+
+ $backups[] = [
+ 'filename' => $file->getFilename(),
+ 'size' => $this->formatBytes($size),
+ 'raw_size' => $size,
+ 'created_at' => \Carbon\Carbon::createFromTimestamp($file->getMTime())->format('d/m/Y H:i:s'),
+ 'mtime' => $file->getMTime(),
+ ];
+ }
+ }
+
+ // Ordenar de más nuevo a más viejo
+ usort($backups, function ($a, $b) {
+ return $b['mtime'] <=> $a['mtime'];
+ });
+
+ // Estadísticas de disco
+ $freeDiskSpace = disk_free_space($this->backupDir);
+ $totalDiskSpace = disk_total_space($this->backupDir);
+
+ $diskStats = [
+ 'total_backups_size' => $this->formatBytes($totalSize),
+ 'free_space' => $this->formatBytes($freeDiskSpace),
+ 'total_space' => $this->formatBytes($totalDiskSpace),
+ 'free_percentage' => round(($freeDiskSpace / $totalDiskSpace) * 100, 1),
+ ];
+
+ return view('admin.backups.index', compact('backups', 'diskStats'));
+ }
+
+ public function create()
+ {
+ try {
+ $exitCode = Artisan::call('db:backup');
+ $output = Artisan::output();
+
+ if ($exitCode === 0) {
+ return redirect()->route('admin.backups.index')
+ ->with('success', 'Copia de seguridad generada correctamente.');
+ } else {
+ \Illuminate\Support\Facades\Log::error("Artisan db:backup failed with exit code {$exitCode}. Output: " . $output);
+ return redirect()->route('admin.backups.index')
+ ->with('error', 'Ocurrió un error al generar la copia de seguridad. Detalles: ' . trim($output));
+ }
+ } catch (\Exception $e) {
+ \Illuminate\Support\Facades\Log::error("Artisan db:backup threw exception: " . $e->getMessage());
+ return redirect()->route('admin.backups.index')
+ ->with('error', 'Error: ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * Descargar una copia de seguridad.
+ */
+ public function download($filename)
+ {
+ // Validación de seguridad para evitar directory traversal
+ if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) {
+ abort(404, 'Nombre de archivo no válido.');
+ }
+
+ $path = $this->backupDir . '/' . $filename;
+
+ if (!File::exists($path)) {
+ abort(404, 'El archivo no existe.');
+ }
+
+ return response()->download($path);
+ }
+
+ /**
+ * Eliminar una copia de seguridad.
+ */
+ public function destroy($filename)
+ {
+ // Validación de seguridad para evitar directory traversal
+ if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) {
+ abort(400, 'Nombre de archivo no válido.');
+ }
+
+ $path = $this->backupDir . '/' . $filename;
+
+ if (File::exists($path)) {
+ File::delete($path);
+ return redirect()->route('admin.backups.index')
+ ->with('success', 'Copia de seguridad eliminada correctamente.');
+ }
+
+ return redirect()->route('admin.backups.index')
+ ->with('error', 'No se encontró el archivo a eliminar.');
+ }
+
+ /**
+ * Subir un archivo de copia de seguridad (.zip).
+ */
+ public function upload(Request $request)
+ {
+ $request->validate([
+ 'backup_file' => 'required|file|mimes:zip|max:50000', // 50MB max
+ ]);
+
+ $file = $request->file('backup_file');
+ $filename = $file->getClientOriginalName();
+
+ // Validación de seguridad para el nombre del archivo
+ if (strpos($filename, 'backup-') !== 0 || $file->getClientOriginalExtension() !== 'zip') {
+ return redirect()->route('admin.backups.index')
+ ->with('error', 'El archivo debe ser un .zip válido de respaldo (su nombre debe empezar con "backup-").');
+ }
+
+ // Asegurarse de que el directorio exista
+ if (!File::exists($this->backupDir)) {
+ File::makeDirectory($this->backupDir, 0755, true);
+ }
+
+ $file->move($this->backupDir, $filename);
+
+ return redirect()->route('admin.backups.index')
+ ->with('success', 'Copia de seguridad subida correctamente.');
+ }
+
+ /**
+ * Restaurar la base de datos a partir de una copia de seguridad.
+ */
+ public function restore($filename)
+ {
+ // Validación de seguridad para evitar directory traversal
+ if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) {
+ abort(400, 'Nombre de archivo no válido.');
+ }
+
+ $zipPath = $this->backupDir . '/' . $filename;
+
+ if (!File::exists($zipPath)) {
+ return redirect()->route('admin.backups.index')->with('error', 'El archivo no existe.');
+ }
+
+ // Extraer el archivo SQL del ZIP
+ $zip = new \ZipArchive();
+ if ($zip->open($zipPath) === true) {
+ // Buscamos el primer archivo SQL dentro del zip
+ $sqlFilename = null;
+ for ($i = 0; $i < $zip->numFiles; $i++) {
+ $stat = $zip->statIndex($i);
+ if (pathinfo($stat['name'], PATHINFO_EXTENSION) === 'sql') {
+ $sqlFilename = $stat['name'];
+ break;
+ }
+ }
+
+ if (!$sqlFilename) {
+ $zip->close();
+ return redirect()->route('admin.backups.index')->with('error', 'No se encontró ningún archivo SQL en el archivo comprimido.');
+ }
+
+ // Extraer el SQL a un directorio temporal de backups
+ $zip->extractTo($this->backupDir, $sqlFilename);
+ $zip->close();
+
+ $sqlPath = $this->backupDir . '/' . $sqlFilename;
+
+ // Datos de conexión
+ $dbConfig = config('database.connections.mysql');
+ $host = $dbConfig['host'] ?? '127.0.0.1';
+ $port = $dbConfig['port'] ?? '3306';
+ $database = $dbConfig['database'] ?? 'lauck';
+ $username = $dbConfig['username'] ?? 'root';
+ $password = $dbConfig['password'] ?? '';
+
+ // Ejecutable de mysql
+ $mysqlPath = 'mysql';
+ if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+ $xamppPath = 'C:\\xampp\\mysql\\bin\\mysql.exe';
+ if (File::exists($xamppPath)) {
+ $mysqlPath = $xamppPath;
+ }
+ }
+
+ // Ejecutar importación usando Process y redirección de entrada de archivo
+ $command = [
+ $mysqlPath,
+ "--host={$host}",
+ "--port={$port}",
+ "--user={$username}",
+ $database
+ ];
+
+ $env = [
+ 'SystemRoot' => getenv('SystemRoot') ?: 'C:\\Windows',
+ 'windir' => getenv('windir') ?: 'C:\\Windows',
+ 'PATH' => getenv('PATH'),
+ ];
+ if ($password !== '') {
+ $env['MYSQL_PWD'] = $password;
+ }
+
+ $process = new \Symfony\Component\Process\Process($command, null, $env);
+
+ // Pasar el contenido del archivo SQL a la entrada estándar (stdin)
+ $process->setInput(File::get($sqlPath));
+ $process->setTimeout(300);
+
+ try {
+ $process->run();
+
+ // Limpiar archivo SQL temporal
+ File::delete($sqlPath);
+
+ if (!$process->isSuccessful()) {
+ throw new \Exception(trim($process->getErrorOutput()) ?: 'Error desconocido al restaurar el respaldo.');
+ }
+
+ return redirect()->route('admin.backups.index')
+ ->with('success', 'Base de datos restaurada correctamente a partir del respaldo: ' . $filename);
+
+ } catch (\Exception $e) {
+ if (File::exists($sqlPath)) {
+ File::delete($sqlPath);
+ }
+ \Illuminate\Support\Facades\Log::error("Backup restore failed: " . $e->getMessage());
+ return redirect()->route('admin.backups.index')
+ ->with('error', 'Error al restaurar base de datos: ' . $e->getMessage());
+ }
+ } else {
+ return redirect()->route('admin.backups.index')->with('error', 'No se pudo abrir el archivo ZIP.');
+ }
+ }
+
+ /**
+ * Formatear bytes a tamaño legible.
+ */
+ private function formatBytes($bytes, $precision = 2)
+ {
+ $units = ['B', 'KB', 'MB', 'GB', 'TB'];
+
+ $bytes = max($bytes, 0);
+ $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
+ $pow = min($pow, count($units) - 1);
+
+ $bytes /= pow(1024, $pow);
+
+ return round($bytes, $precision) . ' ' . $units[$pow];
+ }
+}
diff --git a/app/Http/Controllers/ProductosController.php b/app/Http/Controllers/ProductosController.php
index ea104a4..3e8d234 100644
--- a/app/Http/Controllers/ProductosController.php
+++ b/app/Http/Controllers/ProductosController.php
@@ -68,13 +68,39 @@ class ProductosController extends Controller
'min_stock_alert' => 'required|integer|min:0',
'type' => 'required|in:bike,accessory,clothing,spare,service,children,skate,rollers,other',
'serial_number' => 'nullable|string|max:100',
- 'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
+ 'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:20480',
'suppliers_id' => 'required|exists:suppliers,id',
'description' => 'nullable|string'
+ ], [
+ 'image.uploaded' => 'La imagen supera el límite de subida permitido por el servidor.',
+ 'image.max' => 'La imagen no debe pesar más de 20 MB.',
+ 'image.image' => 'El archivo debe ser una imagen válida.',
+ 'image.mimes' => 'La imagen debe tener formato jpeg, png, jpg o webp.',
]);
if ($request->hasFile('image')) {
- $path = $request->file('image')->store('products', 'public');
+ $file = $request->file('image');
+
+ // Elevar temporalmente el límite de memoria para procesar la imagen
+ ini_set('memory_limit', '512M');
+
+ // Usar Intervention Image v4 con driver GD para procesar la imagen
+ $manager = new \Intervention\Image\ImageManager(new \Intervention\Image\Drivers\Gd\Driver());
+ $image = $manager->decode($file->getRealPath());
+
+ // Redimensionar proporcionalmente si excede 1200px
+ if ($image->width() > 1200 || $image->height() > 1200) {
+ $image->scale(width: 1200, height: 1200);
+ }
+
+ // Comprimir a JPEG con 80% de calidad
+ $encoded = $image->encode(new \Intervention\Image\Encoders\JpegEncoder(80));
+
+ // Nombre de archivo único
+ $path = 'products/' . uniqid() . '.jpg';
+
+ // Guardar en disco public
+ Storage::disk('public')->put($path, $encoded->toString());
$validated['image_path'] = $path;
}
unset($validated['image']);
@@ -106,9 +132,14 @@ class ProductosController extends Controller
'stock_quantity' => 'required|integer|min:0',
'min_stock_alert' => 'required|integer|min:0',
'serial_number' => 'nullable|string|max:100',
- 'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
+ 'image' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:20480',
'suppliers_id' => 'required|exists:suppliers,id',
'description' => 'nullable|string'
+ ], [
+ 'image.uploaded' => 'La imagen supera el límite de subida permitido por el servidor.',
+ 'image.max' => 'La imagen no debe pesar más de 20 MB.',
+ 'image.image' => 'El archivo debe ser una imagen válida.',
+ 'image.mimes' => 'La imagen debe tener formato jpeg, png, jpg o webp.',
]);
if ($request->hasFile('image')) {
@@ -116,7 +147,28 @@ class ProductosController extends Controller
Storage::disk('public')->delete($product->image_path);
}
- $path = $request->file('image')->store('products', 'public');
+ $file = $request->file('image');
+
+ // Elevar temporalmente el límite de memoria para procesar la imagen
+ ini_set('memory_limit', '512M');
+
+ // Usar Intervention Image v4 con driver GD para procesar la imagen
+ $manager = new \Intervention\Image\ImageManager(new \Intervention\Image\Drivers\Gd\Driver());
+ $image = $manager->decode($file->getRealPath());
+
+ // Redimensionar proporcionalmente si excede 1200px
+ if ($image->width() > 1200 || $image->height() > 1200) {
+ $image->scale(width: 1200, height: 1200);
+ }
+
+ // Comprimir a JPEG con 80% de calidad
+ $encoded = $image->encode(new \Intervention\Image\Encoders\JpegEncoder(80));
+
+ // Nombre de archivo único
+ $path = 'products/' . uniqid() . '.jpg';
+
+ // Guardar en disco public
+ Storage::disk('public')->put($path, $encoded->toString());
$validated['image_path'] = $path;
}
unset($validated['image']);
diff --git a/app/Http/Middleware/CheckAdmin.php b/app/Http/Middleware/CheckAdmin.php
new file mode 100644
index 0000000..d3c6ab8
--- /dev/null
+++ b/app/Http/Middleware/CheckAdmin.php
@@ -0,0 +1,19 @@
+check() && auth()->user()->role === 'admin') {
+ return $next($request);
+ }
+
+ abort(403, 'No tienes permiso para entrar aquí.');
+ }
+}
diff --git a/bootstrap/app.php b/bootstrap/app.php
index c183276..01ddb97 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -12,6 +12,9 @@ return Application::configure(basePath: dirname(__DIR__))
)
->withMiddleware(function (Middleware $middleware): void {
//
+ $middleware->alias([
+ 'admin' => \App\Http\Middleware\CheckAdmin::class,
+ ]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
diff --git a/composer.json b/composer.json
index 2a90057..60fd95c 100644
--- a/composer.json
+++ b/composer.json
@@ -7,6 +7,7 @@
"license": "MIT",
"require": {
"php": "^8.2",
+ "intervention/image": "^4.1",
"laravel-lang/common": "^6.7",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1"
diff --git a/composer.lock b/composer.lock
index 566afb2..2764864 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "c5327d228d42dad14f6185dd57e05dde",
+ "content-hash": "54a99d812eb7e5412ada7f4366c3065c",
"packages": [
{
"name": "archtechx/enums",
@@ -1568,6 +1568,150 @@
],
"time": "2025-02-03T10:55:03+00:00"
},
+ {
+ "name": "intervention/gif",
+ "version": "5.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Intervention/gif.git",
+ "reference": "bb395af960deffe64d70c976b4df9283f68e762d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Intervention/gif/zipball/bb395af960deffe64d70c976b4df9283f68e762d",
+ "reference": "bb395af960deffe64d70c976b4df9283f68e762d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.3"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^12.0",
+ "slevomat/coding-standard": "~8.0",
+ "squizlabs/php_codesniffer": "^4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Intervention\\Gif\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Oliver Vogel",
+ "email": "oliver@intervention.io",
+ "homepage": "https://intervention.io/"
+ }
+ ],
+ "description": "PHP GIF Encoder/Decoder",
+ "homepage": "https://github.com/intervention/gif",
+ "keywords": [
+ "animation",
+ "gd",
+ "gif",
+ "image"
+ ],
+ "support": {
+ "issues": "https://github.com/Intervention/gif/issues",
+ "source": "https://github.com/Intervention/gif/tree/5.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://paypal.me/interventionio",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/Intervention",
+ "type": "github"
+ },
+ {
+ "url": "https://ko-fi.com/interventionphp",
+ "type": "ko_fi"
+ }
+ ],
+ "time": "2026-05-03T06:04:47+00:00"
+ },
+ {
+ "name": "intervention/image",
+ "version": "4.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Intervention/image.git",
+ "reference": "ba4a7cc8042882d479a78b0835f3f0e991e40a71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Intervention/image/zipball/ba4a7cc8042882d479a78b0835f3f0e991e40a71",
+ "reference": "ba4a7cc8042882d479a78b0835f3f0e991e40a71",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "intervention/gif": "^5",
+ "php": "^8.3"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.6",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^12.0",
+ "slevomat/coding-standard": "~8.0",
+ "squizlabs/php_codesniffer": "^4"
+ },
+ "suggest": {
+ "ext-exif": "Recommended to be able to read EXIF data properly."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Intervention\\Image\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Oliver Vogel",
+ "email": "oliver@intervention.io",
+ "homepage": "https://intervention.io"
+ }
+ ],
+ "description": "PHP Image Processing",
+ "homepage": "https://image.intervention.io",
+ "keywords": [
+ "gd",
+ "image",
+ "imagick",
+ "resize",
+ "thumbnail",
+ "watermark"
+ ],
+ "support": {
+ "issues": "https://github.com/Intervention/image/issues",
+ "source": "https://github.com/Intervention/image/tree/4.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://paypal.me/interventionio",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/Intervention",
+ "type": "github"
+ },
+ {
+ "url": "https://ko-fi.com/interventionphp",
+ "type": "ko_fi"
+ }
+ ],
+ "time": "2026-05-23T06:51:28+00:00"
+ },
{
"name": "laravel-lang/actions",
"version": "1.10.2",
diff --git a/database/seeders/ProductosInicialesSeeder.php b/database/seeders/ProductosInicialesSeeder.php
index 25cdd60..dd1d557 100644
--- a/database/seeders/ProductosInicialesSeeder.php
+++ b/database/seeders/ProductosInicialesSeeder.php
@@ -81,5 +81,6 @@ class ProductosInicialesSeeder extends Seeder
}
$this->command->info('¡Productos y proveedor insertados correctamente!');
+
}
}
\ No newline at end of file
diff --git a/public/.htaccess b/public/.htaccess
index b574a59..97d3e6b 100644
--- a/public/.htaccess
+++ b/public/.htaccess
@@ -23,3 +23,12 @@
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
+
+ Total acumulado de archivos .zip De un total de {{ $diskStats['total_space'] }} No se han generado copias de seguridad de la base de datos todavía. Haga clic en "Generar Nueva Copia" para iniciar una.
+
+
+
+
+
+
+ @forelse($backups as $backup)
+ Nombre del Archivo
+ Fecha de Creación
+ Tamaño
+ Acciones
+
+
+ @empty
+
+
+
+
+ {{ $backup['created_at'] }}
+
+
+
+ {{ $backup['size'] }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @endforelse
+
+
+
+