ifnex/04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php
Kazem Alghasi 91984dbde3 feat(ui): redesign admin dashboard with layered grid and custom branding
Implement a comprehensive overhaul of the Filament admin dashboard,
introducing a structured 12-column responsive grid layout and enhanced
visual identity.

Key changes include:
- Redesigned Dashboard layout with a 4-layer progressive disclosure
  architecture (KPIs, Exchange Rates, Data Tables, and Trends).
- Introduced custom IFNEX design tokens via CSS, including branded
  stat cards, status badges, and improved sidebar typography.
- Enhanced FinanceOverviewWidget with real-time trend analysis and
  sparkline-ready data structures.
- Refactored RecentShipments and RecentTransactions widgets to use
  custom Blade templates for a more polished, consistent UI.
- Integrated brand assets including the company logo and custom
  authentication pages.
- Added global number formatting helpers and integrated them into
  financial widgets.
- Updated AdminPanelProvider to support custom login pages and
  brand-specific header/footer hooks.
2026-10-03 11:08:50 +03:30

179 lines
7.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Filament\Widgets;
use App\Enums\ShipmentStatus;
use App\Models\Shipment;
use App\Models\Wallet;
use App\Models\WalletTransaction;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Support\Carbon;
/**
* Layer 1 — Top KPI row on the IFNEX dashboard.
*
* Returns exactly 4 stat cards in a 4-column grid:
* 1. سفارشات در انتظار تأیید — amber (clock)
* 2. درآمد امروز — green (trending up)
* 3. پرداخت‌های معوق — red (banknotes)
* 4. موجودی کیف پول‌ها — blue (wallet)
*/
class FinanceOverviewWidget extends StatsOverviewWidget
{
protected static ?string $pollingInterval = '30s';
protected static ?int $sort = 1;
protected static bool $isLazy = false;
// ردیف هدر گرید ۱۲ ستونه است: 10/12 برای KPIها + 2/12 کارت ساعت (DashboardInfoWidget)
protected int | string | array $columnSpan = 10;
/**
* Stat cards render in a 4-column responsive grid:
* - default (mobile): 1 col (stacked)
* - sm: 2 cols
* - lg: 4 cols (the canonical IFNEX layout)
*/
protected int|string|array $columns = 4;
protected function getStats(): array
{
$today = today();
$yesterday = today()->subDay();
// ─── KPI 1: سفارشات در انتظار تأیید ───
$pendingApprovalCount = Shipment::where('status', ShipmentStatus::PendingApproval)->count();
$approvedUnpaidCount = Shipment::where('status', ShipmentStatus::Approved)->count();
$pendingOrdersTotal = $pendingApprovalCount + $approvedUnpaidCount;
$pendingSparkline = [];
for ($i = 6; $i >= 0; $i--) {
$date = today()->subDays($i);
$pendingSparkline[] = (int) Shipment::whereDate('created_at', $date)
->whereIn('status', [ShipmentStatus::PendingApproval, ShipmentStatus::Approved])
->count();
}
// ─── KPI 2: درآمد امروز ───
$todayRevenue = (float) WalletTransaction::where('type', 'order_payment')
->where('status', 'completed')
->where('amount', '>', 0)
->whereDate('created_at', $today)
->sum('amount');
$yesterdayRevenue = (float) WalletTransaction::where('type', 'order_payment')
->where('status', 'completed')
->where('amount', '>', 0)
->whereDate('created_at', $yesterday)
->sum('amount');
[$revPercent, $revDirection, $revFormatted] = ifnex_trend_percent($todayRevenue, $yesterdayRevenue);
$revenueSparkline = [];
for ($i = 6; $i >= 0; $i--) {
$date = today()->subDays($i);
$revenueSparkline[] = (float) WalletTransaction::where('type', 'order_payment')
->where('status', 'completed')
->where('amount', '>', 0)
->whereDate('created_at', $date)
->sum('amount');
}
// ─── KPI 3: پرداخت‌های معوق ───
$pendingAmount = (float) Shipment::where('status', ShipmentStatus::Approved)->sum('total_fee');
$pendingShipmentsCount = Shipment::where('status', ShipmentStatus::Approved)->count();
$yesterdayPendingAmount = (float) Shipment::where('status', ShipmentStatus::Approved)
->whereDate('updated_at', '<', $today)
->sum('total_fee');
[$pendingPercent, $pendingDirection, $pendingFormatted] = ifnex_trend_percent(
$pendingAmount,
$yesterdayPendingAmount
);
// ─── KPI 4: موجودی کیف پول‌ها ───
$totalWalletBalance = (float) Wallet::sum('balance');
$activeWallets = Wallet::where('balance', '>', 0)->count();
$walletSparkline = [];
for ($i = 6; $i >= 0; $i--) {
$date = today()->subDays($i);
$walletSparkline[] = (float) WalletTransaction::where('status', 'completed')
->where('amount', '>', 0)
->whereDate('created_at', $date)
->sum('amount');
}
return [
Stat::make('سفارشات در انتظار', ifnex_persian_digits(number_format($pendingOrdersTotal)))
->description(
$pendingOrdersTotal > 0
? sprintf('%s در انتظار تأیید، %s آماده پرداخت',
ifnex_persian_digits(number_format($pendingApprovalCount)),
ifnex_persian_digits(number_format($approvedUnpaidCount)))
: 'سفارش جدیدی نیست'
)
->descriptionIcon('heroicon-m-clock')
->icon('heroicon-o-clock')
->color('warning')
->chart($pendingSparkline)
->url(
$pendingOrdersTotal > 0
? route('filament.admin.resources.shipments.index')
: null
),
Stat::make('درآمد امروز', ifnex_format_number($todayRevenue))
->description(
$revDirection === 'up' ? sprintf('↑ %s نسبت به دیروز', $revFormatted) :
($revDirection === 'down' ? sprintf('↓ %s نسبت به دیروز', $revFormatted) :
'بدون تغییر نسبت به دیروز')
)
->descriptionIcon(
$revDirection === 'up' ? 'heroicon-m-arrow-trending-up' :
($revDirection === 'down' ? 'heroicon-m-arrow-trending-down' : 'heroicon-m-minus')
)
->icon('heroicon-o-arrow-trending-up')
->color(
$revDirection === 'up' ? 'success' :
($revDirection === 'down' ? 'danger' : 'gray')
)
->chart($revenueSparkline)
->url(route('filament.admin.resources.wallet-transactions.index')),
Stat::make('پرداخت‌های معوق', ifnex_format_number($pendingAmount))
->description(
$pendingShipmentsCount > 0
? sprintf('%s سفارش در انتظار پرداخت — %s',
ifnex_persian_digits(number_format($pendingShipmentsCount)),
$pendingDirection === 'up' ? sprintf('↑ %s', $pendingFormatted) :
($pendingDirection === 'down' ? sprintf('↓ %s', $pendingFormatted) : 'بدون تغییر'))
: 'پرداخت معوقی نیست'
)
->descriptionIcon(
$pendingDirection === 'up' ? 'heroicon-m-arrow-trending-up' :
($pendingDirection === 'down' ? 'heroicon-m-arrow-trending-down' : 'heroicon-m-minus')
)
->icon('heroicon-o-banknotes')
->color('danger')
->chart($pendingSparkline)
->url(
$pendingShipmentsCount > 0
? route('filament.admin.resources.shipments.index')
: null
),
Stat::make('موجودی کیف پول‌ها', ifnex_format_number($totalWalletBalance))
->description(
sprintf('%s کیف پول فعال', ifnex_persian_digits(number_format($activeWallets)))
)
->descriptionIcon('heroicon-m-wallet')
->icon('heroicon-o-wallet')
->color('info')
->chart($walletSparkline)
->url(route('filament.admin.resources.wallets.index')),
];
}
}