diff --git a/app/modules/employees/service.py b/app/modules/employees/service.py index d601136..2578361 100644 --- a/app/modules/employees/service.py +++ b/app/modules/employees/service.py @@ -290,6 +290,15 @@ def create_login_user_for_employee( def create_employee(db: Session, actor: User, scope: EmployeeScope, data: dict[str, Any]) -> Employee: cleaned = _clean_payload(data) + partner_staff_mode = bool(scope.is_partner and not scope.is_system_admin and not scope.is_firm_admin) + + # Partners can create staff only in their own tenant and branch. Never trust + # tenant, branch or role values posted by the browser for this workflow. + if partner_staff_mode: + cleaned["tenant_id"] = scope.tenant_id + cleaned["branch_id"] = scope.branch_id or actor.branch_id + cleaned["employee_role"] = "Staff" + tenant_id = int(cleaned.get("tenant_id") or scope.tenant_id) branch_id = int(cleaned.get("branch_id") or actor.branch_id) @@ -311,6 +320,11 @@ def create_employee(db: Session, actor: User, scope: EmployeeScope, data: dict[s raise HTTPException(status_code=404, detail="Selected user was not found.") if linked_user.tenant_id != tenant_id or linked_user.branch_id != branch_id: raise HTTPException(status_code=400, detail="Selected user must belong to the employee tenant and branch.") + if partner_staff_mode: + linked_roles = _role_set(db, linked_user) + elevated_roles = {"System Admin", "Firm Admin", "Partner", "Branch Manager"} + if "Staff" not in linked_roles or linked_roles.intersection(elevated_roles): + raise HTTPException(status_code=403, detail="Partners can link only a Staff login user.") user_id = linked_user.id elif cleaned.get("create_login_user"): login_user = create_login_user_for_employee( diff --git a/app/modules/employees/templates/employees/form.html b/app/modules/employees/templates/employees/form.html index 7d0b4ee..97a328c 100644 --- a/app/modules/employees/templates/employees/form.html +++ b/app/modules/employees/templates/employees/form.html @@ -11,7 +11,7 @@

{{ title }}

-

Create or update the employee master. Attendance, leave, payroll and ESS will be added in later phases.

+

{% if partner_staff_mode and not is_edit %}Create a Staff employee in your assigned branch and optionally create the Staff login account.{% else %}Create or update the employee master. Attendance, leave, payroll and ESS will be added in later phases.{% endif %}

Back
@@ -25,8 +25,8 @@ {% if not is_edit %}
-

Employee Login Link

-

Either link an existing unlinked login user or create a login user automatically for this employee.

+

{% if partner_staff_mode %}Staff Login Link{% else %}Employee Login Link{% endif %}

+

{% if partner_staff_mode %}Link an existing unlinked Staff login or create a Staff login automatically for this employee.{% else %}Either link an existing unlinked login user or create a login user automatically for this employee.{% endif %}

@@ -43,7 +43,13 @@

Minimum 8 characters. User must change password after login.

- + {% if partner_staff_mode %} + + +

Partners can create Staff users only.

+ {% else %} + + {% endif %}
@@ -101,7 +107,7 @@ -
Cancel
+
Cancel
{% endblock %} diff --git a/app/modules/employees/ui.py b/app/modules/employees/ui.py index a08ee6a..22fd92b 100644 --- a/app/modules/employees/ui.py +++ b/app/modules/employees/ui.py @@ -325,12 +325,21 @@ def _form_payload(form, *, include_context: bool = False): def _form_options(db, current_user, scope, *, include_user_id: int | None = None): + users = list_linkable_users(db, scope, include_user_id=include_user_id) + partner_staff_mode = bool(scope.is_partner and not scope.is_system_admin and not scope.is_firm_admin) + + # A Partner may onboard or link Staff users only. The service layer repeats + # this rule so a forged POST cannot bypass the form restriction. + if partner_staff_mode: + users = [user for user in users if "Staff" in set(get_user_roles(db, user.id))] + return { "tenants": visible_tenants(db, current_user), "branches": visible_branches(db, current_user, scope.tenant_id), - "users": list_linkable_users(db, scope, include_user_id=include_user_id), + "users": users, "managers": list_reporting_managers(db, scope), "scope": scope, + "partner_staff_mode": partner_staff_mode, } @@ -563,7 +572,7 @@ def employee_new(request: Request): "modules/employees/templates/employees/form.html", db, current_user, - title="Add Employee", + title="Add Staff" if scope.is_partner else "Add Employee", employee=None, errors=[], mode="create", @@ -591,6 +600,10 @@ async def employee_create_submit(request: Request): return _redirect_denied() scope = build_employee_scope(db, current_user, tenant_id=form.get("tenant_id"), branch_id=form.get("branch_id")) payload = _form_payload(form, include_context=True) + if scope.is_partner and not scope.is_system_admin and not scope.is_firm_admin: + payload["employee_role"] = "Staff" + payload["tenant_id"] = scope.tenant_id + payload["branch_id"] = scope.branch_id or current_user.branch_id try: emp = create_employee(db, current_user, scope, payload) return RedirectResponse(url=f"/employees/{emp.id}", status_code=303) @@ -600,7 +613,7 @@ async def employee_create_submit(request: Request): "modules/employees/templates/employees/form.html", db, current_user, - title="Add Employee", + title="Add Staff" if scope.is_partner else "Add Employee", employee=payload, errors=[getattr(exc, "detail", str(exc))], mode="create", diff --git a/app/ui/templates/components/partner_navigation_v2.html b/app/ui/templates/components/partner_navigation_v2.html index d041955..b19403c 100644 --- a/app/ui/templates/components/partner_navigation_v2.html +++ b/app/ui/templates/components/partner_navigation_v2.html @@ -46,6 +46,15 @@ {'label': 'Assign Service', 'url': '/services/engagements/new', 'active': _partner_path == '/services/engagements/new'} ] }, + { + 'label': 'Staff', + 'visible': true, + 'active': _partner_path.startswith('/employees'), + 'children': [ + {'label': 'Staff List', 'url': '/employees', 'active': _partner_path == '/employees'}, + {'label': 'Add Staff', 'url': '/employees/new', 'active': _partner_path == '/employees/new'} + ] + }, { 'label': 'Documents', 'url': '/documents',