Alle Beispiele

Preisliste · Gruppierte Liste mit Zwischensummen

Ein Report-Start-Band mit dem Titel, ein Tabellenkopf, ein Detailband, eine Gruppe pro Kategorie mit eigenem Kopf und Fuss, eine summierte Zwischensumme, ein Report-Ende-Band und ein Filterformular.

Gruppen Aggregate Filter Flow /reports/price_list/run
PDF

Was hier typisch ist

  • Ein reportStart()-Band für den Titel und ein reportEnd()-Band für die Zusammenfassung — einmal pro Report
  • Ein Tabellenkopf, danach ein Detailband, das pro Datensatz einmal gerendert wird
  • Eine Gruppe pro Kategorie mit eigenen Kopf- und Fuss-Bändern
  • Eine Zwischensumme pro Gruppe via aggSum()
  • Ein Filterformular mit typisierten Controls (Text, Enum, Number, Relation)

Modul-Struktur

app/Reports/PriceList/
├── Report.php              # bands, groups, filters
├── Source.php              # data + field definitions
└── views/
    ├── report-start.blade.php
    ├── grid-head.blade.php
    ├── row.blade.php
    ├── category-head.blade.php
    ├── category-foot.blade.php
    └── report-end.blade.php

Dateien

<?php

declare(strict_types=1);

namespace App\Reports\PriceList;

use App\Reports\Layout\CompanyLayout;
use Guggach\Reports\Definition\ReportBuilder;
use Guggach\Reports\Filters\FilterBuilder;
use Guggach\Reports\Report as BaseReport;
use Guggach\Reports\Sources\ReportSource;

final class Report extends BaseReport
{
    public function key(): string
    {
        return 'price_list';
    }

    public function name(): string
    {
        return 'Preisliste';
    }

    public function layout(): string
    {
        return CompanyLayout::class;
    }

    public function source(): ReportSource
    {
        return new Source;
    }

    /**
     * Demonstrate the fluent control description: operator listbox for the
     * number, multiselect for the enum and a searchable dual-list for the
     * relation (id/FK) field.
     */
    public function filterControls(): array
    {
        return FilterBuilder::for($this->source())
            ->field('price')->operators(['=', '>=', '<=', 'between', 'in'])
            ->field('category')->multiselect()
            ->field('supplier')->picker(valueField: 'id', labelField: 'label')
            ->build();
    }

    public function define(ReportBuilder $r): void
    {
        // Module-relative view names resolve to app/Reports/PriceList/views/*.
        // Report start/end run once per report; the grid header introduces the
        // table and the detail band renders one row per record.
        $r->reportStart(view: 'report-start');
        $r->gridHeader(view: 'grid-head');
        $r->detail(view: 'row');
        $r->reportEnd(view: 'report-end');

        // Grouped by category (data is sorted by category) with a subtotal.
        $r->group('category', function ($g): void {
            $g->header(view: 'category-head');
            $g->footer(view: 'category-foot');
            $g->aggSum('price', as: 'category_total');
        });
    }
}
<?php

declare(strict_types=1);

namespace App\Reports\PriceList;

use Guggach\Reports\Sources\ReportField;
use Guggach\Reports\Sources\ReportSource;

/**
 * Data source of the price list. Report-local on purpose (spec 7.1): a source
 * is never shared between reports. Filters are applied in memory here; a
 * database-backed variant would push them down instead.
 */
final class Source extends ReportSource
{
    public function records(): iterable
    {
        return $this->applyFilters([
            ['sku' => 'A-100', 'name' => 'Kaffeemaschine', 'category' => 'Küche', 'supplier' => 'Nordwind AG', 'price' => 129.00],
            ['sku' => 'B-200', 'name' => 'Wasserkocher', 'category' => 'Küche', 'supplier' => 'Nordwind AG', 'price' => 39.90],
            ['sku' => 'C-300', 'name' => 'Toaster', 'category' => 'Büro', 'supplier' => 'Berger GmbH', 'price' => 59.50],
            ['sku' => 'D-400', 'name' => 'Mixer', 'category' => 'Büro', 'supplier' => 'Conti AG', 'price' => 89.00],
        ]);
    }

    public function fields(): array
    {
        return [
            new ReportField('sku', 'SKU', type: 'string'),
            new ReportField('name', 'Bezeichnung', type: 'string'),
            new ReportField('category', 'Kategorie', type: 'enum', options: ['Küche', 'Büro']),
            new ReportField('supplier', 'Lieferant', type: 'relation', options: [
                'Nordwind AG', 'Berger GmbH', 'Conti AG', 'Delta Handels',
                'Egger & Co', 'Fuchs AG', 'Graf GmbH', 'Huber AG',
            ]),
            new ReportField('price', 'Preis', type: 'number'),
        ];
    }
}
<h1 style="margin:0 0 12px">{{ $ctx->report->name() }}</h1>
<table style="width:100%; border-collapse:collapse">
    <thead>
        <tr style="border-bottom:2px solid #111">
            <th style="text-align:left">SKU</th>
            <th style="text-align:left">Bezeichnung</th>
            <th style="text-align:right">Preis (CHF)</th>
        </tr>
    </thead>
    <tbody>
<tr style="border-bottom:1px solid #ddd">
    <td>{{ $ctx->sku }}</td>
    <td>{{ $ctx->name }}</td>
    <td style="text-align:right">{{ number_format((float) $ctx->price, 2, '.', "'") }}</td>
</tr>
<tr style="font-weight:600; background:#f3f4f6">
    <td colspan="2">Summe {{ $ctx->currentGroup()['key'] }}</td>
    <td style="text-align:right">{{ number_format((float) $ctx->aggregates['category_total'], 2, '.', "'") }}</td>
</tr>
    </tbody>
</table>

<p style="margin-top:12px; color:#555">{{ $ctx->total }} Positionen</p>
<?php

declare(strict_types=1);

namespace App\Reports\Layout;

use Guggach\Reports\Definition\PageSetup;
use Guggach\Reports\Layouts\Layout;

final class CompanyLayout extends Layout
{
    public function view(): string
    {
        return 'reports.layouts.company';
    }

    public function pageSetup(): PageSetup
    {
        return PageSetup::a4()->portrait()->marginsMm(24, 18, 24, 18);
    }
}