Refactor the currency management system to eliminate the redundant `rate_to_aed` field and transition to a single-base model where all rates are stored relative to the Iranian Rial (IRR). Key changes: - Removed `rate_to_aed` from `currencies` table via new migration. - Updated `ExchangeRateService` to handle rates using a single `currency_code` parameter instead of currency pairs. - Refactored `UpdateExchangeRates` command to fetch rates from the Exir.io API and support multiple currencies (AED, USD, EUR, CNY). - Updated Filament resources (`CurrencyResource`, `ExchangeRateHistoryResource`) and widgets to reflect the new single-base architecture. - Improved `PriceCalculatorService` to use the updated service methodology for fetching the AED/IRR conversion rate. - Localized console command output to Persian.
294 lines
13 KiB
PHP
294 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Resources;
|
|
|
|
use App\Filament\Resources\ExchangeRateHistoryResource\Pages;
|
|
use App\Models\ExchangeRateHistory;
|
|
use App\Services\ExchangeRateService;
|
|
use Filament\Forms;
|
|
use Filament\Forms\Form;
|
|
use Filament\Resources\Resource;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Table;
|
|
use Filament\Tables\Actions\Action;
|
|
use Filament\Notifications\Notification;
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
class ExchangeRateHistoryResource extends Resource
|
|
{
|
|
protected static ?string $model = ExchangeRateHistory::class;
|
|
|
|
protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
|
|
protected static ?string $navigationLabel = 'تاریخچه نرخ ارز';
|
|
protected static ?string $modelLabel = 'تغییر نرخ';
|
|
protected static ?string $pluralModelLabel = 'تاریخچه نرخ ارز';
|
|
protected static ?string $navigationGroup = 'مالی';
|
|
protected static ?int $navigationSort = 6;
|
|
|
|
public static function form(Form $form): Form
|
|
{
|
|
return $form
|
|
->schema([
|
|
Forms\Components\Section::make('جزئیات تغییر نرخ')
|
|
->schema([
|
|
Forms\Components\TextInput::make('currency_from')
|
|
->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_from')
|
|
->label('ارز')
|
|
->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_from')
|
|
->label('ارز')
|
|
->options([
|
|
'AED' => 'درهم (AED)',
|
|
'USD' => 'دلار (USD)',
|
|
'EUR' => 'یورو (EUR)',
|
|
'CNY' => 'یوان (CNY)',
|
|
]),
|
|
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} از " . 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('refreshFromApi')
|
|
->label('آپدیت از API')
|
|
->icon('heroicon-o-arrow-path')
|
|
->color('gray')
|
|
->tooltip('این قابلیت موقتاً غیرفعال است. لطفاً نرخها را بهصورت دستی از منوی «ارزها» در بخش تنظیمات وارد کنید.')
|
|
->action(function () {
|
|
Notification::make()
|
|
->title('قابلیت غیرفعال است')
|
|
->body('دریافت خودکار نرخ از API موقتاً غیرفعال است. لطفاً نرخها را بهصورت دستی از منوی «ارزها» در بخش تنظیمات وارد کنید.')
|
|
->warning()
|
|
->send();
|
|
}),
|
|
])
|
|
->bulkActions([])
|
|
->defaultSort('created_at', 'desc')
|
|
->poll('60s');
|
|
}
|
|
|
|
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';
|
|
}
|
|
} |