102 lines
3.8 KiB
PHP
102 lines
3.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Inventory;
|
|
use App\Models\SaleDetail;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class ReportService
|
|
{
|
|
/**
|
|
* Obtener el producto más vendido
|
|
*
|
|
* @param string|null $fromDate Fecha inicial (formato: Y-m-d)
|
|
* @param string|null $toDate Fecha final (formato: Y-m-d)
|
|
* @return array|null Retorna el producto más vendido o null si no hay ventas
|
|
*/
|
|
public function getTopSellingProduct(?string $fromDate = null, ?string $toDate = null): ?array
|
|
{
|
|
$query = SaleDetail::query()
|
|
->selectRaw('
|
|
inventories.id,
|
|
inventories.name,
|
|
inventories.sku,
|
|
categories.name as category_name,
|
|
SUM(sale_details.quantity) as total_quantity_sold,
|
|
SUM(sale_details.subtotal) as total_revenue,
|
|
COUNT(DISTINCT sale_details.sale_id) as times_sold,
|
|
MAX(sales.created_at) as last_sale_date,
|
|
inventories.created_at as added_date
|
|
')
|
|
->join('inventories', 'sale_details.inventory_id', '=', 'inventories.id')
|
|
->join('categories', 'inventories.category_id', '=', 'categories.id')
|
|
->join('sales', 'sale_details.sale_id', '=', 'sales.id')
|
|
->where('sales.status', 'completed')
|
|
->whereNull('sales.deleted_at');
|
|
|
|
// Aplicar filtro de fechas si se proporcionan ambas
|
|
if ($fromDate && $toDate) {
|
|
$query->whereBetween(DB::raw('DATE(sales.created_at)'), [$fromDate, $toDate]);
|
|
}
|
|
|
|
$result = $query
|
|
->groupBy('inventories.id', 'inventories.name', 'inventories.sku',
|
|
'categories.name', 'inventories.created_at')
|
|
->orderByDesc('total_quantity_sold')
|
|
->first();
|
|
|
|
return $result ? $result->toArray() : null;
|
|
}
|
|
|
|
/**
|
|
* Obtener productos sin movimiento
|
|
*
|
|
* @param int $daysThreshold Días mínimos sin movimiento (default: 30)
|
|
* @param bool $includeStockValue Incluir valor del inventario (default: true)
|
|
* @return array Lista de productos sin ventas
|
|
*/
|
|
public function getProductsWithoutMovement(int $daysThreshold = 30, bool $includeStockValue = true): array
|
|
{
|
|
// Obtener IDs de productos que SÍ tienen ventas
|
|
$inventoriesWithSales = SaleDetail::query()
|
|
->join('sales', 'sale_details.sale_id', '=', 'sales.id')
|
|
->where('sales.status', 'completed')
|
|
->whereNull('sales.deleted_at')
|
|
->distinct()
|
|
->pluck('sale_details.inventory_id')
|
|
->toArray();
|
|
|
|
// Construir query para productos SIN ventas
|
|
$query = Inventory::query()
|
|
->selectRaw('
|
|
inventories.id,
|
|
inventories.name,
|
|
inventories.sku,
|
|
inventories.stock,
|
|
categories.name as category_name,
|
|
inventories.created_at as date_added,
|
|
DATEDIFF(NOW(), inventories.created_at) as days_without_movement,
|
|
prices.retail_price
|
|
');
|
|
|
|
// Agregar valor del inventario si se solicita
|
|
if ($includeStockValue) {
|
|
$query->addSelect(
|
|
DB::raw('(inventories.stock * COALESCE(prices.cost, 0)) as inventory_value')
|
|
);
|
|
}
|
|
|
|
return $query
|
|
->leftJoin('categories', 'inventories.category_id', '=', 'categories.id')
|
|
->leftJoin('prices', 'inventories.id', '=', 'prices.inventory_id')
|
|
->where('inventories.is_active', true)
|
|
->whereNull('inventories.deleted_at')
|
|
->whereNotIn('inventories.id', $inventoriesWithSales)
|
|
->havingRaw('DATEDIFF(NOW(), inventories.created_at) >= ?', [$daysThreshold])
|
|
->orderBy('inventories.created_at')
|
|
->get()
|
|
->toArray();
|
|
}
|
|
}
|