Add instant consultant client filtering without pagination
This commit is contained in:
@@ -1,13 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from math import ceil
|
|
||||||
from datetime import date, datetime, timezone, timedelta
|
from datetime import date, datetime, timezone, timedelta
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
from sqlalchemy import and_, func, or_, select
|
from sqlalchemy import and_, func, or_, select
|
||||||
from sqlalchemy.orm import Session, selectinload
|
from sqlalchemy.orm import Session, selectinload
|
||||||
|
|
||||||
from app.modules.clients.models import Client
|
|
||||||
from app.modules.clients.models import Client, ClientAuditLog
|
from app.modules.clients.models import Client, ClientAuditLog
|
||||||
from app.modules.client_groups.models import ClientGroup
|
from app.modules.client_groups.models import ClientGroup
|
||||||
from app.modules.consultants.models import ClientConsultantLink, ConsultantManagedClient, ConsultantProfile, ConsultantWorkspace, ConsultantServiceRequest
|
from app.modules.consultants.models import ClientConsultantLink, ConsultantManagedClient, ConsultantProfile, ConsultantWorkspace, ConsultantServiceRequest
|
||||||
@@ -338,20 +336,16 @@ def consultant_linked_clients_page(
|
|||||||
status: str = "active",
|
status: str = "active",
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
per_page: int = 25,
|
per_page: int = 25,
|
||||||
sort_by: str = "client_name",
|
sort_by: str = "client_code",
|
||||||
sort_order: str = "asc",
|
sort_order: str = "asc",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Return a compact, searchable and paginated list of linked firm clients.
|
"""Return every active consultant-linked firm client for instant browser filtering.
|
||||||
|
|
||||||
Link permission columns remain unchanged and continue to govern the existing
|
The existing link permission fields and all downstream service, due-date,
|
||||||
service, due-date, communication and document workflows. They are deliberately
|
communication and document controls remain unchanged. Search, group, status
|
||||||
not exposed in the client-list UI.
|
and sorting are performed instantly in the rendered client table, so no
|
||||||
|
pagination or form submission is required.
|
||||||
"""
|
"""
|
||||||
q = (q or "").strip()
|
|
||||||
status = (status or "").strip().lower()
|
|
||||||
per_page = min(max(int(per_page or 25), 1), 100)
|
|
||||||
page = max(int(page or 1), 1)
|
|
||||||
|
|
||||||
stmt = (
|
stmt = (
|
||||||
select(ClientConsultantLink, Client, ClientGroup)
|
select(ClientConsultantLink, Client, ClientGroup)
|
||||||
.join(Client, Client.id == ClientConsultantLink.client_id)
|
.join(Client, Client.id == ClientConsultantLink.client_id)
|
||||||
@@ -363,53 +357,17 @@ def consultant_linked_clients_page(
|
|||||||
Client.tenant_id == consultant.tenant_id,
|
Client.tenant_id == consultant.tenant_id,
|
||||||
Client.is_archived.is_(False),
|
Client.is_archived.is_(False),
|
||||||
)
|
)
|
||||||
|
.order_by(Client.client_code.asc(), Client.client_name.asc(), Client.id.asc())
|
||||||
)
|
)
|
||||||
if status:
|
|
||||||
stmt = stmt.where(Client.status == status)
|
|
||||||
if client_group_id:
|
|
||||||
stmt = stmt.where(Client.client_group_id == int(client_group_id))
|
|
||||||
if q:
|
|
||||||
like = f"%{q}%"
|
|
||||||
stmt = stmt.where(
|
|
||||||
or_(
|
|
||||||
Client.client_code.ilike(like),
|
|
||||||
Client.client_name.ilike(like),
|
|
||||||
Client.trade_name.ilike(like),
|
|
||||||
Client.pan.ilike(like),
|
|
||||||
Client.gstin.ilike(like),
|
|
||||||
ClientGroup.group_name.ilike(like),
|
|
||||||
ClientGroup.group_code.ilike(like),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
total = int(db.execute(select(func.count()).select_from(stmt.subquery())).scalar_one() or 0)
|
|
||||||
pages = max(ceil(total / per_page), 1)
|
|
||||||
page = min(page, pages)
|
|
||||||
|
|
||||||
sort_columns = {
|
|
||||||
"client_code": Client.client_code,
|
|
||||||
"client_name": Client.client_name,
|
|
||||||
"pan": Client.pan,
|
|
||||||
"client_group": ClientGroup.group_name,
|
|
||||||
"status": Client.status,
|
|
||||||
}
|
|
||||||
sort_column = sort_columns.get(sort_by, Client.client_name)
|
|
||||||
ordering = sort_column.desc() if (sort_order or "asc").lower() == "desc" else sort_column.asc()
|
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
result = db.execute(
|
for link, client, group in db.execute(stmt).all():
|
||||||
stmt.order_by(ordering, Client.id.asc())
|
|
||||||
.offset((page - 1) * per_page)
|
|
||||||
.limit(per_page)
|
|
||||||
).all()
|
|
||||||
for link, client, group in result:
|
|
||||||
rows.append({
|
rows.append({
|
||||||
"link": link,
|
"link": link,
|
||||||
"client": client,
|
"client": client,
|
||||||
"client_group_id": getattr(group, "id", None),
|
"client_group_id": getattr(group, "id", None),
|
||||||
"client_group_code": getattr(group, "group_code", None),
|
"client_group_code": getattr(group, "group_code", None),
|
||||||
"client_group_name": getattr(group, "group_name", None),
|
"client_group_name": getattr(group, "group_name", None),
|
||||||
"relationship_label": (link.relationship_type or "accounts_consultant").replace("_", " ").title(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
groups_stmt = (
|
groups_stmt = (
|
||||||
@@ -431,16 +389,18 @@ def consultant_linked_clients_page(
|
|||||||
{"id": int(group_id), "code": code, "name": name}
|
{"id": int(group_id), "code": code, "name": name}
|
||||||
for group_id, code, name in db.execute(groups_stmt).all()
|
for group_id, code, name in db.execute(groups_stmt).all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
total = len(rows)
|
||||||
return {
|
return {
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
"groups": groups,
|
"groups": groups,
|
||||||
"meta": {"total": total, "page": page, "per_page": per_page, "pages": pages},
|
"meta": {"total": total, "page": 1, "per_page": total, "pages": 1},
|
||||||
"filters": {
|
"filters": {
|
||||||
"q": q,
|
"q": "",
|
||||||
"client_group_id": client_group_id,
|
"client_group_id": None,
|
||||||
"status": status,
|
"status": "active",
|
||||||
"sort_by": sort_by if sort_by in sort_columns else "client_name",
|
"sort_by": "client_code",
|
||||||
"sort_order": "desc" if (sort_order or "asc").lower() == "desc" else "asc",
|
"sort_order": "asc",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +1,77 @@
|
|||||||
{% set linked = payload.linked_clients_page %}
|
{% set linked = payload.linked_clients_page %}
|
||||||
{% set filters = linked.filters %}
|
|
||||||
{% set meta = linked.meta %}
|
{% set meta = linked.meta %}
|
||||||
|
|
||||||
<div class="space-y-6">
|
<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 class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold text-slate-900">Linked Firm Clients</h3>
|
<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>
|
<p class="mt-1 text-sm text-slate-500">Firm clients explicitly linked to your consultant profile.</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<form method="get" action="/consultant/dashboard" class="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-6">
|
<div class="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-5">
|
||||||
<input type="hidden" name="tab" value="clients">
|
|
||||||
<div class="md:col-span-2 xl:col-span-2">
|
<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>
|
<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">
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label for="linked-client-group" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Group</label>
|
<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>
|
<option value="">All groups</option>
|
||||||
{% for group in linked.groups %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="linked-client-status" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Status</label>
|
<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">
|
<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" {% if filters.status == 'active' %}selected{% endif %}>Active</option>
|
<option value="active" selected>Active</option>
|
||||||
<option value="" {% if not filters.status %}selected{% endif %}>All statuses</option>
|
<option value="">All statuses</option>
|
||||||
<option value="inactive" {% if filters.status == 'inactive' %}selected{% endif %}>Inactive</option>
|
<option value="inactive">Inactive</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="linked-client-sort" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Sort</label>
|
<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">
|
<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_name" {% if filters.sort_by == 'client_name' %}selected{% endif %}>Client name</option>
|
<option value="client_code" selected>Client code</option>
|
||||||
<option value="client_code" {% if filters.sort_by == 'client_code' %}selected{% endif %}>Client code</option>
|
<option value="client_name">Client name</option>
|
||||||
<option value="pan" {% if filters.sort_by == 'pan' %}selected{% endif %}>PAN</option>
|
<option value="pan">PAN</option>
|
||||||
<option value="client_group" {% if filters.sort_by == 'client_group' %}selected{% endif %}>Client group</option>
|
<option value="client_group">Client group</option>
|
||||||
<option value="status" {% if filters.sort_by == 'status' %}selected{% endif %}>Status</option>
|
<option value="status">Status</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="mt-5 overflow-x-auto rounded-2xl border border-slate-200">
|
<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">
|
<table class="min-w-full divide-y divide-slate-200 text-left">
|
||||||
<thead class="bg-slate-50">
|
<thead class="bg-slate-50">
|
||||||
<tr class="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
<tr class="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||||
<th class="whitespace-nowrap px-4 py-3">Code</th>
|
<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="whitespace-nowrap px-4 py-3">PAN</th>
|
||||||
<th class="min-w-[180px] px-4 py-3">Client Group</th>
|
<th class="min-w-[190px] px-4 py-3">Client Group</th>
|
||||||
<th class="whitespace-nowrap px-4 py-3">Role</th>
|
|
||||||
<th class="whitespace-nowrap px-4 py-3">Status</th>
|
<th class="whitespace-nowrap px-4 py-3">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</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 %}
|
{% 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="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">
|
<td class="px-4 py-3">
|
||||||
<div class="font-semibold text-slate-900">{{ row.client.client_name }}</div>
|
<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 %}
|
{% if row.client_group_code %}<div class="text-xs text-slate-500">{{ row.client_group_code }}</div>{% endif %}
|
||||||
{% else %}-{% endif %}
|
{% else %}-{% endif %}
|
||||||
</td>
|
</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">
|
<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>
|
<span class="rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-semibold text-emerald-700">Active</span>
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 %}
|
{% 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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<section class="af-card">
|
<section class="af-card">
|
||||||
@@ -155,3 +122,79 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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