---
narrative:
  status: enriched
id: SCI-CRM-ENT-004
title: Edit Contact
design: SCR-CRM-ENT-004
prompt: SVP-CRM-ENT-004
product_area: crm
module: entities
revision: 1
status: draft
implementation_evidence: observed
source_sha:
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
route:
  pattern: '/console/{tenantId}/crm/contacts/{contactId}/edit'
  parameters:
    - 'contactId: identifies the contact to load for editing'
  registration: >-
    The page app/(console)/console/[tenantId]/crm/contacts/[id]/edit/page.tsx
    hosts the editor; lib/routes/definitions.ts registers
    crm.contacts.record.edit
  parent_layout: >-
    The workspace shell supplies navigation around the editor; the page content
    has responsive editor padding. This review does not separately verify the
    shell layout file
  navigation: >-
    Reached through contact-edit links or a direct address. The registry
    supplies the Edit segment; a live person-name breadcrumb is not established
    by the inspected files
  guards:
    - >-
      The page uses RequirePermission with permissions.crm.contacts.edit,
      aliased to crm.contacts.update
    - >-
      Workspace and application gates are recorded as shell dependencies; their
      implementation is outside the page files inspected here
  deep_links:
    - A direct address matching the registered pattern opens the editor
    - >-
      lib/routes/legacy.ts redirects /contacts/{id}/edit to the registered
      editor address
source:
  repository: react-console
  files:
    - 'app/(console)/console/[tenantId]/crm/contacts/[id]/edit/page.tsx'
    - components/contact-editor/contact-editor.tsx
    - components/contact-editor/editor-header.tsx
    - components/contact-editor/unsaved-changes.tsx
    - components/contact-editor/change-summary.ts
    - components/contact-editor/save-failure.ts
    - components/contact-editor/logic.ts
    - components/contact-editor/contact-mapping.ts
    - components/contact-editor/collections/*.tsx
    - components/affiliations/affiliation-editor.tsx
    - components/affiliations/organization-affiliations-section.tsx
    - components/affiliations/affiliation-row.tsx
    - components/forms/form-renderer.tsx
    - components/forms/contacts/collection-components.tsx
    - components/forms/contacts/affiliation-components.tsx
    - components/editor/editor-action-bar.tsx
    - lib/contacts/record-operations.ts
    - lib/contacts/live-operations.ts
    - lib/contacts/crm-live.ts
    - lib/forms/contacts/form-data.ts
    - lib/forms/contacts/validators.ts
    - lib/drafts/use-editor-draft.ts
    - lib/affiliations/contact-write.ts
  entry_component: ContactEditor (components/contact-editor/contact-editor.tsx)
  modules:
    - components/forms renders and binds the compiled form
    - lib/contacts supplies record operations and CRM request mapping
    - lib/authz guards entry through the evaluated edit permission
    - lib/drafts holds and restores unsaved editor values
    - lib/routes supplies registered navigation addresses
  pull_requests: []
  commits:
    - >-
      react-console baf6dae382d6f41dce976c7f848667ccc37e076f (the described
      state)
    - 'react-console a3fd60ef12 (affiliations revision 3 integrated, 2026-10-14)'
components:
  - CMI-CRM-ENT-001
  - CMI-CRM-ENT-003
  - CMI-CRM-ENT-004
  - CMI-CRM-ENT-005
  - CMI-CRM-ENT-006
  - CMI-CRM-ENT-007
  - CMI-CRM-ENT-008
data:
  reads:
    - getContact supplies the contact and version through record operations
    - >-
      Platform supplies the contact object description; edit uses the built-in
      contact-admin definition rather than resolving a configured contact-type
      form
    - >-
      Picker and collection integrations obtain workspace record types, roles,
      affiliation and contact-point types, regions and selectable contacts
  writes:
    - >-
      patchContact submits changed bound values with the original version;
      eligible uncertain or stale retry results cause read-back reconciliation
  binding: >-
    Record mapping supplies the initial draft and form values. The built-in edit
    definition and object description determine bound write keys; contact
    collection and affiliation adapters connect their editors to those values.
  server_state: >-
    Opening and Reload read the contact. A successful update returns saved data,
    discards the held draft and navigates to Contact 360 with a named success
    toast; it does not reload this editor after saving.
  local_state: >-
    The editor keeps draft form values, validation issues, review and conflict
    state, and a dirty count computed against its normalized baseline.
    Collection components keep their expanded-row state.
  errors: >-
    A missing read offers Back to Contacts; another failed contact read offers
    Try Again. Failed writes use save-error toasts and any field issues, while a
    version conflict appears in the action bar with recovery choices.
  loading: >-
    A loading panel waits for the contact, metadata, effective form or workspace
    contact types. This is not a guarantee that every independent picker
    finishes before the form renders.
  cache_invalidation: >-
    Successful save navigates to the contact route. A merged-contact load can
    redirect to the surviving contact. The inspected editor declares no separate
    query-cache invalidation.
  persistence: >-
    The draft hooks hold dirty values for sign-in recovery and restore a parked
    draft with its original base version. They discard that draft on save,
    Cancel, Reload or ordinary signed-in departure; this review does not
    establish a universal absence of browser storage.
actions:
  - id: AX-SCR-CRM-ENT-004-01
    operation_id: patchContact
    as_built: >-
      Review Changes leads to a patch of the changed bound fields with the
      original version. Record operations reconcile eligible uncertain answers
      by reading back the values; success releases the guard, discards the held
      draft, reports the update and opens Contact 360.
  - id: AX-SCR-CRM-ENT-004-02
    operation_id: getContact
    as_built: >-
      Reload remounts the editor after discarding its held draft, reads the
      contact again and reports the discarded changes.
  - id: AX-SCR-CRM-ENT-004-03
    operation_id: patchContact
    as_built: >-
      Affiliation and role-period interactions change the form draft through the
      affiliation adapter. The contact submission writes the resulting
      relationships together.
  - id: AX-SCR-CRM-ENT-004-04
    operation_id: patchContact
    as_built: >-
      Choosing a new primary over a current relationship opens the leaving
      question; the answer determines whether the earlier affiliation ends today
      or remains current.
  - id: AX-SCR-CRM-ENT-004-05
    operation_id: none
    as_built: >-
      Discard Changes releases the dirty-draft guard and invokes the pending
      navigation.
  - id: AX-SCR-CRM-ENT-004-06
    operation_id: none
    as_built: >-
      Cancel invokes guarded return to Contact 360 and discards the held draft
      when departure continues.
  - id: AX-SCR-CRM-ENT-004-07
    operation_id: none
    as_built: >-
      The issue trigger opens the action-bar popover; selecting an issue asks
      the form renderer to reveal and focus it.
  - id: AX-SCR-CRM-ENT-004-08
    operation_id: none
    as_built: Keep Editing retains the draft and clears the conflict alert.
  - id: AX-SCR-CRM-ENT-004-09
    operation_id: none
    as_built: >-
      The save attempt opens a comparison dialog after validation succeeds. Its
      Keep Editing action closes review and returns to the draft.
  - id: AX-SCR-CRM-ENT-004-10
    operation_id: none
    as_built: >-
      Collection controls change draft contact points and source choices. The
      current address row source path allows direct removal of a linked row.
  - id: AX-SCR-CRM-ENT-004-11
    operation_id: none
    as_built: >-
      The preference adapter changes enabled states and subscription choices for
      existing preference rows. Email opt-out disables its preference toggles;
      the inspected adapter does not add or remove preference rows.
  - id: AX-SCR-CRM-ENT-004-12
    operation_id: none
    as_built: >-
      Photo selection changes the form asset value. Live write mapping omits
      unsupported photo upload and returns a warning while submitting the
      remaining changes.
authorization:
  frontend_guards:
    - >-
      RequirePermission(permissions.crm.contacts.edit) gates the page through
      the mapped service permission crm.contacts.update
    - >-
      The page checks the entry grant; per-field or per-action permission
      filtering throughout the editor is not established by this source review
  backend:
    - >-
      patchContact declares crm.contacts.update and passes the expected version
      to the update boundary; stale writes return a conflict
    - getContact declares the crm.contacts.read authorization requirement
    - >-
      Record restrictions remain authoritative at the service write boundary;
      browser controls cannot grant an exception
observability:
  logs: >-
    The inspected editor has no dedicated logging call; logging beyond these
    files is not established.
  metrics: No editor-specific metric emission appears in the inspected source.
  events: >-
    The editor makes record-operation calls; the service patch route nudges its
    change-event dispatcher after update.
  errors: >-
    Toasts, field issues and the conflict alert report failures to the person.
    No direct error-service call appears in this editor.
fidelity:
  - difference: >-
      The design requires Save to be unavailable with issues, linked removal to
      wait for unlinking or copying, and confirmation before every dirty
      departure. The built editor validates an enabled Save attempt, permits
      current source-controlled address removal and has no browser Back
      confirmation handler. It uses an action-bar conflict alert rather than the
      listed conflict dialog and the built-in edit definition rather than the
      claimed configured contact-type form.
    kind: open
    reference: FND-CRM-ENT-088
verification:
  component_tests: []
  screen_acceptance:
    - SAT-CRM-ENT-004
    - TST-CRM-ENT-774
    - TST-CRM-ENT-785
    - TST-CRM-ENT-784
  integration:
    - TST-CRM-ENT-786
    - TST-CRM-ENT-787
    - TST-CRM-ENT-790
    - TST-CRM-ENT-794
  feature_acceptance:
    - FAS-CRM-ENT-01
  accessibility: >-
    not-verified (source supplies semantic controls and responsive styling; no
    complete interaction audit is recorded)
  deployment: >-
    No deployment of the pinned commit is verified here; the evidence names
    development checks but supplies no deployment reference for this source
    state
screenshots:
  - state: affiliations tab
    viewport: 1440 × 1300 pixels (image dimensions; the browser viewport is not independently established)
    path: SCI-CRM-ENT-004/affiliations-desktop.png
    sha256: 2a541a4bf163d2adbfb4608a74b39d82f8ff080283b2b6e417c8773f6e9bfaca
    captured_at: '2026-10-17T20:58:13-04:00'
    captured_from: >-
      react-console:docs/affiliations/screens-v2/contact-editor-overview.png at
      baf6dae382d6f41dce976c7f848667ccc37e076f; documented fixture capture.
      The date is the source file commit date, not a verified shutter time.
evidence:
  - >-
    react-console at baf6dae3: the files above; docs/editors.md;
    docs/affiliations/screens-v2/README.md
  - >-
    services-crm at a8d1364f: modules/entities/aws/api/contacts.routes.ts
    (getContact, patchContact)
---

# Edit Contact

## What Was Built

Edit Contact lets staff correct an existing person through the contact editor on a dedicated console route. It reads the contact, compiles the built-in edit form against field metadata, keeps draft values, reviews changes and submits the changed bound fields against the original version. The affiliations components come from the recorded v0 revision-3 work. The inspected evidence does not establish that every other part of the editor was authored by hand or when it was built.

## Routing

The Next.js edit page receives id and renders ContactEditor inside RequirePermission. The route registry names crm.contacts.record.edit with contactId, and the legacy /contacts/:id/edit mapping redirects to the registered route. The entry check uses permissions.crm.contacts.edit, mapped to crm.contacts.update. Registry labels establish the Edit segment; this inspection does not prove that the shell resolves a live contact name in every breadcrumb or separately verify its layout gates.

## Source

ContactEditor coordinates loading, form-definition compilation, draft state, review and submission. Its header, change-summary, save-failure, mapping and collection files divide that work. FormRenderer renders the compiled form, and contact collection and affiliation adapters bind its values to the reusable editors. EditorActionBar supplies the sticky controls, issue list and conflict alert. Record operations choose live CRM or the mock store, with live mapping and update reconciliation in the contact libraries.

## Components

The configured contact form binds the registered address, e-mail and phone editors to draft collections. The affiliation integration composes the registered section and drawer, with the row reached through the section dependency. Organization selection is supplied to the drawer and configured fields. Contact-valued fields use the older picker variant described by the Contact Selector contract, rather than establishing use of its default Quick Search export. The shared form engine and editor action bar belong to Platform; registration of the Entities components does not close those shell contract gaps.

## Data Integration

Record operations load the contact through live CRM or the console's mock store. Platform metadata supplies the object description. Edit disables create-form resolution, so useCreateForm returns the built-in contact-admin definition rather than a configured type-specific form. The loaded record becomes normalized draft and initial form values; the form's supported bindings determine which changed keys enter the write body. Affiliation mapping prepares the relationship write, and live mapping produces version-tested patch directives.

Eligible uncertain write results are reconciled by reading back and comparing sent values. Success discards the held draft and navigates away; it does not replace form state and keep this editor open. A conflict leaves the draft available. Reload discards it and reloads the record with a notice. The draft hooks restore sign-in recovery values against their original base version, preserving stale-write detection.

## Actions as Implemented

Save validates first, then opens Review Changes and submits on confirmation. A validation issue can leave Save clickable, because the attempt reveals and focuses the issue rather than submitting. Reload performs a fresh read; Keep Editing clears the conflict alert without changing the draft. Cancel and ordinary same-origin links use the discard guard, whose source has no browser Back handler.

Affiliation, role and contact-point actions edit the draft until contact submission. Source-controlled address rows can be removed directly on the current row-controls path. Preference controls toggle existing rows and edit subscription choices. Photo selection reaches the draft, but live mapping warns that photo upload is unavailable and continues with supported changes. These observations do not justify the draft's earlier claim that every listed action matches its design exactly.

## Authorization

The page's edit permission maps to the CRM update key. The service declares read authorization for getContact and update authorization for patchContact, with unlock permission evaluation on the write. The patch handler maps directives with conflict details and passes the expected version to updateContact. Its declarations establish the service boundary; they do not establish universal field-level permission filtering in the browser. Record restrictions remain service responsibilities.

## Observability

The editor communicates failures through error toasts, mapped field issues and an action-bar conflict alert. The inspected component contains no dedicated logging, metric emission or error-service call. The service patch handler nudges the change-event dispatcher after updating the record. This source review does not establish wider application telemetry or claim that the screen is the sole source of save observability.

## Design Fidelity

FND-CRM-ENT-088 records open differences in validation, navigation, linked removal and conflict presentation. Save remains enabled for a dirty invalid draft and validates on attempt; current source-controlled address rows have an enabled Remove action; the navigation guard does not intercept browser Back. Conflict choices live in an inline action-bar alert rather than the inventory's conflict dialog. The editor uses the built-in edit definition rather than resolving configured contact-type forms as the earlier design prose claims. The fidelity row preserves these differences without changing the protected design criteria.

The affiliation screenshot notes document Andrew's direct-role-checkbox and separate-tab changes. They establish historical choices for the affiliations work. The current drawer uses dated role rows with selectors and Add Role instead of that historical checkbox layout. These notes do not approve the whole Screen Design or settle the open fidelity differences.

## Verification

The screen specification defines eight proposed browser scenarios. Contact acceptance journeys contain related assertions, but their partial contributions do not establish the full three screen scenarios previously bound to the suite; FND-CRM-ENT-090 records that distinction. The protected verification lists retain the existing browser, fixture and service suites without certifying their results.

Contact editor unit tests exist but lack a registered executable-suite home, as FND-CRM-ENT-086 records. This job inspects source and runs corpus checks; it does not execute the deployed console journeys, conduct an accessibility audit or verify deployment of the pinned console commit.

## Screenshots

The artifact retains one desktop image of the Affiliations tab. The affiliations notes describe this mock-mode editor capture and its revision-3 components. Its committed path, capture date and hash remain unchanged. This review checks the recorded source and image identity; it does not produce a new screenshot or infer a live deployment from the mock capture.
