Files
arrr-erp/app/modules/employees/ui.py
T
2026-08-08 12:55:39 +05:30

3273 lines
132 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 sqlalchemy import select
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.tenancy.settings_models import BranchSettings
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,
start_employee_engagement_workflow,
save_employee_workflow_task,
quick_update_employee_workflow_task,
pause_employee_engagement_workflow,
resume_employee_engagement_workflow,
escalate_employee_engagement_workflow,
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"])
def _csrf_rejected(request: Request):
from app.core.http_responses import forbidden_response
return forbidden_response(request, "CSRF validation failed")
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,
**_upload_policy_context(),
}
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
_ATTENDANCE_MOBILE_ONLY_MESSAGE = (
"Attendance punch-in and punch-out are allowed only from a mobile browser with location/GPS enabled. "
"Please open the ERP on your mobile phone to mark attendance. Desktop access is view-only for attendance history."
)
def _is_mobile_attendance_device(request: Request) -> bool:
"""Server-side guard for staff self-attendance punch actions.
This intentionally does not replace GPS/geofence validation. It only blocks
desktop/laptop browsers from submitting punch-in or punch-out requests.
The existing attendance service still performs employee status, geo/IP,
timing, duplicate punch and approval checks.
"""
user_agent = (request.headers.get("user-agent") or "").strip().lower()
if not user_agent:
return False
mobile_indicators = (
"mobi",
"android",
"iphone",
"ipod",
"ipad",
"windows phone",
"blackberry",
"bb10",
"opera mini",
"opera mobi",
"mobile safari",
)
desktop_indicators = (
"windows nt",
"macintosh",
"x11",
"cros",
"linux x86_64",
)
has_mobile_signal = any(token in user_agent for token in mobile_indicators)
has_desktop_signal = any(token in user_agent for token in desktop_indicators)
if has_mobile_signal:
return True
if has_desktop_signal:
return False
return False
def _attendance_mobile_context(request: Request) -> dict:
is_mobile = _is_mobile_attendance_device(request)
return {
"is_mobile_attendance_device": is_mobile,
"attendance_punch_blocked_reason": None if is_mobile else _ATTENDANCE_MOBILE_ONLY_MESSAGE,
}
def _render_self_attendance_page(request: Request, db, current_user, *, errors: list[str] | None = None):
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),
attendance_rows=rows[:7],
errors=errors or [],
**_attendance_mobile_context(request),
)
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"}
DANGEROUS_DOCUMENT_EXTENSIONS = {
".exe", ".bat", ".cmd", ".com", ".scr", ".pif",
".sh", ".bash", ".zsh", ".ps1", ".psm1",
".js", ".jse", ".vbs", ".vbe", ".wsf",
".msi", ".dll", ".jar", ".apk",
}
DEFAULT_EMPLOYEE_UPLOAD_MAX_MB = 10
def _normalise_extension(ext: str) -> str:
ext = (ext or "").strip().lower()
if not ext:
return ""
return ext if ext.startswith(".") else f".{ext}"
def _extensions_from_csv(raw: str | None, *, fallback: set[str] | None = None) -> set[str]:
values = {_normalise_extension(item) for item in (raw or "").split(",") if item.strip()}
values = {item for item in values if item}
return values or set(fallback or ALLOWED_DOCUMENT_EXTENSIONS)
def _employee_upload_accept(allowed_exts: set[str] | None = None) -> str:
exts = allowed_exts or ALLOWED_DOCUMENT_EXTENSIONS
return ",".join(sorted(exts))
def _employee_upload_help(max_file_mb: int | None = None, allowed_exts: set[str] | None = None) -> str:
exts = ", ".join(ext.lstrip(".").upper() for ext in sorted(allowed_exts or ALLOWED_DOCUMENT_EXTENSIONS))
max_mb = max_file_mb or DEFAULT_EMPLOYEE_UPLOAD_MAX_MB
return f"Allowed file types: {exts}. Maximum size: {max_mb} MB."
def _branch_upload_settings(db, branch_id: int | None) -> tuple[set[str], int]:
if branch_id:
settings = db.execute(select(BranchSettings).where(BranchSettings.branch_id == int(branch_id))).scalar_one_or_none()
if settings:
allowed = _extensions_from_csv(getattr(settings, "allowed_ext_csv", None), fallback=ALLOWED_DOCUMENT_EXTENSIONS)
max_mb = int(getattr(settings, "max_file_mb", DEFAULT_EMPLOYEE_UPLOAD_MAX_MB) or DEFAULT_EMPLOYEE_UPLOAD_MAX_MB)
return allowed, max(1, max_mb)
return set(ALLOWED_DOCUMENT_EXTENSIONS), DEFAULT_EMPLOYEE_UPLOAD_MAX_MB
def _upload_policy_context(db=None, branch_id: int | None = None) -> dict:
allowed, max_mb = _branch_upload_settings(db, branch_id) if db is not None else (set(ALLOWED_DOCUMENT_EXTENSIONS), DEFAULT_EMPLOYEE_UPLOAD_MAX_MB)
return {
"employee_upload_accept": _employee_upload_accept(allowed),
"employee_upload_help": _employee_upload_help(max_mb, allowed),
"employee_upload_max_file_mb": max_mb,
}
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,
allowed_exts: set[str] | None = None,
max_file_mb: int | None = None,
) -> tuple[str, str, str, int | None, str | None]:
original = _safe_upload_filename(upload.filename or "")
if not original:
raise ValueError("Invalid file name. Please choose a valid document to upload.")
suffix = Path(original).suffix.lower()
if not suffix:
raise ValueError("Invalid file type. File extension is required.")
if suffix in DANGEROUS_DOCUMENT_EXTENSIONS:
raise ValueError(f"File type not allowed. Extension {suffix} is blocked for employee document uploads.")
allowed = allowed_exts or ALLOWED_DOCUMENT_EXTENSIONS
if suffix not in allowed:
allowed_label = ", ".join(ext.lstrip(".").upper() for ext in sorted(allowed))
raise ValueError(f"File type not allowed. Extension {suffix} is not permitted. Allowed: {allowed_label}.")
data = await upload.read()
if not data:
raise ValueError("Invalid upload. Please choose a non-empty file to upload.")
max_mb = max_file_mb or DEFAULT_EMPLOYEE_UPLOAD_MAX_MB
max_size = int(max_mb) * 1024 * 1024
if len(data) > max_size:
raise ValueError(f"Upload failed. File size should not exceed {max_mb} 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()
def _safe_employee_return_url(value: str | None, fallback: str) -> str:
candidate = (value or "").strip()
if candidate.startswith("/") and not candidate.startswith("//"):
return candidate
return fallback
@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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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()
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()
# Phase 3 security hardening: list pages must not accept direct unsafe POSTs.
# Existing attendance and leave actions continue to use their dedicated action routes.
@router.post("/attendance")
def employee_attendance_list_post_rejected(request: Request):
return _csrf_rejected(request)
@router.post("/leaves")
@router.post("/leave")
def employee_leave_list_post_rejected(request: Request):
return _csrf_rejected(request)
@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(...),
):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...),
):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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_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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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_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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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_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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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_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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...),
):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...),
):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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)
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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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)
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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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)
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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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")
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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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.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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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.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=[], **_upload_policy_context(db, branch_id))
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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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.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)
allowed_exts, max_file_mb = _branch_upload_settings(db, employee.branch_id)
try:
original, stored, path, size, content_type = await _save_employee_upload(document_file, tenant_id=employee.tenant_id, employee_id=employee.id, allowed_exts=allowed_exts, max_file_mb=max_file_mb)
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))], **_upload_policy_context(db, employee.branch_id))
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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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=[], **_upload_policy_context(db, employee.branch_id if employee else None))
finally:
db.close()
@portal_router.post("/documents/upload")
async def employee_self_document_upload(request: Request, document_file: UploadFile = File(...)):
form = await request.form()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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.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)
allowed_exts, max_file_mb = _branch_upload_settings(db, employee.branch_id)
try:
original, stored, path, size, content_type = await _save_employee_upload(document_file, tenant_id=employee.tenant_id, employee_id=employee.id, allowed_exts=allowed_exts, max_file_mb=max_file_mb)
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))], **_upload_policy_context(db, employee.branch_id))
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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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.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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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)
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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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)
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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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()
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
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()
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
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.post("/work/engagements/{engagement_id}/start")
def employee_my_work_engagement_start(
request: Request,
engagement_id: int,
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()
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
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,
)
start_employee_engagement_workflow(
db,
scope,
engagement_id,
actor_user_id=current_user.id,
financial_year=_active_financial_year(request),
)
return RedirectResponse(
url=f"/employee/work/engagements/{engagement_id}",
status_code=303,
)
finally:
db.close()
@portal_router.get("/work/engagements/{engagement_id}")
def employee_my_work_engagement_board(request: Request, engagement_id: int, task_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.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,
active_task_id=task_id,
)
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()
@portal_router.post("/work/engagements/{engagement_id}/pause")
def employee_workflow_pause(request: Request, engagement_id: int, reason: str = Form(...), notes: str = Form(""), follow_up_date: 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()
try: validate_csrf(request, csrf_token)
except PermissionError: return _csrf_rejected(request)
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"))
try:
pause_employee_engagement_workflow(db, scope, engagement_id, reason=reason, notes=notes, follow_up_date=follow_up_date, actor_user_id=current_user.id, financial_year=_active_financial_year(request))
except ValueError:
db.rollback(); return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?pause_error=1", status_code=303)
return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?paused=1", status_code=303)
finally: db.close()
@portal_router.post("/work/engagements/{engagement_id}/resume")
def employee_workflow_resume(request: Request, engagement_id: int, 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()
try: validate_csrf(request, csrf_token)
except PermissionError: return _csrf_rejected(request)
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"))
try:
resume_employee_engagement_workflow(db, scope, engagement_id, actor_user_id=current_user.id, financial_year=_active_financial_year(request))
except ValueError:
db.rollback(); return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?resume_error=1", status_code=303)
return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?resumed=1", status_code=303)
finally: db.close()
@portal_router.post("/work/engagements/{engagement_id}/escalate")
def employee_workflow_escalate(
request: Request,
engagement_id: int,
level: str = Form("manager"),
message: str = Form(""),
csrf_token: str = Form(""),
):
if not validate_csrf(request, csrf_token):
return _csrf_rejected(request)
with CommonSessionLocal() as db:
current_user = get_current_user(request, db)
if not current_user:
return RedirectResponse(url="/login", status_code=303)
scope = build_employee_scope(
db, current_user,
tenant_id=request.session.get("active_tenant_id") or getattr(current_user, "tenant_id", None),
branch_id=request.session.get("active_branch_id"),
)
try:
escalate_employee_engagement_workflow(
db, scope, engagement_id, level=level, message=message,
actor_user_id=current_user.id,
financial_year=request.session.get("active_financial_year"),
)
except ValueError:
db.rollback()
return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?escalation_error=1", status_code=303)
return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?escalated=1", status_code=303)
@portal_router.post("/work/engagements/{engagement_id}/tasks/{task_id}/quick-action")
def employee_workflow_task_quick_action(
request: Request,
engagement_id: int,
task_id: int,
action: str = Form(...),
reason: 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()
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
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:
saved_task, next_task = quick_update_employee_workflow_task(
db,
scope,
task_id,
action=action,
reason=reason,
actor_user_id=current_user.id,
financial_year=_active_financial_year(request),
)
except ValueError as exc:
db.rollback()
message = str(exc).replace(" ", "+")[:240]
return RedirectResponse(
url=f"/employee/work/engagements/{engagement_id}?task_id={task_id}&quick_error={message}",
status_code=303,
)
target_task = next_task or saved_task
return RedirectResponse(
url=f"/employee/work/engagements/{engagement_id}?task_id={target_task.id}&quick_saved=1",
status_code=303,
)
finally:
db.close()
@portal_router.post("/work/engagements/{engagement_id}/tasks/{task_id}/save")
def employee_workflow_task_save(
request: Request,
engagement_id: int,
task_id: int,
status: str = Form("pending"),
remarks: str = Form(""),
checklist_response: str = Form(""),
checklist_text_response: str = Form(""),
checklist_number_response: str = Form(""),
checklist_date_response: str = Form(""),
checklist_remarks: str = Form(""),
workflow_action: str = Form("save"),
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()
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
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:
saved_task, next_task = save_employee_workflow_task(
db,
scope,
task_id,
status=status,
remarks=remarks,
checklist_response=checklist_response,
checklist_text_response=checklist_text_response,
checklist_number_response=checklist_number_response,
checklist_date_response=checklist_date_response,
checklist_remarks=checklist_remarks,
actor_user_id=current_user.id,
financial_year=_active_financial_year(request),
)
except ValueError:
db.rollback()
return RedirectResponse(
url=f"/employee/work/engagements/{engagement_id}?task_id={task_id}&workflow_error=1",
status_code=303,
)
target_task = next_task if workflow_action == "save_next" and next_task else saved_task
return RedirectResponse(
url=f"/employee/work/engagements/{engagement_id}?task_id={target_task.id}&saved=1",
status_code=303,
)
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=_safe_employee_return_url(request.query_params.get("return_to"), "/employees/work"),
return_to=_safe_employee_return_url(request.query_params.get("return_to"), "/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(...),
return_to: 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()
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
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=_safe_employee_return_url(return_to, 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=_safe_employee_return_url(request.query_params.get("return_to"), "/employee/work"),
return_to=_safe_employee_return_url(request.query_params.get("return_to"), "/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(...),
return_to: 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()
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(request)
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=_safe_employee_return_url(return_to, 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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...),
):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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()
EMPLOYEE_PORTAL_TABS = {
"overview": "modules/employees/templates/employees/portal_partials/overview.html",
"work": "modules/employees/templates/employees/portal_partials/work_hub.html",
"work-board": "modules/employees/templates/employees/portal_partials/work.html",
"due-today": "modules/employees/templates/employees/portal_partials/due_today.html",
"overdue": "modules/employees/templates/employees/portal_partials/overdue.html",
"client-pending": "modules/employees/templates/employees/portal_partials/client_pending.html",
"returned": "modules/employees/templates/employees/portal_partials/returned.html",
"documents": "modules/employees/templates/employees/portal_partials/documents.html",
"attendance": "modules/employees/templates/employees/portal_partials/attendance_hub.html",
"attendance-details": "modules/employees/templates/employees/portal_partials/attendance.html",
"hr": "modules/employees/templates/employees/portal_partials/hr_hub.html",
"profile": "modules/employees/templates/employees/portal_partials/profile.html",
"leave": "modules/employees/templates/employees/portal_partials/leave.html",
"payslips": "modules/employees/templates/employees/portal_partials/payslips.html",
"offboarding": "modules/employees/templates/employees/portal_partials/offboarding.html",
"alerts": "modules/employees/templates/employees/portal_partials/alerts.html",
"reports": "modules/employees/templates/employees/portal_partials/reports.html",
}
def _employee_portal_context(request: Request, db, current_user, *, active_tab: str = "overview"):
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 {
"title": "Employee Portal",
"active_tab": active_tab if active_tab in EMPLOYEE_PORTAL_TABS else "overview",
"employee": employee,
"pending_request": pending_request,
"today_attendance": get_today_attendance_for_user(db, current_user),
"attendance_rows": list_own_attendance(db, current_user, limit=10),
"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],
"own_documents": list_own_employee_documents(db, current_user)[:8],
"work_payload": work_payload,
**_attendance_mobile_context(request),
}
@portal_router.get("/dashboard")
def employee_portal_dashboard(request: Request, tab: str = "overview"):
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()
active_tab = tab if tab in EMPLOYEE_PORTAL_TABS else "overview"
return _render(
request,
"modules/employees/templates/employees/portal_dashboard.html",
db,
current_user,
**_employee_portal_context(request, db, current_user, active_tab=active_tab),
)
finally:
db.close()
@portal_router.get("/dashboard/tab/{tab_name}")
def employee_portal_dashboard_tab(request: Request, tab_name: 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.ess.view")
except Exception:
return _redirect_denied()
active_tab = tab_name if tab_name in EMPLOYEE_PORTAL_TABS else "overview"
return _render(
request,
EMPLOYEE_PORTAL_TABS[active_tab],
db,
current_user,
**_employee_portal_context(request, db, current_user, active_tab=active_tab),
)
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()
return _render_self_attendance_page(request, 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(...),
):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.punch")
except Exception:
return _redirect_denied()
if not _is_mobile_attendance_device(request):
return _render_self_attendance_page(request, db, current_user, errors=[_ATTENDANCE_MOBILE_ONLY_MESSAGE])
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(...),
):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.punch")
except Exception:
return _redirect_denied()
if not _is_mobile_attendance_device(request):
return _render_self_attendance_page(request, db, current_user, errors=[_ATTENDANCE_MOBILE_ONLY_MESSAGE])
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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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(...)):
try:
validate_csrf(request, csrf_token)
except PermissionError:
return _csrf_rejected(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.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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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)
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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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.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()
try:
validate_csrf(request, form.get("csrf_token"))
except PermissionError:
return _csrf_rejected(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()
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()