Files
arrr-erp/app/modules/employees/ui.py
T
2026-06-25 20:33:15 +05:30

2645 lines
109 KiB
Python

from __future__ import annotations
from pathlib import Path
from io import BytesIO
from uuid import uuid4
from fastapi import APIRouter, File, Form, Request, UploadFile
from fastapi.responses import RedirectResponse, StreamingResponse
from app.core.db.common import CommonSessionLocal
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
from app.core.security.session_auth import get_current_user
from app.core.templating import templates
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.core.rbac.permission_guard import require_permission
from app.modules.core.iam.profile_service import save_user_profile_photo, update_user_public_profile
from app.modules.employees.import_service import (
build_template_workbook,
commit_import,
preview_import,
supported_import_types,
)
from app.modules.employees.service import (
EMPLOYEE_ROLE_NAMES,
EMPLOYEE_STATUS,
EMPLOYMENT_TYPES,
build_employee_scope,
change_employee_status,
create_employee,
get_employee_or_404,
get_employee_user_link_summary,
link_employee_to_user,
list_employees,
list_linkable_users,
list_reporting_managers,
parse_date,
update_employee,
visible_branches,
visible_tenants,
REGISTRATION_STATUS,
approve_employee_registration_request,
create_employee_registration_request,
get_employee_for_user,
get_employee_dashboard_stats,
list_employee_work_dashboard,
list_employee_work_kanban,
get_employee_engagement_work_board,
list_employee_work_assignable_users,
list_visible_work_assignment_dashboard,
list_engagement_progress_dashboard,
update_service_task_assignment,
update_own_service_task_status,
TASK_COMMUNICATION_TYPES,
TASK_COMMUNICATION_VISIBILITIES,
add_work_task_communication,
get_work_task_with_communications,
get_employee_registration_request_or_404,
get_pending_registration_for_user,
list_employee_registration_requests,
reject_employee_registration_request,
update_own_employee_profile,
ATTENDANCE_APPROVAL_STATUS,
ATTENDANCE_STATUS,
create_or_update_manual_attendance,
get_attendance_or_404,
get_today_attendance_for_user,
list_attendance_records,
list_own_attendance,
punch_in_attendance,
punch_out_attendance,
review_attendance_record,
LEAVE_REQUEST_STATUS,
adjust_leave_balance,
apply_employee_leave,
cancel_own_leave_request,
create_leave_type,
ensure_default_leave_types,
get_leave_request_or_404,
get_leave_type_or_404,
list_leave_balances,
list_leave_requests,
list_leave_types,
list_own_leave_balances,
list_own_leave_requests,
review_leave_request,
update_leave_type,
DOCUMENT_STATUS,
DOCUMENT_VISIBILITY,
archive_employee_document,
create_document_type,
create_employee_document_record,
ensure_default_document_types,
get_document_type_or_404,
get_employee_document_or_404,
list_document_types,
list_employee_documents,
list_own_employee_documents,
review_employee_document,
update_document_type,
ONBOARDING_TASK_STATUS,
OFFBOARDING_REQUEST_STATUS,
OFFBOARDING_TASK_STATUS,
create_offboarding_request,
create_onboarding_checklist_item,
complete_offboarding_request,
ensure_default_onboarding_checklist,
generate_onboarding_tasks_for_employee,
get_offboarding_request_or_404,
get_offboarding_task_or_404,
get_onboarding_checklist_item_or_404,
get_onboarding_task_or_404,
list_offboarding_requests,
list_offboarding_tasks,
list_onboarding_checklist_items,
list_onboarding_tasks,
list_own_offboarding_requests,
review_offboarding_request,
update_offboarding_task_status,
update_onboarding_checklist_item,
update_onboarding_task_status,
PAYROLL_RUN_STATUS,
PAYSLIP_STATUS,
approve_payroll_run,
create_payroll_run,
create_salary_structure,
generate_payslips_for_run,
get_payroll_run_or_404,
get_salary_structure_or_404,
list_own_payslips,
list_payroll_runs,
list_payslips,
list_salary_structures,
mark_payroll_run_paid,
update_salary_structure,
)
router = APIRouter(prefix="/employees", tags=["employees-ui"])
portal_router = APIRouter(prefix="/employee", tags=["employee-portal-ui"])
def _redirect_login():
return RedirectResponse(url="/login", status_code=303)
def _redirect_denied():
from app.core.http_responses import ui_access_denied
return ui_access_denied()
def _base_ctx(request: Request, db, current_user, **ctx):
base = {
"request": request,
"current_user": current_user,
"current_user_roles": get_user_roles(db, current_user.id),
"current_user_permissions": get_user_permissions(db, current_user.id),
"csrf_token": get_or_create_csrf_token(request),
"employee_statuses": EMPLOYEE_STATUS,
"employment_types": EMPLOYMENT_TYPES,
"employee_role_names": EMPLOYEE_ROLE_NAMES,
"attendance_statuses": ATTENDANCE_STATUS,
"attendance_approval_statuses": ATTENDANCE_APPROVAL_STATUS,
"leave_request_statuses": LEAVE_REQUEST_STATUS,
"document_statuses": DOCUMENT_STATUS,
"document_visibilities": DOCUMENT_VISIBILITY,
"onboarding_task_statuses": ONBOARDING_TASK_STATUS,
"offboarding_request_statuses": OFFBOARDING_REQUEST_STATUS,
"offboarding_task_statuses": OFFBOARDING_TASK_STATUS,
"payroll_run_statuses": PAYROLL_RUN_STATUS,
"payslip_statuses": PAYSLIP_STATUS,
}
base.update(ctx)
return base
def _render(request: Request, template: str, db, current_user, **ctx):
return templates.TemplateResponse(template, _base_ctx(request, db, current_user, **ctx))
def _client_ip(request: Request) -> str | None:
forwarded_for = request.headers.get("x-forwarded-for")
if forwarded_for:
return forwarded_for.split(",")[0].strip() or None
real_ip = request.headers.get("x-real-ip")
if real_ip:
return real_ip.strip() or None
return request.client.host if request.client else None
def _active_financial_year(request: Request) -> str | None:
value = request.session.get("active_financial_year") or getattr(request.state, "year_code", None)
value = (value or "").strip()
return value or None
def _form_bool(value) -> bool:
return value in ("1", "true", "True", "on", "yes")
def _form_payload(form, *, include_context: bool = False):
payload = {
"user_id": int(form.get("user_id")) if form.get("user_id") not in (None, "", "None") else None,
"employee_code": form.get("employee_code", ""),
"full_name": form.get("full_name", ""),
"email": form.get("email"),
"mobile": form.get("mobile"),
"alternate_mobile": form.get("alternate_mobile"),
"date_of_joining": form.get("date_of_joining"),
"date_of_leaving": form.get("date_of_leaving"),
"employment_type": form.get("employment_type") or "full_time",
"status": form.get("status") or "active",
"is_active": _form_bool(form.get("is_active")),
"department": form.get("department"),
"designation": form.get("designation"),
"reporting_manager_user_id": int(form.get("reporting_manager_user_id")) if form.get("reporting_manager_user_id") not in (None, "", "None") else None,
"pan": form.get("pan"),
"uan": form.get("uan"),
"esi_no": form.get("esi_no"),
"pf_no": form.get("pf_no"),
"aadhaar_last4": form.get("aadhaar_last4"),
"bank_name": form.get("bank_name"),
"bank_account_no": form.get("bank_account_no"),
"bank_ifsc": form.get("bank_ifsc"),
"address": form.get("address"),
"emergency_contact_name": form.get("emergency_contact_name"),
"emergency_contact_mobile": form.get("emergency_contact_mobile"),
"notes": form.get("notes"),
"create_login_user": _form_bool(form.get("create_login_user")),
"login_email": form.get("login_email"),
"temporary_password": form.get("temporary_password"),
"employee_role": form.get("employee_role") or "Staff",
}
if include_context:
payload["tenant_id"] = int(form.get("tenant_id")) if form.get("tenant_id") not in (None, "", "None") else None
payload["branch_id"] = int(form.get("branch_id")) if form.get("branch_id") not in (None, "", "None") else None
return payload
def _form_options(db, current_user, scope, *, include_user_id: int | None = None):
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),
"managers": list_reporting_managers(db, scope),
"scope": scope,
}
UPLOAD_ROOT = Path("data/uploads/employee_documents")
ALLOWED_DOCUMENT_EXTENSIONS = {".pdf", ".jpg", ".jpeg", ".png", ".webp", ".doc", ".docx", ".xls", ".xlsx"}
def _document_type_payload(form):
return {
"code": form.get("code"),
"name": form.get("name"),
"description": form.get("description"),
"is_mandatory": _form_bool(form.get("is_mandatory")),
"allow_employee_upload": _form_bool(form.get("allow_employee_upload")),
"requires_verification": _form_bool(form.get("requires_verification")),
"is_active": _form_bool(form.get("is_active")),
}
def _safe_upload_filename(filename: str) -> str:
base = Path(filename or "document").name.replace(" ", "_")
keep = []
for ch in base:
keep.append(ch if ch.isalnum() or ch in {".", "_", "-"} else "_")
safe = "".join(keep).strip("._") or "document"
return safe[:180]
def _onboarding_item_payload(form):
return {
"code": form.get("code"),
"title": form.get("title"),
"description": form.get("description"),
"stage": form.get("stage") or "joining",
"default_due_days": int(form.get("default_due_days") or 0),
"sort_order": int(form.get("sort_order") or 0),
"is_mandatory": _form_bool(form.get("is_mandatory")),
"is_active": _form_bool(form.get("is_active")),
}
def _offboarding_payload(form):
return {
"request_type": form.get("request_type") or "resignation",
"requested_relieving_date": form.get("requested_relieving_date"),
"reason": form.get("reason"),
"handover_notes": form.get("handover_notes"),
}
async def _save_employee_upload(upload: UploadFile, *, tenant_id: int, employee_id: int) -> tuple[str, str, str, int | None, str | None]:
original = _safe_upload_filename(upload.filename or "document")
suffix = Path(original).suffix.lower()
if suffix not in ALLOWED_DOCUMENT_EXTENSIONS:
raise ValueError("Unsupported file type. Allowed: PDF, images, Word and Excel files.")
data = await upload.read()
if not data:
raise ValueError("Please choose a non-empty file to upload.")
max_size = 10 * 1024 * 1024
if len(data) > max_size:
raise ValueError("File size should not exceed 10 MB.")
folder = UPLOAD_ROOT / str(tenant_id) / str(employee_id)
folder.mkdir(parents=True, exist_ok=True)
stored = f"{uuid4().hex}{suffix}"
path = folder / stored
path.write_bytes(data)
return original, stored, str(path.as_posix()), len(data), upload.content_type
@router.get("/dashboard")
def employee_hr_dashboard(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.dashboard.view")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id")
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
stats = get_employee_dashboard_stats(db, scope)
return _render(
request,
"modules/employees/templates/employees/hr_dashboard.html",
db,
current_user,
title="HR Dashboard",
stats=stats,
scope=scope,
)
finally:
db.close()
@router.get("")
def employees_list(request: Request, q: str = "", include_inactive: str | None = None, link_status: str = "all"):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.view")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
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"
rows = list_employees(
db,
scope,
q=q,
include_inactive=bool(include_inactive),
link_status=normalized_link_status,
)
link_summary = get_employee_user_link_summary(db, scope)
return _render(
request,
"modules/employees/templates/employees/list.html",
db,
current_user,
title="Employees",
rows=rows,
q=q,
include_inactive=bool(include_inactive),
link_status=normalized_link_status,
link_summary=link_summary,
scope=scope,
)
finally:
db.close()
@router.get("/new")
def employee_new(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.create")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
return _render(
request,
"modules/employees/templates/employees/form.html",
db,
current_user,
title="Add Employee",
employee=None,
errors=[],
mode="create",
**_form_options(db, current_user, scope),
)
finally:
db.close()
@router.post("/new")
async def employee_create_submit(request: Request):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.create")
except Exception:
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)
try:
emp = create_employee(db, current_user, scope, payload)
return RedirectResponse(url=f"/employees/{emp.id}", status_code=303)
except Exception as exc:
return _render(
request,
"modules/employees/templates/employees/form.html",
db,
current_user,
title="Add Employee",
employee=payload,
errors=[getattr(exc, "detail", str(exc))],
mode="create",
**_form_options(db, current_user, scope),
)
finally:
db.close()
@router.get("/attendance")
def employees_attendance_list(
request: Request,
employee_id: int | None = None,
from_date: str | None = None,
to_date: str | None = None,
status: str | None = None,
approval_status: str | None = None,
):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.attendance.view_all")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
rows = list_attendance_records(
db,
scope,
employee_id=employee_id,
from_date=parse_date(from_date),
to_date=parse_date(to_date),
status=status or None,
approval_status=approval_status or None,
)
employees = list_employees(db, scope, include_inactive=True)
return _render(
request,
"modules/employees/templates/employees/attendance_list.html",
db,
current_user,
title="Employee Attendance",
rows=rows,
employees=employees,
selected_employee_id=employee_id,
from_date=from_date or "",
to_date=to_date or "",
selected_status=status or "",
selected_approval_status=approval_status or "",
scope=scope,
)
finally:
db.close()
@router.post("/attendance/manual")
def employees_attendance_manual_submit(
request: Request,
employee_id: int = Form(...),
attendance_date: str = Form(...),
status: str = Form(...),
remarks: str | None = Form(None),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.attendance.approve")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
create_or_update_manual_attendance(
db,
current_user,
scope,
employee_id=employee_id,
attendance_date=parse_date(attendance_date),
status=status,
remarks=remarks,
)
return RedirectResponse(url="/employees/attendance", status_code=303)
finally:
db.close()
@router.post("/attendance/{attendance_id}/review")
def employees_attendance_review_submit(
request: Request,
attendance_id: int,
approval_status: str = Form(...),
review_notes: str | None = Form(None),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.attendance.approve")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
row = get_attendance_or_404(db, attendance_id, scope)
review_attendance_record(db, current_user, row, approval_status=approval_status, review_notes=review_notes)
return RedirectResponse(url="/employees/attendance", status_code=303)
finally:
db.close()
@router.get("/leave-types")
def employee_leave_types(request: Request, include_inactive: str | None = None):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.leave_type.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id") or current_user.branch_id
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
rows = list_leave_types(db, scope, include_inactive=bool(include_inactive))
return _render(request, "modules/employees/templates/employees/leave_types.html", db, current_user, title="Employee Leave Types", rows=rows, scope=scope, errors=[], include_inactive=bool(include_inactive))
finally:
db.close()
@router.post("/leave-types/defaults")
def employee_leave_type_defaults(request: Request, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.leave_type.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id") or current_user.branch_id
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
ensure_default_leave_types(db, current_user, scope)
return RedirectResponse(url="/employees/leave-types", status_code=303)
finally:
db.close()
@router.post("/leave-types/new")
def employee_leave_type_create(request: Request, code: str = Form(...), name: str = Form(...), description: str | None = Form(None), annual_quota_days: int = Form(0), carry_forward_allowed: str | None = Form(None), allow_negative_balance: str | None = Form(None), requires_approval: str | None = Form(None), is_paid: str | None = Form(None), is_active: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.leave_type.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id") or current_user.branch_id
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
create_leave_type(db, current_user, scope, {"code": code, "name": name, "description": description, "annual_quota_days": annual_quota_days, "carry_forward_allowed": _form_bool(carry_forward_allowed), "allow_negative_balance": _form_bool(allow_negative_balance), "requires_approval": _form_bool(requires_approval), "is_paid": _form_bool(is_paid), "is_active": _form_bool(is_active)})
return RedirectResponse(url="/employees/leave-types", status_code=303)
finally:
db.close()
@router.post("/leave-types/{leave_type_id}/edit")
def employee_leave_type_edit(request: Request, leave_type_id: int, name: str = Form(...), description: str | None = Form(None), annual_quota_days: int = Form(0), carry_forward_allowed: str | None = Form(None), allow_negative_balance: str | None = Form(None), requires_approval: str | None = Form(None), is_paid: str | None = Form(None), is_active: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.leave_type.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id") or current_user.branch_id
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
row = get_leave_type_or_404(db, leave_type_id, scope)
update_leave_type(db, current_user, row, {"name": name, "description": description, "annual_quota_days": annual_quota_days, "carry_forward_allowed": _form_bool(carry_forward_allowed), "allow_negative_balance": _form_bool(allow_negative_balance), "requires_approval": _form_bool(requires_approval), "is_paid": _form_bool(is_paid), "is_active": _form_bool(is_active)})
return RedirectResponse(url="/employees/leave-types", status_code=303)
finally:
db.close()
@router.get("/leave-balances")
def employee_leave_balances(request: Request, employee_id: int | None = None):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.leave_balance.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
rows = list_leave_balances(db, scope, employee_id=employee_id)
employees = list_employees(db, scope, include_inactive=True)
leave_types = list_leave_types(db, scope, include_inactive=False)
return _render(request, "modules/employees/templates/employees/leave_balances.html", db, current_user, title="Employee Leave Balances", rows=rows, employees=employees, leave_types=leave_types, selected_employee_id=employee_id, scope=scope)
finally:
db.close()
@router.post("/leave-balances/adjust")
def employee_leave_balance_adjust(request: Request, employee_id: int = Form(...), leave_type_id: int = Form(...), adjusted_days: int = Form(0), notes: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.leave_balance.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
adjust_leave_balance(db, current_user, scope, employee_id=employee_id, leave_type_id=leave_type_id, adjusted_days=adjusted_days, notes=notes)
return RedirectResponse(url=f"/employees/leave-balances?employee_id={employee_id}", status_code=303)
finally:
db.close()
@router.get("/leaves")
@router.get("/leave")
def employee_leave_requests(request: Request, employee_id: int | None = None, status: str = "pending"):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.leave.view_all")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
rows = list_leave_requests(db, scope, employee_id=employee_id, status=status if status != "all" else None)
employees = list_employees(db, scope, include_inactive=True)
return _render(request, "modules/employees/templates/employees/leave_requests.html", db, current_user, title="Employee Leave Requests", rows=rows, employees=employees, status=status, selected_employee_id=employee_id, statuses=LEAVE_REQUEST_STATUS, scope=scope)
finally:
db.close()
@router.post("/leave/{request_id}/review")
def employee_leave_review_submit(request: Request, request_id: int, status: str = Form(...), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.leave.approve")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
row = get_leave_request_or_404(db, request_id, scope)
review_leave_request(db, current_user, row, status=status, review_notes=review_notes)
return RedirectResponse(url="/employees/leave", status_code=303)
finally:
db.close()
@router.get("/registration-requests")
def employee_registration_requests(request: Request, status: str = "pending"):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.registration.approve")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
rows = list_employee_registration_requests(db, scope, status=status if status != "all" else None)
return _render(
request,
"modules/employees/templates/employees/registration_requests.html",
db,
current_user,
title="Employee Registration Requests",
rows=rows,
status=status,
statuses=REGISTRATION_STATUS,
scope=scope,
)
finally:
db.close()
@router.get("/registration-requests/{request_id}")
def employee_registration_request_detail(request: Request, request_id: int):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.registration.approve")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
req = get_employee_registration_request_or_404(db, request_id, scope)
return _render(
request,
"modules/employees/templates/employees/registration_request_detail.html",
db,
current_user,
title=f"Employee Registration #{req.id}",
item=req,
error=None,
)
finally:
db.close()
@router.post("/registration-requests/{request_id}/approve")
def employee_registration_request_approve(
request: Request,
request_id: int,
employee_code: str | None = Form(None),
review_notes: str | None = Form(None),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.registration.approve")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
req = get_employee_registration_request_or_404(db, request_id, scope)
emp = approve_employee_registration_request(db, current_user, req, employee_code=employee_code, notes=review_notes)
return RedirectResponse(url=f"/employees/{emp.id}", status_code=303)
finally:
db.close()
@router.post("/registration-requests/{request_id}/reject")
def employee_registration_request_reject(
request: Request,
request_id: int,
review_notes: str | None = Form(None),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.registration.approve")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
req = get_employee_registration_request_or_404(db, request_id, scope)
reject_employee_registration_request(db, current_user, req, notes=review_notes)
return RedirectResponse(url="/employees/registration-requests", status_code=303)
finally:
db.close()
@router.get("/onboarding-checklist")
def employee_onboarding_checklist(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.onboarding.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id") or current_user.branch_id
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
return _render(request, "modules/employees/templates/employees/onboarding_checklist.html", db, current_user, title="Onboarding Checklist", rows=list_onboarding_checklist_items(db, scope), errors=[], **_form_options(db, current_user, scope))
finally:
db.close()
@router.post("/onboarding-checklist/defaults")
def employee_onboarding_checklist_defaults(request: Request, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.onboarding.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id") or current_user.branch_id
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
ensure_default_onboarding_checklist(db, current_user, scope)
return RedirectResponse(url="/employees/onboarding-checklist", status_code=303)
finally:
db.close()
@router.post("/onboarding-checklist")
async def employee_onboarding_checklist_create(request: Request):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.onboarding.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id") or current_user.branch_id
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
create_onboarding_checklist_item(db, current_user, scope, _onboarding_item_payload(form))
return RedirectResponse(url="/employees/onboarding-checklist", status_code=303)
finally:
db.close()
@router.post("/onboarding-checklist/{item_id}/edit")
async def employee_onboarding_checklist_edit(request: Request, item_id: int):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.onboarding.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id") or current_user.branch_id
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
item = get_onboarding_checklist_item_or_404(db, item_id, scope)
update_onboarding_checklist_item(db, current_user, item, _onboarding_item_payload(form))
return RedirectResponse(url="/employees/onboarding-checklist", status_code=303)
finally:
db.close()
@router.get("/onboarding")
def employee_onboarding_tasks(request: Request, employee_id: int | None = None):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.onboarding.view")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
return _render(request, "modules/employees/templates/employees/onboarding_tasks.html", db, current_user, title="Employee Onboarding", rows=list_onboarding_tasks(db, scope, employee_id=employee_id), employees=list_employees(db, scope, include_inactive=True), selected_employee_id=employee_id, errors=[])
finally:
db.close()
@router.post("/{employee_id}/onboarding/generate")
def employee_generate_onboarding(request: Request, employee_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.onboarding.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
employee = get_employee_or_404(db, employee_id, scope)
generate_onboarding_tasks_for_employee(db, current_user, employee, scope)
return RedirectResponse(url=f"/employees/onboarding?employee_id={employee_id}", status_code=303)
finally:
db.close()
@router.post("/onboarding/{task_id}/status")
def employee_onboarding_task_status(request: Request, task_id: int, status: str = Form(...), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.onboarding.approve")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
task = get_onboarding_task_or_404(db, task_id, scope)
update_onboarding_task_status(db, current_user, task, status, review_notes)
return RedirectResponse(url=f"/employees/onboarding?employee_id={task.employee_id}", status_code=303)
finally:
db.close()
@router.get("/offboarding")
def employee_offboarding_requests(request: Request, status: str | None = None):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.offboarding.view")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
rows = list_offboarding_requests(db, scope, status=status if status else None)
return _render(request, "modules/employees/templates/employees/offboarding_requests.html", db, current_user, title="Employee Offboarding", rows=rows, employees=list_employees(db, scope, include_inactive=True), selected_status=status, errors=[])
finally:
db.close()
@router.post("/{employee_id}/offboarding/initiate")
async def employee_offboarding_initiate(request: Request, employee_id: int):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.offboarding.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
employee = get_employee_or_404(db, employee_id, scope)
create_offboarding_request(db, current_user, employee, _offboarding_payload(form), source="admin")
return RedirectResponse(url="/employees/offboarding", status_code=303)
finally:
db.close()
@router.post("/offboarding/{request_id}/review")
def employee_offboarding_review(request: Request, request_id: int, status: str = Form(...), approved_relieving_date: str | None = Form(None), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.offboarding.approve")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
req = get_offboarding_request_or_404(db, request_id, scope)
review_offboarding_request(db, current_user, req, status=status, approved_relieving_date=parse_date(approved_relieving_date), review_notes=review_notes)
return RedirectResponse(url="/employees/offboarding", status_code=303)
finally:
db.close()
@router.post("/offboarding/tasks/{task_id}/status")
def employee_offboarding_task_status(request: Request, task_id: int, status: str = Form(...), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.offboarding.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
task = get_offboarding_task_or_404(db, task_id, scope)
update_offboarding_task_status(db, current_user, task, status, review_notes)
return RedirectResponse(url="/employees/offboarding", status_code=303)
finally:
db.close()
@router.post("/offboarding/{request_id}/complete")
def employee_offboarding_complete(request: Request, request_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.offboarding.approve")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
req = get_offboarding_request_or_404(db, request_id, scope)
complete_offboarding_request(db, current_user, req)
return RedirectResponse(url="/employees/offboarding", status_code=303)
finally:
db.close()
@router.get("/document-types")
def employee_document_types(request: Request, include_inactive: str | None = None):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.document_type.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
rows = list_document_types(db, scope, include_inactive=bool(include_inactive))
return _render(request, "modules/employees/templates/employees/document_types.html", db, current_user, title="Employee Document Types", rows=rows, scope=scope, include_inactive=bool(include_inactive), editing=None, errors=[])
finally:
db.close()
@router.post("/document-types/defaults")
def employee_document_types_defaults(request: Request, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.document_type.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
ensure_default_document_types(db, current_user, scope)
return RedirectResponse(url="/employees/document-types", status_code=303)
finally:
db.close()
@router.post("/document-types")
async def employee_document_type_create(request: Request):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.document_type.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
try:
create_document_type(db, current_user, scope, _document_type_payload(form))
return RedirectResponse(url="/employees/document-types", status_code=303)
except Exception as exc:
return _render(request, "modules/employees/templates/employees/document_types.html", db, current_user, title="Employee Document Types", rows=list_document_types(db, scope, include_inactive=True), scope=scope, include_inactive=True, editing=None, errors=[getattr(exc, "detail", str(exc))])
finally:
db.close()
@router.post("/document-types/{type_id}/edit")
async def employee_document_type_update(request: Request, type_id: int):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.document_type.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
row = get_document_type_or_404(db, type_id, scope)
update_document_type(db, current_user, row, _document_type_payload(form))
return RedirectResponse(url="/employees/document-types", status_code=303)
finally:
db.close()
@router.get("/documents")
def employee_documents(request: Request, employee_id: int | None = None, status: str | None = None):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.documents.view_all")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
return _render(request, "modules/employees/templates/employees/documents.html", db, current_user, title="Employee Documents", rows=list_employee_documents(db, scope, employee_id=employee_id, status=status), employees=list_employees(db, scope, include_inactive=True), document_types=list_document_types(db, scope), scope=scope, selected_employee_id=employee_id, selected_status=status, errors=[])
finally:
db.close()
@router.post("/{employee_id}/documents/upload")
async def employee_document_upload(request: Request, employee_id: int, document_file: UploadFile = File(...)):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.documents.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
employee = get_employee_or_404(db, employee_id, scope)
try:
original, stored, path, size, content_type = await _save_employee_upload(document_file, tenant_id=employee.tenant_id, employee_id=employee.id)
create_employee_document_record(
db,
current_user,
employee,
document_type_id=int(form.get("document_type_id")) if form.get("document_type_id") not in (None, "", "None") else None,
title=form.get("title") or original,
document_no=form.get("document_no"),
issue_date=parse_date(form.get("issue_date")),
expiry_date=parse_date(form.get("expiry_date")),
original_filename=original,
stored_filename=stored,
storage_path=path,
content_type=content_type,
file_size_bytes=size,
remarks=form.get("remarks"),
visibility=form.get("visibility") or "employee_and_hr",
)
return RedirectResponse(url=f"/employees/documents?employee_id={employee.id}", status_code=303)
except Exception as exc:
return _render(request, "modules/employees/templates/employees/documents.html", db, current_user, title="Employee Documents", rows=list_employee_documents(db, scope, employee_id=employee.id), employees=list_employees(db, scope, include_inactive=True), document_types=list_document_types(db, scope), scope=scope, selected_employee_id=employee.id, selected_status=None, errors=[getattr(exc, "detail", str(exc))])
finally:
db.close()
@router.post("/documents/{document_id}/review")
def employee_document_review(request: Request, document_id: int, status: str = Form(...), verification_notes: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.documents.verify")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
row = get_employee_document_or_404(db, document_id, scope)
review_employee_document(db, current_user, row, status=status, verification_notes=verification_notes)
return RedirectResponse(url="/employees/documents", status_code=303)
finally:
db.close()
@router.post("/documents/{document_id}/archive")
def employee_document_archive(request: Request, document_id: int, notes: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.documents.delete")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
row = get_employee_document_or_404(db, document_id, scope)
archive_employee_document(db, current_user, row, notes=notes)
return RedirectResponse(url="/employees/documents", status_code=303)
finally:
db.close()
@portal_router.get("/documents")
def employee_self_documents(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.documents.view_self")
except Exception:
return _redirect_denied()
employee = get_employee_for_user(db, current_user)
document_types = []
if employee:
scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
document_types = [dt for dt in list_document_types(db, scope) if dt.allow_employee_upload]
return _render(request, "modules/employees/templates/employees/self_documents.html", db, current_user, title="My Documents", employee=employee, rows=list_own_employee_documents(db, current_user), document_types=document_types, errors=[])
finally:
db.close()
@portal_router.post("/documents/upload")
async def employee_self_document_upload(request: Request, document_file: UploadFile = File(...)):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.documents.upload_self")
except Exception:
return _redirect_denied()
employee = get_employee_for_user(db, current_user)
if not employee:
return RedirectResponse(url="/employee/register", status_code=303)
try:
original, stored, path, size, content_type = await _save_employee_upload(document_file, tenant_id=employee.tenant_id, employee_id=employee.id)
create_employee_document_record(
db,
current_user,
employee,
document_type_id=int(form.get("document_type_id")) if form.get("document_type_id") not in (None, "", "None") else None,
title=form.get("title") or original,
document_no=form.get("document_no"),
issue_date=parse_date(form.get("issue_date")),
expiry_date=parse_date(form.get("expiry_date")),
original_filename=original,
stored_filename=stored,
storage_path=path,
content_type=content_type,
file_size_bytes=size,
remarks=form.get("remarks"),
visibility="employee_and_hr",
)
return RedirectResponse(url="/employee/documents", status_code=303)
except Exception as exc:
scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
return _render(request, "modules/employees/templates/employees/self_documents.html", db, current_user, title="My Documents", employee=employee, rows=list_own_employee_documents(db, current_user), document_types=[dt for dt in list_document_types(db, scope) if dt.allow_employee_upload], errors=[getattr(exc, "detail", str(exc))])
finally:
db.close()
@router.get("/payroll/structures")
def employee_salary_structures(request: Request, employee_id: int | None = None):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.payroll.structure.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
return _render(
request,
"modules/employees/templates/employees/payroll_structures.html",
db,
current_user,
title="Employee Salary Structures",
rows=list_salary_structures(db, scope, employee_id=employee_id),
employees=list_employees(db, scope, include_inactive=True),
selected_employee_id=employee_id,
scope=scope,
errors=[],
)
finally:
db.close()
@router.post("/payroll/structures")
async def employee_salary_structure_submit(request: Request):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.payroll.structure.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
payload = dict(form)
payload["is_active"] = _form_bool(form.get("is_active"))
try:
create_salary_structure(db, current_user, scope, payload)
return RedirectResponse(url="/employees/payroll/structures", status_code=303)
except Exception as exc:
return _render(request, "modules/employees/templates/employees/payroll_structures.html", db, current_user, title="Employee Salary Structures", rows=list_salary_structures(db, scope), employees=list_employees(db, scope, include_inactive=True), selected_employee_id=None, scope=scope, errors=[getattr(exc, "detail", str(exc))], form=payload)
finally:
db.close()
@router.post("/payroll/structures/{structure_id}/update")
async def employee_salary_structure_update(request: Request, structure_id: int):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.payroll.structure.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
row = get_salary_structure_or_404(db, structure_id, scope)
payload = dict(form)
payload["is_active"] = _form_bool(form.get("is_active"))
update_salary_structure(db, current_user, row, payload)
return RedirectResponse(url="/employees/payroll/structures", status_code=303)
finally:
db.close()
@router.get("/payroll/runs")
def employee_payroll_runs(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.payroll.run")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
return _render(request, "modules/employees/templates/employees/payroll_runs.html", db, current_user, title="Payroll Runs", rows=list_payroll_runs(db, scope), scope=scope, errors=[])
finally:
db.close()
@router.post("/payroll/runs")
def employee_payroll_run_create(request: Request, pay_year: int = Form(...), pay_month: int = Form(...), notes: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.payroll.run")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
create_payroll_run(db, current_user, scope, pay_year=pay_year, pay_month=pay_month, notes=notes)
return RedirectResponse(url="/employees/payroll/runs", status_code=303)
finally:
db.close()
@router.post("/payroll/runs/{run_id}/generate")
def employee_payroll_run_generate(request: Request, run_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.payroll.run")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
run = get_payroll_run_or_404(db, run_id, scope)
generate_payslips_for_run(db, current_user, scope, run)
return RedirectResponse(url=f"/employees/payroll/payslips?payroll_run_id={run_id}", status_code=303)
finally:
db.close()
@router.post("/payroll/runs/{run_id}/approve")
def employee_payroll_run_approve(request: Request, run_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.payroll.payout")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
run = get_payroll_run_or_404(db, run_id, scope)
approve_payroll_run(db, current_user, run)
return RedirectResponse(url="/employees/payroll/runs", status_code=303)
finally:
db.close()
@router.post("/payroll/runs/{run_id}/paid")
def employee_payroll_run_paid(request: Request, run_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.payroll.payout")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
run = get_payroll_run_or_404(db, run_id, scope)
mark_payroll_run_paid(db, current_user, run)
return RedirectResponse(url="/employees/payroll/runs", status_code=303)
finally:
db.close()
@router.get("/payroll/payslips")
def employee_payslips(request: Request, payroll_run_id: int | None = None, employee_id: int | None = None):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.payroll.view")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
return _render(request, "modules/employees/templates/employees/payslips.html", db, current_user, title="Employee Payslips", rows=list_payslips(db, scope, payroll_run_id=payroll_run_id, employee_id=employee_id), employees=list_employees(db, scope, include_inactive=True), runs=list_payroll_runs(db, scope), selected_run_id=payroll_run_id, selected_employee_id=employee_id, scope=scope, errors=[])
finally:
db.close()
@router.get("/progress")
def employee_engagement_progress_dashboard(
request: Request,
q: str = "",
status: str = "open",
assigned_to_user_id: int = 0,
):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.progress.view")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
assignee_id = int(assigned_to_user_id or 0) or None
financial_year = _active_financial_year(request)
progress_payload = list_engagement_progress_dashboard(db, scope, q=q, status=status, assigned_to_user_id=assignee_id, financial_year=financial_year)
assignable_users = list_employee_work_assignable_users(db, scope)
return _render(
request,
"modules/employees/templates/employees/progress_dashboard.html",
db,
current_user,
progress_payload=progress_payload,
assignable_users=assignable_users,
q=q,
status=status,
selected_assigned_to_user_id=assignee_id,
page_title="Engagement Progress",
financial_year=financial_year,
)
finally:
db.close()
@router.get("/work")
def employee_work_assignment_dashboard(
request: Request,
q: str = "",
status: str = "open",
assigned_to_user_id: int = 0,
):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.work.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
assignee_id = int(assigned_to_user_id or 0) or None
financial_year = _active_financial_year(request)
work_payload = list_visible_work_assignment_dashboard(db, scope, q=q, status=status, assigned_to_user_id=assignee_id, financial_year=financial_year)
assignable_users = list_employee_work_assignable_users(db, scope)
return _render(
request,
"modules/employees/templates/employees/work_dashboard.html",
db,
current_user,
title="Employee Work Allocation",
work_payload=work_payload,
assignable_users=assignable_users,
q=q,
status=status,
selected_assigned_to_user_id=assignee_id,
scope=scope,
errors=[],
financial_year=financial_year,
)
finally:
db.close()
@router.post("/work/tasks/{task_id}/assign")
def employee_work_task_assign(
request: Request,
task_id: int,
assigned_to_user_id: str = Form(""),
status: str = Form("pending"),
priority: str = Form("normal"),
internal_target_date: str = Form(""),
remarks: str = Form(""),
return_url: str = Form(""),
csrf_token: str = Form(...),
):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.work.manage")
except Exception:
return _redirect_denied()
validate_csrf(request, csrf_token)
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
update_service_task_assignment(
db,
scope,
task_id,
assigned_to_user_id=int(assigned_to_user_id) if assigned_to_user_id else None,
status=status,
priority=priority,
internal_target_date=parse_date(internal_target_date),
remarks=remarks,
actor_user_id=current_user.id,
financial_year=_active_financial_year(request),
)
safe_return_url = return_url if return_url.startswith(("/employees/work", "/manager/work", "/manager/dashboard")) else "/employees/work"
return RedirectResponse(url=safe_return_url, status_code=303)
finally:
db.close()
@portal_router.post("/work/tasks/{task_id}/status")
def employee_my_work_task_status(
request: Request,
task_id: int,
status: str = Form(...),
remarks: str = Form(""),
return_url: str = Form(""),
csrf_token: str = Form(...),
):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.work.view_self")
except Exception:
return _redirect_denied()
validate_csrf(request, csrf_token)
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
update_own_service_task_status(
db,
scope,
task_id,
status=status,
remarks=remarks,
actor_user_id=current_user.id,
financial_year=_active_financial_year(request),
)
safe_return_url = return_url if return_url.startswith("/employee/work") else "/employee/work"
return RedirectResponse(url=safe_return_url, status_code=303)
finally:
db.close()
@portal_router.get("/work")
def employee_my_work(request: Request, q: str = "", status: str = "open"):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.work.view_self")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
employee = get_employee_for_user(db, current_user)
financial_year = _active_financial_year(request)
work_payload = list_employee_work_kanban(db, scope, q=q, status=status, financial_year=financial_year)
return _render(
request,
"modules/employees/templates/employees/self_work.html",
db,
current_user,
title="My Work",
employee=employee,
work_payload=work_payload,
q=q,
status=status,
errors=[],
financial_year=financial_year,
)
finally:
db.close()
@portal_router.get("/work/engagements/{engagement_id}")
def employee_my_work_engagement_board(request: Request, engagement_id: int):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.work.view_self")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
employee = get_employee_for_user(db, current_user)
financial_year = _active_financial_year(request)
board = get_employee_engagement_work_board(db, scope, engagement_id, financial_year=financial_year)
return _render(
request,
"modules/employees/templates/employees/work_engagement_board.html",
db,
current_user,
title="Engagement Work Board",
employee=employee,
board=board,
errors=[],
financial_year=financial_year,
)
finally:
db.close()
@router.get("/work/tasks/{task_id}/communication")
def employee_work_task_communication(
request: Request,
task_id: int,
):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.work.manage")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
financial_year = _active_financial_year(request)
task = get_work_task_with_communications(db, scope, task_id, financial_year=financial_year)
return _render(
request,
"modules/employees/templates/employees/work_task_communication.html",
db,
current_user,
title="Task Communication",
task=task,
communication_types=TASK_COMMUNICATION_TYPES,
communication_visibilities=TASK_COMMUNICATION_VISIBILITIES,
post_url=f"/employees/work/tasks/{task_id}/communication",
back_url="/employees/work",
is_employee_self=False,
errors=[],
)
finally:
db.close()
@router.post("/work/tasks/{task_id}/communication")
def employee_work_task_add_communication(
request: Request,
task_id: int,
comment_type: str = Form("internal_note"),
visibility: str = Form("internal"),
message: str = Form(...),
csrf_token: str = Form(...),
):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.work.manage")
except Exception:
return _redirect_denied()
validate_csrf(request, csrf_token)
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
add_work_task_communication(
db,
scope,
task_id,
comment_type=comment_type,
visibility=visibility,
message=message,
actor_user_id=current_user.id,
financial_year=_active_financial_year(request),
)
return RedirectResponse(url=f"/employees/work/tasks/{task_id}/communication", status_code=303)
finally:
db.close()
@portal_router.get("/work/tasks/{task_id}/communication")
def employee_my_work_task_communication(
request: Request,
task_id: int,
):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.work.view_self")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
financial_year = _active_financial_year(request)
task = get_work_task_with_communications(db, scope, task_id, assigned_only=True, financial_year=financial_year)
return _render(
request,
"modules/employees/templates/employees/work_task_communication.html",
db,
current_user,
title="My Task Communication",
task=task,
communication_types=TASK_COMMUNICATION_TYPES,
communication_visibilities=TASK_COMMUNICATION_VISIBILITIES,
post_url=f"/employee/work/tasks/{task_id}/communication",
back_url="/employee/work",
is_employee_self=True,
errors=[],
financial_year=financial_year,
)
finally:
db.close()
@portal_router.post("/work/tasks/{task_id}/communication")
def employee_my_work_task_add_communication(
request: Request,
task_id: int,
comment_type: str = Form("internal_note"),
visibility: str = Form("internal"),
message: str = Form(...),
csrf_token: str = Form(...),
):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.work.view_self")
except Exception:
return _redirect_denied()
validate_csrf(request, csrf_token)
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
add_work_task_communication(
db,
scope,
task_id,
comment_type=comment_type,
visibility=visibility,
message=message,
actor_user_id=current_user.id,
assigned_only=True,
financial_year=_active_financial_year(request),
)
return RedirectResponse(url=f"/employee/work/tasks/{task_id}/communication", status_code=303)
finally:
db.close()
@portal_router.get("/payslips")
def employee_self_payslips(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.payroll.view_self")
except Exception:
return _redirect_denied()
employee = get_employee_for_user(db, current_user)
return _render(request, "modules/employees/templates/employees/self_payslips.html", db, current_user, title="My Payslips", employee=employee, rows=list_own_payslips(db, current_user), errors=[])
finally:
db.close()
# -------------------------
# Phase 6J HR Excel imports
# -------------------------
@router.get("/imports")
def employee_imports_page(request: Request, message: str | None = None):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.import")
except Exception:
return _redirect_denied()
return _render(
request,
"modules/employees/templates/employees/imports.html",
db,
current_user,
title="HR Excel Imports",
import_types=supported_import_types(),
message=message,
errors=[],
)
finally:
db.close()
@router.get("/imports/template/{import_type}")
def employee_import_template(request: Request, import_type: str):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.import")
except Exception:
return _redirect_denied()
content = build_template_workbook(import_type)
filename = f"hr_{import_type}_template.xlsx"
return StreamingResponse(
BytesIO(content),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
finally:
db.close()
@router.post("/imports/preview")
async def employee_import_preview(request: Request, import_type: str = Form(...), import_file: UploadFile = File(...)):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.import")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
try:
content = await import_file.read()
preview = preview_import(db, scope, import_type, content)
request.session["employee_import_preview"] = preview
return _render(
request,
"modules/employees/templates/employees/import_preview.html",
db,
current_user,
title="HR Import Preview",
preview=preview,
errors=[],
)
except Exception as exc:
return _render(
request,
"modules/employees/templates/employees/imports.html",
db,
current_user,
title="HR Excel Imports",
import_types=supported_import_types(),
message=None,
errors=[getattr(exc, "detail", str(exc))],
)
finally:
db.close()
@router.post("/imports/commit")
def employee_import_commit(request: Request, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.import")
except Exception:
return _redirect_denied()
preview = request.session.get("employee_import_preview")
if not preview:
return _render(request, "modules/employees/templates/employees/imports.html", db, current_user, title="HR Excel Imports", import_types=supported_import_types(), message=None, errors=["No pending import preview found. Please upload and preview again."])
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
result = commit_import(db, current_user, scope, preview)
request.session.pop("employee_import_preview", None)
message = f"Import completed. Created: {result['created']}, Updated: {result['updated']}, Skipped: {result['skipped']}, Failed: {result['failed']}."
errors = result.get("errors") or []
return _render(request, "modules/employees/templates/employees/imports.html", db, current_user, title="HR Excel Imports", import_types=supported_import_types(), message=message, errors=errors)
finally:
db.close()
@router.get("/{employee_id}")
def employee_detail(request: Request, employee_id: int, link_error: str | None = None):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.view")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
try:
employee = get_employee_or_404(db, employee_id, scope)
except Exception:
return _redirect_denied()
return _render(
request,
"modules/employees/templates/employees/detail.html",
db,
current_user,
title=f"Employee - {employee.full_name}",
employee=employee,
linkable_users=list_linkable_users(db, scope, include_user_id=employee.user_id),
link_error=bool(link_error),
)
finally:
db.close()
@router.get("/{employee_id}/edit")
def employee_edit(request: Request, employee_id: int):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.edit")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
try:
employee = get_employee_or_404(db, employee_id, scope)
except Exception:
return _redirect_denied()
form_scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
return _render(
request,
"modules/employees/templates/employees/form.html",
db,
current_user,
title="Edit Employee",
employee=employee,
errors=[],
mode="edit",
**_form_options(db, current_user, form_scope, include_user_id=employee.user_id),
)
finally:
db.close()
@router.post("/{employee_id}/edit")
async def employee_edit_submit(request: Request, employee_id: int):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.edit")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
employee = get_employee_or_404(db, employee_id, scope)
form_scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
payload = _form_payload(form)
try:
employee = update_employee(db, current_user, employee, payload)
return RedirectResponse(url=f"/employees/{employee.id}", status_code=303)
except Exception as exc:
return _render(
request,
"modules/employees/templates/employees/form.html",
db,
current_user,
title="Edit Employee",
employee=payload | {"id": employee.id, "tenant_id": employee.tenant_id, "branch_id": employee.branch_id},
errors=[getattr(exc, "detail", str(exc))],
mode="edit",
**_form_options(db, current_user, form_scope, include_user_id=employee.user_id),
)
finally:
db.close()
@router.post("/{employee_id}/link-user")
def employee_link_user_submit(request: Request, employee_id: int, user_id: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.edit")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
employee = get_employee_or_404(db, employee_id, scope)
try:
resolved_user_id = int(user_id) if user_id not in (None, "", "None") else None
link_employee_to_user(db, current_user, employee, resolved_user_id)
return RedirectResponse(url=f"/employees/{employee.id}", status_code=303)
except Exception:
return RedirectResponse(url=f"/employees/{employee.id}?link_error=1", status_code=303)
finally:
db.close()
@router.post("/{employee_id}/status")
def employee_status_submit(
request: Request,
employee_id: int,
status: str = Form(...),
date_of_leaving: str | None = Form(None),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.status")
except Exception:
return _redirect_denied()
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
employee = get_employee_or_404(db, employee_id, scope)
change_employee_status(db, current_user, employee, status, parse_date(date_of_leaving))
return RedirectResponse(url=f"/employees/{employee_id}", status_code=303)
finally:
db.close()
@portal_router.get("/dashboard")
def employee_portal_dashboard(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.ess.view")
except Exception:
return _redirect_denied()
employee = get_employee_for_user(db, current_user)
pending_request = None if employee else get_pending_registration_for_user(db, current_user)
work_payload = None
if employee:
scope = build_employee_scope(db, current_user, tenant_id=request.session.get("active_tenant_id") or current_user.tenant_id, branch_id=request.session.get("active_branch_id"))
work_payload = list_employee_work_kanban(db, scope, q="", status="open", financial_year=_active_financial_year(request))
return _render(
request,
"modules/employees/templates/employees/portal_dashboard.html",
db,
current_user,
title="Employee Portal",
employee=employee,
pending_request=pending_request,
today_attendance=get_today_attendance_for_user(db, current_user),
leave_balances=list_own_leave_balances(db, current_user),
leave_requests=list_own_leave_requests(db, current_user)[:5],
offboarding_requests=list_own_offboarding_requests(db, current_user)[:5],
payslips=list_own_payslips(db, current_user)[:5],
work_payload=work_payload,
)
finally:
db.close()
@portal_router.get("/attendance")
def employee_self_attendance(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.attendance.view_self")
except Exception:
return _redirect_denied()
employee = get_employee_for_user(db, current_user)
rows = list_own_attendance(db, current_user)
return _render(
request,
"modules/employees/templates/employees/self_attendance.html",
db,
current_user,
title="My Attendance",
employee=employee,
rows=rows,
today_attendance=get_today_attendance_for_user(db, current_user),
errors=[],
)
finally:
db.close()
@portal_router.post("/attendance/punch-in")
def employee_punch_in_submit(
request: Request,
remarks: str | None = Form(None),
latitude: str | None = Form(None),
longitude: str | None = Form(None),
accuracy_meters: str | None = Form(None),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.attendance.punch")
except Exception:
return _redirect_denied()
punch_in_attendance(
db,
current_user,
remarks=remarks,
latitude=latitude,
longitude=longitude,
accuracy_meters=accuracy_meters,
client_ip=_client_ip(request),
)
return RedirectResponse(url="/employee/attendance", status_code=303)
finally:
db.close()
@portal_router.post("/attendance/punch-out")
def employee_punch_out_submit(
request: Request,
remarks: str | None = Form(None),
latitude: str | None = Form(None),
longitude: str | None = Form(None),
accuracy_meters: str | None = Form(None),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.attendance.punch")
except Exception:
return _redirect_denied()
punch_out_attendance(
db,
current_user,
remarks=remarks,
latitude=latitude,
longitude=longitude,
accuracy_meters=accuracy_meters,
client_ip=_client_ip(request),
)
return RedirectResponse(url="/employee/attendance", status_code=303)
finally:
db.close()
@portal_router.get("/leaves")
@portal_router.get("/leave")
def employee_self_leave(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.leave.view_self")
except Exception:
return _redirect_denied()
employee = get_employee_for_user(db, current_user)
leave_types = []
if employee:
scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
leave_types = list_leave_types(db, scope, include_inactive=False)
return _render(request, "modules/employees/templates/employees/self_leave.html", db, current_user, title="My Leave", employee=employee, rows=list_own_leave_requests(db, current_user), balances=list_own_leave_balances(db, current_user), leave_types=leave_types, errors=[])
finally:
db.close()
@portal_router.post("/leave/apply")
def employee_self_leave_apply(request: Request, leave_type_id: int = Form(...), from_date: str = Form(...), to_date: str = Form(...), reason: str | None = Form(None), csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.leave.apply")
except Exception:
return _redirect_denied()
apply_employee_leave(db, current_user, leave_type_id=leave_type_id, from_date=parse_date(from_date), to_date=parse_date(to_date), reason=reason)
return RedirectResponse(url="/employee/leave", status_code=303)
finally:
db.close()
@portal_router.post("/leave/{request_id}/cancel")
def employee_self_leave_cancel(request: Request, request_id: int, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.leave.apply")
except Exception:
return _redirect_denied()
cancel_own_leave_request(db, current_user, request_id)
return RedirectResponse(url="/employee/leave", status_code=303)
finally:
db.close()
@portal_router.get("/offboarding")
def employee_self_offboarding(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.offboarding.request_self")
except Exception:
return _redirect_denied()
employee = get_employee_for_user(db, current_user)
return _render(request, "modules/employees/templates/employees/self_offboarding.html", db, current_user, title="My Offboarding", employee=employee, rows=list_own_offboarding_requests(db, current_user), errors=[])
finally:
db.close()
@portal_router.post("/offboarding/request")
async def employee_self_offboarding_request(request: Request):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.offboarding.request_self")
except Exception:
return _redirect_denied()
employee = get_employee_for_user(db, current_user)
if not employee:
return RedirectResponse(url="/employee/register", status_code=303)
create_offboarding_request(db, current_user, employee, _offboarding_payload(form), source="employee")
return RedirectResponse(url="/employee/offboarding", status_code=303)
finally:
db.close()
@portal_router.get("/profile")
def employee_self_profile(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.ess.view")
except Exception:
return _redirect_denied()
employee = get_employee_for_user(db, current_user)
if not employee:
return RedirectResponse(url="/employee/register", status_code=303)
return _render(
request,
"modules/employees/templates/employees/self_profile.html",
db,
current_user,
title="My Employee Profile",
employee=employee,
errors=[],
saved=False,
)
finally:
db.close()
@portal_router.post("/profile")
async def employee_self_profile_submit(request: Request):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.ess.profile.edit")
except Exception:
return _redirect_denied()
employee = get_employee_for_user(db, current_user)
if not employee:
return RedirectResponse(url="/employee/register", status_code=303)
payload = {
"mobile": form.get("mobile"),
"alternate_mobile": form.get("alternate_mobile"),
"address": form.get("address"),
"emergency_contact_name": form.get("emergency_contact_name"),
"emergency_contact_mobile": form.get("emergency_contact_mobile"),
"bank_name": form.get("bank_name"),
"bank_account_no": form.get("bank_account_no"),
"bank_ifsc": form.get("bank_ifsc"),
}
try:
profile_photo_path = await save_user_profile_photo(current_user, form.get("profile_photo"))
update_user_public_profile(
db,
current_user,
qualification=form.get("qualification"),
designation=form.get("public_designation") or employee.designation,
mobile=form.get("mobile"),
bio=form.get("bio"),
profile_photo_path=profile_photo_path,
)
employee = update_own_employee_profile(db, current_user, employee, payload)
db.commit()
db.refresh(current_user)
return _render(
request,
"modules/employees/templates/employees/self_profile.html",
db,
current_user,
title="My Employee Profile",
employee=employee,
errors=[],
saved=True,
)
except Exception as exc:
return _render(
request,
"modules/employees/templates/employees/self_profile.html",
db,
current_user,
title="My Employee Profile",
employee=employee,
errors=[getattr(exc, "detail", str(exc))],
saved=False,
)
finally:
db.close()
@portal_router.get("/register")
def employee_registration_form(request: Request):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.registration.request")
except Exception:
return _redirect_denied()
employee = get_employee_for_user(db, current_user)
pending_request = get_pending_registration_for_user(db, current_user)
return _render(
request,
"modules/employees/templates/employees/registration_form.html",
db,
current_user,
title="Request Employee Profile",
employee=employee,
pending_request=pending_request,
errors=[],
)
finally:
db.close()
@portal_router.post("/register")
async def employee_registration_submit(request: Request):
form = await request.form()
validate_csrf(request, form.get("csrf_token"))
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
try:
require_permission(db, current_user, "employees.registration.request")
except Exception:
return _redirect_denied()
payload = {
"requested_employee_code": form.get("requested_employee_code"),
"full_name": form.get("full_name"),
"email": form.get("email"),
"mobile": form.get("mobile"),
"department": form.get("department"),
"designation": form.get("designation"),
"date_of_joining": form.get("date_of_joining"),
"remarks": form.get("remarks"),
}
try:
create_employee_registration_request(db, current_user, payload)
return RedirectResponse(url="/employee/dashboard", status_code=303)
except Exception as exc:
return _render(
request,
"modules/employees/templates/employees/registration_form.html",
db,
current_user,
title="Request Employee Profile",
employee=get_employee_for_user(db, current_user),
pending_request=get_pending_registration_for_user(db, current_user),
errors=[getattr(exc, "detail", str(exc))],
form=payload,
)
finally:
db.close()