Phase 7 dashboard widgets, Gitea CI, and branded landing page (T7.1)

- BookingsTodayWidget, RecentBookingsTableWidget (Booking module) and
  RevenueChartWidget, PaymentFailureRateWidget (Payment module) dashboard
  widgets, auto-registered via each plugin's existing discoverWidgets().
- .gitea/workflows/tests.yml: Postgres-backed Pest run on push/PR.
- Landing page (resources/views/welcome.blade.php) now shows the Famous
  Linnyone4 EV logo with a single admin login link, and the Filament admin
  panel uses the same logo as its brand logo.
This commit is contained in:
Nyan Lin Paing
2026-08-10 00:15:19 +07:00
parent d528cf16ec
commit 60413bdebf
12 changed files with 375 additions and 259 deletions
@@ -0,0 +1,42 @@
<?php
namespace Modules\Payment\Filament\Widgets;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Modules\Payment\Enums\PaymentStatus;
use Modules\Payment\Models\Payment;
/**
* Failure rate over the trailing 30 days, among payments that reached a
* terminal state (completed or failed) pending attempts are excluded
* since they haven't resolved either way yet.
*/
class PaymentFailureRateWidget extends BaseWidget
{
protected static ?int $sort = 3;
protected function getStats(): array
{
$since = today()->subDays(30);
$completed = Payment::query()
->where('status', PaymentStatus::Completed)
->where('initiated_at', '>=', $since)
->count();
$failed = Payment::query()
->where('status', PaymentStatus::Failed)
->where('initiated_at', '>=', $since)
->count();
$resolved = $completed + $failed;
$rate = $resolved > 0 ? round(($failed / $resolved) * 100, 1) : 0.0;
return [
Stat::make('Payment Failure Rate', $rate.'%')
->description("{$failed} failed of {$resolved} resolved (last 30 days)")
->color($rate >= 20 ? 'danger' : ($rate > 0 ? 'warning' : 'success')),
];
}
}
@@ -0,0 +1,71 @@
<?php
namespace Modules\Payment\Filament\Widgets;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Modules\Payment\Enums\PaymentStatus;
use Modules\Payment\Models\Payment;
/**
* 30-day revenue trend from completed payments. Cached per day (T7.1)
* refreshes at most once every 5 minutes since this aggregates a whole
* month of rows on every dashboard load otherwise.
*/
class RevenueChartWidget extends ChartWidget
{
protected static ?int $sort = 1;
protected ?string $heading = 'Revenue (Last 30 Days)';
protected function getData(): array
{
$days = Cache::tags('payments')->remember(
'dashboard:revenue-chart:'.today()->toDateString(),
now()->addMinutes(5),
fn () => $this->revenueByDay(),
);
return [
'datasets' => [
[
'label' => 'Revenue',
'data' => $days->pluck('total')->all(),
'fill' => true,
],
],
'labels' => $days->pluck('label')->all(),
];
}
protected function getType(): string
{
return 'line';
}
/**
* @return Collection<int, array{label: string, total: float}>
*/
private function revenueByDay(): Collection
{
$start = today()->subDays(29);
$totals = Payment::query()
->where('status', PaymentStatus::Completed)
->whereDate('completed_at', '>=', $start)
->selectRaw('DATE(completed_at) as day, SUM(amount) as total')
->groupBy('day')
->pluck('total', 'day');
return collect(range(0, 29))
->map(function (int $offset) use ($start, $totals) {
$date = $start->copy()->addDays($offset);
return [
'label' => $date->format('M j'),
'total' => (float) ($totals[$date->toDateString()] ?? 0),
];
});
}
}
@@ -0,0 +1,22 @@
<?php
use App\Models\User;
use Livewire\Livewire;
use Modules\Payment\Filament\Widgets\PaymentFailureRateWidget;
use Modules\Payment\Models\Payment;
test('it computes the failure rate among resolved payments in the last 30 days', function () {
$this->actingAs(User::factory()->create());
Payment::factory()->completed()->create(['initiated_at' => today()]);
Payment::factory()->completed()->create(['initiated_at' => today()]);
Payment::factory()->completed()->create(['initiated_at' => today()]);
Payment::factory()->failed()->create(['initiated_at' => today()]);
Payment::factory()->create(['initiated_at' => today()]); // pending, excluded from resolved total
Payment::factory()->failed()->create(['initiated_at' => today()->subDays(40)]); // outside window
Livewire::test(PaymentFailureRateWidget::class)
->assertOk()
->assertSee('25%')
->assertSee('1 failed of 4 resolved (last 30 days)');
});
@@ -0,0 +1,19 @@
<?php
use Modules\Payment\Filament\Widgets\RevenueChartWidget;
use Modules\Payment\Models\Payment;
test('it sums completed payment amounts per day over the last 30 days', function () {
Payment::factory()->completed()->create(['amount' => 10000, 'completed_at' => today()]);
Payment::factory()->completed()->create(['amount' => 5000, 'completed_at' => today()]);
Payment::factory()->create(['amount' => 99999, 'completed_at' => null]); // pending, excluded
Payment::factory()->completed()->create(['amount' => 77777, 'completed_at' => today()->subDays(40)]); // outside window
$widget = new RevenueChartWidget;
$getData = (new ReflectionMethod($widget, 'getData'));
$getData->setAccessible(true);
$data = $getData->invoke($widget);
expect($data['labels'])->toHaveCount(30)
->and(array_sum($data['datasets'][0]['data']))->toBe(15000.0);
});