ADD EV3-358 statisztika modul bővítése aktuális ár megjelenítéssel phase 6 excel export function

This commit is contained in:
E98Developer 2026-07-06 09:45:19 +02:00
parent 4f11e59fce
commit 834f64a011
3 changed files with 360 additions and 6 deletions

View File

@ -0,0 +1,48 @@
<?php
namespace App\Exports;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
class CurrentPriceExport implements FromCollection, ShouldAutoSize, WithHeadings, WithMapping
{
/**
* @param Collection<int, array{supplier: ?string, producer: ?string, mainGroup: string, subGroup1: string, subGroup2: string, name: string, price: ?float}> $rows
*/
public function __construct(private Collection $rows) {}
public function collection(): Collection
{
return $this->rows;
}
public function headings(): array
{
return [
'Beszállító',
'Gyártó',
'Termék Főcsoport',
'Termék alcsoport 1',
'Termék alcsoport 2',
'Termék megnevezése',
'Aktuális ár',
];
}
public function map($row): array
{
return [
$row['supplier'] ?? '',
$row['producer'] ?? '',
$row['mainGroup'] ?? '',
$row['subGroup1'] ?? '',
$row['subGroup2'] ?? '',
$row['name'],
$row['price'],
];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Pages; namespace App\Filament\Pages;
use App\Exports\CurrentPriceExport;
use App\Models\Producer; use App\Models\Producer;
use App\Models\Product; use App\Models\Product;
use App\Models\ProductGroup; use App\Models\ProductGroup;
@ -13,12 +14,18 @@
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Pages\Page; use Filament\Pages\Page;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class CurrentPrice extends Page implements HasForms class CurrentPrice extends Page implements HasForms
{ {
use InteractsWithForms; use InteractsWithForms;
/** @var int[] */
public const PER_PAGE_OPTIONS = [10, 25, 50, 100];
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-currency-dollar'; protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-currency-dollar';
protected static ?string $title = 'Aktuális ár'; protected static ?string $title = 'Aktuális ár';
@ -41,6 +48,17 @@ public function getBreadcrumbs(): array
public ?array $data = []; public ?array $data = [];
public bool $hasSearched = false;
public int $perPage = 10;
public int $page = 1;
public int $totalResults = 0;
/** @var array<int, array{supplier: ?string, producer: ?string, mainGroup: string, subGroup1: string, subGroup2: string, name: string, price: ?float}> */
public array $results = [];
public function mount(): void public function mount(): void
{ {
$this->form->fill(); $this->form->fill();
@ -157,9 +175,151 @@ public function form(Schema $form): Schema
public function submit(): void public function submit(): void
{ {
$data = $this->form->getState(); $this->data = $this->form->getState();
// Itt lehet majd feldolgozni a szűrést $this->page = 1;
// dd($data); $this->runSearch();
}
public function updatedPerPage(): void
{
$this->page = 1;
if ($this->hasSearched) {
$this->runSearch();
}
}
public function goToPage(int $page): void
{
$this->page = max(1, min($page, $this->lastPage()));
$this->runSearch();
}
public function lastPage(): int
{
return max(1, (int) ceil($this->totalResults / $this->perPage));
}
public function exportExcel(): BinaryFileResponse
{
$products = $this->filteredResultsQuery()->orderBy('name')->get();
$rows = $this->mapProductsToRows($products);
return Excel::download(new CurrentPriceExport($rows), 'aktualis-ar-'.now()->format('Y-m-d_His').'.xlsx');
}
private function runSearch(): void
{
$query = $this->filteredResultsQuery();
$this->totalResults = (clone $query)->count();
$this->page = min($this->page, $this->lastPage());
$products = $query->orderBy('name')
->skip(($this->page - 1) * $this->perPage)
->take($this->perPage)
->get();
$this->results = $this->mapProductsToRows($products)->toArray();
$this->hasSearched = true;
}
/**
* A szűrő űrlap aktuális állapota alapján felépített, még nem lapozott/rendezett Product query.
*/
private function filteredResultsQuery(): Builder
{
$data = $this->data;
$query = Product::query()->with(['Supplier', 'Producer']);
if (filled($data['supplier'] ?? [])) {
$query->whereIn('supplier_id', $data['supplier']);
}
if (filled($data['productGroup'] ?? [])) {
$query->whereIn('product_group_id', $data['productGroup']);
}
if (filled($data['producer'] ?? [])) {
$query->whereIn('producer_id', $data['producer']);
}
if (filled($data['product'] ?? [])) {
$query->whereIn('name', $data['product']);
}
if (filled($data['hooreycaName'] ?? [])) {
$query->whereIn('buyerProductName', $data['hooreycaName']);
}
return $query;
}
/**
* Termékekhez tartozó aktuális ár és termékcsoport-hierarchia feloldása, és a táblázat/export
* soraivá alakítása. Egyetlen extra lekérdezéssel dolgozza fel a kapott terméklistát (nincs N+1).
*
* @return Collection<int, array{supplier: ?string, producer: ?string, mainGroup: string, subGroup1: string, subGroup2: string, name: string, price: ?float}>
*/
private function mapProductsToRows(Collection $products): Collection
{
$today = now()->toDateString();
$currentPrices = DB::table('price_list_prices as plp')
->join('price_lists as pl', 'pl.id', '=', 'plp.price_list_id')
->whereIn('plp.product_id', $products->pluck('id'))
->where('pl.available', '<=', $today)
->orderBy('pl.available', 'desc')
->orderBy('pl.id', 'desc')
->get(['plp.product_id', 'plp.price'])
->groupBy('product_id')
->map(fn ($rows) => (float) $rows->first()->price);
$groupPaths = $this->getProductGroupPaths($products->pluck('product_group_id'));
return $products->map(function (Product $product) use ($currentPrices, $groupPaths) {
$path = $groupPaths->get($product->product_group_id, []);
return [
'supplier' => $product->Supplier?->name,
'producer' => $product->Producer?->name,
'mainGroup' => $path[0] ?? '',
'subGroup1' => $path[1] ?? '',
'subGroup2' => $path[2] ?? '',
'name' => $product->name,
'price' => $currentPrices->get($product->id),
];
})->values();
}
/**
* A termékcsoport-hierarchia első 3 szintje (Termék Főcsoport / alcsoport 1 / alcsoport 2)
* termékcsoport id-nként, ugyanaz a logika, mint a legacy Árváltozás statisztikában
* (ProductGroup::allWithPath() a technikai gyökér csomópont nélkül).
*
* @param \Illuminate\Support\Collection<int, int|null> $productGroupIds
* @return \Illuminate\Support\Collection<int, array<int, string>>
*/
private function getProductGroupPaths($productGroupIds): \Illuminate\Support\Collection
{
$ids = $productGroupIds->filter()->unique()->values();
if ($ids->isEmpty()) {
return collect();
}
return ProductGroup::whereIn('id', $ids)
->with('ancestors')
->get()
->mapWithKeys(function (ProductGroup $group) {
$names = $group->ancestors->pluck('name')->all();
array_shift($names); // technikai gyökér ("Főcsoportok") elhagyása
$names[] = $group->name;
return [$group->id => $names];
});
} }
/** /**

View File

@ -78,6 +78,89 @@
color: #453821; color: #453821;
} }
/* Aktuális ár találati táblázat */
.result-table-wrapper {
overflow-x: auto;
}
.result-table {
width: 100%;
border-collapse: collapse;
}
.result-table th,
.result-table td {
padding: 8px 12px;
border: 1px solid #dee2e6;
text-align: left;
}
.result-table thead th {
background-color: #f8f9fa;
font-weight: 600;
color: #333;
}
.result-table tbody tr:nth-child(even) {
background-color: #f8f9fa;
}
.result-table td.price-cell {
text-align: right;
white-space: nowrap;
}
.result-table-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
color: #333;
}
.per-page-select {
display: flex;
align-items: center;
gap: 6px;
}
.per-page-select select {
padding: 4px 8px;
border: 1px solid #dee2e6;
border-radius: 4px;
background-color: #fff;
}
.result-pager {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 15px;
}
.result-pager button {
padding: 6px 12px;
border: 1px solid #dee2e6;
border-radius: 4px;
background-color: #fff;
color: #333;
cursor: pointer;
}
.result-pager button:hover:not(:disabled) {
background-color: #f8f9fa;
}
.result-pager button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.result-pager-info {
font-weight: 600;
color: #333;
}
.excel-export-button {
padding: 6px 14px;
border: 1px solid #1e7e34;
border-radius: 4px;
background-color: #28a745;
color: #fff;
font-weight: 600;
cursor: pointer;
}
.excel-export-button:hover {
background-color: #218838;
}
/* Aktív menüpont stílusa a sidebar-ban */ /* Aktív menüpont stílusa a sidebar-ban */
.sidebar-menu ul li.active > a { .sidebar-menu ul li.active > a {
color: #b8bfce; color: #b8bfce;
@ -128,11 +211,74 @@
</div> </div>
<div class="statistic-card"> <div class="statistic-card">
<div class="statistic-card-header"> <div class="statistic-card-header" style="display: flex; align-items: center; justify-content: space-between;">
Aktuális ár statisztika <span>Aktuális ár statisztika</span>
@if ($hasSearched && ! empty($results))
<button type="button" wire:click="exportExcel" class="excel-export-button">
Excel export
</button>
@endif
</div> </div>
<div class="statistic-card-body"> <div class="statistic-card-body">
<p>Ez a statisztikai elem még fejlesztés alatt áll.</p> @if (! $hasSearched)
<p>Válassza ki a szűrési feltételeket, majd nyomja meg a "Szűrés alkalmazása" gombot.</p>
@elseif (empty($results))
<p>Nincs a szűrésnek megfelelő találat.</p>
@else
<div class="result-table-toolbar">
<div>
Összesen <strong>{{ $totalResults }}</strong> találat
</div>
<div class="per-page-select">
<label for="perPage">Sorok oldalanként:</label>
<select id="perPage" wire:model.live="perPage">
@foreach ($this::PER_PAGE_OPTIONS as $option)
<option value="{{ $option }}">{{ $option }}</option>
@endforeach
</select>
</div>
</div>
<div class="result-table-wrapper">
<table class="result-table">
<thead>
<tr>
<th>Beszállító</th>
<th>Gyártó</th>
<th>Termék Főcsoport</th>
<th>Termék alcsoport 1</th>
<th>Termék alcsoport 2</th>
<th>Termék megnevezése</th>
<th>Aktuális ár</th>
</tr>
</thead>
<tbody>
@foreach ($results as $row)
<tr>
<td>{{ $row['supplier'] ?? '-' }}</td>
<td>{{ $row['producer'] ?? '-' }}</td>
<td>{{ $row['mainGroup'] ?: '-' }}</td>
<td>{{ $row['subGroup1'] ?: '-' }}</td>
<td>{{ $row['subGroup2'] ?: '-' }}</td>
<td>{{ $row['name'] }}</td>
<td class="price-cell">{{ $row['price'] !== null ? number_format($row['price'], 0, ',', ' ') . ' Ft' : '-' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@php $lastPage = $this->lastPage(); @endphp
@if ($lastPage > 1)
<div class="result-pager">
<button type="button" wire:click="goToPage(1)" @disabled($page <= 1)>&laquo;</button>
<button type="button" wire:click="goToPage({{ $page - 1 }})" @disabled($page <= 1)>&lsaquo; Előző</button>
<span class="result-pager-info">{{ $page }}. / {{ $lastPage }} oldal</span>
<button type="button" wire:click="goToPage({{ $page + 1 }})" @disabled($page >= $lastPage)>Következő &rsaquo;</button>
<button type="button" wire:click="goToPage({{ $lastPage }})" @disabled($page >= $lastPage)>&raquo;</button>
</div>
@endif
@endif
</div> </div>
</div> </div>
</div> </div>