schema([ Forms\Components\Section::make('جزئیات تغییر نرخ') ->schema([ Forms\Components\TextInput::make('currency_from') ->label('ارز مبدأ') ->disabled(), Forms\Components\TextInput::make('currency_to') ->label('ارز مقصد') ->disabled(), Forms\Components\TextInput::make('old_rate') ->label('نرخ قبلی') ->disabled() ->suffix('ریال'), Forms\Components\TextInput::make('new_rate') ->label('نرخ جدید') ->disabled() ->suffix('ریال'), Forms\Components\TextInput::make('change_percent') ->label('درصد تغییر') ->disabled() ->suffix('%'), Forms\Components\TextInput::make('change_source') ->label('منبع تغییر') ->disabled(), Forms\Components\Select::make('changed_by') ->label('توسط') ->relationship('changedBy', 'name') ->disabled(), Forms\Components\TextInput::make('status') ->label('وضعیت') ->disabled(), Forms\Components\Select::make('approved_by') ->label('تأییدکننده') ->relationship('approvedBy', 'name') ->disabled(), Forms\Components\TextInput::make('approved_at') ->label('تاریخ تأیید') ->disabled(), Forms\Components\Textarea::make('notes') ->label('یادداشت') ->disabled() ->columnSpanFull(), ])->columns(2), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('id') ->label('شناسه') ->sortable() ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('currency_pair') ->label('جفت ارز') ->state(fn ($record) => "{$record->currency_from} → {$record->currency_to}") ->badge() ->color('info'), Tables\Columns\TextColumn::make('old_rate') ->label('نرخ قبلی') ->numeric(0) ->suffix(' ریال') ->color('gray'), Tables\Columns\TextColumn::make('new_rate') ->label('نرخ جدید') ->numeric(0) ->suffix(' ریال') ->weight('bold') ->color('primary'), Tables\Columns\TextColumn::make('change_percent') ->label('تغییر') ->state(fn ($record) => $record->change_percent !== null ? number_format($record->change_percent, 2) . '%' : '—') ->badge() ->color(fn ($record) => match(true) { $record->change_percent > 0 => 'danger', $record->change_percent < 0 => 'success', default => 'gray', }) ->icon(fn ($record) => match(true) { $record->change_percent > 0 => 'heroicon-m-arrow-trending-up', $record->change_percent < 0 => 'heroicon-m-arrow-trending-down', default => null, }), Tables\Columns\TextColumn::make('change_source') ->label('منبع') ->badge() ->formatStateUsing(fn ($state) => $state === 'api' ? 'API خودکار' : 'دستی') ->color(fn ($state) => $state === 'api' ? 'info' : 'warning') ->icon(fn ($state) => $state === 'api' ? 'heroicon-m-cog-6-tooth' : 'heroicon-m-pencil-square'), Tables\Columns\TextColumn::make('status') ->label('وضعیت') ->badge() ->formatStateUsing(fn (string $state): string => match($state) { 'pending' => 'در انتظار', 'approved' => 'تأیید شده', 'rejected' => 'رد شده', 'superseded' => 'جای‌گذشته', default => $state, }) ->color(fn (string $state): string => match($state) { 'pending' => 'warning', 'approved' => 'success', 'rejected' => 'danger', 'superseded' => 'gray', default => 'gray', }) ->icon(fn (string $state): string => match($state) { 'pending' => 'heroicon-o-clock', 'approved' => 'heroicon-o-check-circle', 'rejected' => 'heroicon-o-x-circle', default => 'heroicon-o-minus-circle', }), Tables\Columns\TextColumn::make('changedBy.name') ->label('توسط') ->state(fn ($record) => $record->changedBy?->name ?? ($record->change_source === 'api' ? 'سیستم' : '—')) ->icon('heroicon-m-user'), Tables\Columns\TextColumn::make('created_at') ->label('تاریخ تغییر') ->dateTime('Y/m/d H:i') ->sortable(), ]) ->filters([ Tables\Filters\SelectFilter::make('status') ->label('وضعیت') ->options([ 'pending' => 'در انتظار', 'approved' => 'تأیید شده', 'rejected' => 'رد شده', 'superseded' => 'جای‌گذشته', ]), Tables\Filters\SelectFilter::make('change_source') ->label('منبع') ->options([ 'api' => 'API خودکار', 'manual' => 'دستی', ]), Tables\Filters\SelectFilter::make('currency_pair') ->label('جفت ارز') ->options([ 'AED_IRR' => 'AED → IRR', 'USD_IRR' => 'USD → IRR', ]) ->query(function ($query, array $data) { if (!$data['value']) return $query; [$from, $to] = explode('_', $data['value']); return $query->where('currency_from', $from) ->where('currency_to', $to); }), Tables\Filters\Filter::make('created_at') ->form([ Forms\Components\DatePicker::make('from')->label('از تاریخ'), Forms\Components\DatePicker::make('until')->label('تا تاریخ'), ]) ->query(function ($query, array $data) { return $query ->when($data['from'], fn ($q, $d) => $q->whereDate('created_at', '>=', $d)) ->when($data['until'], fn ($q, $d) => $q->whereDate('created_at', '<=', $d)); }), ]) ->actions([ Tables\Actions\ViewAction::make()->label('جزئیات'), Tables\Actions\Action::make('approve') ->label('تأیید') ->icon('heroicon-o-check-circle') ->color('success') ->requiresConfirmation() ->modalHeading('تأیید نرخ پیشنهادی') ->modalDescription(fn ($record) => "نرخ {$record->currency_from}→{$record->currency_to} از " . number_format($record->old_rate) . " به " . number_format($record->new_rate) . " ریال تغییر یابد؟ نرخ زنده پس از تأیید به‌روز می‌شود.") ->modalSubmitActionLabel('بله، تأیید و اعمال کن') ->visible(fn ($record) => $record->status === 'pending') ->action(function ($record) { try { app(ExchangeRateService::class)->approveRate($record, Auth::id()); Notification::make()->title('نرخ تأیید و روی نرخ زنده اعمال شد')->success()->send(); } catch (\Throwable $e) { Notification::make()->title('خطا')->body($e->getMessage())->danger()->send(); } }), Tables\Actions\Action::make('reject') ->label('رد') ->icon('heroicon-o-x-circle') ->color('danger') ->requiresConfirmation() ->modalHeading('رد نرخ پیشنهادی') ->modalDescription('نرخ رد می‌شود. نرخ زنده دست‌نخورده باقی می‌ماند.') ->visible(fn ($record) => $record->status === 'pending') ->action(function ($record) { try { app(ExchangeRateService::class)->rejectRate($record, Auth::id()); Notification::make()->title('نرخ رد شد')->warning()->send(); } catch (\Throwable $e) { Notification::make()->title('خطا')->body($e->getMessage())->danger()->send(); } }), ]) ->headerActions([ Action::make('setNewRate') ->label('تنظیم نرخ جدید') ->icon('heroicon-o-pencil-square') ->color('warning') ->form([ Forms\Components\Select::make('currency_pair') ->label('جفت ارز') ->required() ->options([ 'AED_IRR' => 'درهم به ریال (AED → IRR)', 'USD_IRR' => 'دلار به ریال (USD → IRR)', ]) ->default('AED_IRR'), Forms\Components\TextInput::make('new_rate') ->label('نرخ جدید (ریال)') ->required() ->numeric() ->minValue(1) ->prefix('ریال') ->helperText(fn (callable $get) => 'نرخ فعلی: ' . number_format(self::getCurrentRateForPair($get('currency_pair'))) . ' ریال' ), Forms\Components\Textarea::make('notes') ->label('یادداشت (اختیارر)') ->placeholder('دلیل تغییر نرخ...') ->maxLength(500), ]) ->action(function (array $data) { [$from, $to] = explode('_', $data['currency_pair']); $service = app(ExchangeRateService::class); $service->setRate( from: $from, to: $to, newRate: (float) $data['new_rate'], userId: Auth::id(), notes: $data['notes'] ?? null ); Notification::make() ->title('نرخ ارز با موفقیت به‌روز شد') ->body("نرخ {$from}/{$to} به " . number_format($data['new_rate']) . " ریال تغییر یافت") ->success() ->send(); }), Action::make('refreshFromApi') ->label('آپدیت از API') ->icon('heroicon-o-arrow-path') ->color('info') ->requiresConfirmation() ->modalHeading('آپدیت نرخ از API') ->modalDescription('این عمل یک نرخ پیشنهادی «در انتظار تأیید» می‌سازد. نرخ زنده تغییر نمی‌کند تا تأیید کنید.') ->action(function () { try { \Artisan::call('ifnex:update-rates'); Notification::make() ->title('نرخ پیشنهادی API ساخته شد — منتظر تأیید شما') ->success() ->send(); } catch (\Throwable $e) { Notification::make() ->title('خطا در به‌روزرسانی') ->body($e->getMessage()) ->danger() ->send(); } }), ]) ->bulkActions([]) ->defaultSort('created_at', 'desc') ->poll('60s'); } protected static function getCurrentRateForPair(?string $pair): float { if (!$pair) return 0; [$from, $to] = explode('_', $pair); return app(ExchangeRateService::class)->getCurrentRate($from, $to); } public static function getRelations(): array { return []; } public static function getPages(): array { return [ 'index' => Pages\ListExchangeRateHistories::route('/'), 'view' => Pages\ViewExchangeRateHistory::route('/{record}'), ]; } public static function canViewAny(): bool { $user = Auth::user(); return $user && $user->hasAnyRole(['super_admin', 'admin']); } public static function canCreate(): bool { return false; } public static function canEdit($record): bool { return false; } public static function canDelete($record): bool { return false; } public static function getNavigationBadge(): ?string { $count = app(ExchangeRateService::class)->getPendingCount(); return $count > 0 ? (string) $count : null; } public static function getNavigationBadgeColor(): ?string { return 'warning'; } }