import { ok, type ReadonlyDB, type PaginationInput, buildPaginatedResult, DEFAULT_PAGE_SIZE, } from "@tailor-platform/erp-kit/core"; import type { DB } from "../generated/kysely-tailordb"; export interface ListVacantPositionsInput extends PaginationInput {} /** * Function: listVacantPositions * Returns the paginated list of Positions with no current open primary * Assignment (vacancy/TBH), i.e. the count of current open primary Assignments * for the Position is less than its headcount. Only current (today- * effective) Position generations are considered. */ export async function run(db: ReadonlyDB, input: ListVacantPositionsInput) { const limit = input.limit ?? DEFAULT_PAGE_SIZE; const offset = input.offset ?? 0; const orderBy = input.orderBy ?? "id"; const orderDirection = input.orderDirection ?? "asc"; const now = new Date(); const positions = await db .selectFrom("Position") .selectAll() // Today-effective Position and today-effective occupying Assignments: use window membership, not // "effectiveEnd IS NULL", so a scheduled future change neither hides the position in force today // nor lets a future occupant/vacancy be counted early (M02). .where("effectiveStart", "<=", now) .where((eb) => eb.or([eb("effectiveEnd", "is", null), eb("effectiveEnd", ">=", now)])) .where((eb) => eb( eb .selectFrom("Assignment") .select((eb2) => eb2.fn.countAll().as("count")) .whereRef("Assignment.positionId", "=", "Position.id") .where("Assignment.isPrimary", "=", true) .where("Assignment.effectiveStart", "<=", now) .where((eb2) => eb2.or([ eb2("Assignment.effectiveEnd", "is", null), eb2("Assignment.effectiveEnd", ">=", now), ]), ), "<", eb.ref("Position.headcount"), ), ) .orderBy(orderBy, orderDirection) .limit(limit + 1) .offset(offset) .execute(); return ok(buildPaginatedResult(positions, limit)); }