UPDATE:
- Mejorados los graficos en la parte de reportes. - Agregados 3 nuevos charts para mayor utilidad de la pagina.
This commit is contained in:
@@ -48,6 +48,34 @@ class ReportController extends Controller
|
|||||||
$monthlyOutcomeData[] = $out;
|
$monthlyOutcomeData[] = $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Top 5 Productos más vendidos
|
||||||
|
$topProducts = \App\Models\SaleDetail::with('product')
|
||||||
|
->select('product_id', DB::raw('SUM(quantity) as total_qty'))
|
||||||
|
->groupBy('product_id')
|
||||||
|
->orderByDesc('total_qty')
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// 5. Top 5 Repuestos más usados/vendidos
|
||||||
|
$topSpares = \App\Models\SaleDetail::with('product')
|
||||||
|
->whereHas('product', function ($query) {
|
||||||
|
$query->where('type', 'spare');
|
||||||
|
})
|
||||||
|
->select('product_id', DB::raw('SUM(quantity) as total_qty'))
|
||||||
|
->groupBy('product_id')
|
||||||
|
->orderByDesc('total_qty')
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// 6. Mejores Clientes (Top Spenders)
|
||||||
|
$topClients = Sale::with('client')
|
||||||
|
->whereNotNull('client_id')
|
||||||
|
->select('client_id', DB::raw('SUM(total) as total_spent'))
|
||||||
|
->groupBy('client_id')
|
||||||
|
->orderByDesc('total_spent')
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
return view('reports.index', compact(
|
return view('reports.index', compact(
|
||||||
'totalIncome',
|
'totalIncome',
|
||||||
'totalOutcome',
|
'totalOutcome',
|
||||||
@@ -55,7 +83,10 @@ class ReportController extends Controller
|
|||||||
'expensesByCategory',
|
'expensesByCategory',
|
||||||
'months',
|
'months',
|
||||||
'monthlyIncomeData',
|
'monthlyIncomeData',
|
||||||
'monthlyOutcomeData'
|
'monthlyOutcomeData',
|
||||||
|
'topProducts',
|
||||||
|
'topSpares',
|
||||||
|
'topClients'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\Sale;
|
||||||
|
use App\Models\SaleDetail;
|
||||||
|
|
||||||
|
class ReportTestSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$faker = \Faker\Factory::create('es_AR');
|
||||||
|
|
||||||
|
// 1. Create 10 Fictional Clients
|
||||||
|
$clients = [];
|
||||||
|
for ($i = 0; $i < 10; $i++) {
|
||||||
|
$clients[] = Client::create([
|
||||||
|
'name' => $faker->name,
|
||||||
|
'phone' => $faker->phoneNumber,
|
||||||
|
'email' => $faker->unique()->safeEmail,
|
||||||
|
'address' => $faker->address,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Ensure we have some products and spare parts
|
||||||
|
$supplier = \App\Models\Supplier::first() ?? \App\Models\Supplier::create(['name' => 'Test Supplier', 'phone' => '123']);
|
||||||
|
$products = [];
|
||||||
|
// Create 3 Spares
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
$products[] = Product::create([
|
||||||
|
'name' => 'Repuesto Test ' . $i,
|
||||||
|
'type' => 'spare',
|
||||||
|
'cost' => rand(500, 2000),
|
||||||
|
'price' => rand(3000, 8000),
|
||||||
|
'stock_quantity' => 100,
|
||||||
|
'suppliers_id' => $supplier->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Create 3 Accessories
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
$products[] = Product::create([
|
||||||
|
'name' => 'Accesorio Test ' . $i,
|
||||||
|
'type' => 'accessory',
|
||||||
|
'cost' => rand(1000, 4000),
|
||||||
|
'price' => rand(5000, 15000),
|
||||||
|
'stock_quantity' => 50,
|
||||||
|
'suppliers_id' => $supplier->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allProducts = Product::all();
|
||||||
|
$paymentMethods = ['Efectivo', 'Tarjeta de Débito', 'Tarjeta de Crédito', 'Transferencia'];
|
||||||
|
|
||||||
|
// 3. Create 20 Sales
|
||||||
|
for ($i = 1; $i <= 20; $i++) {
|
||||||
|
// Pick a random client (some clients will buy more often, making them Top Spenders)
|
||||||
|
// Weight the randomness a bit by picking from array_rand with duplicates
|
||||||
|
$weightedClients = array_merge($clients, array_slice($clients, 0, 3));
|
||||||
|
$client = $weightedClients[array_rand($weightedClients)];
|
||||||
|
|
||||||
|
// Random date in the last 4 months
|
||||||
|
$date = now()->subDays(rand(1, 120));
|
||||||
|
|
||||||
|
$sale = Sale::create([
|
||||||
|
'client_id' => $client->id,
|
||||||
|
'total' => 0, // We'll calculate this
|
||||||
|
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
|
||||||
|
'created_at' => $date,
|
||||||
|
'updated_at' => $date,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add 1 to 3 random products to this sale
|
||||||
|
$numProducts = rand(1, 3);
|
||||||
|
$totalSale = 0;
|
||||||
|
$shuffledProducts = $allProducts->shuffle();
|
||||||
|
|
||||||
|
for ($j = 0; $j < $numProducts; $j++) {
|
||||||
|
$product = $shuffledProducts[$j];
|
||||||
|
$quantity = rand(1, 5);
|
||||||
|
$price = $product->price ?? 5000;
|
||||||
|
|
||||||
|
SaleDetail::create([
|
||||||
|
'sale_id' => $sale->id,
|
||||||
|
'product_id' => $product->id,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'price' => $price,
|
||||||
|
'created_at' => $date,
|
||||||
|
'updated_at' => $date,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$totalSale += ($quantity * $price);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total
|
||||||
|
$sale->update(['total' => $totalSale]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Nuevas Métricas -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
|
||||||
|
|
||||||
|
<!-- Top 5 Productos -->
|
||||||
|
<div class="bg-gray-200 dark:bg-panel-bg border border-neutral-400 dark:border-neutral-800 rounded-xl p-6 shadow-lg">
|
||||||
|
<h3 class="text-lg font-bold text-neutral-900 dark:text-white mb-4">Top 5 Productos Vendidos</h3>
|
||||||
|
<div class="relative h-64 w-full">
|
||||||
|
<canvas id="topProductsChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top 5 Repuestos -->
|
||||||
|
<div class="bg-gray-200 dark:bg-panel-bg border border-neutral-400 dark:border-neutral-800 rounded-xl p-6 shadow-lg">
|
||||||
|
<h3 class="text-lg font-bold text-neutral-900 dark:text-white mb-4">Top 5 Repuestos Usados</h3>
|
||||||
|
<div class="relative h-64 w-full">
|
||||||
|
<canvas id="topSparesChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Clientes -->
|
||||||
|
<div class="bg-gray-200 dark:bg-panel-bg border border-neutral-400 dark:border-neutral-800 rounded-xl p-6 shadow-lg">
|
||||||
|
<h3 class="text-lg font-bold text-neutral-900 dark:text-white mb-4">Mejores Clientes</h3>
|
||||||
|
<ul class="space-y-4">
|
||||||
|
@forelse($topClients as $clientSale)
|
||||||
|
<li class="flex items-center justify-between border-b border-gray-300 dark:border-neutral-700 pb-2">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-bold text-neutral-900 dark:text-white">{{ $clientSale->client->name ?? 'Cliente Eliminado' }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ $clientSale->client->phone ?? 'Sin teléfono' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<span class="px-2 py-1 bg-green-200 text-green-800 dark:bg-green-900/50 dark:text-neon-lime text-xs font-bold rounded-lg border border-green-400 dark:border-green-600">
|
||||||
|
${{ number_format($clientSale->total_spent, 2, ',', '.') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
@empty
|
||||||
|
<li class="text-sm text-gray-500 italic">No hay ventas registradas aún.</li>
|
||||||
|
@endforelse
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cargar Chart.js desde CDN -->
|
<!-- Cargar Chart.js desde CDN -->
|
||||||
@@ -168,6 +210,64 @@
|
|||||||
cutout: '60%'
|
cutout: '60%'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- GRÁFICO DE TOP PRODUCTOS ---
|
||||||
|
const ctxTopProducts = document.getElementById('topProductsChart').getContext('2d');
|
||||||
|
const rawTopProducts = @json($topProducts);
|
||||||
|
|
||||||
|
new Chart(ctxTopProducts, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: rawTopProducts.map(i => i.product ? i.product.name : 'Eliminado'),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Cantidad Vendida',
|
||||||
|
data: rawTopProducts.map(i => i.total_qty),
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.7)', // blue-500
|
||||||
|
borderColor: 'rgb(37, 99, 235)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y', // barras horizontales
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
x: { ticks: { color: textColor }, grid: { color: gridColor, drawBorder: false } },
|
||||||
|
y: { ticks: { color: textColor }, grid: { display: false } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- GRÁFICO DE TOP REPUESTOS ---
|
||||||
|
const ctxTopSpares = document.getElementById('topSparesChart').getContext('2d');
|
||||||
|
const rawTopSpares = @json($topSpares);
|
||||||
|
|
||||||
|
new Chart(ctxTopSpares, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: rawTopSpares.map(i => i.product ? i.product.name : 'Eliminado'),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Cantidad Usada/Vendida',
|
||||||
|
data: rawTopSpares.map(i => i.total_qty),
|
||||||
|
backgroundColor: 'rgba(245, 158, 11, 0.7)', // amber-500
|
||||||
|
borderColor: 'rgb(217, 119, 6)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y', // barras horizontales
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
x: { ticks: { color: textColor }, grid: { color: gridColor, drawBorder: false } },
|
||||||
|
y: { ticks: { color: textColor }, grid: { display: false } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user