diff --git a/04_Laravel/app/Filament/Pages/Auth/Login.php b/04_Laravel/app/Filament/Pages/Auth/Login.php new file mode 100644 index 0000000..f8c6c1c --- /dev/null +++ b/04_Laravel/app/Filament/Pages/Auth/Login.php @@ -0,0 +1,65 @@ +schema([ + TextInput::make('email') + ->label(__('filament-panels::pages/auth/login.form.email.label')) + ->email() + ->required() + ->autocomplete() + ->prefixIcon('heroicon-o-at-symbol') + ->extraInputAttributes(['class' => 'ifnex-login-input']), + + TextInput::make('password') + ->label(__('filament-panels::pages/auth/login.form.password.label')) + ->password() + ->revealable() + ->required() + ->autocomplete('current-password') + ->prefixIcon('heroicon-o-lock-closed') + ->extraInputAttributes(['class' => 'ifnex-login-input']), + + Checkbox::make('remember') + ->label(__('filament-panels::pages/auth/login.form.remember.label')) + ->extraAttributes(['class' => 'ifnex-login-remember']), + ]) + ->statePath('data'); + } + + /** + * بازنویسی متن دکمهٔ submit به «ورود به سیستم» و افزودن کلاس + * سفارشی برای استایل گرادیان آمبر. + */ + protected function getAuthenticateFormAction(): Action + { + return parent::getAuthenticateFormAction() + ->label('ورود به سیستم') + ->extraAttributes(['class' => 'ifnex-login-submit w-full']); + } +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Pages/Dashboard.php b/04_Laravel/app/Filament/Pages/Dashboard.php index 1e2470c..a6947f3 100644 --- a/04_Laravel/app/Filament/Pages/Dashboard.php +++ b/04_Laravel/app/Filament/Pages/Dashboard.php @@ -2,31 +2,82 @@ namespace App\Filament\Pages; +use App\Filament\Widgets\DashboardInfoWidget; +use App\Filament\Widgets\ExchangeRateWidget; use App\Filament\Widgets\FinanceOverviewWidget; +use App\Filament\Widgets\RecentShipmentsWidget; use App\Filament\Widgets\RecentTransactionsWidget; use App\Filament\Widgets\TransactionChartWidget; use Filament\Pages\Dashboard as BaseDashboard; +/** + * IFNEX Admin Dashboard — layered layout (12-column grid) + * + * Layer 1 (header): FinanceOverviewWidget (10/12) + DashboardInfoWidget (2/12) + * → 4 KPI cards + small clock/welcome card in the corner + * Layer 2: ExchangeRateWidget (12/12) → 4 rate cards in a comfortable row + * Layer 3: RecentShipmentsWidget (6/12) + RecentTransactionsWidget (6/12) + * → two tables with identical style/size + * Layer 4 (bottom): TransactionChartWidget (12/12) → 7-day trend, full width + * + * Smart Grid: 12-column responsive grid. Each widget uses an array-based + * columnSpan so mobile stacks everything full-width; desktop shows the + * layered progressive-disclosure design. + * + * ⚠️ نام متد ستون‌ها در فیلامنت getColumns() است نه getWidgetsColumns() — + * با نام اشتباه، گرید روی پیش‌فرض ۲ ستون می‌ماند و spanهای ۴/۶/۸ گرید را می‌شکنند. + * این صفحه باید تنها Dashboard ثبت‌شده پنل باشد؛ ثبت صریح Filament\Pages\Dashboard + * در AdminPanelProvider باعث می‌شود داشبورد پایه (با کل استخر ویجت‌ها) بر این override غلبه کند. + */ class Dashboard extends BaseDashboard { protected static ?string $navigationLabel = 'داشبورد'; protected static ?string $slug = 'dashboard'; protected static ?string $title = 'داشبورد مالی'; - protected static ?string $navigationIcon = 'heroicon-o-home'; + /** + * Layer 1 — Top row: 4 KPI cards + small clock/welcome corner card. + */ public function getHeaderWidgets(): array { return [ FinanceOverviewWidget::class, + DashboardInfoWidget::class, ]; } + /** + * Layers 2–4 — rate row, two tables, then the full-width trend chart at the bottom. + * ترتیب نمایش با sort هر ویجت تعیین می‌شود: نرخ(3) ← جدول‌ها(6,7) ← چارت(8). + */ public function getWidgets(): array { return [ - TransactionChartWidget::class, + // Layer 2: exchange rates (12/12) + ExchangeRateWidget::class, + + // Layer 3: recent shipments (6/12) + recent transactions (6/12) + RecentShipmentsWidget::class, RecentTransactionsWidget::class, + + // Layer 4: 7-day revenue trend (12/12) — full width, bottom of the page + TransactionChartWidget::class, ]; } + + /** + * Main widgets grid: 12 columns on desktop, 1 on mobile so each + * widget stacks full-width. Each widget sets its own responsive + * columnSpan; the grid just exposes the columns. + */ + public function getColumns(): int | string | array + { + return 12; + } + + public function getHeaderWidgetsColumns(): int | string | array + { + return 12; + } } diff --git a/04_Laravel/app/Filament/Pages/DashboardWidgetRedesign.php b/04_Laravel/app/Filament/Pages/DashboardWidgetRedesign.php deleted file mode 100644 index e9e0927..0000000 --- a/04_Laravel/app/Filament/Pages/DashboardWidgetRedesign.php +++ /dev/null @@ -1,28 +0,0 @@ -user()?->hasAnyRole(['super_admin', 'admin']) ?? false; - } -} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Widgets/DashboardInfoWidget.php b/04_Laravel/app/Filament/Widgets/DashboardInfoWidget.php index 471e3ee..c1c20a0 100644 --- a/04_Laravel/app/Filament/Widgets/DashboardInfoWidget.php +++ b/04_Laravel/app/Filament/Widgets/DashboardInfoWidget.php @@ -15,7 +15,7 @@ class DashboardInfoWidget extends Widget protected static bool $isLazy = false; - protected int | string | array $columnSpan = 1; + protected int | string | array $columnSpan = 2; protected static string $view = 'filament.widgets.dashboard-info'; diff --git a/04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php b/04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php index 4455cb2..7c7a3cb 100644 --- a/04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php +++ b/04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php @@ -2,21 +2,32 @@ namespace App\Filament\Widgets; +use App\Models\ExchangeRateHistory; use App\Services\ExchangeRateService; use Filament\Widgets\StatsOverviewWidget as BaseWidget; use Filament\Widgets\StatsOverviewWidget\Stat; -use App\Models\ExchangeRateHistory; +/** + * Layer 2 (right) — live exchange-rate summary, spanning 4/12 of the body grid. + */ class ExchangeRateWidget extends BaseWidget { protected static ?int $sort = 3; + protected static bool $isLazy = false; - protected int | string | array $columnSpan = 'full'; + protected int|string|array $columnSpan = 12; + + /** + * Stat cards inside the rate widget: 1 column on the narrow lg+ column, + * 2 on mobile so they don't all stack vertically on tiny screens. + */ + protected int|string|array $columns = 1; protected function getStats(): array { $service = app(ExchangeRateService::class); + $aedInfo = $service->getLatestRateInfo('AED', 'IRR'); $usdInfo = $service->getLatestRateInfo('USD', 'IRR'); @@ -27,28 +38,40 @@ class ExchangeRateWidget extends BaseWidget $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') ?? '—')) + Stat::make('نرخ درهم (AED)', ifnex_format_number($aedInfo['current_rate'] ?? 0)) + ->description( + $aedChangePercent !== null + ? sprintf('%s %s — %s', + $aedChangePercent > 0 ? '↑' : ($aedChangePercent < 0 ? '↓' : '—'), + ifnex_persian_digits(number_format(abs((float) $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') ) + ->icon('heroicon-o-currency-dollar') ->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') ?? '—')) + Stat::make('نرخ دلار (USD)', ifnex_format_number($usdInfo['current_rate'] ?? 0)) + ->description( + $usdChangePercent !== null + ? sprintf('%s %s — %s', + $usdChangePercent > 0 ? '↑' : ($usdChangePercent < 0 ? '↓' : '—'), + ifnex_persian_digits(number_format(abs((float) $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') ) + ->icon('heroicon-o-banknotes') ->color( $usdChangePercent > 0 ? 'danger' : ($usdChangePercent < 0 ? 'success' : 'gray') @@ -56,15 +79,21 @@ class ExchangeRateWidget extends BaseWidget ->chart(!empty($usdChart) ? $usdChart : [0]), Stat::make('تغییرات امروز', - ExchangeRateHistory::whereDate('created_at', today())->count() + ifnex_persian_digits(number_format( + ExchangeRateHistory::whereDate('created_at', today())->count() + )) ) ->description('تغییرات نرخ ثبت‌شده در امروز') ->descriptionIcon('heroicon-m-arrow-path') + ->icon('heroicon-o-arrow-path') ->color('info'), - Stat::make('در انتظار تأیید', $service->getPendingCount()) + Stat::make('در انتظار تأیید', + ifnex_persian_digits(number_format($service->getPendingCount())) + ) ->description('نرخ‌های پیشنهادی API — منتظر تأیید مدیر') ->descriptionIcon('heroicon-m-clock') + ->icon('heroicon-o-clock') ->color('warning'), ]; } diff --git a/04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php b/04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php index eb2f79a..8056e91 100644 --- a/04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php +++ b/04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php @@ -2,54 +2,178 @@ 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\Facades\DB; +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 = now()->startOfDay(); + $today = today(); + $yesterday = today()->subDay(); - $totalRevenue = WalletTransaction::where('type', 'order_payment') + // ─── 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'); - $todayRevenue = WalletTransaction::where('type', 'order_payment') + $yesterdayRevenue = (float) WalletTransaction::where('type', 'order_payment') ->where('status', 'completed') ->where('amount', '>', 0) - ->where('created_at', '>=', $today) + ->whereDate('created_at', $yesterday) ->sum('amount'); - $pendingPayments = Shipment::where('status', 'approved')->count(); - $pendingAmount = Shipment::where('status', 'approved')->sum('total_fee'); + [$revPercent, $revDirection, $revFormatted] = ifnex_trend_percent($todayRevenue, $yesterdayRevenue); - $totalWalletBalance = Wallet::sum('balance'); + $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('درآمد کل (تکمیل شده)', number_format($totalRevenue) . ' ریال') - ->icon('heroicon-o-currency-dollar') - ->color('success'), - - Stat::make('درآمد امروز', number_format($todayRevenue) . ' ریال') - ->icon('heroicon-o-chart-bar') - ->color('info'), - - Stat::make('پرداخت‌های در انتظار', $pendingPayments . ' سفارش') - ->description(number_format($pendingAmount) . ' ریال') + 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'), + ->color('warning') + ->chart($pendingSparkline) + ->url( + $pendingOrdersTotal > 0 + ? route('filament.admin.resources.shipments.index') + : null + ), - Stat::make('مجموع موجودی کیف‌پول‌ها', number_format($totalWalletBalance) . ' ریال') + 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('gray'), + ->color('info') + ->chart($walletSparkline) + ->url(route('filament.admin.resources.wallets.index')), ]; } -} +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Widgets/RecentShipmentsWidget.php b/04_Laravel/app/Filament/Widgets/RecentShipmentsWidget.php index 608a494..30148ad 100644 --- a/04_Laravel/app/Filament/Widgets/RecentShipmentsWidget.php +++ b/04_Laravel/app/Filament/Widgets/RecentShipmentsWidget.php @@ -7,15 +7,15 @@ use Filament\Widgets\Widget; use Morilog\Jalali\Jalalian; /** - * جدول آخرین ۵ سفارش — تمام‌عرض، زیر کارت تاریخ/ساعت. + * Layer 3 (left) — recent shipments table, spanning 6/12 of the body grid. */ class RecentShipmentsWidget extends Widget { - protected static ?int $sort = 2; + protected static ?int $sort = 6; protected static bool $isLazy = false; - protected int | string | array $columnSpan = 'full'; + protected int|string|array $columnSpan = 6; protected static string $view = 'filament.widgets.recent-shipments'; @@ -29,45 +29,58 @@ class RecentShipmentsWidget extends Widget ->map(function ($s) { return [ 'awb' => $s->awb_no, - 'route' => ($s->fromCountry?->name ?? '?') . ' → ' . ($s->toCountry?->name ?? '?'), + '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), + 'status_color' => $this->statusBadgeClass($s->status?->value), 'date' => Jalalian::fromCarbon($s->created_at)->format('Y/m/d'), - 'amount' => $s->total_fee ? number_format($s->total_fee) . ' ریال' : '—', + 'amount' => $s->total_fee ? ifnex_format_number($s->total_fee) : '—', + 'url' => route('filament.admin.resources.shipments.view', $s), ]; }); return [ 'recentShipments' => $recentShipments, + 'viewAllUrl' => route('filament.admin.resources.shipments.index'), ]; } private function statusLabel(?string $status): string { - return match($status) { - 'processed' => 'پردازش شده', - 'picked_up' => 'تحویل گرفته شده', + return match ($status) { + 'pending_approval' => 'در انتظار تأیید', + 'approved' => 'تأیید شده', + 'pending_payment' => 'در انتظار پرداخت', + 'cancelled' => 'لغو شده', + 'processed' => 'در حال پردازش', + 'picked_up' => 'تحویل به حمل‌کننده', 'in_transit' => 'در حال حمل', - 'out_for_delivery' => 'در مسیر تحویل', - 'delivered' => 'تحویل داده شده', - 'failed' => 'ناموفق', + 'out_for_delivery' => 'آماده تحویل', + 'failed' => 'تحویل ناموفق', + 'delivered' => 'تحویل شده', 'returned' => 'برگشتی', + 'archived' => 'آرشیو', default => '—', }; } - private function statusColor(?string $status): string + /** + * Return the badge CSS class name — uses the new ifnex-badge-* styles + * defined in head.blade.php (success / warning / danger / info / gray). + */ + private function statusBadgeClass(?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', + return match ($status) { + 'pending_approval', 'pending_payment' => 'ifnex-badge-warning', + 'approved' => 'ifnex-badge-info', + 'picked_up', 'in_transit' => 'ifnex-badge-info', + 'out_for_delivery' => 'ifnex-badge-info', + 'delivered' => 'ifnex-badge-success', + 'cancelled', 'failed' => 'ifnex-badge-danger', + 'returned' => 'ifnex-badge-warning', + 'processed' => 'ifnex-badge-info', + 'archived' => 'ifnex-badge-gray', + default => 'ifnex-badge-gray', }; } -} +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Widgets/RecentTransactionsWidget.php b/04_Laravel/app/Filament/Widgets/RecentTransactionsWidget.php index 965f270..5af4e69 100644 --- a/04_Laravel/app/Filament/Widgets/RecentTransactionsWidget.php +++ b/04_Laravel/app/Filament/Widgets/RecentTransactionsWidget.php @@ -2,68 +2,71 @@ namespace App\Filament\Widgets; +use App\Enums\TransactionStatus; +use App\Enums\TransactionType; use App\Models\WalletTransaction; -use Filament\Tables; -use Filament\Tables\Table; -use Filament\Widgets\TableWidget as BaseWidget; -use Illuminate\Database\Eloquent\Builder; +use Filament\Widgets\Widget; +use Morilog\Jalali\Jalalian; -class RecentTransactionsWidget extends BaseWidget +/** + * Layer 3 (right) — recent transactions table, spanning 6/12 of the body grid. + * استایل و ساختار همسان با RecentShipmentsWidget (کارت + هدر آیکون‌دار + بج‌های برند). + */ +class RecentTransactionsWidget extends Widget { - protected static ?string $pollingInterval = '30s'; - protected static ?string $heading = 'آخرین تراکنش‌ها'; - protected static ?int $sort = 5; + protected static ?int $sort = 7; - public function table(Table $table): Table + protected static bool $isLazy = false; + + protected int|string|array $columnSpan = 6; + + protected static string $view = 'filament.widgets.recent-transactions'; + + protected function getViewData(): array { - return $table - ->query( - WalletTransaction::query() - ->latest() - ->limit(10) - ) - ->columns([ - Tables\Columns\TextColumn::make('id') - ->label('ID') - ->sortable() - ->width(60), - Tables\Columns\TextColumn::make('wallet.user.name') - ->label('کاربر') - ->searchable() - ->limit(20), - Tables\Columns\TextColumn::make('amount') - ->label('مبلغ') - ->money('IRR', divideBy: 1) - ->color(fn ($record) => $record->amount >= 0 ? 'success' : 'danger') - ->sortable(), - Tables\Columns\BadgeColumn::make('type') - ->label('نوع') - ->getStateUsing(fn ($record) => $record->type->label()) - ->colors([ - 'success' => \App\Enums\TransactionType::DEPOSIT, - 'danger' => \App\Enums\TransactionType::WITHDRAWAL, - 'warning' => \App\Enums\TransactionType::ORDER_PAYMENT, - 'info' => \App\Enums\TransactionType::REFUND, - ]), - Tables\Columns\BadgeColumn::make('status') - ->label('وضعیت') - ->getStateUsing(fn ($record) => $record->status->label()) - ->colors([ - 'success' => \App\Enums\TransactionStatus::COMPLETED, - 'warning' => \App\Enums\TransactionStatus::PENDING, - 'danger' => \App\Enums\TransactionStatus::FAILED, - 'info' => \App\Enums\TransactionStatus::REFUNDED, - ]), - Tables\Columns\TextColumn::make('gateway_reference_id') - ->label('کد پیگیری') - ->searchable() - ->toggleable(), - Tables\Columns\TextColumn::make('created_at') - ->label('تاریخ') - ->dateTime('Y/m/d H:i', 'Asia/Tehran') - ->sortable() - ->since(), - ]) - ->defaultSort('created_at', 'desc'); + $transactions = WalletTransaction::query() + ->with('wallet.user') + ->latest() + ->limit(5) + ->get() + ->map(function ($t) { + return [ + 'user' => $t->wallet?->user?->name ?? '—', + 'amount' => ifnex_format_number(abs((float) $t->amount)), + 'amount_negative' => $t->amount < 0, + 'type_label' => $t->type->label(), + 'type_color' => $this->typeBadgeClass($t->type), + 'status_label' => $t->status->label(), + 'status_color' => $this->statusBadgeClass($t->status), + 'date' => Jalalian::fromCarbon($t->created_at)->format('Y/m/d H:i'), + ]; + }); + + return [ + 'transactions' => $transactions, + 'viewAllUrl' => route('filament.admin.resources.wallet-transactions.index'), + ]; + } + + private function typeBadgeClass(TransactionType $type): string + { + return match ($type) { + TransactionType::DEPOSIT => 'ifnex-badge-success', + TransactionType::WITHDRAWAL => 'ifnex-badge-danger', + TransactionType::ORDER_PAYMENT => 'ifnex-badge-warning', + TransactionType::REFUND => 'ifnex-badge-info', + default => 'ifnex-badge-gray', + }; + } + + private function statusBadgeClass(TransactionStatus $status): string + { + return match ($status) { + TransactionStatus::COMPLETED => 'ifnex-badge-success', + TransactionStatus::PENDING => 'ifnex-badge-warning', + TransactionStatus::FAILED => 'ifnex-badge-danger', + TransactionStatus::REFUNDED => 'ifnex-badge-info', + default => 'ifnex-badge-gray', + }; } } diff --git a/04_Laravel/app/Filament/Widgets/TransactionChartWidget.php b/04_Laravel/app/Filament/Widgets/TransactionChartWidget.php index 4334393..3fb9c91 100644 --- a/04_Laravel/app/Filament/Widgets/TransactionChartWidget.php +++ b/04_Laravel/app/Filament/Widgets/TransactionChartWidget.php @@ -5,14 +5,22 @@ namespace App\Filament\Widgets; use App\Models\WalletTransaction; use Filament\Widgets\ChartWidget; use Illuminate\Support\Facades\DB; +use Morilog\Jalali\Jalalian; +/** + * Layer 2 (left) — 7-day revenue trend chart, spanning 8/12 of the body grid. + */ class TransactionChartWidget extends ChartWidget { - -protected static ?int $sort = 4; -protected static ?string $pollingInterval = '60s'; - protected static ?string $heading = 'روند تراکنش‌های ۳۰ روز اخیر'; - protected static ?string $maxHeight = '300px'; + protected static ?int $sort = 8; + + protected static ?string $pollingInterval = '60s'; + + protected static ?string $heading = 'روند تراکنش‌های ۷ روز اخیر'; + + protected static ?string $maxHeight = '320px'; + + protected int|string|array $columnSpan = 12; protected function getData(): array { @@ -21,14 +29,16 @@ protected static ?string $pollingInterval = '60s'; DB::raw('SUM(CASE WHEN type = "deposit" AND status = "completed" THEN amount ELSE 0 END) as deposits'), DB::raw('SUM(CASE WHEN type IN ("withdrawal", "order_payment") AND status = "completed" THEN amount ELSE 0 END) as withdrawals') ) - ->where('created_at', '>=', now()->subDays(30)) + ->where('created_at', '>=', now()->subDays(7)) ->groupBy('date') ->orderBy('date') ->get(); - $labels = $data->pluck('date')->map(fn ($date) => \Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($date))->format('Y/m/d'))->toArray(); - $deposits = $data->pluck('deposits')->toArray(); - $withdrawals = $data->pluck('withdrawals')->toArray(); + $labels = $data->pluck('date')->map(function ($date) { + return Jalalian::fromCarbon(\Carbon\Carbon::parse($date))->format('l d'); + })->toArray(); + $deposits = $data->pluck('deposits')->map(fn ($v) => (float) $v)->toArray(); + $withdrawals = $data->pluck('withdrawals')->map(fn ($v) => (float) $v)->toArray(); return [ 'datasets' => [ @@ -36,17 +46,23 @@ protected static ?string $pollingInterval = '60s'; 'label' => 'واریزی‌ها', 'data' => $deposits, 'borderColor' => '#10b981', - 'backgroundColor' => 'rgba(16, 185, 129, 0.1)', + 'backgroundColor' => 'rgba(16, 185, 129, 0.15)', 'fill' => true, 'tension' => 0.4, + 'pointRadius' => 3, + 'pointHoverRadius' => 5, + 'pointBackgroundColor' => '#10b981', ], [ 'label' => 'برداشت‌ها', 'data' => $withdrawals, 'borderColor' => '#f59e0b', - 'backgroundColor' => 'rgba(245, 158, 11, 0.1)', + 'backgroundColor' => 'rgba(245, 158, 11, 0.15)', 'fill' => true, 'tension' => 0.4, + 'pointRadius' => 3, + 'pointHoverRadius' => 5, + 'pointBackgroundColor' => '#f59e0b', ], ], 'labels' => $labels, @@ -64,10 +80,23 @@ protected static ?string $pollingInterval = '60s'; 'plugins' => [ 'legend' => [ 'position' => 'top', + 'align' => 'end', 'labels' => [ 'font' => [ - 'family' => 'inherit', + 'family' => 'Vazirmatn', + 'size' => 12, ], + 'usePointStyle' => true, + 'boxWidth' => 8, + ], + ], + 'tooltip' => [ + 'callbacks' => [ + 'label' => 'function(context) { + var v = context.parsed.y; + if (v === null || v === undefined) return ""; + return context.dataset.label + ": " + v.toLocaleString("fa-IR") + " ریال"; + }', ], ], ], @@ -76,14 +105,23 @@ protected static ?string $pollingInterval = '60s'; 'beginAtZero' => true, 'ticks' => [ 'callback' => 'function(value) { return value.toLocaleString("fa-IR"); }', + 'font' => ['family' => 'Vazirmatn', 'size' => 11], + ], + 'grid' => [ + 'color' => 'rgba(0, 0, 0, 0.05)', ], ], 'x' => [ 'ticks' => [ 'maxRotation' => 0, + 'font' => ['family' => 'Vazirmatn', 'size' => 11], + ], + 'grid' => [ + 'display' => false, ], ], ], + 'maintainAspectRatio' => false, ]; } -} +} \ No newline at end of file diff --git a/04_Laravel/app/Helpers/number.php b/04_Laravel/app/Helpers/number.php new file mode 100644 index 0000000..c6cd763 --- /dev/null +++ b/04_Laravel/app/Helpers/number.php @@ -0,0 +1,146 @@ += 1_000_000_000) { + $formatted = number_format($number / 1_000_000_000, 2); + } elseif ($abs >= 1_000_000) { + $formatted = number_format($number / 1_000_000, 1); + } elseif ($abs >= 1_000) { + $formatted = number_format($number / 1_000, 0); + } else { + $formatted = number_format($number); + } + + // Convert Latin digits to Persian (fa-IR) + $formatted = ifnex_persian_digits($formatted); + + // Append compact unit if applicable + if ($abs >= 1_000_000_000) { + $formatted .= ' میلیارد'; + } elseif ($abs >= 1_000_000) { + $formatted .= ' میلیون'; + } elseif ($abs >= 1_000) { + $formatted .= ' هزار'; + } + + if ($currency !== '' && $currency !== null) { + $formatted .= ' ' . $currency; + } + + return trim($formatted); + } +} + +if (!function_exists('ifnex_persian_digits')) { + /** + * Replace Latin (0-9) digits in a string with Persian digits (۰-۹). + * Keeps other characters intact. + */ + function ifnex_persian_digits(string $text): string + { + return strtr($text, [ + '0' => '۰', + '1' => '۱', + '2' => '۲', + '3' => '۳', + '4' => '۴', + '5' => '۵', + '6' => '۶', + '7' => '۷', + '8' => '۸', + '9' => '۹', + ]); + } +} + +if (!function_exists('ifnex_english_digits')) { + /** + * Replace Persian digits back to Latin — handy for form input parsing. + */ + function ifnex_english_digits(string $text): string + { + return strtr($text, [ + '۰' => '0', + '۱' => '1', + '۲' => '2', + '۳' => '3', + '۴' => '4', + '۵' => '5', + '۶' => '6', + '۷' => '7', + '۸' => '8', + '۹' => '9', + '٠' => '0', + '١' => '1', + '٢' => '2', + '٣' => '3', + '٤' => '4', + '٥' => '5', + '٦' => '6', + '٧' => '7', + '٨' => '8', + '٩' => '9', + ]); + } +} + +if (!function_exists('ifnex_trend_percent')) { + /** + * Compute the percent change between two values and return a tuple of + * [percent_value, direction ('up'|'down'|'flat'), formatted Persian string]. + * + * @param float|int|null $current + * @param float|int|null $previous + * @return array{0: float, 1: string, 2: string} + */ + function ifnex_trend_percent(float|int|null $current, float|int|null $previous): array + { + $current = (float) ($current ?? 0); + $previous = (float) ($previous ?? 0); + + if ($previous == 0) { + return [0.0, $current > 0 ? 'up' : 'flat', '—']; + } + + $diff = $current - $previous; + $percent = ($diff / abs($previous)) * 100; + + if ($percent > 0.05) { + $direction = 'up'; + } elseif ($percent < -0.05) { + $direction = 'down'; + } else { + $direction = 'flat'; + } + + $formatted = ifnex_persian_digits(number_format(abs($percent), 1)) . '%'; + + return [$percent, $direction, $formatted]; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Providers/AppServiceProvider.php b/04_Laravel/app/Providers/AppServiceProvider.php index 04bb716..8997fab 100644 --- a/04_Laravel/app/Providers/AppServiceProvider.php +++ b/04_Laravel/app/Providers/AppServiceProvider.php @@ -26,5 +26,13 @@ class AppServiceProvider extends ServiceProvider // ثبت Observer برای اطلاعیه‌های تغییر سفارش Shipment::observe(ShipmentObserver::class); + + // Explicitly require the IFNEX number helper file so + // ifnex_format_number() / ifnex_persian_digits() / ifnex_trend_percent() + // are available even before `composer dump-autoload` is re-run. + $helperPath = base_path('app/Helpers/number.php'); + if (is_file($helperPath)) { + require_once $helperPath; + } } } diff --git a/04_Laravel/app/Providers/Filament/AdminPanelProvider.php b/04_Laravel/app/Providers/Filament/AdminPanelProvider.php index 4f76a0a..ebdaa5f 100644 --- a/04_Laravel/app/Providers/Filament/AdminPanelProvider.php +++ b/04_Laravel/app/Providers/Filament/AdminPanelProvider.php @@ -26,8 +26,11 @@ class AdminPanelProvider extends PanelProvider ->default() ->id('admin') ->path('panel') - ->login() + ->login(\App\Filament\Pages\Auth\Login::class) ->brandName('IFNEX Logistics') + // لوگوی برند در نوار بالا — همان لوگوی صفحه لاگین + ->brandLogo(asset('images/ifnex-logo.png')) + ->brandLogoHeight('2.5rem') ->colors([ 'primary' => Color::Amber, 'gray' => [ @@ -66,16 +69,13 @@ class AdminPanelProvider extends PanelProvider ]) // ─── بارگذاری استایل سفارشی IFNEX به‌عنوان تم فیلمنت ─── // ->viteTheme('resources/css/filament.css') // موقتاً غیرفعال تا npm run build - ->pages([ - Pages\Dashboard::class, - ]) + // ⚠️ Pages\Dashboard::class اینجا ثبت نشود — App\Filament\Pages\Dashboard + // از طریق discoverPages کشف می‌شود؛ ثبت صریح داشبورد پایه باعث می‌شود + // نسخه پایه (با کل استخر ویجت‌های کشف‌شده) بر override سه‌لایه غلبه کند ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->widgets([ - // ویجت‌های سفارشی IFNEX (به ترتیب sort) - \App\Filament\Widgets\DashboardInfoWidget::class, \App\Filament\Widgets\RecentShipmentsWidget::class, \App\Filament\Widgets\ExchangeRateWidget::class, - \App\Filament\Widgets\WalletStats::class, ]) ->middleware([ EncryptCookies::class, @@ -94,6 +94,12 @@ class AdminPanelProvider extends PanelProvider ->renderHook( 'panels::head.start', fn () => view('filament.hooks.head') + ) + ->renderHook( + \Filament\View\PanelsRenderHook::FOOTER, + fn () => '
' + . 'قدرت گرفته از ورنا سافت
' ); } } \ No newline at end of file diff --git a/04_Laravel/composer.json b/04_Laravel/composer.json index e8e1c23..e3dfea3 100644 --- a/04_Laravel/composer.json +++ b/04_Laravel/composer.json @@ -32,7 +32,10 @@ "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" - } + }, + "files": [ + "app/Helpers/number.php" + ] }, "autoload-dev": { "psr-4": { @@ -95,4 +98,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} +} \ No newline at end of file diff --git a/04_Laravel/public/images/ifnex-logo.png b/04_Laravel/public/images/ifnex-logo.png new file mode 100644 index 0000000..0227f3d Binary files /dev/null and b/04_Laravel/public/images/ifnex-logo.png differ diff --git a/04_Laravel/resources/views/filament/hooks/head.blade.php b/04_Laravel/resources/views/filament/hooks/head.blade.php index ca5c7ca..85afe92 100644 --- a/04_Laravel/resources/views/filament/hooks/head.blade.php +++ b/04_Laravel/resources/views/filament/hooks/head.blade.php @@ -190,7 +190,7 @@ /* ─── عناوین گروه‌های سایدبار: سفید روی پس‌زمینه تیره ─── */ .fi-sidebar-group-label { - color: #e2e8f0 !important; + color: #f8fafc !important; font-weight: 600 !important; } @@ -208,6 +208,39 @@ color: #f59e0b !important; } + /* ─── زیرمنوها: متن سفید پیش‌فرض، سفیدِ کامل در هاور، کهربایی برای آیتم فعال ─── */ + .fi-sidebar-item a .fi-sidebar-item-label { + color: #e2e8f0 !important; + } + + .fi-sidebar-item a .fi-icon { + color: #cbd5e1 !important; + } + + /* هاور زیرمنو: پس‌زمینه روشن می‌شود → متن تیره برای خوانایی */ + .fi-sidebar-item a:hover .fi-sidebar-item-label { + color: #1e293b !important; + } + + .fi-sidebar-item a:hover .fi-icon { + color: #1e293b !important; + } + + /* در دارک‌مود پس‌زمینه هاور تیره می‌ماند → متن سفید */ + .dark .fi-sidebar-item a:hover .fi-sidebar-item-label, + .dark .fi-sidebar-item a:hover .fi-icon { + color: #ffffff !important; + } + + /* آیتم فعال همان کهربایی برند می‌ماند (بعد از قواعد سفید تا اولویت بگیرد) */ + .fi-sidebar-item-active > a .fi-sidebar-item-label { + color: #f59e0b !important; + } + + .fi-sidebar-item-active > a .fi-icon { + color: #f59e0b !important; + } + /* ─── دکمه بستن/باز کردن همه منوها ─── */ .ifnex-sidebar-collapse-all { display: block; @@ -216,7 +249,7 @@ padding: 7px 10px; font-size: 11.5px; font-family: inherit; - color: #cbd5e1; + color: #f1f5f9; background: rgba(255, 255, 255, .06); border: 1px solid rgba(255, 255, 255, .14); border-radius: 8px; @@ -230,6 +263,163 @@ border-color: rgba(245, 158, 11, .35); color: #f59e0b; } + + /* ─── IFNEX Dashboard redesign — card / badge / trend design tokens ─── */ + + /* Base card: rounded-2xl, soft multi-layer shadow, 1px gray-200 border */ + .ifnex-card-shadow, + .ifnex-card { + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.08), + 0 1px 2px rgba(0, 0, 0, 0.04) !important; + } + + /* Stat card base — used by StatsOverviewWidget cards */ + .fi-wi-stats-overview-stat.ifnex-stat-card { + border-radius: 16px !important; + border: 1px solid #e5e7eb !important; + padding: 1.25rem !important; + background: #FAF9F6 !important; + } + + .dark .fi-wi-stats-overview-stat.ifnex-stat-card { + background: rgb(30 41 59) !important; + border-color: rgb(51 65 85) !important; + } + + .ifnex-stat-card { + border-radius: 16px !important; + border: 1px solid #e5e7eb !important; + padding: 1.25rem !important; + background: #FAF9F6 !important; + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.08), + 0 1px 2px rgba(0, 0, 0, 0.04) !important; + transition: transform 0.25s ease, box-shadow 0.25s ease !important; + } + + .ifnex-stat-card:hover { + transform: translateY(-2px); + box-shadow: + 0 10px 25px rgba(0, 0, 0, 0.10), + 0 4px 6px rgba(0, 0, 0, 0.05) !important; + } + + /* Stat icon — colored soft circle (40×40), centered heroicon */ + .ifnex-stat-icon { + width: 40px; + height: 40px; + border-radius: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + .ifnex-stat-icon-amber { background: #fef3c7; color: #d97706; } + .ifnex-stat-icon-green { background: #d1fae5; color: #059669; } + .ifnex-stat-icon-red { background: #fee2e2; color: #dc2626; } + .ifnex-stat-icon-blue { background: #dbeafe; color: #2563eb; } + .ifnex-stat-icon-gray { background: #f1f5f9; color: #475569; } + + .dark .ifnex-stat-icon-amber { background: rgba(245,158,11,.18); color: #fbbf24; } + .dark .ifnex-stat-icon-green { background: rgba(16,185,129,.18); color: #34d399; } + .dark .ifnex-stat-icon-red { background: rgba(239,68,68,.18); color: #f87171; } + .dark .ifnex-stat-icon-blue { background: rgba(59,130,246,.18); color: #60a5fa; } + .dark .ifnex-stat-icon-gray { background: rgba(148,163,184,.18); color: #cbd5e1; } + + /* Big number typography */ + .ifnex-stat-number { + font-family: 'Vazirmatn', sans-serif !important; + font-size: 28px; + line-height: 1.2; + font-weight: 800; + color: #111827; + letter-spacing: -0.5px; + } + .dark .ifnex-stat-number { color: #f9fafb; } + + /* Stat label */ + .ifnex-stat-label { + font-family: 'Vazirmatn', sans-serif !important; + font-size: 12px; + font-weight: 500; + color: #6b7280; + } + .dark .ifnex-stat-label { color: #9ca3af; } + + /* Trend indicators */ + .ifnex-stat-trend-up { + color: #10b981; + font-weight: 600; + font-size: 12px; + } + .ifnex-stat-trend-down { + color: #ef4444; + font-weight: 600; + font-size: 12px; + } + .ifnex-stat-trend-flat { + color: #9ca3af; + font-weight: 600; + font-size: 12px; + } + + /* ─── Brand status badges ─── */ + .ifnex-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 0.125rem 0.625rem; + border-radius: 9999px; + font-size: 11.5px; + font-weight: 600; + line-height: 1.4; + white-space: nowrap; + } + .ifnex-badge-success { background: #d1fae5; color: #047857; } + .ifnex-badge-warning { background: #fef3c7; color: #b45309; } + .ifnex-badge-danger { background: #fee2e2; color: #b91c1c; } + .ifnex-badge-info { background: #dbeafe; color: #1d4ed8; } + .ifnex-badge-gray { background: #f1f5f9; color: #475569; } + + .dark .ifnex-badge-success { background: rgba(16,185,129,.18); color: #6ee7b7; } + .dark .ifnex-badge-warning { background: rgba(245,158,11,.18); color: #fcd34d; } + .dark .ifnex-badge-danger { background: rgba(239,68,68,.18); color: #fca5a5; } + .dark .ifnex-badge-info { background: rgba(59,130,246,.18); color: #93c5fd; } + .dark .ifnex-badge-gray { background: rgba(148,163,184,.18); color: #cbd5e1; } + + /* ─── Table row hover (Layer 3 tables) ─── */ + .ifnex-table-row-hover:hover { + background-color: #fffbeb !important; + } + .dark .ifnex-table-row-hover:hover { + background-color: rgba(245, 158, 11, 0.08) !important; + } + + /* Compact table cells + headers used by ifnex widgets */ + .ifnex-th { + padding: 0.625rem 0.75rem; + font-size: 11.5px; + font-weight: 600; + text-transform: none; + } + .ifnex-td { + padding: 0.625rem 0.75rem; + vertical-align: middle; + } + + /* Inline link (e.g. "مشاهده همه ←") */ + .ifnex-link-inline { + display: inline-flex; + align-items: center; + gap: 2px; + } + + /* ─── Stat card icon color tinting for IFNEX KPIs ─── */ + .fi-wi-stats-overview-stat .fi-icon, + .fi-stat-icon { + border-radius: 12px !important; + } + + + + + + + + + داشبورد - IFNEX Logistics + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + + + + +
+ + +
+ + +
+ +
+
+ +

+ داشبورد +

+ +
+ +
+ + + + +
+
+ +
+ +
+ + + + + + +
+ +
+
+
+
+
+
+
امروز
+
10:14
+
+
+ +
+
+ 📅 شمسی: + شنبه، 11 مهر 1405 +
+
+ 🌍 میلادی: + Saturday, 3 October 2026 +
+
+ +
+
خوش آمدید
+
مدیر کاظم
+
+
+
+ + + +
+ +
+
+
+
+ + + نرخ درهم (AED) + +
+ +
+ ۴۱۵ هزار ریال +
+ +
+ + + ↓ ۳.۷۲% — سیستم + + +
+
+ + +
+
+ + + + + +
+
+
+ +
+
+
+ + + نرخ دلار (USD) + +
+ +
+ ۳۸۷ هزار ریال +
+ +
+ + + ↑ ۱۳.۸۸% — سیستم + + +
+
+ + +
+
+ + + + + +
+
+
+ +
+
+
+ + + تغییرات امروز + +
+ +
+ ۰ +
+ +
+ + + تغییرات نرخ ثبت‌شده در امروز + + +
+
+ +
+ +
+
+
+ + + در انتظار تأیید + +
+ +
+ ۰ +
+ +
+ + + نرخ‌های پیشنهادی API — منتظر تأیید مدیر + + +
+
+ +
+ +
+
+ +
+ +
+
+
+
+ + + در انتظار تأیید + +
+ +
+ 0 +
+ +
+ + + سفارش جدیدی نیست + + +
+
+ +
+ +
+
+
+ + + در انتظار پرداخت + +
+ +
+ 8 +
+ +
+ + + سفارش‌های تأییدشده — آماده پرداخت + + +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + موجودی کل + +
+ +
+ 1,019,406,902 ریال +
+ +
+ + + مجموع موجودی همه کاربران + + +
+
+ + +
+
+ + + + + +
+
+
+ +
+
+
+ + + مجموع واریزی‌ها + +
+ +
+ 1,964,935,000 ریال +
+ +
+ + + کل مبالغ واریز شده موفق + + +
+
+ +
+ +
+
+
+ + + مجموع برداشت‌ها + +
+ +
+ 951,028,098 ریال +
+ +
+ + + کل مبالغ برداشت شده موفق + + +
+
+ +
+ +
+
+
+ + + تراکنش‌های امروز + +
+ +
+ 0 +
+ +
+ + + تعداد تراکنش‌های ثبت‌شده در امروز + + +
+
+ +
+ +
+
+
+ + + کیف پول‌های فعال + +
+ +
+ 4 +
+ +
+ + + کاربرانی با موجودی مثبت + + +
+
+ +
+ +
+
+
+ + + در انتظار بررسی + +
+ +
+ 5 +
+ +
+ + + تراکنش‌های نیاز به تأیید + + +
+
+ +
+ +
+
+ +
+
+ + +
+
+ + +

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

+
+ + مشاهده همه ← + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AWBمسیروضعیتمبلغتاریخ
+ + IFN-2026-77973 + + + Iran ← Aruba + + + تحویل به حمل‌کننده + + + ۳۲۵.۳ میلیون ریال + + 1405/07/10 +
+ + IFN-2026-29082 + + + Iran ← Armenia + + + تأیید شده + + + ۵۰.۸ میلیون ریال + + 1405/07/09 +
+ + IFN-2026-74634 + + + Iran ← Bahamas + + + در حال پردازش + + + ۲۱۱.۲ میلیون ریال + + 1405/07/09 +
+ + IFN-2026-34469 + + + Iran ← Angola + + + تأیید شده + + + ۱۷۷.۲ میلیون ریال + + 1405/07/08 +
+ + IFN-2026-39337 + + + Iran ← Anguilla + + + تأیید شده + + + ۱.۴۹ میلیارد ریال + + 1405/07/03 +
+
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+ + + + + +
+ +
+ +
+ +
+ + +
+ + + +
+ + +
+ + + +
+ +
+ + + + + +
+ + +
+ + + + +
+ +
+ + + + + +
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/04_Laravel/storage/panel-dump2.html b/04_Laravel/storage/panel-dump2.html new file mode 100644 index 0000000..e060a76 --- /dev/null +++ b/04_Laravel/storage/panel-dump2.html @@ -0,0 +1,3296 @@ + + + + + + + + + + + + + + + + + + + + داشبورد مالی - IFNEX Logistics + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + + + + +
+ + +
+ + +
+ +
+
+ +

+ داشبورد مالی +

+ +
+ +
+ + + + +
+
+ +
+ +
+ + + + + + + +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + نرخ درهم (AED) + +
+ +
+ ۴۱۵ هزار ریال +
+ +
+ + + ↓ ۳.۷۲% — سیستم + + +
+
+ + +
+
+ + + + + +
+
+
+ +
+
+
+ + + نرخ دلار (USD) + +
+ +
+ ۳۸۷ هزار ریال +
+ +
+ + + ↑ ۱۳.۸۸% — سیستم + + +
+
+ + +
+
+ + + + + +
+
+
+ +
+
+
+ + + تغییرات امروز + +
+ +
+ ۰ +
+ +
+ + + تغییرات نرخ ثبت‌شده در امروز + + +
+
+ +
+ +
+
+
+ + + در انتظار تأیید + +
+ +
+ ۰ +
+ +
+ + + نرخ‌های پیشنهادی API — منتظر تأیید مدیر + + +
+
+ +
+ +
+
+ +
+
+ + +
+
+ + +

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

+
+ + مشاهده همه ← + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AWBمسیروضعیتمبلغتاریخ
+ + IFN-2026-77973 + + + Iran ← Aruba + + + تحویل به حمل‌کننده + + + ۳۲۵.۳ میلیون ریال + + 1405/07/10 +
+ + IFN-2026-29082 + + + Iran ← Armenia + + + تأیید شده + + + ۵۰.۸ میلیون ریال + + 1405/07/09 +
+ + IFN-2026-74634 + + + Iran ← Bahamas + + + در حال پردازش + + + ۲۱۱.۲ میلیون ریال + + 1405/07/09 +
+ + IFN-2026-34469 + + + Iran ← Angola + + + تأیید شده + + + ۱۷۷.۲ میلیون ریال + + 1405/07/08 +
+ + IFN-2026-39337 + + + Iran ← Anguilla + + + تأیید شده + + + ۱.۴۹ میلیارد ریال + + 1405/07/03 +
+
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+ + + + + +
+ +
+ +
+ +
+ + +
+ + + +
+ + +
+ + + +
+ +
+ + + + + +
+ + +
+ + + + +
+ +
+ + + + + +
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +