SmartHub Editor and Code Library

Welcome to  SmartHub in Outbound by Enreach. 

Customise the look and feel of Outbound by Enreach with your own code, and consult our code snippet library.

Read through the article, or click a link below to get started.

In this article: 



What is SmartHub?

SmartHub allows you to apply your own custom programming inside Outbound by Enreach, providing the means to extend, automate, and integrate with the flow of the Contact page. It gives you a complete library to write code that interacts with the Contact Page, the Lead on it, and data outside of Outbound by Enreach.
To enable SmartHub, you need access to the SmartHub add-on module. If you don't have SmartHub access, please contact Support or your CX Manager.

## Examples of things possible with SmartHub
  • Custom validation of fields.
  • Make sure that individual fields and registered offerings completely align with the formats and rules required by your backend systems.
  • Lookup Leads in an external system before dialing.
  • Skip a Lead if it's blacklisted in external systems.
  • Fetch latest data on Lead before dialing.
  • Filter content of dropdown fields.
  • Only show delivery days matching offered product and zip code.
  • Dynamically search for product suggestions and prices in external systems.
  • Address lookup and validation.
  • Use standard address services.
  • Use your own system.
  • Register sales directly in an external system before closing the Lead.
  • Integrate directly to your CRM system from the Contact Page.
  • Supply or filter data for entry group lookup.

Getting Started with SmartHub

To begin building your extensions, expand the Administration dropdown menu, and select SmartHub Editor.
When you are in the editor you're ready to start coding.
SmartHub extensions are written in [ TypeScript], which is strongly typed superset of JavaScript.
The code editor provides syntax hightlighting, auto-suggestions, and documentation on hover.
Any SmartHub script consists of code manipulating three or four (Suggestions are optional) of the following elements:
  • Config object
  • Event handlers
  • Commands
  • Suggestions
The purpose of these elements will be discussed in the following sections.

Config Objects

The config object is the container for your extensions. It provides high-level filtering of when the extension is active.
You can add as many config objects as needed.
The config object exposes a [ fluent api], giving access to all the possible extension points, like event handlers.

Event Handlers

The event handlers represent the things your extension can react to. For example:
  • onEntryChange: The value/validity/visibility/etc. of a field has changed.
  • onLeadReady: Lead is loaded and initialised.
  • onPreLeadSave: Lead is about to be saved, do some last-minute validation.

Commands

Commands are requests for the Contact page to perform some actions. For example:
  • SaveLead: Initiates a save action.
  • AbortSave: Aborts a pending save action.
  • ChangeEntry: Change value on a field, set it to invalid, or hide the field.
  • Message: Inform the user about something.
  • Nop: Do nothing.
Not all commands are allowed at a given time. These restrictions are encoded in the type system, so that for a given event handler, only allowed commands are accepted as return values. The code editor/type checker will show errors, with error messages like `Type 'AbortSave' is not assignable to type 'HookEventHandlerResult<ChangeEntry>'.` indicating that in that instance only ChangeEntry are allowed.

Suggestions

Often it's helpful to display a list of suggestions underneath whichever field is being edited, like in Google search. This functionality is also present and fully programmable in SmartHub.
Suggestions are great for:
  • Address lookup.
  • Product/price lookup.
  • As a mutated super dropdown control.
Configuration of Suggestions are explained further down in this document in Configuring Suggestions.

The Basic Structure of an Extension

This section dissects and discusses a small example extension. The example is an extension that capitalises the first name of the Lead.
new SmartHubConfig(ca => ca.name === 'Test campaign')
.onEntryChange(
     e => e.importName === 'firstName' && e.value.length > 0,
  e => HookResult.changeEntry({
    entryId: e.id,
    newValue: e.value.substr(0,1).toUpperCase() + e.value.substr(1)
}));
Let's break this example down:
First there is instantiation of a new config object:
new SmartHubConfig(ca => ca.name === 'Test campaign')
A config object must be instantiated with a filter function, which is used to determine for which Campaigns a configuration is valid. Here, the filter is based on name for ease of understanding, but normally it would be better to use ID or some other immutable property.
If the config should be used in all cases, the filter could look something like this:
new SmartHubConfig(()) => true)
Next is hooking into the onEntryChange event:
.onEntryChange(
  e => e.importName === 'firstName' && e.value.length > 0,
  e => HookResult.nop());
All event handlers take two parameters: a filter function and a handler function. In this case, the filter function makes sure that the handler is only invoked, if the name of the field is 'firstName,' and there is a non-empty value.
A handler **must** return some command, and in this case it returns Nop (no operation) to indicate that it doesn't require any actions from the Contact page.
Finally, looking at the handler from the full example:
.onEntryChange( 
  e => e.importName === 'firstName' && e.value.length > 0,
  e => HookResult.changeEntry({
  entryId: e.id,
  newValue: e.value.substr(0,1).toUpperCase() + e.value.substr(1)<br>}));
This handler returns a ChangeEntry command, asking that the same entry that invoked the handler has its value changed to start with an upper-case letter.

Fluent api

The fluent api makes it possible to chain event handler configurations:
function CapitalizeFirstLetter (e: Contracts.LeadEntry) {return HookResult.changeEntry({
      entryId: e.id,
      newValue: e.value.substr(0,1).toUpperCase() + e.value.substr(1)
});
}

new SmartHubConfig(ca => ca.name === 'Test campaign')
.onEntryChange(
e => e.importName === 'firstName' && e.value.length > 0,
e => CapitalizeFirstLetter(e))

// onEntryChange returs a reference to the SmartHubConfig object,
// making it possible to chain configurations.

.onEntryChange(
e => e.importName === 'lastName' && e.value.length > 0,
e => CapitalizeFirstLetter(e));

Event Handlers

Event handlers are the extension points that allows for integration with the Contact page. They are invoked by the internal logic when various things occur, and can in turn affect how the Contact page should react to those things.
Each handler comes with a filter that determines whether the handler should run for the current event.
A handler must return one or more commands, a single object or an array of results. Certain events also allows returning promises of commands, or even a mix of promises and direct results.
Currently the following results are supported:
Message | ChangeEntry | AbortSave | SaveLead | Error | Nop
Nop means "No OPeration" and must be returned when the handler wishes to do nothing.
What results are allowed depends on the event type.

Entry Events

Entry events are invoked when something changes in relation to an entry/field.
For now there is just a single entry event: onEntryChange
onEntryChange is invoked whenever value/validity/visibility/etc. of a field has changed, but also once when the Lead is first loaded, after the entry has been initialised.
onEntryChange is configured using the following method:
export type EntryEventResults = Message | ChangeEntry;
export type HookEventHandlerResult<T extends HookResult.Executable> = T | HookResult.Nop | HookResult.Error | ReadonlyArray<T | HookResult.Nop | HookResult.Error>;
export type EntryEventFilter = (
entry: Contracts.LeadEntry,
lead: Contracts.Lead
  ) => boolean;
  export type EntryEventHandler = (
    entry: Contracts.LeadEntry,
    lead: Contracts.Lead
  ) => HookEventHandlerResult<HookResult.EntryEventResults>;

onEntryChange(filter: EntryEventFilter, handler: EntryEventHandler): SmartHubConfig;
Here is an example of an entry handler only invoked on entries named 'product_code'.
If the entry value has string length > 4 then entry is deemed valid.
  config.onEntryChange(
  e => e.importName == 'product_code',
  e => HookResult.changeEntry({
        entryId: e.id,
        isValid: e.value.length > 4
}))
The config interface is further extended by utilities. One extension is:
onEntryChangeValidate(
filter: (entry: Contracts.LeadEntry, lead: Contracts.Lead) => boolean,
  handler: (entry: Contracts.LeadEntry, lead: Contracts.Lead) => (boolean | undefined)
): SmartHubConfig;
With this extension the above simple validation can be configured as:
config.onEntryChangeValidate(
  e => e.importName == 'product_code',
  e => e.value.length > 4
)
Entry handlers can affect other entries. In this example an entry named `sub_product` is shown and made required if the value in the entry `product_code` ends with "_abstract".
config.onEntryChange(
e => e.importName == 'product_code',
    (e,l) => {
      const hasSubProduct = e.value.endsWith('_abstract');
      return HookResult.changeEntry({
        entryId: l.entriesByImportName.get('sub_product')!.id,
        isRequired: hasSubProduct,
        isVisible: hasSubProduct
      });
})

Lead Events

Entry events are invoked when something changes in relation to the Lead.
Currently there are four events:
export type LeadEventFilter = (
lead: Contracts.Lead & Contracts.LeadState,
  ) => boolean;
  onLeadLoad(
      filter: LeadEventFilter,
      handler: (lead: Contracts.Lead) => AsyncHookEventHandlerResult<HookResult.SaveLead>): SmartHubConfig;
  onLeadReady(
      filter: LeadEventFilter,
      handler: (lead: Contracts.Lead) => HookEventHandlerResult<HookResult.EntryEventResults>): SmartHubConfig;
  onPreLeadSave(
      filter: LeadEventFilter,
      handler: (lead: Contracts.Lead) => AsyncHookEventHandlerResult<HookResult.AbortSave | HookResult.Message>): SmartHubConfig;
  onPostLeadSave(
      filter: LeadEventFilter,
handler: (lead: Contracts.Lead) => HookResult.Nop): SmartHubConfig;
In this example, a Lead is checked in an external system immediately after loading. The check should be done for all Leads, hence the filter function just returns true. If the Lead is blacklisted, then it's saved with an appropriate status. After executing the save command, the Contact page will automatically continue to the next Lead.
Note the use of async/ await. Since onLeadLoad returns AsyncHookEventHandlerResult, it's legal to return a promise, and also required when async actions like http requests are involved in processing the event.
async function isBlackListed(l: Contracts.Lead) {
 // Do lookup in external system to check for blacklisting
 // in this example everything is blacklisted
  return true;
  }
  config.onLeadLoad(l => true,
      // In this case the result of isBlackListed is required to continue processing, so it must be awaited.
      async l => await isBlackListed(l)
      ? HookResult.saveLead({status: Contracts.LeadSaveStatus.Removed })
: HookResult.nop())
In this example the Lead is validated by some custom logic, just prior to saving. If it's not valid then the save is aborted with some reason.
async function validateOffer(l: Contracts.Lead) {
// Custom validation logic to make sure that all fields are correctly filled
// Possibly lookup in external system to make sure that the sold product is actually in stock.
// In this example it's always valid
    const isValid = true;
    return isValid
      ? HookResult.nop()
      : HookResult.abortSave({
        reason: "Product not in stock"
      });
  }
  config.onPreLeadSave(
      l => l.newStatus === Contracts.LeadStatus.UserProcessed
        && l.newClosure === Contracts.LeadClosure.Success,
      // validateOffer returns a promise of a HookResult, which is a valid return value, so no need to await it here.
      l => validateOffer(l)
    )

Configuring Suggestions

Auto suggest is supported in a generic fashion, and only assumes an input field and a list of data to pick from.
How suggestion data is queried based on the input is configurable, and so is the action to take when a suggestion is selected.
The input field can be one of three forms:
  • in
  • virtual
  • hover
For ' in' the input happens in the entry field. This is suitable for editable standalone fields, like FullAddress.
For ' virtual' the input happens in a virtual field above a specified entry field. This is suitable for hidden fields, or multiple fields in an entry group, for instance address fields.
For ' hover' the input happens in a virtual field hovering above a specified entry field. This is suitable for non-editable standalone fields.
Suggestions are configured using this method:
export interface IDialerHooksSuggestionsResult {
    text: string;
    changes: ReadonlyArray<HookResult.ChangeEntry>;
  }
  export interface IDialerHooksSuggestionsConfig {
    leadDefinitionEntryId: Contracts.LeadDefinitionEntryId;
    title: string;
    placement: "in" | "hover" | "virtual";
    initialSearchText: (lead: Contracts.Lead) => string;
    search: (search: string) => PromiseLike<ReadonlyArray<IDialerHooksSuggestionsResult>>;
  }
  export type SuggestionConfig = (lead: Contracts.Lead) => ReadonlyArray<IDialerHooksSuggestionsConfig>;
addSuggestions(configure: SuggestionConfig): SmartHubConfig;
This can look daunting but broken down it isn't that bad, and in actual use it's pretty simple. See Custom price suggestions for a walkthrough of how it is used.

DAWA Address Lookup

A further set of extension for easy configuration of DAWA address lookup are:
export interface SmartHubConfig {
  addSuggestionForAddressEntryGroups(
      configurator: ConfigureAddressLookupForEntries,
  filter?: ((groupName: string) => boolean)): SmartHubConfig;
    addDAWASuggestionForAddressEntryGroups(
      filter?: ((groupName: string) => boolean)): SmartHubConfig;
    addDAWASuggestionForEntries(
      filter: ((e: Contracts.LeadDefinitionEntry) => boolean) ): SmartHubConfig;
    addDAWASuggestionForStandaloneAddressEntries(): SmartHubConfig;
    // Calls addDAWASuggestionForAddressEntryGroups and then addDAWASuggestionForStandaloneAddressEntries
    addDAWASuggestionForAddressEntries(): SmartHubConfig;
}
These extensions assume that proper semantic address types are used for the fields and that the fields are grouped appropriately in entry groups.
This is how to simply configure address lookup:
config.addDAWASuggestionForAddressEntries();
That's it. Auto-suggest will now appear on all address entries, or for all entry groups that represent addresses, as long as those entries are created with the correct types in the Lead definition.
To only add address lookup to entries in entry groups where the name starts with Invoice, enter the following:
config.addDAWASuggestionForAddressEntryGroups( groupName => groupName.startsWith( 'Invoice' ) );

Custom Price Suggestions

This example demonstrates how to provide custom product and price data in a suggestion dropdown.
// A custom function that looks up prices based on a search string
async function priceLookup(search: string): Promise<{
    code: string,
    price: string
  }[]>
  {
    // In this example the product data is hardcoded.
    // In actual use, the price data could be requested from a service live, or once
    // on lead load and then just filtered here.
    return [
      { code: 'Prod A', price: '100' },
      { code: 'Prod B', price: '200' }
    ];
  }
  // In this example suggestions are configured for products, each represented by a set of
  // entries grouped in an entryGroup.
  // This function returns the full configuration for such an entryGroup.
  function configurePriceLookupForGroup({ group, entries }: Contracts.LeadDefinitionEntryGroupWithEntries): IDialerHooksSuggestionsConfig {
    // Find the id of some entries in the group. These are the entries that should
    // be updated when a suggestion is picked.
    const codeId = entries.find(e => e.keyInEntryGroup === 'Code')!.id;
    const priceId = entries.find(e => e.keyInEntryGroup === 'Price')!.id;
    // The function to execute when the search text in the suggestion box changes
    var doPriceLookup = async (search) => {
      // Call the search function
      const matchingPrices = await priceLookup(search)
      // For each price returned
      return matchingPrices.map(p => ({
        // Some display text for this suggestion entry
        text: 'Some text for:' + p.code,
        // The change actions to execute when the suggestion is selected
        changes: [
          HookResult.changeEntry({ entryId: codeId, newValue: p.code }),
          HookResult.changeEntry({ entryId: priceId, newValue: p.price })
        ]
        }));
      };
    // This is how a single suggestion box is configured
    return {
      // A description for debug purposes. Will be logged to console when suggestion is configured.
      description: 'A description',
      // Where to place the suggestion
      placement: 'in',
      // How to label it if needed
      title: 'Product price lookup',
      // Which entry the suggestion is attached to
      leadDefinitionEntryId: codeId,
      // A function specifying the initial search text
      initialSearchText: lead => '',
      // The function to perform the search whenever the user enters more text.
      // The search returns the rows to display.
      // Each row is represented by the text to display and he ChangeEntry commands
      // to execute when the row is selected.      search: doPriceLookup
    };
  };
  // Adding suggestions for product entry groups
  // The function passed in here is run each time a new lead is loaded
  config.addSuggestions(l => {
    // CommonUtils.getEntryGroupsWithEntries is a helper function that returns entry groups,
    // along with the entries it contains that matches the given filter.
    // If no entries in a group matches the filter the group is ignored.
    // In this case we filter on the group name thus returning all groups named Product*, along    // with the entries in those groups.
    var groupsToAddSuggestionsTo = CommonUtils.getEntryGroupsWithEntries(
      l.campaign.leadDefinition, _ => _.entryGroup?.name.startsWith('Product') ?? false);
    // Run the configuration for each group
    return groupsToAddSuggestionsTo.map(configurePriceLookupForGroup);
})

Helper Functions


Mapping a Lead to a Custom Structure

When calling external services it's most often required to transform (map) the Lead into a different form.
Manually this can be done the following way:
      const manuallyMapped = {
	leadId: l.id,
	  data: {
	    a: l.getEntryValueByImportName('x'),
	    b: l.getEntryValueByImportName('y'),
    // or
            c: (l.entriesByImportName.get('z') || { value: undefined }).value,
            d: (l.entriesByImportName.get('w') || { value: undefined }).value
          }
  };
However the CommonUtils.mapLeadData function provides a simpler way.
CommonUtils.mapLeadData takes a Lead and a mapping object and returns an object that matches the mapping objects structure, populated with Lead data.
Using this helper the above example would be as following:
      const mapLeadDataEx1  = {
	leadId: l.id,	  data: CommonUtils.mapLeadData( l, {	    a: 'x',	    b: 'y',	    c: 'z',	    d: 'w'
  } )
  };
Some notes about the mapper object:
  • If a keys value is a string, then the value is used to look up by importName.
  • If a keys value is undefined, then the key itself is used to look up by importName.
  • If a keys value is a function, then it is assumed to take a lead as parameter and the result is assigned to the output property.
  • If a keys value is an object, then it will be directly assigned to the output property.
CommonUtils.mapGroupData is another helper function. It allows a mapping similar to mapLeadData, but operating on entry group level instead of lead level, and is useful for mapping flat structures to nested structure.
In this example, multiple products are mapped to an array:
       const productMapping = {
	code: 'Code',
	 unitsSold: '# Units',
	  totalPrice: (g) => g['# Units'] * g['Unit price']
		}
	const productGroups = l.campaign.leadDefinition.entryGroups.filter(g => g.name.startsWith('Product')).map(g => g.name);
	const mapLeadDataEx2 = {
	  leadId: l.id,
	  data: CommonUtils.mapLeadData(l, {
	    a: 'x',
	    b: 'y',
	    products: productGroups.map(groupName => CommonUtils.mapGroupData(l, groupName, productMapping))
  })
 };

Contracts

export enum SemanticEntryType {

  ShortText = 'ShortText',
  LongText = 'LongText',
  Time = 'Time',
  Date = 'Date',
  DateAndTime = 'DateAndTime',
  Integer = 'Integer',
  Decimal = 'Decimal',
  Bool = 'Bool',
  Phone = 'Phone',
  Email = 'Email',
  PickList = 'PickList',
  ZipCode = 'ZipCode',
  Notes = 'Notes',
  SocialSecurityNumber = 'SocialSecurityNumber',
  BankRegistrationNumber = 'BankRegistrationNumber',
  BankAccountNumber = 'BankAccountNumber',
  BankRegistrationAccountNumber = 'BankRegistrationAccountNumber',
  Revenue = 'Revenue',
  Profit = 'Profit',
  Commission = 'Commission',
  Points = 'Points',
  MainClosure = 'MainClosure',
  SubClosure = 'SubClosure',
  URL = 'URL',
  Label = 'Label',
  BookingEntry = 'BookingEntry',
  BookingCalendarCode = 'BookingCalendarCode',
  BookingTime = 'BookingTime',
  UnitPrice = 'UnitPrice',
  NUnitsSold = 'NUnitsSold',
  Birthday = 'Birthday',
  FullName = 'FullName',
  FirstName = 'FirstName',
  LastName = 'LastName',
  Address = 'Address',
  Country = 'Country',
  City = 'City',
  City2 = 'City2',
  Street = 'Street',
  BuildingNumber = 'BuildingNumber',
  ApartmentNumber = 'ApartmentNumber',
  ApartmentDoor = 'ApartmentDoor',
  SensitiveInteger = 'SensitiveInteger',
  SensitiveText = 'SensitiveText',
  SubscriptionStartDate = 'SubscriptionStartDate',
}
export enum EntryCategory {
  MasterData = 'MasterData',
  ResultData = 'ResultData',
}
export enum CampaignType {
  Master = 'Master',
  PowerDialer = 'PowerDialer',
  BasketDialer = 'BasketDialer',
  PredictiveDialer = 'PredictiveDialer',
}
export enum LeadSaveStatus {
  RedialAutomatic = 1,
  RedialManualCommon = 2,
  RedialManualPrivate = 3,
  VIPRedial = 10,
  FollowUpRedial = 20,
  ValidationAwaiting = 105,
  AwaitingExternalEvents = 110,
  OrderConfirmationReadyFor = 115,
  OrderConfirmationAwaiting = 116,
  OrderConfirmationFailed = 117,
  // OrderConfirmationExpired = 118,
  PaymentReadyFor = 120,
  PaymentAwaiting = 121,
  PaymentFailed = 122,
  // PaymentExpired = 123,
  MeetingFeedbackAwaiting = 125,
  UserProcessed = 200,
  Removed = 600,
  // For now we don't support other statuses here, but other that might make sense is:
  // Depleted = 210,
  // Failed = 400,
  // Removed = 600,
  // Anonymized = 610,
  // DoNotCall = 700,
}
export enum LeadStatus {
  New = 0,
  RedialAutomatic = 1,
  RedialManualCommon = 2,
  RedialManualPrivate = 3,
  VIPRedial = 10,
  FollowUpRedial = 20,
  ValidationAwaiting = 105,
  AwaitingExternalEvents = 110,
  OrderConfirmationReadyFor = 115,
  OrderConfirmationAwaiting = 116,
  OrderConfirmationFailed = 117,
  // OrderConfirmationExpired = 118,
  PaymentReadyFor = 120,
  PaymentAwaiting = 121,
  PaymentFailed = 122,
  // PaymentExpired = 123,
  MeetingFeedbackAwaiting = 125,
  Unresolved = 150,
  UserProcessed = 200,
  Depleted = 210,
  Abandoned = 300,
  Failed = 400,
  Expired = 500,
  Removed = 600,
  Anonymized = 610,
  DoNotCall = 700,
  GlobalDoNotCall = 750,
}
export enum LeadClosure {
  NotSet = 0,
  Success = 1,
  NotInterested = 2,
  InvalidLead = 3,
  Unqualified = 4,
}
export enum DataType {
  ShortText = 'varchar',
  LongText = 'text',
  Time = 'time',
  Date = 'date',
  DateTime = 'datetime',
  Integer = 'int',
  Decimal = 'decimal',
  Bool = 'bool',
  Phone = 'phone',
  Email = 'email',
  Set = 'set',
  Url = 'url',
  Label = 'label',
}
export interface LeadDefinitionEntryGroup extends Readonly<{
  id: string;
  name: string;
  style: string;
}> {}
export interface LeadDefinitionEntryOption extends Readonly<{
  name: string;
  value: string;
  closure?: LeadClosure;
  leadClosureCondition?: LeadClosure;
}> { }
export type ImportName = string;
export type LeadId = string;
export type CampaignId = string;
export type LeadDefinitionEntryId = string;
export type LeadDefinitionEntryGroupId = string;
export interface LeadDefinitionEntry extends Readonly<{
  id: LeadDefinitionEntryId;
  displayName: string;
  importName: ImportName;
  entryCategory?: EntryCategory;
  requiredOnClosure: boolean;
  isEditable: boolean;
  isDefaultVisible: boolean;
  isRestricted: boolean;
  valueOptions: ReadonlyArray<LeadDefinitionEntryOption>;
  keyInEntryGroup?: string;
  entryGroup?: LeadDefinitionEntryGroup;
  entryGroupId?: number;
  entryGroupDataLookupType?: string;
  semanticType?: SemanticEntryType;
  dataType: DataType;
  typeFormat: string;
  // LeadDefinitionEntry has no value, LeadEntry holds the value
}> { }
export interface LeadDefinitionEntryGroupWithEntries extends Readonly<{
  group: LeadDefinitionEntryGroup;
  entries: ReadonlyArray<LeadDefinitionEntry>;
}> { }
export interface LeadDefinition extends Readonly<{
  id: string;
  name: string;
  entryGroups: ReadonlyArray<LeadDefinitionEntryGroup>;
  entryDefinitions: ReadonlyArray<LeadDefinitionEntry>;
  entryDefinitionsById: ReadonlyMap<LeadDefinitionEntryId, LeadDefinitionEntry>;
}> { }
export interface Project extends Readonly<{
  id: string;
  name: string;
}> { }
export interface Campaign extends Readonly<{
  id: CampaignId;
  name: string;
  code: string;
  info?: string;
  manuscripts: ReadonlyArray<IContactManuscript>;
  leadDefinition: LeadDefinition;
  project: Project;
}> { }
export interface LeadEntry extends LeadDefinitionEntry {
  readonly value: string;
  readonly isValid: boolean;
  readonly isVisible: boolean;
}
export interface BaseLead extends Readonly<{
  id: string;
  uniqueId: string;
  campaign: Campaign;
}> { }
// Represents the current intermediate state of the lead.
export interface LeadState extends Readonly<{
  oldStatus: LeadStatus;
  newStatus: LeadStatus;
  newClosure: LeadClosure;
  leadReadyState: LeadReadyState;
}> {}
export interface Lead extends BaseLead, Readonly<{
  entriesById: ReadonlyMap<LeadDefinitionEntryId, LeadEntry>;
  entriesByImportName: ReadonlyMap<string, LeadEntry>;
  entries: ReadonlyArray<LeadEntry>;
  getEntryValueById(id: LeadDefinitionEntryId): string | undefined;
  getEntryValueByImportName(importName: string): string | undefined;
}> { }
export interface LeadEntryForSaving extends Readonly<{
  leadDefinitionEntryId: LeadDefinitionEntryId;
  value: string;
}> {}
export interface OrganizationalUnit {
  uniqueId: string;
  orgCode: string;
  name: string;
}
export enum Rights {
  AllowRestrictedEntries = 'AllowRestrictedEntries',
}
export interface CurrentUser extends Readonly<{
  uniqueId: string;
  organization: ReadonlyArray<OrganizationalUnit>;
  orgCode: string;
  username: string;
  name: string;
  rights: {
    [rightName in Rights]: Boolean;
  };
  pageLayout: 'natural' | 'reverse';
  leadEntryLayout: 'normal' | 'compact' | 'loose';
}> {}
export enum LeadReadyState { NoLead, Loaded, Active }
export enum PageMode { Dialer = 0, Edit = 1, New = 2, Preview = 3 }
<br>

Hook Results

export enum ResultAction {
  Message = 'message',
  ChangeEntry = 'changeEntry',
  SaveLead = 'saveLead',
  AbortSave = 'abortSave',
  Nop = 'nop',
  Error = 'error',
}
export interface MessageParams {
  text: string;
  // data?: any;
}
export interface Message extends MessageParams {
  kind: ResultAction.Message;
}
export function message(params: MessageParams): Message {
  return {
    kind: ResultAction.Message,
    ...params,
  };
}
export interface ErrorParams {
  text: string;
  data?: any;
}
export interface Error extends ErrorParams {
  kind: ResultAction.Error;
}
export function error(params: ErrorParams): Error {
  return {
    kind: ResultAction.Error,
    ...params,
  };
}
interface ChangeEntryParams {
  leadId?: Contracts.LeadId;
  entryId: Contracts.LeadDefinitionEntryId;
  newValue?: string;
  isValid?: boolean;
  isVisible?: boolean;
  isRequired?: boolean;
  filterValueOptions?: ReadonlyArray<string> | ((option: string) => boolean);
}
export interface ChangeEntry extends ChangeEntryParams {
  kind: ResultAction.ChangeEntry;
}
export function changeEntry(params: ChangeEntryParams): ChangeEntry {
  return {
    kind: ResultAction.ChangeEntry,
    ...params,
  };
}
export interface AbortSaveParams {
  reason: string;
}
export interface AbortSave extends AbortSaveParams {
  kind: ResultAction.AbortSave;
}
export function abortSave(params: AbortSaveParams): AbortSave {
  return {
    kind: ResultAction.AbortSave,
    ...params,
  };
}
export interface SaveLeadParams {
  status?: Contracts.LeadSaveStatus;
  closure?: Contracts.LeadClosure;
  nextDialTime?: Date;
  changedEntries?: ReadonlyArray<Contracts.LeadEntryForSaving>;
}
export interface SaveLead extends SaveLeadParams {
  kind: ResultAction.SaveLead;
}
export function saveLead(params: SaveLeadParams): SaveLead {
  return {
    kind: ResultAction.SaveLead,
    ...params,
  };
}
export interface Nop {
  kind: ResultAction.Nop;
}
export function nop(): Nop {
  return {
    kind: ResultAction.Nop,
  };
}
// export type Awaitable<T> = PromiseLike<T | Error>;
export type AlwaysAllowedResults = Nop | Message | Error;
export type EntryEventResults = AlwaysAllowedResults | ChangeEntry;
export type Executable =
  EntryEventResults
  | AbortSave
  | SaveLead;
export type HookEventHandlerResult<T extends Executable = Nop> =
  T | AlwaysAllowedResults | ReadonlyArray<T | AlwaysAllowedResults>;
export type AsyncResult<T extends Executable = Nop> =
  PromiseLike<T | AlwaysAllowedResults | ReadonlyArray<T | AlwaysAllowedResults>>;
export type AsyncHookEventHandlerResult<T extends Executable = Nop> =
  T | AlwaysAllowedResults
  | AsyncResult<T>
  | ReadonlyArray<T | AlwaysAllowedResults | AsyncResult<T>>;
```
# Entry group event results
```
export enum ResultAction {
  Nop = 'nop',
  Override = 'override',
  Data = 'data',
}
export type LookupDataHeader = Readonly<{
  // isVisible: boolean;
  displayName: string;
  keyInEntryGroup: string;
}>;
export type LookupData = Readonly<{
  dataByKeyInGroup: ReadonlyArray<Readonly<{[key: string]: string}>>;
  headers: ReadonlyArray<LookupDataHeader>;
}>;
export interface DataParams {
  data: Promise<LookupData>;
}
export interface Data extends DataParams {
  kind: ResultAction.Data;
}
export function data(params: DataParams): Data {
  return {
    kind: ResultAction.Data,
    ...params,
  };
}
export interface OverrideParams {
  campaignId?: Contracts.CampaignId;
  startDate?: Date;
}
export interface Override extends OverrideParams {
  kind: ResultAction.Override;
}
export function override(params: OverrideParams): Override {
  return {
    kind: ResultAction.Override,
    ...params,
  };
}
export interface Nop {
  kind: ResultAction.Nop;
}
export function nop(): Nop {
  return {
    kind: ResultAction.Nop,
  };
}
export type AllResults = Nop | Override | Data;
<br>

Still need help? Contact Us Contact Us