Files
arrr-erp/app/modules/services/templates/services/subscriptions/bulk.html
T
2026-08-05 19:36:55 +05:30

163 lines
15 KiB
HTML

{% 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><h2 class="text-xl font-semibold text-slate-900">Bulk Subscription Setup</h2>
<p class="text-sm text-slate-500">The selected service determines whether you select Clients, Business Units, Client Branches or registrations.</p></div>
<a href="/services/subscriptions" class="rounded-xl border border-slate-300 px-4 py-2 text-sm">Back</a>
</div>
{% if error_message %}<div class="rounded-2xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">{{ error_message }}</div>{% endif %}
{% if subscriptions_created or subscriptions_reused or engagements_created or engagements_skipped %}
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{% for label,value in [('Subscriptions Created',subscriptions_created),('Existing Reused',subscriptions_reused),('Engagements Created',engagements_created),('Duplicates Skipped',engagements_skipped)] %}
<div class="rounded-2xl 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">{{ value }}</div></div>
{% endfor %}
</div>{% endif %}
<form method="post" action="/services/subscriptions/bulk" id="bulk-subscription-form" class="space-y-5">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="grid gap-4 rounded-2xl bg-white p-5 shadow-soft md:grid-cols-2 xl:grid-cols-3">
<div><label class="mb-2 block text-sm font-medium">Financial Year</label><input name="financial_year" id="bulk-financial-year" value="{{ financial_year }}" required class="w-full rounded-xl border px-4 py-2 text-sm"></div>
<div class="xl:col-span-2"><label class="mb-2 block text-sm font-medium">Enabled Firm Service</label>
<select name="service_catalogue_id" id="bulk-service" required class="w-full rounded-xl border px-4 py-2 text-sm">
<option value="">Select service</option>
{% for selection in enabled_services %}
<option value="{{ selection.catalogue.id }}" data-type="{{ selection.catalogue.engagement_type or 'non_assurance' }}" data-recurrence="{{ selection.catalogue.recurrence_type or '' }}" data-scope="{{ selection.catalogue.service_scope_type or 'client' }}" data-registration="{{ selection.catalogue.required_registration_type or '' }}">
{{ selection.catalogue.service_name }} ({{ selection.catalogue.service_code }})
</option>{% endfor %}
</select>
<p id="service-derived-info" class="mt-1 text-xs text-slate-500">Select a service to load the applicable subscription scope.</p>
</div>
<div><label class="mb-2 block text-sm font-medium">Return / Engagement Period</label><select name="period_label" id="bulk-period" class="w-full rounded-xl border px-4 py-2 text-sm"><option value="">Not applicable</option></select></div>
<div><label class="mb-2 block text-sm font-medium">Engagement Partner</label><select name="default_partner_user_id" required class="w-full rounded-xl border px-4 py-2 text-sm"><option value="">Select partner</option>{% for u in partners %}<option value="{{ u.id }}">{{ u.full_name or u.email }}</option>{% endfor %}</select></div>
<div><label class="mb-2 block text-sm font-medium">Performing Partner</label><select name="default_performing_partner_user_id" class="w-full rounded-xl border px-4 py-2 text-sm"><option value="">Same as Engagement Partner</option>{% for u in partners %}<option value="{{ u.id }}">{{ u.full_name or u.email }}</option>{% endfor %}</select></div>
<div><label class="mb-2 block text-sm font-medium">Default Manager</label><select name="default_manager_user_id" class="w-full rounded-xl border px-4 py-2 text-sm"><option value="">Not assigned</option>{% for u in managers %}<option value="{{ u.id }}">{{ u.full_name or u.email }}</option>{% endfor %}</select></div>
<div><label class="mb-2 block text-sm font-medium">Default Staff</label><select name="default_staff_user_id" class="w-full rounded-xl border px-4 py-2 text-sm"><option value="">Not assigned</option>{% for u in staff_users %}<option value="{{ u.id }}">{{ u.full_name or u.email }}</option>{% endfor %}</select></div>
<div><label class="mb-2 block text-sm font-medium">Review Partner</label><select name="default_review_partner_user_id" id="bulk-review-partner" class="w-full rounded-xl border px-4 py-2 text-sm"><option value="">Not assigned</option>{% for u in review_partners %}<option value="{{ u.id }}">{{ u.full_name or u.email }}</option>{% endfor %}</select></div>
<div><label class="mb-2 block text-sm font-medium">Effective From</label><input type="date" name="effective_from" class="w-full rounded-xl border px-4 py-2 text-sm"></div>
<div><label class="mb-2 block text-sm font-medium">Effective To</label><input type="date" name="effective_to" class="w-full rounded-xl border px-4 py-2 text-sm"></div>
<div class="md:col-span-2 xl:col-span-3"><label class="mb-2 block text-sm font-medium">Remarks applied to all subscriptions and generated engagements</label><textarea name="remarks" rows="2" class="w-full rounded-xl border px-4 py-2 text-sm"></textarea></div>
</div>
<div class="rounded-2xl bg-white p-5 shadow-soft">
<h3 class="font-semibold">Engagement Generation</h3>
<div class="mt-4 grid gap-3 lg:grid-cols-3">
{% for value,title,help in [
('subscription_only','Subscriptions only','Create or reuse the service master without generating engagements.'),
('current_period','One engagement','Generate one selected period for each chosen scope.'),
('all_periods','All FY engagements','Generate all monthly or quarterly periods, or one annual engagement.')
] %}
<label class="flex items-start gap-3 rounded-xl border p-4"><input type="radio" name="generation_mode" value="{{ value }}" {% if value=='subscription_only' %}checked{% endif %} class="mt-1"><span><span class="block text-sm font-medium">{{ title }}</span><span class="text-xs text-slate-500">{{ help }}</span></span></label>
{% endfor %}
</div>
<label class="mt-4 inline-flex gap-2 text-sm"><input type="checkbox" name="auto_generate_periods" value="1"> Keep automatic period generation enabled</label>
</div>
<div class="rounded-2xl bg-white shadow-soft">
<div class="flex flex-wrap items-end justify-between gap-3 border-b p-4">
<div class="flex flex-wrap items-end gap-3">
<div><label id="scope-search-label" class="mb-1 block text-xs font-semibold uppercase text-slate-500">Select subscription scope</label>
<input type="search" id="scope-search" disabled placeholder="Select an enabled firm service first" class="w-96 max-w-full rounded-xl border px-3 py-2 text-sm"></div>
<button type="button" id="clear-search" class="rounded-xl border px-4 py-2 text-sm">Clear</button>
</div>
<div class="text-sm"><span id="selected-count" class="font-semibold">0</span> <span id="selected-noun">items</span> selected</div>
</div>
<div id="scope-message" class="p-8 text-center text-sm text-slate-500">Select an enabled firm service to load Clients, Business Units, Client Branches or registrations.</div>
<div id="scope-table-wrap" class="hidden max-h-[32rem] overflow-auto">
<table class="min-w-full divide-y">
<thead class="sticky top-0 bg-slate-50"><tr id="scope-table-head"></tr></thead>
<tbody id="scope-table-body" class="divide-y"></tbody>
</table>
</div>
</div>
<div class="flex justify-end gap-3 rounded-2xl bg-white p-4 shadow-soft">
<a href="/services/subscriptions" class="rounded-xl border px-4 py-2 text-sm">Cancel</a>
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm text-white">Create Subscriptions</button>
</div>
</form>
</div>
<script>
(function(){
const form=document.getElementById('bulk-subscription-form'), service=document.getElementById('bulk-service'),
search=document.getElementById('scope-search'), clear=document.getElementById('clear-search'),
head=document.getElementById('scope-table-head'), body=document.getElementById('scope-table-body'),
wrap=document.getElementById('scope-table-wrap'), message=document.getElementById('scope-message'),
count=document.getElementById('selected-count'), noun=document.getElementById('selected-noun'),
label=document.getElementById('scope-search-label'), info=document.getElementById('service-derived-info'),
fy=document.getElementById('bulk-financial-year'), period=document.getElementById('bulk-period'),
review=document.getElementById('bulk-review-partner');
let targets=[], scopeType='client', registrationType='';
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const meta={
client:{title:'Search Clients',noun:'clients',placeholder:'Code, client name, PAN or entity type',columns:[['client_code','Code'],['client_name','Client'],['pan','PAN'],['entity_type','Type']]},
business_unit:{title:'Search Business Units',noun:'business units',placeholder:'Client, Business Unit, trade name, PAN or nature',columns:[['client_name','Client'],['pan','PAN'],['business_unit','Business Unit'],['trade_name','Trade Name']]},
client_branch:{title:'Search Client Branches',noun:'client branches',placeholder:'Client, Business Unit, Client Branch, code or state',columns:[['client_name','Client'],['business_unit','Business Unit'],['client_branch','Client Branch'],['state','State']]},
registration:{title:'Search Registrations',noun:'registrations',placeholder:'Client, business, branch, registration number or state',columns:[['client_name','Client'],['business_unit','Business Unit'],['client_branch','Client Branch'],['trade_name','Trade / Unit Name'],['registration_number','Registration'],['state','State']]}
};
function checked(){return Array.from(body.querySelectorAll('.target-box:checked'));}
function updateCount(){count.textContent=checked().length;}
function render(){
const m=meta[scopeType]||meta.client;
label.textContent=registrationType?`Search ${registrationType} Registrations`:m.title;
noun.textContent=registrationType?`${registrationType} registrations`:m.noun;
search.placeholder=m.placeholder;
head.innerHTML=`<th class="w-12 px-4 py-3"><input type="checkbox" id="select-all"></th>`+
m.columns.map(c=>`<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">${esc(c[1])}</th>`).join('')+
`<th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Subscription Status</th>`;
const term=search.value.trim().toLowerCase();
const visible=targets.filter(t=>!term||Object.values(t).join(' ').toLowerCase().includes(term));
body.innerHTML=visible.map(t=>`<tr>
<td class="px-4 py-3"><input class="target-box" type="checkbox" name="scope_targets" value="${esc(t.token)}"></td>
${m.columns.map(c=>`<td class="px-4 py-3 text-sm ${c[0]==='client_name'||c[0]==='business_unit'||c[0]==='client_branch'||c[0]==='registration_number'?'font-medium':''}">${esc(t[c[0]]||'-')}</td>`).join('')}
<td class="px-4 py-3 text-xs"><span class="rounded-full px-2 py-1 ${t.subscription_status==='not_subscribed'?'bg-slate-100 text-slate-600':'bg-emerald-100 text-emerald-700'}">${esc(t.subscription_status.replaceAll('_',' '))}</span></td>
</tr>`).join('');
document.querySelectorAll('.target-box').forEach(x=>x.addEventListener('change',updateCount));
const all=document.getElementById('select-all');
if(all) all.addEventListener('change',()=>{document.querySelectorAll('.target-box').forEach(x=>x.checked=all.checked);updateCount();});
updateCount();
}
async function loadTargets(){
const id=service.value;
targets=[]; body.innerHTML=''; count.textContent='0';
if(!id){wrap.classList.add('hidden');message.classList.remove('hidden');search.disabled=true;return;}
search.disabled=true; message.textContent='Loading applicable subscription scope...';message.classList.remove('hidden');wrap.classList.add('hidden');
const response=await fetch(`/services/subscriptions/bulk/targets?service_catalogue_id=${encodeURIComponent(id)}`,{headers:{'Accept':'application/json'}});
const data=await response.json();
if(!response.ok){message.textContent=data.error||'Unable to load subscription scope.';return;}
scopeType=data.scope_type||'client';registrationType=data.registration_type||'';targets=data.targets||[];
search.disabled=false;message.classList.toggle('hidden',targets.length>0);wrap.classList.toggle('hidden',targets.length===0);
if(!targets.length)message.textContent='No permitted scope records are available. Add the required Business Unit, Client Branch or Registration in the Client Business Structure page.';
render();
}
function updateService(){
const o=service.options[service.selectedIndex], type=o?.dataset.type||'', recurrence=o?.dataset.recurrence||'', scope=o?.dataset.scope||'client', reg=o?.dataset.registration||'';
review.required=type==='assurance';
info.textContent=o&&o.value?`Scope: ${scope.replaceAll('_',' ')}${reg?' — '+reg:''} · Type: ${type.replaceAll('_',' ')} · Recurrence: ${recurrence.replaceAll('_',' ')||'one time'}`:'Select a service to load the applicable subscription scope.';
rebuildPeriods(recurrence);
loadTargets();
}
function rebuildPeriods(recurrence){
const mode=document.querySelector('input[name="generation_mode"]:checked').value;
period.innerHTML='<option value="">Not applicable</option>';period.disabled=mode!=='current_period';period.required=false;
const start=parseInt((fy.value||'').split('-')[0]);
if(mode!=='current_period'||!start)return;
if(recurrence==='monthly'){[['04','Apr',start],['05','May',start],['06','Jun',start],['07','Jul',start],['08','Aug',start],['09','Sep',start],['10','Oct',start],['11','Nov',start],['12','Dec',start],['01','Jan',start+1],['02','Feb',start+1],['03','Mar',start+1]].forEach(r=>period.add(new Option(`${r[1]} ${r[2]}`,`${r[2]}-${r[0]}`)));period.required=true;}
else if(recurrence==='quarterly'){['Q1','Q2','Q3','Q4'].forEach(q=>period.add(new Option(`${q} ${fy.value}`,q)));period.required=true;}
}
service.addEventListener('change',updateService);search.addEventListener('input',render);clear.addEventListener('click',()=>{search.value='';render();search.focus();});
fy.addEventListener('input',updateService);document.querySelectorAll('input[name="generation_mode"]').forEach(x=>x.addEventListener('change',updateService));
form.addEventListener('submit',e=>{if(!checked().length){e.preventDefault();alert('Select at least one applicable subscription scope.');}});
})();
</script>
{% endblock %}