# 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
```