diff --git a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php index 0817dc5..329327a 100644 --- a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php +++ b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php @@ -252,7 +252,87 @@ function ifnex_user_profile_shortcode($atts) { + + +
+
+ 🌍 بدهی‌های چندارزی +
+ + + + + + + + + + + + + + + + + + + +
ارزمبلغ کلتسویه‌شدهمانده
+ + + + + + + + + +
+
+ ⚠️ بدهی‌های بالا به ارز اصلی ثبت شده‌اند و با نرخ روز تسویه می‌شوند. +
+
+ + + +
+
+ 🌍 بدهی‌های چندارزی +
+ + + + + + + + + + + + + + + + + + + +
ارزمبلغ کلتسویه‌شدهمانده
+ + + + + + + + + +
+
+ +
@@ -1749,6 +1829,73 @@ function ifnex_customer_dashboard_shortcode($atts) { مشاهده تراکنش‌ها
+ + + 0): ?> +
+
+ ⚠️ بدهی در انتظار پرداخت +
+
+ ریال +
+
+ سفارش تأییدشده در انتظار پرداخت +
+
+ + + + +
+
+ 🌍 بدهی‌های چندارزی +
+ + + + + + + + + + + + + + + + + + + +
ارزمبلغ کلتسویه‌شدهمانده بدهی
+ + + + + + + + + +
+
+ ℹ️ این بدهی‌ها به ارز اصلی ثبت شده‌اند و با نرخ روز تسویه می‌شوند. + برای تسویه با مدیر تماس بگیرید. +
+
+ + + +
+
مانده حساب (موجودی منهای بدهی‌های ریالی)
+
+ ریال +
+
+ diff --git a/04_Laravel/app/Filament/Pages/Dashboard.php b/04_Laravel/app/Filament/Pages/Dashboard.php index a6947f3..dbcb1e4 100644 --- a/04_Laravel/app/Filament/Pages/Dashboard.php +++ b/04_Laravel/app/Filament/Pages/Dashboard.php @@ -9,6 +9,8 @@ use App\Filament\Widgets\RecentShipmentsWidget; use App\Filament\Widgets\RecentTransactionsWidget; use App\Filament\Widgets\TransactionChartWidget; use Filament\Pages\Dashboard as BaseDashboard; +use App\Filament\Widgets\UnsettledCreditsWidget; + /** * IFNEX Admin Dashboard — layered layout (12-column grid) @@ -42,8 +44,9 @@ class Dashboard extends BaseDashboard public function getHeaderWidgets(): array { return [ - FinanceOverviewWidget::class, - DashboardInfoWidget::class, + FinanceOverviewWidget::class, + DashboardInfoWidget::class, + UnsettledCreditsWidget::class, ]; } diff --git a/04_Laravel/app/Filament/Resources/CustomerCreditResource.php b/04_Laravel/app/Filament/Resources/CustomerCreditResource.php index ca61c4e..ec8cf6f 100644 --- a/04_Laravel/app/Filament/Resources/CustomerCreditResource.php +++ b/04_Laravel/app/Filament/Resources/CustomerCreditResource.php @@ -296,6 +296,20 @@ class CustomerCreditResource extends Resource ]; } + /** + * Badge روی منوی سایدبار — تعداد اعتبارهای باز (تسویه‌نشده). + */ + public static function getNavigationBadge(): ?string + { + $count = \App\Models\CustomerCredit::whereIn('status', ['open', 'partially_settled'])->count(); + return $count > 0 ? (string) ifnex_persian_digits(number_format($count)) : null; + } + + public static function getNavigationBadgeColor(): ?string + { + return 'danger'; + } + // ─── Authorization ──────────────────────────────── public static function canViewAny(): bool diff --git a/04_Laravel/app/Filament/Resources/UserResource.php b/04_Laravel/app/Filament/Resources/UserResource.php index 2c1246f..93217e1 100644 --- a/04_Laravel/app/Filament/Resources/UserResource.php +++ b/04_Laravel/app/Filament/Resources/UserResource.php @@ -3,6 +3,7 @@ namespace App\Filament\Resources; use App\Filament\Resources\UserResource\Pages; +use App\Filament\Resources\UserResource\RelationManagers; use App\Models\User; use Filament\Forms; use Filament\Forms\Form; @@ -206,7 +207,9 @@ class UserResource extends Resource public static function getRelations(): array { - return []; + return [ + RelationManagers\CreditsRelationManager::class, + ]; } public static function getPages(): array diff --git a/04_Laravel/app/Filament/Resources/UserResource/RelationManagers/CreditsRelationManager.php b/04_Laravel/app/Filament/Resources/UserResource/RelationManagers/CreditsRelationManager.php new file mode 100644 index 0000000..028fa3f --- /dev/null +++ b/04_Laravel/app/Filament/Resources/UserResource/RelationManagers/CreditsRelationManager.php @@ -0,0 +1,116 @@ +schema([ + Forms\Components\TextInput::make('currency_code') + ->label('ارز') + ->disabled(), + Forms\Components\TextInput::make('amount') + ->label('مبلغ کل') + ->disabled(), + Forms\Components\TextInput::make('settled_amount') + ->label('تسویه‌شده') + ->disabled(), + ]); + } + + public function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('currency_code') + ->label('ارز') + ->badge() + ->color('info'), + + Tables\Columns\TextColumn::make('amount') + ->label('مبلغ کل') + ->numeric(2) + ->sortable(), + + Tables\Columns\TextColumn::make('settled_amount') + ->label('تسویه‌شده') + ->numeric(2) + ->color('success'), + + Tables\Columns\TextColumn::make('balance') + ->label('مانده') + ->numeric(2) + ->badge() + ->color(fn ($record) => $record->balance > 0 ? 'danger' : 'gray'), + + Tables\Columns\TextColumn::make('status') + ->label('وضعیت') + ->badge() + ->formatStateUsing(fn (string $state) => match ($state) { + 'open' => 'باز', + 'partially_settled' => 'نیمه‌تسویه', + 'settled' => 'تسویه‌شده', + default => $state, + }) + ->color(fn (string $state) => match ($state) { + 'open' => 'danger', + 'partially_settled' => 'warning', + 'settled' => 'success', + default => 'gray', + }), + + Tables\Columns\TextColumn::make('reason') + ->label('دلیل') + ->limit(40) + ->toggleable(), + + Tables\Columns\TextColumn::make('granted_at') + ->label('تاریخ اعطا') + ->dateTime('Y/m/d') + ->sortable(), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('status') + ->label('وضعیت') + ->options([ + 'open' => 'باز', + 'partially_settled' => 'نیمه‌تسویه', + 'settled' => 'تسویه‌شده', + ]), + Tables\Filters\SelectFilter::make('currency_code') + ->label('ارز') + ->options(fn () => CustomerCredit::select('currency_code') + ->distinct() + ->orderBy('currency_code') + ->pluck('currency_code', 'currency_code') + ->toArray()), + ]) + ->headerActions([ + // ایجاد اعتبار فقط از طریق CustomerCreditResource (با Policy) + Tables\Actions\Action::make('manage_credits') + ->label('مدیریت اعتبارها') + ->icon('heroicon-o-arrow-left') + ->url(fn () => route('filament.admin.resources.customer-credits.index')) + ->color('primary'), + ]) + ->actions([ + Tables\Actions\ViewAction::make(), + ]) + ->defaultSort('created_at', 'desc'); + } +} \ No newline at end of file diff --git a/04_Laravel/app/Filament/Widgets/UnsettledCreditsWidget.php b/04_Laravel/app/Filament/Widgets/UnsettledCreditsWidget.php new file mode 100644 index 0000000..c5c98ed --- /dev/null +++ b/04_Laravel/app/Filament/Widgets/UnsettledCreditsWidget.php @@ -0,0 +1,81 @@ +select('currency_code') + ->selectRaw('SUM(amount) as total_amount') + ->selectRaw('SUM(settled_amount) as total_settled') + ->selectRaw('SUM(amount - settled_amount) as total_balance') + ->selectRaw('COUNT(*) as credit_count') + ->groupBy('currency_code') + ->orderBy('total_balance', 'desc') + ->get() + ->map(fn ($r) => [ + 'currency_code' => $r->currency_code, + 'total_amount' => (float) $r->total_amount, + 'total_settled' => (float) $r->total_settled, + 'total_balance' => (float) $r->total_balance, + 'credit_count' => (int) $r->credit_count, + ]); + + // ─── تعداد کل اعتبارهای باز ─── + $totalOpenCredits = CustomerCredit::whereIn('status', ['open', 'partially_settled'])->count(); + + // ─── تعداد مشتریان بدهکار ─── + $totalDebtors = CustomerCredit::whereIn('status', ['open', 'partially_settled']) + ->distinct('user_id') + ->count('user_id'); + + // ─── ۵ بدهکار برتر (بر اساس مانده کل به ارز) ─── + $topDebtors = CustomerCredit::whereIn('customer_credits.status', ['open', 'partially_settled']) + ->join('users', 'customer_credits.user_id', '=', 'users.id') + ->select( + 'users.id as user_id', + 'users.name as user_name', + 'users.phone as user_phone', + DB::raw('COUNT(customer_credits.id) as credit_count'), + DB::raw('GROUP_CONCAT(DISTINCT customer_credits.currency_code) as currencies'), + DB::raw('SUM(customer_credits.amount - customer_credits.settled_amount) as total_balance_sum'), + DB::raw('MAX(customer_credits.granted_at) as latest_grant') + ) + ->groupBy('users.id', 'users.name', 'users.phone') + ->orderBy('total_balance_sum', 'desc') + ->limit(5) + ->get(); + + return [ + 'currencySummary' => $currencySummary, + 'totalOpenCredits' => $totalOpenCredits, + 'totalDebtors' => $totalDebtors, + 'topDebtors' => $topDebtors, + 'hasDebts' => $currencySummary->isNotEmpty(), + ]; + } +} \ No newline at end of file diff --git a/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php b/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php index 8e555f7..c2a4ea4 100644 --- a/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php +++ b/04_Laravel/app/Http/Controllers/Api/Customer/CustomerOrderController.php @@ -719,6 +719,16 @@ class CustomerOrderController extends Controller $wallet_balance = $user->wallet ? $user->wallet->balance : 0; $account_balance = $wallet_balance - $pending_payments_total; + // بدهی‌های چندارزی (EUR/USD/AED/CNY) — به همان ارز خارجی، نه معادل ریالی + $foreign_credits = app(\App\Services\CustomerCreditService::class) + ->getCustomerDebtsByCurrency($user); + + $account_balance = $wallet_balance - $pending_payments_total; + + // بدهی‌های چندارزی — به همان ارز خارجی + $foreign_credits = app(\App\Services\CustomerCreditService::class) + ->getCustomerDebtsByCurrency($user); + // آمار سفارشات $stats = [ 'total_orders' => Shipment::where('user_id', $user->id)->count(), @@ -762,6 +772,7 @@ class CustomerOrderController extends Controller ] : null, 'account_balance' => $account_balance, 'pending_payments_total' => $pending_payments_total, + 'foreign_credits' => $foreign_credits, 'stats' => $stats, ]); } diff --git a/04_Laravel/app/Providers/Filament/AdminPanelProvider.php b/04_Laravel/app/Providers/Filament/AdminPanelProvider.php index ebdaa5f..5fc378e 100644 --- a/04_Laravel/app/Providers/Filament/AdminPanelProvider.php +++ b/04_Laravel/app/Providers/Filament/AdminPanelProvider.php @@ -76,6 +76,7 @@ class AdminPanelProvider extends PanelProvider ->widgets([ \App\Filament\Widgets\RecentShipmentsWidget::class, \App\Filament\Widgets\ExchangeRateWidget::class, + \App\Filament\Widgets\UnsettledCreditsWidget::class, ]) ->middleware([ EncryptCookies::class, diff --git a/04_Laravel/app/Services/CustomerCreditService.php b/04_Laravel/app/Services/CustomerCreditService.php index 0b4dd7c..30c5cd8 100644 --- a/04_Laravel/app/Services/CustomerCreditService.php +++ b/04_Laravel/app/Services/CustomerCreditService.php @@ -86,6 +86,15 @@ class CustomerCreditService ) { $credit = CustomerCredit::lockForUpdate()->findOrFail($credit->id); + // بررسی مجدد مانده بعد از lock + $currentBalance = (float) $credit->amount - (float) $credit->settled_amount; + if ($amountForeign > $currentBalance + 0.005) { + throw new RuntimeException( + 'مانده بدهی تغییر کرده. مانده فعلی: ' + . number_format($currentBalance, 2) . ' ' . $credit->currency_code + ); + } + $settlementAmountIrr = round($amountForeign * $exchangeRate, 2); $walletTransactionId = null; diff --git a/04_Laravel/resources/views/filament/widgets/unsettled-credits.blade.php b/04_Laravel/resources/views/filament/widgets/unsettled-credits.blade.php new file mode 100644 index 0000000..464c87a --- /dev/null +++ b/04_Laravel/resources/views/filament/widgets/unsettled-credits.blade.php @@ -0,0 +1,109 @@ +{{-- + IFNEX — Unsettled foreign currency credits dashboard widget. + Shows: currency summary badges + top 5 debtors table. +--}} + +
+ + {{-- ──────── Header ──────── --}} +
+
+ + + +
+

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

+ + {{ ifnex_persian_digits(number_format($totalDebtors)) }} مشتری بدهکار، + {{ ifnex_persian_digits(number_format($totalOpenCredits)) }} اعتبار باز + +
+
+ + مشاهده همه ← + +
+ + @if(!$hasDebts) + {{-- Empty state --}} +
+
+ +
+

+ ✅ هیچ بدهی ارزی تسویه‌نشده‌ای وجود ندارد +

+
+ @else + {{-- ──────── Currency Summary ──────── --}} +
+
+ @foreach($currencySummary as $cs) + + {{ $cs['currency_code'] }}: + {{ number_format($cs['total_balance'], 2) }} + + ({{ ifnex_persian_digits(number_format($cs['credit_count'])) }} رکورد) + + + @endforeach +
+
+ + {{-- ──────── Top Debtors Table ──────── --}} +
+ + + + + + + + + + + + + @foreach($topDebtors as $debtor) + + + + + + + + + @endforeach + +
مشتریموبایلارزهاتعداد اعتبارآخرین اعطااقدام
+ + {{ $debtor->user_name }} + + + {{ $debtor->user_phone ?: '—' }} + + @php $currencies = explode(',', $debtor->currencies); @endphp + @foreach($currencies as $cur) + + {{ $cur }} + + @endforeach + + + {{ ifnex_persian_digits(number_format($debtor->credit_count)) }} + + + {{ $debtor->latest_grant ? \Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($debtor->latest_grant))->format('Y/m/d') : '—' }} + + + وضعیت مالی + +
+
+ @endif +
+
\ No newline at end of file