# Offers This directory contains everything related to job offer management: creating offers, managing their hiring pipeline, publishing them to job boards, and tracking applicants through the pipeline stages. ## Directory structure ``` Offers/ ├── index.tsx # Offers list page ├── constants.ts # Form field definitions and validation config ├── utils.ts # Form resolvers, validators, initial values, answer helpers │ ├── shared/ │ └── summaryTypes.ts # Section / Subsection / SummaryItem types for DetailSectionSummary │ ├── components/ # Shared UI components for the Offers sub-module │ ├── OffersFiltersDrawerContent/ # Drawer with department + brand filter accordions │ │ ├── index.tsx │ │ └── components/FilterList/ │ └── OffersSearchBar/ # Search input for the offers list │ ├── JobOfferForm/ # CREATE wizard (multi-step form) │ ├── index.tsx │ └── components/ │ ├── GeneralConfigStep.tsx # Step 1 — isolated, accepts form prop │ ├── ProcessStages/ # Step 2 — isolated, accepts form + jobId props │ │ ├── index.tsx │ │ └── components/ │ │ ├── ProcessStage.tsx │ │ └── ProcessStagesFieldArray.tsx │ ├── PublishStep/ # Step 3 — isolated, accepts publishForm prop │ │ ├── index.tsx │ │ ├── constants.ts │ │ └── components/ │ │ └── ApplicationFormTab/ # Field-visibility editor for the application form │ │ ├── index.tsx │ │ ├── FieldRow.tsx │ │ ├── constants.ts │ │ └── utils.ts │ ├── ReviewStep/ # Step 4 — summary before launch │ │ └── index.tsx │ ├── StepMultipleSummary/ # Multi-row summary widget used in ReviewStep │ │ ├── index.tsx │ │ └── ItemsList.tsx │ └── StepSummary.tsx # Single-row summary widget used in ReviewStep │ ├── JobDetail/ # Read-only job detail view (sections summary) │ ├── index.tsx │ └── components/ │ └── DetailSectionSummary/ # Reusable card for displaying key/value section data │ ├── index.tsx │ ├── ItemsList.tsx │ └── SummaryItemRow.tsx │ ├── JobOfferEdit/ # Edit page — reuses isolated step components (General Config) │ └── index.tsx │ ├── OfferDetail/ # Candidates view for a job offer │ ├── index.tsx │ ├── components/ │ │ ├── DraftJobCard.tsx │ │ ├── JobOfferCandidatesTable/ │ │ ├── JobOfferStatusFilter.tsx │ │ └── JobOfferStagesFilter.tsx │ └── hooks/ │ ├── useApplicantDrawerContent.tsx │ ├── useChangeJobStatus.ts │ ├── useCloseJobOfferErrorModal.tsx │ ├── useCloseJobOfferModal.tsx │ ├── useDeleteJobOfferModal.tsx │ ├── useJobDetailApplicants.ts │ ├── useOfferDetailActions.ts │ ├── useOpenJobOfferModal.tsx │ ├── usePauseJobPosting.ts │ └── useShareJobModal.tsx │ ├── JobApplicationDetail/ # Applicant screening view │ ├── index.tsx │ ├── utils.ts │ ├── constants/ │ │ └── profileDisplayRules.ts # SECTION_LAYOUT_RULES, HIDDEN_SECTION_CODES, FIELD_DISPLAY_RULES, SOCIAL_NETWORK_CONFIG │ ├── components/ │ │ ├── AdditionalDocumentation.tsx │ │ ├── ApplicationStage.tsx │ │ ├── ApplicationStageFiles.tsx │ │ ├── ApplicationStageFooter.tsx │ │ ├── ApplicationStageNotes.tsx │ │ ├── ContactInfoAndActions.tsx │ │ ├── DeleteFileModal.tsx │ │ ├── DeleteNoteModal.tsx │ │ ├── FilesAndNotesEmptyState.tsx │ │ ├── HiredOrRejectedAlert.tsx │ │ ├── MoveApplicantForwardModal.tsx │ │ ├── OfferFile.tsx │ │ ├── OfferSwitch.tsx │ │ ├── RejectApplicantModal.tsx │ │ ├── SideContentTabs.tsx │ │ └── profile/ # Candidate profile tab components │ │ ├── ProfileTab.tsx # Orchestrates all profile sections │ │ ├── ApplicationDataSection.tsx │ │ ├── ApplyFormSection.tsx │ │ ├── LinkListItem.tsx │ │ ├── ProfileFieldValue.tsx │ │ └── SocialNetworksSection.tsx │ └── hooks/ │ ├── useActionDrawers.tsx │ ├── useApplicantUpdateDrawerContent.tsx │ ├── useApplicationFiles.ts │ ├── useFilesDrawerContent.tsx │ ├── useHireModal.tsx │ ├── useMoveApplicantStageDrawerContent.tsx │ └── useNotesDrawerContent.tsx │ ├── JobOfferPublish/ # Manage job board postings │ ├── index.tsx │ └── hooks/ │ ├── useJobOpenFromPublishModal.tsx │ └── usePublicDescriptionWarningModal.tsx │ └── hooks/ # All offer-level hooks ├── useGeneralConfigSection.ts # Isolated General Config form + mutation ├── useProcessStagesSection.ts # Isolated Process Stages form + mutation ├── useJobPublicDescriptionSection.ts # Isolated Publish form + mutation ├── useJobApplicationFormSection.ts # Isolated Application Form visibility mutation ├── useJobForm.tsx # CREATE wizard orchestrator (composes section hooks) ├── useJob.ts # Fetch a single job offer ├── useInfiniteJobs.ts # Paginated job offers list ├── useHiringPipeline.ts # Fetch + mutate hiring pipeline stages ├── useHiringReasons.ts # Fetch hiring reason options ├── useJobBoards.tsx # Check if job boards exist ├── useJobPosting.tsx # Fetch active job posting + construct public URL ├── useJobPublicDescription.ts # Fetch public job description ├── usePublishForm.tsx # Publish step form factory ├── useApplicationDetail.ts # Fetch a single applicant application ├── useApplicationFormAnswers.ts # Fetch applicant form answers ├── useApplicationFormChanges.ts # Track in-progress field-visibility changes (delta map) ├── useApplicationSources.ts # Fetch application source options ├── useJobApplicationForm.ts # Fetch job application form structure ├── useJobApplicationCounts.ts # Fetch per-stage & per-status application counts for a job ├── useJobFilterOptions.ts # Fetch department + brand options for filters ├── useOffersFilters.ts # URL-param-backed filter state (search, departments, brands) ├── useOffersFiltersDrawer.tsx # Opens OffersFiltersDrawerContent via DrawerLayer ├── useRejectionReasons.ts # Fetch rejection reason options └── useRouteId.ts # Parse numeric :id from the current route ``` --- ## Form architecture Each form section is **fully isolated** — it owns its own `useForm` instance, its own `FormProvider`, its own validation schema and resolver. Sections do not share form state. ### Section pattern Every section follows the same contract: ``` useXxxSection(args) → { form, onSave, isLoading, ...sectionSpecificData } → wraps render in → uses form.watch / form.trigger directly (never useFormContext at root level) ``` ### The four form types | Type | Fields | Validator | |---|---|---| | `GeneralConfigForm` | `id`, `name`, `externalId`, `isTestJob`, `department`, `positionName`, `hireReasonId`, `targetCloseDate`, `targetEmployeeStartDate`, `jobOpenings` | `generalConfigFormResolver` | | `ProcessStagesForm` | `processStages: ProcessStageForm[]` | `processStagesFormResolver` | | `JobOfferPublishForm` | `title`, `description` | `JobOfferPublishFormResolver` | | Application Form (visibility) | Field-by-field `JobApplicationFormFieldVisibilityValue` — not an RHF form; managed via `useApplicationFormChanges` delta map | — | ### Field definitions Form field metadata (name, label, placeholder) is defined in `constants.ts`: | Constant / Utility | Purpose | |---|---| | `generalConfigFormFields(t)` | Field metadata typed to `GeneralConfigForm` | | `processStagesFormFields(t)` | Field metadata typed to `ProcessStagesForm` | | `jobOfferPublishFormFields(t)` | Field metadata typed to `JobOfferPublishForm` | | `requiredLabel(label)` / `optionalLabel(label, text)` | Label formatters in `../utils/form.ts` — append `*` or ` (text)` | --- ## Hooks ### Section hooks (reusable across pages) #### `useGeneralConfigSection(job?)` Manages the General Config form and the create/edit job mutation. ```ts const { form, onSave, isLoading, hiringReasons, jobId } = useGeneralConfigSection(job?); ``` - `form` — `UseFormReturn` - `onSave(cb?)` — submits the form; calls `createJob` or `editJob` depending on whether `jobId` exists; calls `cb(showAlert)` on success - `jobId` — starts as `job?.id`, updated after first successful create #### `useProcessStagesSection(jobId)` Manages the Process Stages form and the hiring pipeline mutation. ```ts const { form, onSave, isLoading, backendHiringPipeline, isLoadingBackendHiringPipeline } = useProcessStagesSection(jobId); ``` - `form` — `UseFormReturn` - `onSave(cb?)` — submits the form; calls `updateHiringPipeline`; resets form with returned stage IDs on success - `backendHiringPipeline` — current pipeline from the API (used to render locked stages) #### `useJobPublicDescriptionSection({ jobId, publishForm, publicDescription? })` Manages the save mutation for the publish form. ```ts const { onSave, isLoading } = useJobPublicDescriptionSection({ jobId, publishForm, publicDescription }); ``` - `onSave(cb?)` — calls `createJobOfferPublicDescription` or `editJobOfferPublicDescription` depending on whether `publicDescription` exists; resets form on success #### `useJobApplicationFormSection({ jobId, changes, onSuccess })` Manages the field-visibility update mutation for the Application Form tab. ```ts const { onSave, isLoading, isApplicationFormDirty } = useJobApplicationFormSection({ jobId, changes, onSuccess }); ``` - `changes` — delta map from `useApplicationFormChanges` (only fields that differ from the backend value) - `onSave(cb?)` — no-ops if `changes` is empty; otherwise calls `updateJobApplicationFormVisibility`; invalidates `jobKeys.applicationForm(jobId)` on success ### Wizard hook #### `useJobForm()` — CREATE only Orchestrates the four-step CREATE wizard by composing the three section hooks. Manages step navigation, footer button state, and the open/publish job mutations triggered from the Review step. ```ts const { generalConfigForm, // UseFormReturn processStagesForm, // UseFormReturn publishForm, // UseFormReturn steps, // [{ id, label, disabled }] — used by SideStepper footer, // FooterProps — next/back/draft buttons activeStep, setActiveStep, onSave, // dispatches to the correct section's onSave based on activeStep jobId, hiringReasons, backendHiringPipeline, isLoadingBackendHiringPipeline, hasJobBoards, isJobPublicDescriptionSaved, jobName, isLoadingSave, } = useJobForm(); ``` ### Filter hooks #### `useOffersFilters()` URL search-param-backed filter state. All filter values live in the URL — no useState. ```ts const { search, departments, brands, activeFiltersCount, setSearch, setFilters, clearFilters } = useOffersFilters(); ``` - Lists are encoded as `|`-separated strings in the URL (`FILTERS_LIST_SEPARATOR`) - All setters use `replace: true` to avoid polluting browser history #### `useOffersFiltersDrawer()` Opens `OffersFiltersDrawerContent` in the DrawerLayer with the current filter state. --- ## Pages ### Offers list (`index.tsx`) - Uses `useInfiniteJobs` for paginated, infinite-scroll data - Search and filter state managed by `useOffersFilters` - Filter drawer opened via `useOffersFiltersDrawer` - Sortable by name, date, applicant count - Navigates to `recruitingRoutes.jobOffer(id)` (OfferDetail) on row click ### JobOfferForm — CREATE wizard Four steps rendered by `renderActiveStep` in `index.tsx`. The outer `` holds `generalConfigForm` so `ReviewStep` can read general config values via `useFormContext`. Each step component brings its own `FormProvider`. ``` Step 0: GeneralConfigStep → useGeneralConfigSection form Step 1: ProcessStages → useProcessStagesSection form Step 2: PublishStep → usePublishForm form + ApplicationFormTab (field visibility) Step 3: ReviewStep → reads GeneralConfigForm from outer context + receives publishForm as prop ``` After the Review step the user can: - **Open without publish** — calls `openJobOffer`, navigates to `jobDetail` - **Publish** — calls `openJobOffer` then `createJobPosting`, navigates to `jobDetail` ### JobDetail (`JobDetail/index.tsx`) Read-only summary view of an existing job offer. Renders each section (General Config, Process Stages, Publish) as a `DetailSectionSummary` card. Navigates to `JobOfferEdit` for editing. Shows a publish empty state if no public description exists and job boards are available. ### JobOfferEdit (`JobOfferEdit/index.tsx`) Edit page for an existing job offer's General Config. Uses `useGeneralConfigSection` to manage form state and the save mutation. On success navigates back to `JobDetail`. ### OfferDetail (`OfferDetail/index.tsx`) The candidates view for a job. Displays: - Job offer header (name, status pill, context-sensitive actions) - Pipeline stage filters - Status filters (Active / Hired / Rejected) - `JobOfferCandidatesTable` with inline candidate actions #### Header actions per status | Status | `mainActions` | `extraActions` | |---|---|---| | All | Job Details | — | | **Draft** | Job Details | Open · Publish (if no posting & job boards) · Delete | | **Open** (no posting) | Job Details | Publish (if job boards) · Close | | **Open** (published) | Job Details | View Posting · Share · Pause · Close | | **Closed** | Job Details | Open (if no hired) · Publish (if job boards) · Delete | Both `mainActions` and `extraActions` return `[]` while any data is loading. ### JobApplicationDetail (`JobApplicationDetail/index.tsx`) Full-screen applicant screening view. Two-column layout: - **Left** — ordered list of pipeline stages, each showing dates, files, and notes - **Right** — tabs: Profile (candidate application answers), Overview (contact info + quick actions), Activity, Files Key actions available: Reject, Move Forward, Hire, Pause. The **Profile tab** renders `ProfileTab`, which reads sections from `jobApplicationFormAnswers`. `application.resume` overrides the form-answer value for the `resume` field so edits via the update drawer are always reflected without a re-fetch. ### JobOfferPublish (`JobOfferPublish/index.tsx`) Manage job board postings for an offer. Allows creating, editing, and deleting postings on connected career sites. The publish button is only gated on `isValid` when creating a new posting — `isDirty` is additionally required when editing an existing one. Shows a confirmation modal (`usePublicDescriptionWarningModal`) when updating a live posting. --- ## Data flow ``` services.ts (Axios calls) ↓ hooks/ (useQuery / useMutation via React Query) ↓ Section hooks (form + mutation composed together) ↓ Step components (FormProvider + form fields) ↓ Page components (orchestrate sections + navigation) ``` ## API base URL All requests go through `/ats/admin`. The endpoint structure follows: ``` /jobs → list / create /jobs/:id → get / edit / delete /jobs/:id/state → update status (open, pause, close) /jobs/:id/hiring-pipeline → get / create / update pipeline /jobs/:id/hiring-pipeline/stages/:stageId → delete stage /jobs/:id/applications → list candidates /jobs/:id/applications/:appId → get applicant detail /jobs/:id/applications/:appId/update → update applicant info /jobs/:id/public-description → get / create / edit public description /jobs/:id/postings → list / create / delete job board posting /jobs/:id/application-form → get application form structure /jobs/:id/application-form/fields/visibility → update field visibility (bulk) /application-sources → list application sources /filter-options → list department + brand filter values /rejection-reasons → list rejection reasons ```