Compare commits

...

1 Commits

Author SHA1 Message Date
kelly
3fb5747aa2 feat: add server-side search to all index pages
Standardize search functionality across all listing pages:
- Products, Contacts, Quotes, Tasks, Leads, Accounts, Invoices, Orders

All pages now use simple form-based server-side search:
- Type search term, press Enter or click magnifying glass
- Full database search (not limited to current page)
- Removed confusing live-search dropdowns that only searched current page
- Added JSON response support for AJAX requests in controllers

Updated filter-bar component to support alpine mode with optional
server-side search on Enter key press.
2025-12-11 10:01:35 -07:00
17 changed files with 416 additions and 146 deletions

View File

@@ -82,6 +82,18 @@ class OrderController extends Controller
$orders = $query->paginate(20)->withQueryString();
// Return JSON for AJAX/API requests (live search)
if ($request->wantsJson()) {
return response()->json([
'data' => $orders->map(fn ($o) => [
'order_number' => $o->order_number,
'name' => $o->order_number.' - '.$o->business->name,
'customer' => $o->business->name,
'status' => $o->status,
])->values()->toArray(),
]);
}
return view('seller.orders.index', compact('orders', 'business'));
}

View File

@@ -43,6 +43,18 @@ class AccountController extends Controller
$accounts = $query->orderBy('name')->paginate(25);
// Return JSON for AJAX/API requests (live search)
if ($request->wantsJson()) {
return response()->json([
'data' => $accounts->map(fn ($a) => [
'slug' => $a->slug,
'name' => $a->name,
'email' => $a->business_email,
'status' => $a->status,
])->values()->toArray(),
]);
}
return view('seller.crm.accounts.index', compact('business', 'accounts'));
}

View File

@@ -67,6 +67,18 @@ class ContactController extends Controller
->paginate(25)
->withQueryString();
// Return JSON for AJAX/API requests (live search)
if ($request->wantsJson()) {
return response()->json([
'data' => $contacts->map(fn ($c) => [
'hashid' => $c->hashid,
'name' => $c->getFullName(),
'email' => $c->email,
'account' => $c->business?->name,
])->values()->toArray(),
]);
}
// Get accounts for filter dropdown
$accounts = Business::where('type', 'buyer')
->where('status', 'approved')

View File

@@ -35,6 +35,19 @@ class LeadController extends Controller
$leads = $query->latest()->paginate(25);
// Return JSON for AJAX/API requests (live search)
if ($request->wantsJson()) {
return response()->json([
'data' => $leads->map(fn ($l) => [
'hashid' => $l->hashid,
'name' => $l->company_name,
'contact' => $l->contact_name,
'email' => $l->contact_email,
'status' => $l->status,
])->values()->toArray(),
]);
}
return view('seller.crm.leads.index', compact('business', 'leads'));
}

View File

@@ -44,6 +44,19 @@ class QuoteController extends Controller
$quotes = $query->orderByDesc('created_at')->paginate(25);
// Return JSON for AJAX/API requests (live search)
if ($request->wantsJson()) {
return response()->json([
'data' => $quotes->map(fn ($q) => [
'id' => $q->id,
'name' => $q->quote_number.' - '.($q->title ?? 'Untitled'),
'contact' => $q->contact?->name ?? '-',
'status' => $q->status,
'total' => '$'.number_format($q->total, 2),
])->values()->toArray(),
]);
}
return view('seller.crm.quotes.index', compact('quotes', 'business'));
}

View File

@@ -40,8 +40,30 @@ class TaskController extends Controller
$tasksQuery->where('type', $request->type);
}
// Search filter
if ($request->filled('q')) {
$search = $request->q;
$tasksQuery->where(function ($q) use ($search) {
$q->where('title', 'ILIKE', "%{$search}%")
->orWhere('details', 'ILIKE', "%{$search}%");
});
}
$tasks = $tasksQuery->paginate(25);
// Return JSON for AJAX/API requests (live search)
if ($request->wantsJson()) {
return response()->json([
'data' => $tasks->map(fn ($t) => [
'id' => $t->id,
'name' => $t->title,
'type' => $t->type,
'assignee' => $t->assignee?->name ?? 'Unassigned',
'due_at' => $t->due_at?->format('M j, Y'),
])->values()->toArray(),
]);
}
// Get stats with single efficient query
$statsQuery = CrmTask::where('seller_business_id', $business->id)
->selectRaw('

View File

@@ -167,6 +167,19 @@ class InvoiceController extends Controller
->paginate(25)
->withQueryString();
// Return JSON for AJAX/API requests (live search)
if ($request->wantsJson()) {
return response()->json([
'data' => $invoices->map(fn ($i) => [
'hashid' => $i->hashid,
'name' => $i->invoice_number.' - '.$i->business->name,
'invoice_number' => $i->invoice_number,
'customer' => $i->business->name,
'status' => $i->payment_status,
])->values()->toArray(),
]);
}
return view('seller.invoices.index', compact('business', 'invoices', 'stats'));
}

View File

@@ -29,6 +29,11 @@ class ProductController extends Controller
// Get brand IDs to filter by (respects brand context switcher)
$brandIds = BrandSwitcherController::getFilteredBrandIds();
// Get all brands for the business for the filter dropdown
$brands = \App\Models\Brand::where('business_id', $business->id)
->orderBy('name')
->get(['id', 'name']);
// Calculate missing BOM count for health alert
$missingBomCount = Product::whereIn('brand_id', $brandIds)
->where('is_assembly', true)
@@ -150,7 +155,20 @@ class ProductController extends Controller
'to' => $paginator->lastItem(),
];
return view('seller.products.index', compact('business', 'products', 'missingBomCount', 'paginator', 'pagination'));
// Return JSON for AJAX/API requests (live search)
if ($request->wantsJson()) {
return response()->json([
'data' => $products->map(fn ($p) => [
'hashid' => $p['hashid'],
'name' => $p['product'],
'sku' => $p['sku'],
'brand' => $p['brand'],
])->values()->toArray(),
'pagination' => $pagination,
]);
}
return view('seller.products.index', compact('business', 'brands', 'products', 'missingBomCount', 'paginator', 'pagination'));
}
/**

View File

@@ -34,6 +34,22 @@
alpine
clear-action="clearFilters"
>
For Live Search with dropdown (recommended):
<x-ui.filter-bar
search-placeholder="Search products..."
:search-value="request('search')"
live-search-url="{{ route('seller.business.products.index', $business->slug) }}"
live-search-label="name"
live-search-sublabel="sku"
live-search-route-prefix="{{ route('seller.business.products.show', [$business->slug, '__ID__']) }}"
clear-url="{{ route('seller.business.products.index', $business->slug) }}"
>
Props:
- live-search-url: API endpoint that returns JSON with 'data' array
- live-search-label: Primary field to display (default: 'name')
- live-search-sublabel: Secondary field to display (optional, e.g., 'sku')
- live-search-route-prefix: URL template with __ID__ replaced by item hashid/id
--}}
@props([
'searchPlaceholder' => 'Search...',
@@ -45,9 +61,144 @@
'alpine' => false,
'formAction' => null,
'formMethod' => 'GET',
'liveSearchUrl' => null,
'liveSearchLabel' => 'name',
'liveSearchSublabel' => null,
'liveSearchRoutePrefix' => null,
])
@if($alpine)
@if($liveSearchUrl)
{{-- Live Search Mode with Dropdown --}}
<div x-data="{
liveSearchQuery: '{{ $searchValue ?? '' }}',
liveSearchResults: [],
liveSearchOpen: false,
liveSearchLoading: false,
liveSearchIndex: -1,
liveSearchUrl: '{{ $liveSearchUrl }}',
liveSearchRoutePrefix: '{{ $liveSearchRoutePrefix }}',
liveSearchLabelField: '{{ $liveSearchLabel }}',
liveSearchSublabelField: '{{ $liveSearchSublabel }}',
async doLiveSearch() {
if (this.liveSearchQuery.length < 2) {
this.liveSearchResults = [];
this.liveSearchOpen = false;
return;
}
this.liveSearchLoading = true;
try {
let url = this.liveSearchUrl;
if (url.includes('?')) {
url = url + encodeURIComponent(this.liveSearchQuery);
} else {
url = url + '?search=' + encodeURIComponent(this.liveSearchQuery);
}
const response = await fetch(url, {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
});
const data = await response.json();
this.liveSearchResults = data.data || data || [];
this.liveSearchOpen = this.liveSearchResults.length > 0;
this.liveSearchIndex = -1;
} catch (error) {
console.error('Live search error:', error);
this.liveSearchResults = [];
}
this.liveSearchLoading = false;
},
navigateLiveSearch(item) {
const id = item.hashid || item.slug || item.id || item.order_number;
if (this.liveSearchRoutePrefix && id) {
window.location.href = this.liveSearchRoutePrefix.replace('__ID__', id);
}
},
handleLiveSearchKeydown(event) {
if (!this.liveSearchOpen) return;
if (event.key === 'ArrowDown') {
event.preventDefault();
this.liveSearchIndex = Math.min(this.liveSearchIndex + 1, this.liveSearchResults.length - 1);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
this.liveSearchIndex = Math.max(this.liveSearchIndex - 1, -1);
} else if (event.key === 'Enter' && this.liveSearchIndex >= 0) {
event.preventDefault();
this.navigateLiveSearch(this.liveSearchResults[this.liveSearchIndex]);
} else if (event.key === 'Escape') {
this.liveSearchOpen = false;
}
},
getLiveSearchLabel(item) {
return item[this.liveSearchLabelField] || item.name || '';
},
getLiveSearchSublabel(item) {
if (!this.liveSearchSublabelField) return null;
return item[this.liveSearchSublabelField] || null;
}
}" x-on:keydown="handleLiveSearchKeydown" x-on:click.outside="liveSearchOpen = false"
class="{{ $attributes->get('class', 'rounded-lg border border-base-300 bg-base-100 px-4 py-3 flex flex-col gap-3 md:flex-row md:items-center md:justify-between') }}">
{{-- Left: Search + Filters --}}
<div class="flex flex-1 gap-2 items-center">
<div class="relative flex-1 max-w-sm">
<input type="text"
x-model="liveSearchQuery"
x-on:input.debounce.300ms="doLiveSearch()"
placeholder="{{ $searchPlaceholder }}"
x-on:focus="if (liveSearchResults.length > 0) liveSearchOpen = true"
class="input input-sm input-bordered w-full bg-base-100">
<template x-if="liveSearchLoading">
<span class="absolute right-2.5 top-1/2 -translate-y-1/2">
<span class="loading loading-spinner loading-xs text-base-content/30"></span>
</span>
</template>
{{-- Live Search Dropdown --}}
<div class="absolute z-[9999] left-0 right-0 top-full mt-1 bg-base-100 border border-base-300 rounded-lg shadow-lg max-h-72 overflow-hidden"
x-bind:style="liveSearchOpen ? '' : 'display: none'">
<div class="px-3 py-1.5 border-b border-base-200 bg-base-200/30">
<span class="text-xs font-medium text-base-content/50">Search Results</span>
</div>
<ul class="menu menu-sm p-1 max-h-56 overflow-y-auto">
<template x-for="(item, index) in liveSearchResults" :key="item.hashid || item.slug || item.id || index">
<li>
<a @click.prevent="navigateLiveSearch(item)"
:class="{ 'active': liveSearchIndex === index }"
class="flex flex-col items-start py-2 cursor-pointer">
<span class="font-medium text-sm" x-text="getLiveSearchLabel(item)"></span>
<template x-if="getLiveSearchSublabel(item)">
<span class="text-xs text-base-content/60" x-text="getLiveSearchSublabel(item)"></span>
</template>
</a>
</li>
</template>
</ul>
</div>
</div>
{{ $filters ?? '' }}
</div>
{{-- Right: Actions + Clear --}}
<div class="flex items-center gap-2">
{{ $actions ?? '' }}
@if($clearUrl)
<a href="{{ $clearUrl }}" class="btn btn-ghost btn-xs text-xs text-base-content/50 hover:text-base-content">Clear</a>
@endif
</div>
</div>
@elseif($alpine)
<div {{ $attributes->merge(['class' => 'rounded-lg border border-base-300 bg-base-100 px-4 py-3 flex flex-col gap-3 md:flex-row md:items-center md:justify-between']) }}>
{{-- Left: Search + Filters --}}
<div class="flex flex-1 gap-2 items-center">
@@ -55,10 +206,22 @@
<input type="text"
x-model.debounce.200ms="{{ $searchModel }}"
placeholder="{{ $searchPlaceholder }}"
@if($formAction)
x-on:keydown.enter="window.location.href='{{ $formAction }}' + '?search=' + encodeURIComponent({{ $searchModel }})"
@endif
class="input input-sm input-bordered w-full pr-8 bg-base-100">
@if($formAction)
<button type="button"
x-on:click="window.location.href='{{ $formAction }}' + '?search=' + encodeURIComponent({{ $searchModel }})"
class="absolute right-2.5 top-1/2 -translate-y-1/2"
title="Search all products (Enter)">
<span class="icon-[heroicons--magnifying-glass] size-4 text-base-content/30 hover:text-base-content/60"></span>
</button>
@else
<span class="absolute right-2.5 top-1/2 -translate-y-1/2">
<span class="icon-[heroicons--magnifying-glass] size-4 text-base-content/30"></span>
</span>
@endif
</div>
{{ $filters ?? '' }}

View File

@@ -48,18 +48,20 @@
{{-- Filter Toolbar --}}
<x-ui.filter-bar
form-action="{{ route('seller.business.crm.accounts.index', $business->slug) }}"
search-placeholder="Search accounts..."
search-name="q"
:search-value="request('q')"
search-name="q"
form-action="{{ route('seller.business.crm.accounts.index', $business->slug) }}"
clear-url="{{ route('seller.business.crm.accounts.index', $business->slug) }}"
>
<x-slot:filters>
<select name="status" class="select select-sm select-bordered w-36" onchange="this.form.submit()">
<option value="">All Status</option>
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>Active</option>
<option value="inactive" {{ request('status') === 'inactive' ? 'selected' : '' }}>Inactive</option>
</select>
<form method="GET" action="{{ route('seller.business.crm.accounts.index', $business->slug) }}" class="contents">
<select name="status" class="select select-sm select-bordered w-36" onchange="this.form.submit()">
<option value="">All Status</option>
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>Active</option>
<option value="inactive" {{ request('status') === 'inactive' ? 'selected' : '' }}>Inactive</option>
</select>
</form>
</x-slot:filters>
</x-ui.filter-bar>

View File

@@ -22,32 +22,34 @@
{{-- Filter Toolbar --}}
<x-ui.filter-bar
form-action="{{ route('seller.business.crm.contacts.index', $business->slug) }}"
search-placeholder="Search contacts..."
search-name="q"
:search-value="request('q')"
search-name="q"
form-action="{{ route('seller.business.crm.contacts.index', $business->slug) }}"
clear-url="{{ route('seller.business.crm.contacts.index', $business->slug) }}"
>
<x-slot:filters>
<select name="account" class="select select-sm select-bordered w-48" onchange="this.form.submit()">
<option value="">All Accounts</option>
@foreach($accounts as $account)
<option value="{{ $account->id }}" {{ request('account') == $account->id ? 'selected' : '' }}>
{{ $account->dba_name ?: $account->name }}
</option>
@endforeach
</select>
<select name="type" class="select select-sm select-bordered w-36" onchange="this.form.submit()">
<option value="">All Types</option>
@foreach(\App\Models\Contact::CONTACT_TYPES as $key => $label)
<option value="{{ $key }}" {{ request('type') === $key ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
<select name="status" class="select select-sm select-bordered w-32" onchange="this.form.submit()">
<option value="">Active</option>
<option value="inactive" {{ request('status') === 'inactive' ? 'selected' : '' }}>Inactive</option>
<option value="all" {{ request('status') === 'all' ? 'selected' : '' }}>All</option>
</select>
<form method="GET" action="{{ route('seller.business.crm.contacts.index', $business->slug) }}" class="contents">
<select name="account" class="select select-sm select-bordered w-48" onchange="this.form.submit()">
<option value="">All Accounts</option>
@foreach($accounts as $account)
<option value="{{ $account->id }}" {{ request('account') == $account->id ? 'selected' : '' }}>
{{ $account->dba_name ?: $account->name }}
</option>
@endforeach
</select>
<select name="type" class="select select-sm select-bordered w-36" onchange="this.form.submit()">
<option value="">All Types</option>
@foreach(\App\Models\Contact::CONTACT_TYPES as $key => $label)
<option value="{{ $key }}" {{ request('type') === $key ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
<select name="status" class="select select-sm select-bordered w-32" onchange="this.form.submit()">
<option value="">Active</option>
<option value="inactive" {{ request('status') === 'inactive' ? 'selected' : '' }}>Inactive</option>
<option value="all" {{ request('status') === 'all' ? 'selected' : '' }}>All</option>
</select>
</form>
</x-slot:filters>
</x-ui.filter-bar>

View File

@@ -29,31 +29,24 @@
</div>
{{-- Search & Filters --}}
<div class="card bg-base-100 shadow-sm mb-6">
<div class="card-body p-4">
<form method="GET" class="flex flex-wrap items-center gap-4">
<div class="form-control flex-1 min-w-64">
<input type="text"
name="q"
value="{{ request('q') }}"
placeholder="Search leads..."
class="input input-sm input-bordered w-full">
</div>
<div class="form-control">
<select name="status" class="select select-sm select-bordered">
<option value="">All Status</option>
@foreach(\App\Models\Crm\CrmLead::STATUSES as $value => $label)
<option value="{{ $value }}" {{ request('status') === $value ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-sm btn-primary">
<span class="icon-[heroicons--magnifying-glass] size-4"></span>
Search
</button>
<x-ui.filter-bar
search-placeholder="Search leads..."
:search-value="request('q')"
search-name="q"
form-action="{{ route('seller.business.crm.leads.index', $business->slug) }}"
clear-url="{{ route('seller.business.crm.leads.index', $business->slug) }}"
>
<x-slot:filters>
<form method="GET" action="{{ route('seller.business.crm.leads.index', $business->slug) }}" class="contents">
<select name="status" class="select select-sm select-bordered w-36" onchange="this.form.submit()">
<option value="">All Status</option>
@foreach(\App\Models\Crm\CrmLead::STATUSES as $value => $label)
<option value="{{ $value }}" {{ request('status') === $value ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
</form>
</div>
</div>
</x-slot:filters>
</x-ui.filter-bar>
{{-- Leads List --}}
@if($leads->isEmpty())

View File

@@ -18,20 +18,23 @@
{{-- Search & Filter Toolbar --}}
<x-ui.filter-bar
form-action="{{ route('seller.business.crm.quotes.index', $business) }}"
search-placeholder="Search quotes..."
:search-value="request('search')"
search-name="search"
form-action="{{ route('seller.business.crm.quotes.index', $business) }}"
clear-url="{{ route('seller.business.crm.quotes.index', $business) }}"
>
<x-slot:filters>
<select name="status" class="select select-sm select-bordered w-36" onchange="this.form.submit()">
<option value="">All Status</option>
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }}>Draft</option>
<option value="sent" {{ request('status') === 'sent' ? 'selected' : '' }}>Sent</option>
<option value="accepted" {{ request('status') === 'accepted' ? 'selected' : '' }}>Accepted</option>
<option value="declined" {{ request('status') === 'declined' ? 'selected' : '' }}>Declined</option>
<option value="expired" {{ request('status') === 'expired' ? 'selected' : '' }}>Expired</option>
</select>
<form method="GET" action="{{ route('seller.business.crm.quotes.index', $business) }}" class="contents">
<select name="status" class="select select-sm select-bordered w-36" onchange="this.form.submit()">
<option value="">All Status</option>
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }}>Draft</option>
<option value="sent" {{ request('status') === 'sent' ? 'selected' : '' }}>Sent</option>
<option value="accepted" {{ request('status') === 'accepted' ? 'selected' : '' }}>Accepted</option>
<option value="declined" {{ request('status') === 'declined' ? 'selected' : '' }}>Declined</option>
<option value="expired" {{ request('status') === 'expired' ? 'selected' : '' }}>Expired</option>
</select>
</form>
</x-slot:filters>
</x-ui.filter-bar>

View File

@@ -44,36 +44,40 @@
{{-- Filters --}}
<x-ui.filter-bar
form-action="{{ route('seller.business.crm.tasks.index', $business->slug) }}"
search-placeholder="Search tasks..."
:search-value="request('q')"
search-name="q"
form-action="{{ route('seller.business.crm.tasks.index', $business->slug) }}"
clear-url="{{ route('seller.business.crm.tasks.index', $business->slug) }}"
>
<x-slot:filters>
<select name="assignee" class="select select-sm select-bordered w-40" onchange="this.form.submit()">
<option value="">All Team</option>
<option value="me" {{ request('assignee') === 'me' ? 'selected' : '' }}>My Tasks</option>
@foreach($teamMembers as $member)
<option value="{{ $member->id }}" {{ request('assignee') == $member->id ? 'selected' : '' }}>
{{ $member->name }}
</option>
@endforeach
</select>
<select name="business_id" class="select select-sm select-bordered w-40" onchange="this.form.submit()">
<option value="">All Accounts</option>
@foreach($buyerBusinesses as $buyer)
<option value="{{ $buyer->id }}" {{ request('business_id') == $buyer->id ? 'selected' : '' }}>
{{ $buyer->name }}
</option>
@endforeach
</select>
<select name="priority" class="select select-sm select-bordered w-32" onchange="this.form.submit()">
<option value="">All Priority</option>
@foreach(\App\Models\Crm\CrmTask::PRIORITIES as $key => $label)
<option value="{{ $key }}" {{ request('priority') === $key ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
<form method="GET" action="{{ route('seller.business.crm.tasks.index', $business->slug) }}" class="contents">
<select name="assignee" class="select select-sm select-bordered w-40" onchange="this.form.submit()">
<option value="">All Team</option>
<option value="me" {{ request('assignee') === 'me' ? 'selected' : '' }}>My Tasks</option>
@foreach($teamMembers as $member)
<option value="{{ $member->id }}" {{ request('assignee') == $member->id ? 'selected' : '' }}>
{{ $member->name }}
</option>
@endforeach
</select>
<select name="business_id" class="select select-sm select-bordered w-40" onchange="this.form.submit()">
<option value="">All Accounts</option>
@foreach($buyerBusinesses as $buyer)
<option value="{{ $buyer->id }}" {{ request('business_id') == $buyer->id ? 'selected' : '' }}>
{{ $buyer->name }}
</option>
@endforeach
</select>
<select name="priority" class="select select-sm select-bordered w-32" onchange="this.form.submit()">
<option value="">All Priority</option>
@foreach(\App\Models\Crm\CrmTask::PRIORITIES as $key => $label)
<option value="{{ $key }}" {{ request('priority') === $key ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
</form>
</x-slot:filters>
</x-ui.filter-bar>

View File

@@ -16,19 +16,21 @@
{{-- Filter Bar --}}
<x-ui.filter-bar
form-action="{{ route('seller.business.invoices.index', $business->slug) }}"
search-placeholder="Search invoices..."
search-name="search"
:search-value="request('search')"
search-name="search"
form-action="{{ route('seller.business.invoices.index', $business->slug) }}"
clear-url="{{ route('seller.business.invoices.index', $business->slug) }}"
>
<x-slot:filters>
<select name="status" class="select select-sm select-bordered w-36" onchange="this.form.submit()">
<option value="">All Status</option>
<option value="unpaid" {{ request('status') === 'unpaid' ? 'selected' : '' }}>Unpaid</option>
<option value="paid" {{ request('status') === 'paid' ? 'selected' : '' }}>Paid</option>
<option value="overdue" {{ request('status') === 'overdue' ? 'selected' : '' }}>Overdue</option>
</select>
<form method="GET" action="{{ route('seller.business.invoices.index', $business->slug) }}" class="contents">
<select name="status" class="select select-sm select-bordered w-36" onchange="this.form.submit()">
<option value="">All Status</option>
<option value="unpaid" {{ request('status') === 'unpaid' ? 'selected' : '' }}>Unpaid</option>
<option value="paid" {{ request('status') === 'paid' ? 'selected' : '' }}>Paid</option>
<option value="overdue" {{ request('status') === 'overdue' ? 'selected' : '' }}>Overdue</option>
</select>
</form>
</x-slot:filters>
</x-ui.filter-bar>

View File

@@ -12,33 +12,36 @@
{{-- Search + Filter Bar --}}
<x-ui.filter-bar
form-action="{{ route('seller.business.orders.index', $business->slug) }}"
search-placeholder="Search order # or customer..."
:search-value="request('search')"
search-name="search"
form-action="{{ route('seller.business.orders.index', $business->slug) }}"
clear-url="{{ route('seller.business.orders.index', $business->slug) }}"
>
<x-slot:filters>
<select name="status" class="select select-sm select-bordered w-36" onchange="this.form.submit()">
<option value="">All Statuses</option>
<option value="new" {{ request('status') === 'new' ? 'selected' : '' }}>New</option>
<option value="accepted" {{ request('status') === 'accepted' ? 'selected' : '' }}>Accepted</option>
<option value="in_progress" {{ request('status') === 'in_progress' ? 'selected' : '' }}>In Progress</option>
<option value="ready_for_manifest" {{ request('status') === 'ready_for_manifest' ? 'selected' : '' }}>Ready for Manifest</option>
<option value="ready_for_delivery" {{ request('status') === 'ready_for_delivery' ? 'selected' : '' }}>Buyer Review</option>
<option value="approved_for_delivery" {{ request('status') === 'approved_for_delivery' ? 'selected' : '' }}>Order Ready</option>
<option value="out_for_delivery" {{ request('status') === 'out_for_delivery' ? 'selected' : '' }}>Out for Delivery</option>
<option value="delivered" {{ request('status') === 'delivered' ? 'selected' : '' }}>Delivered</option>
<option value="buyer_approved" {{ request('status') === 'buyer_approved' ? 'selected' : '' }}>Completed</option>
<option value="rejected" {{ request('status') === 'rejected' ? 'selected' : '' }}>Rejected</option>
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }}>Cancelled</option>
</select>
<form method="GET" action="{{ route('seller.business.orders.index', $business->slug) }}" class="contents">
<select name="status" class="select select-sm select-bordered w-36" onchange="this.form.submit()">
<option value="">All Statuses</option>
<option value="new" {{ request('status') === 'new' ? 'selected' : '' }}>New</option>
<option value="accepted" {{ request('status') === 'accepted' ? 'selected' : '' }}>Accepted</option>
<option value="in_progress" {{ request('status') === 'in_progress' ? 'selected' : '' }}>In Progress</option>
<option value="ready_for_manifest" {{ request('status') === 'ready_for_manifest' ? 'selected' : '' }}>Ready for Manifest</option>
<option value="ready_for_delivery" {{ request('status') === 'ready_for_delivery' ? 'selected' : '' }}>Buyer Review</option>
<option value="approved_for_delivery" {{ request('status') === 'approved_for_delivery' ? 'selected' : '' }}>Order Ready</option>
<option value="out_for_delivery" {{ request('status') === 'out_for_delivery' ? 'selected' : '' }}>Out for Delivery</option>
<option value="delivered" {{ request('status') === 'delivered' ? 'selected' : '' }}>Delivered</option>
<option value="buyer_approved" {{ request('status') === 'buyer_approved' ? 'selected' : '' }}>Completed</option>
<option value="rejected" {{ request('status') === 'rejected' ? 'selected' : '' }}>Rejected</option>
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }}>Cancelled</option>
</select>
<select name="workorder_filter" class="select select-sm select-bordered w-32 hidden lg:block" onchange="this.form.submit()">
<option value="">Fulfillment</option>
<option value="not_started" {{ request('workorder_filter') === 'not_started' ? 'selected' : '' }}>Not Started</option>
<option value="in_progress" {{ request('workorder_filter') === 'in_progress' ? 'selected' : '' }}>In Progress</option>
<option value="completed" {{ request('workorder_filter') === 'completed' ? 'selected' : '' }}>Completed</option>
</select>
<select name="workorder_filter" class="select select-sm select-bordered w-32 hidden lg:block" onchange="this.form.submit()">
<option value="">Fulfillment</option>
<option value="not_started" {{ request('workorder_filter') === 'not_started' ? 'selected' : '' }}>Not Started</option>
<option value="in_progress" {{ request('workorder_filter') === 'in_progress' ? 'selected' : '' }}>In Progress</option>
<option value="completed" {{ request('workorder_filter') === 'completed' ? 'selected' : '' }}>Completed</option>
</select>
</form>
</x-slot:filters>
</x-ui.filter-bar>

View File

@@ -3,11 +3,7 @@
@section('content')
<div class="max-w-7xl mx-auto px-6 py-4 space-y-4"
x-data="{
// State - initialize from URL params to sync with server-side filtering
search: '{{ request('search', '') }}',
brandFilter: '{{ request('brand_id', 'all') ?: 'all' }}',
statusFilter: '{{ request('status', 'all') ?: 'all' }}',
visibilityFilter: 'all',
// State for view controls
focusFilter: 'all',
viewMode: 'table',
selectedProduct: null,
@@ -16,7 +12,7 @@
// Pagination info from server
pagination: @js($pagination),
// Real product data from database
// Product data from database (already filtered by server-side search)
listings: @js($products->map(function($product) {
$isListed = $product['status'] === 'active' && $product['visibility'] !== 'private';
$stock = 0;
@@ -33,16 +29,9 @@
]);
})),
// Computed properties
// Client-side focus filters only (search is server-side)
get filteredListings() {
return this.listings.filter(listing => {
const matchesSearch = !this.search ||
listing.product.toLowerCase().includes(this.search.toLowerCase()) ||
listing.sku.toLowerCase().includes(this.search.toLowerCase());
const matchesBrand = this.brandFilter === 'all' || listing.brand === this.brandFilter;
const matchesStatus = this.statusFilter === 'all' || listing.status === this.statusFilter;
const matchesVisibility = this.visibilityFilter === 'all' || listing.visibility === this.visibilityFilter;
let matchesFocus = true;
if (this.focusFilter === 'low-stock') {
matchesFocus = listing.stock > 0 && listing.stock <= listing.lowStockThreshold;
@@ -56,7 +45,7 @@
matchesFocus = listing.status === 'draft';
}
return matchesSearch && matchesBrand && matchesStatus && matchesVisibility && matchesFocus;
return matchesFocus;
});
},
@@ -79,10 +68,6 @@
},
clearFilters() {
this.search = '';
this.brandFilter = 'all';
this.statusFilter = 'all';
this.visibilityFilter = 'all';
this.focusFilter = 'all';
}
}">
@@ -111,19 +96,19 @@
@endcan
@endif
{{-- Search + Filter Bar --}}
{{-- Search + Filter Bar - server-side search --}}
<x-ui.filter-bar
form-action="{{ route('seller.business.products.index', $business->slug) }}"
search-placeholder="Search products..."
search-name="search"
search-placeholder="Search by name or SKU..."
:search-value="request('search')"
search-name="search"
form-action="{{ route('seller.business.products.index', $business->slug) }}"
clear-url="{{ route('seller.business.products.index', $business->slug) }}"
>
<x-slot:filters>
<select name="brand_id" class="select select-sm select-bordered w-32" onchange="this.form.submit()">
<option value="">All Brands</option>
@foreach($products->pluck('brand')->unique()->sort() as $brand)
<option value="{{ $products->where('brand', $brand)->first()['id'] ?? '' }}" {{ request('brand_id') == ($products->where('brand', $brand)->first()['id'] ?? '') ? 'selected' : '' }}>{{ $brand }}</option>
@foreach($brands as $brand)
<option value="{{ $brand->id }}" {{ request('brand_id') == $brand->id ? 'selected' : '' }}>{{ $brand->name }}</option>
@endforeach
</select>
<select name="status" class="select select-sm select-bordered w-28 hidden lg:block" onchange="this.form.submit()">
@@ -461,6 +446,4 @@
</div>
@endsection
@push('scripts')
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
@endpush
{{-- Alpine.js is already included in the main app bundle - no need for CDN --}}