feat: link a Job/Contract's employer to a network Organization #375

Closed
opened 2026-06-29 15:59:30 +00:00 by james · 0 comments
Owner

Tie a company you've worked at (a Job's/Contract's employer, currently free-text jobs.company) to a tracked network Organization, mirroring the people↔organizations "link-or-stub" pattern. Discussed + agreed design below.

Model

  • Add a nullable organization_id FK on the jobs table (ON DELETE SET NULL). Jobs are 1:1 with an employer, so it's a column, not a junction.
  • Unlinked (default): company text holds the name — every existing job keeps working untouched (it's the stub).
  • Linked: organization_id points at an Organization; the org's name is the live source for display (rename the org → the job reflects it). company is kept as a fallback snapshot.
  • Applies to Contracts too automatically (same jobs table, is_contract flag).

Decisions (settled in discussion)

  1. Name authority when linked = live from the org (matches person_organizations, which uses stub_name only when unlinked).
  2. Org delete = copy-down. In OrganizationsRepository.deleteById, before removing the org, UPDATE jobs SET company = <org.name>, organization_id = NULL WHERE organization_id = <id> so linked jobs gracefully degrade to a stub with the last-known name (no data loss). The FK ON DELETE SET NULL is the belt-and-suspenders safety net.

Build (API-first, then client)

Backend (PR 1):

  • Migration 026: jobs.organization_id nullable FK → organizations(id) ON DELETE SET NULL.
  • JobsRepository: accept organizationId on create/update; resolve the live org name (LEFT JOIN organizations) so the DTO carries organizationId + organizationName; add listByOrganizationId(userId, orgId) for the reverse view.
  • OrganizationsRepository.deleteById: the copy-down before delete.
  • DTO: zJobCreate/zJobUpdate gain optional organizationId (nullable; per-user 404 if it points at someone else's org or a missing one); zJobDto gains organizationId + organizationName.
  • Reverse view endpoint: list the jobs/roles linked to an Organization (for the org detail's "your roles here").
  • openapi + api-client regen; dual-engine tests (link, unlink, live-name resolution, org-delete copy-down, reverse list, cross-user link rejected); update idea.md (Jobs: "company, optionally linked to a network Organization").

Client (PR 2, follow-up):

  • Job create/edit: a company field that can pick an existing Organization or stay free text (reuse the person→org picker pattern); tapping the company on a Job → the Org detail.
  • Org detail: a "your roles here" section listing linked jobs.

Out of scope (follow-up)

  • An agent tool to link/unlink (link_job_to_org, mirroring link_person_to_org).

Updates idea.md. Per-user isolation throughout; cross-user org links return 404.

Tie a company you've worked at (a Job's/Contract's employer, currently free-text `jobs.company`) to a tracked network **Organization**, mirroring the people↔organizations "link-or-stub" pattern. Discussed + agreed design below. ## Model - Add a **nullable `organization_id` FK on the `jobs` table** (`ON DELETE SET NULL`). Jobs are 1:1 with an employer, so it's a column, not a junction. - **Unlinked (default):** `company` text holds the name — every existing job keeps working untouched (it's the stub). - **Linked:** `organization_id` points at an Organization; the **org's name is the live source** for display (rename the org → the job reflects it). `company` is kept as a fallback snapshot. - Applies to **Contracts too** automatically (same `jobs` table, `is_contract` flag). ## Decisions (settled in discussion) 1. **Name authority when linked = live from the org** (matches `person_organizations`, which uses `stub_name` only when unlinked). 2. **Org delete = copy-down.** In `OrganizationsRepository.deleteById`, before removing the org, `UPDATE jobs SET company = <org.name>, organization_id = NULL WHERE organization_id = <id>` so linked jobs gracefully degrade to a stub with the last-known name (no data loss). The FK `ON DELETE SET NULL` is the belt-and-suspenders safety net. ## Build (API-first, then client) **Backend (PR 1):** - Migration 026: `jobs.organization_id` nullable FK → `organizations(id)` `ON DELETE SET NULL`. - `JobsRepository`: accept `organizationId` on create/update; resolve the live org name (LEFT JOIN organizations) so the DTO carries `organizationId` + `organizationName`; add `listByOrganizationId(userId, orgId)` for the reverse view. - `OrganizationsRepository.deleteById`: the copy-down before delete. - DTO: `zJobCreate`/`zJobUpdate` gain optional `organizationId` (nullable; per-user 404 if it points at someone else's org or a missing one); `zJobDto` gains `organizationId` + `organizationName`. - Reverse view endpoint: list the jobs/roles linked to an Organization (for the org detail's "your roles here"). - openapi + api-client regen; dual-engine tests (link, unlink, live-name resolution, org-delete copy-down, reverse list, cross-user link rejected); update `idea.md` (Jobs: "company, optionally linked to a network Organization"). **Client (PR 2, follow-up):** - Job create/edit: a company field that can pick an existing Organization or stay free text (reuse the person→org picker pattern); tapping the company on a Job → the Org detail. - Org detail: a "your roles here" section listing linked jobs. ## Out of scope (follow-up) - An agent tool to link/unlink (`link_job_to_org`, mirroring `link_person_to_org`). Updates `idea.md`. Per-user isolation throughout; cross-user org links return 404.
james closed this issue 2026-06-29 17:00:09 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#375
No description provided.