diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php new file mode 100644 index 0000000..c128252 --- /dev/null +++ b/app/Http/Controllers/ExpenseController.php @@ -0,0 +1,96 @@ +input('search'); + $category = $request->input('category'); + + $expenses = Expense::with('supplier') + ->when($query, function ($q) use ($query) { + return $q->where('description', 'like', "%{$query}%") + ->orWhereHas('supplier', function($sq) use ($query) { + $sq->where('name', 'like', "%{$query}%"); + }); + }) + ->when($category, function ($q) use ($category) { + return $q->where('expense_category', $category); + }) + ->orderBy('expense_date', 'desc') + ->orderBy('id', 'desc') + ->paginate(10) + ->withQueryString(); + + return view('expenses.index', compact('expenses')); + } + + public function create() + { + $suppliers = Supplier::orderBy('name')->get(); + return view('expenses.create', compact('suppliers')); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'supplier_id' => 'nullable|exists:suppliers,id', + 'description' => 'required|string|max:255', + 'expense_category' => 'required|in:Mercadería,Servicios,Impuestos,Logística,Otros', + 'type' => 'required|in:Compra,Pedido,Factura', + 'status' => 'required|in:Pendiente,Pagado,Cancelado', + 'total_amount' => 'required|numeric|min:0', + 'document_number' => 'nullable|string|max:255', + 'expense_date' => 'nullable|date', + ]); + + if(empty($validated['expense_date'])) { + $validated['expense_date'] = now()->toDateString(); + } + + Expense::create($validated); + + return redirect()->route('expenses.index')->with('success', 'Gasto registrado correctamente.'); + } + + public function show(Expense $expense) + { + return view('expenses.show', compact('expense')); + } + + public function edit(Expense $expense) + { + $suppliers = Supplier::orderBy('name')->get(); + return view('expenses.edit', compact('expense', 'suppliers')); + } + + public function update(Request $request, Expense $expense) + { + $validated = $request->validate([ + 'supplier_id' => 'nullable|exists:suppliers,id', + 'description' => 'required|string|max:255', + 'expense_category' => 'required|in:Mercadería,Servicios,Impuestos,Logística,Otros', + 'type' => 'required|in:Compra,Pedido,Factura', + 'status' => 'required|in:Pendiente,Pagado,Cancelado', + 'total_amount' => 'required|numeric|min:0', + 'document_number' => 'nullable|string|max:255', + 'expense_date' => 'nullable|date', + ]); + + $expense->update($validated); + + return redirect()->route('expenses.index')->with('success', 'Gasto actualizado correctamente.'); + } + + public function destroy(Expense $expense) + { + $expense->delete(); + return redirect()->route('expenses.index')->with('success', 'Gasto eliminado correctamente.'); + } +} diff --git a/app/Http/Controllers/SupplierController.php b/app/Http/Controllers/SupplierController.php index f279594..7938f8c 100644 --- a/app/Http/Controllers/SupplierController.php +++ b/app/Http/Controllers/SupplierController.php @@ -98,4 +98,35 @@ class SupplierController extends Controller return view('suppliers.order', compact('supplier', 'products')); } + + /** + * guarda el pedido como un gasto + */ + public function storeOrder(Request $request, Supplier $supplier) + { + $validated = $request->validate([ + 'products' => 'required|array', + 'products.*.name' => 'required|string', + 'products.*.qty' => 'required|integer|min:1', + ]); + + $description = 'Pedido a proveedor. Productos: '; + $items = []; + foreach ($validated['products'] as $prod) { + $items[] = $prod['qty'] . 'x ' . $prod['name']; + } + $description .= implode(', ', $items); + + $expense = \App\Models\Expense::create([ + 'supplier_id' => $supplier->id, + 'description' => \Illuminate\Support\Str::limit($description, 250), + 'expense_category' => 'Mercadería', + 'type' => 'Pedido', + 'status' => 'Pendiente', + 'total_amount' => 0, + 'expense_date' => now()->toDateString(), + ]); + + return response()->json(['success' => true, 'expense_id' => $expense->id]); + } } \ No newline at end of file diff --git a/app/Models/Expense.php b/app/Models/Expense.php new file mode 100644 index 0000000..327a8c0 --- /dev/null +++ b/app/Models/Expense.php @@ -0,0 +1,33 @@ + 'date', + 'total_amount' => 'decimal:2', + ]; + + public function supplier() + { + return $this->belongsTo(Supplier::class); + } +} diff --git a/app/Models/Supplier.php b/app/Models/Supplier.php index fa7b1ab..b1467c7 100644 --- a/app/Models/Supplier.php +++ b/app/Models/Supplier.php @@ -17,4 +17,9 @@ class Supplier extends Model { return $this->hasMany(Product::class, 'suppliers_id'); } + + public function expenses() + { + return $this->hasMany(Expense::class); + } } \ No newline at end of file diff --git a/database/migrations/2026_05_28_120249_create_expenses_table.php b/database/migrations/2026_05_28_120249_create_expenses_table.php new file mode 100644 index 0000000..d9fdd64 --- /dev/null +++ b/database/migrations/2026_05_28_120249_create_expenses_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('supplier_id')->nullable()->constrained('suppliers')->nullOnDelete(); + $table->string('description'); + $table->enum('expense_category', ['Mercadería', 'Servicios', 'Impuestos', 'Logística', 'Otros'])->default('Otros'); + $table->enum('type', ['Compra', 'Pedido', 'Factura'])->default('Compra'); + $table->enum('status', ['Pendiente', 'Pagado', 'Cancelado'])->default('Pendiente'); + $table->decimal('total_amount', 10, 2)->default(0); + $table->string('document_number')->nullable(); + $table->date('expense_date')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('expenses'); + } +}; diff --git a/database/seeders/FakeDataSeeder.php b/database/seeders/FakeDataSeeder.php new file mode 100644 index 0000000..3adbb30 --- /dev/null +++ b/database/seeders/FakeDataSeeder.php @@ -0,0 +1,90 @@ + 'Cliente Falso', 'phone' => '123456', 'email' => 'cliente@fake.com']); + $product = Product::first() ?? Product::create([ + 'name' => 'Bicicleta de Prueba', + 'description' => 'Prueba', + 'sale_price' => 50000, + 'stock_quantity' => 10, + 'type' => 'bike' + ]); + $supplier = Supplier::first() ?? Supplier::create(['name' => 'Proveedor Falso', 'phone' => '123456', 'email' => 'prov@fake.com']); + + // 1. Create 10 Sales + $paymentMethods = ['Efectivo', 'Tarjeta de Débito', 'Tarjeta de Crédito', 'Transferencia']; + for ($i = 1; $i <= 10; $i++) { + $sale = Sale::create([ + 'client_id' => $client->id, + 'total' => 50000, + 'payment_method' => $paymentMethods[array_rand($paymentMethods)], + 'created_at' => now()->subDays(rand(1, 30)), + ]); + + SaleDetail::create([ + 'sale_id' => $sale->id, + 'product_id' => $product->id, + 'quantity' => 1, + 'price' => 50000, + ]); + } + + // 2. Create 10 Expenses (Servicios/Impuestos) + $expenseCategories = ['Servicios', 'Impuestos', 'Otros']; + $services = ['Factura de Luz - Edesur', 'Internet - Fibertel', 'Agua - AySA', 'Gas - Metrogas', 'Alquiler Local', 'Impuesto Municipal', 'Monotributo', 'Papelería', 'Limpieza', 'Café y Mate']; + for ($i = 0; $i < 10; $i++) { + Expense::create([ + 'description' => $services[$i], + 'expense_category' => $expenseCategories[array_rand($expenseCategories)], + 'type' => 'Factura', + 'status' => 'Pagado', + 'total_amount' => rand(1500, 25000), + 'expense_date' => now()->subDays(rand(1, 30)), + ]); + } + + // 3. Create 10 Orders (Pedidos a Proveedores) + for ($i = 1; $i <= 10; $i++) { + Expense::create([ + 'supplier_id' => $supplier->id, + 'description' => 'Pedido de repuestos varios #' . $i, + 'expense_category' => 'Mercadería', + 'type' => 'Pedido', + 'status' => 'Pendiente', + 'total_amount' => 0, // Orders might not have amounts until they arrive + 'expense_date' => now()->subDays(rand(1, 10)), + ]); + } + + // 4. Create 10 Bills (Facturas de Proveedores) + for ($i = 1; $i <= 10; $i++) { + Expense::create([ + 'supplier_id' => $supplier->id, + 'description' => 'Compra de bicicletas y cascos', + 'expense_category' => 'Mercadería', + 'type' => 'Compra', + 'status' => 'Pagado', + 'total_amount' => rand(100000, 500000), + 'document_number' => 'FAC-0001-0000' . rand(1000, 9999), + 'expense_date' => now()->subDays(rand(1, 30)), + ]); + } + } +} diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 0db80de..0e0a38a 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -45,7 +45,7 @@ - + diff --git a/resources/views/expenses/create.blade.php b/resources/views/expenses/create.blade.php new file mode 100644 index 0000000..02cef82 --- /dev/null +++ b/resources/views/expenses/create.blade.php @@ -0,0 +1,86 @@ + + + +
+
+ @csrf + +
+ +
+ + + @error('description')

{{ $message }}

@enderror +
+ +
+ + + @error('expense_category')

{{ $message }}

@enderror +
+ +
+ + + @error('supplier_id')

{{ $message }}

@enderror +
+ +
+ + + @error('total_amount')

{{ $message }}

@enderror +
+ +
+ + + @error('type')

{{ $message }}

@enderror +
+ +
+ + + @error('status')

{{ $message }}

@enderror +
+ +
+ + + @error('document_number')

{{ $message }}

@enderror +
+ +
+ + + @error('expense_date')

{{ $message }}

@enderror +
+
+ +
+ Cancelar + +
+
+
+
diff --git a/resources/views/expenses/edit.blade.php b/resources/views/expenses/edit.blade.php new file mode 100644 index 0000000..468834f --- /dev/null +++ b/resources/views/expenses/edit.blade.php @@ -0,0 +1,87 @@ + + + +
+
+ @csrf + @method('PUT') + +
+ +
+ + + @error('description')

{{ $message }}

@enderror +
+ +
+ + + @error('expense_category')

{{ $message }}

@enderror +
+ +
+ + + @error('supplier_id')

{{ $message }}

@enderror +
+ +
+ + + @error('total_amount')

{{ $message }}

@enderror +
+ +
+ + + @error('type')

{{ $message }}

@enderror +
+ +
+ + + @error('status')

{{ $message }}

@enderror +
+ +
+ + + @error('document_number')

{{ $message }}

@enderror +
+ +
+ + + @error('expense_date')

{{ $message }}

@enderror +
+
+ +
+ Cancelar + +
+
+
+
diff --git a/resources/views/expenses/index.blade.php b/resources/views/expenses/index.blade.php new file mode 100644 index 0000000..841803f --- /dev/null +++ b/resources/views/expenses/index.blade.php @@ -0,0 +1,128 @@ + + + +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ + + + Nuevo Gasto + +
+ +
+ + + + + + + + + + + + @forelse($expenses as $expense) + + + + + + + + @empty + + + + @endforelse + +
Fecha / TipoCategoríaDescripciónMonto / EstadoAcciones
+
{{ $expense->expense_date ? $expense->expense_date->format('d/m/Y') : 'Sin Fecha' }}
+
+ {{ $expense->type }} +
+
+ + {{ $expense->expense_category }} + + +
+ {{ $expense->description }} +
+ @if($expense->supplier) +
+ Proveedor: {{ $expense->supplier->name }} +
+ @endif +
+
+ ${{ number_format($expense->total_amount, 2, ',', '.') }} +
+
+ @if($expense->status == 'Pagado') + Pagado + @elseif($expense->status == 'Pendiente') + Pendiente + @else + Cancelado + @endif +
+
+ + + + + + +
+ @csrf + @method('DELETE') + +
+
+
+ + + +

No se encontraron gastos.

+
+
+
+ +
+ {{ $expenses->links() }} +
+ +
diff --git a/resources/views/expenses/show.blade.php b/resources/views/expenses/show.blade.php new file mode 100644 index 0000000..89c54c0 --- /dev/null +++ b/resources/views/expenses/show.blade.php @@ -0,0 +1,77 @@ + + + +
+
+
+

Resumen de Gasto

+

Registrado el: {{ $expense->created_at->format('d/m/Y H:i') }}

+
+ +
+ +
+
+

Descripción

+

{{ $expense->description }}

+
+ +
+

Monto Total

+

${{ number_format($expense->total_amount, 2, ',', '.') }}

+
+ +
+

Categoría

+ + {{ $expense->expense_category }} + +
+ +
+

Estado

+ @if($expense->status == 'Pagado') + Pagado + @elseif($expense->status == 'Pendiente') + Pendiente + @else + Cancelado + @endif +
+ +
+

Tipo de Gasto

+

{{ $expense->type }}

+
+ +
+

Fecha del Gasto

+

{{ $expense->expense_date ? $expense->expense_date->format('d/m/Y') : '-' }}

+
+ +
+

Proveedor Relacionado

+ @if($expense->supplier) + + {{ $expense->supplier->name }} + + @else +

No especificado

+ @endif +
+ +
+

Nº Comprobante / Factura

+

{{ $expense->document_number ?: '-' }}

+
+
+ + +
+
diff --git a/resources/views/suppliers/order.blade.php b/resources/views/suppliers/order.blade.php index 34a5429..479e494 100644 --- a/resources/views/suppliers/order.blade.php +++ b/resources/views/suppliers/order.blade.php @@ -161,8 +161,8 @@ }); }); - // enviar a wsp - function enviarPedido() { + // enviar a wsp y guardar gasto + async function enviarPedido() { let telefonoRaw = "{{ $supplier->phone }}"; let telefono = telefonoRaw.replace(/\D/g, ''); @@ -177,14 +177,16 @@ let mensaje = `Hola *{{ $supplier->name }}*, me comunico de Bicicletería Lauck para realizar el siguiente pedido:\n\n`; let hayProductos = false; + let productsToSave = []; document.querySelectorAll('.product-row').forEach(row => { const isChecked = row.querySelector('.product-check').checked; - const qty = row.querySelector('.product-qty').value; + const qty = parseInt(row.querySelector('.product-qty').value); const name = row.querySelector('.product-name').innerText.trim(); if (isChecked && qty > 0) { mensaje += `- ${qty}x ${name}\n`; + productsToSave.push({ name: name, qty: qty }); hayProductos = true; } }); @@ -196,6 +198,28 @@ mensaje += `\n¡Muchas gracias!`; + try { + // Guardar el Gasto en el backend + const response = await fetch("{{ route('suppliers.storeOrder', $supplier) }}", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + }, + body: JSON.stringify({ products: productsToSave }) + }); + + const data = await response.json(); + if(!data.success) { + alert('Hubo un error al registrar el pedido en el sistema.'); + return; + } + } catch (error) { + alert('Hubo un error de conexión al intentar guardar el pedido.'); + console.error(error); + return; + } + const url = `https://wa.me/${telefono}?text=${encodeURIComponent(mensaje)}`; window.open(url, '_blank'); } diff --git a/routes/web.php b/routes/web.php index 51f733a..c3ff6d0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -57,7 +57,9 @@ Route::middleware(['auth'])->group(function () { Route::view('dashboard', 'dashboard')->name('dashboard'); Route::resource('clients', ClientController::class); Route::get('suppliers/{supplier}/order', [SupplierController::class, 'order'])->name('suppliers.order'); + Route::post('suppliers/{supplier}/order', [SupplierController::class, 'storeOrder'])->name('suppliers.storeOrder'); Route::resource('suppliers', SupplierController::class); + Route::resource('expenses', \App\Http\Controllers\ExpenseController::class); Route::resource('productos', ProductosController::class)->parameters([ 'productos' => 'product' ]);