# Sales Pipeline

Canonical page: https://www.adaptabletools.com/docs/showcase-demos-sales-pipeline

**Example: Showcase: Sales Pipeline**

Demo project (Vanilla); the files without a marker are shown below:

```text
src/
  adaptableOptions.ts
  agGridModules.ts     [template]
  columnDefs.ts
  gridOptions.ts
  index.ts             [template]
  onAdaptableReady.ts  [template]
  rowData.ts
  styles.css           [template]
index.html             [template]
package.json           [template]
tsconfig.json          [template]
```

Files marked `[template]` are the same in every demo; see the [demo template](https://www.adaptabletools.com/llms/demo-template.md#vanilla).

**`src/adaptableOptions.ts`**

```ts
import {
  ActionColumnContext,
  AdaptableButton,
  AdaptableOptions,
  CustomToolbarFormContext,
} from '@adaptabletools/adaptable';
import {DealStage, PipelineDeal} from './rowData';

const FORWARD_STAGE: Record<DealStage, DealStage | null> = {
  Lead: 'Qualified',
  Qualified: 'Proposal',
  Proposal: 'Negotiation',
  Negotiation: 'Won',
  Won: null,
  Lost: null,
};

const BACK_STAGE: Record<DealStage, DealStage | null> = {
  Lead: null,
  Qualified: 'Lead',
  Proposal: 'Qualified',
  Negotiation: 'Proposal',
  Won: 'Negotiation',
  Lost: 'Negotiation',
};

function applyPipelineToolbarFilter(context: CustomToolbarFormContext) {
  const owner = context.formData.owner as string | undefined;
  const stage = context.formData.stage as string | undefined;
  const openOnly = Boolean(context.formData.openOnly);

  const expressions: string[] = [];
  if (owner && owner !== 'All') {
    expressions.push(`[owner] = "${owner}"`);
  }
  if (stage && stage !== 'All') {
    expressions.push(`[stage] = "${stage}"`);
  }
  if (openOnly) {
    expressions.push('QUERY("Open Pipeline")');
  }

  context.adaptableApi.filterApi.gridFilterApi.setGridFilterExpression(
    expressions.length ? expressions.join(' AND ') : null
  );
}

function moveStageForward(context: ActionColumnContext) {
  const row = context.rowNode?.data as PipelineDeal | undefined;
  if (!row) {
    return;
  }
  const nextStage = FORWARD_STAGE[row.stage];
  if (!nextStage) {
    return;
  }
  context.adaptableApi.gridApi.setCellValue({
    columnId: 'stage',
    newValue: nextStage,
    primaryKeyValue: context.primaryKeyValue,
    rowNode: context.rowNode,
  });
}

function moveStageBack(context: ActionColumnContext) {
  const row = context.rowNode?.data as PipelineDeal | undefined;
  if (!row) {
    return;
  }
  const previousStage = BACK_STAGE[row.stage];
  if (!previousStage) {
    return;
  }
  context.adaptableApi.gridApi.setCellValue({
    columnId: 'stage',
    newValue: previousStage,
    primaryKeyValue: context.primaryKeyValue,
    rowNode: context.rowNode,
  });
}

export const adaptableOptions: AdaptableOptions<PipelineDeal> = {
  primaryKey: 'id',
  adaptableId: 'Showcase: Sales Pipeline',
  userName: 'Alex Morgan',
  dashboardOptions: {
    customToolbars: [
      {
        name: 'PipelineToolbar',
        title: 'Pipeline',
        toolbarForm: {
          fields: [
            {
              name: 'owner',
              label: 'Owner',
              fieldType: 'select',
              defaultValue: 'All',
              options: [
                {label: 'All owners', value: 'All'},
                {label: 'Alex Morgan', value: 'Alex Morgan'},
                {label: 'Jordan Lee', value: 'Jordan Lee'},
                {label: 'Sam Patel', value: 'Sam Patel'},
                {label: 'Riley Chen', value: 'Riley Chen'},
              ],
              onValueChange: (_value, ctx) =>
                applyPipelineToolbarFilter(ctx as CustomToolbarFormContext),
            },
            {
              name: 'stage',
              label: 'Stage',
              fieldType: 'select',
              defaultValue: 'All',
              options: [
                {label: 'All stages', value: 'All'},
                {label: 'Lead', value: 'Lead'},
                {label: 'Qualified', value: 'Qualified'},
                {label: 'Proposal', value: 'Proposal'},
                {label: 'Negotiation', value: 'Negotiation'},
                {label: 'Won', value: 'Won'},
                {label: 'Lost', value: 'Lost'},
              ],
              onValueChange: (_value, ctx) =>
                applyPipelineToolbarFilter(ctx as CustomToolbarFormContext),
            },
            {
              name: 'openOnly',
              label: 'Open deals only',
              fieldType: 'checkbox',
              defaultValue: false,
              onValueChange: (_value, ctx) =>
                applyPipelineToolbarFilter(ctx as CustomToolbarFormContext),
            },
          ],
          buttons: [
            {
              label: 'Clear',
              buttonStyle: {tone: 'neutral', variant: 'text'},
              onClick: (
                _button: AdaptableButton<CustomToolbarFormContext>,
                context: CustomToolbarFormContext
              ) => {
                context.adaptableApi.filterApi.gridFilterApi.clearGridFilter();
                context.adaptableApi.dashboardApi.resetCustomToolbarFormData(
                  'PipelineToolbar'
                );
              },
            },
          ],
        },
      },
    ],
  },
  actionColumnOptions: {
    actionColumns: [
      {
        columnId: 'change_stage',
        friendlyName: 'Change',
        settings: {
          autoWidth: true,
          minWidth: 120,
        },
        controls: [
          {
            type: 'button',
            label: 'Back',
            style: {variant: 'outlined', tone: 'neutral'},
            icon: {
              style: {height: 20, width: 20},
              name: 'arrow-left',
            },
            disabled: (context: ActionColumnContext) => {
              const stage = (context.data as PipelineDeal | undefined)?.stage;
              return stage == null || BACK_STAGE[stage] == null;
            },
            onClick: (context: ActionColumnContext) => {
              moveStageBack(context);
            },
          },
          {
            type: 'button',
            label: 'Forward',
            iconPosition: 'end',
            style: {variant: 'outlined', tone: 'accent'},
            icon: {
              style: {height: 20, width: 20},
              name: 'arrow-right',
            },
            disabled: (context: ActionColumnContext) => {
              const stage = (context.data as PipelineDeal | undefined)?.stage;
              return stage == null || FORWARD_STAGE[stage] == null;
            },
            onClick: (context: ActionColumnContext) => {
              moveStageForward(context);
            },
          },
        ],
      },
    ],
  },
  initialState: {
    Dashboard: {
      Tabs: [
        {
          Name: 'Pipeline',
          Toolbars: [
            'PipelineToolbar',
            'Layout',
            'GridFilter',
            'QuickSearch',
            'Alert',
          ],
        },
      ],
      ModuleButtons: [
        'Layout',
        'StyledColumn',
        'Alert',
        'Note',
      ],
    },
    StatusBar: {
      StatusBars: [
        {
          Key: 'Center Panel',
          StatusBarPanels: ['Layout', 'Alert'],
        },
      ],
    },
    Theme: {CurrentTheme: 'dark'},
    NamedQuery: {
      NamedQueries: [
        {
          Name: 'Open Pipeline',
          Expression: '[stage] != "Won" AND [stage] != "Lost"',
        },
        {
          Name: 'Closing Soon',
          Expression:
            '([stage] = "Proposal" OR [stage] = "Negotiation") AND [winProbability] >= 0.6',
        },
        {
          Name: 'Stale Deals',
          Expression:
            '[lastActivityDays] > 30 AND [stage] != "Won" AND [stage] != "Lost"',
        },
        {
          Name: 'My Deals',
          Expression: '[owner] = "Alex Morgan"',
        },
      ],
    },
    Layout: {
      CurrentLayout: 'Pipeline',
      Layouts: [
        {
          Name: 'Pipeline',
          AutoSizeColumns: true,
          TableColumns: [
            'account',
            'contact',
            'owner',
            'stage',
            'change_stage',
            'dealValue',
            'winProbability',
            'priority',
            'lastActivityDays',
            'closeDate',
          ],
          ColumnSizing: {
            change_stage: {Width: 158},
            dealValue: {Width: 100},
            winProbability: {Width: 76},
          },
        },
        {
          Name: 'My Deals',
          AutoSizeColumns: true,
          TableColumns: [
            'account',
            'contact',
            'stage',
            'change_stage',
            'dealValue',
            'winProbability',
            'priority',
            'lastActivityDays',
            'closeDate',
          ],
          GridFilter: {
            Expression: 'QUERY("My Deals")',
          },
        },
        {
          Name: 'Closing Soon',
          AutoSizeColumns: true,
          TableColumns: [
            'account',
            'owner',
            'stage',
            'change_stage',
            'dealValue',
            'winProbability',
            'closeDate',
          ],
          GridFilter: {
            Expression: 'QUERY("Closing Soon")',
          },
        },
        {
          Name: 'Stale',
          AutoSizeColumns: true,
          TableColumns: [
            'account',
            'owner',
            'stage',
            'change_stage',
            'lastActivityDays',
            'dealValue',
            'winProbability',
          ],
          GridFilter: {
            Expression: 'QUERY("Stale Deals")',
          },
        },
        {
          Name: 'By Stage',
          AutoSizeColumns: true,
          SuppressAggFuncInHeader: true,
          RowGroupedColumns: ['stage'],
          RowGroupValues: {
            RowGroupDefaultBehavior: 'expanded',
          },
          ColumnPinning: {'ag-Grid-AutoColumn': 'left'},
          TableAggregationColumns: [
            {ColumnId: 'dealValue', AggFunc: 'sum'},
            {ColumnId: 'winProbability', AggFunc: 'avg'},
            {ColumnId: 'id', AggFunc: 'count'},
          ],
          TableColumns: [
            'ag-Grid-AutoColumn',
            'account',
            'contact',
            'owner',
            'dealValue',
            'winProbability',
            'priority',
            'lastActivityDays',
            'closeDate',
            'change_stage',
          ],
        },
      ],
    },
    StyledColumn: {
      StyledColumns: [
        {
          Name: 'stage Badge',
          ColumnId: 'stage',
          BadgeStyle: {
            Badges: [
              {
                Rule: '[stage] = "Lead"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-9)',
                  ForeColor: 'var(--ab-color-palette-10)',
                },
              },
              {
                Rule: '[stage] = "Qualified"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-7)',
                  ForeColor: 'var(--ab-color-palette-8)',
                },
              },
              {
                Rule: '[stage] = "Proposal"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-3)',
                  ForeColor: 'var(--ab-color-palette-4)',
                },
              },
              {
                Rule: '[stage] = "Negotiation"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-11)',
                  ForeColor: 'var(--ab-color-palette-12)',
                },
              },
              {
                Rule: '[stage] = "Won"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-5)',
                  ForeColor: 'var(--ab-color-palette-6)',
                },
              },
              {
                Rule: '[stage] = "Lost"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-1)',
                  ForeColor: 'var(--ab-color-palette-2)',
                },
              },
            ],
          },
        },
        {
          Name: 'priority Badge',
          ColumnId: 'priority',
          BadgeStyle: {
            Badges: [
              {
                Rule: '[priority] = "High"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-1)',
                  ForeColor: 'var(--ab-color-palette-2)',
                  FontWeight: 'Bold',
                },
              },
              {
                Rule: '[priority] = "Medium"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-3)',
                  ForeColor: 'var(--ab-color-palette-4)',
                },
              },
              {
                Rule: '[priority] = "Low"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-9)',
                  ForeColor: 'var(--ab-color-palette-10)',
                },
              },
            ],
          },
        },
      ],
    },
    FormatColumn: {
      FormatColumns: [
        {
          Name: 'format-stale',
          Scope: {ColumnIds: ['lastActivityDays']},
          Rule: '[lastActivityDays] > 30',
          Style: {
            ForeColor: 'var(--ab-color-destructive)',
            FontWeight: 'Bold',
          },
        },
        {
          Name: 'format-deal-value',
          Scope: {ColumnIds: ['dealValue']},
          DisplayFormat: {
            Formatter: 'NumberFormatter',
            Options: {Prefix: '$', FractionDigits: 0},
          },
        },
        {
          Name: 'format-win-probability',
          Scope: {ColumnIds: ['winProbability']},
          DisplayFormat: 'Percentage',
        },
        {
          Name: 'format-close-date',
          Scope: {ColumnIds: ['closeDate']},
          DisplayFormat: {
            Formatter: 'DateFormatter',
            Options: {Pattern: 'dd MMM yyyy'},
          },
        },
      ],
    },
    Alert: {
      AlertDefinitions: [
        {
          Name: 'Closed deal locked',
          MessageType: 'Warning',
          MessageText: 'Won and lost deals cannot be edited.',
          Scope: {ColumnIds: ['dealValue', 'winProbability', 'stage']},
          Rule: '[stage] = "Won" OR [stage] = "Lost"',
          AlertProperties: {
            PreventEdit: true,
            DisplayNotification: true,
          },
        },
        {
          Name: 'Stale deal reminder',
          MessageType: 'Info',
          MessageHeader: 'Follow up',
          MessageText: 'No activity in over 30 days.',
          Scope: {All: true},
          Rule: 'QUERY("Stale Deals")',
          AlertProperties: {
            DisplayNotification: true,
          },
        },
      ],
    },
    Note: {
      Notes: [
        {
          PrimaryKeyValue: 1,
          ColumnId: 'account',
          Text: 'Security review scheduled — expect sign-off next week.',
          Author: 'Alex Morgan',
          NoteType: 'Info',
          CreatedAt: Date.now() - 86_400_000,
          UpdatedAt: Date.now() - 86_400_000,
        },
        {
          PrimaryKeyValue: 8,
          ColumnId: 'account',
          Text: 'Champion on leave until the 15th; follow up with procurement.',
          Author: 'Alex Morgan',
          NoteType: 'Warning',
          CreatedAt: Date.now() - 172_800_000,
          UpdatedAt: Date.now() - 172_800_000,
        },
        {
          PrimaryKeyValue: 15,
          ColumnId: 'account',
          Text: 'Competitor undercut on year-one pricing — need exec sponsor.',
          Author: 'Alex Morgan',
          NoteType: 'Error',
          CreatedAt: Date.now() - 259_200_000,
          UpdatedAt: Date.now() - 259_200_000,
        },
      ],
    },
  },
};
```

**`src/rowData.ts`**

```ts
export type DealStage =
  | 'Lead'
  | 'Qualified'
  | 'Proposal'
  | 'Negotiation'
  | 'Won'
  | 'Lost';
export type DealPriority = 'High' | 'Medium' | 'Low';
export type DealRegion = 'Americas' | 'EMEA' | 'APAC';

export interface PipelineDeal {
  id: number;
  account: string;
  contact: string;
  owner: string;
  region: DealRegion;
  industry: string;
  stage: DealStage;
  dealValue: number;
  winProbability: number;
  priority: DealPriority;
  lastActivityDays: number;
  closeDate: string;
}

const ACCOUNTS = [
  {account: 'Northwind Logistics', industry: 'Transport'},
  {account: 'Acme Retail Group', industry: 'Retail'},
  {account: 'BluePeak Software', industry: 'Technology'},
  {account: 'Helios Manufacturing', industry: 'Industrials'},
  {account: 'Summit Health Partners', industry: 'Healthcare'},
  {account: 'Orion Media', industry: 'Media'},
  {account: 'Vertex Insurance', industry: 'Financial Services'},
  {account: 'Cascade Energy', industry: 'Energy'},
  {account: 'Pioneer Foods', industry: 'Consumer'},
  {account: 'Atlas Consulting', industry: 'Professional Services'},
  {account: 'Lumen Telecom', industry: 'Telecom'},
  {account: 'Harbor Hotels', industry: 'Hospitality'},
  {account: 'Nova EdTech', industry: 'Education'},
  {account: 'Sterling Legal', industry: 'Legal'},
  {account: 'Brightline Pharma', industry: 'Healthcare'},
] as const;

const CONTACTS = [
  'Jordan Blake',
  'Sam Rivera',
  'Taylor Kim',
  'Morgan Ellis',
  'Casey Nguyen',
  'Riley Okafor',
  'Jamie Hart',
  'Alex Chen',
  'Drew Patel',
  'Quinn Foster',
];

const OWNERS = [
  'Alex Morgan',
  'Jordan Lee',
  'Sam Patel',
  'Riley Chen',
] as const;

const STAGES: DealStage[] = [
  'Lead',
  'Qualified',
  'Proposal',
  'Negotiation',
  'Won',
  'Lost',
];

const REGIONS: DealRegion[] = ['Americas', 'EMEA', 'APAC'];
const PRIORITIES: DealPriority[] = ['High', 'Medium', 'Low'];

function pseudoRandom(seed: number): number {
  const x = Math.sin(seed * 2711) * 10000;
  return x - Math.floor(x);
}

function addDays(base: Date, days: number): string {
  const d = new Date(base);
  d.setDate(d.getDate() + days);
  return d.toISOString().slice(0, 10);
}

const today = new Date();

export const rowData: PipelineDeal[] = Array.from({length: 54}, (_, index) => {
  const id = index + 1;
  const accountMeta = ACCOUNTS[Math.floor(pseudoRandom(id * 3) * ACCOUNTS.length)];
  const stage = STAGES[Math.floor(pseudoRandom(id * 7) * STAGES.length)];
  const isClosed = stage === 'Won' || stage === 'Lost';
  const daysToClose = Math.round(5 + pseudoRandom(id * 11) * 75);
  const lastActivityDays = isClosed
    ? Math.round(1 + pseudoRandom(id * 13) * 14)
    : Math.round(2 + pseudoRandom(id * 17) * 55);

  return {
    id,
    account: accountMeta.account,
    contact: CONTACTS[Math.floor(pseudoRandom(id * 5) * CONTACTS.length)],
    owner: OWNERS[Math.floor(pseudoRandom(id * 19) * OWNERS.length)],
    region: REGIONS[Math.floor(pseudoRandom(id * 23) * REGIONS.length)],
    industry: accountMeta.industry,
    stage,
    dealValue: Math.round(25_000 + pseudoRandom(id * 29) * 875_000),
    winProbability: Math.round((0.08 + pseudoRandom(id * 31) * 0.82) * 100) / 100,
    priority: PRIORITIES[Math.floor(pseudoRandom(id * 37) * PRIORITIES.length)],
    lastActivityDays,
    closeDate: addDays(today, isClosed ? -lastActivityDays : daysToClose),
  };
});

// Seed a few recognisable rows for notes and toolbar demos.
rowData[0] = {
  ...rowData[0],
  account: 'BluePeak Software',
  owner: 'Alex Morgan',
  stage: 'Proposal',
  winProbability: 0.72,
  priority: 'High',
  lastActivityDays: 4,
};
rowData[7] = {
  ...rowData[7],
  account: 'Summit Health Partners',
  owner: 'Alex Morgan',
  stage: 'Negotiation',
  winProbability: 0.81,
  priority: 'High',
  lastActivityDays: 38,
};
rowData[14] = {
  ...rowData[14],
  account: 'Northwind Logistics',
  owner: 'Jordan Lee',
  stage: 'Qualified',
  winProbability: 0.45,
  lastActivityDays: 12,
};
```

**`src/columnDefs.ts`**

```ts
import {ColDef} from 'ag-grid-enterprise';

export const columnDefs: ColDef[] = [
  {field: 'account', headerName: 'Account', cellDataType: 'text', width: 180},
  {field: 'contact', cellDataType: 'text', width: 130},
  {
    field: 'owner',
    cellDataType: 'text',
    enableRowGroup: true,
    width: 120,
  },
  {
    field: 'region',
    cellDataType: 'text',
    enableRowGroup: true,
    width: 100,
  },
  {field: 'industry', cellDataType: 'text', width: 140},
  {
    field: 'stage',
    cellDataType: 'text',
    enableRowGroup: true,
    width: 120,
  },
  {
    field: 'dealValue',
    headerName: 'Deal Value',
    cellDataType: 'number',
    type: 'rightAligned',
    editable: true,
    enableValue: true,
  },
  {
    field: 'winProbability',
    headerName: 'Win %',
    cellDataType: 'number',
    type: 'rightAligned',
    editable: true,
    aggFunc: 'avg',
  },
  {field: 'priority', cellDataType: 'text', width: 100},
  {
    field: 'lastActivityDays',
    headerName: 'Days Idle',
    cellDataType: 'number',
    type: 'rightAligned',
    width: 100,
  },
  {
    field: 'closeDate',
    headerName: 'Close Date',
    cellDataType: 'date',
    width: 120,
  },
  {field: 'id', hide: true, editable: false},
];
```

**`src/gridOptions.ts`**

```ts
import {GridOptions, themeBalham} from 'ag-grid-enterprise';

import {columnDefs} from './columnDefs';
import {PipelineDeal, rowData as salesPipelineRowData} from './rowData';

export const gridOptions: GridOptions<PipelineDeal> = {
  theme: themeBalham,
  defaultColDef: {
    resizable: true,
    sortable: true,
    editable: false,
    filter: true,
    floatingFilter: true,
  },
  columnDefs: columnDefs,
  rowData: salesPipelineRowData,
  sideBar: true,
  suppressMenuHide: true,
  columnMenu: 'new',
  cellSelection: true,
  statusBar: {
    statusPanels: [
      {statusPanel: 'agTotalRowCountComponent', align: 'left'},
      {statusPanel: 'agFilteredRowCountComponent'},
      {
        key: 'Center Panel',
        statusPanel: 'AdaptableStatusPanel',
        align: 'center',
      },
    ],
  },
};
```
