Add compact live-filter client lists for partners

This commit is contained in:
A R R R Associates
2026-07-31 15:03:52 +05:30
parent db23d28346
commit f5bfdda775
6 changed files with 257 additions and 49 deletions
+1 -1
View File
@@ -107,7 +107,7 @@ def list_clients(
client_type=client_type, client_group_id=client_group_id, include_archived=include_archived, client_type=client_type, client_group_id=client_group_id, include_archived=include_archived,
) )
total = db.execute(select(func.count()).select_from(stmt.subquery())).scalar_one() total = db.execute(select(func.count()).select_from(stmt.subquery())).scalar_one()
per_page = min(max(int(per_page or 25), 1), 100) per_page = min(max(int(per_page or 25), 1), 5000)
pages = max(ceil(total / per_page), 1) pages = max(ceil(total / per_page), 1)
page = min(max(int(page or 1), 1), pages) page = min(max(int(page or 1), 1), pages)
offset = (page - 1) * per_page offset = (page - 1) * per_page
@@ -34,7 +34,7 @@
{% include "modules/clients/templates/clients/partials/table.html" %} {% include "modules/clients/templates/clients/partials/table.html" %}
{% if meta %} {% if meta and not partner_client_view %}
<div class="flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white px-4 py-4 shadow-soft sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white px-4 py-4 shadow-soft sm:flex-row sm:items-center sm:justify-between">
<div class="text-sm text-slate-600"> <div class="text-sm text-slate-600">
{% if meta.total > 0 %} {% if meta.total > 0 %}
@@ -1,3 +1,142 @@
{% if partner_client_view %}
<div class="rounded-2xl bg-white p-4 shadow-soft" data-partner-master-client-list>
<div class="flex flex-col gap-3 lg:flex-row lg:items-end">
<label class="min-w-0 flex-1">
<span class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Search</span>
<input type="search" data-client-search autocomplete="off" placeholder="Client code, name, 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">
</label>
<label class="w-full sm:w-56">
<span class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Group</span>
<select data-client-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">
<option value="">All groups</option>
{% set group_names = [] %}
{% for row in rows %}
{% if row.client_group_name and row.client_group_name not in group_names %}
{% set _ = group_names.append(row.client_group_name) %}
{% endif %}
{% endfor %}
{% for group_name in group_names|sort %}<option value="{{ group_name|lower|e }}">{{ group_name }}</option>{% endfor %}
</select>
</label>
<label class="w-full sm:w-44">
<span class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Status</span>
<select data-client-status 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">
<option value="">All statuses</option>
<option value="active" selected>Active</option>
<option value="inactive">Inactive</option>
<option value="archived">Archived</option>
</select>
</label>
<label class="w-full sm:w-48">
<span class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Sort</span>
<select data-client-sort 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">
<option value="code">Client code</option>
<option value="name">Client name</option>
<option value="pan">PAN / GSTIN</option>
<option value="group">Client group</option>
</select>
</label>
<button type="button" data-client-clear class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Clear</button>
</div>
<div class="mt-3 flex items-center justify-between text-sm text-slate-600">
<span data-visible-count>{{ rows|length }} clients</span>
<span>Assigned and review-access clients only</span>
</div>
<div data-client-scroll class="mt-3 max-h-[540px] overflow-auto rounded-2xl border border-slate-200">
<table class="min-w-full divide-y divide-slate-200">
<thead class="sticky top-0 z-10 bg-slate-50 shadow-sm">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Code</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Client</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Group</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Branch</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 bg-white" data-client-body>
{% for row in rows %}
{% set row_status = (row.status or 'inactive')|lower %}
{% set row_group = (row.client_group_name or '')|lower %}
<tr data-client-row
data-code="{{ (row.client_code or '')|lower|e }}"
data-name="{{ (row.client_name or '')|lower|e }}"
data-pan="{{ ((row.pan or '') ~ ' ' ~ (row.gstin or ''))|lower|e }}"
data-group="{{ row_group|e }}"
data-status="{{ row_status|e }}"
data-search="{{ ((row.client_code or '') ~ ' ' ~ (row.client_name or '') ~ ' ' ~ (row.pan or '') ~ ' ' ~ (row.gstin or '') ~ ' ' ~ (row.client_group_name or '') ~ ' ' ~ (row.client_group_code or ''))|lower|e }}">
<td class="whitespace-nowrap px-4 py-3 text-sm font-semibold text-slate-900">{{ row.client_code }}</td>
<td class="px-4 py-3 text-sm text-slate-700">
<div class="font-medium text-slate-900">{{ row.client_name }}</div>
<div class="text-xs text-slate-500">{{ row.pan or row.gstin or '-' }}</div>
</td>
<td class="px-4 py-3 text-sm text-slate-700">
{% if row.client_group_name %}
<a href="/client-groups/{{ row.client_group_id }}" class="font-medium text-brand-700">{{ row.client_group_name }}</a>
<div class="text-xs text-slate-500">{{ row.group_relationship or row.client_group_code }}</div>
{% else %}-{% endif %}
</td>
<td class="px-4 py-3 text-sm text-slate-700">{{ row.branch_name or row.assoc_firm_branch_id or row.branch_id or '-' }}</td>
<td class="whitespace-nowrap px-4 py-3 text-sm">
{% if row_status == 'active' %}<span class="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-700">Active</span>
{% elif row_status == 'archived' %}<span class="rounded-full bg-amber-100 px-2 py-1 text-xs font-medium text-amber-800">Archived</span>
{% else %}<span class="rounded-full bg-slate-200 px-2 py-1 text-xs font-medium text-slate-700">Inactive</span>{% endif %}
</td>
<td class="whitespace-nowrap px-4 py-3 text-right"><a href="/clients/{{ row.id }}" class="text-sm font-medium text-brand-700 hover:underline">Open</a></td>
</tr>
{% else %}
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-slate-500">No clients found.</td></tr>
{% endfor %}
<tr data-no-results hidden><td colspan="6" class="px-4 py-8 text-center text-sm text-slate-500">No clients match the selected filters.</td></tr>
</tbody>
</table>
</div>
</div>
<script>
(function () {
const root = document.querySelector('[data-partner-master-client-list]');
if (!root || root.dataset.ready === '1') return;
root.dataset.ready = '1';
const search = root.querySelector('[data-client-search]');
const group = root.querySelector('[data-client-group]');
const status = root.querySelector('[data-client-status]');
const sort = root.querySelector('[data-client-sort]');
const clear = root.querySelector('[data-client-clear]');
const scroll = root.querySelector('[data-client-scroll]');
const body = root.querySelector('[data-client-body]');
const count = root.querySelector('[data-visible-count]');
const empty = root.querySelector('[data-no-results]');
const rows = Array.from(root.querySelectorAll('[data-client-row]'));
let timer;
function apply() {
const term = (search.value || '').trim().toLowerCase();
const wantedGroup = group.value;
const wantedStatus = status.value;
let visible = 0;
rows.forEach((row) => {
const show = (!term || row.dataset.search.includes(term)) && (!wantedGroup || row.dataset.group === wantedGroup) && (!wantedStatus || row.dataset.status === wantedStatus);
row.hidden = !show;
if (show) visible += 1;
});
const key = sort.value || 'code';
rows.sort((a, b) => (a.dataset[key] || '').localeCompare(b.dataset[key] || '', undefined, {numeric: true, sensitivity: 'base'}));
rows.forEach((row) => body.insertBefore(row, empty));
count.textContent = visible + (visible === 1 ? ' client' : ' clients');
empty.hidden = visible !== 0;
scroll.scrollTop = 0;
}
search.addEventListener('input', () => { window.clearTimeout(timer); timer = window.setTimeout(apply, 250); });
group.addEventListener('change', apply);
status.addEventListener('change', apply);
sort.addEventListener('change', apply);
clear.addEventListener('click', () => { search.value = ''; group.value = ''; status.value = 'active'; sort.value = 'code'; apply(); search.focus(); });
apply();
})();
</script>
{% else %}
<div class="overflow-x-auto rounded-2xl bg-white shadow-soft"> <div class="overflow-x-auto rounded-2xl bg-white shadow-soft">
<table class="min-w-full divide-y divide-slate-200"> <table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"> <thead class="bg-slate-50">
@@ -16,40 +155,16 @@
{% for row in rows %} {% for row in rows %}
<tr> <tr>
<td class="whitespace-nowrap px-4 py-3 text-sm font-medium text-slate-900">{{ row.client_code }}</td> <td class="whitespace-nowrap px-4 py-3 text-sm font-medium text-slate-900">{{ row.client_code }}</td>
<td class="px-4 py-3 text-sm text-slate-700"> <td class="px-4 py-3 text-sm text-slate-700"><div class="font-medium">{{ row.client_name }}</div><div class="text-xs text-slate-500">{{ row.pan or row.gstin or '-' }}</div></td>
<div class="font-medium">{{ row.client_name }}</div> <td class="px-4 py-3 text-sm text-slate-700">{% if row.client_group_name %}<a href="/client-groups/{{ row.client_group_id }}" class="font-medium text-brand-700">{{ row.client_group_name }}</a><div class="text-xs text-slate-500">{{ row.group_relationship or row.client_group_code }}</div>{% else %}-{% endif %}</td>
<div class="text-xs text-slate-500">{{ row.pan or row.gstin or '-' }}</div> <td class="px-4 py-3 text-sm text-slate-700"><div>{{ row.association_type or 'legacy_firm' }}</div><div class="text-xs text-slate-500">{{ row.assoc_created_source or 'legacy' }}</div></td>
</td>
<td class="px-4 py-3 text-sm text-slate-700">
{% if row.client_group_name %}
<a href="/client-groups/{{ row.client_group_id }}" class="font-medium text-brand-700">{{ row.client_group_name }}</a>
<div class="text-xs text-slate-500">{{ row.group_relationship or row.client_group_code }}</div>
{% else %}-{% endif %}
</td>
<td class="px-4 py-3 text-sm text-slate-700">
<div>{{ row.association_type or 'legacy_firm' }}</div>
<div class="text-xs text-slate-500">{{ row.assoc_created_source or 'legacy' }}</div>
</td>
<td class="px-4 py-3 text-sm text-slate-700">{{ row.partner_name or row.effective_partner_id or '-' }}</td> <td class="px-4 py-3 text-sm text-slate-700">{{ row.partner_name or row.effective_partner_id or '-' }}</td>
<td class="px-4 py-3 text-sm text-slate-700">{{ row.branch_name or row.assoc_firm_branch_id or row.branch_id or '-' }}</td> <td class="px-4 py-3 text-sm text-slate-700">{{ row.branch_name or row.assoc_firm_branch_id or row.branch_id or '-' }}</td>
<td class="whitespace-nowrap px-4 py-3 text-sm"> <td class="whitespace-nowrap px-4 py-3 text-sm">{% if row.status == 'active' %}<span class="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-700">Active</span>{% elif row.status == 'archived' %}<span class="rounded-full bg-amber-100 px-2 py-1 text-xs font-medium text-amber-800">Archived</span>{% else %}<span class="rounded-full bg-slate-200 px-2 py-1 text-xs font-medium text-slate-700">Inactive</span>{% endif %}</td>
{% if row.status == 'active' %} <td class="whitespace-nowrap px-4 py-3 text-right"><a href="/clients/{{ row.id }}" class="text-sm font-medium text-brand-700 hover:underline">Open</a></td>
<span class="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-700">Active</span>
{% elif row.status == 'archived' %}
<span class="rounded-full bg-amber-100 px-2 py-1 text-xs font-medium text-amber-800">Archived</span>
{% else %}
<span class="rounded-full bg-slate-200 px-2 py-1 text-xs font-medium text-slate-700">Inactive</span>
{% endif %}
</td>
<td class="whitespace-nowrap px-4 py-3 text-right">
<a href="/clients/{{ row.id }}" class="text-sm font-medium text-brand-700 hover:underline">Open</a>
</td>
</tr> </tr>
{% else %} {% else %}<tr><td colspan="8" class="px-4 py-8 text-center text-sm text-slate-500">No clients found.</td></tr>{% endfor %}
<tr>
<td colspan="8" class="px-4 py-8 text-center text-sm text-slate-500">No clients found.</td>
</tr>
{% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% endif %}
+8 -6
View File
@@ -321,6 +321,7 @@ def clients_list(
return _redirect_denied() return _redirect_denied()
role_names = _role_names(db, user) role_names = _role_names(db, user)
partner_client_view = is_partner_role(role_names)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _apply_role_scope(scope, user, role_names) scope = _apply_role_scope(scope, user, role_names)
@@ -347,13 +348,13 @@ def clients_list(
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
partner_id=None if is_partner_role(role_names) else filters.partner_id, partner_id=None if is_partner_role(role_names) else filters.partner_id,
viewer_partner_id=_viewer_partner_id(user, role_names), viewer_partner_id=_viewer_partner_id(user, role_names),
q=filters.q, q="" if partner_client_view else filters.q,
status=filters.status, status="" if partner_client_view else filters.status,
client_type=filters.client_type, client_type="" if partner_client_view else filters.client_type,
client_group_id=filters.client_group_id, client_group_id=None if partner_client_view else filters.client_group_id,
include_archived=filters.include_archived, include_archived=filters.include_archived,
page=filters.page, page=1 if partner_client_view else filters.page,
per_page=filters.per_page, per_page=5000 if partner_client_view else filters.per_page,
sort_by=filters.sort_by, sort_by=filters.sort_by,
sort_order=filters.sort_order, sort_order=filters.sort_order,
) )
@@ -369,6 +370,7 @@ def clients_list(
can_create=has("clients.create"), can_create=has("clients.create"),
can_export=has("clients.export"), can_export=has("clients.export"),
can_import=has("clients.import"), can_import=has("clients.import"),
partner_client_view=partner_client_view,
q=filters.q, q=filters.q,
status=filters.status, status=filters.status,
client_type=filters.client_type, client_type=filters.client_type,
+2 -1
View File
@@ -228,7 +228,7 @@ def _load_tasks(db: Session, tenant_id: int | None, branch_id: int | None, curre
def _load_clients(db: Session, tenant_id: int | None, branch_id: int | None, current_user, roles: set[str]) -> list[dict[str, Any]]: def _load_clients(db: Session, tenant_id: int | None, branch_id: int | None, current_user, roles: set[str]) -> list[dict[str, Any]]:
stmt = _client_scope(select(Client), tenant_id, branch_id, current_user, roles) stmt = _client_scope(select(Client), tenant_id, branch_id, current_user, roles)
clients = db.execute(stmt.order_by(Client.client_name.asc()).limit(100)).scalars().all() clients = db.execute(stmt.order_by(Client.client_name.asc()).limit(5000)).scalars().all()
out: list[dict[str, Any]] = [] out: list[dict[str, Any]] = []
for client in clients: for client in clients:
task_count = _count(db, _task_scope(select(func.count(ClientServiceTaskInstance.id)), tenant_id, branch_id, current_user, roles).where(ClientServiceTaskInstance.client_id == client.id)) task_count = _count(db, _task_scope(select(func.count(ClientServiceTaskInstance.id)), tenant_id, branch_id, current_user, roles).where(ClientServiceTaskInstance.client_id == client.id))
@@ -246,6 +246,7 @@ def _load_clients(db: Session, tenant_id: int | None, branch_id: int | None, cur
"gstin": client.gstin, "gstin": client.gstin,
"email": client.email, "email": client.email,
"mobile": client.mobile, "mobile": client.mobile,
"status": client.status or ("active" if client.is_active else "inactive"),
"service_count": service_count, "service_count": service_count,
"task_count": task_count, "task_count": task_count,
"overdue_count": overdue_count, "overdue_count": overdue_count,
@@ -1,11 +1,101 @@
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft"> <div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-soft" data-partner-client-list>
<div class="flex items-center justify-between"><div><h2 class="text-lg font-semibold text-slate-900">Assigned Clients</h2><p class="text-sm text-slate-500">Owned clients and review-assigned clients, with service count, open tasks and overdue items.</p></div><a href="/clients" class="rounded-2xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Manage Clients</a></div> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="mt-5 grid gap-4 lg:grid-cols-2 xl:grid-cols-3"> <div>
{% for c in clients %} <h2 class="text-lg font-semibold text-slate-900">Assigned Clients</h2>
<a href="{{ c.href }}" class="rounded-3xl border border-slate-200 p-5 hover:bg-slate-50"> <p class="text-sm text-slate-500">Owned clients and review-assigned clients, with existing service, task and overdue totals.</p>
<div class="flex items-start justify-between gap-3"><div><h3 class="font-semibold text-slate-900">{{ c.client_name }}</h3><p class="mt-1 text-xs text-slate-500">{{ c.client_code }}{% if c.gstin %} · {{ c.gstin }}{% elif c.pan %} · {{ c.pan }}{% endif %}</p></div>{% if c.overdue_count > 0 %}<span class="rounded-full bg-red-100 px-2 py-1 text-xs font-semibold text-red-700">{{ c.overdue_count }} overdue</span>{% endif %}</div> </div>
<div class="mt-4 grid grid-cols-3 gap-2 text-center text-xs"><div class="rounded-2xl bg-slate-50 p-3"><div class="text-lg font-semibold">{{ c.service_count }}</div><div class="text-slate-500">Services</div></div><div class="rounded-2xl bg-slate-50 p-3"><div class="text-lg font-semibold">{{ c.task_count }}</div><div class="text-slate-500">Tasks</div></div><div class="rounded-2xl bg-slate-50 p-3"><div class="text-lg font-semibold">{{ c.overdue_count }}</div><div class="text-slate-500">Overdue</div></div></div> <div class="flex items-center gap-3">
</a> <span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700" data-visible-count>{{ clients|length }} clients</span>
{% else %}<div class="rounded-3xl border border-dashed border-slate-300 p-8 text-center text-sm text-slate-500 xl:col-span-3">No assigned or review clients found.</div>{% endfor %} <a href="/clients" class="rounded-2xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Manage Clients</a>
</div>
</div>
<div class="mt-5 flex flex-col gap-3 sm:flex-row sm:items-end">
<label class="min-w-0 flex-1">
<span class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Search</span>
<input type="search" data-client-search autocomplete="off" placeholder="Client code, name, PAN or GSTIN"
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">
</label>
<label class="w-full sm:w-52">
<span class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Status</span>
<select data-client-status 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">
<option value="">All statuses</option>
<option value="active" selected>Active</option>
<option value="inactive">Inactive</option>
<option value="archived">Archived</option>
</select>
</label>
<button type="button" data-client-clear class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Clear</button>
</div>
<div data-client-scroll class="mt-4 max-h-[540px] overflow-auto rounded-2xl border border-slate-200">
<table class="min-w-full divide-y divide-slate-200">
<thead class="sticky top-0 z-10 bg-slate-50 shadow-sm">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Code</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Client</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">PAN / GSTIN</th>
<th class="px-4 py-3 text-center text-xs font-semibold uppercase tracking-wide text-slate-500">Services</th>
<th class="px-4 py-3 text-center text-xs font-semibold uppercase tracking-wide text-slate-500">Tasks</th>
<th class="px-4 py-3 text-center text-xs font-semibold uppercase tracking-wide text-slate-500">Overdue</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 bg-white">
{% for c in clients %}
{% set row_status = (c.status or 'active')|lower %}
<tr data-client-row data-search="{{ (c.client_code ~ ' ' ~ c.client_name ~ ' ' ~ (c.pan or '') ~ ' ' ~ (c.gstin or ''))|lower|e }}" data-status="{{ row_status|e }}">
<td class="whitespace-nowrap px-4 py-3 text-sm font-semibold text-slate-900">{{ c.client_code }}</td>
<td class="px-4 py-3 text-sm text-slate-800">{{ c.client_name }}</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-slate-600">{{ c.pan or c.gstin or '-' }}</td>
<td class="px-4 py-3 text-center text-sm text-slate-700">{{ c.service_count }}</td>
<td class="px-4 py-3 text-center text-sm text-slate-700">{{ c.task_count }}</td>
<td class="px-4 py-3 text-center text-sm {% if c.overdue_count > 0 %}font-semibold text-red-700{% else %}text-slate-700{% endif %}">{{ c.overdue_count }}</td>
<td class="whitespace-nowrap px-4 py-3 text-sm">
{% if row_status == 'active' %}<span class="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-700">Active</span>
{% elif row_status == 'archived' %}<span class="rounded-full bg-amber-100 px-2 py-1 text-xs font-medium text-amber-800">Archived</span>
{% else %}<span class="rounded-full bg-slate-200 px-2 py-1 text-xs font-medium text-slate-700">Inactive</span>{% endif %}
</td>
<td class="whitespace-nowrap px-4 py-3 text-right"><a href="{{ c.href }}" class="text-sm font-medium text-brand-700 hover:underline">Open</a></td>
</tr>
{% else %}
<tr><td colspan="8" class="px-4 py-8 text-center text-sm text-slate-500">No assigned clients found.</td></tr>
{% endfor %}
<tr data-no-results hidden><td colspan="8" class="px-4 py-8 text-center text-sm text-slate-500">No clients match the search.</td></tr>
</tbody>
</table>
</div> </div>
</div> </div>
<script>
(function () {
const root = document.querySelector('[data-partner-client-list]');
if (!root || root.dataset.ready === '1') return;
root.dataset.ready = '1';
const search = root.querySelector('[data-client-search]');
const status = root.querySelector('[data-client-status]');
const clear = root.querySelector('[data-client-clear]');
const scroll = root.querySelector('[data-client-scroll]');
const count = root.querySelector('[data-visible-count]');
const empty = root.querySelector('[data-no-results]');
const rows = Array.from(root.querySelectorAll('[data-client-row]'));
let timer;
function apply() {
const term = (search.value || '').trim().toLowerCase();
const wantedStatus = status.value;
let visible = 0;
rows.forEach((row) => {
const show = (!term || row.dataset.search.includes(term)) && (!wantedStatus || row.dataset.status === wantedStatus);
row.hidden = !show;
if (show) visible += 1;
});
count.textContent = visible + (visible === 1 ? ' client' : ' clients');
empty.hidden = visible !== 0;
scroll.scrollTop = 0;
}
search.addEventListener('input', () => { window.clearTimeout(timer); timer = window.setTimeout(apply, 250); });
status.addEventListener('change', apply);
clear.addEventListener('click', () => { search.value = ''; status.value = 'active'; apply(); search.focus(); });
apply();
})();
</script>