Files
arrr-erp/app/modules/registrations/templates/registrations/bulk.html
T
2026-08-10 12:33:08 +05:30

375 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
{% set _uiux_partner_role_text = (current_user_roles or [])|join('|')|lower %}
{% if 'partner' in _uiux_partner_role_text %}
{% include "ui/templates/components/partner_navigation_v2.html" %}
{% endif %}
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<h1 class="text-2xl font-semibold text-slate-900">Bulk Registration Addition</h1>
<p class="text-sm text-slate-500">
Add the same registration type to multiple clients while entering each client’s unique registration number.
</p>
</div>
<a href="/registrations"
class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">
Registrations Dashboard
</a>
</div>
{% if created or skipped or invalid %}
<div class="grid gap-3 sm:grid-cols-3">
<div class="rounded-2xl bg-white p-4 shadow-soft">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Created</div>
<div class="mt-1 text-2xl font-semibold text-emerald-700">{{ created }}</div>
</div>
<div class="rounded-2xl bg-white p-4 shadow-soft">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Duplicates Skipped</div>
<div class="mt-1 text-2xl font-semibold text-amber-700">{{ skipped }}</div>
</div>
<div class="rounded-2xl bg-white p-4 shadow-soft">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Invalid / Missing</div>
<div class="mt-1 text-2xl font-semibold text-rose-700">{{ invalid }}</div>
</div>
</div>
{% endif %}
<form method="post" action="/registrations/bulk" id="bulk-registration-form" class="space-y-5">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<section class="rounded-2xl bg-white p-5 shadow-soft">
<h2 class="text-base font-semibold text-slate-900">Common Registration Settings</h2>
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Registration Type *</label>
<select name="registration_type_id" required
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="">Select registration type</option>
{% for row in types %}
<option value="{{ row.id }}">{{ row.name }} ({{ row.code }})</option>
{% endfor %}
</select>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Default Scope</label>
<select name="default_scope"
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="client">Client / Entity</option>
<option value="primary_business">Primary Business Unit</option>
<option value="primary_branch">Primary Client Branch</option>
</select>
<p class="mt-1 text-xs text-slate-500">
Per-client Business Unit and Branch selections in the table override this default.
</p>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Lifecycle Rule</label>
<select name="lifecycle_rule_id"
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="">Automatic rule</option>
{% for rule, registration_type in rules %}
<option value="{{ rule.id }}">
{{ registration_type.name }} — {{ rule.cycle_type|replace('_', ' ')|title }}
</option>
{% endfor %}
</select>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Issue Date</label>
<input type="date" name="issue_date"
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Effective From</label>
<input type="date" name="effective_from"
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Valid Until</label>
<input type="date" name="valid_until"
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Next Update / Confirmation</label>
<input type="date" name="next_action_date"
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Jurisdiction</label>
<input name="jurisdiction"
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">State Code</label>
<input name="state_code" maxlength="10"
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Responsible Party</label>
<select name="responsible_party"
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="firm">Firm</option>
<option value="client">Client</option>
<option value="consultant">Consultant</option>
</select>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-slate-700">Assigned Consultant</label>
<select name="assigned_consultant_id"
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="">No consultant</option>
{% for row in consultants %}
<option value="{{ row.id }}">
{{ row.display_name or row.name or ('Consultant #' ~ row.id) }}
</option>
{% endfor %}
</select>
</div>
<div class="flex items-end">
<label class="inline-flex items-center gap-2 pb-2 text-sm text-slate-700">
<input type="checkbox" name="auto_create_task" checked
class="rounded border-slate-300">
Auto-create renewal work
</label>
</div>
<div class="md:col-span-2 xl:col-span-3">
<label class="mb-1 block text-sm font-medium text-slate-700">Common Notes</label>
<textarea name="notes" rows="2"
class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea>
</div>
</div>
</section>
<section class="rounded-2xl bg-white shadow-soft">
<div class="flex flex-wrap items-end justify-between gap-3 border-b border-slate-200 p-4">
<div class="flex flex-wrap items-end gap-3">
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">
Search Clients
</label>
<input type="search" id="client-search"
placeholder="Code, client name, PAN, GSTIN or trade name"
class="w-96 max-w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<button type="button" id="clear-search"
class="rounded-xl border border-slate-300 px-4 py-2 text-sm text-slate-700">
Clear
</button>
</div>
<div class="text-sm text-slate-600">
<span id="selected-count" class="font-semibold text-slate-900">0</span> clients selected
</div>
</div>
<div class="max-h-[36rem] overflow-auto">
<table class="min-w-[1500px] divide-y divide-slate-200">
<thead class="sticky top-0 z-10 bg-slate-50">
<tr>
<th class="w-12 px-3 py-3 text-left">
<input type="checkbox" id="select-all-visible" class="rounded border-slate-300">
</th>
<th class="px-3 py-3 text-left text-xs font-semibold uppercase text-slate-500">Client</th>
<th class="px-3 py-3 text-left text-xs font-semibold uppercase text-slate-500">PAN / Existing GSTIN</th>
<th class="px-3 py-3 text-left text-xs font-semibold uppercase text-slate-500">Registration Number *</th>
<th class="px-3 py-3 text-left text-xs font-semibold uppercase text-slate-500">Business Unit</th>
<th class="px-3 py-3 text-left text-xs font-semibold uppercase text-slate-500">Client Branch</th>
<th class="px-3 py-3 text-left text-xs font-semibold uppercase text-slate-500">Legal Name</th>
<th class="px-3 py-3 text-left text-xs font-semibold uppercase text-slate-500">Trade / Unit Name</th>
<th class="px-3 py-3 text-left text-xs font-semibold uppercase text-slate-500">State</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for client in clients %}
{% set client_businesses = businesses_by_client.get(client.id, []) %}
{% set client_branches = branches_by_client.get(client.id, []) %}
<tr class="client-row align-top"
data-search="{{ ((client.client_code or '') ~ ' ' ~ (client.client_name or '') ~ ' ' ~ (client.pan or '') ~ ' ' ~ (gstins_by_client.get(client.id, [])|join(' ')) ~ ' ' ~ (client.trade_name or ''))|lower|e }}">
<td class="px-3 py-3">
<input type="checkbox" name="client_ids" value="{{ client.id }}"
class="client-checkbox rounded border-slate-300">
</td>
<td class="px-3 py-3 text-sm">
<div class="font-medium text-slate-900">{{ client.client_name }}</div>
<div class="text-xs text-slate-500">{{ client.client_code or '' }}</div>
</td>
<td class="px-3 py-3 text-xs text-slate-600">
<div>PAN: {{ client.pan or '-' }}</div>
{% set existing_gstins = gstins_by_client.get(client.id, []) %}
<div>GSTIN: {{ existing_gstins|join(', ') if existing_gstins else '-' }}</div>
</td>
<td class="px-3 py-3">
<input name="registration_number_{{ client.id }}"
placeholder="Unique number"
class="w-48 rounded-lg border border-slate-300 px-2 py-1.5 text-sm uppercase">
</td>
<td class="px-3 py-3">
<select name="business_unit_id_{{ client.id }}"
class="business-select w-52 rounded-lg border border-slate-300 px-2 py-1.5 text-sm"
data-client="{{ client.id }}">
<option value="">Use default / Client level</option>
{% for row in client_businesses %}
<option value="{{ row.id }}">{{ row.business_name }}</option>
{% endfor %}
</select>
</td>
<td class="px-3 py-3">
<select name="client_branch_id_{{ client.id }}"
class="branch-select w-52 rounded-lg border border-slate-300 px-2 py-1.5 text-sm"
data-client="{{ client.id }}">
<option value="">Use default / No branch</option>
{% for row in client_branches %}
<option value="{{ row.id }}" data-business="{{ row.business_unit_id }}">
{{ row.branch_name }}
</option>
{% endfor %}
</select>
</td>
<td class="px-3 py-3">
<input name="legal_name_{{ client.id }}"
value="{{ client.client_name or '' }}"
class="w-52 rounded-lg border border-slate-300 px-2 py-1.5 text-sm">
</td>
<td class="px-3 py-3">
<input name="trade_name_{{ client.id }}"
value="{{ client.trade_name or '' }}"
class="w-52 rounded-lg border border-slate-300 px-2 py-1.5 text-sm">
</td>
<td class="px-3 py-3">
<input name="state_{{ client.id }}"
value="{{ client.state or '' }}"
class="w-40 rounded-lg border border-slate-300 px-2 py-1.5 text-sm">
</td>
</tr>
{% else %}
<tr>
<td colspan="9" class="px-4 py-8 text-center text-sm text-slate-500">
No active clients are available in the current Firm Branch.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<div class="flex flex-wrap items-center justify-between gap-3 rounded-2xl bg-white p-4 shadow-soft">
<p class="text-sm text-slate-600">
Rows without a registration number are treated as invalid. Existing registration numbers are skipped without changing their records.
</p>
<div class="flex gap-3">
<a href="/registrations"
class="rounded-xl border border-slate-300 px-4 py-2 text-sm text-slate-700">
Cancel
</a>
<button type="submit"
class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">
Add Registrations
</button>
</div>
</div>
</form>
</div>
<script>
(function () {
const form = document.getElementById('bulk-registration-form');
const search = document.getElementById('client-search');
const clear = document.getElementById('clear-search');
const rows = Array.from(document.querySelectorAll('.client-row'));
const boxes = Array.from(document.querySelectorAll('.client-checkbox'));
const selectAll = document.getElementById('select-all-visible');
const count = document.getElementById('selected-count');
function visibleRows() {
return rows.filter(row => !row.classList.contains('hidden'));
}
function refreshCount() {
count.textContent = String(boxes.filter(box => box.checked).length);
const visibleBoxes = visibleRows().map(row => row.querySelector('.client-checkbox'));
const checkedVisible = visibleBoxes.filter(box => box.checked).length;
selectAll.checked = visibleBoxes.length > 0 && checkedVisible === visibleBoxes.length;
selectAll.indeterminate = checkedVisible > 0 && checkedVisible < visibleBoxes.length;
}
function filterRows() {
const term = search.value.trim().toLowerCase();
rows.forEach(row => {
row.classList.toggle('hidden', term && !row.dataset.search.includes(term));
});
refreshCount();
}
search.addEventListener('input', filterRows);
clear.addEventListener('click', function () {
search.value = '';
filterRows();
search.focus();
});
boxes.forEach(box => box.addEventListener('change', refreshCount));
selectAll.addEventListener('change', function () {
visibleRows().forEach(row => {
row.querySelector('.client-checkbox').checked = selectAll.checked;
});
refreshCount();
});
document.querySelectorAll('.branch-select').forEach(branchSelect => {
branchSelect.addEventListener('change', function () {
const selected = branchSelect.options[branchSelect.selectedIndex];
const businessId = selected ? selected.dataset.business : '';
if (!businessId) return;
const businessSelect = document.querySelector(
`.business-select[data-client="${branchSelect.dataset.client}"]`
);
if (businessSelect) businessSelect.value = businessId;
});
});
form.addEventListener('submit', function (event) {
const selected = boxes.filter(box => box.checked);
if (!selected.length) {
event.preventDefault();
alert('Select at least one client.');
return;
}
const missing = selected.filter(box => {
const number = document.querySelector(
`[name="registration_number_${box.value}"]`
);
return !number || !number.value.trim();
});
if (missing.length) {
event.preventDefault();
alert('Enter the registration number for every selected client.');
return;
}
if (!confirm(`Add registrations for ${selected.length} selected client${selected.length === 1 ? '' : 's'}?`)) {
event.preventDefault();
}
});
refreshCount();
})();
</script>
{% endblock %}