Add System and Firm Admin task controls

This commit is contained in:
A R R R Associates
2026-08-08 15:02:30 +05:30
parent c746a7e323
commit 1a02af213d
3 changed files with 171 additions and 4 deletions
@@ -145,12 +145,111 @@
</section> </section>
<section class="rounded-2xl bg-white p-5 shadow-soft"> <section class="rounded-2xl bg-white p-5 shadow-soft">
<div class="flex items-center justify-between"><h3 class="text-sm font-semibold text-slate-900">System Default Tasks</h3>{% if can_edit %}<a href="/services/catalogue/{{ catalogue.id }}/defaults" class="text-sm font-medium text-brand-700 hover:underline">Manage</a>{% endif %}</div> <div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="text-sm font-semibold text-slate-900">System Default Tasks</h3>
<p class="mt-1 text-xs text-slate-500">System Admin controls the standard task template used as the base for firms.</p>
</div>
{% if is_system_admin and can_edit %}
<a href="/services/catalogue/{{ catalogue.id }}/defaults" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Manage Defaults</a>
{% endif %}
</div>
<div class="mt-4 space-y-3"> <div class="mt-4 space-y-3">
{% for task in default_templates %} {% for task in default_templates %}
<div class="rounded-xl border border-slate-200 p-4"><div class="text-sm font-semibold text-slate-900">{{ task.sequence_no }}. {{ task.task_name }}</div><div class="mt-1 text-xs text-slate-500">Role: {{ task.default_role_name or '-' }} · Mandatory: {{ 'Yes' if task.is_mandatory else 'No' }} · Review: {{ 'Yes' if task.requires_review else 'No' }}</div>{% if task.description %}<p class="mt-2 text-sm text-slate-700">{{ task.description }}</p>{% endif %}</div> <div class="rounded-xl border border-slate-200 p-4">
{% else %}<div class="rounded-xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No system default tasks configured yet.</div>{% endfor %} <div class="flex flex-wrap items-start justify-between gap-3">
<div class="min-w-0">
<div class="text-sm font-semibold text-slate-900">{{ task.sequence_no }}. {{ task.task_name }}</div>
<div class="mt-1 text-xs text-slate-500">
Role: {{ task.default_role_name or '-' }} ·
Mandatory: {{ 'Yes' if task.is_mandatory else 'No' }} ·
Review: {{ 'Yes' if task.requires_review else 'No' }} ·
{{ 'Active' if task.is_active else 'Inactive' }}
</div>
{% if task.description %}<p class="mt-2 text-sm text-slate-700">{{ task.description }}</p>{% endif %}
</div>
{% if is_system_admin and can_edit %}
<a href="/services/catalogue/{{ catalogue.id }}/defaults/{{ task.id }}/edit"
class="shrink-0 rounded-lg border border-brand-200 bg-brand-50 px-3 py-1.5 text-xs font-semibold text-brand-700 hover:bg-brand-100">
Edit
</a>
{% endif %}
</div>
</div>
{% else %}
<div class="rounded-xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">No system default tasks configured yet.</div>
{% endfor %}
</div> </div>
</section> </section>
{% if current_selection and current_selection.is_enabled %}
<section class="rounded-2xl bg-white p-5 shadow-soft">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="text-sm font-semibold text-slate-900">Firm Task Configuration</h3>
<p class="mt-1 text-xs text-slate-500">
Firm Admin can edit, disable or enable tasks for this firm and add extra firm-only tasks.
System defaults are not changed.
</p>
</div>
{% if can_manage_firm_tasks %}
<div class="flex flex-wrap gap-2">
<a href="/services/templates/{{ catalogue.id }}"
class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">
Add / Manage Firm Tasks
</a>
</div>
{% endif %}
</div>
<div class="mt-4 space-y-3">
{% for task in current_templates %}
<div class="rounded-xl border p-4 {% if task.is_active %}border-slate-200{% else %}border-slate-200 bg-slate-50 opacity-80{% endif %}">
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<div class="text-sm font-semibold text-slate-900">{{ task.sequence_no }}. {{ task.task_name }}</div>
<span class="rounded-full px-2 py-1 text-[11px] font-semibold {% if task.is_active %}bg-emerald-100 text-emerald-700{% else %}bg-slate-200 text-slate-600{% endif %}">
{{ 'Enabled for Firm' if task.is_active else 'Disabled for Firm' }}
</span>
</div>
<div class="mt-1 text-xs text-slate-500">
Role: {{ task.default_role_name or '-' }} ·
Mandatory: {{ 'Yes' if task.is_mandatory else 'No' }} ·
Review: {{ 'Yes' if task.requires_review else 'No' }}
</div>
{% if task.description %}<p class="mt-2 text-sm text-slate-700">{{ task.description }}</p>{% endif %}
</div>
{% if can_manage_firm_tasks %}
<div class="flex shrink-0 flex-wrap items-center gap-2">
<a href="/services/templates/{{ catalogue.id }}/tasks/{{ task.id }}/edit"
class="rounded-lg border border-brand-200 bg-brand-50 px-3 py-1.5 text-xs font-semibold text-brand-700 hover:bg-brand-100">
Edit
</a>
<form method="post" action="/services/templates/{{ catalogue.id }}/tasks/{{ task.id }}/toggle-active">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit"
class="rounded-lg border px-3 py-1.5 text-xs font-semibold {% if task.is_active %}border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100{% else %}border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100{% endif %}">
{{ 'Disable for Firm' if task.is_active else 'Enable for Firm' }}
</button>
</form>
</div>
{% endif %}
</div>
</div>
{% else %}
<div class="rounded-xl border border-dashed border-slate-300 p-6 text-sm text-slate-500">
No firm task templates are configured yet. Use <strong>Add / Manage Firm Tasks</strong> to copy defaults or add firm-specific tasks.
</div>
{% endfor %}
</div>
<div class="mt-4 rounded-xl border border-blue-200 bg-blue-50 px-4 py-3 text-xs text-blue-800">
Editing or disabling a firm task affects future task generation only. Existing engagement task instances remain unchanged.
</div>
</section>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}
@@ -186,7 +186,21 @@
{% endif %} {% endif %}
</td> </td>
<td class="px-4 py-3 text-right align-top"> <td class="px-4 py-3 text-right align-top">
{% if can_manage_tasks %}<a href="/services/templates/{{ service.id }}/tasks/{{ task.id }}/edit" class="text-sm font-medium text-brand-700 hover:underline">Edit</a>{% endif %} {% if can_manage_tasks %}
<div class="flex flex-col items-end gap-2">
<a href="/services/templates/{{ service.id }}/tasks/{{ task.id }}/edit"
class="rounded-lg border border-brand-200 bg-brand-50 px-3 py-1.5 text-xs font-semibold text-brand-700 hover:bg-brand-100">
Edit
</a>
<form method="post" action="/services/templates/{{ service.id }}/tasks/{{ task.id }}/toggle-active">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit"
class="rounded-lg border px-3 py-1.5 text-xs font-semibold {% if task.is_active %}border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100{% else %}border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100{% endif %}">
{{ 'Disable for Firm' if task.is_active else 'Enable for Firm' }}
</button>
</form>
</div>
{% endif %}
</td> </td>
</tr> </tr>
{% else %} {% else %}
+54
View File
@@ -602,6 +602,8 @@ def catalogue_detail(request: Request, catalogue_id: int):
catalogue=row, catalogue=row,
can_edit=can_edit, can_edit=can_edit,
can_manage_firm_services=_can_manage_firm_services(db, user), can_manage_firm_services=_can_manage_firm_services(db, user),
can_manage_firm_tasks=_can_manage_firm_tasks(db, user),
is_system_admin=_is_system_admin(db, user),
branches=branches, branches=branches,
current_selection=get_firm_selection(db, tenant_id=tenant_id, catalogue_id=row.id), current_selection=get_firm_selection(db, tenant_id=tenant_id, catalogue_id=row.id),
current_templates=get_firm_task_templates(db, tenant_id=tenant_id, catalogue_id=row.id), current_templates=get_firm_task_templates(db, tenant_id=tenant_id, catalogue_id=row.id),
@@ -1548,6 +1550,58 @@ def firm_task_document_template_download(request: Request, template_id: int):
db.close() db.close()
@router.post('/templates/{catalogue_id}/tasks/{task_id}/toggle-active')
def firm_task_template_toggle_active(
request: Request,
catalogue_id: int,
task_id: int,
csrf_token: str = Form(...),
):
"""Enable/disable a firm task template without changing the system default.
Existing engagement task instances are snapshots and are intentionally not
modified. This affects task generation for future engagements only.
"""
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url='/login', status_code=303)
if not _can_manage_firm_tasks(db, user):
return _redirect_denied()
tenant_id = _active_tenant_id(request, user)
selection = get_firm_selection(db, tenant_id=tenant_id, catalogue_id=catalogue_id)
if not selection or not selection.is_enabled:
return RedirectResponse(url='/services', status_code=303)
task = get_firm_task_template(
db,
tenant_id=tenant_id,
catalogue_id=catalogue_id,
task_id=task_id,
)
if not task:
return RedirectResponse(
url=f'/services/templates/{catalogue_id}?error=task_missing',
status_code=303,
)
task.is_active = not bool(task.is_active)
task.updated_by_user_id = user.id
db.commit()
state = 'enabled' if task.is_active else 'disabled'
return RedirectResponse(
url=f'/services/templates/{catalogue_id}?task_{state}=1',
status_code=303,
)
finally:
db.close()
@router.get('/templates/{catalogue_id}/tasks/{task_id}/edit') @router.get('/templates/{catalogue_id}/tasks/{task_id}/edit')
def firm_task_template_edit_page(request: Request, catalogue_id: int, task_id: int): def firm_task_template_edit_page(request: Request, catalogue_id: int, task_id: int):
db = CommonSessionLocal() db = CommonSessionLocal()