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/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') +