Add task administration upgrade with duplicate merge and task tools

This commit is contained in:
A R R R Associates
2026-08-30 22:12:27 +05:30
parent ad0815c807
commit af4f6e8b28
17 changed files with 2072 additions and 14 deletions
@@ -98,6 +98,11 @@
{% endfor %}
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-emerald-800">Task Tool</label>
<select name="task_tool_code" class="w-full rounded-xl border border-emerald-200 px-3 py-2 text-sm">{% for tool in task_tools %}<option value="{{ tool.code }}" {% if (task.task_tool_code or 'NONE') == tool.code %}selected{% endif %}>{{ tool.name }}</option>{% endfor %}</select>
<p class="mt-1 text-xs text-emerald-700">Links an existing ERP verification workspace to this task; normal checklist behavior is preserved.</p>
</div>
</div>
<div class="mt-4 grid gap-3 text-sm text-slate-700 md:grid-cols-3">
<label class="inline-flex items-center gap-2"><input type="checkbox" name="response_required" {% if task.response_required %}checked{% endif %}> Response required</label>
@@ -6,8 +6,15 @@
<h2 class="text-xl font-semibold text-slate-900">{{ service.service_name }}</h2>
<p class="text-sm text-slate-500">System default task templates for {{ service.service_code }}</p>
</div>
<div class="flex gap-2"><a href="/services/catalogue/{{ service.id }}/task-categories" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Task Categories</a><a href="/services/defaults" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Back</a><a href="/services/catalogue/{{ service.id }}" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Catalogue Detail</a></div>
<div class="flex flex-wrap gap-2"><a href="/services/catalogue/{{ service.id }}/defaults/export" class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 text-sm font-medium text-emerald-800 hover:bg-emerald-100">Export Tasks</a><a href="/services/catalogue/{{ service.id }}/defaults/duplicates" class="rounded-xl border border-amber-300 bg-amber-50 px-4 py-2 text-sm font-medium text-amber-800 hover:bg-amber-100">Duplicate Review</a><a href="/services/catalogue/{{ service.id }}/task-categories" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Task Categories</a><a href="/services/defaults" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Back</a><a href="/services/catalogue/{{ service.id }}" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Catalogue Detail</a></div>
</div>
{% if request.query_params.get('imported') %}<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Excel import completed: {{ request.query_params.get('created','0') }} created, {{ request.query_params.get('updated','0') }} updated.</div>{% endif %}
{% if request.query_params.get('import_error') %}<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Excel import failed validation. No partial update was committed. Review the workbook and try again.</div>{% endif %}
<section class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"><div><h3 class="text-sm font-semibold text-emerald-950">Export · Edit · Re-import This Service</h3><p class="mt-1 text-xs text-emerald-800">Export this service's current tasks, edit the Import Ready sheet, then re-import. Matching remains service code + sequence number.</p></div>
<form method="post" action="/services/catalogue/{{ service.id }}/defaults/import" enctype="multipart/form-data" class="flex flex-wrap items-center gap-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="file" name="file" accept=".xlsx" required class="max-w-xs rounded-xl border border-emerald-200 bg-white px-3 py-2 text-xs"><button class="rounded-xl bg-emerald-700 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-800">Import Updated Excel</button></form></div>
</section>
<section class="rounded-2xl bg-white p-5 shadow-soft">
<div class="space-y-3">
{% for task in default_tasks %}
@@ -43,7 +50,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div><label class="mb-2 block text-sm font-medium text-slate-700">Task Name</label><input type="text" name="task_name" required class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"></div>
<div><label class="mb-2 block text-sm font-medium text-slate-700">Default Role</label><input type="text" name="default_role_name" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"></div>
<div><label class="mb-2 block text-sm font-medium text-slate-700">Task Category</label><select name="task_category" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Uncategorised / General Workflow</option>{% for category in task_categories or [] %}{% if category.is_active %}<option value="{{ category.name }}">{{ category.name }}</option>{% endif %}{% endfor %}</select></div>
<div><label class="mb-2 block text-sm font-medium text-slate-700">Task Category</label><select name="task_category" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Uncategorised / General Workflow</option>{% for category in task_categories or [] %}{% if category.is_active %}<option value="{{ category.name }}">{{ category.name }}</option>{% endif %}{% endfor %}</select></div><div><label class="mb-2 block text-sm font-medium text-slate-700">Task Tool</label><select name="task_tool_code" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">{% for tool in task_tools %}<option value="{{ tool.code }}">{{ tool.name }}</option>{% endfor %}</select><p class="mt-1 text-xs text-slate-500">Optional ERP tool linked to the task. Existing task workflow remains unchanged.</p></div>
<div><label class="mb-2 block text-sm font-medium text-slate-700">Sequence No</label><input type="number" min="1" name="sequence_no" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"></div>
<div class="flex items-center gap-6 pt-7 text-sm text-slate-700"><label class="inline-flex items-center gap-2"><input type="checkbox" name="is_mandatory" checked> Mandatory</label><label class="inline-flex items-center gap-2"><input type="checkbox" name="requires_review"> Review</label><label class="inline-flex items-center gap-2">Normal reviewer <select name="normal_review_role" class="rounded border border-slate-300 px-2 py-1 text-xs"><option value="manager">Manager</option><option value="partner">Partner</option><option value="manager_or_partner">Manager / Partner</option></select></label><label class="inline-flex items-center gap-2"><input type="checkbox" name="is_active" checked> Active</label></div>
<div class="md:col-span-2"><label class="mb-2 block text-sm font-medium text-slate-700">Description</label><textarea name="description" rows="4" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"></textarea></div>
@@ -98,6 +98,11 @@
{% endfor %}
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-emerald-800">Task Tool</label>
<select name="task_tool_code" class="w-full rounded-xl border border-emerald-200 px-3 py-2 text-sm">{% for tool in task_tools %}<option value="{{ tool.code }}" {% if (task.task_tool_code or 'NONE') == tool.code %}selected{% endif %}>{{ tool.name }}</option>{% endfor %}</select>
<p class="mt-1 text-xs text-emerald-700">Links an existing ERP verification workspace to this task; normal checklist behavior is preserved.</p>
</div>
</div>
<div class="mt-4 grid gap-3 text-sm text-slate-700 md:grid-cols-3">
<label class="inline-flex items-center gap-2"><input type="checkbox" name="response_required" {% if task.response_required %}checked{% endif %}> Response required</label>
@@ -0,0 +1,24 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div><h2 class="text-xl font-semibold text-slate-900">Duplicate Task Review</h2><p class="mt-1 text-sm text-slate-500">{{ service.service_code }} · {{ service.service_name }} · {{ 'System defaults' if scope == 'default' else 'Firm task templates' }}</p></div>
<a href="{% if scope == 'default' %}/services/catalogue/{{ service.id }}/defaults{% else %}/services/templates/{{ service.id }}{% endif %}" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Back to Tasks</a>
</div>
{% if request.query_params.get('merged') %}<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">{{ request.query_params.get('merged') }} duplicate template(s) merged safely. Source templates were deactivated; historical engagement tasks were not changed.</div>{% endif %}
{% if request.query_params.get('merge_error') %}<div class="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Merge could not be completed. Select one master task and at least one different duplicate task.</div>{% endif %}
<section class="rounded-2xl bg-white p-5 shadow-soft">
<h3 class="text-sm font-semibold text-slate-900">Likely Duplicates</h3><p class="mt-1 text-sm text-slate-500">Suggestions use task-name similarity only. Review the meaning before merging.</p>
<div class="mt-4 space-y-2">{% for pair in pairs %}<div class="grid gap-2 rounded-xl border border-slate-200 p-3 text-sm md:grid-cols-[1fr_auto_1fr]"><div><span class="font-semibold">{{ pair.left.sequence_no }}.</span> {{ pair.left.task_name }}</div><div class="rounded-full bg-amber-50 px-2 py-1 text-xs font-semibold text-amber-800">{{ pair.score }}%</div><div><span class="font-semibold">{{ pair.right.sequence_no }}.</span> {{ pair.right.task_name }}</div></div>{% else %}<div class="rounded-xl border border-dashed border-slate-300 p-5 text-sm text-slate-500">No high-confidence duplicate pairs detected automatically. You can still merge manually below.</div>{% endfor %}</div>
</section>
<section class="rounded-2xl bg-white p-5 shadow-soft">
<h3 class="text-sm font-semibold text-slate-900">Merge Selected Templates</h3><p class="mt-1 text-sm text-slate-500">The master template remains active and unchanged. Selected source templates are deactivated, not deleted. Existing engagement task history is preserved.{% if scope == 'firm' %} Unique document requirements and uploaded task templates are reassigned to the master where safe.{% endif %}</p>
<form method="post" action="{% if scope == 'default' %}/services/catalogue/{{ service.id }}/defaults/duplicates/merge{% else %}/services/templates/{{ service.id }}/duplicates/merge{% endif %}" class="mt-5 space-y-5" onsubmit="return confirm('Merge the selected duplicate task templates? Source templates will be deactivated and historical engagement work will remain unchanged.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div><label class="mb-2 block text-sm font-medium text-slate-700">Master Task to Keep</label><select name="master_task_id" required class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Select master task</option>{% for task in tasks %}{% if task.is_active %}<option value="{{ task.id }}">{{ task.sequence_no }}. {{ task.task_name }}</option>{% endif %}{% endfor %}</select></div>
<div><div class="mb-2 text-sm font-medium text-slate-700">Duplicate Tasks to Merge Into Master</div><div class="max-h-96 space-y-2 overflow-y-auto rounded-xl border border-slate-200 p-3">{% for task in tasks %}{% if task.is_active %}<label class="flex items-start gap-3 rounded-lg px-2 py-2 hover:bg-slate-50"><input type="checkbox" name="source_task_ids" value="{{ task.id }}" class="mt-1"><span class="text-sm"><strong>{{ task.sequence_no }}.</strong> {{ task.task_name }}{% if task.task_category %}<span class="ml-2 text-xs text-slate-500">{{ task.task_category }}</span>{% endif %}</span></label>{% endif %}{% endfor %}</div></div>
<div class="flex justify-end"><button class="rounded-xl bg-amber-600 px-4 py-2 text-sm font-semibold text-white hover:bg-amber-700">Merge Selected Tasks</button></div>
</form>
</section>
</div>
{% endblock %}
@@ -11,6 +11,8 @@
</div>
<div class="flex flex-wrap gap-2">
<a href="/services/templates" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Back to Templates</a>
<a href="/services/templates/{{ service.id }}/export" class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 text-sm font-medium text-emerald-800 hover:bg-emerald-100">Export Tasks</a>
<a href="/services/templates/{{ service.id }}/duplicates" class="rounded-xl border border-amber-300 bg-amber-50 px-4 py-2 text-sm font-medium text-amber-800 hover:bg-amber-100">Duplicate Review</a>
<a href="/services/templates/{{ service.id }}/categories" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Task Categories</a>
<a href="/services/catalogue/{{ service.id }}" class="rounded-xl border border-brand-300 px-4 py-2 text-sm font-medium text-brand-700 hover:bg-brand-50">View Catalogue</a>
</div>
@@ -21,7 +23,15 @@
{% if request.query_params.get('error') %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Action failed. Please check the selected task, file and permissions.</div>{% endif %}
{% if request.query_params.get('moved') %}<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Task sequence updated. Use Sync Firm Tasks to Open Engagements if you want safe pending/open task snapshots refreshed.</div>{% endif %}
{% if request.query_params.get('move_error') %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Task could not be moved.</div>{% endif %}
{% if request.query_params.get('imported') %}<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Excel import completed: {{ request.query_params.get('created','0') }} created, {{ request.query_params.get('updated','0') }} updated.</div>{% endif %}
{% if request.query_params.get('import_error') %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Excel import failed validation. No partial update was committed.</div>{% endif %}
{% if can_manage_tasks %}
<section class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"><div><h3 class="text-sm font-semibold text-emerald-950">Export · Edit · Re-import Firm Tasks</h3><p class="mt-1 text-xs text-emerald-800">Use the exported workbook for this service. Re-import updates by service code + sequence number and preserves all other ERP features.</p></div><form method="post" action="/services/templates/{{ service.id }}/import" enctype="multipart/form-data" class="flex flex-wrap items-center gap-2"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="file" name="file" accept=".xlsx" required class="max-w-xs rounded-xl border border-emerald-200 bg-white px-3 py-2 text-xs"><button class="rounded-xl bg-emerald-700 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-800">Import Updated Excel</button></form></div>
</section>
{% endif %}
<section class="grid gap-4 md:grid-cols-3">
<div class="rounded-2xl bg-white p-5 shadow-soft">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Firm Status</div>
@@ -87,6 +97,10 @@
{% for category in task_categories or [] %}{% if category.is_active %}<option value="{{ category.name }}">{{ category.name }}</option>{% endif %}{% endfor %}
</select>
</div>
<div class="lg:col-span-3">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Task Tool</label>
<select name="task_tool_code" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">{% for tool in task_tools %}<option value="{{ tool.code }}">{{ tool.name }}</option>{% endfor %}</select>
</div>
<div class="lg:col-span-3">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Options</label>
<div class="flex flex-wrap gap-3 rounded-xl border border-slate-200 px-3 py-2 text-xs text-slate-700">
@@ -197,7 +211,7 @@
<td class="px-4 py-3 text-sm text-slate-700"><div>{{ task.default_role_name or '-' }}</div><div class="mt-1 text-xs text-slate-500">Eligible: {{ task.eligible_role_names or task.default_role_name or '-' }}</div></td>
<td class="px-4 py-3 text-xs text-slate-600">
<div class="flex flex-wrap gap-1">
{% if task.task_category %}<span class="rounded-full bg-blue-50 px-2 py-1 text-blue-700">{{ task.task_category }}</span>{% endif %}
{% if task.task_category %}<span class="rounded-full bg-blue-50 px-2 py-1 text-blue-700">{{ task.task_category }}</span>{% endif %}{% if task.task_tool_code and task.task_tool_code != "NONE" %}<span class="rounded-full bg-violet-50 px-2 py-1 text-violet-700">Tool: {{ task.task_tool_code|replace("_"," ")|title }}</span>{% endif %}
{% if task.is_mandatory %}<span class="rounded-full bg-slate-100 px-2 py-1">Mandatory</span>{% endif %}
{% if task.requires_review %}<span class="rounded-full bg-amber-100 px-2 py-1 text-amber-800">Review: {{ (task.normal_review_role or "manager").replace("_"," ").title() }}</span>{% endif %}
{% if task.is_aqmm_task %}<span class="rounded-full bg-indigo-100 px-2 py-1 text-indigo-800">AQMM</span>{% endif %}
@@ -86,6 +86,12 @@
{% if not can_reassign and task.assigned_to %}<p class="mt-1 text-xs text-slate-500">Assignee changes are restricted for this role.</p>{% endif %}
{% if can_edit and not can_manage_fields %}<p class="mt-1 text-xs text-slate-500">You can update status and work note only for your own assigned task.</p>{% endif %}
</div>
{% if task_tool and task_tool.code != 'NONE' %}
<div class="md:col-span-2 rounded-2xl border border-violet-200 bg-violet-50 p-4">
<div class="flex flex-wrap items-center justify-between gap-3"><div><div class="text-sm font-semibold text-violet-950">ERP Task Tool · {{ task_tool.name }}</div><p class="mt-1 text-xs text-violet-800">{{ task_tool.description }}</p></div>{% if task_tool.launch_url %}<a href="{{ task_tool.launch_url }}{% if task.client_id %}?client_id={{ task.client_id }}{% endif %}" class="rounded-xl bg-violet-700 px-4 py-2 text-sm font-semibold text-white hover:bg-violet-800">Open Tool</a>{% endif %}</div>
<p class="mt-2 text-xs text-violet-700">Opening the tool does not automatically mark this task complete. Save the task response/evidence after completing the verification.</p>
</div>
{% endif %}
{% if (task.response_type or 'NONE') != 'NONE' or task.response_required or task.evidence_required %}
<div class="md:col-span-2 rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
<div class="flex flex-wrap items-start justify-between gap-3">