# Contact Channels Panel URL: /docs/web/docyrus/contact-channels-panel A Docyrus-backed panel for managing a record's contact channels, the append-only consent ledger and the validation engine. `ContactChannelsPanel` wires the full **Contact Channels & Consent API** to a single component, laid out as a **Contact Points & Consent** detail view: a preferred email / phone **summary**, grouped **Emails** / **Phones** (and messaging / social / web when present) cards showing each channel's validation status, validating provider and source, per-brand **consent** breakdowns, and a merged consent + validation **history** trail. Give it an authenticated `client` and the owning `(appSlug, dataSourceSlug, recordId)` and it lists the record's channels, lets you add / edit / archive / restore / delete them, promote a primary per kind, append to the **consent ledger** (scoped by brand → medium → purpose), record a validation result and run the **validation engine** — all through [`useDocyrusContactChannels`](#hook), which you can also use standalone to build a custom UI. **Demo:** ```tsx 'use client'; import { Button } from '@docyrus/ui/primitives/ui/button'; export function ContactChannelsPanelDemo() { return (

{''} requires an authenticated{' '} RestApiClient plus the owning{' '} (appSlug, dataSourceSlug, recordId). It talks to the Contact Channels & Consent API — listing channels, recording the consent ledger (brand · medium · purpose) and running the validation engine — so the preview here is informational only. Wire it against your tenant (or the playground page on base.contact) to see it live.

); } ``` ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/ui-contact-channels-panel ``` **Dependencies:** - [@docyrus/api-client](https://www.npmjs.com/package/@docyrus/api-client) - [@tanstack/react-query](https://www.npmjs.com/package/@tanstack/react-query) - [lucide-react](https://www.npmjs.com/package/lucide-react) ## Usage ```tsx import { useDocyrusAuth } from '@docyrus/signin'; import { ContactChannelsPanel } from '@docyrus/ui/components/contact-channels-panel'; function ContactChannels({ recordId }: { recordId: string }) { const { client } = useDocyrusAuth(); if (!client) return null; return ( ); } ``` ### As a side drawer Pass `open` / `onOpenChange` to render the panel inside an [`AwesomeDialog`](/docs/web/components/awesome-dialog) — a **right-side sheet** by default. The typical pairing is the compact [`ContactChannelsField`](/docs/web/components/contact-channels-field) summary whose **Details** link opens this panel, with the contact / organization name as the `subtitle`: ```tsx const [open, setOpen] = useState(false); setOpen(true)} /> ``` ## Endpoints The panel (via `useDocyrusContactChannels`) targets, under the record base path `/v1/apps/{appSlug}/data-sources/{dataSourceSlug}/items/{recordId}`: | Group | Operations | |-------|-----------| | Channels | `GET /channels` · `POST /channels` · `PATCH /channels/{id}` · `POST /channels/{id}/make-primary` · `POST /channels/{id}/archive` · `POST /channels/{id}/unarchive` · `DELETE /channels/{id}` | | Consent | `GET /consent` (record-wide) · `GET /channels/{id}/consent` · `POST /channels/{id}/consent` · `POST /channels/{id}/consent/bulk` · `POST /channels/consent/bulk` | | Validation | `GET /validations` (record-wide) · `GET /channels/{id}/validations` · `POST /channels/{id}/validations` · `POST /channels/{id}/validate` | | Brands | `GET /v1/tenant/brands` (for brand-aware consent labels) | Read endpoints require the `DS.Read.All` scope; write endpoints require `DS.ReadWrite.All`. The record-wide consent / validation queries avoid a per-channel N+1 in the panel. The consent and validation ledgers are **append-only**; the channel's `consent` / `validation_status` caches are maintained server-side by DB triggers and treated as read-only. ## API Reference | Prop | Type | Default | Description | |------|------|---------|-------------| | `client` | `ContactChannelsClient` | — | Authenticated REST client (`@docyrus/api-client`'s `RestApiClient` works). | | `appSlug` | `string` | — | App slug of the owning record. | | `dataSourceSlug` | `string` | — | Data source slug of the owning record. | | `recordId` | `string` | — | Owning record id (uuid). | | `title` | `string` | `'Contact Points & Consent'` | Dialog heading. | | `subtitle` | `string` | — | Sub-heading under the title (e.g. the contact and organization names). | | `includeArchived` | `boolean` | `false` | Initial state of the "Show archived" toggle. | | `enabled` | `boolean` | `true` | Defer queries until ready (e.g. auth resolved). | | `loadBrands` | `boolean` | `true` | Fetch tenant brands for brand-aware consent. | | `readOnly` | `boolean` | `false` | Hide every write affordance. | | `maxHeight` | `string` | `'40rem'` | Max height when rendered inline (the dialog body scrolls). | | `onChange` | `(channels: ContactChannel[]) => void` | — | Fired with the refreshed channel list after mutations. | | `className` | `string` | — | Additional class on the root. | | `open` | `boolean` | — | Dialog open state. Providing `open`/`onOpenChange` renders the panel in an `AwesomeDialog`. | | `onOpenChange` | `(open: boolean) => void` | — | Dialog open-change handler. | | `container` | `'sheet' \| 'drawer' \| 'modal'` | `'sheet'` | Dialog container when shown as a dialog. | | `side` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'right'` | Side for the sheet / drawer container. | | `size` | `'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'` | `'xl'` | Dialog size preset. | ## Hook `useDocyrusContactChannels({ client, appSlug, dataSourceSlug, recordId })` returns the data and a typed mutation for every endpoint: ```tsx const channels = useDocyrusContactChannels({ client, appSlug, dataSourceSlug, recordId }); // data channels.channels; // ContactChannel[] channels.brands; // ContactBrand[] channels.consentHistory; // ConsentRecord[] (record-wide) channels.validationHistory; // ValidationRecord[] (record-wide) channels.isLoading; channels.error; // mutations (all return promises) await channels.createChannel({ channelKind: 'email', channelType: 'email', value: 'a@b.com' }); await channels.updateChannel(id, { label: 'work' }); await channels.makePrimary(id); await channels.archiveChannel(id, 'old work email'); await channels.unarchiveChannel(id); await channels.deleteChannel(id); await channels.recordConsent(id, { purpose: 'marketing', action: 'opt_in', consentStatus: 'opted_in', consentChannel: 'sms' }); await channels.bulkRecordConsent(id, items); await channels.bulkRecordRecordConsent(items); // spans many channels await channels.recordValidation(id, { method: 'manual', status: 'valid' }); await channels.runValidation(id); // POST /channels/{id}/validate ``` ## Components | Component | Description | |-----------|-------------| | `ContactChannelsPanel` | Full Docyrus-backed panel (inline or as an `AwesomeDialog` drawer). | | `PanelSummary` | Preferred email / phone summary cards. | | `ChannelCard` | Per-channel read card with status grid (validation, provider, source) and the action menu. | | `BrandConsentCard` | One brand's consent breakdown (EMAIL / PHONE mediums + capture metadata). | | `ConsentHistory` | Merged, collapsible consent + validation audit trail. | | `ChannelFormDialog` | Add / edit dialog (wraps the backend-agnostic `ChannelEditor`). | | `ConsentDialog` | Records a single consent action (channel · purpose · action · status · medium · brand). | | `ValidationDialog` | Records an externally-supplied validation result (`POST /channels/{id}/validations`). | | `useDocyrusContactChannels` | The data + mutation hook. | ## Type Exports | Type | Description | |------|-------------| | `ContactChannelsPanelProps` | Props for `ContactChannelsPanel`. | | `ContactChannelsClient` | Minimal `{ get, post, patch, delete }` client contract. | | `UseDocyrusContactChannelsOptions` / `UseDocyrusContactChannelsResult` | Hook options + return shape. | | `ConsentEntryWithChannel` | A consent entry plus its target `channelId` (record-wide bulk). | | `ContactChannel` / `ConsentRecord` / `ValidationRecord` / `ContactBrand` | Re-exported from the field package. |