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.
179 lines
7.8 KiB
PHP
179 lines
7.8 KiB
PHP
<?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')),
|
||
];
|
||
}
|
||
} |