Add compact live-filter client lists for partners
This commit is contained in:
@@ -34,7 +34,7 @@
|
||||
|
||||
{% 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="text-sm text-slate-600">
|
||||
{% 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">
|
||||
<table class="min-w-full divide-y divide-slate-200">
|
||||
<thead class="bg-slate-50">
|
||||
@@ -16,40 +155,16 @@
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<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">
|
||||
<div class="font-medium">{{ 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">
|
||||
<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"><div class="font-medium">{{ 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"><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.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>
|
||||
<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="8" class="px-4 py-8 text-center text-sm text-slate-500">No clients found.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}<tr><td colspan="8" class="px-4 py-8 text-center text-sm text-slate-500">No clients found.</td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user