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