Add bulk firm selection to service catalogue
This commit is contained in:
@@ -19,6 +19,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.query_params.get('bulk_action') == 'enable' or request.query_params.get('bulk_action') == 'disable' %}
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">
|
||||
Bulk update completed: <span class="font-semibold">{{ request.query_params.get('bulk_updated', '0') }}</span> changed,
|
||||
<span class="font-semibold">{{ request.query_params.get('bulk_unchanged', '0') }}</span> already in the requested state,
|
||||
and <span class="font-semibold">{{ request.query_params.get('bulk_skipped', '0') }}</span> skipped.
|
||||
</div>
|
||||
{% elif request.query_params.get('bulk_action') == 'no_selection' %}
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">Select at least one service before applying a bulk action.</div>
|
||||
{% elif request.query_params.get('bulk_action') == 'invalid_branch' %}
|
||||
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">The selected default branch is not available for the active firm.</div>
|
||||
{% elif request.query_params.get('bulk_action') == 'invalid' %}
|
||||
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">The requested bulk action is invalid.</div>
|
||||
{% endif %}
|
||||
|
||||
{% if can_manage_firm_services %}
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">
|
||||
Select the services your firm provides. Once selected, you can open <span class="font-semibold">Firm Task Templates</span> and customise tasks for your firm.
|
||||
@@ -59,10 +73,41 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if can_manage_firm_services %}
|
||||
<form id="bulk-service-form" method="post" action="/services/catalogue/bulk-selection" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="return_q" value="{{ q }}">
|
||||
<input type="hidden" name="return_category_id" value="{{ category_id or '' }}">
|
||||
<input type="hidden" name="return_recurrence_type" value="{{ recurrence_type }}">
|
||||
<input type="hidden" name="return_engagement_type" value="{{ engagement_type }}">
|
||||
<input type="hidden" name="return_page" value="{{ page }}">
|
||||
<input type="hidden" name="return_per_page" value="{{ per_page }}">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="mr-auto text-sm font-medium text-slate-700"><span id="bulk-selected-count">0</span> services selected</div>
|
||||
{% if branches %}
|
||||
<select name="default_branch_id" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">Keep existing branch / no default for new selections</option>
|
||||
{% for branch in branches %}<option value="{{ branch.id }}">{{ branch.name }}</option>{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
<input type="hidden" name="default_branch_id" value="">
|
||||
{% endif %}
|
||||
<button type="submit" name="bulk_action" value="enable" class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700">Select for Firm</button>
|
||||
<button type="submit" name="bulk_action" value="disable" class="rounded-xl border border-amber-300 px-4 py-2 text-sm font-semibold text-amber-700 hover:bg-amber-50">Disable for Firm</button>
|
||||
<button type="button" id="bulk-clear-selection" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Clear Selection</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
{% if can_manage_firm_services %}
|
||||
<th class="w-12 px-4 py-3 text-left">
|
||||
<input id="bulk-select-all-visible" type="checkbox" class="h-4 w-4 rounded border-slate-300 text-brand-600" aria-label="Select all services visible on this page">
|
||||
</th>
|
||||
{% endif %}
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Code</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Name</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Category</th>
|
||||
@@ -75,6 +120,11 @@
|
||||
{% for row in rows %}
|
||||
{% set selection = firm_selection_by_catalogue.get(row.id) if firm_selection_by_catalogue else None %}
|
||||
<tr>
|
||||
{% if can_manage_firm_services %}
|
||||
<td class="px-4 py-3 align-top">
|
||||
<input type="checkbox" name="service_ids" value="{{ row.id }}" form="bulk-service-form" class="bulk-service-checkbox h-4 w-4 rounded border-slate-300 text-brand-600" aria-label="Select {{ row.service_name }}">
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="px-4 py-3 text-sm font-medium text-slate-900">{{ row.service_code }}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700">
|
||||
<div class="font-medium text-slate-900">{{ row.service_name }}</div>
|
||||
@@ -118,7 +168,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-slate-500">No catalogue services found.</td></tr>
|
||||
<tr><td colspan="{{ 7 if can_manage_firm_services else 6 }}" class="px-4 py-8 text-center text-sm text-slate-500">No catalogue services found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -190,4 +240,41 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if can_manage_firm_services %}
|
||||
<script>
|
||||
(() => {
|
||||
const selectAll = document.getElementById('bulk-select-all-visible');
|
||||
const clearButton = document.getElementById('bulk-clear-selection');
|
||||
const countLabel = document.getElementById('bulk-selected-count');
|
||||
const checkboxes = Array.from(document.querySelectorAll('.bulk-service-checkbox'));
|
||||
|
||||
const refreshSelectionState = () => {
|
||||
const selectedCount = checkboxes.filter((checkbox) => checkbox.checked).length;
|
||||
countLabel.textContent = String(selectedCount);
|
||||
if (selectAll) {
|
||||
selectAll.checked = checkboxes.length > 0 && selectedCount === checkboxes.length;
|
||||
selectAll.indeterminate = selectedCount > 0 && selectedCount < checkboxes.length;
|
||||
}
|
||||
};
|
||||
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener('change', () => {
|
||||
checkboxes.forEach((checkbox) => { checkbox.checked = selectAll.checked; });
|
||||
refreshSelectionState();
|
||||
});
|
||||
}
|
||||
|
||||
checkboxes.forEach((checkbox) => checkbox.addEventListener('change', refreshSelectionState));
|
||||
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener('click', () => {
|
||||
checkboxes.forEach((checkbox) => { checkbox.checked = false; });
|
||||
refreshSelectionState();
|
||||
});
|
||||
}
|
||||
|
||||
refreshSelectionState();
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -849,6 +849,144 @@ def catalogue_edit_submit(request: Request, catalogue_id: int, service_name: str
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post('/catalogue/bulk-selection')
|
||||
def bulk_update_firm_services(
|
||||
request: Request,
|
||||
service_ids: list[int] = Form([]),
|
||||
bulk_action: str = Form(...),
|
||||
default_branch_id: str = Form(''),
|
||||
return_q: str = Form(''),
|
||||
return_category_id: str = Form(''),
|
||||
return_recurrence_type: str = Form(''),
|
||||
return_engagement_type: str = Form(''),
|
||||
return_page: int = Form(1),
|
||||
return_per_page: int = Form(20),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
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_services(db, user):
|
||||
return _redirect_denied()
|
||||
|
||||
tenant_id = _active_tenant_id(request, user)
|
||||
action = (bulk_action or '').strip().lower()
|
||||
if action not in {'enable', 'disable'}:
|
||||
action = 'invalid'
|
||||
|
||||
unique_service_ids = list(dict.fromkeys(service_id for service_id in service_ids if service_id > 0))
|
||||
branch_id: int | None = None
|
||||
branch_value = str(default_branch_id or '').strip()
|
||||
if branch_value:
|
||||
try:
|
||||
parsed_branch_id = int(branch_value)
|
||||
except (TypeError, ValueError):
|
||||
parsed_branch_id = 0
|
||||
branch = db.execute(
|
||||
select(Branch).where(
|
||||
Branch.id == parsed_branch_id,
|
||||
Branch.tenant_id == tenant_id,
|
||||
Branch.is_active.is_(True),
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not branch:
|
||||
action = 'invalid_branch'
|
||||
else:
|
||||
branch_id = branch.id
|
||||
|
||||
updated_count = 0
|
||||
unchanged_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
if action in {'enable', 'disable'} and unique_service_ids:
|
||||
catalogue_rows = db.execute(
|
||||
select(ServiceCatalogue).where(
|
||||
ServiceCatalogue.id.in_(unique_service_ids),
|
||||
ServiceCatalogue.is_active.is_(True),
|
||||
)
|
||||
).scalars().all()
|
||||
catalogue_by_id = {row.id: row for row in catalogue_rows}
|
||||
|
||||
selections = db.execute(
|
||||
select(FirmServiceSelection).where(
|
||||
FirmServiceSelection.tenant_id == tenant_id,
|
||||
FirmServiceSelection.service_catalogue_id.in_(unique_service_ids),
|
||||
)
|
||||
).scalars().all()
|
||||
selection_by_catalogue = {
|
||||
selection.service_catalogue_id: selection for selection in selections
|
||||
}
|
||||
|
||||
for catalogue_id in unique_service_ids:
|
||||
if catalogue_id not in catalogue_by_id:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
selection = selection_by_catalogue.get(catalogue_id)
|
||||
if action == 'enable':
|
||||
if selection is None:
|
||||
selection = FirmServiceSelection(
|
||||
tenant_id=tenant_id,
|
||||
service_catalogue_id=catalogue_id,
|
||||
is_enabled=True,
|
||||
default_branch_id=branch_id,
|
||||
activated_by_user_id=user.id,
|
||||
updated_by_user_id=user.id,
|
||||
)
|
||||
db.add(selection)
|
||||
updated_count += 1
|
||||
else:
|
||||
changed = not bool(selection.is_enabled)
|
||||
selection.is_enabled = True
|
||||
if branch_id is not None and selection.default_branch_id != branch_id:
|
||||
selection.default_branch_id = branch_id
|
||||
changed = True
|
||||
selection.updated_by_user_id = user.id
|
||||
if changed:
|
||||
updated_count += 1
|
||||
else:
|
||||
unchanged_count += 1
|
||||
else:
|
||||
if selection is not None and selection.is_enabled:
|
||||
selection.is_enabled = False
|
||||
selection.updated_by_user_id = user.id
|
||||
updated_count += 1
|
||||
else:
|
||||
unchanged_count += 1
|
||||
|
||||
db.commit()
|
||||
elif not unique_service_ids:
|
||||
action = 'no_selection'
|
||||
|
||||
query_parts = [
|
||||
f'bulk_action={action}',
|
||||
f'bulk_updated={updated_count}',
|
||||
f'bulk_unchanged={unchanged_count}',
|
||||
f'bulk_skipped={skipped_count}',
|
||||
f'page={max(1, return_page)}',
|
||||
f'per_page={return_per_page if return_per_page in (20, 50, 100) else 20}',
|
||||
]
|
||||
if return_q:
|
||||
from urllib.parse import quote_plus
|
||||
query_parts.append(f'q={quote_plus(return_q)}')
|
||||
if return_category_id:
|
||||
query_parts.append(f'category_id={return_category_id}')
|
||||
if return_recurrence_type:
|
||||
query_parts.append(f'recurrence_type={return_recurrence_type}')
|
||||
if return_engagement_type:
|
||||
query_parts.append(f'engagement_type={return_engagement_type}')
|
||||
|
||||
return RedirectResponse(
|
||||
url='/services/catalogue?' + '&'.join(query_parts),
|
||||
status_code=303,
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post('/catalogue/{catalogue_id}/toggle')
|
||||
def toggle_firm_service(request: Request, catalogue_id: int, default_branch_id: str = Form(''), csrf_token: str = Form(...)):
|
||||
validate_csrf(request, csrf_token)
|
||||
|
||||
Reference in New Issue
Block a user