I posted this Dynamics 365 CRM Field Service Report I had built on the r/PowerApps subreddit it blew up and got 15k views. I received a lot of postivie feedback.

Field Service Report preview

Sample Field Service Report


Here’s a glimpse of what people had to say:

u/MadeInWestGermany
I think it‘s exactly what most clients wish for… Great work👍

u/mrf1uff1es
I thought I was looking at Crystal reports in this subreddit. Nice job.

And many others asked how they could build the same:

u/malhosainy
That’s a good looking report, I appreciate more details on how is that done, or if there is a tutorial to create a similar report. Thanks!

u/Franki3B_
Great job. Could you share some of your steps of how you got from the start to this?

u/National_Ad_3995_
Love the report layout and the dual signature. This report covers a lot of bases clients request. Any chance you would be able to share a sample of your multiple columns repeating layout?


Let’s build it together

🏁 Before You Start

Who this guide is for:
This guide is for Dynamics 365 consultants and developers with some React/TypeScript familiarity who want to customize Field Service reports quickly without rebuilding from scratch.

In this guide, I’ll show you how to take Microsoft’s sample Field Service report solution and customize it to your own data and layout needs inside Dynamics 365 CRM without rebuilding everything from scratch. You’ll learn how to set up the base solution quickly and understand where the main pieces of logic are so you can adapt them for your own scenarios.

⚠️ What This Guide Covers (and Doesn’t)

This isn’t a deep dive into React, TypeScript, or CSS styling. The goal here isn’t to teach frontend development, but to highlight which parts of the report’s code are safe to modify and how to hook in your own data from Dataverse using Xrm.WebApi.

Introduction & Installation

First of all you need to start with the most important steps from the official microsoft tutorial which I’m also including here:

  1. Download the official ReportingSolution_managed.zip

  2. Import the reporting solution into your environment. The import installs a reporting form, a command for the command bar, and includes a sample report. We recommend importing the solution as a managed solution.

  3. Find the Field Service Mobile app module in your list of Dynamics 365 apps and select the ellipsis (…) > Open in App Designer.

  4. In the navigation, select the Bookings form.

  5. On the right side pane, select the ellipsis (…) for the Reporting form and select Add. This step enables the Reporting form for the Bookable Resource Booking entity.

  6. Select Save & Publish.

  7. Test the sample reports


Download the source code

Now that you’ve imported and tested the solution, we can begin by downloading the official source code of the solution, you can do this manually or, if you prefer, use Git to clone the repo.

Whichever method you decide to use just make sure to navigate to "Field Service/Component Library/FSMobile" and extract the Service Report folder.

🛠 Customization Scope

In this section, we’ll look at where the report’s logic lives and how to safely plug in your own data without touching the report’s styling or layout code.

Customizing the control

Prerequisites

Initial Run & Test

Navigate into SERVICE REPORT/Controls/ReportPreview and from the terminal run npm install after you’re done installing, run npm start then navigate tohttp://localhost:8181/and you should see the PowerApps component framework Test Environment along with your Contoso Service Report.

This is what you should see

Contoso Service Report preview

Contoso Service Report

Understanding the file structure

📁 SERVICE REPORT
  📁 Controls
    📁 ReportPreview
      📁 DataProviders              # Queries to fetch data
      📁 models                     # Define data models
      📁 SampleReport               # Service Report code
      📄 ControlManifest.Input.xml  # PCF configuration file
      📄 index.ts                   # Entry point passing data to report
      📄 ReportViewer.tsx
  📁 solutions                      # Built solutions
  📄 README.md
  📄 .gitignore
  📄 dirs.proj

With the above structure in mind we can start customizing the report, I’m not going to simply give you the exact code I have because it’s specific to my case and not yours, but I’ll show you what you can do to adapt the report to your use case.

TheControlManifest.Input.xmlfile is the most important, it defines the data, resources and features that are passed down from the Dynamics FORM to the Report. If you want to learn more about it you can learn here . In short:

  1. Thecontrolelement defines the component’s namespace, version, and display information, it contains a very important tag calledversionwhich you need to increment each time you build your code in a solution ready to be published.
  2. The<property>element defines a specific, configurable piece of data that the component expects from the Microsoft Dataverse, so if you want another signature field you can simply define it here and then pass it down from the Reporting Form to the Report.
  3. Theresourceselement refers to the resource files that component requires to implement it’s visualization.
  4. Thefeature-usageelement acts as a wrapper around the uses-feature elements.
  5. Theuses-featureindicates which feature the code components want to use such as Utility, WebApi and more.

TheDataProviders/GetReportData.tsfile contains the code used for fetching data from Dataverse, if you want to add queries or modify existing ones you need to have knowledge of Xrm.WebApi or you can use a tool like Dataverse REST Builder to simplify writing queries for you.

Before making changes to a query or adding a new one you need to modify or add the appropriate model which represents the structure of a record from Dataverse in ReportPreview/models, so if I were to add a query to fetch user data then I would start by adding a new model class called User.

Once that is done, in the case of a new query, you need to call your new function when the report is initially loaded and that is done inside the getDataFetchPromises() function in index.ts which passes the data to the SampleReport.tsx component.

If you have knowledge of React, Typescript and Xrm.WebApi you can proceed on your own and skip to Package & publish., if not I’m going to show you a very basic example of adding a new model, writing the query which returns data based on that model and then rendering that data in the report.

You can experiment with the sample queries and learn a lot, so I’m going to show you something different, what if I need to display the current user’s data, how would I go about doing it ?

Displaying current user’s data

  1. Define the data we need
  2. Retrieve the current user’s ID
  3. Fetch User Data
  4. Display it

Define the data model

Write down the logical names of the fields you want to display then go into theReportPreview/models/ReportViewerModel.tsfile and define the User & BusinessUnit classes.

export class User {
  fullname: string;
  email: string;
  businessUnit: BusinessUnit;
}

export class BusinessUnit {
  divisionName: string;
  phoneNumber: string;
}

Retrieve the current user’s ID

Navigate to theReportPreview/DataProviders/GetReportData.tsfile, where you’ll find the GetReportData class, using one of the attributes of this class we can gain direct access to the user’s ID.


export class GetReportData {
  private bookingID: string;
  private workOrderID: string;

  constructor(private context: ComponentFramework.Context<IInputs>) {
    this.bookingID = context.parameters?.BookingId?.formatted;
    const workOrder = context.parameters?.WorkOrder?.raw;
    this.workOrderID = workOrder ? workOrder[0]?.id : undefined;
  }
  /*
  The "private context" inside the constructor is a TypeScript shortcut which:
  1. Declares a private property called context on your class
  2. Assigns the passed argument to it.

  So effectively, it’s the same as if you had written:

  private context: ComponentFramework.Context<IInputs>;
  constructor(context: ComponentFramework.Context<IInputs>) {
    this.context = context;
  }
  */

  // Inside the context we have a userSettings field and we can use it to retrieve the current user's ID.
  // Keep this in mind since we're not going to write this in a separate function but we're going to use it in STEP 3
  const currentUserId = this.context.userSettings.userId;
}
...

Fetch User Data

Let’s write our new query function inReportPreview/DataProviders/GetReportData.ts

  public getCurrentUserData = async (): Promise<User> => {

    // retrieve the current user's ID from the context.userSettings object as seen in Step 2
    const currentUserId = this.context.userSettings.userId;

    // execute the query and fetch
    const userData = await this.context.webAPI.retrieveRecord(
      "systemuser",
      currentUserId,
      "?$select=fullname,internalemailaddress,_businessunitid_value&$expand=businessunitid($select=divisionname,address1_telephone1)"
    );

    // Define empty User object
    let user: User = {
      fullname: "",
      email: "",
      businessUnit: {
        divisionName: "",
        phoneNumber: "",
      },
    };

    // assign data to user object if successfully fetched
    if (userData) {
      user = {
        fullname: userData.fullname,
        email: userData.internalemailaddress,
        businessUnit: {
          divisionName: userData.businessunitid?.divisionname ?? "",
          phoneNumber: userData.businessunitid?.address1_telephone1 ?? "",
        },
      };
    }

    return user;
  };

Display User’s data

Navigate inside theReportPreview/models/ReportViewerModel.tsfile and add a User field to the ReportViewerProps interface

export interface ReportViewerProps {
  booking: Booking;
  serviceInfo: ServiceInfo;
  context: ComponentFramework.Context<IInputs>;
  products: Array<Product>;
  servicetasks: Array<ServiceTask>;
  services: Array<Service>;
  user: User; // <<-- new
  signature: string;
  isSpinnerVisible: boolean;
}

Then navigate inside theReportPreview/index.tsfile and add initialize theuserprop as undefined.

this.props = {
  user: undefined, // <<---
  booking: undefined,
  serviceInfo: undefined,
  products: [],
  servicetasks: [],
  services: [],
  signature: this.signature,
  context: context,
  isSpinnerVisible: false,
};

Find thegetDataFetchPromises() function inside the same file and add call the getCurrentUserData() like so

const updateUserData = dataGetter.getCurrentUserData().then(
  (user) => {
    this.props.user = user;
    this.renderReportViewer(this.props);
  },
  (err) => {
    // eslint-disable-next-line no-console
    console.log("Failed to retrieve current user's data: ", err);
  }
);
...
return [
  updateUserData,     // <<--
  updateBookingData,
  updateProductData,
  updateTasksData,
  updateServiceInfo,
  updateServicesData,
];

Navigate inside theReportPreview/SampleReport/SampleReport.tsxand add a user as prop of the SampleReport component.

export default {
  booking,
  products,
  servicetasks,
  serviceInfo,
  signature,
  services,
  user, // <<--
};

And in the end go inside theReportViewer.tsxfile and add the user as a prop for the SampleReport component.

<SampleReport
  booking={this.props.booking}
  products={this.props.products}
  servicetasks={this.props.servicetasks}
  serviceInfo={this.props.serviceInfo}
  signature={this.props.signature}
  user={this.props.user} // <<--
  {...this.props}
/>

Now we can finally get started with displaying the data, considering that the current user whose data we just fetched is an approver we can add their data under the signature just like so:

<div style={{ marginTop: "40px" }}>
  <SectionTitle>Approved By</SectionTitle>
  <FieldInfo
    name="Name"
    value={user?.fullname}
    valueStyles={styles.singleColValue}
  />
  <FieldInfo
    name="Email"
    value={user?.email}
    valueStyles={styles.singleColValue}
  />
  <FieldInfo
    name="Business Unit"
    value={user?.businessUnit?.divisionName}
    valueStyles={styles.singleColValue}
  />
  <FieldInfo
    name="Phone"
    value={user?.businessUnit?.phoneNumber}
    valueStyles={styles.singleColValue}
  />
</div>

Now we can proceed with building the PCF using thenpm startcomand and this would be the result.

Contoso Report After Changes

Contoso Field Service Report v2

Package & Publish

You can follow this official tutorial from Microsoft

At this point, you’ve seen how to take Microsoft’s sample Field Service report, understand its structure, and extend it with your own data and logic. The beauty of this approach is that you don’t need to rebuild everything from scratch you can focus on the parts that matter most to your business case.

Enjoy!