Commit 4de529eb authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix system coherence: RTL layout, DB column mismatches, permissions, global error handler

- Fix sidebar positioning (end-0 → start-0) so it appears on the RIGHT in RTL
- Fix main content margin (me-64 → ms-64) to offset sidebar correctly
- Fix TrainingProgram queries using non-existent is_active column → status
- Fix User queries using is_active → status
- Fix invoice type 'invoice' → 'standard' to match CHECK constraint
- Fix CollectPaymentWizard using balance_due → due_amount + billable morphs
- Fix notification template Blade parse error (unclosed parenthesis)
- Align 10+ sidebar permission checks with actual route middleware permissions
- Add missing permissions to seeder (pos.sell, inventory.list, reports.view, etc.)
- Add comprehensive global error handler with full stack trace, SQL log, request data
- Add Arabic error pages (500, 403, 404) with detailed debugging info
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 45c59e25
......@@ -137,7 +137,7 @@ public function save(BranchService $branchService): void
public function render()
{
return view('livewire.branches.branch-form', [
'managers' => User::where('is_active', true)->orderBy('name_ar')->get(['id', 'name', 'name_ar']),
'managers' => User::where('status', 'active')->orderBy('name_ar')->get(['id', 'name', 'name_ar']),
]);
}
}
......@@ -45,6 +45,8 @@ class FacilityForm extends Component
public function mount(?Facility $facility = null): void
{
$this->authorize($facility?->exists ? 'facilities.update' : 'facilities.create');
if ($facility && $facility->exists) {
$this->facility = $facility;
$this->editing = true;
......
......@@ -95,7 +95,7 @@ public function save(InvoiceService $service): void
$invoiceData = [
'academy_id' => auth()->user()->academy_id,
'number' => $service->generateNumber(auth()->user()->academy_id),
'type' => 'invoice',
'type' => 'standard',
'billable_type' => $this->participant_id ? Participant::class : null,
'billable_id' => $this->participant_id,
'contact_name' => $this->contact_name,
......
......@@ -286,7 +286,7 @@ public function newTransaction(): void
public function render()
{
$programs = TrainingProgram::where('is_active', true)->orderBy('name_ar')->get();
$programs = TrainingProgram::where('status', 'active')->orderBy('name_ar')->get();
return view('livewire.pos.pos-terminal', [
'programs' => $programs,
......
......@@ -82,7 +82,7 @@ public function selectInvoice(int $id): void
// Pre-fill the full balance due
$invoice = Invoice::find($id);
if ($invoice) {
$this->payment_amount_display = number_format($invoice->balance_due / 100, 2, '.', '');
$this->payment_amount_display = number_format($invoice->due_amount / 100, 2, '.', '');
}
}
......@@ -90,7 +90,7 @@ public function payFullAmount(): void
{
$invoice = Invoice::find($this->selected_invoice_id);
if ($invoice) {
$this->payment_amount_display = number_format($invoice->balance_due / 100, 2, '.', '');
$this->payment_amount_display = number_format($invoice->due_amount / 100, 2, '.', '');
}
}
......@@ -103,7 +103,7 @@ public function nextStep(): void
$invoice = Invoice::find($this->selected_invoice_id);
$amountPiasters = (int) round((float) $this->payment_amount_display * 100);
if ($invoice && $amountPiasters > $invoice->balance_due) {
if ($invoice && $amountPiasters > $invoice->due_amount) {
$this->addError('payment_amount_display', 'المبلغ أكبر من الرصيد المستحق');
return;
}
......@@ -132,7 +132,7 @@ public function confirm(PaymentService $service): void
// PaymentService::recordPayment() handles:
// - Creating payment record
// - Double-entry transactions (debit cash/card, credit AR)
// - Updating invoice.paid_amount and balance_due
// - Updating invoice.paid_amount and due_amount
// - Transitioning invoice status if fully paid
// - Updating cash session if cash payment
......@@ -177,13 +177,14 @@ public function render()
// Outstanding invoices for selected participant
$invoices = collect();
if ($this->selected_participant_id) {
$invoices = Invoice::where('participant_id', $this->selected_participant_id)
$invoices = Invoice::where('billable_type', \App\Domain\Participant\Models\Participant::class)
->where('billable_id', $this->selected_participant_id)
->whereIn('status', [
InvoiceStatus::Sent,
InvoiceStatus::PartiallyPaid,
InvoiceStatus::Overdue,
])
->where('balance_due', '>', 0)
->where('due_amount', '>', 0)
->orderByDesc('due_date')
->get();
}
......
......@@ -155,7 +155,7 @@ public function render()
->pluck('training_program_id');
$programs = TrainingProgram::where('activity_id', $this->selected_activity_id)
->where('is_active', true)
->where('status', 'active')
->whereNotIn('id', $enrolledProgramIds)
->orderBy('name_ar')
->get();
......
......@@ -175,7 +175,7 @@ public function render()
if ($this->selected_activity_id) {
$programs = TrainingProgram::where('activity_id', $this->selected_activity_id)
->where('is_active', true)
->where('status', 'active')
->orderBy('name_ar')
->get();
}
......
......@@ -4,6 +4,7 @@
use App\Domain\Identity\Services\PermissionService;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
......@@ -22,6 +23,8 @@ public function register(): void
*/
public function boot(): void
{
DB::enableQueryLog();
Gate::before(function ($user, $ability) {
return app(PermissionService::class)->can($user, $ability) ?: null;
});
......
......@@ -4,6 +4,10 @@
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
......@@ -26,6 +30,130 @@
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
fn (Request $request) => $request->is('api/*'),
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
);
$exceptions->respond(function (\Symfony\Component\HttpFoundation\Response $response, \Throwable $e, Request $request) {
$statusCode = $response->getStatusCode();
if ($request->is('api/*') || $request->expectsJson()) {
$errorId = Str::uuid()->toString();
Log::error('[' . $errorId . '] API Error', [
'error_id' => $errorId,
'exception' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'url' => $request->fullUrl(),
'method' => $request->method(),
'input' => $request->except(['password', 'password_confirmation', '_token']),
'user_id' => $request->user()?->id,
]);
return response()->json([
'error' => true,
'error_id' => $errorId,
'message' => $e->getMessage(),
'exception' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => collect($e->getTrace())->take(20)->map(fn ($frame) => [
'file' => $frame['file'] ?? null,
'line' => $frame['line'] ?? null,
'function' => ($frame['class'] ?? '') . ($frame['type'] ?? '') . ($frame['function'] ?? ''),
])->toArray(),
'url' => $request->fullUrl(),
'method' => $request->method(),
'input' => $request->except(['password', 'password_confirmation', '_token']),
'user_id' => $request->user()?->id,
'timestamp' => now()->toIso8601String(),
], $statusCode >= 400 ? $statusCode : 500);
}
if ($statusCode === 500 || (!$e instanceof HttpExceptionInterface && $statusCode >= 500)) {
$errorId = Str::uuid()->toString();
$queries = [];
try {
$queryLog = DB::getQueryLog();
$queries = collect($queryLog)->takeRight(10)->map(fn ($q) => [
'query' => $q['query'] ?? '',
'time' => $q['time'] ?? null,
])->toArray();
} catch (\Throwable $ignored) {
}
$trace = collect($e->getTrace())->take(30)->map(fn ($frame) => [
'file' => $frame['file'] ?? '(internal)',
'line' => $frame['line'] ?? null,
'class' => $frame['class'] ?? '',
'type' => $frame['type'] ?? '',
'function' => $frame['function'] ?? '',
])->toArray();
$inputData = '';
try {
$filtered = $request->except(['password', 'password_confirmation', '_token']);
$inputData = !empty($filtered) ? json_encode($filtered, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : '';
} catch (\Throwable $ignored) {
}
$headers = '';
try {
$headerBag = $request->headers->all();
unset($headerBag['cookie'], $headerBag['authorization']);
$headers = json_encode($headerBag, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
} catch (\Throwable $ignored) {
}
$sessionData = '';
try {
if ($request->hasSession()) {
$sess = $request->session()->all();
unset($sess['_token'], $sess['_previous'], $sess['_flash']);
$sessionData = !empty($sess) ? json_encode($sess, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : '';
}
} catch (\Throwable $ignored) {
}
Log::error('[' . $errorId . '] Unhandled Exception', [
'error_id' => $errorId,
'exception' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'url' => $request->fullUrl(),
'method' => $request->method(),
'route' => $request->route()?->getName(),
'input' => $request->except(['password', 'password_confirmation', '_token']),
'user_id' => $request->user()?->id,
'user_email' => $request->user()?->email,
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'trace' => $e->getTraceAsString(),
]);
return response()->view('errors.500', [
'errorId' => $errorId,
'exceptionClass' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'url' => $request->fullUrl(),
'method' => $request->method(),
'routeName' => $request->route()?->getName() ?? $request->route()?->uri(),
'userName' => $request->user()?->name_ar ?? $request->user()?->name,
'userId' => $request->user()?->id,
'ip' => $request->ip(),
'trace' => $trace,
'inputData' => $inputData,
'headers' => $headers,
'sessionData' => $sessionData,
'queries' => $queries,
], 500);
}
return $response;
});
})->create();
......@@ -100,10 +100,34 @@ public function run(): void
'approvals.list', 'approvals.approve', 'approvals.reject', 'approvals.configure',
// Settings
'settings.manage', 'settings.view_audit_log',
'settings.manage', 'settings.view', 'settings.view_audit_log',
// Evaluations
'evaluations.list', 'evaluations.create', 'evaluations.update',
'evaluations.list', 'evaluations.create', 'evaluations.update', 'evaluations.manage',
// Audit
'audit.list', 'audit.export',
// POS (route-aligned aliases)
'pos.sell', 'pos.list',
// Notifications (route-aligned)
'notifications.manage',
// Cash Sessions (route-aligned)
'cash_sessions.manage',
// Wallets (route-aligned)
'wallets.view',
// Inventory (route-aligned)
'inventory.list', 'inventory.create', 'inventory.update',
// Reports (route-aligned)
'reports.view',
// Super Admin
'super_admin.access',
];
// Insert permissions (idempotent)
......
......@@ -12,9 +12,9 @@
['label' => 'الأنشطة', 'route' => 'activities.list', 'icon' => 'bolt', 'permission' => 'activities.list'],
['label' => 'البرامج', 'route' => 'programs.list', 'icon' => 'academic-cap', 'permission' => 'programs.list'],
['label' => 'المجموعات', 'route' => 'groups.list', 'icon' => 'user-group', 'permission' => 'groups.list'],
['label' => 'التسجيلات', 'route' => 'enrollments.list', 'icon' => 'clipboard-check', 'permission' => 'participants.list'],
['label' => 'التقييمات', 'route' => 'evaluations.list', 'icon' => 'chart-bar', 'permission' => 'participants.list'],
['label' => 'التعيينات', 'route' => 'assignments.list', 'icon' => 'calendar', 'permission' => 'participants.list'],
['label' => 'التسجيلات', 'route' => 'enrollments.list', 'icon' => 'clipboard-check', 'permission' => 'enrollments.list'],
['label' => 'التقييمات', 'route' => 'evaluations.list', 'icon' => 'chart-bar', 'permission' => 'evaluations.list'],
['label' => 'التعيينات', 'route' => 'assignments.list', 'icon' => 'calendar', 'permission' => 'assignments.list'],
]],
['section' => 'الحضور', 'items' => [
......@@ -24,25 +24,25 @@
['section' => 'المالية', 'items' => [
['label' => 'الفواتير', 'route' => 'invoices.list', 'icon' => 'document', 'permission' => 'invoices.list'],
['label' => 'المحافظ', 'route' => 'wallets.list', 'icon' => 'wallet', 'permission' => 'wallets.list'],
['label' => 'جلسات الكاشير', 'route' => 'cash-sessions.list', 'icon' => 'calculator', 'permission' => 'invoices.list'],
['label' => 'جلسات الكاشير', 'route' => 'cash-sessions.list', 'icon' => 'calculator', 'permission' => 'cash_sessions.list'],
]],
['section' => 'نقطة البيع', 'items' => [
['label' => 'نقطة البيع', 'route' => 'pos.terminal', 'icon' => 'shopping-cart', 'permission' => 'pos.access'],
['label' => 'سجل المبيعات', 'route' => 'pos.history', 'icon' => 'clock', 'permission' => 'pos.access'],
['label' => 'نقطة البيع', 'route' => 'pos.terminal', 'icon' => 'shopping-cart', 'permission' => 'pos.sell'],
['label' => 'سجل المبيعات', 'route' => 'pos.history', 'icon' => 'clock', 'permission' => 'pos.list'],
]],
['section' => 'التسعير', 'items' => [
['label' => 'الأسعار الأساسية', 'route' => 'pricing.base-prices', 'icon' => 'tag', 'permission' => 'pricing_rules.list'],
['label' => 'قواعد التسعير', 'route' => 'pricing.rules', 'icon' => 'adjustments-horizontal', 'permission' => 'pricing_rules.list'],
['label' => 'العروض والكوبونات', 'route' => 'pricing.promotions', 'icon' => 'gift', 'permission' => 'pricing_rules.list'],
['label' => 'الأسعار الأساسية', 'route' => 'pricing.base-prices', 'icon' => 'tag', 'permission' => 'pricing.list'],
['label' => 'قواعد التسعير', 'route' => 'pricing.rules', 'icon' => 'adjustments-horizontal', 'permission' => 'pricing.list'],
['label' => 'العروض والكوبونات', 'route' => 'pricing.promotions', 'icon' => 'gift', 'permission' => 'pricing.list'],
]],
['section' => 'المخزون', 'items' => [
['label' => 'المنتجات', 'route' => 'inventory.products', 'icon' => 'cube', 'permission' => 'products.list'],
['label' => 'المستودعات', 'route' => 'inventory.warehouses', 'icon' => 'building-storefront', 'permission' => 'products.list'],
['label' => 'الحركات', 'route' => 'inventory.movements', 'icon' => 'truck', 'permission' => 'products.list'],
['label' => 'التسويات', 'route' => 'inventory.adjustments', 'icon' => 'clipboard-document-list', 'permission' => 'products.list'],
['label' => 'المنتجات', 'route' => 'inventory.products', 'icon' => 'cube', 'permission' => 'inventory.list'],
['label' => 'المستودعات', 'route' => 'inventory.warehouses', 'icon' => 'building-storefront', 'permission' => 'inventory.list'],
['label' => 'الحركات', 'route' => 'inventory.movements', 'icon' => 'truck', 'permission' => 'inventory.list'],
['label' => 'التسويات', 'route' => 'inventory.adjustments', 'icon' => 'clipboard-document-list', 'permission' => 'inventory.adjust'],
]],
['section' => 'المنشآت', 'items' => [
......@@ -50,17 +50,17 @@
]],
['section' => 'الإشعارات', 'items' => [
['label' => 'مركز الإشعارات', 'route' => 'notifications.center', 'icon' => 'bolt', 'permission' => 'settings.manage'],
['label' => 'القوالب', 'route' => 'notifications.templates', 'icon' => 'document-text', 'permission' => 'settings.manage'],
['label' => 'سجل الإرسال', 'route' => 'notifications.logs', 'icon' => 'eye', 'permission' => 'settings.manage'],
['label' => 'مركز الإشعارات', 'route' => 'notifications.center', 'icon' => 'bolt', 'permission' => 'dashboard.view'],
['label' => 'القوالب', 'route' => 'notifications.templates', 'icon' => 'document-text', 'permission' => 'notifications.manage'],
['label' => 'سجل الإرسال', 'route' => 'notifications.logs', 'icon' => 'eye', 'permission' => 'notifications.manage'],
]],
['section' => 'الإدارة', 'items' => [
['label' => 'التقارير', 'route' => 'reports.view', 'icon' => 'chart-bar', 'permission' => 'reports.financial'],
['label' => 'التقارير', 'route' => 'reports.view', 'icon' => 'chart-bar', 'permission' => 'reports.view'],
['label' => 'المستخدمين', 'route' => 'users.list', 'icon' => 'users', 'permission' => 'users.list'],
['label' => 'الأدوار', 'route' => 'roles.list', 'icon' => 'shield-check', 'permission' => 'roles.list'],
['label' => 'الفروع', 'route' => 'branches.list', 'icon' => 'building-office', 'permission' => 'settings.manage'],
['label' => 'سجل المراجعة', 'route' => 'audit.list', 'icon' => 'eye', 'permission' => 'settings.view_audit_log'],
['label' => 'سجل المراجعة', 'route' => 'audit.list', 'icon' => 'eye', 'permission' => 'audit.list'],
['label' => 'إعدادات الأكاديمية', 'route' => 'settings.academy', 'icon' => 'cog-6-tooth', 'permission' => 'settings.manage'],
['label' => 'إعدادات النظام', 'route' => 'settings.system', 'icon' => 'cog-6-tooth', 'permission' => 'settings.manage'],
['label' => 'معالج الإعداد', 'route' => 'setup.wizard', 'icon' => 'bolt', 'permission' => 'settings.manage'],
......@@ -110,7 +110,7 @@
};
@endphp
<aside dir="rtl" class="fixed top-0 end-0 h-screen w-64 bg-slate-900 text-white flex flex-col z-40 overflow-hidden">
<aside dir="rtl" class="fixed top-0 start-0 h-screen w-64 bg-slate-900 text-white flex flex-col z-40 overflow-hidden">
{{-- Logo --}}
<div class="flex items-center justify-center h-16 border-b border-slate-700 px-4 shrink-0">
<h1 class="text-xl font-bold text-white">الكابتن</h1>
......
<!DOCTYPE html>
<html dir="rtl" lang="ar" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>غير مصرح</title>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>body { font-family: 'Cairo', sans-serif; }</style>
</head>
<body class="h-full bg-gray-50 flex items-center justify-center">
<div class="text-center p-8">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-orange-100 mb-6">
<svg class="w-10 h-10 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>
</div>
<h1 class="text-4xl font-bold text-gray-800 mb-2">403</h1>
<p class="text-xl text-gray-600 mb-6">غير مصرح لك بالوصول لهذه الصفحة</p>
<div class="flex items-center justify-center gap-4">
<a href="{{ url()->previous() }}" class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 text-sm font-medium">
العودة
</a>
<a href="{{ url('/') }}" class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium">
الصفحة الرئيسية
</a>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html dir="rtl" lang="ar" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>الصفحة غير موجودة</title>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>body { font-family: 'Cairo', sans-serif; }</style>
</head>
<body class="h-full bg-gray-50 flex items-center justify-center">
<div class="text-center p-8">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-blue-100 mb-6">
<svg class="w-10 h-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<h1 class="text-4xl font-bold text-gray-800 mb-2">404</h1>
<p class="text-xl text-gray-600 mb-6">الصفحة المطلوبة غير موجودة</p>
<div class="flex items-center justify-center gap-4">
<a href="{{ url()->previous() }}" class="inline-flex items-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 text-sm font-medium">
العودة
</a>
<a href="{{ url('/') }}" class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium">
الصفحة الرئيسية
</a>
</div>
</div>
</body>
</html>
This diff is collapsed.
......@@ -27,7 +27,7 @@ class="fixed inset-0 z-30 bg-gray-600/75 lg:hidden" @click="sidebarOpen = false"
@include('components.layouts.sidebar')
<!-- Main content area -->
<div class="lg:me-64">
<div class="lg:ms-64">
<!-- Topbar -->
@include('components.layouts.topbar')
......
......@@ -63,7 +63,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin
<p class="text-xs font-medium text-blue-700 mb-1">{{ __('المتغيرات المتاحة لهذا الحدث:') }}</p>
<div class="flex flex-wrap gap-2">
@foreach($this->availableVariables as $var)
<code class="px-2 py-0.5 bg-blue-100 text-blue-800 rounded text-xs" dir="ltr">@{{ '{{' . $var . '}}' }}</code>
<code class="px-2 py-0.5 bg-blue-100 text-blue-800 rounded text-xs" dir="ltr">{{ '{{' . $var . '}}' }}</code>
@endforeach
</div>
</div>
......
......@@ -186,7 +186,7 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-amber-600 text-white
</div>
<div class="text-end">
<p class="text-lg font-bold text-gray-800" dir="ltr">
{{ number_format($invoice->balance_due / 100, 2) }} {{ __('ج.م') }}
{{ number_format($invoice->due_amount / 100, 2) }} {{ __('ج.م') }}
</p>
@if($invoice->paid_amount > 0)
<p class="text-xs text-green-600">
......@@ -254,7 +254,7 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-amber-600 text-white
<div class="flex items-center justify-between">
<span class="text-gray-600">{{ __('الرصيد المستحق') }}</span>
<span class="text-xl font-bold text-gray-800" dir="ltr">
{{ number_format($selectedInvoice->balance_due / 100, 2) }} {{ __('ج.م') }}
{{ number_format($selectedInvoice->due_amount / 100, 2) }} {{ __('ج.م') }}
</span>
</div>
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment