243 lines
10 KiB
PHP
243 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Resources;
|
|
|
|
use App\Filament\Resources\InformeResource\Pages;
|
|
use App\Filament\Resources\InformeResource\Widgets;
|
|
use App\Models\Venta;
|
|
use App\Models\DetalleVenta;
|
|
use App\Models\Producto;
|
|
use App\Models\Categoria;
|
|
use Filament\Forms;
|
|
use Filament\Forms\Form;
|
|
use Filament\Resources\Resource;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class InformeResource extends Resource
|
|
{
|
|
protected static ?string $model = Venta::class;
|
|
|
|
protected static ?string $navigationIcon = 'heroicon-o-chart-bar';
|
|
|
|
protected static ?string $navigationGroup = 'Reportes';
|
|
|
|
protected static ?string $modelLabel = 'Informe';
|
|
|
|
protected static ?string $pluralModelLabel = 'Informes';
|
|
|
|
protected static ?string $navigationLabel = 'Promedio Ponderado';
|
|
|
|
public static function canViewAny(): bool
|
|
{
|
|
return true; // Permitir acceso básico, se puede personalizar según necesidades
|
|
}
|
|
|
|
public static function canCreate(): bool
|
|
{
|
|
return false; // No se pueden crear informes
|
|
}
|
|
|
|
public static function canEdit($record): bool
|
|
{
|
|
return false; // No se pueden editar informes
|
|
}
|
|
|
|
public static function canDelete($record): bool
|
|
{
|
|
return false; // No se pueden eliminar informes
|
|
}
|
|
|
|
public static function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->query(self::getInformeQuery())
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('producto_nombre')
|
|
->label('Producto')
|
|
->searchable()
|
|
->sortable(),
|
|
|
|
Tables\Columns\TextColumn::make('categoria_nombre')
|
|
->label('Categoría')
|
|
->searchable()
|
|
->sortable(),
|
|
|
|
Tables\Columns\TextColumn::make('total_vendido')
|
|
->label('Cantidad Vendida')
|
|
->alignCenter()
|
|
->sortable()
|
|
->formatStateUsing(function ($state, $record) {
|
|
$unidadMedida = $record->unidad_medida ?? 'unidad';
|
|
$cantidadEnUnidad = Producto::convertirDesdeUnidades($state, $unidadMedida);
|
|
$nombreUnidad = explode(' ', Producto::getUnidadesMedida()[$unidadMedida])[0];
|
|
return "{$cantidadEnUnidad} {$nombreUnidad}";
|
|
})
|
|
->tooltip(function ($state, $record) {
|
|
$unidadMedida = $record->unidad_medida ?? 'unidad';
|
|
$cantidadEnUnidad = Producto::convertirDesdeUnidades($state, $unidadMedida);
|
|
return "Cantidad en unidades: {$state}\nCantidad en {$unidadMedida}: {$cantidadEnUnidad}";
|
|
}),
|
|
|
|
Tables\Columns\TextColumn::make('precio_compra_unitario')
|
|
->label('Precio Compra Actual')
|
|
->money('COP')
|
|
->alignEnd()
|
|
->sortable(),
|
|
|
|
Tables\Columns\TextColumn::make('promedio_ponderado_compra')
|
|
->label('Promedio Ponderado Compra')
|
|
->money('COP')
|
|
->alignEnd()
|
|
->sortable()
|
|
->tooltip('Precio promedio de compra ponderado por cantidades compradas'),
|
|
|
|
Tables\Columns\TextColumn::make('valor_total_compra')
|
|
->label('Valor Total Compra')
|
|
->money('COP')
|
|
->alignEnd()
|
|
->sortable(),
|
|
|
|
Tables\Columns\TextColumn::make('precio_venta_promedio')
|
|
->label('Precio Venta Promedio')
|
|
->money('COP')
|
|
->alignEnd()
|
|
->sortable(),
|
|
|
|
Tables\Columns\TextColumn::make('promedio_ponderado')
|
|
->label('Promedio Ponderado')
|
|
->money('COP')
|
|
->alignEnd()
|
|
->sortable()
|
|
->tooltip('Precio promedio ponderado por cantidad vendida'),
|
|
|
|
Tables\Columns\TextColumn::make('margen_beneficio')
|
|
->label('Margen %')
|
|
->alignCenter()
|
|
->sortable()
|
|
->getStateUsing(function ($record) {
|
|
$precioCompra = $record->precio_compra_unitario;
|
|
$precioVenta = $record->precio_venta_promedio;
|
|
|
|
if ($precioCompra > 0) {
|
|
$margen = (($precioVenta - $precioCompra) / $precioCompra) * 100;
|
|
return round($margen, 2);
|
|
}
|
|
return 0;
|
|
})
|
|
->formatStateUsing(function ($state) {
|
|
$color = $state >= 0 ? 'success' : 'danger';
|
|
$icon = $state >= 0 ? '📈' : '📉';
|
|
return "{$icon} {$state}%";
|
|
}),
|
|
])
|
|
->defaultSort('valor_total_compra', 'desc')
|
|
->filters([
|
|
Tables\Filters\Filter::make('fecha_rango')
|
|
->form([
|
|
Forms\Components\DatePicker::make('fecha_desde')
|
|
->label('Fecha Desde')
|
|
->default(Carbon::now()->startOfMonth()),
|
|
|
|
Forms\Components\DatePicker::make('fecha_hasta')
|
|
->label('Fecha Hasta')
|
|
->default(Carbon::now()->endOfMonth()),
|
|
])
|
|
->query(function (Builder $query, array $data): Builder {
|
|
return $query
|
|
->when(
|
|
$data['fecha_desde'],
|
|
fn (Builder $query, $date): Builder => $query->whereDate('ventas.created_at', '>=', $date),
|
|
)
|
|
->when(
|
|
$data['fecha_hasta'],
|
|
fn (Builder $query, $date): Builder => $query->whereDate('ventas.created_at', '<=', $date),
|
|
);
|
|
})
|
|
->indicateUsing(function (array $data): array {
|
|
$indicators = [];
|
|
if ($data['fecha_desde']) {
|
|
$indicators['fecha_desde'] = 'Desde: ' . Carbon::parse($data['fecha_desde'])->format('d/m/Y');
|
|
}
|
|
if ($data['fecha_hasta']) {
|
|
$indicators['fecha_hasta'] = 'Hasta: ' . Carbon::parse($data['fecha_hasta'])->format('d/m/Y');
|
|
}
|
|
return $indicators;
|
|
}),
|
|
|
|
Tables\Filters\SelectFilter::make('categoria')
|
|
->options(\App\Models\Categoria::pluck('nombre', 'id'))
|
|
->query(function (Builder $query, array $data): Builder {
|
|
if (!empty($data['value'])) {
|
|
return $query->where('categorias.id', $data['value']);
|
|
}
|
|
return $query;
|
|
})
|
|
->searchable()
|
|
->preload(),
|
|
])
|
|
->actions([
|
|
// No actions needed for reports
|
|
])
|
|
->bulkActions([
|
|
// No bulk actions for reports
|
|
])
|
|
->emptyStateHeading('No hay ventas en el período seleccionado')
|
|
->emptyStateDescription('Ajuste los filtros de fecha para ver resultados')
|
|
->poll('60s'); // Actualizar cada minuto
|
|
}
|
|
|
|
/**
|
|
* Query personalizado para el informe de promedio ponderado
|
|
*/
|
|
protected static function getInformeQuery(): Builder
|
|
{
|
|
return DetalleVenta::query()
|
|
->select([
|
|
'productos.id as id', // Filament necesita 'id' como clave del registro
|
|
'productos.id as producto_id', // Mantener para compatibilidad
|
|
'productos.nombre as producto_nombre',
|
|
'productos.precio_compra as precio_compra_unitario',
|
|
'productos.unidad_medida',
|
|
'categorias.nombre as categoria_nombre',
|
|
DB::raw('SUM(detalle_ventas.cantidad) as total_vendido'),
|
|
DB::raw('ROUND(AVG(detalle_ventas.precio_unitario), 2) as precio_venta_promedio'),
|
|
// Promedio ponderado real: suma de (cantidad * precio) / suma de cantidades
|
|
DB::raw('ROUND(SUM(detalle_ventas.cantidad * detalle_ventas.precio_unitario) / SUM(detalle_ventas.cantidad), 2) as promedio_ponderado'),
|
|
// Promedio ponderado de compra basado en compras reales
|
|
DB::raw('COALESCE(
|
|
(SELECT ROUND(SUM(dc.cantidad * dc.precio_unitario) / SUM(dc.cantidad), 2)
|
|
FROM detalle_compras dc
|
|
INNER JOIN compras c ON dc.compra_id = c.id
|
|
WHERE dc.producto_id = productos.id AND c.estado = \'Recibida\'),
|
|
CAST(productos.precio_compra AS DECIMAL(10,2))
|
|
) as promedio_ponderado_compra'),
|
|
DB::raw('COUNT(DISTINCT ventas.id) as numero_ventas'),
|
|
DB::raw('MIN(ventas.created_at) as primera_venta'),
|
|
DB::raw('MAX(ventas.created_at) as ultima_venta'),
|
|
// Agregar el campo calculado para poder ordenar por él con cast explícito
|
|
DB::raw('SUM(detalle_ventas.cantidad) * CAST(productos.precio_compra AS DECIMAL(10,2)) as valor_total_compra'),
|
|
])
|
|
->join('productos', 'detalle_ventas.producto_id', '=', 'productos.id')
|
|
->join('categorias', 'productos.categoria_id', '=', 'categorias.id')
|
|
->join('ventas', 'detalle_ventas.venta_id', '=', 'ventas.id')
|
|
->groupBy([
|
|
'productos.id',
|
|
'productos.nombre',
|
|
'productos.precio_compra',
|
|
'productos.unidad_medida',
|
|
'categorias.nombre'
|
|
])
|
|
->havingRaw('SUM(detalle_ventas.cantidad) > 0');
|
|
}
|
|
|
|
public static function getPages(): array
|
|
{
|
|
return [
|
|
'index' => Pages\ListInformes::route('/'),
|
|
];
|
|
}
|
|
} |