# CRM Contact Page

> A React CRM contact page: a company record with facts and actions, tabs for overview, activity, deals, files and notes, and a details panel.

- Type: React block (Blocks)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.1
- Install: `npx wingo-ui@latest add crm-contact-page`
- Docs: https://wingo-ui.com/components/crm-contact-page

## Interaction

The header shows the company with its status, a star and a follow toggle, key facts (copy the tax ID, open the last invoice) and actions: Email and Call open the mail app and the phone, New deal opens a validated dialog (title, value, stage, close date) and the new deal lands in the deals tab, the rest (New invoice, Add contact, Export record, Archive) fold into ⋯ when they do not fit and always on phones. Double click the name, press F2 or tap the pencil to rename it. The pager walks to the previous and next client (keys are off by default; the demo binds Alt + arrows). Once the header scrolls away a glass bar keeps the name and the primary action. The tabs (arrows, Home and End) switch Overview, Activity, Deals, Files and Notes; the panel slides in and the counts follow the data. Overview: the company details with copy buttons, the contacts, the open deals (View all jumps to the tab) and the last four events. Activity: write a comment (@ mentions a colleague, ⌘ Enter sends) and it lands at the top of today; the chips filter emails, calls, meetings and invoices and announce the count. Deals: a sortable table (cards on phones) with the open total. Files: drop or pick files, they upload with progress and join the list; preview, download or delete (asked first). Notes: a rich text editor with mentions, ⌘ Enter or Save note saves; pin, edit or delete a note from ⋯ (delete has an undo toast). The side panel shows the primary contact (call, email, WhatsApp), billing, tags (remove with ×, add from the field) and the team; on phones it is the Details tab. Archiving shows an alert with Restore and makes the record read-only. Failed saves roll back with a toast. Sections rise in once on mount; reduced motion keeps everything still.

## Installation

```bash
npx wingo-ui@latest add crm-contact-page
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/crm-contact-page` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/blocks/crm-contact-page.tsx`
- `components/blocks/crm-contact-page-tabs.tsx`
- `components/blocks/crm-contact-page-data.ts`

npm dependencies: `npm i motion lucide-react react-hook-form zod`

Wingo UI dependencies (installed automatically):

- [Alert](https://wingo-ui.com/components/alert) ([Markdown](https://wingo-ui.com/components/alert.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Comment Thread](https://wingo-ui.com/components/comment-thread) ([Markdown](https://wingo-ui.com/components/comment-thread.md))
- [Contact card](https://wingo-ui.com/components/contact-card) ([Markdown](https://wingo-ui.com/components/contact-card.md))
- [Currency Input](https://wingo-ui.com/components/currency-input) ([Markdown](https://wingo-ui.com/components/currency-input.md))
- [Date Picker](https://wingo-ui.com/components/date-picker) ([Markdown](https://wingo-ui.com/components/date-picker.md))
- [Description List](https://wingo-ui.com/components/description-list) ([Markdown](https://wingo-ui.com/components/description-list.md))
- [Detail Layout](https://wingo-ui.com/components/detail-layout) ([Markdown](https://wingo-ui.com/components/detail-layout.md))
- [Dialog](https://wingo-ui.com/components/dialog) ([Markdown](https://wingo-ui.com/components/dialog.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [Entity Header](https://wingo-ui.com/components/entity-header) ([Markdown](https://wingo-ui.com/components/entity-header.md))
- [File List](https://wingo-ui.com/components/file-list) ([Markdown](https://wingo-ui.com/components/file-list.md))
- [File Upload](https://wingo-ui.com/components/file-upload) ([Markdown](https://wingo-ui.com/components/file-upload.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Kbd](https://wingo-ui.com/components/kbd) ([Markdown](https://wingo-ui.com/components/kbd.md))
- [Rich Text Editor](https://wingo-ui.com/components/rich-text-editor) ([Markdown](https://wingo-ui.com/components/rich-text-editor.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Table](https://wingo-ui.com/components/table) ([Markdown](https://wingo-ui.com/components/table.md))
- [Tabs](https://wingo-ui.com/components/tabs) ([Markdown](https://wingo-ui.com/components/tabs.md))
- [Tag](https://wingo-ui.com/components/tag) ([Markdown](https://wingo-ui.com/components/tag.md))
- [Timeline](https://wingo-ui.com/components/timeline) ([Markdown](https://wingo-ui.com/components/timeline.md))
- [Toast](https://wingo-ui.com/components/toast) ([Markdown](https://wingo-ui.com/components/toast.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Commerce](https://wingo-ui.com/components/commerce) ([Markdown](https://wingo-ui.com/components/commerce.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [Form validation](https://wingo-ui.com/components/form-validation) ([Markdown](https://wingo-ui.com/components/form-validation.md))
- [Invoicing](https://wingo-ui.com/components/invoicing) ([Markdown](https://wingo-ui.com/components/invoicing.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
// app/(admin)/clients/[id]/page.tsx (the shell lives in app/(admin)/layout.tsx: <AdminShell>{children}</AdminShell>)
"use client"

import { CrmContactPage, type CrmNote } from "@/components/blocks/crm-contact-page"

export default function ClientPage() {
  return (
    <CrmContactPage
      previousShortcut="alt+arrowleft"
      nextShortcut="alt+arrowright"
      onNoteSave={async (note: CrmNote) => {
        await fetch(`/api/clients/c01/notes/${note.id}`, { method: "PUT", body: JSON.stringify(note) })
      }}
      onTagsChange={async (tags) => {
        await fetch("/api/clients/c01", { method: "PATCH", body: JSON.stringify({ tags }) })
      }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `company?` | `CrmCompany` |  | The record ({ id, name, cui, regCom, status, industry, city, address, website, email, phone, iban, bank, owner, tags, createdAt, balance, revenueYtd, lastInvoice, lastPayment, overdueInvoices, paymentTermDays, followers, archived }); defaults to CRM_CONTACT_SAMPLE.company (Tech Solutions Inc.). |
| `contacts?` | `ContactData[]` |  | The people at the company; defaults to CRM_CONTACT_SAMPLE.contacts (Daniel Brooks, Laura Price, Sam Porter). |
| `primaryContactId?` | `string` | `"daniel-brooks"` | The contact in the side panel card, listed first with the Primary badge; defaults to the first contact. |
| `deals?` | `CrmContactDeal[]` |  | The client's deals ({ id, title, stageId, amount, currency, probability, owner, expectedCloseDate, href }); defaults to CRM_CONTACT_SAMPLE.deals (3). |
| `stages?` | `CrmDealStage[]` |  | The pipeline stages the deals point to ({ id, name, probability, kind }); defaults to CRM_CONTACT_STAGES (B2B sales). |
| `activity?` | `CrmActivity[]` |  | Timeline items with a kind (email, call, meeting, invoice, payment, stage, comment, note, file) for the icon and the filters; defaults to CRM_CONTACT_SAMPLE.activity (14). |
| `files?` | `FileItem[]` |  | The record's files; defaults to CRM_CONTACT_SAMPLE.files (6). |
| `notes?` | `CrmNote[]` |  | Notes ({ id, author, createdAt, content (HTML), pinned, editedAt }); defaults to CRM_CONTACT_SAMPLE.notes (3). |
| `people?` | `CommentAuthor[]` |  | Who can be @mentioned in comments and notes; defaults to CRM_CONTACT_TEAM. |
| `currentUser?` | `CommentAuthor` |  | The signed-in user: the composer avatar, the author of new notes and comments; defaults to Ana Pop. |
| `tab?` | `"overview" \| "activity" \| "deals" \| "files" \| "notes"` |  | The open tab, controlled; pair it with onTabChange. |
| `defaultTab?` | `"overview" \| "activity" \| "deals" \| "files" \| "notes"` | `"overview"` | The tab open first while uncontrolled. |
| `onTabChange?` | `(tab: CrmContactTab) => void` |  | Called when a tab is picked (also by View all and Add a note). |
| `pager?` | `EntityPager \| null` | `"3 of 128"` | Previous / next client ({ index, total, onPrevious, onNext, previousHref, nextHref }); null hides it. |
| `previousShortcut?` | `string` | `""` | Key for the previous client, e.g. alt+arrowleft; empty binds nothing (the demo binds Alt + arrows). |
| `nextShortcut?` | `string` | `""` | Key for the next client, e.g. alt+arrowright; empty binds nothing. |
| `noteShortcut?` | `string` | `"mod+enter"` | Saves the note from inside the editor (shown as ⌘ Enter or Ctrl + Enter); empty binds nothing. |
| `showAside?` | `boolean` | `true` | The side panel with the primary contact, billing, tags and team (the Detalii tab on phones). |
| `factsLayout?` | `"strip" \| "grid" \| "tiles"` | `"strip"` | The header facts: one scrolling strip, a grid, or gray tiles (grid and tiles fold after 4 on phones). |
| `headerSticky?` | `"none" \| "condensed"` | `"condensed"` | condensed pins a glass bar with the name and the primary action once the header scrolls away. |
| `defaultUploads?` | `UploadFile[]` |  | Uploads already running when the page opens; entries without a picked file finish on a timer (the files example). |
| `autoFocusNote?` | `boolean` | `false` | The note editor takes focus when the notes tab mounts. |
| `defaultFavorite?` | `boolean` | `true` | The header star starts on. |
| `defaultFollowing?` | `boolean` | `true` | The follow toggle starts on (you appear in the team list). |
| `listHref?` | `string` | `"/clients"` | Where Clients in the breadcrumb goes. |
| `invoiceHref?` | `string` | `"/invoices/new?client=<id>"` | Where New invoice goes (header menu and billing). |
| `contactHref?` | `string` | `"/contacts/new?client=<id>"` | Where Add contact goes. |
| `onTitleChange?` | `(title: string) => (void \| Promise<unknown>)` |  | Renaming from the header; a rejection restores the name and shows the error. Simulated by default. |
| `onAction?` | `(action: CrmContactAction, company: CrmCompany) => (void \| Promise<unknown>)` |  | Every header action. Archive and restore are optimistic (a rejection rolls back with a toast); export shows a loading toast. |
| `onNoteSave?` | `(note: CrmNote) => (void \| Promise<unknown>)` |  | A new note (new id) or a changed one (edit, pin); a rejection keeps the draft or rolls back with a toast. |
| `onNoteDelete?` | `(note: CrmNote) => (void \| Promise<unknown>)` |  | Deleting a note: removed at once, with an undo toast; a rejection brings it back. |
| `onComment?` | `(body: string, context: { mentions: string[]; }) => (void \| Promise<unknown>)` |  | A comment from the activity composer; the composer keeps the text until it resolves. |
| `onUpload?` | `(file: File, context: { signal, onProgress }) => Promise<unknown>` |  | Uploads a picked file; done files join the list. Default: a pretend upload of about 1.6 s. |
| `onFileDelete?` | `(file: FileItem) => (void \| Promise<unknown>)` |  | Deleting a file after the confirmation; the row fades while it runs. |
| `onFileOpen?` | `(file: FileItem) => void` |  | Clicking a file; default opens its preview url in a new tab. |
| `onDealCreate?` | `(values: CrmNewDealValues) => (void \| Promise<unknown>)` |  | The new deal dialog's submit; a rejection shows its message in the dialog. |
| `onTagsChange?` | `(tags: string[]) => (void \| Promise<unknown>)` |  | Tags added or removed in the side panel; optimistic, rolled back on rejection. |
| `onFavoriteChange?` | `(favorite: boolean) => (void \| Promise<unknown>)` |  | The header star. |
| `onFollowingChange?` | `(following: boolean) => (void \| Promise<unknown>)` |  | The follow toggle. |
| `labels?` | `Partial<CrmContactPageLabels>` | `"DEFAULT_CRM_CONTACT_PAGE_LABELS"` | Every visible string (English by default); CRM_CONTACT_PAGE_LABELS_RO is the ready Romanian set. Nested keys translate the composed items. |
| `locale?` | `string` | `"en-US"` | Intl locale for money, numbers and dates. |
| `currency?` | `string` | `"USD"` | The currency of the balance, revenue and deals. |
| `now?` | `Date \| number` | `"useNow()"` | The reference time for relative times and the day headings; the demo pins Sep 24, 2026 16:40. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The accent of the primary actions, the tab indicator, the chips, the editor and the upload progress. |
| `radius?` | `"sm" \| "md" \| "lg"` | `"md"` | The panels (12, 16 or 24px), the contact card, the dropzone, the editor and the dialog. |
| `loading?` | `boolean` | `false` | The header, the sections and the side panel as skeletons. |
| `animateIn?` | `boolean` | `true` | The header, the panel and the side sections rise in once on mount; off in thumbnails and under reduced motion. |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` | `"<a>"` | The router link for the breadcrumb, the pager, the deal and invoice links; defaults to the LinkProvider's. |
| `className?` | `string` |  | Merged onto the root. |
| `classNames?` | `Partial<Record<CrmContactPageSlot, string>>` |  | Classes for the parts: header, tabs, panel, section, aside, archived, dealDialog. |

## Parts

### DEFAULT_CRM_CONTACT_PAGE_LABELS (constant)

Every visible string of the page in English, including the nested labels of the composed items. Spread it and override a few to translate. Type: CrmContactPageLabels.

### CRM_CONTACT_PAGE_LABELS_EN (constant)

The same English set under an explicit name, next to the Romanian one.

### CRM_CONTACT_PAGE_LABELS_RO (constant)

The ready Romanian set of every string, for the localization demo or a Romanian app.

### OverviewPanel (component)

The Overview tab on its own: company details with copy buttons, the contacts, the open deals and the last four events. Takes ctx (CrmPanelContext), company, contacts, primaryContactId, deals, activity, onTabChange and onAddContact.

### ActivityPanel (component)

The Activity tab: a comment box with @ mentions, the kind filter chips and the grouped timeline. Takes ctx, activity, people, currentUser, onComment, onAddNote and onAnnounce.

### DealsPanel (component)

The Deals tab: a sortable table (cards on phones) with the open total and a New deal button. Takes ctx, deals and onNewDeal.

### FilesPanel (component)

The Files tab: a drop zone with upload progress and the file list with preview, download and delete. Takes ctx, files, uploads, onUploadsChange, upload, onDelete, onPreview, onDownload and radius.

### NotesPanel (component)

The Notes tab: a rich text editor with mentions and the saved notes with pin, edit and delete. Takes ctx, notes, people, currentUser, noteShortcut, autoFocus, radius, onCreate, onUpdate, onDelete and onAnnounce.

### toTimelineItem (function)

Turns a CRM activity into a Timeline item, filling in the icon of its kind. Signature: (item: CrmActivity) => TimelineItemData.

### stageTone (function)

The badge tone of a deal stage: success when won, danger when lost, neutral otherwise. Signature: (stage: CrmDealStage | undefined) => Tone.

### ACTIVITY_ICONS (constant)

The lucide icon per activity kind (email, call, meeting, invoice, payment, stage, comment, note, file).

### CRM_CONTACT_SAMPLE (constant)

Sample data: Tech Solutions Inc. with its contacts, 3 deals, 14 activity items, 6 files and 3 notes. The page's default content.

### CRM_CONTACT_ARCHIVED_SAMPLE (constant)

Sample data for an archived record (Westside Books, a former client), shown read-only.

### CRM_CONTACT_UPLOAD_SAMPLE (constant)

Sample uploads in progress, for the files example.

### CRM_CONTACT_TEAM (constant)

Sample team members with roles, the people you can @mention.

### CRM_CONTACT_CURRENT_USER (constant)

The sample signed-in user who writes comments and notes.

### CRM_CONTACT_STAGES (constant)

Sample B2B pipeline stages with probabilities: Qualified, Needs analysis, Proposal sent, Negotiation, Won and Lost.

### CRM_CONTACT_NOW (constant)

The fixed date the sample data is relative to, so relative times read the same on every render.

### CrmPanelContext (type)

What the page hands every tab panel: labels, locale, currency, tone, the section surface class, now, loading, archived, stages and the money, day, relative and percent formatters.

### CrmContactSample (type)

{ company, contacts, primaryContactId?, deals, activity, files, notes }, the shape of a record's data.

## Examples

- **Activity**: defaultTab="activity": the comment composer, the filter chips and 14 events grouped by day. Alt + arrows walk the pager.
- **Notes, editor focused**: defaultTab="notes" with autoFocusNote: write, then press ⌘ Enter to save; pin, edit or delete from ⋯.
- **Files, one upload running**: defaultUploads: Delivery-acceptance-report.pdf finishes uploading and joins the list.
- **Archived client**: Westside Books: the Archived status, the Restore alert and read-only actions.
- **Loading**: loading: the header, the sections, the contact card and the side panel as skeletons.
- **Brand tone, tiles**: tone="brand", radius="lg", factsLayout="tiles", on the deals tab.
- **Romanian copy, RON**: CRM_CONTACT_PAGE_LABELS_RO, locale="ro-RO", currency="RON": the ready Romanian set for the page and its parts.

## See also

- [CRM Pipeline Page](https://wingo-ui.com/components/crm-pipeline-page) ([Markdown](https://wingo-ui.com/components/crm-pipeline-page.md))
- [Entity Header](https://wingo-ui.com/components/entity-header) ([Markdown](https://wingo-ui.com/components/entity-header.md))
- [Detail Layout](https://wingo-ui.com/components/detail-layout) ([Markdown](https://wingo-ui.com/components/detail-layout.md))
- [Contact card](https://wingo-ui.com/components/contact-card) ([Markdown](https://wingo-ui.com/components/contact-card.md))
- [Timeline](https://wingo-ui.com/components/timeline) ([Markdown](https://wingo-ui.com/components/timeline.md))
- [Admin Shell](https://wingo-ui.com/components/admin-shell) ([Markdown](https://wingo-ui.com/components/admin-shell.md))

---

Canonical: https://wingo-ui.com/components/crm-contact-page
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
