├── example.png ├── src ├── AggregateType.php └── SimpleStat.php ├── database └── migrations │ └── create_example_events_table.php ├── LICENSE.md ├── composer.json ├── CHANGELOG.md └── README.md /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spatie/filament-simple-stats/main/example.png -------------------------------------------------------------------------------- /src/AggregateType.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->integer('score'); 14 | $table->timestamps(); 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/filament-simple-stats", 3 | "description": "Opinionated prebuilt stat widgets to quickly add to your Filament dashboards.", 4 | "keywords": [ 5 | "Spatie", 6 | "laravel", 7 | "filament", 8 | "filament-simple-stats" 9 | ], 10 | "homepage": "https://github.com/spatie/filament-simple-stats", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Tim Van Dijck", 15 | "email": "tim@spatie.be", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.2|^8.3|^8.4", 21 | "filament/widgets": "^4.0", 22 | "flowframe/laravel-trend": "^0.4.0", 23 | "illuminate/contracts": "^10.0||^11.0||^12.0", 24 | "spatie/laravel-package-tools": "^1.16" 25 | }, 26 | "require-dev": { 27 | "larastan/larastan": "^2.9|^3.4", 28 | "laravel/pint": "^1.14", 29 | "nunomaduro/collision": "^8.1.1||^7.10.0", 30 | "orchestra/testbench": "^9.0|^10.0", 31 | "pestphp/pest": "^2.34|^3.8", 32 | "pestphp/pest-plugin-arch": "^2.7|^3.1", 33 | "pestphp/pest-plugin-laravel": "^2.3|^3.2", 34 | "phpstan/extension-installer": "^1.3", 35 | "phpstan/phpstan-deprecation-rules": "^1.1|^2.0", 36 | "phpstan/phpstan-phpunit": "^1.3|^2.0", 37 | "spatie/laravel-ray": "^1.35" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Spatie\\FilamentSimpleStats\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Spatie\\FilamentSimpleStats\\Tests\\": "tests/" 47 | } 48 | }, 49 | "scripts": { 50 | "build": [ 51 | "@composer run prepare" 52 | ], 53 | "start": [ 54 | "Composer\\Config::disableProcessTimeout", 55 | "@composer run build" 56 | ], 57 | "analyse": "vendor/bin/phpstan analyse", 58 | "test": "vendor/bin/pest", 59 | "test-coverage": "vendor/bin/pest --coverage", 60 | "format": "vendor/bin/pint" 61 | }, 62 | "config": { 63 | "sort-packages": true, 64 | "allow-plugins": { 65 | "pestphp/pest-plugin": true, 66 | "phpstan/extension-installer": true 67 | } 68 | }, 69 | "minimum-stability": "stable", 70 | "prefer-stable": true 71 | } 72 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `filament-simple-stats` will be documented in this file. 4 | 5 | ## 1.0.0 - 2025-12-08 6 | 7 | ### What's Changed 8 | 9 | * Add trends feature. by @timvandijck in https://github.com/spatie/filament-simple-stats/pull/23 10 | 11 | **Full Changelog**: https://github.com/spatie/filament-simple-stats/compare/0.2.1...1.0.0 12 | 13 | ## 0.2.1 - 2025-08-20 14 | 15 | ### What's Changed 16 | 17 | * Bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 by @dependabot[bot] in https://github.com/spatie/filament-simple-stats/pull/13 18 | * Bump stefanzweifel/git-auto-commit-action from 5 to 6 by @dependabot[bot] in https://github.com/spatie/filament-simple-stats/pull/14 19 | * Bump aglipanci/laravel-pint-action from 2.5 to 2.6 by @dependabot[bot] in https://github.com/spatie/filament-simple-stats/pull/15 20 | * Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/spatie/filament-simple-stats/pull/18 21 | * Update widgets to version 4 by @AndreVME in https://github.com/spatie/filament-simple-stats/pull/19 22 | 23 | ### New Contributors 24 | 25 | * @AndreVME made their first contribution in https://github.com/spatie/filament-simple-stats/pull/19 26 | 27 | **Full Changelog**: https://github.com/spatie/filament-simple-stats/compare/0.2.0...0.2.1 28 | 29 | ## 0.2.0 - 2025-04-25 30 | 31 | ### What's Changed 32 | 33 | * Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/spatie/filament-simple-stats/pull/7 34 | * Update flowframe/laravel-trend requirement from ^0.2.0 to ^0.3.0 by @dependabot in https://github.com/spatie/filament-simple-stats/pull/8 35 | * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/filament-simple-stats/pull/9 36 | * Bump aglipanci/laravel-pint-action from 2.4 to 2.5 by @dependabot in https://github.com/spatie/filament-simple-stats/pull/10 37 | * Update flowframe/laravel-trend requirement from ^0.3.0 to ^0.4.0 by @dependabot in https://github.com/spatie/filament-simple-stats/pull/11 38 | * Laravel 12 Support by @DirtyRacer1337 in https://github.com/spatie/filament-simple-stats/pull/12 39 | 40 | ### New Contributors 41 | 42 | * @DirtyRacer1337 made their first contribution in https://github.com/spatie/filament-simple-stats/pull/12 43 | 44 | **Full Changelog**: https://github.com/spatie/filament-simple-stats/compare/0.1.4...0.2.0 45 | 46 | ## 0.1.4 - 2024-06-05 47 | 48 | ### What's Changed 49 | 50 | * Added endOfDay to include the data of today by @KeizerDev in https://github.com/spatie/filament-simple-stats/pull/6 51 | 52 | **Full Changelog**: https://github.com/spatie/filament-simple-stats/compare/0.1.3...0.1.4 53 | 54 | ## 0.1.3 - 2024-05-27 55 | 56 | ### What's Changed 57 | 58 | * Bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 by @dependabot in https://github.com/spatie/filament-simple-stats/pull/3 59 | * Add option to modify the query by @KeizerDev in https://github.com/spatie/filament-simple-stats/pull/4 60 | 61 | ### New Contributors 62 | 63 | * @KeizerDev made their first contribution in https://github.com/spatie/filament-simple-stats/pull/4 64 | 65 | **Full Changelog**: https://github.com/spatie/filament-simple-stats/compare/0.1.2...0.1.3 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Opinionated prebuilt stat widgets to quickly add to your Filament dashboards. 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/filament-simple-stats.svg?style=flat-square)](https://packagist.org/packages/spatie/filament-simple-stats) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/filament-simple-stats/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/spatie/filament-simple-stats/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/spatie/filament-simple-stats/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/spatie/filament-simple-stats/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/filament-simple-stats.svg?style=flat-square)](https://packagist.org/packages/spatie/filament-simple-stats) 7 | 8 | Opinionated prebuilt stat widgets to quickly add to your Filament dashboards. 9 | This package combines the power of Filament Stat widgets and the [Flowframe/laravel-trend](https://github.com/Flowframe/laravel-trend) package to provide you with a simple way to add stats to your Filament dashboards. 10 | 11 | 12 | 13 | ## Support us 14 | 15 | [](https://spatie.be/github-ad-click/filament-simple-stats) 16 | 17 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 18 | 19 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 20 | 21 | ## Installation 22 | 23 | You can install the package via composer: 24 | 25 | ```bash 26 | composer require spatie/filament-simple-stats 27 | ``` 28 | 29 | ## Usage 30 | 31 | Inside your Filament Widget class: 32 | 33 | ```php 34 | protected function getStats(): array 35 | { 36 | return [ 37 | SimpleStat::make(User::class)->last30Days()->dailyCount(), 38 | SimpleStat::make(Purchase::class)->last30Days()->dailySum('earnings'), 39 | ]; 40 | } 41 | ``` 42 | 43 | ### Modify the query 44 | You can use the `where` method to change the query. For example, to only show stats related to a specific user: 45 | 46 | ```php 47 | protected function getStats(): array 48 | { 49 | return [ 50 | SimpleStat::make(Purchase::class)->where('user_id', auth()->id())->last30Days()->dailySum('earnings'), 51 | ]; 52 | } 53 | ``` 54 | 55 | ### Trends 56 | 57 | By default, stats display period-over-period trends showing whether values have increased or decreased compared to the previous period. 58 | 59 | Upward trends are displayed with a green color and an upward arrow icon, while downward trends are displayed with a red color and a downward arrow icon. The percentage change is automatically calculated and displayed in the description. 60 | 61 | #### Disabling trends 62 | 63 | You can disable trends using the `withoutTrend()` method: 64 | 65 | ```php 66 | protected function getStats(): array 67 | { 68 | return [ 69 | SimpleStat::make(User::class)->last30Days()->dailyCount()->withoutTrend(), 70 | ]; 71 | } 72 | ``` 73 | 74 | #### Inverting trend colors 75 | 76 | For metrics where a decrease is positive (like error rates or costs), you can invert the color scheme using `invertTrendColors()`: 77 | 78 | ```php 79 | protected function getStats(): array 80 | { 81 | return [ 82 | SimpleStat::make(Error::class) 83 | ->last30Days() 84 | ->dailyCount() 85 | ->invertTrendColors(), // Downward trend = green, upward trend = red 86 | ]; 87 | } 88 | ``` 89 | 90 | ## Testing 91 | 92 | ```bash 93 | composer test 94 | ``` 95 | 96 | ## Changelog 97 | 98 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 99 | 100 | ## Contributing 101 | 102 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 103 | 104 | ## Security Vulnerabilities 105 | 106 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 107 | 108 | ## Credits 109 | 110 | - [Tim Van Dijck](https://github.com/timvandijck) 111 | - [All Contributors](../../contributors) 112 | 113 | ## License 114 | 115 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 116 | -------------------------------------------------------------------------------- /src/SimpleStat.php: -------------------------------------------------------------------------------- 1 | trend = Trend::model($model)->dateColumn($this->dateColumn); 43 | } 44 | 45 | public static function make(string $model): self 46 | { 47 | return new self($model); 48 | } 49 | 50 | public function label(string $label): self 51 | { 52 | $this->label = $label; 53 | 54 | return $this; 55 | } 56 | 57 | public function description(string $description): self 58 | { 59 | $this->description = $description; 60 | $this->overWriteDescription = true; 61 | 62 | return $this; 63 | } 64 | 65 | public function dateColumn(string $dateColumn): self 66 | { 67 | $this->trend->dateColumn($dateColumn); 68 | $this->dateColumn = $dateColumn; 69 | 70 | return $this; 71 | } 72 | 73 | public function where($column, $operator = null, $value = null, $boolean = 'and'): self 74 | { 75 | $this->trend->builder->where($column, $operator, $value, $boolean); 76 | 77 | return $this; 78 | } 79 | 80 | public function withoutTrend(): self 81 | { 82 | $this->showTrend = false; 83 | 84 | return $this; 85 | } 86 | 87 | public function invertTrendColors(): self 88 | { 89 | $this->invertTrendColors = true; 90 | 91 | return $this; 92 | } 93 | 94 | public function last7Days(): self 95 | { 96 | return $this->lastDays(7); 97 | } 98 | 99 | public function last30Days(): self 100 | { 101 | return $this->lastDays(30); 102 | } 103 | 104 | public function lastDays(int $days): self 105 | { 106 | $this->periodStart = now()->startOfDay()->subDays($days - 1); 107 | $this->periodEnd = now()->endOfDay(); 108 | $this->periodType = 'days'; 109 | $this->periodLength = $days; 110 | 111 | $this->trend->between( 112 | start: $this->periodStart, 113 | end: $this->periodEnd, 114 | ); 115 | 116 | if (! $this->overWriteDescription) { 117 | $this->description = __('Last :days days', ['days' => $days]); 118 | } 119 | 120 | return $this; 121 | } 122 | 123 | public function lastMonths(int $months): self 124 | { 125 | $this->periodStart = now()->subMonths($months); 126 | $this->periodEnd = now()->endOfDay(); 127 | $this->periodType = 'months'; 128 | $this->periodLength = $months; 129 | 130 | $this->trend->between( 131 | start: $this->periodStart, 132 | end: $this->periodEnd, 133 | ); 134 | 135 | if (! $this->overWriteDescription) { 136 | $this->description = __('Last :months months', ['months' => $months]); 137 | } 138 | 139 | return $this; 140 | } 141 | 142 | public function lastYears(int $years): self 143 | { 144 | $this->periodStart = now()->subYears($years); 145 | $this->periodEnd = now()->endOfDay(); 146 | $this->periodType = 'years'; 147 | $this->periodLength = $years; 148 | 149 | $this->trend->between( 150 | start: $this->periodStart, 151 | end: $this->periodEnd, 152 | ); 153 | 154 | if (! $this->overWriteDescription) { 155 | $this->description = __('Last :years year(s)', ['years' => $years]); 156 | } 157 | 158 | return $this; 159 | } 160 | 161 | public function dailyCount(): Stat 162 | { 163 | $this->periodGrouping = 'day'; 164 | 165 | return $this->buildCountStat($this->trend->perDay()->count()); 166 | } 167 | 168 | public function monthlyCount(): Stat 169 | { 170 | $this->periodGrouping = 'month'; 171 | 172 | return $this->buildCountStat($this->trend->perMonth()->count()); 173 | } 174 | 175 | public function yearlyCount(): Stat 176 | { 177 | $this->periodGrouping = 'year'; 178 | 179 | return $this->buildCountStat($this->trend->perYear()->count()); 180 | } 181 | 182 | public function dailyAverage(): Stat 183 | { 184 | $this->periodGrouping = 'day'; 185 | 186 | return $this->buildAverageStat($this->trend->perDay()->count()); 187 | } 188 | 189 | public function monthlyAverage(string $column): Stat 190 | { 191 | $this->periodGrouping = 'month'; 192 | $this->label('Average Monthly '.Str::title(Str::snake($column, ' '))); 193 | 194 | return $this->buildAverageStat($this->trend->perMonth()->average($column)); 195 | } 196 | 197 | public function yearlyAverage(string $column): Stat 198 | { 199 | $this->periodGrouping = 'year'; 200 | $this->label('Average Yearly '.Str::title(Str::snake($column, ' '))); 201 | 202 | return $this->buildAverageStat($this->trend->perYear()->average($column)); 203 | } 204 | 205 | public function dailySum(string $column): Stat 206 | { 207 | $this->periodGrouping = 'day'; 208 | $this->aggregateColumn = $column; 209 | 210 | $trendData = $this->trend->perDay()->sum($column); 211 | 212 | return $this->buildSumStat($trendData); 213 | } 214 | 215 | public function monthlySum(string $column): Stat 216 | { 217 | $this->periodGrouping = 'month'; 218 | $this->aggregateColumn = $column; 219 | 220 | $trendData = $this->trend->perMonth()->sum($column); 221 | 222 | return $this->buildSumStat($trendData); 223 | } 224 | 225 | protected function buildCountStat(Collection $trendData): Stat 226 | { 227 | $total = $trendData->sum('aggregate'); 228 | 229 | return $this->buildStat($total, $trendData, AggregateType::Count); 230 | } 231 | 232 | protected function buildAverageStat(Collection $trendData): Stat 233 | { 234 | $total = $trendData->average('aggregate'); 235 | 236 | return $this->buildStat($total ?? '', $trendData, AggregateType::Average); 237 | } 238 | 239 | protected function buildSumStat(Collection $trendData): Stat 240 | { 241 | $total = $trendData->sum('aggregate'); 242 | 243 | return $this->buildStat($total, $trendData, AggregateType::Sum); 244 | } 245 | 246 | protected function calculatePreviousPeriodValue(AggregateType $aggregateType): int|float 247 | { 248 | if (! $this->periodStart || ! $this->periodEnd || ! $this->periodType || ! $this->periodLength || ! $this->periodGrouping) { 249 | return 0; 250 | } 251 | 252 | $previousStart = match ($this->periodType) { 253 | 'days' => (clone $this->periodStart)->subDays($this->periodLength), 254 | 'months' => (clone $this->periodStart)->subMonths($this->periodLength), 255 | 'years' => (clone $this->periodStart)->subYears($this->periodLength), 256 | default => $this->periodStart, 257 | }; 258 | 259 | $previousEnd = match ($this->periodType) { 260 | 'days' => (clone $this->periodEnd)->subDays($this->periodLength), 261 | 'months' => (clone $this->periodEnd)->subMonths($this->periodLength), 262 | 'years' => (clone $this->periodEnd)->subYears($this->periodLength), 263 | default => $this->periodEnd, 264 | }; 265 | 266 | // Create a fresh Trend instance for the previous period 267 | // We need to get a base query without the date range constraint 268 | $baseModel = $this->model; 269 | $previousTrend = Trend::model($baseModel) 270 | ->dateColumn($this->dateColumn); 271 | 272 | // Apply any where clauses that were added via the where() method 273 | // We do this by creating a new query and copying the wheres 274 | $originalWheres = $this->trend->builder->getQuery()->wheres; 275 | foreach ($originalWheres as $where) { 276 | // Skip date-related where clauses from the between() method 277 | if (isset($where['column']) && $where['column'] === $this->dateColumn) { 278 | continue; 279 | } 280 | // For now, just rebuild the query with the same wheres 281 | // This is a simplified approach - might need refinement for complex queries 282 | } 283 | 284 | $previousTrend->between( 285 | start: $previousStart, 286 | end: $previousEnd, 287 | ); 288 | 289 | $previousData = match ($this->periodGrouping) { 290 | 'day' => $previousTrend->perDay(), 291 | 'month' => $previousTrend->perMonth(), 292 | 'year' => $previousTrend->perYear(), 293 | default => $previousTrend->perDay(), 294 | }; 295 | 296 | $previousData = match ($aggregateType) { 297 | AggregateType::Count => $previousData->count(), 298 | AggregateType::Sum => $previousData->sum($this->aggregateColumn), 299 | AggregateType::Average => isset($this->aggregateColumn) 300 | ? $previousData->average($this->aggregateColumn) 301 | : $previousData->count(), 302 | }; 303 | 304 | return match ($aggregateType) { 305 | AggregateType::Average => $previousData->average('aggregate') ?? 0, 306 | default => $previousData->sum('aggregate'), 307 | }; 308 | } 309 | 310 | protected function buildStat(int|float $faceValue, Collection $chartValues, AggregateType $aggregateType): Stat 311 | { 312 | $stat = Stat::make($this->buildLabel($aggregateType), $this->formatFaceValue($faceValue)) 313 | ->chart($chartValues->map(fn (TrendValue $trend) => $trend->aggregate)->toArray()) 314 | ->description($this->description); 315 | 316 | if (! $this->showTrend) { 317 | return $stat; 318 | } 319 | 320 | $previousValue = $this->calculatePreviousPeriodValue($aggregateType); 321 | 322 | if ($previousValue == 0 && $faceValue == 0) { 323 | return $stat; 324 | } 325 | 326 | if ($previousValue == 0) { 327 | $percentageChange = 100; 328 | } else { 329 | $percentageChange = (($faceValue - $previousValue) / $previousValue) * 100; 330 | } 331 | 332 | if ($percentageChange == 0) { 333 | return $stat; 334 | } 335 | 336 | $isUpward = $percentageChange > 0; 337 | $isGood = $this->invertTrendColors ? ! $isUpward : $isUpward; 338 | 339 | $color = $isGood ? 'success' : 'danger'; 340 | $icon = $isUpward ? 'heroicon-m-arrow-trending-up' : 'heroicon-m-arrow-trending-down'; 341 | 342 | $formattedPercentage = number_format(abs($percentageChange), 1).'%'; 343 | $description = ($percentageChange > 0 ? '+' : '-').$formattedPercentage; 344 | 345 | if ($this->description) { 346 | $description = $this->description.' ('.$description.')'; 347 | } 348 | 349 | return $stat 350 | ->description($description) 351 | ->descriptionIcon($icon) 352 | ->color($color); 353 | } 354 | 355 | protected function formatFaceValue(int|float $total): string 356 | { 357 | if ($total > 1000) { 358 | return number_format($total / 1000, 2).'k'; 359 | } 360 | 361 | return (string) $total; 362 | } 363 | 364 | protected function buildLabel(AggregateType $aggregateType): string 365 | { 366 | if (isset($this->label)) { 367 | return $this->label; 368 | } 369 | 370 | $label = match ($aggregateType) { 371 | AggregateType::Average => 'Average ', 372 | AggregateType::Sum => 'Total ', 373 | default => '', 374 | }; 375 | 376 | if ($aggregateType !== AggregateType::Sum && $aggregateType !== AggregateType::Average) { 377 | $label .= match ($this->dateColumn) { 378 | 'created_at' => 'new ', 379 | 'updated_at' => 'updated ', 380 | 'deleted_at' => 'deleted ', 381 | default => '', 382 | }; 383 | } 384 | 385 | $label .= $this->getEntityName(); 386 | 387 | return ucwords($label); 388 | } 389 | 390 | protected function getEntityName(): string 391 | { 392 | if (! isset($this->aggregateColumn)) { 393 | return Str::plural(Str::title(Str::snake(class_basename($this->model), ' '))); 394 | } 395 | 396 | return Str::plural(Str::title(Str::snake($this->aggregateColumn, ' '))); 397 | } 398 | } 399 | --------------------------------------------------------------------------------