Skip to main content

Extension

This page shows you how to build your own AL extension on top of the standard Dime.Scheduler connector for Business Central, so you can add customer-specific logic without touching the connector itself.

The pattern is like Matryoshka dolls, where each doll fits inside a larger one. You build on functionality that already exists and layer your own logic on top. The result is a clean stack where your extension holds only the business logic for one specific purpose.

The Dime.Scheduler extension for Business Central is deliberately thin. It adds just the components you need to get started quickly. The experience is batteries-included, and it was built to be extended by partners.

Requirements

To extend the connector, create a blank extension or reuse one you already have. You will need:

These two are optional, but we recommend them:

Adding the dependency

Declare the standard Dime.Scheduler extension as a dependency in your app.json file. Add Dime.Scheduler to the dependencies array:

"dependencies": [{
"id": "f15bbe6d-c2a3-4828-aa98-5a77cff4cd41",
"publisher": "Dime",
"name": "Dime.Scheduler",
"version": "2.3.0.0"
}
]

Here is a typical example of a complete app.json that we create for our customers:

{
"id": "d9c75381-bb78-4de7-81d3-7db784177465",
"name": "Customer XYZ Dime.Scheduler Customization",
"publisher": "Dime Software",
"version": "1.0.0.0",
"brief": "This extension adds custom behavior for Customer XYZ to the standard Business Central Dime.Scheduler connector",
"description": "This extension adds custom behavior for Customer XYZ to the standard Business Central Dime.Scheduler connector",
"privacyStatement": "https://www.dimescheduler.com/privacy",
"EULA": "https://docs.dimescheduler.com/",
"help": "https://docs.dimescheduler.com/",
"url": "https://www.dimescheduler.com/",
"logo": "./assets/logo.png",
"dependencies": [{
"id": "f15bbe6d-c2a3-4828-aa98-5a77cff4cd41",
"publisher": "Dime",
"name": "Dime.Scheduler",
"version": "2.3.0.0"
}
],
"screenshots": [],
"platform": "21.0.0.0",
"application": "21.0.0.0",
"idRanges": [
{
"from": 50000,
"to": 50200
}
],
"contextSensitiveHelpUrl": "https://docs.dimescheduler.com/",
"resourceExposurePolicy": {
"allowDebugging": true,
"allowDownloadingSource": false,
"includeSourceInSymbolFile": false
},
"runtime":"10.0"
}

Custom development

What you build next is up to you. You might add new tables, extend existing ones, or add your own code unit to handle incoming appointments from Dime.Scheduler. The sections below walk through a few examples to show what extending the connector looks like.

Extend Dime.Scheduler artifacts

The standard connector adds only a handful of new objects to Business Central. One essential object is the setup page, which exposes the functionality to link BC tables to Dime.Scheduler. You can add your own fields to this table. Extending Dime.Scheduler's own artifacts is optional, but you are free to do so as you see fit.

Here is an example that adds a few fields to the service order module on the setup page:

tableextension 50161 "MYCUSTOMER DS Setup" extends "Dime DS Setup"
{
fields
{
field(50000; "MYCUSTOMER DS Initial Repair Status"; Code[10])
{
Caption = 'Initial Repair Status Code';
TableRelation = "Repair Status".Code;
ValidateTableRelation = true;
}
field(50001; "MYCUSTOMER DS Planned Repair Status"; Code[10])
{
Caption = 'Planned Repair Status Code';
TableRelation = "Repair Status".Code;
ValidateTableRelation = true;
}
field(50002; "MYCUSTOMER DS Replan Repair Status"; Code[10])
{
Caption = 'Replan Repair Status Code';
TableRelation = "Repair Status".Code;
ValidateTableRelation = true;
}
}
}

pageextension 50161 "MYCUSTOMER DS Setup" extends "Dime DS Setup"
{
layout
{
addafter(Testing)
{
group(MYCUSTOMER)
{
Caption = 'MYCUSTOMER';

field("MYCUSTOMER DS Initial Repair Status"; Rec."MYCUSTOMER DS Initial Repair Status")
{
ApplicationArea = All;
}
field("MYCUSTOMER DS Planned Repair Status"; Rec."MYCUSTOMER DS Planned Repair Status")
{
ApplicationArea = All;
}
field("MYCUSTOMER DS Replan Repair Status"; Rec."MYCUSTOMER DS Replan Repair Status")
{
ApplicationArea = All;
}
}
}
}
}

On its own, this does not change the message flow from BC to Dime.Scheduler. To send the extra information across, you tap into the right events. See the list of events and the event listener example below for how to send additional information to Dime.Scheduler.

Extend standard tables

A common pattern is to add additional configuration to standard objects like the Resource Card.

pageextension 50160 "MYCUSTOMER DS Resource Card" extends "Resource Card"
{
layout
{
addafter("Dime DS Enable Exchange")
{
field("MYCUSTOMER DS Planning Duration"; Rec."MYCUSTOMER DS Planning Duration")
{
ApplicationArea = All;
}
}
}
}

Subscribe to events

Here is a code unit that listens to a few events that Dime.Scheduler raises:

  • OnBeforeSendResourceAsResourceToDimeScheduler adds logic to control which appointments are to be sent to Dime.Scheduler.
  • OnBeforeSendServiceHeaderAsJobToDimeScheduler maps custom fields to Dime.Scheduler free fields.
  • OnBeforeSendServiceItemLineAsTaskToDimeScheduler adds a field to one of Dime.Scheduler's free fields.
codeunit 50305 "DS MYCUSTOMER Events Subscriber"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Dime DS Events", 'OnBeforeSendResourceAsResourceToDimeScheduler', '', false, false)]
local procedure FilterOnResourceGroup(var Resource: Record Resource; var DsWebServMgt: Codeunit "Dime DS Web Service Management")
var
ResourceGroup: Record "Resource Group";
Employee: Record "Employee";
DimeDSDimeSchedulerMgt: Codeunit "Dime DS Dime.Scheduler Mgt.";
NotSendResourceGroup: Boolean;

begin
if ResourceGroup.Get(Resource."Resource Group No.") then
NotSendResourceGroup := not (ResourceGroup."DS MYCUSTOMER Send To DS")
else
NotSendResourceGroup := true;

DsWebServMgt.ReplaceParameter('DoNotShow', DSWebServMgt.HandleBool((Resource.Blocked) or (Resource."Dime DS Hide In Planning Board") or (NotSendResourceGroup)));

// If there's no employee, use the resource information
Employee.SetRange("Resource No.", Resource."No.");
if not Employee.FindFirst() then begin
DSWebServMgt.ReplaceParameter('HomeAddress', Resource.Address + ', ' + Resource."Address 2" + ', ' + Resource."Post Code" + ', ' + Resource.City);
DSWebServMgt.ReplaceParameter('HomeCountry', DimeDSDimeSchedulerMgt.GetCountryCode(Resource."Country/Region Code"));
DSWebServMgt.ReplaceParameter('InServiceFrom', DSWebServMgt.HandleDate(Resource."Employment Date"));
DsWebServMgt.ReplaceParameter('Email', Resource."DS MYCUSTOMER Exchange Email");
DsWebServMgt.ReplaceParameter('ExchangeIntegrationEnabled', DSWebServMgt.HandleBool(Resource."DS MYCUSTOMER Enable Exchange"));
end;
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Dime DS Events", 'OnBeforeSendServiceHeaderAsJobToDimeScheduler', '', false, false)]
local procedure SendServiceHeader(var DsWebServMgt: Codeunit "Dime DS Web Service Management"; var ServiceHeader: Record "Service Header")
begin
DsWebServMgt.ReplaceParameter('FreeText1', ServiceHeader.SomeCustomField);
DsWebServMgt.ReplaceParameter('FreeDate1', DsWebServMgt.HandleDate(ServiceHeader."Some Random Date"));
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Dime DS Events", 'OnBeforeSendServiceItemLineAsTaskToDimeScheduler', '', false, false)]
local procedure SendServiceItemLine(var DsWebServMgt: Codeunit "Dime DS Web Service Management"; var ServiceItemLine: Record "Service Item Line")
begin
DsWebServMgt.ReplaceParameter('FreeText1', ServiceItemLine."Service Item Group Code");
end;
}

Handling incoming appointments

Getting data from Dime.Scheduler back into BC takes very little Dime.Scheduler knowledge. When the planner changes the schedule, Dime.Scheduler posts the appointment to the Dime.Scheduler Appointments table. Event listeners are ready to pick up that data and process it into the right table.

Here is skeleton code that outlines the first stages of a code unit that processes incoming appointments from Dime.Scheduler. This one processes appointments as service order allocation lines, but you can do whatever you need with it.

codeunit 50094 "MYCUSTOMER DS Functions"
{
TableNo = "Dime DS Appointment";

trigger OnRun()
begin
PerformAllocation(Rec);
end;

local procedure PerformAllocation(DimeDSAppointment: Record "Dime DS Appointment")
var
DimeDSAppointmentResource: Record "Dime DS Appointment Resource";
DimeDSServOrderAllocLnk: Record "Dime DS Serv. Order Alloc. Lnk";
DimeDSSetup: Record "Dime DS Setup";
ServHeader: Record "Service Header";
ServItemLine: Record "Service Item Line";
ServOrdAllocation: Record "Service Order Allocation";
ServAllocMgt: Codeunit ServAllocationManagement;
DimeDSHandleTimeSheet: Codeunit "Dime DS Handle Time Sheet";
LineNo: Integer;
OrderNotFoundErr: Label 'Service Order "(%1)" could not be found.', Comment = '%1 = Service Order No.';
OrderStatusErr: Label 'You cannot allocate a resource to the service order %1 because it is %2.', Comment = '%1 = Service Order Number ; %2 = Status';
AllocationExists: Boolean;
begin
// YOUR LOGIC GOES HERE

// TYPICAL STRUCTURE
// 1. VALIDATE ITEM IN BC
// 2. CHECK WHAT KIND OF PLANNING WAS DONE: DimeDSAppointment."Database Action" CAN BE "I" (insert), "U" (update), or "D" (delete)
// 3. PROCESS ACCORDINGLY SUCH AS CREATING AN ALLOCATION LINE, OR WHEN THAT ALREADY EXISTS, ENSURE A LINK IS ESTABLISHED BETWEEN THE ITEM IN BC AND DIME.SCHEDULER (e.g. "Dime DS Job Planning Line Link","Dime DS Serv. Order Alloc. Lnk")
end;
}

You may want to pair this code unit with a permission set:

permissionset 50002 "MYCUSTOMER DS ALL"
{
Assignable = true;
Caption = 'Dime.Scheduler MYCUSTOMER customizations', Locked = true;
Permissions = codeunit "MYCUSTOMER DS Functions" = X;
}

The Dime.Scheduler Web Service code unit

Every call to the Dime.Scheduler API goes through the Dime DS Web Service Management code unit, as you have seen in the earlier samples. When you use the default workflow and only need minor customizations, you can end up with tiny solutions like this:

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Dime DS Events", 'OnBeforeSendServiceItemLineAsTaskToDimeScheduler', '', false, false)]
local procedure SendServiceItemLine(var DsWebServMgt: Codeunit "Dime DS Web Service Management"; var ServiceItemLine: Record "Service Item Line")
begin
// Replace
DsWebServMgt.ReplaceParameter('FreeText1', ServiceItemLine."Service Item Group Code");

// Add
DSWebservMgt.AddParameter('BillNo', ServiceHeader."Bill-to Customer No.");
end;

Subscribe to the right event, find the field you want to modify, and make the change in code.

The approach is the same if you want to deviate from the standard connector altogether. Fetch the data from BC and use the Dime DS Dime.Scheduler Mgt. code unit. Each call to AddParameter adds a key/value pair to the API call and passes it to the import procedure you specify in the CallDimeSchedulerWS method call.

Below is a realistic example that overrides the standard logic. It maps custom fields to Dime.Scheduler's free fields (see the highlighted rows) and plans at the service item level instead of the default header level used in service orders.

procedure SendServiceItemLineAsTask(var ServiceItemLine: Record "Service Item Line")
var
ServiceHeader: Record "Service Header";
ServContractHdr: Record "Service Contract Header";
ServItem: Record "Service Item";
Customer: Record Customer;
DimeDSDocFilterValueMgt: Codeunit "Dime DS Doc. Filter Value Mgt.";
DimeDSDimeSchedulerMgt: Codeunit "Dime DS Dime.Scheduler Mgt.";
DSMYCUSTOMERMaterialStatusMgt: Codeunit "DS MYCUSTOMER Material Status Mgt.";
RecRef: RecordRef;
ProjectObject: Record "MYCUSTOMER Project Object";
ContactName: Text[100];
ContactPhone: Text[30];
ContactEmail: Text[80];
begin
DimeDSConnectorSetup.Get();
DimeDSSetup.Get();

if not ServiceHeader.Get(ServiceItemLine."Document Type", ServiceItemLine."Document No.") then
exit;

if not ServContractHdr.Get(ServContractHdr."Contract Type"::Contract, ServiceItemLine."Contract No.") then
ServContractHdr.Init();

if not ServItem.Get(ServiceItemLine."Service Item No.") then
ServItem.Init();

// Transfer the Filter Values for the Task - uses the DS Doc. Filter Value Sources setup
RecRef.GetTable(ServiceItemLine);
RecRef.SetRecFilter();
DimeDSDocFilterValueMgt.TransferDocFilterValues(RecRef);

DSWebservMgt.InitParameters();

// Mandatory fields
DSWebservMgt.AddParameter('SourceApp', DimeDSConnectorSetup."Source App");
DSWebservMgt.AddParameter('SourceType', DimeDSDimeSchedulerMgt.GetSourceType(DATABASE::"Service Header"));
DSWebservMgt.AddParameter('TaskNo', Format(ServiceItemLine."Line No.", 0, 9));
DSWebservMgt.AddParameter('JobNo', ServiceItemLine."Document No." + '_' + format(ServiceItemLine."Line No."));
DSWebservMgt.AddParameter('ShortDescription', '');

// Map the category visual indicator
case DimeDSSetup."Service - Default Category" of
DimeDSSetup."Service - Default Category"::" ":
DSWebservMgt.AddParameter('Category', '');
DimeDSSetup."Service - Default Category"::"Repair Status":
DSWebservMgt.AddParameter('Category', ServiceItemLine."Repair Status Code");
DimeDSSetup."Service - Default Category"::"Service Order Type":
DSWebservMgt.AddParameter('Category', ServiceHeader."Service Order Type");
DimeDSSetup."Service - Default Category"::"Service Zone":
DSWebservMgt.AddParameter('Category', ServiceHeader."Service Zone Code");
end;

// Map the time marker visual indicator
case DimeDSSetup."Service - Default Timemarker" of
DimeDSSetup."Service - Default Timemarker"::" ":
DSWebservMgt.AddParameter('TimeMarker', '');
DimeDSSetup."Service - Default Timemarker"::"Repair Status":
DSWebservMgt.AddParameter('TimeMarker', ServiceItemLine."Repair Status Code");
DimeDSSetup."Service - Default Timemarker"::"Service Order Type":
DSWebservMgt.AddParameter('TimeMarker', ServiceHeader."Service Order Type");
DimeDSSetup."Service - Default Timemarker"::"Service Zone":
DSWebservMgt.AddParameter('TimeMarker', ServiceHeader."Service Zone Code");
end;

DSWebservMgt.AddParameter('Subject', '');
DSWebservMgt.AddParameter('Body', '');
DSWebservMgt.AddParameter('AppointmentTemplate', DimeDSDimeSchedulerMgt.GetAppointmentTemplate(DATABASE::"Service Header"));

// Fields that affect the color of the pin on the map when displaying the address
DSWebservMgt.AddParameter('Pin', '');

DSWebservMgt.AddParameter('Description', ServiceItemLine.Description);
DSWebservMgt.AddParameter('Type', ServiceItemLine."Service Item Group Code");
DSWebservMgt.AddParameter('ServiceNo', ServiceItemLine."Service Item No.");
DSWebservMgt.AddParameter('ServiceGroup', ServItem."Service Item Group Code");
DSWebservMgt.AddParameter('ServiceClass', ServItem."Service Price Group Code");
DSWebservMgt.AddParameter('ServiceSerialNo', ServiceItemLine."Serial No.");
DSWebservMgt.AddParameter('ServiceName', ServItem."Item Description");
DSWebservMgt.AddParameter('IRISFault', ServiceItemLine."Fault Code");
DSWebservMgt.AddParameter('IRISSymptom', ServiceItemLine."Symptom Code");
DSWebservMgt.AddParameter('IRISArea', ServiceItemLine."Fault Area Code");
DSWebservMgt.AddParameter('IRISReason', ServiceItemLine."Fault Reason Code");
DSWebservMgt.AddParameter('IRISResolution', ServiceItemLine."Resolution Code");
DSWebservMgt.AddParameter('ContractNo', ServiceItemLine."Contract No.");
DSWebservMgt.AddParameter('ContractType', ServContractHdr."Serv. Contract Acc. Gr. Code");
DSWebservMgt.AddParameter('ContractDescription', ServContractHdr.Description);
DSWebservMgt.AddParameter('ContractStartDate', DSWebservMgt.HandleDate(ServContractHdr."Starting Date"));
DSWebservMgt.AddParameter('ContractEndDate', DSWebservMgt.HandleDate(ServContractHdr."Expiration Date"));
DSWebservMgt.AddParameter('PartsWarrantyStartDate', DSWebservMgt.HandleDate(ServiceItemLine."Warranty Starting Date (Parts)"));
DSWebservMgt.AddParameter('PartsWarrantyEndDate', DSWebservMgt.HandleDate(ServiceItemLine."Warranty Ending Date (Parts)"));
DSWebservMgt.AddParameter('LaborWarrantyStartDate', DSWebservMgt.HandleDate(ServiceItemLine."Warranty Starting Date (Labor)"));
DSWebservMgt.AddParameter('LaborWarrantyEndDate', DSWebservMgt.HandleDate(ServiceItemLine."Warranty Ending Date (Labor)"));

// Map importance Indicator to custom "Material Status" field
DsWebServMgt.AddParameter('Importance', Format(DSMYCUSTOMERMaterialStatusMgt.ConvertMaterialStatusIntoImportance(ServiceItemLine."Material Status")));

DSWebservMgt.AddParameter('Status', ServiceItemLine."Repair Status Code");
DSWebservMgt.AddParameter('ExpectedResponseDateTime', DSWebservMgt.HandleDateTime(CreateDateTime(ServiceItemLine."Response Date", 0T)));
DSWebservMgt.AddParameter('ActualResponseDateTime', DSWebservMgt.HandleDateTime(CreateDateTime(ServiceItemLine."Starting Date", 0T)));
DSWebservMgt.AddParameter('RequestedStartDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('RequestedEndDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('ConfirmedStartDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('ConfirmedEndDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('ActualStartDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('ActualEndDate', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('LocationDescription', ServItem."Location of Service Item");

// Fields that affect default duration and capacity
DSWebservMgt.AddParameter('DurationInSeconds', DimeDSDimeSchedulerMgt.ConvertDecimaltoSeconds(DimeDSSetup."Default Duration Service Line"));
DSWebservMgt.AddParameter('PlanningUOM', '');
DSWebservMgt.AddParameter('PlanningUOMConversion', Format(0, 0, 9));
DSWebservMgt.AddParameter('PlanningQty', Format(0, 0, 9));
DSWebservMgt.AddParameter('UseFixPlanningQty', DSWebservMgt.HandleBool(false));
DSWebservMgt.AddParameter('RoundToUOM', DSWebservMgt.HandleBool(false));

// Fields that control if the task is an open task
DSWebservMgt.AddParameter('IsComplete', DSWebservMgt.HandleBool(false));
DSWebservMgt.AddParameter('InfiniteTask', DSWebservMgt.HandleBool(false));
DSWebservMgt.AddParameter('TaskOpenAsOf', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('TaskOpenTill', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('RequiredTotalDurationInSeconds', Format(0, 0, 9));
DSWebservMgt.AddParameter('RequiredNoResources', Format(0, 0, 9));
DSWebservMgt.AddParameter('DoNotCountAppointmentResource', DSWebservMgt.HandleBool(false));

// Fields to define Resource Certificates
DSWebservMgt.AddParameter('CertificateNo', '');

DSWebservMgt.AddParameter('AppointmentEarliestAllowed', DSWebservMgt.HandleDateTime(0DT));
DSWebservMgt.AddParameter('AppointmentLatestAllowed', DSWebservMgt.HandleDateTime(0DT));

DSWebservMgt.AddParameter('FreeText1', ServiceItemLine."MYCUSTOMER Project Object No.");
DSWebservMgt.AddParameter('FreeText2', format(ServiceItemLine."MYCUSTOMER Project Object Type"));
DSWebservMgt.AddParameter('FreeText3', ServiceItemLine."Item No.");
DSWebservMgt.AddParameter('FreeText4', ServiceItemLine."MYCUSTOMER VIN");

ContactName := ServiceHeader."Ship-to Contact";
ContactPhone := ServiceHeader."Ship-to Phone";
ContactEmail := ServiceHeader."Ship-to E-Mail";
if ServiceItemLine."MYCUSTOMER Project Object No." <> '' then begin
ProjectObject.Get(ServiceItemLine."MYCUSTOMER Project Object No.");
if ProjectObject."Contact Name" <> '' then
ContactName := ProjectObject."Contact Name";
if ProjectObject."Contact Phone No." <> '' then
ContactPhone := ProjectObject."Contact Phone No.";
if ProjectObject."Contact E-mail" <> '' then
ContactEmail := ProjectObject."Contact E-mail";
end;

DSWebservMgt.AddParameter('FreeText6', ContactName);
DSWebservMgt.AddParameter('FreeText7', ContactPhone);
DSWebservMgt.AddParameter('FreeText8', ContactEmail);

// Sending Links with the Task - up to 3 are available in mboc_upsertTask, use mboc_upsertTaskUrl for more
RecRef.GetTable(ServiceHeader);
DSWebservMgt.AddParameter('url1', DimeDSDimeSchedulerMgt.GeneratePageUrl(RecRef, 5900));
DSWebservMgt.AddParameter('urldesc1', 'Service Order');

// DSWebservMgt.AddParameter('url2', DimeDSDimeSchedulerMgt.GeneratePageUrlWeb(RecRef,5900));
// DSWebservMgt.AddParameter('urldesc2', 'Service Order (Web)');

if Customer.Get(ServiceItemLine."Customer No.") then begin
RecRef.GetTable(Customer);
DSWebservMgt.AddParameter('url3', DimeDSDimeSchedulerMgt.GeneratePageUrl(RecRef, 21));
DSWebservMgt.AddParameter('urldesc3', 'Customer card');
end;

// Call the Dime.Scheduler web service will insert or update the Task
DSWebservMgt.CallDimeSchedulerWS('mboc_upsertTask');

end;

The data already lives in Business Central, Dime.Scheduler can hold it, and the connector gives you the tools to move it across. Your job is to identify and map the data so your customer can schedule with the information they need.

This is the signature of the code unit:

The code unit handles authentication (by default, a JSON Web Token is created and renewed when you run the FastTrack wizard), composes the request body, and posts the request to Dime.Scheduler's API. You only specify the right parameters and call the right procedure name. The code unit takes care of the rest.