From 166fa1c8fa2b3def743090151a927f6839dcde52 Mon Sep 17 00:00:00 2001 From: Coassolo-Lucas Date: Sat, 4 Apr 2026 22:58:48 -0300 Subject: [PATCH 1/7] =?UTF-8?q?Agregue=20boton=20de=20Ojo=20para=20ver=20c?= =?UTF-8?q?ontrase=C3=B1a=20en=20el=20login,=20usando=20JQuery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/js/app.js | 9 +++ resources/views/login.blade.php | 107 ++++++++++++++++++++++---------- 2 files changed, 84 insertions(+), 32 deletions(-) diff --git a/resources/js/app.js b/resources/js/app.js index 814fd37..a486220 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -120,3 +120,12 @@ document.addEventListener('DOMContentLoaded', function () { }) +// Ver contraseña, con JQuery +$(document).on('click', '#togglePassword', function () { + const input = $('#passwordInput'); + const isPassword = input.attr('type') === 'password'; + + input.attr('type', isPassword ? 'text' : 'password'); + $('#iconShow').toggleClass('hidden', isPassword); + $('#iconHide').toggleClass('hidden', !isPassword); +}); \ No newline at end of file diff --git a/resources/views/login.blade.php b/resources/views/login.blade.php index 9e0bc6b..e6a7bf3 100644 --- a/resources/views/login.blade.php +++ b/resources/views/login.blade.php @@ -2,43 +2,86 @@
-
-
- @csrf - {{-- Bloque de errores --}} - @if ($errors->any()) -
-

- {{ $errors->first() }} -

-
- @endif + subtitle="Inicio de sesion" + title="Bicicletería " + highlight="Lauck" + /> -
- Correo electrónico - +
+ + + @csrf + + {{-- Bloque de errores --}} + @if ($errors->any()) +
+

+ {{ $errors->first() }} +

+ @endif -
- Contraseña - +
+ + Correo electrónico + + +
+ +
+ + Contraseña + +
+ +
+
-
- -
+
+ +
-

- ¿No tienes una cuenta? Regístrate acá. -

- -
+

+ ¿No tienes una cuenta? + + Regístrate acá + +

+ +
+
+ \ No newline at end of file From cf0b40d79ef2ace9ddddf5429fc4118d5e11724e Mon Sep 17 00:00:00 2001 From: Coassolo-Lucas Date: Sun, 5 Apr 2026 22:57:11 -0300 Subject: [PATCH 2/7] Agregue Ojo para Password, ademas en register, y acomode seeder. --- database/seeders/DatabaseSeeder.php | 4 +- resources/js/app.js | 9 ++ resources/views/register.blade.php | 166 +++++++++++++++------------- 3 files changed, 102 insertions(+), 77 deletions(-) diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 0ccfcb8..58ad0a4 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -30,7 +30,7 @@ class DatabaseSeeder extends Seeder 'role' => 'employee', ]); - Supplier::create([ + /*Supplier::create([ 'name' => 'Cámara 29 Válvula Auto', 'phone' => '3434567890', 'email' => 'suplier@suplier.com' @@ -61,7 +61,7 @@ class DatabaseSeeder extends Seeder ]); // Generar 50 productos aleatorios más - Product::factory(50)->create(); + Product::factory(50)->create(); */ // Clientes y Turnos $client = Client::create([ diff --git a/resources/js/app.js b/resources/js/app.js index a486220..c207710 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -128,4 +128,13 @@ $(document).on('click', '#togglePassword', function () { input.attr('type', isPassword ? 'text' : 'password'); $('#iconShow').toggleClass('hidden', isPassword); $('#iconHide').toggleClass('hidden', !isPassword); +}); + +$(document).on('click', '#togglePasswordConfirm', function () { + const input = $('#passwordConfirmInput'); + const isPassword = input.attr('type') === 'password'; + + input.attr('type', isPassword ? 'text' : 'password'); + $('#iconShowConfirm').toggleClass('hidden', isPassword); + $('#iconHideConfirm').toggleClass('hidden', !isPassword); }); \ No newline at end of file diff --git a/resources/views/register.blade.php b/resources/views/register.blade.php index 18d03ff..07d7567 100644 --- a/resources/views/register.blade.php +++ b/resources/views/register.blade.php @@ -1,95 +1,111 @@ -
- -
+
+ +
-
-
+
+
-
- @csrf + + @csrf - {{-- Bloque de errores --}} - @if ($errors->any()) -
-

- {{ $errors->first() }} -

-
- @endif + {{-- Bloque de errores --}} + @if ($errors->any()) +
+

+ {{ $errors->first() }} +

+
+ @endif
Nombre completo - +
-
- - Correo electrónico - - -
+
+ + Correo electrónico + + +
-
- - Contraseña - - -
+
+ + Contraseña + +
+ + +
-
- - Confirmar contraseña - - -
+
-
- -
+
+ + Confirmar contraseña + +
+ -

- ¿Ya tenés una cuenta? - - Iniciá sesión acá - . -

- + +
+
+ +
+ +
+ +

+ ¿Ya tenés una cuenta? + + Iniciá sesión acá + . +

+ + +
-
From 00247399a4b9af06630368f444f87934fe7b43f7 Mon Sep 17 00:00:00 2001 From: Coassolo-Lucas Date: Fri, 10 Apr 2026 16:15:01 -0300 Subject: [PATCH 3/7] Agregue middleware a productos Destroy y Edit. Y cambios en la vista de stock. --- app/Http/Middleware/CheckAdmin.php | 19 ++ bootstrap/app.php | 3 + resources/views/productos/index.blade.php | 205 ++++++++++++++-------- routes/web.php | 36 ++-- 4 files changed, 176 insertions(+), 87 deletions(-) create mode 100644 app/Http/Middleware/CheckAdmin.php 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/resources/views/productos/index.blade.php b/resources/views/productos/index.blade.php index 04e37b6..11efe83 100644 --- a/resources/views/productos/index.blade.php +++ b/resources/views/productos/index.blade.php @@ -1,5 +1,5 @@ - + @@ -7,25 +7,28 @@
- + -
- + +
-
-
- - - - + + +
- @if(request('search') || request('type') || request('stock_status')) + @if (request('search') || request('type') || request('stock_status')) @@ -65,7 +73,8 @@ -
+ + Nuevo Producto
@@ -73,7 +82,8 @@
- + @@ -84,61 +94,108 @@ @forelse($products as $product) - - - - - - + + + + - + + + @php + $isAdmin = auth()->user()?->role === 'admin'; + $classes = $isAdmin + ? 'text-blue-800 border-blue-800 hover:bg-blue-800 hover:text-white' + : 'border-black-400 text-black-400 opacity-50 cursor-not-allowed dark:border-gray-400 dark:text-gray-400'; + @endphp + + + + + + @if (auth()->user()?->role === 'admin') + + @csrf + @method('DELETE') + + + @else + + @endif + + @empty - - - + + + @endforelse
Producto Tipo
-
{{ $product->name }}
-
{{ $product->sku ?? 'Sin SKU' }}
-
- @if($product->type === 'bike') Bicicleta - @elseif($product->type === 'clothing') Indumentaria - @elseif($product->type === 'spare') Repuesto - @elseif($product->type === 'children') Infantil - @elseif($product->type === 'rollers') Rollers - @elseif($product->type === 'skate') Skate - @else Accesorio / Otro @endif - - ${{ number_format($product->price, 2) }} - - @if($product->stock_quantity < $product->min_stock_alert) - {{ $product->stock_quantity }} - @elseif($product->stock_quantity <= $product->min_stock_alert+5) - {{ $product->stock_quantity }} - @else - {{ $product->stock_quantity }} - @endif - - - - - - - -
- @csrf - @method('DELETE') -
+ @if ($product->type === 'bike') + Bicicleta + @elseif($product->type === 'clothing') + Indumentaria + @elseif($product->type === 'spare') + Repuesto + @elseif($product->type === 'children') + Infantil + @elseif($product->type === 'rollers') + Rollers + @elseif($product->type === 'skate') + Skate + @else + Accesorio / Otro + @endif + + ${{ number_format($product->price, 2) }} + + @if ($product->stock_quantity < $product->min_stock_alert) + {{ $product->stock_quantity }} + @elseif($product->stock_quantity <= $product->min_stock_alert + 5) + {{ $product->stock_quantity }} + @else + {{ $product->stock_quantity }} + @endif + + + - - -
- No se encontraron productos. -
+ No se encontraron productos. +
@@ -148,4 +205,4 @@
{{ $products->links() }}
- \ No newline at end of file + diff --git a/routes/web.php b/routes/web.php index 2fcbc18..44a06e5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -16,41 +16,51 @@ use App\Models\Product; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Session; -Route::get('/',HomeController::class); - -// Route::get('/catalogo', [CatalogoController::class,'catalogo'])->name('catalogo'); +// --- RUTAS PÚBLICAS (Cualquiera accede) --- +Route::get('/', HomeController::class); Route::resource('catalogo', CatalogoController::class)->only(['index', 'show'])->parameters(['catalogo' => 'product']); Route::get('login', function(){ return view('login'); })->name('login'); Route::post('login', LoginController::class)->middleware('throttle:5,1')->name('login.attempt'); - Route::view('register', 'register')->name('register'); Route::post('register', RegisterController::class)->name('register.store'); Route::post('logout', function(){ Auth::guard('web')->logout(); - Session::invalidate(); Session::regenerateToken(); - return redirect('/'); })->name('logout'); + +// --- RUTAS PROTEGIDAS (Requieren Login) --- Route::middleware(['auth'])->group(function () { Route::view('dashboard', 'dashboard')->name('dashboard'); Route::resource('clients', ClientController::class); - Route::resource('productos', ProductosController::class)->parameters([ - 'productos' => 'product' - ]); Route::resource('sales', SaleController::class)->only(['index', 'create', 'store', 'show']); - // Rutas de Taller + Route::get('/agenda', [AgendaController::class, 'index'])->name('agenda'); + Route::get('/appointments/{appointment}', [AppointmentController::class, 'show'])->name('appointments.show'); + + // Taller Route::controller(TallerController::class)->prefix('taller')->name('taller.')->group(function () { Route::get('/', 'index')->name('index'); Route::get('/create', 'create')->name('create'); Route::post('/', 'store')->name('store'); Route::patch('/{appointment}/status', 'updateStatus')->name('updateStatus'); }); - Route::get('/agenda', [AgendaController::class, 'index'])->name('agenda'); - Route::get('/appointments/{appointment}', [AppointmentController::class, 'show']) - ->name('appointments.show'); + + // Productos: Rutas de lectura para cualquier usuario logueado + Route::resource('productos', ProductosController::class) + ->only(['index', 'show']) + ->parameters(['productos' => 'product']); + + // --- RUTAS DE ADMINISTRADOR (Solo Admin) --- + Route::middleware(['admin'])->group(function () { + // Solo el admin puede crear, editar o borrar productos + Route::resource('productos', ProductosController::class) + ->only(['create', 'store', 'edit', 'update', 'destroy']) + ->parameters(['productos' => 'product']); + + // Si tienes más cosas de admin (ej: reportes), van aquí + }); }); From 54fd75b12044f3ad7b41946879a9c2f90b4768ef Mon Sep 17 00:00:00 2001 From: Coassolo-Lucas Date: Mon, 20 Apr 2026 23:15:42 -0300 Subject: [PATCH 4/7] Ajuste estilos para que sean consistentes entre paginas. --- resources/views/agenda.blade.php | 4 +- resources/views/catalogo/index.blade.php | 6 +- resources/views/clients/edit.blade.php | 2 +- resources/views/productos/edit.blade.php | 2 +- resources/views/sales/index.blade.php | 185 +++++++++++++---------- resources/views/taller/index.blade.php | 103 ++++++++----- 6 files changed, 179 insertions(+), 123 deletions(-) diff --git a/resources/views/agenda.blade.php b/resources/views/agenda.blade.php index 2e1185f..c17570a 100644 --- a/resources/views/agenda.blade.php +++ b/resources/views/agenda.blade.php @@ -19,7 +19,7 @@
+ class="w-full xl:w-auto text-center px-5 py-3 text-sm font-bold text-neon-lime dark:text-neutral-900 bg-neutral-950 dark:bg-neon-lime rounded-lg hover:bg-neutral-900/80 hover:dark:bg-[#b3e600] transition-colors uppercase tracking-wide"> Volver a Taller @@ -57,7 +57,7 @@ Ver turno diff --git a/resources/views/catalogo/index.blade.php b/resources/views/catalogo/index.blade.php index ff3bd4b..2056ce6 100644 --- a/resources/views/catalogo/index.blade.php +++ b/resources/views/catalogo/index.blade.php @@ -78,8 +78,8 @@
-
- @if(request('search') || request('type')) @@ -120,7 +120,7 @@ {{-- Botón --}} + class="font-bold uppercase mt-4 block w-full bg-neutral-900 dark:bg-neon-lime/80 text-neon-lime dark:text-black py-2 rounded-lg text-center shadow-md shadow-gray-900/10 dark:shadow-neon-lime/10 hover:bg-neon-lime hover:dark:bg-neutral-900/70 border border-transparent hover:text-black hover:dark:text-neon-lime hover:border-black hover:dark:border-neon-lime transition-all"> Ver Detalle diff --git a/resources/views/clients/edit.blade.php b/resources/views/clients/edit.blade.php index b3137fd..bf379c6 100644 --- a/resources/views/clients/edit.blade.php +++ b/resources/views/clients/edit.blade.php @@ -35,7 +35,7 @@
Cancelar -
diff --git a/resources/views/productos/edit.blade.php b/resources/views/productos/edit.blade.php index e79b0f5..c5c0fc4 100644 --- a/resources/views/productos/edit.blade.php +++ b/resources/views/productos/edit.blade.php @@ -60,7 +60,7 @@
Cancelar -
diff --git a/resources/views/sales/index.blade.php b/resources/views/sales/index.blade.php index 782cbd4..24634c8 100644 --- a/resources/views/sales/index.blade.php +++ b/resources/views/sales/index.blade.php @@ -1,27 +1,30 @@ - + -
+
- -
- + +
+
- +
- +
-
- - - @foreach($clients as $client) + @foreach ($clients as $client) @@ -30,25 +33,32 @@
- - + +
- - + +
-
- + @@ -76,63 +90,78 @@ @forelse($sales as $sale) - - + + - + - + - + - + - - + + @empty - - - + + + @endforelse
# Ref Fecha
- #{{ str_pad($sale->id, 5, '0', STR_PAD_LEFT) }} -
+ #{{ str_pad($sale->id, 5, '0', STR_PAD_LEFT) }} + - {{ $sale->created_at->format('d/m/Y H:i') }} - + {{ $sale->created_at->format('d/m/Y H:i') }} + - @if($sale->client) -
{{ $sale->client->name }}
-
{{ $sale->client->phone ?? '' }}
- @else - Consumidor Final - @endif -
+ @if ($sale->client) +
{{ $sale->client->name }}
+
{{ $sale->client->phone ?? '' }} +
+ @else + Consumidor Final + @endif +
- @php - $colors = [ - 'Efectivo' => 'text-white dark:text-green-400 bg-green-600/80 dark:bg-green-900/20 border-green-800', - 'Transferencia' => 'text-white dark:text-blue-400 bg-blue-600/80 dark:bg-blue-900/20 border-blue-800', - 'Tarjeta de Débito' => 'text-white dark:text-purple-400 bg-purple-600/80 dark:bg-purple-900/20 border-purple-800', - 'Tarjeta de Crédito' => 'text-white dark:text-orange-400 bg-orange-600/80 dark:bg-orange-900/20 border-orange-800', - ]; - $badgeClass = $colors[$sale->payment_method] ?? 'bg-gray-400 dark:bg-gray-800 border-gray-700'; - @endphp - - {{ $sale->payment_method }} - - + @php + $colors = [ + 'Efectivo' => + 'text-white dark:text-green-400 bg-green-600/80 dark:bg-green-900/20 border-green-800', + 'Transferencia' => + 'text-white dark:text-blue-400 bg-blue-600/80 dark:bg-blue-900/20 border-blue-800', + 'Tarjeta de Débito' => + 'text-white dark:text-purple-400 bg-purple-600/80 dark:bg-purple-900/20 border-purple-800', + 'Tarjeta de Crédito' => + 'text-white dark:text-orange-400 bg-orange-600/80 dark:bg-orange-900/20 border-orange-800', + ]; + $badgeClass = + $colors[$sale->payment_method] ?? 'bg-gray-400 dark:bg-gray-800 border-gray-700'; + @endphp + + {{ $sale->payment_method }} + + - ${{ number_format($sale->total, 2) }} - + ${{ number_format($sale->total, 2) }} + - - - - - - -
+ + + + + + +
-
- -

No se encontraron ventas registradas.

-
-
+
+ + + + +

No se encontraron ventas registradas.

+
+
@@ -141,4 +170,4 @@
{{ $sales->links() }}
- \ No newline at end of file + diff --git a/resources/views/taller/index.blade.php b/resources/views/taller/index.blade.php index e786f4a..5765a07 100644 --- a/resources/views/taller/index.blade.php +++ b/resources/views/taller/index.blade.php @@ -10,57 +10,84 @@
+ +
- -
-
-

🔴 Pendientes / A Revisar

- {{ $pending->count() }} -
-
- @foreach($pending as $job) - - @endforeach -
+
+

+ 🔴 Pendientes / A Revisar +

+ + {{ $pending->count() }} +
- -
-
-

🟡 En Reparación

- {{ $inProgress->count() }} -
-
- @foreach($inProgress as $job) - - @endforeach -
+
+ @foreach($pending as $job) + + @endforeach
- - -
-
-

🟢 Listas para Retirar

- {{ $ready->count() }} -
-
- @foreach($ready as $job) - - @endforeach -
-
-
+
+ +
+

+ 🟡 En Reparación +

+ + {{ $inProgress->count() }} + +
+ +
+ @foreach($inProgress as $job) + + @endforeach +
+
+ +
+ +
+

+ 🟢 Listas para Retirar +

+ + {{ $ready->count() }} + +
+ +
+ @foreach($ready as $job) + + @endforeach +
+
+ +
+ \ No newline at end of file From 753a1318c46076ed8fe1ba0cca5b943b35d638c3 Mon Sep 17 00:00:00 2001 From: Coassolo-Lucas Date: Mon, 20 Apr 2026 23:54:00 -0300 Subject: [PATCH 5/7] Cambios menores en seeder --- database/seeders/DatabaseSeeder.php | 2 -- database/seeders/ProductosInicialesSeeder.php | 1 + resources/views/productos/index.blade.php | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 58ad0a4..3db6d67 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -77,7 +77,5 @@ class DatabaseSeeder extends Seeder 'problem_description' => 'Service completo y ajuste de cambios', 'status' => 'pending' ]); - - $this->call(ProductSeeder::class); } } 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/resources/views/productos/index.blade.php b/resources/views/productos/index.blade.php index c8f31fc..45d12be 100644 --- a/resources/views/productos/index.blade.php +++ b/resources/views/productos/index.blade.php @@ -137,7 +137,7 @@
-
+
Cargando productos...
From b2e8021c0203764d17ff3f6ab114cf3161215267 Mon Sep 17 00:00:00 2001 From: Coassolo-Lucas Date: Wed, 27 May 2026 15:59:44 -0300 Subject: [PATCH 6/7] Backup y detalles minimos --- app/Console/Commands/BackupDatabase.php | 159 ++++++++++ .../Controllers/Admin/BackupController.php | 284 ++++++++++++++++++ resources/views/admin/backups/index.blade.php | 168 +++++++++++ .../views/components/taller-card.blade.php | 8 +- resources/views/dashboard.blade.php | 9 + resources/views/login.blade.php | 25 +- resources/views/register.blade.php | 37 ++- resources/views/taller/index.blade.php | 112 +++---- routes/console.php | 3 + routes/web.php | 11 + 10 files changed, 722 insertions(+), 94 deletions(-) create mode 100644 app/Console/Commands/BackupDatabase.php create mode 100644 app/Http/Controllers/Admin/BackupController.php create mode 100644 resources/views/admin/backups/index.blade.php 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/resources/views/admin/backups/index.blade.php b/resources/views/admin/backups/index.blade.php new file mode 100644 index 0000000..c12a57b --- /dev/null +++ b/resources/views/admin/backups/index.blade.php @@ -0,0 +1,168 @@ + + + + + +
+ + +
+ Espacio en Copias +
+ {{ $diskStats['total_backups_size'] }} +
+

Total acumulado de archivos .zip

+
+ + +
+ Espacio Libre del Servidor +
+ {{ $diskStats['free_space'] }} +
+

De un total de {{ $diskStats['total_space'] }}

+
+ + +
+
+ Capacidad de Almacenamiento +
+ Libre: {{ $diskStats['free_percentage'] }}% + Usado: {{ 100 - $diskStats['free_percentage'] }}% +
+
+
+
+
+
+
+ + +
+ + +
+ + @csrf +
+ + +
+ + +
+ + +
+
+ @csrf + +
+
+
+ + +
+ + + + + + + + + + + @forelse($backups as $backup) + + + + + + + @empty + + + + @endforelse + +
Nombre del ArchivoFecha de CreaciónTamañoAcciones
+
+ + + + + + {{ $backup['filename'] }} + +
+
+ + {{ $backup['created_at'] }} + + + {{ $backup['size'] }} + + +
+ @csrf + +
+ + + + + + + +
+ @csrf + @method('DELETE') + +
+
+
+ + + +

No se han generado copias de seguridad de la base de datos todavía.

+

Haga clic en "Generar Nueva Copia" para iniciar una.

+
+
+
+ + + @push('scripts') + + @endpush + +
diff --git a/resources/views/components/taller-card.blade.php b/resources/views/components/taller-card.blade.php index 239a7e7..85cb378 100644 --- a/resources/views/components/taller-card.blade.php +++ b/resources/views/components/taller-card.blade.php @@ -2,10 +2,10 @@ @php $borderColor = match($color) { - 'red' => 'border-l-red-500', - 'yellow' => 'border-l-yellow-500', - 'neon' => 'border-l-neon-lime', - default => 'border-l-gray-500' + 'red' => 'border-l-red-500 dark:border-l-red-500', + 'yellow' => 'border-l-yellow-500 dark:border-l-yellow-500', + 'neon' => 'border-l-neon-lime dark:border-l-neon-lime', + default => 'border-l-gray-500 dark:border-l-gray-500' }; @endphp diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 0db80de..1372d8b 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -51,6 +51,15 @@ + @if(auth()->user()->role === 'admin') + + + + + + + @endif +
diff --git a/resources/views/login.blade.php b/resources/views/login.blade.php index 1cc3e0d..a9a728d 100644 --- a/resources/views/login.blade.php +++ b/resources/views/login.blade.php @@ -33,12 +33,30 @@
- @endif
- +
+ + +
-
+ class="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-700 dark:hover:text-white transition-colors"> + {{-- Ojo abierto --}} + + + + + {{-- Ojo cerrado --}} + +
diff --git a/resources/views/taller/index.blade.php b/resources/views/taller/index.blade.php index de13893..51c01ec 100644 --- a/resources/views/taller/index.blade.php +++ b/resources/views/taller/index.blade.php @@ -1,8 +1,6 @@ - -
-
- - -
-
-

🔴 Pendientes / A Revisar

- {{ $pending->count() }} + +
+ +
+

+ 🔴 Pendientes / A Revisar +

+ + {{ $pending->count() }} +
+
@foreach($pending as $job) @@ -35,12 +39,22 @@
- -
-
-

🟡 En Reparación

- {{ $inProgress->count() }} + +
+ +
+

+ 🟡 En Reparación +

+ + {{ $inProgress->count() }} +
+
@foreach($inProgress as $job) @@ -48,12 +62,22 @@
- -
-
-

🟢 Listas para Retirar

- {{ $ready->count() }} + +
+ +
+

+ 🟢 Listas para Retirar +

+ + {{ $ready->count() }} +
+
@foreach($ready as $job) @@ -63,50 +87,4 @@
-
- -
-

- 🟡 En Reparación -

- - {{ $inProgress->count() }} - -
- -
- @foreach($inProgress as $job) - - @endforeach -
-
- -
- -
-

- 🟢 Listas para Retirar -

- - {{ $ready->count() }} - -
- -
- @foreach($ready as $job) - - @endforeach -
-
- -
- \ No newline at end of file diff --git a/routes/console.php b/routes/console.php index 3c9adf1..2cef2f6 100644 --- a/routes/console.php +++ b/routes/console.php @@ -2,7 +2,10 @@ use Illuminate\Foundation\Inspiring; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Schedule; Artisan::command('inspire', function () { $this->comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); + +Schedule::command('db:backup')->weekly(); diff --git a/routes/web.php b/routes/web.php index 118f43b..79b1e15 100644 --- a/routes/web.php +++ b/routes/web.php @@ -15,6 +15,7 @@ use App\Http\Controllers\AppointmentController; use App\Http\Controllers\SupplierController; use App\Http\Controllers\PasswordResetLinkController; use App\Http\Controllers\NewPasswordController; +use App\Http\Controllers\Admin\BackupController; use App\Models\Product; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Session; @@ -75,4 +76,14 @@ Route::middleware(['auth'])->group(function () { Route::get('/appointments/{appointment}', [AppointmentController::class, 'show']) ->name('appointments.show'); Route::view('/faq', 'faq')->name('faq'); + + // Copias de Seguridad (Solo Administradores) + Route::middleware(['admin'])->prefix('admin')->name('admin.')->group(function () { + Route::get('/backups', [BackupController::class, 'index'])->name('backups.index'); + Route::post('/backups/create', [BackupController::class, 'create'])->name('backups.create'); + Route::post('/backups/upload', [BackupController::class, 'upload'])->name('backups.upload'); + Route::post('/backups/{filename}/restore', [BackupController::class, 'restore'])->name('backups.restore'); + Route::get('/backups/{filename}/download', [BackupController::class, 'download'])->name('backups.download'); + Route::delete('/backups/{filename}', [BackupController::class, 'destroy'])->name('backups.destroy'); + }); }); From e8f022ca7e5b0e687838405d89b39065004f2a03 Mon Sep 17 00:00:00 2001 From: Coassolo-Lucas Date: Wed, 27 May 2026 21:22:57 -0300 Subject: [PATCH 7/7] Imagenes en productos. --- app/Http/Controllers/ProductosController.php | 60 +++++++- composer.json | 1 + composer.lock | 146 ++++++++++++++++++- public/.htaccess | 9 ++ public/.user.ini | 4 + 5 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 public/.user.ini 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/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/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] + + + php_value upload_max_filesize 20M + php_value post_max_size 20M + php_value memory_limit 512M + + + + diff --git a/public/.user.ini b/public/.user.ini new file mode 100644 index 0000000..e55f623 --- /dev/null +++ b/public/.user.ini @@ -0,0 +1,4 @@ +upload_max_filesize = 20M +post_max_size = 20M +max_execution_time = 300 +memory_limit = 256M