Add System and Firm Admin task controls
This commit is contained in:
@@ -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 %}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user