Commit 63cd6d25 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add reference number search to audit log (INV-/PAY- lookup)

Adds two new filters to the activity log:
- Reference search: searches inside JSON old_values/new_values
  for invoice numbers (INV-...) or payment references (PAY-...)
- Entity type filter: quickly narrow to invoices, payments, etc.

Uses PostgreSQL jsonb::text ILIKE for the deep JSON search.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 419462ef
...@@ -31,6 +31,12 @@ class ActivityLog extends Component ...@@ -31,6 +31,12 @@ class ActivityLog extends Component
#[Url] #[Url]
public string $userFilter = ''; public string $userFilter = '';
#[Url]
public string $entityFilter = '';
#[Url]
public string $referenceSearch = '';
public int $expandedRow = 0; public int $expandedRow = 0;
public function mount(): void public function mount(): void
...@@ -63,6 +69,16 @@ public function updatedUserFilter(): void ...@@ -63,6 +69,16 @@ public function updatedUserFilter(): void
$this->resetPage(); $this->resetPage();
} }
public function updatedEntityFilter(): void
{
$this->resetPage();
}
public function updatedReferenceSearch(): void
{
$this->resetPage();
}
public function toggleExpand(int $id): void public function toggleExpand(int $id): void
{ {
$this->expandedRow = $this->expandedRow === $id ? 0 : $id; $this->expandedRow = $this->expandedRow === $id ? 0 : $id;
...@@ -70,7 +86,7 @@ public function toggleExpand(int $id): void ...@@ -70,7 +86,7 @@ public function toggleExpand(int $id): void
public function resetFilters(): void public function resetFilters(): void
{ {
$this->reset(['search', 'actionFilter', 'dateFrom', 'dateTo', 'userFilter']); $this->reset(['search', 'actionFilter', 'dateFrom', 'dateTo', 'userFilter', 'entityFilter', 'referenceSearch']);
$this->resetPage(); $this->resetPage();
} }
...@@ -123,6 +139,29 @@ public function render() ...@@ -123,6 +139,29 @@ public function render()
->orWhere('name_ar', 'ilike', "%{$search}%")); ->orWhere('name_ar', 'ilike', "%{$search}%"));
}); });
}) })
->when($this->referenceSearch, function ($q) {
$ref = $this->referenceSearch;
$q->where(function ($q2) use ($ref) {
$q2->whereRaw("new_values::text ILIKE ?", ["%{$ref}%"])
->orWhereRaw("old_values::text ILIKE ?", ["%{$ref}%"]);
});
})
->when($this->entityFilter, function ($q) {
$entityMap = [
'invoice' => 'Invoice',
'payment' => 'Payment',
'transaction' => 'Transaction',
'enrollment' => 'Enrollment',
'participant' => 'Participant',
'session' => 'TrainingSession',
'group' => 'TrainingGroup',
'program' => 'TrainingProgram',
'user' => 'User',
'attendance' => 'AttendanceRecord',
];
$entity = $entityMap[$this->entityFilter] ?? $this->entityFilter;
$q->where('auditable_type', 'ilike', "%{$entity}%");
})
->when($this->actionFilter, fn ($q) => $q->where('action', $this->actionFilter)) ->when($this->actionFilter, fn ($q) => $q->where('action', $this->actionFilter))
->when($this->dateFrom, fn ($q) => $q->whereDate('created_at', '>=', $this->dateFrom)) ->when($this->dateFrom, fn ($q) => $q->whereDate('created_at', '>=', $this->dateFrom))
->when($this->dateTo, fn ($q) => $q->whereDate('created_at', '<=', $this->dateTo)) ->when($this->dateTo, fn ($q) => $q->whereDate('created_at', '<=', $this->dateTo))
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('سجل النشاطات') }}</h1> <h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('سجل النشاطات') }}</h1>
<p class="text-sm text-gray-500 mt-1">{{ __('متابعة جميع التغييرات والإجراءات في النظام') }}</p> <p class="text-sm text-gray-500 mt-1">{{ __('متابعة جميع التغييرات والإجراءات في النظام') }}</p>
</div> </div>
@if($search || $actionFilter || $dateFrom || $dateTo || $userFilter) @if($search || $actionFilter || $dateFrom || $dateTo || $userFilter || $entityFilter || $referenceSearch)
<button wire:click="resetFilters" class="inline-flex items-center justify-center gap-1.5 px-3 py-2.5 w-full sm:w-auto text-sm text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"> <button wire:click="resetFilters" class="inline-flex items-center justify-center gap-1.5 px-3 py-2.5 w-full sm:w-auto text-sm text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
...@@ -17,14 +17,32 @@ ...@@ -17,14 +17,32 @@
{{-- Filters --}} {{-- Filters --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-4"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4"> {{-- Row 1: Reference search + general search --}}
{{-- Search --}} <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
<div class="sm:col-span-2 lg:col-span-2"> {{-- Reference Number Search --}}
<label class="block text-xs font-medium text-gray-600 mb-1">{{ __('بحث') }}</label> <div>
<label class="block text-xs font-medium text-gray-600 mb-1">{{ __('بحث برقم مرجعي') }}</label>
<div class="relative">
<input type="text"
wire:model.live.debounce.300ms="referenceSearch"
placeholder="{{ __('رقم الفاتورة (INV-...) أو رقم الدفع (PAY-...)') }}"
class="w-full ps-10 pe-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm font-mono"
dir="ltr">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/>
</svg>
</div>
</div>
</div>
{{-- General Search --}}
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">{{ __('بحث عام') }}</label>
<div class="relative"> <div class="relative">
<input type="text" <input type="text"
wire:model.live.debounce.300ms="search" wire:model.live.debounce.300ms="search"
placeholder="{{ __('بحث بنوع الكيان أو اسم المستخدم أو IP...') }}" placeholder="{{ __('اسم المستخدم أو IP...') }}"
class="w-full ps-10 pe-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"> class="w-full ps-10 pe-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none"> <div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
...@@ -33,6 +51,28 @@ class="w-full ps-10 pe-4 py-2 border border-gray-300 rounded-lg focus:ring-2 foc ...@@ -33,6 +51,28 @@ class="w-full ps-10 pe-4 py-2 border border-gray-300 rounded-lg focus:ring-2 foc
</div> </div>
</div> </div>
</div> </div>
</div>
{{-- Row 2: Filters --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
{{-- Entity Filter --}}
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">{{ __('نوع الكيان') }}</label>
<select wire:model.live="entityFilter"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm">
<option value="">{{ __('كل الكيانات') }}</option>
<option value="invoice">{{ __('فاتورة') }}</option>
<option value="payment">{{ __('دفعة') }}</option>
<option value="transaction">{{ __('معاملة مالية') }}</option>
<option value="enrollment">{{ __('تسجيل') }}</option>
<option value="participant">{{ __('مشارك') }}</option>
<option value="session">{{ __('حصة تدريبية') }}</option>
<option value="group">{{ __('مجموعة') }}</option>
<option value="program">{{ __('برنامج') }}</option>
<option value="attendance">{{ __('حضور') }}</option>
<option value="user">{{ __('مستخدم') }}</option>
</select>
</div>
{{-- Action Filter --}} {{-- Action Filter --}}
<div> <div>
...@@ -60,7 +100,7 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin ...@@ -60,7 +100,7 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin
</div> </div>
{{-- Date Range --}} {{-- Date Range --}}
<div> <div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-600 mb-1">{{ __('الفترة') }}</label> <label class="block text-xs font-medium text-gray-600 mb-1">{{ __('الفترة') }}</label>
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<input type="date" wire:model.live="dateFrom" <input type="date" wire:model.live="dateFrom"
......
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