From f86c90807603812df99fd900dc109fac1d59ecaa Mon Sep 17 00:00:00 2001 From: "ahmed.mujtaba" Date: Mon, 14 Sep 2026 18:13:27 +0500 Subject: [PATCH] job post and deaprtrment li kage --- backend/candidate_forms/enums.py | 3 +- backend/candidate_forms/models.py | 15 +-- backend/candidate_forms/serializers.py | 8 +- backend/department/app.py | 4 +- backend/department/models.py | 21 ++-- backend/job/app.py | 1 + backend/job/job_post/models.py | 7 +- backend/job/job_post/serializers.py | 4 +- backend/job/job_post/views.py | 21 ++++ .../manual/040_job_post_department_id.sql | 63 ++++++++++ frontend/src/api/jobs.js | 1 + frontend/src/lib/queryKeys.js | 2 +- frontend/src/screens/Jobs.jsx | 117 ++++++++++++++---- frontend/src/screens/Requisitions.jsx | 31 +---- frontend/src/styles/styles.css | 2 +- 15 files changed, 208 insertions(+), 92 deletions(-) create mode 100644 backend/migrations/manual/040_job_post_department_id.sql diff --git a/backend/candidate_forms/enums.py b/backend/candidate_forms/enums.py index c3a1027..51b0afa 100644 --- a/backend/candidate_forms/enums.py +++ b/backend/candidate_forms/enums.py @@ -2,7 +2,6 @@ from enum import Enum from pydantic import BaseModel from typing import Optional from datetime import date -import uuid class EmploymentType(str,Enum): PERMANENT = "permanent" @@ -11,7 +10,7 @@ class EmploymentType(str,Enum): INTERNEE="internee" class Position(BaseModel): - department_id:Optional[uuid.UUID]=None + department:Optional[str] title:Optional[str] date:Optional[date] date_needed:Optional[date] diff --git a/backend/candidate_forms/models.py b/backend/candidate_forms/models.py index dddfdb0..db8b957 100644 --- a/backend/candidate_forms/models.py +++ b/backend/candidate_forms/models.py @@ -8,7 +8,6 @@ from sqlmodel import Field, Relationship, SQLModel, select from candidate_forms.enums import EmploymentType if TYPE_CHECKING: - from department.models import Department from job.job_post.models import JobPosts @@ -19,8 +18,7 @@ class Requisition(SQLModel, table=True): __tablename__ = "requisitions" id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) - department_id: Optional[uuid.UUID] = Field(default=None, foreign_key="departments.id") - department: Optional["Department"] = Relationship(back_populates="requisitions", sa_relationship_kwargs={"lazy": "selectin"}) + department: Optional[str] = None position_title: Optional[str] = None date: Optional[Date] = None date_needed: Optional[Date] = None @@ -110,7 +108,6 @@ class Requisition(SQLModel, table=True): live job post. Pass `job_post_id` when editing so that job's current requisition stays in the list until the link is cleared. """ - from department.models import Department from job.job_post.models import JobPosts statement = select(cls).where(cls.is_deleted == False) # noqa: E712 @@ -126,7 +123,7 @@ class Requisition(SQLModel, table=True): if term: like = f"%{term}%" statement = statement.where( - or_(cls.position_title.ilike(like), cls.department.has(Department.name.ilike(like))) + or_(cls.position_title.ilike(like), cls.department.ilike(like)) ) limit = max(1, min(int(top or 50), 100)) statement = statement.order_by(cls.created_at.desc(), cls.id.desc()).limit(limit) @@ -139,7 +136,7 @@ class Requisition(SQLModel, table=True): replacement = fields.get("replacement_for") if fields.get("replacement_for") else {} referral = fields.get("refferal_by") if fields.get("refferal_by") else {} row = cls( - department_id=position.get("department_id") if position.get("department_id") else None, + department=position.get("department") if position.get("department") else None, position_title=position.get("title") if position.get("title") else None, date=position.get("date") if position.get("date") else None, date_needed=position.get("date_needed") if position.get("date_needed") else None, @@ -181,8 +178,8 @@ class Requisition(SQLModel, table=True): return None if "position" in fields: position = fields.get("position") if fields.get("position") else {} - if "department_id" in position: - row.department_id = position.get("department_id") if position.get("department_id") else None + if "department" in position: + row.department = position.get("department") if position.get("department") else None if "title" in position: row.position_title = position.get("title") if position.get("title") else None if "date" in position: @@ -247,8 +244,6 @@ class Requisition(SQLModel, table=True): session.add(row) await session.commit() await session.refresh(row) - # department_id may have changed; reload the relationship so the response names the new department. - await session.refresh(row, ["department"]) return row class CandidateForms(SQLModel, table=True): diff --git a/backend/candidate_forms/serializers.py b/backend/candidate_forms/serializers.py index 7b24378..b2968de 100644 --- a/backend/candidate_forms/serializers.py +++ b/backend/candidate_forms/serializers.py @@ -66,12 +66,11 @@ def _enum(value): def serialize_requisition_option(row) -> dict: """Compact row for a searchable picker: `{job title} - {department}`.""" title = (row.position_title or "").strip() - department = (row.department.name if row.department else "").strip() + department = (row.department or "").strip() return { "id": str(row.id) if row.id else None, "title": row.position_title, - "department_id": str(row.department_id) if row.department_id else None, - "department": row.department.name if row.department else None, + "department": row.department, "label": f"{title or 'Untitled'} - {department or '—'}", } @@ -80,8 +79,7 @@ def serialize_requisition(row) -> dict: return { "id": str(row.id) if row.id else None, "position": { - "department_id": str(row.department_id) if row.department_id else None, - "department": row.department.name if row.department else None, + "department": row.department, "title": row.position_title, "date": _date(row.date), "date_needed": _date(row.date_needed), diff --git a/backend/department/app.py b/backend/department/app.py index 6d7d7f0..c987b10 100644 --- a/backend/department/app.py +++ b/backend/department/app.py @@ -147,8 +147,8 @@ async def fetch_department_names( current_user: dict = Depends( require_permission( PermissionTag.DEPARTMENT_VIEW, - PermissionTag.REQUISITIONS_CREATE, - PermissionTag.REQUISITIONS_EDIT, + PermissionTag.JOB_BOARD_CREATE, + PermissionTag.JOBS_EDIT, require_all=False, ) ), diff --git a/backend/department/models.py b/backend/department/models.py index 051d3df..c241dff 100644 --- a/backend/department/models.py +++ b/backend/department/models.py @@ -24,7 +24,9 @@ class Department(SQLModel, table=True): name: str = Field(index=True, unique=True) short_code: str = Field(index=True, unique=True, max_length=10) - requisitions: List["JobPosts"] = Relationship(back_populates="department", sa_relationship_kwargs={"lazy": "selectin"}) + # noload: selectin here would load every job post of a department whenever any job + # post loads its department_ref. Query JobPosts by department_id instead. + job_posts: List["JobPosts"] = Relationship(back_populates="department_ref", sa_relationship_kwargs={"lazy": "noload"}) subtitle: Optional[str] = Field(default=None) description: Optional[str] = Field(default=None) @@ -149,26 +151,17 @@ class Department(SQLModel, table=True): @classmethod async def job_posts_for(cls, session: AsyncSession, department_ids): - """(department_id, job_post_id, requisition_status) for non-deleted job posts linked - to these departments. job_posts.department is free text, so the link is a - case-insensitive match on the department's name or short code. + """(department_id, job_post_id, requisition_status) for non-deleted job posts + linked to these departments through job_posts.department_id. """ from job.job_post.models import JobPosts ids = [i for i in (department_ids or []) if i] if not ids: return [] - job_department = func.lower(func.btrim(JobPosts.department)) result = await session.execute( - select(cls.id, JobPosts.id, JobPosts.requisition_status) - .join( - JobPosts, - or_( - job_department == func.lower(cls.name), - job_department == func.lower(cls.short_code), - ), - ) - .where(cls.id.in_(ids), JobPosts.is_deleted == False) # noqa: E712 + select(JobPosts.department_id, JobPosts.id, JobPosts.requisition_status) + .where(JobPosts.department_id.in_(ids), JobPosts.is_deleted == False) # noqa: E712 ) return result.all() diff --git a/backend/job/app.py b/backend/job/app.py index 0f4eec9..64c4042 100644 --- a/backend/job/app.py +++ b/backend/job/app.py @@ -143,6 +143,7 @@ class HiringCostCreate(BaseModel): class JobUpdate(BaseModel): title: str | None = None department: str | None = None + department_id: UUID | None = None location: str | None = None employment_type: str | None = None vacancies: int | None = None diff --git a/backend/job/job_post/models.py b/backend/job/job_post/models.py index 4793208..0800ac4 100644 --- a/backend/job/job_post/models.py +++ b/backend/job/job_post/models.py @@ -26,8 +26,11 @@ class JobPosts(SQLModel, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) title: str = Field(index=True) - department_id: Optional[uuid.UUID] = Field(default=None, foreign_key="departments.id") - department: Optional["Department"] = Relationship(back_populates="job_posts", sa_relationship_kwargs={"lazy": "selectin"}) + # Many job posts -> one department. `department` (below) stays the free-text name that + # analytics / filters / talent pool key off; set both together (see JobPost views). + # The relationship is `department_ref` because `department` is already that column. + department_id: Optional[uuid.UUID] = Field(default=None, foreign_key="departments.id", index=True) + department_ref: Optional["Department"] = Relationship(back_populates="job_posts", sa_relationship_kwargs={"lazy": "selectin"}) user: Optional["Users"] = Relationship( back_populates="job_posts", diff --git a/backend/job/job_post/serializers.py b/backend/job/job_post/serializers.py index 8bc5474..c9ab14b 100644 --- a/backend/job/job_post/serializers.py +++ b/backend/job/job_post/serializers.py @@ -41,6 +41,7 @@ def serialize_job_post(row, *, names=None) -> dict: "title": row.title, # Talent Pool / candidate filters key off attached job_posts.department. "department": row.department or None, + "department_id": str(row.department_id) if row.department_id else None, "employment_type": row.employment_type, "location": row.location, "experience_min": row.experience_min, @@ -83,6 +84,7 @@ def serialize_job_row(row, *, names=None, recruiter_name=None, hiring_manager_na "id": str(row.id), "title": row.title, "department": row.department or None, + "department_id": str(row.department_id) if row.department_id else None, "location": row.location, "employment_type": row.employment_type, "vacancies": row.vacancies, @@ -104,7 +106,7 @@ def serialize_job_row(row, *, names=None, recruiter_name=None, hiring_manager_na "hiring_manager_name": hiring_manager_name, "requisition_id": str(row.requisition_id) if getattr(row, "requisition_id", None) else None, "requisition_title": req.position_title if req else None, - "requisition_department": req.department.name if req and req.department else None, + "requisition_department": req.department if req else None, "applicant_count": applicant_count, "created_by": str(row.created_by) if row.created_by else None, "created_by_name": row.user.name if getattr(row, "user", None) else None, diff --git a/backend/job/job_post/views.py b/backend/job/job_post/views.py index 3f4f408..33e149b 100644 --- a/backend/job/job_post/views.py +++ b/backend/job/job_post/views.py @@ -83,6 +83,7 @@ class JobPostCreate(BaseModel): location: str | None = None employment_type: str | None = None department: str | None = None + department_id: UUID | None = None vacancies: int = 1 description: str | None = None platform: str | None = None @@ -181,6 +182,7 @@ class JobPost: "salary":payload.get("salary") or "Anonymous", # department is NOT NULL with a server_default of "" — pass "", never None. "department":payload.get("department") or "", + "department_id":None, "vacancies":payload.get("vacancies") or 1, "description":payload.get("description"), "post_text":text, @@ -191,6 +193,10 @@ class JobPost: # Only set platform when it is actually known: passing None would override the # column default and break the NOT NULL constraint. Buffer's channelService # replaces this with the authoritative value once the post is created. + if payload.get("department_id"): + department=await self._require_department(payload.get("department_id")) + fields["department_id"]=department.id + fields["department"]=department.name known_platform=service or normalize_platform(payload.get("platform"),aliases) if known_platform: fields["platform"]=known_platform @@ -424,6 +430,13 @@ class JobPost: hiring_manager_name=names.get(str(row.hiring_manager_id)), ) + async def _require_department(self,department_id): + from department.models import Department + department=await Department.get_by_id(self.session,department_id) + if not department: + raise HTTPException(status_code=404,detail="Department not found") + return department + async def update_job(self,job_post_id,payload,current_user): if not current_user: raise HTTPException(status_code=401,detail="Not authenticated") @@ -444,6 +457,14 @@ class JobPost: fields["salary"]=str(high) if "department" in fields and fields["department"] is None: fields["department"]="" + if "department_id" in payload: + if payload.get("department_id"): + department=await self._require_department(payload.get("department_id")) + fields["department_id"]=department.id + fields["department"]=department.name + else: + fields["department_id"]=None + fields["department"]="" assignment=Assignment(self.session) assigned_by=current_user.get("id") if isinstance(current_user,dict) else None diff --git a/backend/migrations/manual/040_job_post_department_id.sql b/backend/migrations/manual/040_job_post_department_id.sql new file mode 100644 index 0000000..14c9314 --- /dev/null +++ b/backend/migrations/manual/040_job_post_department_id.sql @@ -0,0 +1,63 @@ +-- 040_job_post_department_id.sql +-- Move the department link from requisitions to job posts: many job posts -> one +-- department. Reverses 039 (requisitions.department_id) and adds +-- job_posts.department_id, the FK behind JobPosts.department_id / +-- JobPosts.department_ref and Department.job_posts. +-- +-- job_posts.department (free text) stays: analytics, filters and the talent pool +-- key off it, and the app now writes the department's name there whenever +-- department_id is set. +-- +-- Idempotent; applied at startup by alembic_setup.run_manual_sql() after 039. + +-- ============================================================================= +-- 1. requisitions: drop the 039 link, keeping the department name as text +-- ============================================================================= +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'app' AND table_name = 'requisitions' AND column_name = 'department_id' + ) THEN + -- Rows created while the link existed wrote only department_id; carry the name back. + UPDATE app.requisitions r + SET department = d.name + FROM app.departments d + WHERE r.department_id = d.id + AND (r.department IS NULL OR btrim(r.department) = ''); + + ALTER TABLE app.requisitions + DROP CONSTRAINT IF EXISTS fk_requisitions_department_id_departments; + DROP INDEX IF EXISTS app.ix_requisitions_department_id; + ALTER TABLE app.requisitions DROP COLUMN department_id; + END IF; +END $$; + +-- ============================================================================= +-- 2. job_posts.department_id -> departments.id +-- ============================================================================= +ALTER TABLE app.job_posts + ADD COLUMN IF NOT EXISTS department_id uuid; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'fk_job_posts_department_id_departments' + ) THEN + ALTER TABLE app.job_posts + ADD CONSTRAINT fk_job_posts_department_id_departments + FOREIGN KEY (department_id) REFERENCES app.departments (id); + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS ix_job_posts_department_id + ON app.job_posts (department_id); + +-- Backfill: job posts whose text department equals a department's name or short +-- code (case-insensitive, trimmed). Unmatched rows stay NULL. +UPDATE app.job_posts j +SET department_id = d.id +FROM app.departments d +WHERE j.department_id IS NULL + AND lower(btrim(j.department)) IN (lower(d.name), lower(d.short_code)); diff --git a/frontend/src/api/jobs.js b/frontend/src/api/jobs.js index 1278337..021a942 100644 --- a/frontend/src/api/jobs.js +++ b/frontend/src/api/jobs.js @@ -90,6 +90,7 @@ export function toJobView(row) { id: row.id, title: row.title, department: row.department, + departmentId: row.department_id || null, location: row.location, type: row.employment_type, vacancies: row.vacancies, diff --git a/frontend/src/lib/queryKeys.js b/frontend/src/lib/queryKeys.js index f76b2b2..868d9c9 100644 --- a/frontend/src/lib/queryKeys.js +++ b/frontend/src/lib/queryKeys.js @@ -151,7 +151,7 @@ export const qk = { list: (p = {}) => ['departments', 'list', p], heads: (p = {}) => ['departments', 'heads', p], locations: () => ['departments', 'locations'], - names: () => ['departments', 'names'], + names: (q = '') => ['departments', 'names', q], }, interviews: { all: () => ['interviews'], diff --git a/frontend/src/screens/Jobs.jsx b/frontend/src/screens/Jobs.jsx index 03dae78..72804d3 100644 --- a/frontend/src/screens/Jobs.jsx +++ b/frontend/src/screens/Jobs.jsx @@ -29,6 +29,7 @@ import * as assignmentsApi from '../api/assignments' import * as tasksApi from '../api/tasks' import * as usersApi from '../api/users' import * as requisitionsApi from '../api/requisitions' +import * as departmentsApi from '../api/departments' import * as offersApi from '../api/offers' import { fmtDate, fmtDateTime, fmtShort, toDate } from '../lib/format' import { empTypes } from '../data/seed' @@ -410,7 +411,6 @@ export default function Jobs() { {editing && ( setEditing(null)} onSubmit={(body) => updateJob.mutate({ id: editing.id, body })} @@ -419,7 +419,6 @@ export default function Jobs() { {creating && ( setCreating(false)} onSubmit={(payload, imageFile) => createJob.mutate({ payload, imageFile })} @@ -656,6 +655,49 @@ function useRecruiterDirectory() { }) } +/** + * Department search dropdown over GET /department/names?search=. Options are + * `{id, name}`; the picked row is kept in the list so a selection made from an + * earlier search (or the job's saved department) still renders its name. + */ +function useDepartmentPicker(initialPicked = null) { + const [deptQ, setDeptQ] = useState('') + const [debouncedDeptQ, setDebouncedDeptQ] = useState('') + const [pickedDept, setPickedDept] = useState(initialPicked) + + useEffect(() => { + const t = setTimeout(() => setDebouncedDeptQ(deptQ.trim()), 250) + return () => clearTimeout(t) + }, [deptQ]) + + const departmentsQuery = useQuery({ + queryKey: qk.departments.names(debouncedDeptQ), + queryFn: async () => departmentsApi.toRows(await departmentsApi.listNames({ search: debouncedDeptQ })), + placeholderData: keepPreviousData, + retry: false, + }) + + const departmentOptions = useMemo(() => { + const rows = departmentsQuery.data ?? [] + if (pickedDept && !rows.some((o) => String(o.id) === String(pickedDept.id))) { + return [pickedDept, ...rows] + } + return rows + }, [departmentsQuery.data, pickedDept]) + + /** Pick by name — the requisition picker only knows its department as text. */ + async function pickByName(name) { + const term = String(name || '').trim().toLowerCase() + if (!term) return null + const rows = departmentsApi.toRows(await departmentsApi.listNames({ search: name.trim() })) + const match = rows.find((d) => String(d.name).trim().toLowerCase() === term) + if (match) setPickedDept(match) + return match || null + } + + return { departmentOptions, departmentsQuery, setDeptQ, pickedDept, setPickedDept, pickByName } +} + function useRequisitionPicker(initialPicked = null, jobPostId = null) { const [reqQ, setReqQ] = useState('') const [debouncedReqQ, setDebouncedReqQ] = useState('') @@ -693,17 +735,43 @@ function useRequisitionPicker(initialPicked = null, jobPostId = null) { return { requisitionOptions, requisitionsQuery, setReqQ, setPickedReq } } -function JobForm({ departmentOptions, busy, onClose, onSubmit }) { +/** The Department field on both job forms: search dropdown, stores department_id. */ +function DepartmentSearchSelect({ dept, value, onChange, disabled }) { + return ( + <> + { + onChange(id) + dept.setPickedDept(dept.departmentOptions.find((o) => String(o.id) === String(id)) || null) + }} + onQueryChange={dept.setDeptQ} + placeholder="Search departments…" + disabled={disabled} + loading={dept.departmentsQuery.isPending && !dept.departmentsQuery.data} + allowEmpty + emptyLabel="No department" + /> + {dept.departmentsQuery.isError && ( +

Could not load departments.

+ )} + + ) +} + +function JobForm({ busy, onClose, onSubmit }) { const managersQuery = useManagerDirectory() const recruitersQuery = useRecruiterDirectory() const { requisitionOptions, requisitionsQuery, setReqQ, setPickedReq } = useRequisitionPicker() + const dept = useDepartmentPicker() const form = useFormState({ hiring_manager_id: '', current_recruiter_ids: [], requisition_id: '', title: '', - department: '', + department_id: '', location: '', employment_type: empTypes[0] || 'Full-time', vacancies: '1', @@ -778,7 +846,7 @@ function JobForm({ departmentOptions, busy, onClose, onSubmit }) { // The cover image is uploaded separately right after the row exists. onSubmit({ title: v.title.trim(), - department: v.department.trim() || null, + department_id: v.department_id || null, location: v.location.trim() || null, employment_type: v.employment_type || null, vacancies, @@ -802,7 +870,7 @@ function JobForm({ departmentOptions, busy, onClose, onSubmit }) { // field itself and empty values before building the prompt. const assistContext = () => ({ title: form.values.title, - department: form.values.department, + department: dept.pickedDept?.name || '', location: form.values.location, employment_type: form.values.employment_type, experience_min: form.values.experience_min, @@ -856,7 +924,9 @@ function JobForm({ departmentOptions, busy, onClose, onSubmit }) { if (opt) { setPickedReq(opt) if (!form.values.title.trim() && opt.title) form.setField('title', opt.title) - if (!form.values.department.trim() && opt.department) form.setField('department', opt.department) + if (!form.values.department_id && opt.department) { + dept.pickByName(opt.department).then((d) => { if (d) form.setField('department_id', d.id) }) + } } }} onQueryChange={setReqQ} @@ -881,7 +951,7 @@ function JobForm({ departmentOptions, busy, onClose, onSubmit }) {
- +
- {assist('department')}
- - - {departmentOptions.map((d) => + form.setField('department_id', id)} disabled={busy} />
@@ -1059,7 +1121,7 @@ function JobForm({ departmentOptions, busy, onClose, onSubmit }) { ) } -function EditJobForm({ job: j, departmentOptions, busy, onClose, onSubmit }) { +function EditJobForm({ job: j, busy, onClose, onSubmit }) { const managersQuery = useManagerDirectory() const recruitersQuery = useRecruiterDirectory() const { requisitionOptions, requisitionsQuery, setReqQ, setPickedReq } = useRequisitionPicker( @@ -1073,10 +1135,11 @@ function EditJobForm({ job: j, departmentOptions, busy, onClose, onSubmit }) { : null, j.id, ) + const dept = useDepartmentPicker(j.departmentId ? { id: j.departmentId, name: j.department || 'Current department' } : null) const form = useFormState({ requisition_id: j.requisitionId || '', title: j.title || '', - department: j.department || '', + department_id: j.departmentId || '', location: j.location || '', employment_type: j.type || '', vacancies: j.vacancies != null ? String(j.vacancies) : '1', @@ -1089,7 +1152,7 @@ function EditJobForm({ job: j, departmentOptions, busy, onClose, onSubmit }) { const assistContext = () => ({ title: form.values.title, - department: form.values.department, + department: dept.pickedDept?.name || '', location: form.values.location, employment_type: form.values.employment_type, experience_min: form.values.experience_min, @@ -1118,7 +1181,7 @@ function EditJobForm({ job: j, departmentOptions, busy, onClose, onSubmit }) { if (Object.keys(errors).length) return onSubmit({ title, - department: form.values.department.trim() || null, + department_id: form.values.department_id || null, location: form.values.location.trim() || null, employment_type: form.values.employment_type || null, vacancies: Number(form.values.vacancies) || 1, @@ -1163,7 +1226,9 @@ function EditJobForm({ job: j, departmentOptions, busy, onClose, onSubmit }) { if (opt) { setPickedReq(opt) if (!form.values.title.trim() && opt.title) form.setField('title', opt.title) - if (!form.values.department.trim() && opt.department) form.setField('department', opt.department) + if (!form.values.department_id && opt.department) { + dept.pickByName(opt.department).then((d) => { if (d) form.setField('department_id', d.id) }) + } } }} onQueryChange={setReqQ} @@ -1186,7 +1251,7 @@ function EditJobForm({ job: j, departmentOptions, busy, onClose, onSubmit }) { {form.errors.title}
- +
- {assist('department')}
- form.setField('department', e.target.value)} disabled={busy} /> - {departmentOptions.map((d) => + form.setField('department_id', id)} disabled={busy} />
@@ -1284,7 +1347,7 @@ function JobOwnership({ job, canEdit }) {
Ownership
- + {canEdit ? ( setFields((f) => ({ ...f, [k]: v })) - const departmentsQuery = useQuery({ - queryKey: qk.departments.names(), - queryFn: async () => departmentsApi.toRows(await departmentsApi.listNames()), - staleTime: 5 * 60 * 1000, - }) - const departmentOptions = useMemo(() => { - const opts = departmentsQuery.data ?? [] - // Keep a saved department selectable even if it has since been deactivated. - const pos = row?.position - if (pos?.department_id && !opts.some((d) => d.id === pos.department_id)) { - return [{ id: pos.department_id, name: pos.department || 'Current department' }, ...opts] - } - return opts - }, [departmentsQuery.data, row]) - const save = useMutation({ mutationFn: () => { const body = toPayload(fields) @@ -469,14 +453,7 @@ function RequisitionEditor({ row, allowed, onClose, onSaved, toast }) {
- + set('department', e.target.value)} />
diff --git a/frontend/src/styles/styles.css b/frontend/src/styles/styles.css index 5abbb7c..e3c12bd 100644 --- a/frontend/src/styles/styles.css +++ b/frontend/src/styles/styles.css @@ -769,7 +769,7 @@ canvas { width: 100%; max-width: 100%; display: block; } .form-section-title { margin: var(--space-5) 0 var(--space-1); grid-column: 1/-1; } /* AI field assist (ui/AiFieldAssist.jsx) */ -.field-label-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; } +.field-label-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 22px; } .ai-assist { position: relative; display: inline-flex; } .ai-assist-btn { display: inline-grid; place-items: center; width: 22px; height: 22px; border-radius: 6px; color: var(--primary); background: transparent; transition: .15s; } .ai-assist-btn svg { width: 14px; height: 14px; }