Add instant consultant client filtering without pagination
This commit is contained in:
@@ -1,74 +1,77 @@
|
||||
{% set linked = payload.linked_clients_page %}
|
||||
{% set filters = linked.filters %}
|
||||
{% set meta = linked.meta %}
|
||||
|
||||
<div class="space-y-6">
|
||||
<section class="af-card">
|
||||
<section class="af-card" id="linked-firm-clients-section">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900">Linked Firm Clients</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">Firm clients explicitly linked to your consultant profile.</p>
|
||||
</div>
|
||||
<span class="w-fit rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600">{{ meta.total }} client{% if meta.total != 1 %}s{% endif %}</span>
|
||||
<span id="linked-client-count" class="w-fit rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600">
|
||||
{{ meta.total }} client{% if meta.total != 1 %}s{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form method="get" action="/consultant/dashboard" class="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-6">
|
||||
<input type="hidden" name="tab" value="clients">
|
||||
<div class="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-5">
|
||||
<div class="md:col-span-2 xl:col-span-2">
|
||||
<label for="linked-client-search" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Search</label>
|
||||
<input id="linked-client-search" name="linked_q" value="{{ filters.q }}" placeholder="Client name, code, PAN, GSTIN or group"
|
||||
<input id="linked-client-search" type="search" autocomplete="off"
|
||||
placeholder="Client name, code, PAN, GSTIN or group"
|
||||
class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm text-slate-800 focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
|
||||
</div>
|
||||
<div>
|
||||
<label for="linked-client-group" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Group</label>
|
||||
<select id="linked-client-group" name="linked_group_id" class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm text-slate-800">
|
||||
<select id="linked-client-group" class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm text-slate-800">
|
||||
<option value="">All groups</option>
|
||||
{% for group in linked.groups %}
|
||||
<option value="{{ group.id }}" {% if filters.client_group_id == group.id %}selected{% endif %}>{{ group.name }}{% if group.code %} ({{ group.code }}){% endif %}</option>
|
||||
<option value="{{ group.id }}">{{ group.name }}{% if group.code %} ({{ group.code }}){% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="linked-client-status" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Status</label>
|
||||
<select id="linked-client-status" name="linked_status" class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm text-slate-800">
|
||||
<option value="active" {% if filters.status == 'active' %}selected{% endif %}>Active</option>
|
||||
<option value="" {% if not filters.status %}selected{% endif %}>All statuses</option>
|
||||
<option value="inactive" {% if filters.status == 'inactive' %}selected{% endif %}>Inactive</option>
|
||||
<select id="linked-client-status" class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm text-slate-800">
|
||||
<option value="active" selected>Active</option>
|
||||
<option value="">All statuses</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="linked-client-sort" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Sort</label>
|
||||
<select id="linked-client-sort" name="linked_sort_by" class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm text-slate-800">
|
||||
<option value="client_name" {% if filters.sort_by == 'client_name' %}selected{% endif %}>Client name</option>
|
||||
<option value="client_code" {% if filters.sort_by == 'client_code' %}selected{% endif %}>Client code</option>
|
||||
<option value="pan" {% if filters.sort_by == 'pan' %}selected{% endif %}>PAN</option>
|
||||
<option value="client_group" {% if filters.sort_by == 'client_group' %}selected{% endif %}>Client group</option>
|
||||
<option value="status" {% if filters.sort_by == 'status' %}selected{% endif %}>Status</option>
|
||||
<select id="linked-client-sort" class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm text-slate-800">
|
||||
<option value="client_code" selected>Client code</option>
|
||||
<option value="client_name">Client name</option>
|
||||
<option value="pan">PAN</option>
|
||||
<option value="client_group">Client group</option>
|
||||
<option value="status">Status</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<input type="hidden" name="linked_sort_order" value="{{ filters.sort_order }}">
|
||||
<input type="hidden" name="linked_per_page" value="{{ meta.per_page }}">
|
||||
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Find</button>
|
||||
<a href="/consultant/dashboard?tab=clients" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Clear</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 overflow-x-auto rounded-2xl border border-slate-200">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-left">
|
||||
<thead class="bg-slate-50">
|
||||
<tr class="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
<th class="whitespace-nowrap px-4 py-3">Code</th>
|
||||
<th class="min-w-[260px] px-4 py-3">Client Name</th>
|
||||
<th class="min-w-[300px] px-4 py-3">Client Name</th>
|
||||
<th class="whitespace-nowrap px-4 py-3">PAN</th>
|
||||
<th class="min-w-[180px] px-4 py-3">Client Group</th>
|
||||
<th class="whitespace-nowrap px-4 py-3">Role</th>
|
||||
<th class="min-w-[190px] px-4 py-3">Client Group</th>
|
||||
<th class="whitespace-nowrap px-4 py-3">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
<tbody id="linked-client-table-body" class="divide-y divide-slate-100 bg-white">
|
||||
{% for row in linked.rows %}
|
||||
<tr class="hover:bg-slate-50">
|
||||
{% set client_status = (row.client.status or 'inactive')|lower %}
|
||||
<tr class="linked-client-row hover:bg-slate-50"
|
||||
data-code="{{ (row.client.client_code or '')|lower|e }}"
|
||||
data-name="{{ (row.client.client_name or '')|lower|e }}"
|
||||
data-trade-name="{{ (row.client.trade_name or '')|lower|e }}"
|
||||
data-pan="{{ (row.client.pan or '')|lower|e }}"
|
||||
data-gstin="{{ (row.client.gstin or '')|lower|e }}"
|
||||
data-group-id="{{ row.client_group_id or '' }}"
|
||||
data-group="{{ (row.client_group_name or '')|lower|e }} {{ (row.client_group_code or '')|lower|e }}"
|
||||
data-status="{{ client_status|e }}">
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm font-semibold text-slate-700">{{ row.client.client_code or '-' }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="font-semibold text-slate-900">{{ row.client.client_name }}</div>
|
||||
@@ -81,57 +84,21 @@
|
||||
{% if row.client_group_code %}<div class="text-xs text-slate-500">{{ row.client_group_code }}</div>{% endif %}
|
||||
{% else %}-{% endif %}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm text-slate-700">{{ row.relationship_label }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3">
|
||||
{% if row.client.status == 'active' %}
|
||||
{% if client_status == 'active' %}
|
||||
<span class="rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-semibold text-emerald-700">Active</span>
|
||||
{% else %}
|
||||
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-600">{{ (row.client.status or 'inactive').replace('_',' ').title() }}</span>
|
||||
<span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-semibold text-slate-600">{{ client_status.replace('_',' ').title() }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="px-4 py-10 text-center text-sm text-slate-500">No linked firm clients match the selected filters.</td></tr>
|
||||
{% endfor %}
|
||||
<tr id="linked-client-empty-row" class="hidden">
|
||||
<td colspan="5" class="px-4 py-10 text-center text-sm text-slate-500">No linked firm clients match the selected filters.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="text-sm text-slate-600">
|
||||
{% if meta.total %}
|
||||
Showing <span class="font-semibold text-slate-900">{{ ((meta.page - 1) * meta.per_page) + 1 }}</span> to
|
||||
<span class="font-semibold text-slate-900">{{ meta.total if meta.page * meta.per_page > meta.total else meta.page * meta.per_page }}</span>
|
||||
of <span class="font-semibold text-slate-900">{{ meta.total }}</span> clients
|
||||
{% else %}No clients found{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<form method="get" action="/consultant/dashboard" class="flex items-center gap-2">
|
||||
<input type="hidden" name="tab" value="clients">
|
||||
<input type="hidden" name="linked_q" value="{{ filters.q }}">
|
||||
{% if filters.client_group_id %}<input type="hidden" name="linked_group_id" value="{{ filters.client_group_id }}">{% endif %}
|
||||
<input type="hidden" name="linked_status" value="{{ filters.status }}">
|
||||
<input type="hidden" name="linked_sort_by" value="{{ filters.sort_by }}">
|
||||
<input type="hidden" name="linked_sort_order" value="{{ filters.sort_order }}">
|
||||
<input type="hidden" name="linked_page" value="1">
|
||||
<label for="linked-per-page" class="text-sm text-slate-600">Rows</label>
|
||||
<select id="linked-per-page" name="linked_per_page" onchange="this.form.submit()" class="rounded-lg border border-slate-300 bg-white px-2 py-1.5 text-sm text-slate-700">
|
||||
{% for size in [25, 50, 100] %}<option value="{{ size }}" {% if meta.per_page == size %}selected{% endif %}>{{ size }}</option>{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
{% set query = namespace(value='tab=clients&linked_q=' ~ (filters.q|urlencode) ~ '&linked_status=' ~ (filters.status|urlencode) ~ '&linked_per_page=' ~ meta.per_page ~ '&linked_sort_by=' ~ (filters.sort_by|urlencode) ~ '&linked_sort_order=' ~ (filters.sort_order|urlencode)) %}
|
||||
{% if filters.client_group_id %}{% set query.value = query.value ~ '&linked_group_id=' ~ filters.client_group_id %}{% endif %}
|
||||
{% if meta.page > 1 %}<a href="/consultant/dashboard?{{ query.value }}&linked_page={{ meta.page - 1 }}" class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Previous</a>{% endif %}
|
||||
{% for number in range(1, meta.pages + 1) %}
|
||||
{% if number == meta.page %}
|
||||
<span class="rounded-lg bg-brand-600 px-3 py-2 text-sm font-semibold text-white">{{ number }}</span>
|
||||
{% elif number == 1 or number == meta.pages or (number >= meta.page - 2 and number <= meta.page + 2) %}
|
||||
<a href="/consultant/dashboard?{{ query.value }}&linked_page={{ number }}" class="rounded-lg px-3 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-100">{{ number }}</a>
|
||||
{% elif number == meta.page - 3 or number == meta.page + 3 %}<span class="px-1 text-slate-400">…</span>{% endif %}
|
||||
{% endfor %}
|
||||
{% if meta.page < meta.pages %}<a href="/consultant/dashboard?{{ query.value }}&linked_page={{ meta.page + 1 }}" class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Next</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="af-card">
|
||||
@@ -155,3 +122,79 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const section = document.getElementById('linked-firm-clients-section');
|
||||
if (!section || section.dataset.liveFilterReady === 'true') return;
|
||||
section.dataset.liveFilterReady = 'true';
|
||||
|
||||
const search = document.getElementById('linked-client-search');
|
||||
const group = document.getElementById('linked-client-group');
|
||||
const status = document.getElementById('linked-client-status');
|
||||
const sort = document.getElementById('linked-client-sort');
|
||||
const body = document.getElementById('linked-client-table-body');
|
||||
const count = document.getElementById('linked-client-count');
|
||||
const empty = document.getElementById('linked-client-empty-row');
|
||||
const rows = Array.from(body.querySelectorAll('.linked-client-row'));
|
||||
let timer = null;
|
||||
|
||||
const normalise = (value) => (value || '').toString().trim().toLocaleLowerCase();
|
||||
|
||||
function applyFilters() {
|
||||
const query = normalise(search.value);
|
||||
const selectedGroup = group.value;
|
||||
const selectedStatus = normalise(status.value);
|
||||
const sortKey = sort.value;
|
||||
|
||||
const visible = rows.filter((row) => {
|
||||
const searchable = [
|
||||
row.dataset.code,
|
||||
row.dataset.name,
|
||||
row.dataset.tradeName,
|
||||
row.dataset.pan,
|
||||
row.dataset.gstin,
|
||||
row.dataset.group
|
||||
].join(' ');
|
||||
const matchesSearch = !query || searchable.includes(query);
|
||||
const matchesGroup = !selectedGroup || row.dataset.groupId === selectedGroup;
|
||||
const matchesStatus = !selectedStatus || row.dataset.status === selectedStatus;
|
||||
return matchesSearch && matchesGroup && matchesStatus;
|
||||
});
|
||||
|
||||
visible.sort((a, b) => {
|
||||
const keyMap = {
|
||||
client_code: 'code',
|
||||
client_name: 'name',
|
||||
pan: 'pan',
|
||||
client_group: 'group',
|
||||
status: 'status'
|
||||
};
|
||||
const dataKey = keyMap[sortKey] || 'code';
|
||||
return normalise(a.dataset[dataKey]).localeCompare(normalise(b.dataset[dataKey]), undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
});
|
||||
});
|
||||
|
||||
rows.forEach((row) => row.classList.add('hidden'));
|
||||
visible.forEach((row) => {
|
||||
row.classList.remove('hidden');
|
||||
body.insertBefore(row, empty);
|
||||
});
|
||||
|
||||
empty.classList.toggle('hidden', visible.length !== 0);
|
||||
count.textContent = `${visible.length} client${visible.length === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
search.addEventListener('input', () => {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(applyFilters, 250);
|
||||
});
|
||||
group.addEventListener('change', applyFilters);
|
||||
status.addEventListener('change', applyFilters);
|
||||
sort.addEventListener('change', applyFilters);
|
||||
|
||||
applyFilters();
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user