diff --git a/app/Http/Controllers/CatalogoController.php b/app/Http/Controllers/CatalogoController.php index b7e5632..e74a30a 100644 --- a/app/Http/Controllers/CatalogoController.php +++ b/app/Http/Controllers/CatalogoController.php @@ -13,4 +13,28 @@ class CatalogoController extends Controller $productos = Product::all(); return view('catalogo', compact('productos')); } + + /** + * Muestra los registros de ventas con buscador y paginación. + */ + public function index(Request $request) + { + // Recuperamos lo que el usuario escribió en el buscador (si escribió algo) + $query = $request->input('search'); + // Recuperamos el filtro aplicado (si aplica) + $filter = $request->input('filter'); + + // Construimos la consulta + $sales = Product::query() + ->when($query, function ($q) use ($query) { + // Si hay búsqueda, filtra por nombre o SKU + return $q->where('name', 'like', "%{$query}%") + ->orWhere('sku', 'like', "%{$query}%"); + }) + ->orderBy('stock_quantity', 'asc') // Ordenamos primero los que tienen poco stock (Alerta visual) + ->paginate(10) // Paginamos de a 10 + ->withQueryString(); // Mantiene el filtro de búsqueda al cambiar de página + + return view('sales', compact('sales')); + } } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php new file mode 100644 index 0000000..fcf5db8 --- /dev/null +++ b/app/Http/Controllers/ClientController.php @@ -0,0 +1,64 @@ +', 0)->get(); + + $clients = \App\Models\Client::orderBy('nombre')->get(); + + return view('sales.create', compact('products', 'clients')); + } + + public function store(Request $request) + { + $request->validate([ + 'payment_method' => 'required|string', + 'client_id' => 'nullable|exists:clients,id', + 'items' => 'required|array', + 'items.*.product_id' => 'required|exists:products,id', + 'items.*.quantity' => 'required|integer|min:1', + ]); + + try { + DB::transaction(function () use ($request) { + $totalVenta = 0; + + foreach ($request->items as $item) { + $product = Product::find($item['product_id']); + $totalVenta += $product->precio * $item['quantity']; + + if ($product->stock < $item['quantity']) { + throw new \Exception("No hay suficiente stock de " . $product->nombre); + } + } + + //cabecera + $sale = Sale::create([ + //'user_id' => auth()->id(), //empleado logueado + 'client_id' => $request->client_id, + 'total' => $totalVenta, + 'payment_method' => $request->payment_method, + ]); + + foreach ($request->items as $item) { + $product = Product::find($item['product_id']); + + SaleDetail::create([ + 'sale_id' => $sale->id, + 'product_id' => $product->id, + 'cantidad' => $item['quantity'], + 'precio' => $product->precio, + ]); + $nuevoStock = $product->stock - $item['quantity']; + $product->stock = $nuevoStock; + $product->save(); + } + }); + + // 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()); + } + } +} diff --git a/app/Models/Client.php b/app/Models/Client.php index 236bd9b..6286e3f 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -8,9 +8,16 @@ use Illuminate\Database\Eloquent\Model; class Client extends Model { use HasFactory; - + protected $guarded = []; protected $fillable = ['name', 'phone', 'email', 'address']; + // Un usuario (cliente) realiza muchas compras (ventas) + public function sales() + { + return $this->hasMany(Sale::class); + } + + // Relación: Un cliente tiene muchos turnos public function appointments() { diff --git a/app/Models/Product.php b/app/Models/Product.php index bda64e3..25d29cf 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -8,21 +8,34 @@ use Illuminate\Database\Eloquent\Model; class Product extends Model { use HasFactory; + protected $guarded = []; - protected $fillable = [ - 'name', - 'sku', - 'description', - 'price', - 'cost', - 'stock_quantity', - 'min_stock_alert', - 'type', - 'serial_number' - ]; public function hasLowStock(): bool { + //cambiar return $this->stock_quantity <= $this->min_stock_alert; } + + public function supplier() + { + return $this->belongsTo(Suppliers::class); + } + + // Opción 1: Relación directa con los detalles (Renglones de ticket) + // Útil para saber cantidad total vendida: $product->saleDetails->sum('quantity') + public function saleDetails() + { + return $this->hasMany(SaleDetail::class); + } + + // Opción 2 (Pro Tip): Relación directa con las Ventas a través de los detalles + // Útil para saber EN QUÉ fechas se vendió: $product->sales + public function sales() + { + return $this->belongsToMany(Sale::class, 'sale_details'); + } } +/**$user->sales: Te da la lista de todas las compras de ese usuario. + +$product->saleDetails->count(): Te dice cuántas veces aparece ese producto en tickets.**/ \ No newline at end of file diff --git a/app/Models/Sale.php b/app/Models/Sale.php new file mode 100644 index 0000000..aeeca90 --- /dev/null +++ b/app/Models/Sale.php @@ -0,0 +1,25 @@ +belongsTo(User::class); + } + + // Relación 2: Una venta tiene muchos items o detalles + public function details() + { + return $this->hasMany(SaleDetail::class); + } +} diff --git a/app/Models/SaleDetail.php b/app/Models/SaleDetail.php new file mode 100644 index 0000000..597eff4 --- /dev/null +++ b/app/Models/SaleDetail.php @@ -0,0 +1,23 @@ +belongsTo(Sale::class); + } + + // este detalle corresponde a un Producto + public function product() + { + return $this->belongsTo(Product::class); + } +} \ No newline at end of file diff --git a/app/Models/Supplier.php b/app/Models/Supplier.php new file mode 100644 index 0000000..8acf047 --- /dev/null +++ b/app/Models/Supplier.php @@ -0,0 +1,18 @@ +hasMany(Product::class); + } +} diff --git a/database/migrations/2025_12_06_182356_create_suppliers_table.php b/database/migrations/2025_12_06_182356_create_suppliers_table.php new file mode 100644 index 0000000..c5e0c12 --- /dev/null +++ b/database/migrations/2025_12_06_182356_create_suppliers_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('nombre'); + $table->string('telefono'); + $table->string('email'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('suppliers'); + } +}; diff --git a/database/migrations/2025_08_05_005156_create_products_table.php b/database/migrations/2025_12_06_182358_create_products_table.php similarity index 94% rename from database/migrations/2025_08_05_005156_create_products_table.php rename to database/migrations/2025_12_06_182358_create_products_table.php index d044a7f..5b743f7 100644 --- a/database/migrations/2025_08_05_005156_create_products_table.php +++ b/database/migrations/2025_12_06_182358_create_products_table.php @@ -26,6 +26,7 @@ return new class extends Migration $table->enum('type', ['bike', 'accessory', 'service']); $table->string('serial_number')->nullable(); // Solo para bicis + $table->foreignId('suppliers_id')->constrained(); $table->timestamps(); }); } @@ -37,4 +38,4 @@ return new class extends Migration { Schema::dropIfExists('products'); } -}; +}; \ No newline at end of file diff --git a/database/migrations/2025_12_06_184833_create_clients_table.php b/database/migrations/2025_12_06_184833_create_clients_table.php new file mode 100644 index 0000000..398e55b --- /dev/null +++ b/database/migrations/2025_12_06_184833_create_clients_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('nombre'); + $table->string('telefono'); + $table->string('email'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('clients'); + } +}; diff --git a/database/migrations/2025_12_06_193913_create_sales_table.php b/database/migrations/2025_12_06_193913_create_sales_table.php new file mode 100644 index 0000000..2125264 --- /dev/null +++ b/database/migrations/2025_12_06_193913_create_sales_table.php @@ -0,0 +1,32 @@ +id(); + //$table->foreignId('user_id')->constrained(); + // nullable para que no sea obligatrio + $table->foreignId('client_id')->nullable()->constrained(); + $table->decimal('total', 10, 2); + $table->string('payment_method'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sales'); + } +}; diff --git a/database/migrations/2025_12_06_201853_create_sale_details_table.php b/database/migrations/2025_12_06_201853_create_sale_details_table.php new file mode 100644 index 0000000..e357fa9 --- /dev/null +++ b/database/migrations/2025_12_06_201853_create_sale_details_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('sale_id')->constrained()->onDelete('cascade'); + $table->foreignId('product_id')->constrained(); + $table->integer('cantidad'); + $table->decimal('precio', 10, 2); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sale_details'); + } +}; diff --git a/database/migrations/2025_12_07_023045_create_appointments_table.php b/database/migrations/2025_12_07_023045_create_appointments_table.php new file mode 100644 index 0000000..3bac73b --- /dev/null +++ b/database/migrations/2025_12_07_023045_create_appointments_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('client_id')->constrained(); + $table->datetime('fecha_programada'); + $table->string('modelo_bici')->nullable(); + $table->string('descripcion')->nullable(); + $table->string('estado')->default('pendiente'); + $table->string('notas')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('appointments'); + } +}; diff --git a/resources/a.md b/resources/a.md deleted file mode 100644 index ad025d6..0000000 --- a/resources/a.md +++ /dev/null @@ -1,165 +0,0 @@ -¡Perfecto\! Tus respuestas aclaran muchísimo el panorama. Al ser un sistema **Intranet (Local)** con un **Admin** y **Empleados** (visores/operadores), y con procesos manuales en el taller, podemos simplificar la arquitectura para lanzar un MVP (Producto Mínimo Viable) rápido y robusto, pero preparado para crecer. - -Aquí tienes la **Guía Maestra de Desarrollo para Lauck Systems**. - ------ - -### 1\. Arquitectura de Base de Datos (El Cimiento) - -Antes de escribir código, definamos las tablas. Laravel hace esto fácil con migraciones. - -#### Tablas Principales: - -1. **`users`** (Ya viene en Laravel) - - * Agregamos campo: `role` (enum: 'admin', 'employee'). - * *Lógica:* El Admin crea, edita y borra. El Empleado solo ve y registra ventas/turnos (según definas). - -2. **`clients`** (Clientes) - - * `name`, `phone` (clave para WhatsApp), `email`, `address`. - * *Nota:* Fundamental para agendar turnos y registrar garantías. - -3. **`products`** (Inventario Mixto) - - * `name`, `sku` (código único), `description`. - * `price` (precio venta), `cost` (costo, solo visible para admin). - * `stock_quantity` (entero). - * `min_stock_alert` (entero, para la alerta que pediste). - * `type` (enum: 'bike', 'accessory', 'service'). - * `serial_number` (nullable, solo para bicicletas). - -4. **`appointments`** (Turnos / Taller) - - * `client_id` (relación). - * `scheduled_at` (datetime - fecha y hora del turno). - * `bike_model` (texto libre, ej: "Venzo Loki 29"). - * `problem_description` (motivo de la consulta). - * `status` (enum: 'pending', 'confirmed', 'in\_progress', 'ready', 'delivered'). - * `notes` (uso interno del mecánico). - -5. **`sales`** (Ventas Internas / Historial) - - * `user_id` (quién vendió). - * `client_id` (opcional, si es consumidor final anónimo). - * `total_amount`. - * `payment_method` (efectivo, tarjeta, transferencia). - * `created_at` (fecha de venta). - -6. **`sale_items`** (Detalle de venta) - - * `sale_id`, `product_id`, `quantity`, `unit_price`. - ------ - -### 2\. Estructura de Rutas y Controladores (Backend) - -En Laravel, organizaremos esto por "Dominios". - - * **Autenticación:** (Ya lo tienes con Breeze/Jetstream). - * **DashboardController:** - * `index()`: Muestra las tarjetas, alertas de stock bajo (query simple `Product::whereColumn('stock', '<=', 'min_stock')->get()`) y turnos de hoy. - * **ProductController:** - * CRUD completo (Crear, Leer, Actualizar, Borrar). - * Función extra `search()`: Para el buscador de precios de los empleados. - * **ClientController:** - * CRUD simple. - * **AppointmentController (Gestión de Taller):** - * `calendar()`: Vista de calendario o lista cronológica. - * `statusUpdate()`: Para mover el turno de "Pendiente" a "Listo". - * **SaleController:** - * `create()`: Formulario para registrar una salida de mercadería. - * `store()`: Resta el stock y guarda la venta. - ------ - -### 3\. Componentes de UI (Frontend - Blade + Tailwind) - -Para mantener el estilo "Lauck" que ya definimos, necesitaremos crear estos componentes reutilizables (además de los que ya tienes): - -1. **`x-ui.status-badge`**: - * Una etiqueta pequeña redondeada que cambia de color según el estado (Verde para 'Stock Alto', Rojo para 'Sin Stock' o 'Turno Atrasado', Amarillo para 'En Reparación'). -2. **`x-ui.table`**: - * Una tabla estilizada con el modo oscuro, filas alternadas y cabeceras fijas. Vital para listas de precios y clientes. -3. **`x-forms.input` / `x-forms.select`**: - * Inputs con el estilo oscuro y borde neón al hacer foco, para no repetir las clases de Tailwind en cada formulario. -4. **`x-ui.alert`**: - * Para mostrar mensajes de éxito ("Producto guardado") o alertas ("¡Quedan solo 2 cámaras rodado 29\!"). - ------ - -### 4\. Funcionalidades Específicas a Implementar - -Aquí está la lógica para los requerimientos que mencionaste: - -#### A. Alertas de Stock ⚠️ - - * **Lógica:** No necesitas un sistema complejo de notificaciones en tiempo real todavía. - * **Implementación:** En el `DashboardController`, pasas una variable `$lowStockProducts` a la vista. - * **Vista:** En el Dashboard, si esa lista no está vacía, muestras una tarjeta roja o amarilla avisando "X productos con stock crítico". - -#### B. Consulta de Precios (Modo Solo Lectura) 🔍 - - * **Requerimiento:** Empleados consultan, no editan. - * **Implementación:** - * Crear una vista `products.checker`. - * Un input de búsqueda grande en el centro. - * Al escribir (AJAX o Livewire sería ideal aquí, pero un form simple con botón "Buscar" funciona), muestra una tarjeta gigante con el Nombre y el Precio. - * *Seguridad:* Usar **Laravel Gates** o **Policies**. - ```php - // En AuthServiceProvider - Gate::define('edit-products', function ($user) { - return $user->role === 'admin'; - }); - ``` - En Blade: `@can('edit-products') @endcan`. Así el empleado ve el producto pero no el botón de editar. - -#### C. Agenda de Turnos 📅 - - * **Lógica:** "Consulta -\> Cita". - * **Implementación:** - * No te compliques con un calendario visual complejo (tipo Google Calendar) al principio. - * Usa una **Lista Agrupada por Días**. - * *Ejemplo visual:* - * **HOY:** - * 09:00 - Jose (Pincharura) [Ver] - * 10:30 - Maria (Service General) [Ver] - * **MAÑANA:** - * ... - ------ - -### 5\. Roadmap Sugerido (Paso a Paso) - -Este es el orden lógico para programar sin perderse: - -1. **Semana 1: Cimientos y Stock (Lo más urgente)** - - * Configurar Migraciones (`products`, `clients`). - * Crear Modelos y Seeders (datos falsos para probar). - * Hacer el CRUD de Productos (Alta, Baja y Modificación). - * *Hito:* Poder cargar una bicicleta y verla en la lista. - -2. **Semana 2: Seguridad y Consultas** - - * Agregar campo `role` a Users. - * Crear la vista "Consulta de Precios" (solo lectura). - * Proteger las rutas de edición para que solo el Admin entre. - * *Hito:* El empleado puede loguearse y buscar un precio, pero no borrar nada. - -3. **Semana 3: El Taller (Turnos)** - - * Crear migración `appointments`. - * Crear formulario para "Nuevo Turno" (Seleccionar Cliente + Fecha + Motivo). - * Crear vista de "Lista de Turnos" en el Dashboard. - * *Hito:* Dejar de usar el cuaderno de papel para los turnos. - -4. **Semana 4: Refinamiento** - - * Alertas de stock visuales. - * Mejoras estéticas (Dark Mode en tablas). - * Pruebas finales en el servidor local. - -### ¿Cómo seguimos? - -¿Te gustaría que generemos el código para la **Migración de Productos** y el **Modelo**, o prefieres que diseñemos primero el componente visual de la **Tabla de Stock**? \ No newline at end of file diff --git a/resources/views/clients/create.blade.php b/resources/views/clients/create.blade.php new file mode 100644 index 0000000..6265ad0 --- /dev/null +++ b/resources/views/clients/create.blade.php @@ -0,0 +1,5 @@ +@extends('layouts.app') +@section('main') +

formulario de crear cliente

+ +@endsection \ No newline at end of file diff --git a/resources/views/components/forms/select.blade.php b/resources/views/components/forms/select.blade.php index 1346f60..697461c 100644 --- a/resources/views/components/forms/select.blade.php +++ b/resources/views/components/forms/select.blade.php @@ -1,13 +1,39 @@ -@props(['disabled' => false, 'options', 'error' => null]) +@props(['disabled' => false, 'error' => null, 'options' => [], 'placeholder' => 'Seleccionar...']) -merge(['class' => ' + appearance-none bg-transparent border-0 border-b-2 border-neutral-700 text-white text-sm + py-2.5 px-0 w-full focus:outline-none focus:ring-0 focus:border-neon-lime peer cursor-pointer + transition-colors' . ($error ? 'border-red-500 focus:border-red-500' : '') + ]) !!}> + + @if($placeholder) + + @endif + + {{ $slot }} + + + + +
+ + + +
+ + + @if($error) +

{{ $error }}

@endif - -@if($error) -

{{ $error }}

-@endif \ No newline at end of file + + +{{-- + NOTA DE ESTILO: + - `appearance-none`: Quita el estilo feo por defecto del navegador. + - `bg-transparent`: Fondo transparente para que se vea el color de fondo de tu web. + - `border-b-2`: Borde solo abajo (estilo línea). + - `focus:ring-0`: Quita el anillo azul de Chrome al hacer click. + - `peer`: Permite que el icono de la flecha cambie de color cuando el select tiene foco. +--}} \ No newline at end of file diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index c245307..22715e9 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -36,11 +36,16 @@
  • - Taller + + Taller +
  • - Ventas -
  • + + Ventas + diff --git a/resources/views/components/section-header.blade.php b/resources/views/components/section-header.blade.php index 4864bbf..560d266 100644 --- a/resources/views/components/section-header.blade.php +++ b/resources/views/components/section-header.blade.php @@ -5,7 +5,7 @@

    {{ $title }} @if($highlight) - {{ $highlight }} + {{ $highlight }} @endif

    \ No newline at end of file diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php deleted file mode 100644 index 67c46ad..0000000 --- a/resources/views/layouts/app.blade.php +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - @yield('title','Lauck - Home') - - - -
    Cabeza
    - - @yield('main') - - - - \ No newline at end of file diff --git a/resources/views/sales.blade.php b/resources/views/sales.blade.php new file mode 100644 index 0000000..40a18ab --- /dev/null +++ b/resources/views/sales.blade.php @@ -0,0 +1,91 @@ + + + + + + + +
    + + +
    +
    +
    + +
    + +
    +
    + + + + + +
    +
    + + + + + Registrar Venta + +
    + + +
    + + + + + + + + + + + + @forelse($sales as $product) + + + + + + + + @empty + + + + @endforelse + +
    ID VentaDescripcionCant. ProductosTotal VentaAcciones
    +
    {{ $product->sku }}
    +
    +
    {{ $product->name }}
    +
    + {{ $product->stock_quantity }} + + ${{ number_format(($product->price * 2), 2) }} + {{-- @if($product->type === 'service') + - + @elseif($product->stock_quantity <= $product->min_stock_alert) + {{ $product->stock_quantity }} + @else + {{ $product->stock_quantity }} + @endif --}} + + Editar +
    + No se encontraron productos. +
    +
    + + +
    + {{ $sales->links() }} +
    + +
    \ No newline at end of file diff --git a/resources/views/sales/create.blade.php b/resources/views/sales/create.blade.php new file mode 100644 index 0000000..fd20768 --- /dev/null +++ b/resources/views/sales/create.blade.php @@ -0,0 +1,227 @@ +@extends('layouts.app') + +@section('title', 'Nueva Venta') + +@section('main') +
    +
    + +
    +
    +

    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 }}
    • + @endforeach +
    +
    + @endif + + + +
    + @csrf + +
    + +
    + + + + + + +
    +
    + +
    +
    +
    + + +
    + +
    + + +
    + +
    + $0.00 +
    + +
    + +
    +
    +
    + +
    + +
    + +
    +
    + Total a Pagar: $0.00 +
    +
    + +
    +
    + + +
    + +
    + + Cancelar + + +
    +
    +
    +
    +
    +
    + +@push('scripts') + +@endpush +@endsection \ No newline at end of file diff --git a/resources/views/sales/index.blade.php b/resources/views/sales/index.blade.php new file mode 100644 index 0000000..8174db7 --- /dev/null +++ b/resources/views/sales/index.blade.php @@ -0,0 +1,12 @@ + + + + + + + Document + + +

    Gwssssssssssssssss

    + + \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index f9b9f3d..bd87c32 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,8 @@ use App\Http\Controllers\AuthController; use App\Http\Controllers\CatalogoController; +use App\Http\Controllers\SaleController; +use App\Http\Controllers\ClientController; use Illuminate\Support\Facades\Route; use App\Http\Controllers\HomeController; use App\Http\Controllers\LoginController; @@ -41,9 +43,14 @@ Route::middleware(['auth'])->group(function () { Route::resource('productos', ProductosController::class)->parameters([ 'productos' => 'producto' ]); - Route::get('/productos',[ProductosController::class,'index'])->name('productos.index'); + Route::get('/productos/{id}', [ProductosController::class,'show'])->name('productos.show'); + Route::get('/productos/{id}/edit', [ProductosController::class,'edit'])->name('productos.edit'); Route::get('/checker', [ProductosController::class, 'checker'])->name('productos.checker'); + //Route::get('/sales', [SaleController::class, 'index'])->name('sales.index'); + Route::get('/sales/create', [SaleController::class, 'create'])->name('sales.create'); + Route::resource('clients', ClientController::class); }); + Route::get('/catalogo', [CatalogoController::class,'catalogo'])->name('catalogo'); Route::get('/productos/{id}/vistaUsuario', [ProductosController::class,'vistaUsuario'])->name('productos.vistaUsuario'); \ No newline at end of file