feat(ui): redesign admin dashboard with layered grid and custom branding
Implement a comprehensive overhaul of the Filament admin dashboard, introducing a structured 12-column responsive grid layout and enhanced visual identity. Key changes include: - Redesigned Dashboard layout with a 4-layer progressive disclosure architecture (KPIs, Exchange Rates, Data Tables, and Trends). - Introduced custom IFNEX design tokens via CSS, including branded stat cards, status badges, and improved sidebar typography. - Enhanced FinanceOverviewWidget with real-time trend analysis and sparkline-ready data structures. - Refactored RecentShipments and RecentTransactions widgets to use custom Blade templates for a more polished, consistent UI. - Integrated brand assets including the company logo and custom authentication pages. - Added global number formatting helpers and integrated them into financial widgets. - Updated AdminPanelProvider to support custom login pages and brand-specific header/footer hooks.
This commit is contained in:
parent
43187e4b90
commit
91984dbde3
65
04_Laravel/app/Filament/Pages/Auth/Login.php
Normal file
65
04_Laravel/app/Filament/Pages/Auth/Login.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Pages\Auth\Login as BaseLogin;
|
||||
|
||||
/**
|
||||
* پیج سفارشی ورود به فیلمنت با هویت بصری IFNEX.
|
||||
*
|
||||
* این کلاس فقط نمای سفارشی `filament.pages.auth.login` را به کلاس پایه
|
||||
* اتصال میدهد و اسکمای فرم را برای افزودن پیشوندهای آیکون (آدرس ایمیل
|
||||
* و قفل) و همچنین دکمهٔ «ورود به سیستم» بازنویسی میکند. هیچیک از
|
||||
* عملکردهای احراز هویت پایه تغییر نکرده است.
|
||||
*/
|
||||
class Login extends BaseLogin
|
||||
{
|
||||
protected static string $view = 'filament.pages.auth.login';
|
||||
|
||||
/**
|
||||
* بازنویسی اسکمای فرم برای افزودن پیشوند آیکون به فیلدها
|
||||
* و حفظ دکمهٔ چشم برای نمایش/مخفی کردن رمز عبور.
|
||||
*/
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->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']);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use Filament\Pages\Page;
|
||||
|
||||
/**
|
||||
* گزینهٔ placeholder برای «طراحی مجدد ویجتهای داشبورد».
|
||||
* پس از تحویل، ویجتهای فعلی بازطراحی خواهند شد. این صفحه فقط یک یادآور
|
||||
* و فهرست ویجتهای موردنظر است.
|
||||
*/
|
||||
class DashboardWidgetRedesign extends Page
|
||||
{
|
||||
protected static ?string $navigationIcon = 'heroicon-o-swatch';
|
||||
|
||||
protected static string $view = 'filament.pages.dashboard-widget-redesign';
|
||||
|
||||
protected static ?string $navigationLabel = 'طراحی مجدد ویجتها';
|
||||
|
||||
protected static ?string $navigationGroup = 'ابزارها';
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()?->hasAnyRole(['super_admin', 'admin']) ?? false;
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -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')),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
146
04_Laravel/app/Helpers/number.php
Normal file
146
04_Laravel/app/Helpers/number.php
Normal file
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* IFNEX — Helpers for compact Persian-friendly number formatting.
|
||||
*
|
||||
* File: app/Helpers/number.php
|
||||
* Registered in composer.json under autoload.files so the helpers
|
||||
* below are globally available across the Laravel app (no namespace).
|
||||
*/
|
||||
|
||||
if (!function_exists('ifnex_format_number')) {
|
||||
/**
|
||||
* Format a (potentially huge) integer into a compact Persian-friendly string.
|
||||
*
|
||||
* Examples:
|
||||
* 950 → "۹۵۰"
|
||||
* 1500 → "۲ هزار"
|
||||
* 2_400_000 → "۲.۴ میلیون"
|
||||
* 1_019_406_902 → "۱.۰۲ میلیارد"
|
||||
*
|
||||
* @param int|float|string $number
|
||||
* @param string $currency Optional currency suffix (default: ریال). Pass '' to skip.
|
||||
* @return string
|
||||
*/
|
||||
function ifnex_format_number(int|float|string $number, string $currency = 'ریال'): string
|
||||
{
|
||||
$number = (float) $number;
|
||||
$abs = abs($number);
|
||||
|
||||
if ($abs >= 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];
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 () => '<div style="text-align:center;padding:10px 0;font-size:12px;color:#94a3b8;">'
|
||||
. 'قدرت گرفته از <a href="https://vernasoft.ir" target="_blank" rel="noopener" '
|
||||
. 'style="color:#f59e0b;font-weight:600;text-decoration:none;">ورنا سافت</a></div>'
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
BIN
04_Laravel/public/images/ifnex-logo.png
Normal file
BIN
04_Laravel/public/images/ifnex-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@ -264,19 +454,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// کلیک روی ردیف عنوان گروه = باز/بسته کردن گروه (علاوه بر دکمه فلش)
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.fi-sidebar-group-button');
|
||||
if (!btn || e.target.closest('.fi-sidebar-group-collapse-button')) return;
|
||||
if (!window.Alpine) return;
|
||||
var store = window.Alpine.store('sidebar');
|
||||
if (!store) return;
|
||||
var labelEl = btn.querySelector('.fi-sidebar-group-label');
|
||||
// در حالت جمعشدهٔ کناری (فقط آیکون)، عنوان مخفی است — نادیده بگیر
|
||||
if (!labelEl || labelEl.offsetParent === null) return;
|
||||
var label = labelEl.textContent.trim();
|
||||
if (label) store.toggleCollapsedGroup(label);
|
||||
});
|
||||
// ⚠️ کلیک روی عنوان گروه را خود فیلامنت بهصورت بومی هندل میکند
|
||||
// (x-on:click روی .fi-sidebar-group-button در vendor) — هندلر اضافه اینجا
|
||||
// باعث toggle دوباره و در نتیجه «هیچ اتفاقی نمیافتد» میشد؛ حذف شد.
|
||||
|
||||
// دکمه «بستن/باز کردن همه» بالای سایدبار
|
||||
function injectToggle() {
|
||||
|
||||
352
04_Laravel/resources/views/filament/pages/auth/login.blade.php
Normal file
352
04_Laravel/resources/views/filament/pages/auth/login.blade.php
Normal file
@ -0,0 +1,352 @@
|
||||
@php
|
||||
$logoUrl = asset('images/ifnex-logo.png');
|
||||
@endphp
|
||||
|
||||
<div class="ifnex-login-page">
|
||||
{{-- Decorative background pattern: subtle globe + airplane in corners --}}
|
||||
<div class="ifnex-login-decor" aria-hidden="true">
|
||||
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" class="ifnex-decor-globe">
|
||||
<circle cx="100" cy="100" r="92" fill="none" stroke="#f59e0b" stroke-width="0.6"/>
|
||||
<ellipse cx="100" cy="100" rx="92" ry="34" fill="none" stroke="#f59e0b" stroke-width="0.6"/>
|
||||
<ellipse cx="100" cy="100" rx="92" ry="62" fill="none" stroke="#f59e0b" stroke-width="0.6"/>
|
||||
<line x1="8" y1="100" x2="192" y2="100" stroke="#f59e0b" stroke-width="0.6"/>
|
||||
<line x1="100" y1="8" x2="100" y2="192" stroke="#f59e0b" stroke-width="0.6"/>
|
||||
<path d="M8,70 Q100,90 192,70" fill="none" stroke="#f59e0b" stroke-width="0.4"/>
|
||||
<path d="M8,130 Q100,110 192,130" fill="none" stroke="#f59e0b" stroke-width="0.4"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="ifnex-decor-plane" fill="none" stroke="#f59e0b" stroke-width="0.5">
|
||||
<path d="M21,16v-2l-8-5V3.5C13,2.67 12.33,2 11.5,2S10,2.67 10,3.5V9l-8,5v2l8-2.5V19l-2,1.5V22l3.5-1l3.5,1v-1.5L13,19v-5.5L21,16z" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="ifnex-login-card">
|
||||
<img src="{{ $logoUrl }}" alt="IFNEX Logo" class="ifnex-login-logo" />
|
||||
|
||||
<h1 class="ifnex-login-title">ورود به سیستم</h1>
|
||||
<p class="ifnex-login-subtitle">IFNEX Logistics — مدیریت لجستیک</p>
|
||||
|
||||
<form wire:submit="authenticate">
|
||||
<div class="ifnex-login-form">
|
||||
{{ $this->form }}
|
||||
</div>
|
||||
|
||||
<div class="ifnex-login-submit">
|
||||
{{ $this->getAuthenticateFormAction() }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="ifnex-login-footer">© 2026 IFNEX Logistics — VernaSoft</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ============================================================
|
||||
IFNEX Login Page — custom redesign
|
||||
scoped under .ifnex-login-* so it never leaks onto
|
||||
the rest of the Filament admin UI.
|
||||
============================================================ */
|
||||
|
||||
/* ---- Full-screen background matching WP hero ---- */
|
||||
.ifnex-login-page {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #f59e0b 200%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
direction: rtl;
|
||||
font-family: 'Vazirmatn', 'IranYekan', Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
/* ---- Subtle decorative globe + airplane in corners ---- */
|
||||
.ifnex-login-decor {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ifnex-decor-globe {
|
||||
position: absolute;
|
||||
bottom: -220px;
|
||||
left: -180px;
|
||||
width: 620px;
|
||||
height: 620px;
|
||||
opacity: 0.07;
|
||||
}
|
||||
|
||||
.ifnex-decor-plane {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 60px;
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
opacity: 0.06;
|
||||
transform: rotate(-18deg);
|
||||
}
|
||||
|
||||
/* ---- Centered login card ---- */
|
||||
.ifnex-login-card {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
background: #FAF9F6;
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2.25rem 1.75rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
animation: ifnexFadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes ifnexFadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ---- Logo ---- */
|
||||
.ifnex-login-logo {
|
||||
display: block;
|
||||
margin: 0 auto 1.5rem;
|
||||
width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* ---- Title & subtitle ---- */
|
||||
.ifnex-login-title {
|
||||
font-family: 'Vazirmatn', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 22px;
|
||||
color: #1f2937;
|
||||
text-align: center;
|
||||
margin: 0 0 0.5rem;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.ifnex-login-subtitle {
|
||||
font-family: 'Vazirmatn', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
margin: 0 0 2rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ---- Form wrapper ---- */
|
||||
.ifnex-login-form {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* === Filament form overrides (scoped to card) === */
|
||||
|
||||
/* Field labels */
|
||||
.ifnex-login-card .fi-input-wrp-label,
|
||||
.ifnex-login-card .fi-field-label,
|
||||
.ifnex-login-card label.fi-input-wrp-label,
|
||||
.ifnex-login-card .fi-label {
|
||||
font-family: 'Vazirmatn', sans-serif !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #374151 !important;
|
||||
margin-bottom: 0.375rem !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Input wrapper */
|
||||
.ifnex-login-card .fi-input-wrp,
|
||||
.ifnex-login-card .fi-input-text-wrp,
|
||||
.ifnex-login-card .fi-input-wp {
|
||||
border-radius: 8px !important;
|
||||
background: #ffffff !important;
|
||||
border: 1px solid #e5e7eb !important;
|
||||
transition: border-color 0.2s, box-shadow 0.2s !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ifnex-login-card .fi-input-wrp:focus-within,
|
||||
.ifnex-login-card .fi-input-text-wrp:focus-within {
|
||||
border-color: #f59e0b !important;
|
||||
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.15) !important;
|
||||
}
|
||||
|
||||
/* Input field itself */
|
||||
.ifnex-login-card .fi-input-wrp-input,
|
||||
.ifnex-login-card .ifnex-login-input,
|
||||
.ifnex-login-card input[type="email"],
|
||||
.ifnex-login-card input[type="password"],
|
||||
.ifnex-login-card input[type="text"] {
|
||||
padding: 0.75rem 1rem !important;
|
||||
font-size: 14px !important;
|
||||
color: #1f2937 !important;
|
||||
background: transparent !important;
|
||||
font-family: 'Vazirmatn', sans-serif !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ifnex-login-card input::placeholder {
|
||||
color: #9ca3af !important;
|
||||
font-family: 'Vazirmatn', sans-serif !important;
|
||||
}
|
||||
|
||||
/* Prefix icons (at-symbol on email, lock on password) */
|
||||
.ifnex-login-card .fi-input-wrp-icon,
|
||||
.ifnex-login-card .fi-icon,
|
||||
.ifnex-login-card .fi-input-wp-icon {
|
||||
color: #9ca3af !important;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.ifnex-login-card .fi-input-wrp:focus-within .fi-input-wrp-icon,
|
||||
.ifnex-login-card .fi-input-wrp:focus-within .fi-icon,
|
||||
.ifnex-login-card .fi-input-text-wrp:focus-within .fi-icon {
|
||||
color: #f59e0b !important;
|
||||
}
|
||||
|
||||
/* Spacing between form fields */
|
||||
.ifnex-login-card .fi-components > div + div,
|
||||
.ifnex-login-card .fi-field + .fi-field,
|
||||
.ifnex-login-card .fi-form-field + .fi-form-field,
|
||||
.ifnex-login-card .fi-fo-wrp + .fi-fo-wrp {
|
||||
margin-top: 1rem !important;
|
||||
}
|
||||
|
||||
/* Checkbox "مرا به خاطر بسپار" — amber accent */
|
||||
.ifnex-login-card .ifnex-login-remember,
|
||||
.ifnex-login-card .fi-checkbox-wrp,
|
||||
.ifnex-login-card .fi-checkbox {
|
||||
accent-color: #f59e0b !important;
|
||||
}
|
||||
|
||||
.ifnex-login-card .fi-checkbox-wrp-label,
|
||||
.ifnex-login-card .fi-checkbox-label,
|
||||
.ifnex-login-card .fi-checkbox-wp-label,
|
||||
.ifnex-login-card .ifnex-login-remember + label {
|
||||
font-size: 13px !important;
|
||||
color: #4b5563 !important;
|
||||
font-family: 'Vazirmatn', sans-serif !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.ifnex-login-card input[type="checkbox"] {
|
||||
accent-color: #f59e0b !important;
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Submit button — gradient amber, full-width */
|
||||
.ifnex-login-card .fi-btn,
|
||||
.ifnex-login-card .ifnex-login-submit .fi-btn,
|
||||
.ifnex-login-card button[type="submit"] {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #FDB913 100%) !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 700 !important;
|
||||
font-family: 'Vazirmatn', sans-serif !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 0.75rem 1rem !important;
|
||||
width: 100% !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 4px 14px rgba(245, 158, 11, 0.35) !important;
|
||||
transition: transform 0.2s, box-shadow 0.2s, background 0.2s !important;
|
||||
cursor: pointer !important;
|
||||
font-size: 15px !important;
|
||||
display: block !important;
|
||||
text-align: center !important;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.ifnex-login-card .fi-btn:hover,
|
||||
.ifnex-login-card .ifnex-login-submit .fi-btn:hover,
|
||||
.ifnex-login-card button[type="submit"]:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.45) !important;
|
||||
background: linear-gradient(135deg, #FDB913 0%, #f59e0b 100%) !important;
|
||||
}
|
||||
|
||||
.ifnex-login-card .fi-btn:active,
|
||||
.ifnex-login-card button[type="submit"]:active {
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
/* Form actions layout — make submit action stretch full-width */
|
||||
.ifnex-login-card .fi-form-actions,
|
||||
.ifnex-login-card .fi-form-actions-actions,
|
||||
.ifnex-login-card .fi-actions,
|
||||
.ifnex-login-card .ifnex-login-submit {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
margin-top: 1.25rem !important;
|
||||
}
|
||||
|
||||
.ifnex-login-card .fi-form-actions-action,
|
||||
.ifnex-login-card .fi-actions-action {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Hide the default Filament auth heading/subheading/brand — we render our own */
|
||||
.ifnex-login-card .fi-auth-heading,
|
||||
.ifnex-login-card .fi-auth-subheading,
|
||||
.ifnex-login-card .fi-auth-brand,
|
||||
.ifnex-login-card .fi-panels-auth-card-heading,
|
||||
.ifnex-login-card .fi-panels-auth-card-subheading {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Footer text */
|
||||
.ifnex-login-footer {
|
||||
font-family: 'Vazirmatn', sans-serif;
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
margin: 1.25rem 0 0;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
direction: ltr;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ---- Mobile responsive ---- */
|
||||
@media (max-width: 480px) {
|
||||
.ifnex-login-page {
|
||||
padding: 1rem;
|
||||
}
|
||||
.ifnex-login-card {
|
||||
max-width: 100%;
|
||||
padding: 2rem 1.5rem 1.5rem;
|
||||
border-radius: 14px;
|
||||
}
|
||||
.ifnex-login-logo {
|
||||
width: 100px;
|
||||
}
|
||||
.ifnex-login-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
.ifnex-login-subtitle {
|
||||
font-size: 12px;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
.ifnex-decor-globe,
|
||||
.ifnex-decor-plane {
|
||||
opacity: 0.04;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Respect reduced-motion users ---- */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ifnex-login-card {
|
||||
animation: none;
|
||||
}
|
||||
.ifnex-login-card .fi-btn:hover,
|
||||
.ifnex-login-card button[type="submit"]:hover {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
@ -1,52 +0,0 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="space-y-6">
|
||||
<x-filament::section>
|
||||
<x-slot:heading>
|
||||
<x-filament::icon icon="heroicon-o-swatch" class="h-5 w-5 text-amber-500" />
|
||||
طراحی مجدد ویجتهای داشبورد — پیشرو (پس از تحویل)
|
||||
</x-slot:heading>
|
||||
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-sm">
|
||||
این بخش برای فاز بعد از تحویل در نظر گرفته شده است. ویجتهای فعلی داشبورد عملکردی هستند
|
||||
و درست کار میکنند، ولی طراحی ظاهری نهایی نشده است.
|
||||
پس از تحویل پروژه به مشتری، ویجتهای زیر بازطراحی خواهند شد.
|
||||
</p>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section>
|
||||
<x-slot:heading>
|
||||
<x-filament::icon icon="heroicon-o-rectangle-group" class="h-5 w-5 text-gray-500" />
|
||||
ویجتهایی که بازطراحی خواهند شد
|
||||
</x-slot:heading>
|
||||
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li class="flex items-center gap-2">
|
||||
<x-filament::icon icon="heroicon-o-chevron-left" class="h-4 w-4 text-gray-400" />
|
||||
<span><strong>DashboardInfoWidget</strong> — آمار کلی مرسولات</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<x-filament::icon icon="heroicon-o-chevron-left" class="h-4 w-4 text-gray-400" />
|
||||
<span><strong>ExchangeRateWidget</strong> — نرخ ارز + نمودار + تعداد در انتظار تأیید</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<x-filament::icon icon="heroicon-o-chevron-left" class="h-4 w-4 text-gray-400" />
|
||||
<span><strong>WalletStats</strong> — آمار کیف پول</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<x-filament::icon icon="heroicon-o-chevron-left" class="h-4 w-4 text-gray-400" />
|
||||
<span><strong>TransactionChartWidget</strong> — نمودار تراکنشها</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<x-filament::icon icon="heroicon-o-chevron-left" class="h-4 w-4 text-gray-400" />
|
||||
<span><strong>FinanceOverviewWidget</strong> — نمای کلی مالی</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<x-filament::icon icon="heroicon-o-chevron-left" class="h-4 w-4 text-gray-400" />
|
||||
<span><strong>RecentTransactionsWidget</strong> — تراکنشهای اخیر</span>
|
||||
</li>
|
||||
</ul>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
@ -1,53 +1,70 @@
|
||||
{{--
|
||||
IFNEX — Layer 3 (left): Recent shipments table widget.
|
||||
Compact 5-row table with brand-aligned status badges + hover state.
|
||||
--}}
|
||||
<x-filament-widgets::widget>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<x-heroicon-o-truck class="w-5 h-5 text-amber-500" />
|
||||
<h3 class="font-bold text-gray-800 dark:text-white">آخرین سفارشات</h3>
|
||||
<div class="ifnex-card-shadow ifnex-card bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
|
||||
{{-- ──────── Header ──────── --}}
|
||||
<div class="px-5 py-4 border-b border-gray-100 dark:border-gray-700 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="inline-flex items-center justify-center w-9 h-9 rounded-xl bg-amber-100 dark:bg-amber-900/40 text-amber-600 dark:text-amber-400">
|
||||
<x-heroicon-o-truck class="w-5 h-5" />
|
||||
</span>
|
||||
<h3 class="text-base font-bold text-gray-900 dark:text-white">آخرین سفارشات</h3>
|
||||
</div>
|
||||
<a href="{{ route('filament.admin.resources.shipments.index') }}"
|
||||
class="text-xs text-amber-600 hover:text-amber-700 font-semibold">
|
||||
<a href="{{ $viewAllUrl ?? '#' }}"
|
||||
class="ifnex-link-inline text-xs font-semibold text-amber-600 hover:text-amber-700 dark:text-amber-400 dark:hover:text-amber-300 transition-colors">
|
||||
مشاهده همه ←
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- ──────── Body ──────── --}}
|
||||
@if($recentShipments->isEmpty())
|
||||
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<x-heroicon-o-inbox class="w-12 h-12 mx-auto mb-2 text-gray-300" />
|
||||
<p>هنوز سفارشی ثبت نشده است</p>
|
||||
<div class="p-10 text-center">
|
||||
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 dark:bg-gray-700/50 mb-3">
|
||||
<x-heroicon-o-inbox class="w-7 h-7 text-gray-400" />
|
||||
</div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">هنوز سفارشی ثبت نشده است</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-900 text-gray-600 dark:text-gray-400">
|
||||
<tr>
|
||||
<th class="text-right px-4 py-2 font-medium">AWB</th>
|
||||
<th class="text-right px-4 py-2 font-medium">مسیر</th>
|
||||
<th class="text-right px-4 py-2 font-medium">وضعیت</th>
|
||||
<th class="text-right px-4 py-2 font-medium">تاریخ</th>
|
||||
<th class="text-right px-4 py-2 font-medium">مبلغ</th>
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-900/40 text-gray-500 dark:text-gray-400 border-b border-gray-100 dark:border-gray-700">
|
||||
<th class="ifnex-th text-right">AWB</th>
|
||||
<th class="ifnex-th text-right">مسیر</th>
|
||||
<th class="ifnex-th text-right">وضعیت</th>
|
||||
<th class="ifnex-th text-right">مبلغ</th>
|
||||
<th class="ifnex-th text-right">تاریخ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
<tbody class="divide-y divide-gray-50 dark:divide-gray-700/60">
|
||||
@foreach($recentShipments as $shipment)
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-900/50">
|
||||
<td class="px-4 py-3 font-mono text-xs font-semibold text-gray-800 dark:text-white">
|
||||
{{ $shipment['awb'] }}
|
||||
<tr class="ifnex-table-row-hover transition-colors duration-150">
|
||||
<td class="ifnex-td">
|
||||
@if(!empty($shipment['url']))
|
||||
<a href="{{ $shipment['url'] }}" class="font-mono text-xs font-bold text-gray-900 dark:text-white hover:text-amber-600 dark:hover:text-amber-400 transition-colors">
|
||||
{{ $shipment['awb'] }}
|
||||
</a>
|
||||
@else
|
||||
<span class="font-mono text-xs font-bold text-gray-900 dark:text-white">{{ $shipment['awb'] }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
|
||||
<td class="ifnex-td text-xs text-gray-700 dark:text-gray-300">
|
||||
{{ $shipment['route'] }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="inline-block px-2 py-1 rounded-full text-xs font-semibold {{ $shipment['status_color'] }}">
|
||||
<td class="ifnex-td">
|
||||
<span class="ifnex-badge {{ $shipment['status_color'] }}">
|
||||
{{ $shipment['status_label'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $shipment['date'] }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<td class="ifnex-td text-xs font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ $shipment['amount'] }}
|
||||
</td>
|
||||
<td class="ifnex-td text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $shipment['date'] }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
@ -55,4 +72,4 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament-widgets::widget>
|
||||
</x-filament-widgets::widget>
|
||||
@ -0,0 +1,71 @@
|
||||
{{--
|
||||
IFNEX — Layer 3 (right): Recent transactions table widget.
|
||||
ساختار و استایل همسان با recent-shipments (کارت + هدر آیکوندار + بجهای برند).
|
||||
--}}
|
||||
<x-filament-widgets::widget>
|
||||
<div wire:poll.30s class="ifnex-card-shadow ifnex-card bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
|
||||
{{-- ──────── Header ──────── --}}
|
||||
<div class="px-5 py-4 border-b border-gray-100 dark:border-gray-700 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="inline-flex items-center justify-center w-9 h-9 rounded-xl bg-amber-100 dark:bg-amber-900/40 text-amber-600 dark:text-amber-400">
|
||||
<x-heroicon-o-banknotes class="w-5 h-5" />
|
||||
</span>
|
||||
<h3 class="text-base font-bold text-gray-900 dark:text-white">آخرین تراکنشها</h3>
|
||||
</div>
|
||||
<a href="{{ $viewAllUrl ?? '#' }}"
|
||||
class="ifnex-link-inline text-xs font-semibold text-amber-600 hover:text-amber-700 dark:text-amber-400 dark:hover:text-amber-300 transition-colors">
|
||||
مشاهده همه ←
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- ──────── Body ──────── --}}
|
||||
@if($transactions->isEmpty())
|
||||
<div class="p-10 text-center">
|
||||
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 dark:bg-gray-700/50 mb-3">
|
||||
<x-heroicon-o-inbox class="w-7 h-7 text-gray-400" />
|
||||
</div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">هنوز تراکنشی ثبت نشده است</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-900/40 text-gray-500 dark:text-gray-400 border-b border-gray-100 dark:border-gray-700">
|
||||
<th class="ifnex-th text-right">کاربر</th>
|
||||
<th class="ifnex-th text-right">مبلغ</th>
|
||||
<th class="ifnex-th text-right">نوع</th>
|
||||
<th class="ifnex-th text-right">وضعیت</th>
|
||||
<th class="ifnex-th text-right">تاریخ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-50 dark:divide-gray-700/60">
|
||||
@foreach($transactions as $transaction)
|
||||
<tr class="ifnex-table-row-hover transition-colors duration-150">
|
||||
<td class="ifnex-td text-xs text-gray-700 dark:text-gray-300">
|
||||
{{ $transaction['user'] }}
|
||||
</td>
|
||||
<td class="ifnex-td text-xs font-semibold {{ $transaction['amount_negative'] ? 'text-red-600 dark:text-red-400' : 'text-emerald-600 dark:text-emerald-400' }}">
|
||||
{{ $transaction['amount_negative'] ? '− ' : '+ ' }}{{ $transaction['amount'] }}
|
||||
</td>
|
||||
<td class="ifnex-td">
|
||||
<span class="ifnex-badge {{ $transaction['type_color'] }}">
|
||||
{{ $transaction['type_label'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="ifnex-td">
|
||||
<span class="ifnex-badge {{ $transaction['status_color'] }}">
|
||||
{{ $transaction['status_label'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="ifnex-td text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $transaction['date'] }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament-widgets::widget>
|
||||
3619
04_Laravel/storage/panel-dump.html
Normal file
3619
04_Laravel/storage/panel-dump.html
Normal file
File diff suppressed because it is too large
Load Diff
3296
04_Laravel/storage/panel-dump2.html
Normal file
3296
04_Laravel/storage/panel-dump2.html
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user