Add compact live-filter client list for Firm Admin

This commit is contained in:
A R R R Associates
2026-08-02 10:26:01 +05:30
parent 6c308c4217
commit a7da29badb
3 changed files with 103 additions and 7 deletions
@@ -34,7 +34,7 @@
{% include "modules/clients/templates/clients/partials/table.html" %} {% include "modules/clients/templates/clients/partials/table.html" %}
{% if meta and not partner_client_view %} {% if meta and not compact_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 %}
@@ -136,6 +136,98 @@
apply(); apply();
})(); })();
</script> </script>
{% elif firm_admin_client_view %}
<div class="rounded-2xl bg-white p-4 shadow-soft" data-firm-admin-client-list>
<div class="flex flex-col gap-3 xl:flex-row xl: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, group or partner"
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-48">
<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-52">
<span class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Partner</span>
<select data-client-partner 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 partners</option>
{% set partner_names = [] %}
{% for row in rows %}{% if row.partner_name and row.partner_name not in partner_names %}{% set _ = partner_names.append(row.partner_name) %}{% endif %}{% endfor %}
{% for partner_name in partner_names|sort %}<option value="{{ partner_name|lower|e }}">{{ partner_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">Branch</span>
<select data-client-branch 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 branches</option>
{% set branch_names = [] %}
{% for row in rows %}{% if row.branch_name and row.branch_name not in branch_names %}{% set _ = branch_names.append(row.branch_name) %}{% endif %}{% endfor %}
{% for branch_name in branch_names|sort %}<option value="{{ branch_name|lower|e }}">{{ branch_name }}</option>{% endfor %}
</select>
</label>
<label class="w-full sm:w-40">
<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-44">
<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><option value="partner">Partner</option><option value="branch">Branch</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>Firm client administration</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">Partner</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 %}{% set row_partner = (row.partner_name or '')|lower %}{% set row_branch = (row.branch_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-partner="{{ row_partner|e }}" data-branch="{{ row_branch|e }}" data-status="{{ row_status|e }}" data-search="{{ ((row.client_code or '') ~ ' ' ~ (row.client_name or '') ~ ' ' ~ (row.trade_name or '') ~ ' ' ~ (row.pan or '') ~ ' ' ~ (row.gstin or '') ~ ' ' ~ (row.client_group_name or '') ~ ' ' ~ (row.client_group_code or '') ~ ' ' ~ (row.partner_name or '') ~ ' ' ~ (row.branch_name 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.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="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="7" 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="7" 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-firm-admin-client-list]'); if (!root || root.dataset.ready === '1') return; root.dataset.ready = '1';
const search=root.querySelector('[data-client-search]'), group=root.querySelector('[data-client-group]'), partner=root.querySelector('[data-client-partner]'), branch=root.querySelector('[data-client-branch]'), status=root.querySelector('[data-client-status]'), sort=root.querySelector('[data-client-sort]'), clear=root.querySelector('[data-client-clear]'), scroll=root.querySelector('[data-client-scroll]'), body=root.querySelector('[data-client-body]'), count=root.querySelector('[data-visible-count]'), empty=root.querySelector('[data-no-results]'), rows=Array.from(root.querySelectorAll('[data-client-row]')); let timer;
function apply(){const term=(search.value||'').trim().toLowerCase(), wg=group.value, wp=partner.value, wb=branch.value, ws=status.value; let visible=0; rows.forEach(row=>{const show=(!term||row.dataset.search.includes(term))&&(!wg||row.dataset.group===wg)&&(!wp||row.dataset.partner===wp)&&(!wb||row.dataset.branch===wb)&&(!ws||row.dataset.status===ws); 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);partner.addEventListener('change',apply);branch.addEventListener('change',apply);status.addEventListener('change',apply);sort.addEventListener('change',apply);clear.addEventListener('click',()=>{search.value='';group.value='';partner.value='';branch.value='';status.value='active';sort.value='code';apply();search.focus();});apply();
})();
</script>
{% else %} {% 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">
+10 -6
View File
@@ -322,6 +322,8 @@ def clients_list(
role_names = _role_names(db, user) role_names = _role_names(db, user)
partner_client_view = is_partner_role(role_names) partner_client_view = is_partner_role(role_names)
firm_admin_client_view = "firm admin" in role_names and not partner_client_view
compact_client_view = partner_client_view or firm_admin_client_view
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)
@@ -348,13 +350,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="" if partner_client_view else filters.q, q="" if compact_client_view else filters.q,
status="" if partner_client_view else filters.status, status="" if compact_client_view else filters.status,
client_type="" if partner_client_view else filters.client_type, client_type="" if compact_client_view else filters.client_type,
client_group_id=None if partner_client_view else filters.client_group_id, client_group_id=None if compact_client_view else filters.client_group_id,
include_archived=filters.include_archived, include_archived=filters.include_archived,
page=1 if partner_client_view else filters.page, page=1 if compact_client_view else filters.page,
per_page=5000 if partner_client_view else filters.per_page, per_page=5000 if compact_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,
) )
@@ -371,6 +373,8 @@ def clients_list(
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, partner_client_view=partner_client_view,
firm_admin_client_view=firm_admin_client_view,
compact_client_view=compact_client_view,
q=filters.q, q=filters.q,
status=filters.status, status=filters.status,
client_type=filters.client_type, client_type=filters.client_type,