268 lines
17 KiB
HTML
268 lines
17 KiB
HTML
{% extends "ui/templates/base/layout.html" %}
|
||
{% block content %}
|
||
<div class="space-y-5">
|
||
<div class="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
|
||
<div>
|
||
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-brand-600">Clients · Unified Management</div>
|
||
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Client Management</h1>
|
||
<p class="mt-1 text-sm text-slate-500">Compact Client → Business → Branch → Registration management using the existing ERP masters and permissions.</p>
|
||
</div>
|
||
<div class="flex flex-wrap gap-2">
|
||
{% if can_create %}<a href="/clients/new" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">+ Add Client</a>{% endif %}
|
||
<a href="/clients" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Existing Client List</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-6">
|
||
{% for label, value in [
|
||
('Clients', counters.clients), ('Businesses', counters.businesses), ('Branches', counters.branches),
|
||
('Registrations', counters.registrations), ('GSTINs', counters.gstin), ('TANs', counters.tan)
|
||
] %}
|
||
<div class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||
<div class="text-xs font-semibold uppercase text-slate-500">{{ label }}</div>
|
||
<div class="mt-1 text-2xl font-semibold text-slate-900">{{ value }}</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<form id="client-management-filter" method="get" action="/clients/management" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-6">
|
||
<div class="xl:col-span-2">
|
||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Search</label>
|
||
<input id="client-management-search" name="q" value="{{ q }}" autocomplete="off" {% if q %}autofocus{% endif %} placeholder="Client, trade name, PAN, GSTIN, TAN, branch, email..." class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||
</div>
|
||
<div>
|
||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Client status</label>
|
||
<select name="status" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||
<option value="">All</option>
|
||
{% for value in ['active','inactive','archived'] %}<option value="{{ value }}" {% if status|lower == value %}selected{% endif %}>{{ value|title }}</option>{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Client type</label>
|
||
<select name="client_type" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||
<option value="">All</option>
|
||
{% for value in client_types %}<option value="{{ value }}" {% if client_type == value %}selected{% endif %}>{{ value }}</option>{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Registration</label>
|
||
<select name="registration_type" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||
<option value="">All</option>
|
||
{% for rt in registration_types %}<option value="{{ rt.code }}" {% if registration_type|lower == rt.code|lower %}selected{% endif %}>{{ rt.code }} · {{ rt.name }}</option>{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="mb-1 block text-xs font-semibold uppercase text-slate-500">Reg. status</label>
|
||
<select name="registration_status" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||
<option value="">All</option>
|
||
{% for value in registration_statuses %}<option value="{{ value }}" {% if registration_status == value %}selected{% endif %}>{{ value|title }}</option>{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="mt-3 flex flex-wrap items-center justify-between gap-3">
|
||
<div class="text-xs text-slate-500">Search covers client name/code, business/trade name, PAN, GSTIN, TAN, CIN/LLPIN, branch, mobile and email.</div>
|
||
<div class="flex gap-2">
|
||
<a href="/clients/management" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700">Clear</a>
|
||
<button class="rounded-xl bg-slate-900 px-5 py-2 text-sm font-semibold text-white">Apply</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-soft">
|
||
<div class="flex items-center justify-between border-b border-slate-200 px-4 py-3">
|
||
<div>
|
||
<div class="font-semibold text-slate-900">{{ total }} client{{ '' if total == 1 else 's' }}</div>
|
||
<div class="text-xs text-slate-500">Expand only the client you need. Existing detail/edit/business-structure pages remain available.</div>
|
||
</div>
|
||
<div class="text-xs font-medium text-slate-500">Page {{ page }} of {{ pages }}</div>
|
||
</div>
|
||
|
||
{% for node in hierarchy %}
|
||
{% set client = node.client %}
|
||
<details class="group border-b border-slate-100 last:border-b-0" {% if q %}open{% endif %}>
|
||
<summary class="cursor-pointer list-none px-4 py-4 hover:bg-slate-50">
|
||
<div class="grid items-center gap-3 lg:grid-cols-[minmax(280px,1.7fr)_140px_110px_120px_210px]">
|
||
<div class="flex min-w-0 items-start gap-3">
|
||
<span class="mt-0.5 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-blue-50 text-blue-700 transition group-open:rotate-90">›</span>
|
||
<div class="min-w-0">
|
||
<div class="truncate font-semibold text-blue-700">{{ client.client_name }}</div>
|
||
<div class="mt-0.5 flex flex-wrap gap-x-2 text-xs text-slate-500">
|
||
<span>{{ client.client_code }}</span>
|
||
{% if client.trade_name and client.trade_name != client.client_name %}<span>· {{ client.trade_name }}</span>{% endif %}
|
||
{% if client.pan %}<span>· PAN {{ client.pan }}</span>{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div><span class="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-700">{{ client.client_type }}</span></div>
|
||
<div class="text-sm"><span class="font-semibold">{{ node.business_count }}</span><span class="ml-1 text-xs text-slate-500">business</span></div>
|
||
<div class="text-sm"><span class="font-semibold">{{ node.branch_count }}</span><span class="ml-1 text-xs text-slate-500">branch</span></div>
|
||
<div class="flex flex-wrap justify-end gap-1.5" onclick="event.stopPropagation()">
|
||
{% if can_edit %}
|
||
<a href="/clients/{{ client.id }}/business-structure?action=business#add-business" class="rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-1.5 text-xs font-semibold text-emerald-700">+ Business</a>
|
||
<a href="/clients/{{ client.id }}/business-structure?action=branch#add-branch" class="rounded-lg border border-blue-200 bg-blue-50 px-2.5 py-1.5 text-xs font-semibold text-blue-700">+ Branch</a>
|
||
<a href="/clients/{{ client.id }}/business-structure?action=registration#add-registration" class="rounded-lg border border-violet-200 bg-violet-50 px-2.5 py-1.5 text-xs font-semibold text-violet-700">+ Registration</a>
|
||
{% endif %}
|
||
<a href="/clients/{{ client.id }}" class="rounded-lg border border-slate-300 px-2.5 py-1.5 text-xs font-semibold text-slate-700">View</a>
|
||
<a href="/clients/{{ client.id }}/business-structure" class="rounded-lg border border-brand-200 bg-brand-50 px-2.5 py-1.5 text-xs font-semibold text-brand-700">Manage</a>
|
||
{% if can_edit %}<a href="/clients/{{ client.id }}/edit" class="rounded-lg border border-slate-300 px-2.5 py-1.5 text-xs font-semibold text-slate-700">Edit</a>{% endif %}
|
||
</div>
|
||
</div>
|
||
</summary>
|
||
|
||
<div class="border-t border-slate-100 bg-slate-50/70 px-4 py-4">
|
||
{% if node.businesses %}
|
||
<div class="space-y-3">
|
||
{% for business_node in node.businesses %}
|
||
{% set business = business_node.row %}
|
||
<div class="overflow-hidden rounded-xl border border-slate-200 bg-white">
|
||
<div class="flex flex-col gap-2 border-b border-slate-100 px-4 py-3 md:flex-row md:items-center md:justify-between">
|
||
<div>
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<span class="text-sm font-semibold text-slate-900">{{ business.business_name }}</span>
|
||
{% if business.is_primary %}<span class="rounded-full bg-emerald-50 px-2 py-0.5 text-[11px] font-semibold text-emerald-700">Primary Business</span>{% endif %}
|
||
{% if not business.is_active %}<span class="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-semibold text-slate-500">Inactive</span>{% endif %}
|
||
</div>
|
||
<div class="mt-0.5 text-xs text-slate-500">{{ business.business_code }}{% if business.trade_name %} · Trade: {{ business.trade_name }}{% endif %}{% if business.nature_of_business %} · {{ business.nature_of_business }}{% endif %}</div>
|
||
</div>
|
||
<div class="flex flex-wrap gap-2">
|
||
{% if can_edit %}
|
||
<a href="/clients/{{ client.id }}/business-structure?action=branch&business_unit_id={{ business.id }}#add-branch" class="rounded-lg border border-blue-200 bg-blue-50 px-2.5 py-1 text-xs font-semibold text-blue-700">+ Branch</a>
|
||
<a href="/clients/{{ client.id }}/business-structure?action=registration&business_unit_id={{ business.id }}#add-registration" class="rounded-lg border border-violet-200 bg-violet-50 px-2.5 py-1 text-xs font-semibold text-violet-700">+ Registration</a>
|
||
{% endif %}
|
||
<a href="/clients/{{ client.id }}/business-structure" class="rounded-lg border border-slate-200 px-2.5 py-1 text-xs font-semibold text-brand-700">Manage business</a>
|
||
</div>
|
||
</div>
|
||
|
||
{% for branch_node in business_node.branches %}
|
||
{% set branch = branch_node.row %}
|
||
<div class="border-b border-slate-100 px-5 py-3 last:border-b-0">
|
||
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||
<div>
|
||
<span class="text-sm font-medium text-slate-800">↳ {{ branch.branch_name }}</span>
|
||
<span class="ml-2 text-xs text-slate-500">{{ branch.branch_code }}{% if branch.city %} · {{ branch.city }}{% endif %}{% if branch.state %}, {{ branch.state }}{% endif %}</span>
|
||
{% if branch.is_primary %}<span class="ml-2 rounded-full bg-blue-50 px-2 py-0.5 text-[11px] font-semibold text-blue-700">Primary Branch</span>{% endif %}
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-xs text-slate-500">{{ branch_node.registrations|length }} registration{{ '' if branch_node.registrations|length == 1 else 's' }}</span>
|
||
{% if can_edit %}
|
||
<a href="/clients/{{ client.id }}/business-structure?action=registration&business_unit_id={{ business.id }}&client_branch_id={{ branch.id }}#add-registration" class="rounded-lg border border-violet-200 bg-violet-50 px-2.5 py-1 text-xs font-semibold text-violet-700">+ Registration</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% if branch_node.registrations %}
|
||
<div class="mt-2 grid gap-2 xl:grid-cols-2">
|
||
{% for reg, rt in branch_node.registrations %}
|
||
<div class="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
|
||
<div class="min-w-0">
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<span class="rounded-md bg-white px-2 py-0.5 text-[11px] font-bold text-slate-700 ring-1 ring-slate-200">{{ rt.code }}</span>
|
||
<span class="truncate text-sm font-medium text-slate-800">{{ reg.registration_number }}</span>
|
||
<span class="rounded-full px-2 py-0.5 text-[11px] font-semibold {% if reg.status|lower in ['active','valid','registered'] %}bg-emerald-50 text-emerald-700{% else %}bg-slate-100 text-slate-600{% endif %}">{{ reg.status|title }}</span>
|
||
</div>
|
||
{% if reg.trade_name or reg.legal_name %}<div class="mt-0.5 truncate text-xs text-slate-500">{{ reg.trade_name or reg.legal_name }}</div>{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
|
||
{% if business_node.registrations %}
|
||
<div class="px-5 py-3">
|
||
<div class="mb-2 text-xs font-semibold uppercase text-slate-500">Business-level registrations</div>
|
||
<div class="grid gap-2 xl:grid-cols-2">
|
||
{% for reg, rt in business_node.registrations %}
|
||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm">
|
||
<span class="mr-2 rounded-md bg-white px-2 py-0.5 text-[11px] font-bold ring-1 ring-slate-200">{{ rt.code }}</span>
|
||
<span class="font-medium">{{ reg.registration_number }}</span>
|
||
<span class="ml-2 text-xs text-slate-500">{{ reg.trade_name or reg.legal_name or '' }}</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="rounded-xl border border-dashed border-slate-300 bg-white px-4 py-5 text-center text-sm text-slate-500">
|
||
No Business/Business Unit has been created for this client.
|
||
{% if can_edit %}<a href="/clients/{{ client.id }}/business-structure" class="ml-1 font-semibold text-brand-700">Create business structure</a>{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if node.unassigned_registrations %}
|
||
<div class="mt-3 rounded-xl border border-amber-200 bg-amber-50/60 p-4">
|
||
<div class="text-xs font-semibold uppercase text-amber-800">Client-level / unassigned registrations</div>
|
||
<div class="mt-2 grid gap-2 xl:grid-cols-2">
|
||
{% for reg, rt in node.unassigned_registrations %}
|
||
<div class="rounded-lg border border-amber-200 bg-white px-3 py-2 text-sm">
|
||
<span class="mr-2 rounded-md bg-amber-50 px-2 py-0.5 text-[11px] font-bold text-amber-800">{{ rt.code }}</span>
|
||
<span class="font-medium">{{ reg.registration_number }}</span>
|
||
<span class="ml-2 text-xs text-slate-500">{{ reg.trade_name or reg.legal_name or '' }}</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</details>
|
||
{% else %}
|
||
<div class="px-6 py-16 text-center">
|
||
<div class="text-base font-semibold text-slate-700">No clients match the selected filters.</div>
|
||
<div class="mt-1 text-sm text-slate-500">Clear one or more filters and try again.</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
{% if pages > 1 %}
|
||
<div class="flex flex-col gap-3 rounded-2xl border border-slate-200 bg-white px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||
<div class="text-sm text-slate-500">Showing {{ ((page - 1) * per_page) + 1 if total else 0 }}–{{ [page * per_page, total]|min }} of {{ total }}</div>
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
{% set qs = 'q=' ~ (q|urlencode) ~ '&status=' ~ (status|urlencode) ~ '&client_type=' ~ (client_type|urlencode) ~ '®istration_type=' ~ (registration_type|urlencode) ~ '®istration_status=' ~ (registration_status|urlencode) ~ '&per_page=' ~ per_page %}
|
||
{% if page > 1 %}<a href="/clients/management?{{ qs }}&page={{ page - 1 }}" class="rounded-lg border border-slate-300 px-3 py-1.5 text-sm">Previous</a>{% endif %}
|
||
<span class="px-2 text-sm font-medium text-slate-700">{{ page }} / {{ pages }}</span>
|
||
{% if page < pages %}<a href="/clients/management?{{ qs }}&page={{ page + 1 }}" class="rounded-lg border border-slate-300 px-3 py-1.5 text-sm">Next</a>{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
const form = document.getElementById('client-management-filter');
|
||
const search = document.getElementById('client-management-search');
|
||
if (!form || !search) return;
|
||
|
||
let timer = null;
|
||
let composing = false;
|
||
|
||
function submitFromFilter() {
|
||
const page = form.querySelector('input[name="page"]');
|
||
if (page) page.value = '1';
|
||
form.requestSubmit();
|
||
}
|
||
|
||
search.addEventListener('compositionstart', function () { composing = true; });
|
||
search.addEventListener('compositionend', function () {
|
||
composing = false;
|
||
clearTimeout(timer);
|
||
timer = setTimeout(submitFromFilter, 350);
|
||
});
|
||
|
||
search.addEventListener('input', function () {
|
||
if (composing) return;
|
||
clearTimeout(timer);
|
||
timer = setTimeout(submitFromFilter, 350);
|
||
});
|
||
|
||
form.querySelectorAll('select').forEach(function (select) {
|
||
select.addEventListener('change', submitFromFilter);
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
{% endblock %}
|