From dc400e010213e006f511265330413c6c77639f99 Mon Sep 17 00:00:00 2001 From: gianella Date: Thu, 8 Jan 2026 19:41:21 -0300 Subject: [PATCH] creo que pude instalar select2, create sales y crud client o casi crud --- app/Http/Controllers/ClientController.php | 81 +++- app/Http/Controllers/SaleController.php | 61 ++- package-lock.json | 9 +- package.json | 3 +- resources/js/app.js | 12 + resources/views/clients/create.blade.php | 52 ++- resources/views/clients/edit.blade.php | 79 ++++ resources/views/clients/index.blade.php | 105 +++++ resources/views/dashboard.blade.php | 2 +- resources/views/sales/create.blade.php | 460 +++++++++++++--------- routes/web.php | 2 +- 11 files changed, 612 insertions(+), 254 deletions(-) create mode 100644 resources/views/clients/edit.blade.php create mode 100644 resources/views/clients/index.blade.php diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index fcf5db8..b456d43 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -3,15 +3,28 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; +use App\Models\Client; class ClientController extends Controller { /** * Display a listing of the resource. */ - public function index() + public function index(Request $request) { - // + $query = $request->input('search'); + + $clients = Client::query() + ->when($query, function ($q) use ($query) { + // Si hay búsqueda, filtra por nombre o SKU + return $q->where('name', 'like', "%{$query}%"); + }) + ->orderBy('name', 'asc') + //->orderBy('create_at', 'asc') + ->paginate(10) // Paginamos de a 10 + ->withQueryString(); // Mantiene el filtro de búsqueda al cambiar de página + + return view('clients.index', compact('clients')); } /** @@ -19,7 +32,7 @@ class ClientController extends Controller */ public function create() { - // + return view('clients.create'); } /** @@ -27,7 +40,28 @@ class ClientController extends Controller */ public function store(Request $request) { - // + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'phone' => 'nullable|string|max:50', + 'email' => 'nullable|email|max:255|unique:clients,email', + 'address' => 'nullable|string|max:255', + ]); + + // 1. Guardamos el cliente en una variable para tener su ID + $client = Client::create($validated); + + // 2. Verificamos el origen + if ($request->input('origin') === 'sales') { + + // Si vino de ventas, volvemos a ventas + // Y pasamos el ID del nuevo cliente para auto-seleccionarlo + return redirect()->route('sales.create', ['new_client_id' => $client->id]) + ->with('success', 'Cliente creado. Ya puedes seleccionarlo.'); + } + + // 3. Si no, comportamiento normal (volver al index de clientes) + return redirect()->route('clients.index') + ->with('success', 'Cliente creado correctamente.'); } /** @@ -38,27 +72,38 @@ class ClientController extends Controller // } - /** - * Show the form for editing the specified resource. - */ - public function edit(string $id) + public function edit(Client $client) { - // + // Reutilizamos la vista de create, o creamos una edit.blade.php similar + return view('clients.edit', compact('client')); + } + + public function update(Request $request, Client $client) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'phone' => 'nullable|string|max:50', + 'email' => 'nullable|email|max:255|unique:clients,email,' . $client->id, // Ignorar email propio + 'address' => 'nullable|string|max:255', + ]); + + $client->update($validated); + + return redirect()->route('clients.index')->with('success', 'Cliente actualizado correctamente.'); } - /** - * Update the specified resource in storage. - */ - public function update(Request $request, string $id) - { - // - } /** * Remove the specified resource from storage. */ - public function destroy(string $id) + public function destroy(Client $client) { - // + try { + $client->delete(); + return redirect()->route('clients.index')->with('success', 'Cliente eliminado correctamente.'); + } catch (\Illuminate\Database\QueryException $e) { + + return back()->with('error', 'No se puede eliminar el cliente porque tiene ventas registradas.'); + } } } diff --git a/app/Http/Controllers/SaleController.php b/app/Http/Controllers/SaleController.php index 8e8a7f7..b7f3832 100644 --- a/app/Http/Controllers/SaleController.php +++ b/app/Http/Controllers/SaleController.php @@ -3,78 +3,77 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; -use App\Models\Product; +use App\Models\Product; +use App\Models\Client; use App\Models\Sale; use App\Models\SaleDetail; use Illuminate\Support\Facades\DB; -// -use App\Models\Client; class SaleController extends Controller { public function create() { - // Buscamos productos que tengan stock mayor a 0 para mostrar en el selector + // Traemos productos con stock y clientes para los selectores $products = Product::where('stock_quantity', '>', 0)->get(); - - $clients = \App\Models\Client::orderBy('name')->get(); + $clients = Client::orderBy('name')->get(); return view('sales.create', compact('products', 'clients')); } public function store(Request $request) { + // Validación estricta $request->validate([ + 'client_id' => 'nullable|exists:clients,id', 'payment_method' => 'required|string', - 'client_id' => 'nullable|exists:clients,id', - 'items' => 'required|array', + 'items' => 'required|array', 'items.*.product_id' => 'required|exists:products,id', - 'items.*.quantity' => 'required|integer|min:1', + 'items.*.quantity' => 'required|integer|min:1', ]); try { DB::transaction(function () use ($request) { - $totalSale = 0; + + $totalVenta = 0; - foreach ($request->items as $item) { - $product = Product::find($item['product_id']); - $totalSale += $product->precio * $item['quantity']; + // 1. Calcular total previo (Solo habrá 1 item por ahora, pero el bucle sirve igual) + foreach ($request->items as $itemData) { + $product = Product::findOrFail($itemData['product_id']); - if ($product->stock_quantity < $item['quantity']) { - throw new \Exception("No hay suficiente stock de " . $product->nombre); + if ($product->stock_quantity < $itemData['quantity']) { + throw new \Exception("Stock insuficiente para: " . $product->name); } + + $totalVenta += $product->price * $itemData['quantity']; } - //cabecera + // 2. Crear Venta $sale = Sale::create([ - //'user_id' => auth()->id(), //empleado logueado - 'client_id' => $request->client_id, - 'total' => $totalSale, + 'client_id' => $request->client_id, + 'total' => $totalVenta, 'payment_method' => $request->payment_method, + // 'user_id' => auth()->id(), // Descomenta si usas autenticación ]); - foreach ($request->items as $item) { - $product = Product::find($item['product_id']); + // 3. Guardar Detalle y Restar Stock + foreach ($request->items as $itemData) { + $product = Product::findOrFail($itemData['product_id']); SaleDetail::create([ - 'sale_id' => $sale->id, + 'sale_id' => $sale->id, 'product_id' => $product->id, - 'quantity' => $item['quantity'], - 'price' => $product->price, + 'quantity' => $itemData['quantity'], + 'price' => $product->price, ]); - $nuevoStock = $product->stock_quantity - $item['quantity']; - $product->stock_quantity = $nuevoStock; - $product->save(); + + $product->decrement('stock_quantity', $itemData['quantity']); } }); - // 5. REDIRECCIÓN DE ÉXITO - // Redirigimos al usuario con un mensaje flash [cite: 668] return redirect()->route('sales.create')->with('success', '¡Venta registrada correctamente!'); } catch (\Exception $e) { - // Si algo falló (ej: falta stock), volvemos atrás con el error - return back()->with('error', 'Error en la venta: ' . $e->getMessage()); + return back()->with('error', $e->getMessage())->withInput(); } } } diff --git a/package-lock.json b/package-lock.json index 1f1aba3..278d680 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,8 @@ "": { "dependencies": { "@tailwindcss/postcss": "^4.1.18", - "jquery": "^3.7.1" + "jquery": "^3.7.1", + "select2": "^4.1.0-rc.0" }, "devDependencies": { "@tailwindcss/vite": "^4.1.18", @@ -2243,6 +2244,12 @@ "tslib": "^2.1.0" } }, + "node_modules/select2": { + "version": "4.1.0-rc.0", + "resolved": "https://registry.npmjs.org/select2/-/select2-4.1.0-rc.0.tgz", + "integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A==", + "license": "MIT" + }, "node_modules/shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", diff --git a/package.json b/package.json index 385e02e..259ef1c 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@tailwindcss/postcss": "^4.1.18", - "jquery": "^3.7.1" + "jquery": "^3.7.1", + "select2": "^4.1.0-rc.0" } } diff --git a/resources/js/app.js b/resources/js/app.js index e59d6a0..d0ead51 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +1,13 @@ import './bootstrap'; +// 1. Importar jQuery +import jQuery from 'jquery'; + +// 2. Hacerlo global (IMPORTANTE para que funcione $(document).ready en Blade) +window.$ = window.jQuery = jQuery; + +// 3. Importar Select2 +import select2 from 'select2'; +select2(); // Inicializar el plugin + +// 4. Importar los estilos de Select2 (Opcional aquí, o en CSS) +import 'select2/dist/css/select2.css'; diff --git a/resources/views/clients/create.blade.php b/resources/views/clients/create.blade.php index 6265ad0..5bddb0b 100644 --- a/resources/views/clients/create.blade.php +++ b/resources/views/clients/create.blade.php @@ -1,5 +1,49 @@ -@extends('layouts.app') -@section('main') -

formulario de crear cliente

+ -@endsection \ No newline at end of file + + +
+ +
+ @csrf + + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + @error('address') + {{ $message }} + @enderror +
+ +
+ +
+ Cancelar + + +
+ +
+
+
\ No newline at end of file diff --git a/resources/views/clients/edit.blade.php b/resources/views/clients/edit.blade.php new file mode 100644 index 0000000..bb10afb --- /dev/null +++ b/resources/views/clients/edit.blade.php @@ -0,0 +1,79 @@ + + + + +
+ +
+ @csrf + @method('PUT')
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + @error('address') + {{ $message }} + @enderror +
+ +
+ +
+ + + Cancelar + + + +
+ +
+
+
\ No newline at end of file diff --git a/resources/views/clients/index.blade.php b/resources/views/clients/index.blade.php new file mode 100644 index 0000000..d2853b0 --- /dev/null +++ b/resources/views/clients/index.blade.php @@ -0,0 +1,105 @@ + + + + + @if(session('success')) +
+ {{ session('success') }} +
+ @endif + +
+ +
+
+
+ +
+ +
+
+ + + + Nuevo Cliente + +
+ +
+ + + + + + + + + + + @forelse($clients as $client) + + + + + + + + + + + + @empty + + + + @endforelse + +
Cliente / EmailTeléfono / WhatsAppDirecciónAcciones
+
{{ $client->name }}
+
+ {{ $client->email ?? 'Sin email registrado' }} +
+
+ @if($client->phone) +
+ {{ $client->phone }} + WA +
+ @else + No registrado + @endif +
+ + {{ $client->address ?? '-' }} + + + + Editar + + +
+ @csrf + @method('DELETE') + + +
+
+
+ + + +

No se encontraron clientes.

+
+
+
+ +
+ {{ $clients->links() }} +
+ +
\ No newline at end of file diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 4be0926..a085c3a 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -21,7 +21,7 @@ - + diff --git a/resources/views/sales/create.blade.php b/resources/views/sales/create.blade.php index 26ec74a..5dc5ace 100644 --- a/resources/views/sales/create.blade.php +++ b/resources/views/sales/create.blade.php @@ -1,223 +1,289 @@ - -
-
- -
-
-

Nueva Venta en Mostrador

-
- - @if (session('success')) -
- ¡Éxito! {{ session('success') }} -
- @endif - - @if (session('error')) -
- Error: {{ session('error') }} -
- @endif - - @if ($errors->any()) -
-
    - @foreach ($errors->all() as $error) -
  • • {{ $error }}
  • + + @push('styles') + + @endpush + + + +
    + + @if(session('success')) +
    + {{ session('success') }} +
    + @endif + @if(session('error')) +
    + {{ session('error') }} +
    + @endif + @if($errors->any()) +
    +
      + @foreach($errors->all() as $error) +
    • {{ $error }}
    • + @endforeach +
    +
    + @endif + +
    + @csrf + +
    +
    + +
    +
    - @endif - - - - - @csrf - -
    - -
    - + + @foreach($products as $product) + @endforeach - - - + -
    -
    - -
    -
    -
    - - -
    - -
    - - -
    - -
    - $0.00 -
    - -
    - -
    + +
    + +
    -
    - -
    - -
    - -
    -
    - Total a Pagar: $0.00 + +
    + Subtotal + $0.00
    -
    - -
    -
    - - -
    - -
    - - Cancelar - -
    - +
    + +
    + +
    + +
    +
    + Monto Total a Pagar + $0.00 +
    +
    -
    + +
    + +
    + + +
    + +
    + + Cancelar + + +
    +
    + +
    - -@push('scripts') - -@endpush \ No newline at end of file + function calcularFila(row) { + let select = row.find('.product-select'); + let input = row.find('.quantity-input'); + let option = select.find(':selected'); + let price = parseFloat(option.data('price')) || 0; + let qty = parseInt(input.val()) || 0; + let subtotal = price * qty; + row.find('.row-total').text('$' + subtotal.toFixed(2)); + } + + function recalcularTodo() { + let grandTotal = 0; + $('.item-row').each(function() { + let row = $(this); + let price = parseFloat(row.find('.product-select').find(':selected').data('price')) || 0; + let qty = parseInt(row.find('.quantity-input').val()) || 0; + grandTotal += (price * qty); + }); + $('#grand-total').text('$' + grandTotal.toFixed(2)); + } + + function actualizarContadorItems() { + $('#item-count').text($('.item-row').length); + } + + function actualizarBloqueos() { + let seleccionados = []; + $('.product-select').each(function() { + let valor = $(this).val(); + if (valor) seleccionados.push(valor); + }); + + $('.product-select').each(function() { + let selectActual = $(this); + let miValor = selectActual.val(); + selectActual.find('option').each(function() { + let opcion = $(this); + let valorOpcion = opcion.val(); + if (!valorOpcion) return; + + if (seleccionados.includes(valorOpcion) && valorOpcion != miValor) { + opcion.prop('disabled', true); + } else { + opcion.prop('disabled', false); + } + }); + }); + } + + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index ea79e8f..0e86915 100644 --- a/routes/web.php +++ b/routes/web.php @@ -47,7 +47,7 @@ Route::middleware(['auth'])->group(function () { //Route::get('/sales', [SaleController::class, 'index'])->name('sales.index'); Route::get('/sales/create', [SaleController::class, 'create'])->name('sales.create'); - Route::get('/sales/store', [SaleController::class, 'store'])->name('sales.store'); + Route::post('/sales/store', [SaleController::class, 'store'])->name('sales.store'); });