From f61de6f3a5085baa346bcb77b75006d315c4ccc2 Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Mon, 10 Aug 2026 04:08:29 +0330 Subject: [PATCH] refactor(ui): redesign admin dashboard widgets and exchange service Refactor the Filament dashboard by replacing the legacy FinanceOverviewWidget with a new set of specialized widgets: - DashboardInfoWidget for general information - ExchangeRateWidget for real-time rate monitoring - WalletStats for financial overview Additionally, refactor the ExchangeRateService to improve encapsulation and clean up the ExchangeRateHistory model by moving business logic (change calculation and recording) from the model to the service layer. Changes include: - Removing deprecated helper methods from ExchangeRateHistory model - Implementing direct queries in ExchangeRateService to replace model scopes - Adding support for rate chart data retrieval - Reordering and updating widget sorting in AdminPanelProvider --- .../Filament/Widgets/DashboardInfoWidget.php | 85 +++++++++++++++++ .../Filament/Widgets/ExchangeRateWidget.php | 67 ++++++++++++++ .../Widgets/FinanceOverviewWidget.php | 44 --------- .../Widgets/RecentTransactionsWidget.php | 2 +- .../Widgets/TransactionChartWidget.php | 4 +- .../app/Filament/Widgets/WalletStats.php | 72 +++++++++++++++ 04_Laravel/app/Models/ExchangeRateHistory.php | 39 -------- .../Providers/Filament/AdminPanelProvider.php | 10 +- .../app/Services/ExchangeRateService.php | 57 +++++++++++- .../filament/widgets/dashboard-info.blade.php | 92 +++++++++++++++++++ 10 files changed, 378 insertions(+), 94 deletions(-) create mode 100644 04_Laravel/app/Filament/Widgets/DashboardInfoWidget.php create mode 100644 04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php delete mode 100644 04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php create mode 100644 04_Laravel/app/Filament/Widgets/WalletStats.php create mode 100644 04_Laravel/resources/views/filament/widgets/dashboard-info.blade.php diff --git a/04_Laravel/app/Filament/Widgets/DashboardInfoWidget.php b/04_Laravel/app/Filament/Widgets/DashboardInfoWidget.php new file mode 100644 index 0000000..3949d9d --- /dev/null +++ b/04_Laravel/app/Filament/Widgets/DashboardInfoWidget.php @@ -0,0 +1,85 @@ +format('l, j F Y'); + + // تاریخ شمسی با Morilog\Jalali (ساده‌تر) + $jalaliDate = Jalalian::fromCarbon($now)->format('l، d F Y'); + + // ساعت + $time = $now->format('H:i'); + + // آخرین ۵ سفارش + $recentShipments = Shipment::query() + ->with(['fromCountry', 'toCountry']) + ->orderBy('created_at', 'desc') + ->limit(5) + ->get() + ->map(function ($s) { + return [ + 'awb' => $s->awb_no, + 'route' => ($s->fromCountry?->name ?? '?') . ' → ' . ($s->toCountry?->name ?? '?'), + 'status' => $s->status?->value ?? '—', + 'status_label' => $this->statusLabel($s->status?->value), + 'status_color' => $this->statusColor($s->status?->value), + 'date' => Jalalian::fromCarbon($s->created_at)->format('Y/m/d'), + 'amount' => $s->total_fee ? number_format($s->total_fee) . ' ریال' : '—', + ]; + }); + + return [ + 'gregorianDate' => $gregorianDate, + 'jalaliDate' => $jalaliDate, + 'time' => $time, + 'recentShipments' => $recentShipments, + 'userName' => auth()->user()?->name ?? 'کاربر', + ]; + } + + private function statusLabel(?string $status): string + { + return match($status) { + 'processed' => 'پردازش شده', + 'picked_up' => 'تحویل گرفته شده', + 'in_transit' => 'در حال حمل', + 'out_for_delivery' => 'در مسیر تحویل', + 'delivered' => 'تحویل داده شده', + 'failed' => 'ناموفق', + 'returned' => 'برگشتی', + default => '—', + }; + } + + private function statusColor(?string $status): string + { + return match($status) { + 'processed' => 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300', + 'picked_up' => 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300', + 'in_transit' => 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300', + 'out_for_delivery' => 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300', + 'delivered' => 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300', + 'failed' => 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300', + 'returned' => 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300', + default => 'bg-gray-100 text-gray-700', + }; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php b/04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php new file mode 100644 index 0000000..253e17f --- /dev/null +++ b/04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php @@ -0,0 +1,67 @@ +getLatestRateInfo('AED', 'IRR'); + $usdInfo = $service->getLatestRateInfo('USD', 'IRR'); + + $aedChangePercent = $aedInfo['change_percent']; + $usdChangePercent = $usdInfo['change_percent']; + + // ✅ استفاده از متد جدید getRateChart (بدون scope) + $aedChart = $service->getRateChart('AED', 'IRR', 7); + $usdChart = $service->getRateChart('USD', 'IRR', 7); + + return [ + Stat::make('نرخ درهم (AED)', number_format($aedInfo['current_rate']) . ' ریال') + ->description($aedChangePercent !== null + ? 'تغییر: ' . number_format($aedChangePercent, 2) . '% — توسط ' . ($aedInfo['changed_by'] ?? 'سیستم') + : 'آخرین تغییر: ' . ($aedInfo['last_updated']?->format('Y/m/d H:i') ?? '—')) + ->descriptionIcon( + $aedChangePercent > 0 ? 'heroicon-m-arrow-trending-up' : + ($aedChangePercent < 0 ? 'heroicon-m-arrow-trending-down' : 'heroicon-m-minus') + ) + ->color( + $aedChangePercent > 0 ? 'danger' : + ($aedChangePercent < 0 ? 'success' : 'gray') + ) + ->chart(!empty($aedChart) ? $aedChart : [0]), + + Stat::make('نرخ دلار (USD)', number_format($usdInfo['current_rate']) . ' ریال') + ->description($usdChangePercent !== null + ? 'تغییر: ' . number_format($usdChangePercent, 2) . '% — توسط ' . ($usdInfo['changed_by'] ?? 'سیستم') + : 'آخرین تغییر: ' . ($usdInfo['last_updated']?->format('Y/m/d H:i') ?? '—')) + ->descriptionIcon( + $usdChangePercent > 0 ? 'heroicon-m-arrow-trending-up' : + ($usdChangePercent < 0 ? 'heroicon-m-arrow-trending-down' : 'heroicon-m-minus') + ) + ->color( + $usdChangePercent > 0 ? 'danger' : + ($usdChangePercent < 0 ? 'success' : 'gray') + ) + ->chart(!empty($usdChart) ? $usdChart : [0]), + + Stat::make('تغییرات امروز', + ExchangeRateHistory::whereDate('created_at', today())->count() + ) + ->description('تغییرات نرخ ثبت‌شده در امروز') + ->descriptionIcon('heroicon-m-arrow-path') + ->color('info'), + ]; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php b/04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php deleted file mode 100644 index 3750b00..0000000 --- a/04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php +++ /dev/null @@ -1,44 +0,0 @@ -count(); - - return [ - Stat::make('مجموع موجودی کیف پول‌ها', number_format($totalBalance) . ' ریال') - ->description('موجودی کل کاربران') - ->descriptionIcon('heroicon-o-wallet') - ->color('success'), - - Stat::make('مجموع واریزی‌ها', number_format($totalDeposited) . ' ریال') - ->description('کل مبالغ شارژ شده') - ->descriptionIcon('heroicon-o-arrow-down') - ->color('info'), - - Stat::make('مجموع برداشت‌ها', number_format($totalWithdrawn) . ' ریال') - ->description('کل مبالغ برداشت شده') - ->descriptionIcon('heroicon-o-arrow-up') - ->color('warning'), - - Stat::make('تراکنش‌های در انتظار', number_format($pendingCount)) - ->description('از کل ' . number_format($transactionCount) . ' تراکنش') - ->descriptionIcon('heroicon-o-clock') - ->color($pendingCount > 0 ? 'danger' : 'success'), - ]; - } -} diff --git a/04_Laravel/app/Filament/Widgets/RecentTransactionsWidget.php b/04_Laravel/app/Filament/Widgets/RecentTransactionsWidget.php index 296410c..965f270 100644 --- a/04_Laravel/app/Filament/Widgets/RecentTransactionsWidget.php +++ b/04_Laravel/app/Filament/Widgets/RecentTransactionsWidget.php @@ -12,7 +12,7 @@ class RecentTransactionsWidget extends BaseWidget { protected static ?string $pollingInterval = '30s'; protected static ?string $heading = 'آخرین تراکنش‌ها'; - protected static ?int $sort = 2; + protected static ?int $sort = 5; public function table(Table $table): Table { diff --git a/04_Laravel/app/Filament/Widgets/TransactionChartWidget.php b/04_Laravel/app/Filament/Widgets/TransactionChartWidget.php index 132b0c7..4334393 100644 --- a/04_Laravel/app/Filament/Widgets/TransactionChartWidget.php +++ b/04_Laravel/app/Filament/Widgets/TransactionChartWidget.php @@ -8,7 +8,9 @@ use Illuminate\Support\Facades\DB; class TransactionChartWidget extends ChartWidget { - protected static ?string $pollingInterval = '60s'; + +protected static ?int $sort = 4; +protected static ?string $pollingInterval = '60s'; protected static ?string $heading = 'روند تراکنش‌های ۳۰ روز اخیر'; protected static ?string $maxHeight = '300px'; diff --git a/04_Laravel/app/Filament/Widgets/WalletStats.php b/04_Laravel/app/Filament/Widgets/WalletStats.php new file mode 100644 index 0000000..3a46339 --- /dev/null +++ b/04_Laravel/app/Filament/Widgets/WalletStats.php @@ -0,0 +1,72 @@ +where('amount', '>', 0) + ->sum('amount'); + $totalWithdrawals = WalletTransaction::where('status', 'completed') + ->where('amount', '<', 0) + ->sum(DB::raw('ABS(amount)')); + $todayTransactions = WalletTransaction::whereDate('created_at', today())->count(); + $activeWallets = Wallet::where('balance', '>', 0)->count(); + $pendingTransactions = WalletTransaction::where('status', 'pending')->count(); + + $chartData = []; + for ($i = 6; $i >= 0; $i--) { + $date = now()->subDays($i)->format('Y-m-d'); + $chartData[] = WalletTransaction::whereDate('created_at', $date) + ->where('status', 'completed') + ->sum('amount'); + } + + return [ + Stat::make('موجودی کل', number_format($totalBalance) . ' ریال') + ->description('مجموع موجودی همه کاربران') + ->descriptionIcon('heroicon-m-banknotes') + ->color('success') + ->chart($chartData), + + Stat::make('مجموع واریزی‌ها', number_format($totalDeposits) . ' ریال') + ->description('کل مبالغ واریز شده موفق') + ->descriptionIcon('heroicon-m-arrow-down-on-square') + ->color('primary'), + + Stat::make('مجموع برداشت‌ها', number_format($totalWithdrawals) . ' ریال') + ->description('کل مبالغ برداشت شده موفق') + ->descriptionIcon('heroicon-m-arrow-up-on-square') + ->color('danger'), + + Stat::make('تراکنش‌های امروز', number_format($todayTransactions)) + ->description('تعداد تراکنش‌های ثبت‌شده در امروز') + ->descriptionIcon('heroicon-m-calendar') + ->color('info'), + + Stat::make('کیف پول‌های فعال', number_format($activeWallets)) + ->description('کاربرانی با موجودی مثبت') + ->descriptionIcon('heroicon-m-user-group') + ->color('success'), + + Stat::make('در انتظار بررسی', number_format($pendingTransactions)) + ->description('تراکنش‌های نیاز به تأیید') + ->descriptionIcon('heroicon-m-clock') + ->color($pendingTransactions > 0 ? 'warning' : 'gray'), + ]; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Models/ExchangeRateHistory.php b/04_Laravel/app/Models/ExchangeRateHistory.php index 4ab37ef..902eafb 100644 --- a/04_Laravel/app/Models/ExchangeRateHistory.php +++ b/04_Laravel/app/Models/ExchangeRateHistory.php @@ -31,43 +31,4 @@ class ExchangeRateHistory extends Model { return $this->belongsTo(User::class, 'changed_by'); } - - // Helper برای محاسبه درصد تغییر - public static function calculateChangePercent($old, $new): ?float - { - if (!$old || $old == 0) return null; - return round((($new - $old) / $old) * 100, 4); - } - - // Helper برای ثبت تغییر - public static function recordChange( - string $from, - string $to, - float $oldRate, - float $newRate, - string $source = 'manual', - ?int $userId = null, - ?string $apiSource = null, - ?string $notes = null - ): self { - return self::create([ - 'currency_from' => $from, - 'currency_to' => $to, - 'old_rate' => $oldRate, - 'new_rate' => $newRate, - 'change_percent' => self::calculateChangePercent($oldRate, $newRate), - 'change_source' => $source, - 'changed_by' => $userId, - 'api_source' => $apiSource, - 'notes' => $notes, - ]); - } - - // Scope برای آخرین تغییر - public function scopeLatest($query, string $from = 'AED', string $to = 'IRR') - { - return $query->where('currency_from', $from) - ->where('currency_to', $to) - ->orderBy('created_at', 'desc'); - } } \ No newline at end of file diff --git a/04_Laravel/app/Providers/Filament/AdminPanelProvider.php b/04_Laravel/app/Providers/Filament/AdminPanelProvider.php index 0fcf3c1..df71df4 100644 --- a/04_Laravel/app/Providers/Filament/AdminPanelProvider.php +++ b/04_Laravel/app/Providers/Filament/AdminPanelProvider.php @@ -28,7 +28,6 @@ class AdminPanelProvider extends PanelProvider ->path('admin') ->login() ->brandName('IFNEX Logistics') - // 🎨 رنگ‌بندی سفارشی مطابق طراحی ->colors([ 'primary' => Color::Amber, 'gray' => [ @@ -41,7 +40,7 @@ class AdminPanelProvider extends PanelProvider 600 => '#475569', 700 => '#334155', 800 => '#1e293b', - 900 => '#1a1a2e', // 🎯 Dark Navy برای Sidebar + 900 => '#1a1a2e', 950 => '#0f172a', ], 'danger' => Color::Red, @@ -62,8 +61,10 @@ class AdminPanelProvider extends PanelProvider ]) ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([ - Widgets\AccountWidget::class, - Widgets\FilamentInfoWidget::class, + // ویجت‌های سفارشی IFNEX (به ترتیب sort) + \App\Filament\Widgets\DashboardInfoWidget::class, + \App\Filament\Widgets\ExchangeRateWidget::class, + \App\Filament\Widgets\WalletStats::class, ]) ->middleware([ EncryptCookies::class, @@ -79,7 +80,6 @@ class AdminPanelProvider extends PanelProvider ->authMiddleware([ Authenticate::class, ]) - // 🎯 اضافه کردن CSS/JS سفارشی ->renderHook( 'panels::head.start', fn () => view('filament.hooks.head') diff --git a/04_Laravel/app/Services/ExchangeRateService.php b/04_Laravel/app/Services/ExchangeRateService.php index a312551..12759f2 100644 --- a/04_Laravel/app/Services/ExchangeRateService.php +++ b/04_Laravel/app/Services/ExchangeRateService.php @@ -35,7 +35,6 @@ class ExchangeRateService $key = strtolower("{$from}_to_{$to}"); $oldRate = $this->getCurrentRate($from, $to); - // Update system_settings SystemSetting::updateOrCreate( ['key' => $key], [ @@ -45,8 +44,7 @@ class ExchangeRateService ] ); - // ثبت در تاریخچه - return ExchangeRateHistory::recordChange( + return $this->recordChange( from: $from, to: $to, oldRate: $oldRate, @@ -70,10 +68,17 @@ class ExchangeRateService /** * دریافت اطلاعات آخرین نرخ برای Widget + * ⚠️ بدون استفاده از scope - کوئری مستقیم */ public function getLatestRateInfo(string $from = 'AED', string $to = 'IRR'): array { - $latest = ExchangeRateHistory::latest($from, $to)->with('changedBy')->first(); + // ✅ کوئری مستقیم بدون scope + $latest = ExchangeRateHistory::where('currency_from', $from) + ->where('currency_to', $to) + ->orderBy('created_at', 'desc') + ->with('changedBy') + ->first(); + $currentRate = $this->getCurrentRate($from, $to); return [ @@ -85,4 +90,48 @@ class ExchangeRateService 'change_percent' => $latest?->change_percent, ]; } + + /** + * دریافت آخرین N نرخ برای Chart + */ + public function getRateChart(string $from = 'AED', string $to = 'IRR', int $limit = 7): array + { + return ExchangeRateHistory::where('currency_from', $from) + ->where('currency_to', $to) + ->orderBy('created_at', 'asc') + ->limit($limit) + ->pluck('new_rate') + ->toArray(); + } + + /** + * ثبت تغییر نرخ + */ + private function recordChange( + string $from, + string $to, + float $oldRate, + float $newRate, + string $source = 'manual', + ?int $userId = null, + ?string $apiSource = null, + ?string $notes = null + ): ExchangeRateHistory { + $changePercent = null; + if ($oldRate > 0) { + $changePercent = round((($newRate - $oldRate) / $oldRate) * 100, 4); + } + + return ExchangeRateHistory::create([ + 'currency_from' => $from, + 'currency_to' => $to, + 'old_rate' => $oldRate, + 'new_rate' => $newRate, + 'change_percent' => $changePercent, + 'change_source' => $source, + 'changed_by' => $userId, + 'api_source' => $apiSource, + 'notes' => $notes, + ]); + } } \ No newline at end of file diff --git a/04_Laravel/resources/views/filament/widgets/dashboard-info.blade.php b/04_Laravel/resources/views/filament/widgets/dashboard-info.blade.php new file mode 100644 index 0000000..def9589 --- /dev/null +++ b/04_Laravel/resources/views/filament/widgets/dashboard-info.blade.php @@ -0,0 +1,92 @@ + +
+ + {{-- کارت تاریخ و ساعت --}} +
+
+
+ +
+
+
امروز
+
{{ $time }}
+
+
+ +
+
+ 📅 شمسی: + {{ $jalaliDate }} +
+
+ 🌍 میلادی: + {{ $gregorianDate }} +
+
+ +
+
خوش آمدید
+
{{ $userName }}
+
+
+ + {{-- کارت آخرین سفارشات --}} +
+
+
+ +

آخرین سفارشات

+
+ + مشاهده همه ← + +
+ + @if($recentShipments->isEmpty()) +
+ +

هنوز سفارشی ثبت نشده است

+
+ @else +
+ + + + + + + + + + + + @foreach($recentShipments as $shipment) + + + + + + + + @endforeach + +
AWBمسیروضعیتتاریخمبلغ
+ {{ $shipment['awb'] }} + + {{ $shipment['route'] }} + + + {{ $shipment['status_label'] }} + + + {{ $shipment['date'] }} + + {{ $shipment['amount'] }} +
+
+ @endif +
+ +
+
\ No newline at end of file