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:
@@ -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),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user