From 433b2086b7a5c88632f96ad1b5a80cda7b523383 Mon Sep 17 00:00:00 2001 From: "ahmed.mujtaba" Date: Mon, 14 Sep 2026 19:12:02 +0500 Subject: [PATCH] tested department --- backend/candidate_forms/app.py | 16 +++++++++++++++ backend/candidate_forms/models.py | 28 ++++++++++++++++++++++++++- backend/candidate_forms/views.py | 5 +++++ frontend/src/api/requisitions.js | 5 +++++ frontend/src/lib/queryKeys.js | 1 + frontend/src/screens/Departments.jsx | 23 +++++++++++++++++++--- frontend/src/screens/Requisitions.jsx | 8 ++++---- frontend/src/styles/styles.css | 4 ++++ 8 files changed, 82 insertions(+), 8 deletions(-) diff --git a/backend/candidate_forms/app.py b/backend/candidate_forms/app.py index e596d25..a7d61f0 100644 --- a/backend/candidate_forms/app.py +++ b/backend/candidate_forms/app.py @@ -99,6 +99,22 @@ async def search_requisitions( raise HTTPException(status_code=500, detail=str(e)) +@router.get("/forms/requisition/open-count") +async def count_open_requisitions( + current_user:dict=Depends(require_permission(PermissionTag.REQUISITIONS_VIEW)), + session:AsyncSession=Depends(get_session), +): + """Open requisitions: unlinked, or linked to a job post that is still open.""" + try: + service=RequisitionForm(session=session) + data=await service.count_open(current_user) + return JSONResponse(content={"data":{"open":data},"status_code":200}) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + @router.get("/forms/requisition/fetch") async def fetch_requisition_form( current_user:dict=Depends(require_permission(PermissionTag.REQUISITIONS_VIEW)), diff --git a/backend/candidate_forms/models.py b/backend/candidate_forms/models.py index db8b957..15aa3f6 100644 --- a/backend/candidate_forms/models.py +++ b/backend/candidate_forms/models.py @@ -2,7 +2,7 @@ import uuid from datetime import datetime, date as Date, timezone from typing import TYPE_CHECKING, Optional -from sqlalchemy import DateTime, Enum as SAEnum, JSON, func, or_ +from sqlalchemy import DateTime, Enum as SAEnum, JSON, and_, func, or_ from sqlalchemy.ext.asyncio import AsyncSession from sqlmodel import Field, Relationship, SQLModel, select @@ -130,6 +130,32 @@ class Requisition(SQLModel, table=True): result = await session.execute(statement) return list(result.scalars().all()) + @classmethod + async def count_open(cls, session: AsyncSession, created_by=None) -> int: + """Open requisitions: not linked to a live job post, or linked to one whose + requisition_status is still open. A closed / on-hold / completed job closes + its requisition. job_posts.requisition_id is 1:1, so the join never fans out. + """ + from job.job_post.enums import RequisitionStatus + from job.job_post.models import JobPosts + + statement = ( + select(func.count()) + .select_from(cls) + .outerjoin( + JobPosts, + and_(JobPosts.requisition_id == cls.id, JobPosts.is_deleted == False), # noqa: E712 + ) + .where( + cls.is_deleted == False, # noqa: E712 + or_(JobPosts.id.is_(None), JobPosts.requisition_status == RequisitionStatus.OPEN.value), + ) + ) + if created_by is not None: + statement = statement.where(cls.created_by == created_by) + result = await session.execute(statement) + return int(result.scalar_one()) + @classmethod async def insert_form(cls, session: AsyncSession, fields: dict): position = fields.get("position") if fields.get("position") else {} diff --git a/backend/candidate_forms/views.py b/backend/candidate_forms/views.py index 64681aa..379a508 100644 --- a/backend/candidate_forms/views.py +++ b/backend/candidate_forms/views.py @@ -435,6 +435,11 @@ class RequisitionForm: rows = await Requisition.get_form_by_id(self.session, created_by=created_by) return [serialize_requisition(r) for r in rows] + async def count_open(self, current_user): + # Same scope as the requisition list: admins count every row, others their own. + created_by = None if is_admin(current_user) else _user_id(current_user) + return await Requisition.count_open(self.session, created_by=created_by) + async def search(self, q, top=50, job_post_id=None): rows = await Requisition.search( self.session, q, top=top, job_post_id=job_post_id, diff --git a/frontend/src/api/requisitions.js b/frontend/src/api/requisitions.js index 28026e3..3415fce 100644 --- a/frontend/src/api/requisitions.js +++ b/frontend/src/api/requisitions.js @@ -22,6 +22,11 @@ export function list() { return request('/forms/requisition/fetch') } +/** GET /forms/requisition/open-count — `{data:{open}}`: unlinked, or linked job still open. */ +export function countOpen() { + return request('/forms/requisition/open-count') +} + export function getById(formId) { return request('/forms/requisition/fetch', { params: { form_id: formId } }) } diff --git a/frontend/src/lib/queryKeys.js b/frontend/src/lib/queryKeys.js index 868d9c9..d9d53c9 100644 --- a/frontend/src/lib/queryKeys.js +++ b/frontend/src/lib/queryKeys.js @@ -143,6 +143,7 @@ export const qk = { requisitions: { all: () => ['requisitions'], list: () => ['requisitions', 'list'], + openCount: () => ['requisitions', 'open-count'], detail: (id) => ['requisitions', 'detail', id], search: (q = '', jobPostId = null) => ['requisitions', 'search', q, jobPostId || null], }, diff --git a/frontend/src/screens/Departments.jsx b/frontend/src/screens/Departments.jsx index c7d18ba..3f2e0c3 100644 --- a/frontend/src/screens/Departments.jsx +++ b/frontend/src/screens/Departments.jsx @@ -19,6 +19,7 @@ import { qk } from '../lib/queryKeys' import { friendlyAuthError } from '../lib/errors' import { exportStyledWorkbook } from '../lib/exportXlsx' import * as departmentsApi from '../api/departments' +import * as requisitionsApi from '../api/requisitions' // app.roles id of `department_head` — the backend's default for /department/heads/fetch. const DEPARTMENT_HEAD_ROLE_ID = 5 @@ -64,7 +65,7 @@ function buildExportSheets(rows, stats, exportedAt) { rows: [ { metric: 'Total Departments', value: stats.total, notes: 'Across the organization' }, { metric: 'Candidates Applied', value: stats.candidates, notes: 'Applicants on jobs linked to departments' }, - { metric: 'Open Requisitions', value: stats.openRoles, notes: 'Job posts with requisition status Open' }, + { metric: 'Open Requisitions', value: stats.openRequisitions ?? '—', notes: 'Not linked to a job post, or linked to a job post that is still open' }, { metric: 'Avg. Open Job Posts', value: stats.openShareLabel, notes: `${stats.openRoles} open of ${stats.jobPosts} job posts` }, ], }, @@ -121,6 +122,7 @@ export default function Departments() { const canCreate = can('department.create') const canEdit = can('department.edit') const canExport = can('department.export') + const canViewRequisitions = can('requisitions.view') const listQuery = useQuery({ queryKey: qk.departments.list(), @@ -133,7 +135,16 @@ export default function Departments() { // { row: null | department, editable: boolean } const [editor, setEditor] = useState(null) - const stats = useMemo(() => departmentStats(rowsAll), [rowsAll]) + const requisitionsQuery = useQuery({ + queryKey: qk.requisitions.openCount(), + queryFn: async () => (await requisitionsApi.countOpen())?.data?.open ?? 0, + enabled: canViewRequisitions, + }) + const openRequisitions = requisitionsQuery.data ?? null + const stats = useMemo( + () => ({ ...departmentStats(rowsAll), openRequisitions }), + [rowsAll, openRequisitions], + ) const [exporting, setExporting] = useState(false) async function exportDepartments() { @@ -197,7 +208,13 @@ export default function Departments() {
- +
-