---
id: SVP-CRM-ENT-004
title: Edit Contact v0 prompt (the Organization Affiliations section, revision 3)
screen: SCR-CRM-ENT-004
product_area: crm
module: entities
revision: 1
status: draft
source_sha:
  rhythm-v2-docs: 23d55505b92455c377730e25d88b6e78197b1a39
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
use: ticket-change
change: The recorded react-console affiliations revision 3 work on agent/aff-v0, 2026-10-13 to 2026-10-14; no tracker ticket is recorded in the inspected evidence
recorded_after_the_fact: true
prompt_status: integrated
design_basis:
  screen: SCR-CRM-ENT-004
  revision: 1
  corpus_commit: 23d55505b92455c377730e25d88b6e78197b1a39
  note: This is a retrospective link to the 2026-10-23 Screen Design. The prompt record concerns the 2026-10-13 revision-3 brief and Andrew's rejection of revision 1; the inspected prompt does not derive from a corpus design.
components: []
design_system:
  name: shadcn on Base UI with Tailwind v4 and lucide icons (the console's component library, base-nova style)
  version: react-console at the agent/aff-v0 branch tip of 2026-10-13 (package versions pinned there)
standards:
  - react-console:AGENTS.md (copy rules, dialog header bands, buttons)
  - react-console:docs/editors.md (editor headings, field labels, the pinned action bar)
  - react-console:docs/page-layouts.md
  - react-console:scripts/v0/README.md (how the drivers run v0, cost rules)
generation:
  tool: v0 through scripts/v0/affiliations-v2.mjs, using the SDK client with a repo chat on agent/aff-v0
  model: not recorded (the driver supplies no model selection; an account plan does not identify the run model)
  parameters:
    chat_type: repo
    repo_branch: agent/aff-v0
    group: a
    poll_timeout_minutes: 45
    quiet_seconds: 180
  project: none recorded for this run; the one-project rule is dated 2026-10-17
  reference: Recorded chat p8Pdclr2GT9 with b_x4gTd9ktnkh for the row and section, and b_SwN70SVQA2W for the drawer
  executed_at: '2026-10-13'
  executed_by: The record attributes execution to the affiliations revision 3 fleet job using scripts/v0/affiliations-v2.mjs --group a; no individual operator is identified in the inspected notes
  output: The notes attribute components/affiliations/affiliation-row.tsx and organization-affiliations-section.tsx to b_x4gTd9ktnkh and affiliation-editor.tsx to b_SwN70SVQA2W; the driver copies changed output to .v0tmp/affiliations-v2-a/ for integration on agent/aff-v0
  status: run
  reason: ''
iterations:
  - n: 1
    at: '2026-10-13'
    change: The first of 2 prompt steps requests the row and combined affiliation section.
    result: The notes assign affiliation-row.tsx and organization-affiliations-section.tsx to version b_x4gTd9ktnkh.
  - n: 2
    at: '2026-10-13'
    change: The second of 2 steps requests the affiliation drawer in the same chat.
    result: The notes assign affiliation-editor.tsx to version b_SwN70SVQA2W.
  - n: 3
    at: '2026-10-14'
    change: Andrew's recorded integration changes use direct role checkboxes and place the section on an Affiliations tab after Overview.
    result: The screenshot notes document those historical choices; the pinned drawer subsequently uses dated role rows with selectors and Add Role, while retaining the separate Affiliations tab.
verification:
  status: evaluated
  at: '2026-10-14'
  against: The recorded revision-3 brief and Andrew's 2026-10-14 review; evaluation against a contemporaneous corpus Screen Design is not established
  discrepancies:
    - what: The integration notes say generated library files and the package manifest were excluded from the copied output.
      resolution: corrected
    - what: Integration removes the generated size prop because the console's ToggleGroup does not support it.
      resolution: corrected
    - what: The prompt requests role chips; Andrew's recorded change lists contact roles directly as checkboxes.
      resolution: approved-change
    - what: The prompt places the section within the existing contact experience; the notes record Andrew's move to an Affiliations tab after Overview.
      resolution: approved-change
staleness:
  status: current
controls: [TC-DAT-001, TC-PRV-001]
evidence:
  - 'react-console at baf6dae3: scripts/v0/affiliations-v2-a1-row-and-section.prompt.md and affiliations-v2-a2-editor.prompt.md (the prompt text, verbatim below); scripts/v0/affiliations-v2.mjs (the driver and its parameters); docs/affiliations/screens-v2/README.md (the chats, versions, integration changes and screenshots); git history of those files (committed 2026-10-13 22:36 UTC, screenshots regenerated 2026-10-14)'
---

# Edit Contact v0 prompt (the Organization Affiliations section, revision 3)

## Purpose

This record explains the generation work used to present a contact's organization relationships and edit one relationship in a drawer. It concerns the revision-3 affiliations change to an existing contact screen after Andrew rejected the earlier design. The two prompts and retained notes establish this generation's scope; they do not establish a regeneration of the complete Edit Contact page. The artifact records existing evidence rather than submitting a new run.

## What the Prompt Was Generated From

The prompt describes Andrew's revision-3 direction: one affiliation section with explicit current and historical relationships and an optional primary relationship. It specifies presentational contracts for the row, section and drawer, cites the console copy rules, and limits the files v0 may inspect. It asks for no data fetching, wiring, dependencies or changes to other screens.

Those contracts appear in the retained prompt files rather than contemporaneous Component Designs in this corpus. The linked Screen Design is retrospective. The prompt and notes support Andrew's rejection of the earlier structure, but this review does not identify the individual who composed each prompt sentence. The missing affiliation component registrations remain a finding.

## The Prompt

```text
Redesign, revision 3: **Organization Affiliations** on a contact, inside the existing Rhythm React console (Next.js App Router, React 19, Tailwind v4, shadcn on Base UI — `@base-ui/react`, NOT Radix — lucide icons). This branch holds a first design Andrew REJECTED: an identity-band Organization field, an Organization card with "use organization address/phone" toggles, an "Additional Affiliations" section and an "Organization History" tab. Do not reuse that structure under new labels (do not read or copy `components/affiliations/affiliation-list.tsx`, `affiliation-row-editor.tsx`, `add-another-organization.tsx` or `primary-organization-change.tsx`). He wants ONE straightforward feature: every relationship in one place, past ones visibly historical, one optionally primary.

Rules: presentational components only, no data fetching, no wiring, no new dependencies, do not edit `components/ui/*`, do not touch any other screen. Follow `AGENTS.md` copy rules (Title Case headings and buttons, sentence-case helper text, `cursor-pointer` on custom clickables). Read at most `components/ui/badge.tsx` and `components/ui/dropdown-menu.tsx`, then write immediately: this turn must end with the two files below written.

## Write `components/affiliations/affiliation-row.tsx`

```ts
export type AffiliationStatus = "current" | "past" | "future"

/** One affiliation, already resolved to display strings by the caller. */
export interface AffiliationRowView {
  id: string
  organizationName: string
  /** Link target for the organization name, when it has a page. */
  organizationHref?: string
  /** "Employer", or "Board Member (inactive)" for a deactivated type. */
  typeName: string
  jobTitle: string | null
  /** Role names; `dates` only when the role has its own dates, e.g. "2016 – 2019". */
  roles: { id: string; name: string; dates?: string }[]
  /** Display strings, already formatted: "Mar 1, 2014" or null. */
  startDate: string | null
  /** Inclusive; null while it continues. */
  endDate: string | null
  status: AffiliationStatus
  isPrimary: boolean
  /** The contact-information rows linked through this affiliation, already worded: ["Address: Headquarters → Business", "Phone: Main Line → Direct"]. Empty = "Address: Do not link · Phone: Do not link" is passed explicitly by the caller as strings too. */
  links: string[]
}

export interface AffiliationRowProps {
  row: AffiliationRowView
  /** Opens the affiliation editor. Without it the row is read-only. */
  onEdit?: () => void
  /** Offered for a current, non-primary, open-ended affiliation. */
  onMakePrimary?: () => void
  onRemove?: () => void
  /** A validation problem on this affiliation, shown on the row. */
  issue?: string
  /** "compact" for dense lists (an organization's people). */
  density?: "default" | "compact"
  /** Replaces the organization name line, e.g. the person's name on an organization's people list. */
  title?: React.ReactNode
  className?: string
}

export function AffiliationRow(props: AffiliationRowProps): JSX.Element
/** The explicit status label: "Current", "Past" with "Ended Jun 30, 2019", "Starts Nov 1, 2026". Never colour alone: icon + word. */
export function AffiliationStatusLabel(props: { status: AffiliationStatus; startDate: string | null; endDate: string | null }): JSX.Element
export function AffiliationPrimaryBadge(): JSX.Element
export function AffiliationRoleList(props: { roles: AffiliationRowView["roles"] }): JSX.Element
/** "Mar 1, 2014 – Jun 30, 2019", "Since Mar 1, 2019", "No dates set". */
export function AffiliationDateRange(props: { startDate: string | null; endDate: string | null }): JSX.Element
/** The compact link summary line: "Address: Headquarters → Business · Phone: Do not link", link icon, muted small text. */
export function AffiliationLinkSummary(props: { links: string[] }): JSX.Element
```

One row (a bordered rounded row with a building icon, same visual language as the console's other record lists): organization name (semibold, a link when `organizationHref`), a **Primary** badge when primary (star icon + the word "Primary"), the affiliation type as a quiet outline badge, the job title, the date range, the explicit status label, the role chips, and the link summary line. A past row gets a historical treatment that stays fully readable: a muted background and a dashed border, the "Past" label with a history icon and "Ended <date>"; never colour alone, never strikethrough, never collapsed or truncated text. Actions on the right: an Edit icon button (pencil, tooltip, accessible name "Edit <organization> affiliation") and an overflow menu (accessible name "More actions for <organization>") holding Make Primary (only when `onMakePrimary`) and Remove (only when `onRemove`, destructive tone). The whole row is not clickable. `data-testid="affiliation-row"`, `data-status`, `data-primary`.

## Write `components/affiliations/organization-affiliations-section.tsx`

```ts
import type { AffiliationRowView } from "./affiliation-row"

export interface OrganizationAffiliationsSectionProps {
  /** Every affiliation, current and past together, ALREADY ORDERED: primary first, then current, then upcoming, then past. */
  rows: AffiliationRowView[]
  readOnly?: boolean
  onAdd?: () => void
  onEdit?: (id: string) => void
  onMakePrimary?: (id: string) => void
  onRemove?: (id: string) => void
  /** Issues keyed by affiliation id. */
  issues?: Record<string, string>
  /** A contact with current affiliations but none primary: shown as a quiet inline notice. */
  noPrimaryNotice?: string
  /** Where a read-only Contact 360 sends "Manage" (the contact editor), when the viewer may edit. */
  manageHref?: string
}
export function OrganizationAffiliationsSection(props: OrganizationAffiliationsSectionProps): JSX.Element
```

The INSIDE of a section card (the caller supplies the card and its "Organization Affiliations" title): a slim toolbar with counts ("2 Current · 1 Past"), an optional filter (All / Current / Past, a small segmented control from `components/ui/toggle-group.tsx`) whose default is **All** so nothing is hidden by default, and an "Add Affiliation" button (not when `readOnly`; when read-only and `manageHref`, a "Manage" link instead); the `noPrimaryNotice` as a quiet inline notice; then the rows in the given order — past rows after a thin "Past" divider label, in the same list and fully visible. Empty state: "No organization affiliations yet." with the Add button. `data-testid="organization-affiliations"`. Rows get `onMakePrimary` only when the row is current or upcoming, not primary, and has no end date.

---- Step 2 (sent in the same chat once step 1 settled) ----

Step 2 of 2. Presentational only, as in step 1. Read at most `components/ui/sheet.tsx`, `components/ui/select.tsx`, `components/ui/checkbox.tsx` and `components/affiliations/affiliation-row.tsx` (yours, from step 1), then write immediately: this turn must end with the file below written.

## Write `components/affiliations/affiliation-editor.tsx`

ONE affiliation editor, a right-hand drawer (`Sheet`) used both to add and to edit an affiliation. It edits a draft; the contact's own action bar saves the contact later, so the footer is "Cancel" and "Add Affiliation" (add) or "Apply" (edit), with helper text "Changes are saved with the contact." The header is a tinted band (AGENTS.md: dialog headers use a tinted header band) titled "Add Affiliation" / "Edit Affiliation" (never the organization's name in the title) with a one-line description.

There are NO address or phone link controls in this editor. Contact information is linked by reference on the contact's own Addresses and Phone Numbers sections. The editor only (a) says what the affiliation SUPPLIES, and (b) for a NEW affiliation previews the rows its type's defaults will link, each deselectable.

```ts
export interface AffiliationDraft {
  organizationId: string
  organizationName: string
  affiliationTypeId: string
  isPrimary: boolean
  jobTitle: string
  startDate: string // YYYY-MM-DD or ""
  endDate: string // YYYY-MM-DD or "", inclusive
  notes: string
  /** Contact-role ids; the caller keeps each role's own dates. */
  roleIds: string[]
}

export interface TypeOption { id: string; name: string }

/** One contact-information row the new affiliation's type would link. */
export interface DefaultLinkPreview {
  key: string
  kind: "address" | "phone"
  /** "Link Business Address to Northwind Logistics's Headquarters" */
  label: string
  /** e.g. "100 Harbor Way, Boston, MA 02110", or why it can't: "Northwind Logistics has no Billing Office address". */
  detail?: string
  /** "replaces" when the contact row exists but is empty; "skipped" when the row already has a value (never overwritten) — shown, unchecked and disabled, with the reason. */
  state: "new" | "replaces" | "skipped" | "missing"
  selected: boolean
}

/** A message under one control; "error" blocks applying, "warning" does not. */
export interface EditorMessage { tone: "error" | "warning"; text: string }

export interface AffiliationEditorProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  mode: "add" | "edit"
  draft: AffiliationDraft
  onDraftChange: (next: AffiliationDraft) => void
  onApply: () => void
  /** The console's organization picker. */
  renderOrganizationPicker: (props: { id: string; value: string | null; onChange: (id: string | null, name: string | null) => void; invalid?: boolean }) => React.ReactNode
  /** Active types, plus the held inactive one already labelled "Name (inactive)". */
  affiliationTypes: TypeOption[]
  contactRoles: TypeOption[]
  /** Read-only: what this affiliation supplies to the contact today, e.g. ["Business Address", "Direct Phone"]. Empty = "Supplies no contact information." */
  supplies: string[]
  /** Add mode only: the rows the chosen type's defaults would link. Empty with a type chosen = "Employer links no contact information by default." */
  defaultLinks?: DefaultLinkPreview[]
  onToggleDefaultLink?: (key: string, selected: boolean) => void
  /** Messages keyed by control. */
  messages: Partial<Record<"organizationId" | "affiliationTypeId" | "startDate" | "endDate" | "isPrimary", EditorMessage>>
  /** Set when the affiliation has an end date: Primary needs an open-ended affiliation. */
  primaryDisabledReason?: string
  /** E.g. "Ending Northwind Logistics leaves Amara Okafor with no primary affiliation until you make another one primary." */
  primaryNotice?: string
  /** When an end date is set on an affiliation that supplies linked rows: ["Business Address", "Direct Phone"] — shown as a warning "These rows will stop resolving: …" listing them. */
  stopsResolving?: string[]
}
export function AffiliationEditor(props: AffiliationEditorProps): JSX.Element
```

Body, in this order, in clear groups with small section headings:
1. **Organization** (the render prop) and **Affiliation Type** (select).
2. **Job Title**, **Start Date**, **End Date** (native date inputs are fine; helper text under End Date: "Leave empty while the affiliation continues. The end date is the last day."). Under End Date, the `stopsResolving` warning when present (amber, icon, text always present).
3. **Roles**: a multi-select of `contactRoles` shown as removable chips (a popover with checkable options is fine).
4. **Primary Affiliation**: a switch with helper text "The contact's main organization. Only one affiliation can be primary." Disabled with `primaryDisabledReason` shown when set. Show `primaryNotice` as an inline notice here.
5. **Contact Information**: a read-only line "Supplies Business Address, Direct Phone" (link icon) or "Supplies no contact information."; in add mode, under it, a sub-heading "Link When Added" and one checkbox row per `defaultLinks` entry: the label in normal text, the `detail` muted under it, a small state tag ("Replaces empty row", "Already has a value — not changed", "No source value"); helper text "From the affiliation type's defaults. Linked rows follow the organization and can be changed later on the contact's Addresses and Phone Numbers."
6. **Notes** (textarea).

Every control has a visible label and an id (`affiliation-editor-<field>`) so validation can focus it; the Apply / Add button is disabled while any message has tone "error". `data-testid="affiliation-editor"`; each default-link checkbox has `data-testid="default-link-<key>"`.
```

## Required Behavior and Mock Behavior

The request requires presentational components that accept the stated props, display readable historical relationships, identify status through words and icons, and expose accessible action names. The drawer applies changes to a contact draft; its footer does not save the contact independently. These are the recorded generation requirements, not evidence that a complete accessibility audit passes.

The prompts supply display contracts and examples rather than backend integration. The generated components rely on caller-provided data. The integration notes describe mock fixtures and subsequent form, source-resolution and write wiring outside v0. Mock behavior belongs to that surrounding integration; saying the generation contains no mock data would obscure the illustrative values in the prompt.

## Generation Record

The notes identify chat p8Pdclr2GT9 and its row/section and drawer versions. The driver sends sorted group-a prompt files in sequence, polls for a settled version and copies changed output into its temporary directory. The notes attribute the integrated components to those versions and say rewritten library files and the manifest were excluded.

The retained record dates the work to 2026-10-13 and the reviewed integration to 2026-10-14. Available local git history exposes later retained commits, so it does not independently prove the earlier run timestamp or a precise operator. No run model or project identifier is recorded. The newer one-project policy does not establish which project held this historical chat.

## Evaluation Against the Design

The notes document removal of an unsupported ToggleGroup size prop and exclusion of v0's library and manifest changes. They also attribute direct role checkboxes and the separate Affiliations tab to Andrew on 2026-10-14. The pinned form retains the separate tab, but its drawer now uses dated role rows with selectors, Add Role and Add Period rather than direct role checkboxes. The notes establish the historical integration choices, not unchanged generation fidelity at the current commit or approval of the complete screen artifact.

The corpus Screen Design is retrospective, so this review does not claim a field-by-field evaluation against an approved design at generation time. The prompt record retains its evaluated status and discrepancy resolutions; open differences elsewhere in the contact screen remain in the Screen Implementation and findings.

## Open Questions

The generated affiliation row, section and drawer still need registered component contracts for future screen and prompt reuse. This record establishes their retained generation references but leaves the model, historical project and individual operator unrecorded. The retrofit can resolve component ownership and registration without inventing those missing execution details or treating a retrospective design as prior approval.
