Add staff visibility workstation enrollment registration vault and category task views
This commit is contained in:
@@ -23,7 +23,7 @@ from app.modules.core.tenancy.settings_models import BranchSettings
|
||||
from app.modules.employees.models import Employee, EmployeeAttendance, EmployeeRegistrationRequest, EmployeeLeaveType, EmployeeLeaveBalance, EmployeeLeaveRequest, EmployeeDocumentType, EmployeeDocument, EmployeeOnboardingChecklistItem, EmployeeOnboardingTask, EmployeeOffboardingRequest, EmployeeOffboardingTask, EmployeeSalaryStructure, EmployeePayrollRun, EmployeePayslip
|
||||
from app.modules.clients.models import Client, ClientBusinessUnit, ClientBranch
|
||||
from app.modules.documents.models import EngagementDocument
|
||||
from app.modules.services.models import ClientServiceTaskInstance, ClientServiceSubscription, ServiceCatalogue, ServiceTaskComment, FirmTaskDocumentRequirement
|
||||
from app.modules.services.models import ClientServiceTaskInstance, ClientServiceSubscription, ServiceCatalogue, ServiceTaskComment, FirmTaskDocumentRequirement, FirmServiceTaskTemplate, ServiceTaskCategory
|
||||
from app.modules.registrations.models import ClientRegistration, RegistrationType
|
||||
from app.modules.services.engagement_resources import build_engagement_resource_context
|
||||
from app.modules.services.execution import (
|
||||
@@ -152,11 +152,18 @@ def list_employees(
|
||||
q: str = "",
|
||||
include_inactive: bool = False,
|
||||
link_status: str = "all",
|
||||
status_filter: str | None = None,
|
||||
) -> list[Employee]:
|
||||
stmt = select(Employee).where(Employee.tenant_id == scope.tenant_id)
|
||||
if scope.branch_id is not None:
|
||||
stmt = stmt.where(Employee.branch_id == scope.branch_id)
|
||||
if not include_inactive:
|
||||
|
||||
normalized_status = (status_filter or "").strip().lower()
|
||||
if normalized_status in {"active", "inactive", "relieved"}:
|
||||
stmt = stmt.where(Employee.status == normalized_status)
|
||||
elif normalized_status == "all":
|
||||
pass
|
||||
elif not include_inactive:
|
||||
stmt = stmt.where(Employee.is_active.is_(True))
|
||||
|
||||
link_status = (link_status or "all").lower()
|
||||
@@ -3617,7 +3624,11 @@ def list_employee_engagement_documents(db: Session, scope: EmployeeScope, engage
|
||||
|
||||
|
||||
def _employee_task_category(task: ClientServiceTaskInstance) -> str:
|
||||
value = (getattr(task, "task_category", None) or "").strip()
|
||||
value = (
|
||||
getattr(task, "_resolved_task_category", None)
|
||||
or getattr(task, "task_category", None)
|
||||
or ""
|
||||
).strip()
|
||||
return value or "General Workflow"
|
||||
|
||||
|
||||
@@ -4021,10 +4032,37 @@ def get_employee_engagement_work_board(
|
||||
if int(row.id) in assigned_ids
|
||||
or (getattr(row, "default_role_name", None) or "").strip().lower() in reviewer_roles
|
||||
]
|
||||
use_task_categories = any(
|
||||
bool((getattr(row, "task_category", None) or "").strip())
|
||||
# Resolve task categories from the current Task Category master/template when
|
||||
# older generated task instances do not carry the category snapshot. This
|
||||
# keeps historical engagements category-based without rewriting task history.
|
||||
template_ids = {
|
||||
int(row.firm_task_template_id)
|
||||
for row in tasks
|
||||
)
|
||||
if getattr(row, "firm_task_template_id", None) is not None
|
||||
}
|
||||
resolved_categories: dict[int, tuple[str, int]] = {}
|
||||
if template_ids:
|
||||
category_rows = db.execute(
|
||||
select(
|
||||
FirmServiceTaskTemplate.id,
|
||||
FirmServiceTaskTemplate.task_category,
|
||||
ServiceTaskCategory.name,
|
||||
ServiceTaskCategory.sort_order,
|
||||
)
|
||||
.outerjoin(ServiceTaskCategory, ServiceTaskCategory.id == FirmServiceTaskTemplate.task_category_id)
|
||||
.where(FirmServiceTaskTemplate.id.in_(template_ids))
|
||||
).all()
|
||||
for template_id, legacy_name, master_name, sort_order in category_rows:
|
||||
resolved_name = (master_name or legacy_name or "").strip()
|
||||
if resolved_name:
|
||||
resolved_categories[int(template_id)] = (resolved_name, int(sort_order or 100))
|
||||
for row in tasks:
|
||||
template_id = getattr(row, "firm_task_template_id", None)
|
||||
resolved = resolved_categories.get(int(template_id)) if template_id is not None else None
|
||||
row._resolved_task_category = resolved[0] if resolved else ((getattr(row, "task_category", None) or "").strip() or "General Workflow")
|
||||
row._resolved_task_category_sort = resolved[1] if resolved else 100
|
||||
|
||||
use_task_categories = any(_employee_task_category(row) != "General Workflow" for row in tasks)
|
||||
|
||||
summary = {
|
||||
"total": len(tasks),
|
||||
@@ -4056,6 +4094,7 @@ def get_employee_engagement_work_board(
|
||||
if category is None:
|
||||
category = {
|
||||
"name": category_name,
|
||||
"sort_order": int(getattr(task, "_resolved_task_category_sort", 100) or 100),
|
||||
"tasks": [],
|
||||
"total": 0,
|
||||
"completed": 0,
|
||||
@@ -4075,6 +4114,8 @@ def get_employee_engagement_work_board(
|
||||
elif status_code == "in_progress":
|
||||
category["in_progress"] += 1
|
||||
|
||||
categories.sort(key=lambda item: (int(item.get("sort_order", 100)), item["name"].lower()))
|
||||
|
||||
for category in categories:
|
||||
weighted_category = _weighted_progress(category["tasks"])
|
||||
category.update(weighted_category)
|
||||
|
||||
@@ -22,9 +22,12 @@
|
||||
<option value="linked" {% if link_status == 'linked' %}selected{% endif %}>Linked to login user</option>
|
||||
<option value="unlinked" {% if link_status == 'unlinked' %}selected{% endif %}>Not linked to login user</option>
|
||||
</select>
|
||||
<label class="inline-flex items-center gap-2 rounded-xl border border-slate-300 px-3 py-2 text-sm text-slate-700">
|
||||
<input type="checkbox" name="include_inactive" value="1" {% if include_inactive %}checked{% endif %}> Include inactive
|
||||
</label>
|
||||
<select name="status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm text-slate-700">
|
||||
<option value="all" {% if selected_status == 'all' %}selected{% endif %}>All staff statuses</option>
|
||||
<option value="active" {% if selected_status == 'active' %}selected{% endif %}>Active</option>
|
||||
<option value="inactive" {% if selected_status == 'inactive' %}selected{% endif %}>Inactive</option>
|
||||
<option value="relieved" {% if selected_status == 'relieved' %}selected{% endif %}>Relieved</option>
|
||||
</select>
|
||||
<button class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -41,7 +44,7 @@
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
|
||||
<div class="font-semibold">{{ link_summary.unlinked }} employee(s) are not linked to login users.</div>
|
||||
<p class="mt-1">Employee self-service pages such as My Workspace, attendance, leave, documents and payslips work fully only after the employee master is linked to an IAM user.</p>
|
||||
<a href="/employees?link_status=unlinked{% if include_inactive %}&include_inactive=1{% endif %}" class="mt-2 inline-flex rounded-lg border border-amber-300 px-3 py-1.5 text-xs font-semibold text-amber-900 hover:bg-amber-100">Show unlinked employees</a>
|
||||
<a href="/employees?link_status=unlinked&status={{ selected_status or 'all' }}" class="mt-2 inline-flex rounded-lg border border-amber-300 px-3 py-1.5 text-xs font-semibold text-amber-900 hover:bg-amber-100">Show unlinked employees</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -114,9 +114,12 @@
|
||||
<div class="divide-y divide-slate-100">
|
||||
{% for category in board.categories %}
|
||||
{% if board.use_task_categories %}
|
||||
<div class="bg-slate-50 px-5 py-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
{{ category.name }} · {{ category.completed }}/{{ category.total }}
|
||||
</div>
|
||||
<details class="group" {% if loop.first %}open{% endif %}>
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between gap-3 bg-slate-50 px-5 py-3 text-sm font-semibold text-slate-700 hover:bg-slate-100">
|
||||
<span class="flex items-center gap-2"><span class="text-slate-400 transition-transform group-open:rotate-90">▶</span>{{ category.name }}</span>
|
||||
<span class="rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-slate-600">{{ category.completed }}/{{ category.total }} completed</span>
|
||||
</summary>
|
||||
<div class="divide-y divide-slate-100">
|
||||
{% endif %}
|
||||
|
||||
{% for task in category.tasks %}
|
||||
@@ -365,6 +368,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if board.use_task_categories %}</div></details>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -513,7 +513,7 @@ def _safe_employee_return_url(value: str | None, fallback: str) -> str:
|
||||
|
||||
|
||||
@router.get("")
|
||||
def employees_list(request: Request, q: str = "", include_inactive: str | None = None, link_status: str = "all"):
|
||||
def employees_list(request: Request, q: str = "", include_inactive: str | None = None, link_status: str = "all", status: str | None = None):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
current_user = get_current_user(request, db=db)
|
||||
@@ -527,12 +527,19 @@ def employees_list(request: Request, q: str = "", include_inactive: str | None =
|
||||
branch_id = request.session.get("active_branch_id")
|
||||
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
||||
normalized_link_status = link_status if link_status in ("all", "linked", "unlinked") else "all"
|
||||
requested_status = (status or "").strip().lower()
|
||||
if requested_status not in {"all", "active", "inactive", "relieved"}:
|
||||
requested_status = "all" if (scope.is_firm_admin or scope.is_partner or scope.is_system_admin) else "active"
|
||||
# Keep the legacy include_inactive query string working for bookmarked URLs.
|
||||
if include_inactive and status is None:
|
||||
requested_status = "all"
|
||||
rows = list_employees(
|
||||
db,
|
||||
scope,
|
||||
q=q,
|
||||
include_inactive=bool(include_inactive),
|
||||
include_inactive=(requested_status == "all"),
|
||||
link_status=normalized_link_status,
|
||||
status_filter=requested_status,
|
||||
)
|
||||
link_summary = get_employee_user_link_summary(db, scope)
|
||||
return _render(
|
||||
@@ -543,7 +550,8 @@ def employees_list(request: Request, q: str = "", include_inactive: str | None =
|
||||
title="Employees",
|
||||
rows=rows,
|
||||
q=q,
|
||||
include_inactive=bool(include_inactive),
|
||||
include_inactive=(requested_status == "all"),
|
||||
selected_status=requested_status,
|
||||
link_status=normalized_link_status,
|
||||
link_summary=link_summary,
|
||||
scope=scope,
|
||||
|
||||
Reference in New Issue
Block a user