complete story 15.2
This commit is contained in:
parent
959cc0e717
commit
b2b287e156
|
|
@ -189,19 +189,37 @@ Follow the existing pattern from `admin/clients/individual/index.blade.php`:
|
||||||
|
|
||||||
## Dev Checklist
|
## Dev Checklist
|
||||||
|
|
||||||
- [ ] Add route for potential clients index
|
- [x] Add route for potential clients index
|
||||||
- [ ] Create Volt component `admin/potential-clients/index.blade.php`
|
- [x] Create Volt component `admin/potential-clients/index.blade.php`
|
||||||
- [ ] Implement search functionality with debounce
|
- [x] Implement search functionality with debounce
|
||||||
- [ ] Implement type filter dropdown
|
- [x] Implement type filter dropdown
|
||||||
- [ ] Implement pagination with per-page selector
|
- [x] Implement pagination with per-page selector
|
||||||
- [ ] Add navigation item to admin sidebar
|
- [x] Add navigation item to admin sidebar
|
||||||
- [ ] Style type badges with correct colors
|
- [x] Style type badges with correct colors
|
||||||
- [ ] Handle empty state (no data)
|
- [x] Handle empty state (no data)
|
||||||
- [ ] Handle empty state (no filter matches)
|
- [x] Handle empty state (no filter matches)
|
||||||
- [ ] Add English translations
|
- [x] Add English translations
|
||||||
- [ ] Add Arabic translations
|
- [x] Add Arabic translations
|
||||||
- [ ] Test RTL layout
|
- [x] Test RTL layout
|
||||||
- [ ] Write feature tests for list and filtering
|
- [x] Write feature tests for list and filtering
|
||||||
|
|
||||||
|
## File List
|
||||||
|
|
||||||
|
### Created
|
||||||
|
- `resources/views/livewire/admin/potential-clients/index.blade.php` - Volt component for list page
|
||||||
|
- `tests/Feature/Admin/PotentialClientListTest.php` - Feature tests for list and filtering
|
||||||
|
|
||||||
|
### Modified
|
||||||
|
- `routes/web.php` - Added potential clients route
|
||||||
|
- `resources/views/components/layouts/app/sidebar.blade.php` - Added navigation item
|
||||||
|
- `lang/en/potential-clients.php` - Added list-related translations
|
||||||
|
- `lang/ar/potential-clients.php` - Added list-related translations
|
||||||
|
- `lang/en/navigation.php` - Added potential_clients key
|
||||||
|
- `lang/ar/navigation.php` - Added potential_clients key
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Ready for Review
|
||||||
|
|
||||||
## Estimation
|
## Estimation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ return [
|
||||||
'clients' => 'العملاء',
|
'clients' => 'العملاء',
|
||||||
'individual_clients' => 'العملاء الأفراد',
|
'individual_clients' => 'العملاء الأفراد',
|
||||||
'company_clients' => 'الشركات العملاء',
|
'company_clients' => 'الشركات العملاء',
|
||||||
|
'potential_clients' => 'العملاء المحتملون',
|
||||||
'bookings' => 'الحجوزات',
|
'bookings' => 'الحجوزات',
|
||||||
'pending_bookings' => 'الحجوزات المعلقة',
|
'pending_bookings' => 'الحجوزات المعلقة',
|
||||||
'all_consultations' => 'جميع الاستشارات',
|
'all_consultations' => 'جميع الاستشارات',
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,19 @@
|
||||||
return [
|
return [
|
||||||
'title' => 'العملاء المحتملون',
|
'title' => 'العملاء المحتملون',
|
||||||
'singular' => 'عميل محتمل',
|
'singular' => 'عميل محتمل',
|
||||||
|
'subtitle' => 'إدارة العملاء المحتملين',
|
||||||
|
'add_potential_client' => 'إضافة عميل محتمل',
|
||||||
|
'all_types' => 'جميع الأنواع',
|
||||||
|
'search_placeholder' => 'البحث بالاسم أو البريد أو الهاتف...',
|
||||||
|
'no_potential_clients_found' => 'لم يتم العثور على عملاء محتملون',
|
||||||
|
'no_potential_clients_match' => 'لا يوجد عملاء محتملون يطابقون الفلاتر',
|
||||||
|
'clear_filters' => 'مسح الفلاتر',
|
||||||
|
'per_page' => 'لكل صفحة',
|
||||||
|
'actions' => 'الإجراءات',
|
||||||
|
'view' => 'عرض',
|
||||||
|
'edit' => 'تعديل',
|
||||||
|
'delete' => 'حذف',
|
||||||
|
'created_at' => 'تاريخ الإنشاء',
|
||||||
'types' => [
|
'types' => [
|
||||||
'individual' => 'فرد',
|
'individual' => 'فرد',
|
||||||
'company' => 'شركة',
|
'company' => 'شركة',
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ return [
|
||||||
'clients' => 'Clients',
|
'clients' => 'Clients',
|
||||||
'individual_clients' => 'Individual Clients',
|
'individual_clients' => 'Individual Clients',
|
||||||
'company_clients' => 'Company Clients',
|
'company_clients' => 'Company Clients',
|
||||||
|
'potential_clients' => 'Potential Clients',
|
||||||
'bookings' => 'Bookings',
|
'bookings' => 'Bookings',
|
||||||
'pending_bookings' => 'Pending Bookings',
|
'pending_bookings' => 'Pending Bookings',
|
||||||
'all_consultations' => 'All Consultations',
|
'all_consultations' => 'All Consultations',
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,19 @@
|
||||||
return [
|
return [
|
||||||
'title' => 'Potential Clients',
|
'title' => 'Potential Clients',
|
||||||
'singular' => 'Potential Client',
|
'singular' => 'Potential Client',
|
||||||
|
'subtitle' => 'Manage prospective clients',
|
||||||
|
'add_potential_client' => 'Add Potential Client',
|
||||||
|
'all_types' => 'All Types',
|
||||||
|
'search_placeholder' => 'Search by name, email, or phone...',
|
||||||
|
'no_potential_clients_found' => 'No potential clients found',
|
||||||
|
'no_potential_clients_match' => 'No potential clients match your filters',
|
||||||
|
'clear_filters' => 'Clear filters',
|
||||||
|
'per_page' => 'per page',
|
||||||
|
'actions' => 'Actions',
|
||||||
|
'view' => 'View',
|
||||||
|
'edit' => 'Edit',
|
||||||
|
'delete' => 'Delete',
|
||||||
|
'created_at' => 'Created',
|
||||||
'types' => [
|
'types' => [
|
||||||
'individual' => 'Individual',
|
'individual' => 'Individual',
|
||||||
'company' => 'Company',
|
'company' => 'Company',
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,14 @@
|
||||||
>
|
>
|
||||||
{{ __('navigation.company_clients') }}
|
{{ __('navigation.company_clients') }}
|
||||||
</flux:navlist.item>
|
</flux:navlist.item>
|
||||||
|
<flux:navlist.item
|
||||||
|
icon="user-plus"
|
||||||
|
:href="route('admin.potential-clients.index')"
|
||||||
|
:current="request()->routeIs('admin.potential-clients.*')"
|
||||||
|
wire:navigate
|
||||||
|
>
|
||||||
|
{{ __('navigation.potential_clients') }}
|
||||||
|
</flux:navlist.item>
|
||||||
</flux:navlist.group>
|
</flux:navlist.group>
|
||||||
|
|
||||||
<flux:navlist.group :heading="__('navigation.case_management')" class="grid">
|
<flux:navlist.group :heading="__('navigation.case_management')" class="grid">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\PotentialClientType;
|
||||||
|
use App\Models\PotentialClient;
|
||||||
|
use Livewire\Volt\Component;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
|
new class extends Component {
|
||||||
|
use WithPagination;
|
||||||
|
|
||||||
|
public string $search = '';
|
||||||
|
public string $typeFilter = '';
|
||||||
|
public int $perPage = 10;
|
||||||
|
|
||||||
|
public function updatedSearch(): void
|
||||||
|
{
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedTypeFilter(): void
|
||||||
|
{
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedPerPage(): void
|
||||||
|
{
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearFilters(): void
|
||||||
|
{
|
||||||
|
$this->search = '';
|
||||||
|
$this->typeFilter = '';
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function with(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'potentialClients' => PotentialClient::query()
|
||||||
|
->when($this->search, fn ($q) => $q->where(function ($q) {
|
||||||
|
$q->where('name', 'like', "%{$this->search}%")
|
||||||
|
->orWhere('email', 'like', "%{$this->search}%")
|
||||||
|
->orWhere('phone', 'like', "%{$this->search}%");
|
||||||
|
}))
|
||||||
|
->when($this->typeFilter, fn ($q) => $q->where('type', $this->typeFilter))
|
||||||
|
->latest()
|
||||||
|
->paginate($this->perPage),
|
||||||
|
'types' => PotentialClientType::cases(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}; ?>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="page-header mb-6">
|
||||||
|
<div>
|
||||||
|
<flux:heading size="xl" class="text-xl sm:text-2xl">{{ __('potential-clients.title') }}</flux:heading>
|
||||||
|
<flux:text class="mt-1 text-zinc-500">{{ __('potential-clients.subtitle') }}</flux:text>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<flux:button variant="primary" href="#" icon="plus" class="w-full sm:w-auto justify-center">
|
||||||
|
{{ __('potential-clients.add_potential_client') }}
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6 rounded-lg border border-zinc-200 bg-white p-4">
|
||||||
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-end">
|
||||||
|
<div class="flex-1">
|
||||||
|
<flux:input
|
||||||
|
wire:model.live.debounce.300ms="search"
|
||||||
|
:placeholder="__('potential-clients.search_placeholder')"
|
||||||
|
icon="magnifying-glass"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full sm:w-48">
|
||||||
|
<flux:select wire:model.live="typeFilter">
|
||||||
|
<flux:select.option value="">{{ __('potential-clients.all_types') }}</flux:select.option>
|
||||||
|
@foreach ($types as $type)
|
||||||
|
<flux:select.option value="{{ $type->value }}">
|
||||||
|
{{ $type->label() }}
|
||||||
|
</flux:select.option>
|
||||||
|
@endforeach
|
||||||
|
</flux:select>
|
||||||
|
</div>
|
||||||
|
<div class="w-full sm:w-32">
|
||||||
|
<flux:select wire:model.live="perPage">
|
||||||
|
<flux:select.option value="10">10 {{ __('potential-clients.per_page') }}</flux:select.option>
|
||||||
|
<flux:select.option value="25">25 {{ __('potential-clients.per_page') }}</flux:select.option>
|
||||||
|
<flux:select.option value="50">50 {{ __('potential-clients.per_page') }}</flux:select.option>
|
||||||
|
</flux:select>
|
||||||
|
</div>
|
||||||
|
@if ($search || $typeFilter)
|
||||||
|
<flux:button wire:click="clearFilters" variant="ghost" icon="x-mark">
|
||||||
|
{{ __('potential-clients.clear_filters') }}
|
||||||
|
</flux:button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-hidden rounded-lg border border-zinc-200 bg-white">
|
||||||
|
<div class="table-scroll-wrapper">
|
||||||
|
<table class="min-w-full divide-y divide-zinc-200">
|
||||||
|
<thead class="bg-zinc-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-start text-xs font-medium uppercase tracking-wider text-zinc-500">
|
||||||
|
{{ __('potential-clients.fields.name') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-start text-xs font-medium uppercase tracking-wider text-zinc-500">
|
||||||
|
{{ __('potential-clients.fields.type') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-start text-xs font-medium uppercase tracking-wider text-zinc-500">
|
||||||
|
{{ __('potential-clients.fields.email') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-start text-xs font-medium uppercase tracking-wider text-zinc-500">
|
||||||
|
{{ __('potential-clients.fields.phone') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-start text-xs font-medium uppercase tracking-wider text-zinc-500">
|
||||||
|
{{ __('potential-clients.created_at') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-end text-xs font-medium uppercase tracking-wider text-zinc-500">
|
||||||
|
{{ __('potential-clients.actions') }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-zinc-200 bg-white">
|
||||||
|
@forelse ($potentialClients as $potentialClient)
|
||||||
|
<tr wire:key="potential-client-{{ $potentialClient->id }}">
|
||||||
|
<td class="whitespace-nowrap px-6 py-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<flux:avatar size="sm" :name="$potentialClient->name ?? '?'" />
|
||||||
|
<span class="font-medium text-zinc-900">{{ $potentialClient->name ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-6 py-4">
|
||||||
|
@switch($potentialClient->type)
|
||||||
|
@case(PotentialClientType::Individual)
|
||||||
|
<flux:badge color="blue" size="sm">{{ $potentialClient->type->label() }}</flux:badge>
|
||||||
|
@break
|
||||||
|
@case(PotentialClientType::Company)
|
||||||
|
<flux:badge color="purple" size="sm">{{ $potentialClient->type->label() }}</flux:badge>
|
||||||
|
@break
|
||||||
|
@case(PotentialClientType::Agency)
|
||||||
|
<flux:badge color="amber" size="sm">{{ $potentialClient->type->label() }}</flux:badge>
|
||||||
|
@break
|
||||||
|
@endswitch
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-6 py-4 text-zinc-600">
|
||||||
|
{{ $potentialClient->email ?? '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-6 py-4 text-zinc-600">
|
||||||
|
{{ $potentialClient->phone ?? '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-6 py-4 text-zinc-600">
|
||||||
|
{{ $potentialClient->created_at->format('Y-m-d') }}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-6 py-4 text-end">
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<flux:button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon="eye"
|
||||||
|
href="#"
|
||||||
|
:title="__('potential-clients.view')"
|
||||||
|
/>
|
||||||
|
<flux:button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon="pencil"
|
||||||
|
href="#"
|
||||||
|
:title="__('potential-clients.edit')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="px-6 py-12 text-center">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<flux:icon name="user-group" class="mb-4 h-12 w-12 text-zinc-400" />
|
||||||
|
<flux:text class="text-zinc-500">
|
||||||
|
@if ($search || $typeFilter)
|
||||||
|
{{ __('potential-clients.no_potential_clients_match') }}
|
||||||
|
@else
|
||||||
|
{{ __('potential-clients.no_potential_clients_found') }}
|
||||||
|
@endif
|
||||||
|
</flux:text>
|
||||||
|
@if ($search || $typeFilter)
|
||||||
|
<flux:button wire:click="clearFilters" variant="ghost" class="mt-4">
|
||||||
|
{{ __('potential-clients.clear_filters') }}
|
||||||
|
</flux:button>
|
||||||
|
@else
|
||||||
|
<flux:button variant="primary" href="#" class="mt-4">
|
||||||
|
{{ __('potential-clients.add_potential_client') }}
|
||||||
|
</flux:button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($potentialClients->hasPages())
|
||||||
|
<div class="border-t border-zinc-200 bg-zinc-50 px-6 py-4">
|
||||||
|
{{ $potentialClients->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -92,6 +92,11 @@ Route::middleware(['auth', 'active'])->group(function () {
|
||||||
Volt::route('/blocked-times', 'admin.settings.blocked-times')->name('blocked-times');
|
Volt::route('/blocked-times', 'admin.settings.blocked-times')->name('blocked-times');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Potential Clients Management
|
||||||
|
Route::prefix('potential-clients')->name('admin.potential-clients.')->group(function () {
|
||||||
|
Volt::route('/', 'admin.potential-clients.index')->name('index');
|
||||||
|
});
|
||||||
|
|
||||||
// Legal Pages Management
|
// Legal Pages Management
|
||||||
Route::prefix('pages')->name('admin.pages.')->group(function () {
|
Route::prefix('pages')->name('admin.pages.')->group(function () {
|
||||||
Volt::route('/', 'admin.pages.index')->name('index');
|
Volt::route('/', 'admin.pages.index')->name('index');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,283 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\PotentialClient;
|
||||||
|
use App\Models\User;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->admin = User::factory()->admin()->create();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Access Tests
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
test('admin can access potential clients index page', function () {
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->get(route('admin.potential-clients.index'))
|
||||||
|
->assertOk();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-admin cannot access potential clients index page', function () {
|
||||||
|
$client = User::factory()->individual()->create();
|
||||||
|
|
||||||
|
$this->actingAs($client)
|
||||||
|
->get(route('admin.potential-clients.index'))
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unauthenticated user cannot access potential clients index page', function () {
|
||||||
|
$this->get(route('admin.potential-clients.index'))
|
||||||
|
->assertRedirect(route('login'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// List Display Tests
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
test('index page displays potential clients', function () {
|
||||||
|
$potentialClient = PotentialClient::factory()->create(['name' => 'Test Potential']);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->assertSee('Test Potential');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('potential clients sorted by created_at desc by default', function () {
|
||||||
|
$oldClient = PotentialClient::factory()->create([
|
||||||
|
'name' => 'Old Client',
|
||||||
|
'created_at' => now()->subDays(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newClient = PotentialClient::factory()->create([
|
||||||
|
'name' => 'New Client',
|
||||||
|
'created_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->assertSeeInOrder(['New Client', 'Old Client']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays type badge for individual potential client', function () {
|
||||||
|
PotentialClient::factory()->individual()->create(['name' => 'Individual Test']);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->assertSee('Individual Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays type badge for company potential client', function () {
|
||||||
|
PotentialClient::factory()->company()->create(['name' => 'Company Test']);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->assertSee('Company Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays type badge for agency potential client', function () {
|
||||||
|
PotentialClient::factory()->agency()->create(['name' => 'Agency Test']);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->assertSee('Agency Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Search Tests
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
test('can search potential clients by name', function () {
|
||||||
|
PotentialClient::factory()->create(['name' => 'John Doe']);
|
||||||
|
PotentialClient::factory()->create(['name' => 'Jane Smith']);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->set('search', 'John')
|
||||||
|
->assertSee('John Doe')
|
||||||
|
->assertDontSee('Jane Smith');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can search potential clients by email', function () {
|
||||||
|
PotentialClient::factory()->create([
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
]);
|
||||||
|
PotentialClient::factory()->create([
|
||||||
|
'name' => 'Jane Smith',
|
||||||
|
'email' => 'jane@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->set('search', 'john@')
|
||||||
|
->assertSee('John Doe')
|
||||||
|
->assertDontSee('Jane Smith');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can search potential clients by phone', function () {
|
||||||
|
PotentialClient::factory()->create([
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'phone' => '+970599111111',
|
||||||
|
]);
|
||||||
|
PotentialClient::factory()->create([
|
||||||
|
'name' => 'Jane Smith',
|
||||||
|
'phone' => '+970599222222',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->set('search', '111111')
|
||||||
|
->assertSee('John Doe')
|
||||||
|
->assertDontSee('Jane Smith');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Type Filter Tests
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
test('can filter potential clients by individual type', function () {
|
||||||
|
PotentialClient::factory()->individual()->create(['name' => 'Individual Client']);
|
||||||
|
PotentialClient::factory()->company()->create(['name' => 'Company Client']);
|
||||||
|
PotentialClient::factory()->agency()->create(['name' => 'Agency Client']);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->set('typeFilter', 'individual')
|
||||||
|
->assertSee('Individual Client')
|
||||||
|
->assertDontSee('Company Client')
|
||||||
|
->assertDontSee('Agency Client');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can filter potential clients by company type', function () {
|
||||||
|
PotentialClient::factory()->individual()->create(['name' => 'Individual Client']);
|
||||||
|
PotentialClient::factory()->company()->create(['name' => 'Company Client']);
|
||||||
|
PotentialClient::factory()->agency()->create(['name' => 'Agency Client']);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->set('typeFilter', 'company')
|
||||||
|
->assertDontSee('Individual Client')
|
||||||
|
->assertSee('Company Client')
|
||||||
|
->assertDontSee('Agency Client');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can filter potential clients by agency type', function () {
|
||||||
|
PotentialClient::factory()->individual()->create(['name' => 'Individual Client']);
|
||||||
|
PotentialClient::factory()->company()->create(['name' => 'Company Client']);
|
||||||
|
PotentialClient::factory()->agency()->create(['name' => 'Agency Client']);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->set('typeFilter', 'agency')
|
||||||
|
->assertDontSee('Individual Client')
|
||||||
|
->assertDontSee('Company Client')
|
||||||
|
->assertSee('Agency Client');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Clear Filters Tests
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
test('clear filters resets search and type filter', function () {
|
||||||
|
PotentialClient::factory()->create(['name' => 'Test Client']);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
$component = Volt::test('admin.potential-clients.index')
|
||||||
|
->set('search', 'something')
|
||||||
|
->set('typeFilter', 'individual')
|
||||||
|
->call('clearFilters');
|
||||||
|
|
||||||
|
expect($component->get('search'))->toBe('');
|
||||||
|
expect($component->get('typeFilter'))->toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Pagination Tests
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
test('can change per page value', function () {
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
$component = Volt::test('admin.potential-clients.index')
|
||||||
|
->set('perPage', 25);
|
||||||
|
|
||||||
|
expect($component->get('perPage'))->toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changing per page resets to first page', function () {
|
||||||
|
PotentialClient::factory()->count(15)->create();
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
$component = Volt::test('admin.potential-clients.index')
|
||||||
|
->set('perPage', 10)
|
||||||
|
->call('gotoPage', 2)
|
||||||
|
->set('perPage', 25);
|
||||||
|
|
||||||
|
// After changing perPage, page should reset
|
||||||
|
expect($component->get('perPage'))->toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Empty State Tests
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
test('shows empty state message when no potential clients exist', function () {
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->assertSee(__('potential-clients.no_potential_clients_found'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows filter empty state when no potential clients match filters', function () {
|
||||||
|
PotentialClient::factory()->company()->create(['name' => 'Company Only']);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
Volt::test('admin.potential-clients.index')
|
||||||
|
->set('typeFilter', 'individual')
|
||||||
|
->assertSee(__('potential-clients.no_potential_clients_match'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================
|
||||||
|
// Search Resets Pagination Tests
|
||||||
|
// ===========================================
|
||||||
|
|
||||||
|
test('updating search resets page to 1', function () {
|
||||||
|
PotentialClient::factory()->count(15)->create();
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
$component = Volt::test('admin.potential-clients.index')
|
||||||
|
->call('gotoPage', 2)
|
||||||
|
->set('search', 'test');
|
||||||
|
|
||||||
|
// Page should be reset after search update
|
||||||
|
expect($component->get('search'))->toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updating type filter resets page to 1', function () {
|
||||||
|
PotentialClient::factory()->count(15)->create();
|
||||||
|
|
||||||
|
$this->actingAs($this->admin);
|
||||||
|
|
||||||
|
$component = Volt::test('admin.potential-clients.index')
|
||||||
|
->call('gotoPage', 2)
|
||||||
|
->set('typeFilter', 'individual');
|
||||||
|
|
||||||
|
// Page should be reset after filter update
|
||||||
|
expect($component->get('typeFilter'))->toBe('individual');
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue