Add controlled service catalogue merge workflow

This commit is contained in:
A R R R Associates
2026-07-17 22:59:02 +05:30
parent 9f51461548
commit 0b58bf182b
4 changed files with 504 additions and 13 deletions
@@ -12,7 +12,7 @@
<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">Firm Task Templates</a>
{% if can_create %}
<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">System Default Tasks</a>
<a href="/services/catalogue/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 Excel</a>
<a href="/services/catalogue/merge" class="rounded-xl border border-amber-300 px-4 py-2 text-sm font-medium text-amber-800 hover:bg-amber-50">Merge Services</a>
<a href="/services/catalogue/new" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Add Catalogue Service</a>
{% endif %}
</div>
@@ -0,0 +1,104 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-xl font-semibold text-slate-900">Merge Service Catalogue</h2>
<p class="text-sm text-slate-500">Consolidate an older duplicate service into the service that should be used for all future work.</p>
</div>
<a href="/services/catalogue" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Back to Catalogue</a>
</div>
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
The source service will be disabled. Firm selections, default tasks, firm task templates, document requirements, document templates and due-date rules are consolidated into the target. Existing client subscriptions, completed or active execution work, invoices and consultant requests remain linked to the source service to preserve historical records.
</div>
{% if request.query_params.get('error') == 'confirmation' %}
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">The confirmation text did not match. No records were changed.</div>
{% elif request.query_params.get('error') or error %}
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">{{ error or 'The selected merge could not be prepared. No records were changed.' }}</div>
{% endif %}
<form method="get" action="/services/catalogue/merge" class="rounded-2xl bg-white p-5 shadow-soft">
<div class="grid gap-4 md:grid-cols-2">
<label class="space-y-2">
<span class="text-sm font-medium text-slate-700">Source service to disable</span>
<select name="source_id" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="">-- Select old/duplicate service --</option>
{% for service in services %}
<option value="{{ service.id }}" {% if source_id == service.id %}selected{% endif %}>{{ service.service_code }} — {{ service.service_name }}{% if not service.is_active %} (Inactive){% endif %}</option>
{% endfor %}
</select>
</label>
<label class="space-y-2">
<span class="text-sm font-medium text-slate-700">Target service to keep</span>
<select name="target_id" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="">-- Select surviving service --</option>
{% for service in services %}
<option value="{{ service.id }}" {% if target_id == service.id %}selected{% endif %}>{{ service.service_code }} — {{ service.service_name }}{% if not service.is_active %} (Inactive){% endif %}</option>
{% endfor %}
</select>
</label>
</div>
<div class="mt-4 flex justify-end">
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Preview Merge</button>
</div>
</form>
{% if preview %}
<div class="grid gap-4 lg:grid-cols-2">
<div class="rounded-2xl bg-white p-5 shadow-soft">
<h3 class="font-semibold text-slate-900">Merge direction</h3>
<div class="mt-4 space-y-3 text-sm">
<div class="rounded-xl border border-red-200 bg-red-50 p-3"><span class="font-semibold">Source:</span> {{ preview.source.service_code }} — {{ preview.source.service_name }}</div>
<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-3"><span class="font-semibold">Target:</span> {{ preview.target.service_code }} — {{ preview.target.service_name }}</div>
</div>
</div>
<div class="rounded-2xl bg-white p-5 shadow-soft">
<h3 class="font-semibold text-slate-900">Conflict handling</h3>
<dl class="mt-4 grid grid-cols-2 gap-3 text-sm">
<div><dt class="text-slate-500">Firm selections</dt><dd class="font-semibold">{{ preview.conflicts.firm_selection_conflicts }}</dd></div>
<div><dt class="text-slate-500">Default task names</dt><dd class="font-semibold">{{ preview.conflicts.system_default_task_name_conflicts }}</dd></div>
<div><dt class="text-slate-500">Firm task names</dt><dd class="font-semibold">{{ preview.conflicts.firm_task_name_conflicts }}</dd></div>
<div><dt class="text-slate-500">Due-rule names</dt><dd class="font-semibold">{{ preview.conflicts.due_rule_name_conflicts }}</dd></div>
</dl>
<p class="mt-4 text-xs text-slate-500">Conflicting firm selections are consolidated. Tasks and due rules with matching normalised names are deduplicated; non-matching items are appended to the target.</p>
</div>
</div>
<div class="overflow-hidden rounded-2xl bg-white shadow-soft">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Source dependency</th><th class="px-4 py-3 text-right text-xs font-semibold uppercase text-slate-500">Count</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Merge treatment</th></tr></thead>
<tbody class="divide-y divide-slate-100">
<tr><td class="px-4 py-3 text-sm">Firm selections</td><td class="px-4 py-3 text-right text-sm font-semibold">{{ preview.counts.firm_selections }}</td><td class="px-4 py-3 text-sm">Move or consolidate into target</td></tr>
<tr><td class="px-4 py-3 text-sm">System default tasks</td><td class="px-4 py-3 text-right text-sm font-semibold">{{ preview.counts.system_default_tasks }}</td><td class="px-4 py-3 text-sm">Move; matching task names deduplicated</td></tr>
<tr><td class="px-4 py-3 text-sm">Firm task templates</td><td class="px-4 py-3 text-right text-sm font-semibold">{{ preview.counts.firm_task_templates }}</td><td class="px-4 py-3 text-sm">Move; matching tenant/task names consolidated</td></tr>
<tr><td class="px-4 py-3 text-sm">Document requirements</td><td class="px-4 py-3 text-right text-sm font-semibold">{{ preview.counts.document_requirements }}</td><td class="px-4 py-3 text-sm">Remain with moved/consolidated task template</td></tr>
<tr><td class="px-4 py-3 text-sm">Document templates</td><td class="px-4 py-3 text-right text-sm font-semibold">{{ preview.counts.document_templates }}</td><td class="px-4 py-3 text-sm">Remain with moved/consolidated task template</td></tr>
<tr><td class="px-4 py-3 text-sm">Due-date rules</td><td class="px-4 py-3 text-right text-sm font-semibold">{{ preview.counts.due_date_rules }}</td><td class="px-4 py-3 text-sm">Move; matching rule names consolidated</td></tr>
<tr><td class="px-4 py-3 text-sm">Due-date extensions</td><td class="px-4 py-3 text-right text-sm font-semibold">{{ preview.counts.due_date_extensions }}</td><td class="px-4 py-3 text-sm">Move to target and surviving due rule</td></tr>
<tr><td class="px-4 py-3 text-sm">Client subscriptions</td><td class="px-4 py-3 text-right text-sm font-semibold">{{ preview.counts.client_subscriptions_preserved }}</td><td class="px-4 py-3 text-sm">Preserved on source for historical continuity</td></tr>
</tbody>
</table>
</div>
<form method="post" action="/services/catalogue/merge" class="rounded-2xl border border-red-200 bg-white p-5 shadow-soft">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="source_id" value="{{ preview.source.id }}">
<input type="hidden" name="target_id" value="{{ preview.target.id }}">
<label class="space-y-2">
<span class="text-sm font-semibold text-red-800">Type the exact confirmation below</span>
<code class="block rounded-xl bg-slate-900 px-4 py-3 text-sm text-white">MERGE {{ preview.source.service_code }} INTO {{ preview.target.service_code }}</code>
<input name="confirmation" required autocomplete="off" class="w-full rounded-xl border border-red-300 px-3 py-2 text-sm" placeholder="Enter confirmation text">
</label>
<label class="mt-4 flex items-start gap-3 text-sm text-slate-700">
<input type="checkbox" required class="mt-1 rounded border-slate-300">
<span>I have verified the source and target. I understand that the source will be disabled and historical operational records will remain preserved.</span>
</label>
<div class="mt-5 flex justify-end">
<button class="rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700">Merge and Disable Source</button>
</div>
</form>
{% endif %}
</div>
{% endblock %}