Efficient Report Management: Save and Load Reports in Vue Pivot Table with SQL Server and Node.js Express Server

Efficient Report Management: Save and Load Reports in Vue Pivot Table with SQL Server and Node.js Express Server

·

15 min read


TL;DR: Integrate Syncfusion Vue Pivot Table with a Node.js Express server and MS SQL Server for efficient report management. Set up the SQL Server database and table, configure the Node.js server with necessary packages, and implement functions for saving, loading, renaming, and removing reports.

Managing reports in data-intensive apps can be challenging, mainly when dynamically saving and loading configurations. The Syncfusion Vue Pivot Table offers a comprehensive solution for creating interactive and robust pivot tables. When combined with a Node.js Express server, saving and loading reports becomes streamlined and efficient.

Let’s see how to save and load reports in the Vue Pivot Table using MS SQL Server as the backend database and Node JS Express for server-side logic. By the end, you’ll learn how to implement robust report management capabilities in your Vue.js apps.

Let’s get started!

Set up the MS SQL server database

First, we must configure the MS SQL server database to save and load the reports.

Step 1: Connect to the SQL Server Management Studio (SSMS)

First, open the SQL Server Management Studio and connect to your SQL server instance using the appropriate credentials.

Connecting to SQL Server

Step 2: Creating a new database

In the Object Explorer, right-click on Databases and select New Database.

Creating New Database

Now, enter a name for your database in the Database name field and click OK to create it.

Naming New Database

Step 3: Create a new table

Once the new database has been created, expand it, then right-click on the Tables and choose New-> Table.

Creating New Table

Define your table’s columns, including the column name, data type, and restrictions. Then, set up a primary key for the table if required.

Setting Table Columns

To save the table, click Save, enter a name when prompted, and click OK.

Saving the Table

To ensure your new table is created, open Object Explorer and expand the Tables folder under your Reports database. Refer to the following image.

Verifying Table Creation

Set up Node JS Express server

Let’s start by configuring a Node JS Express server. Open your terminal and execute the following commands.

mkdir server
cd server
npm init -y
npm install express mssql body-parser cors msnodesqlv8

Next, create a file named api.js and add the following code.

var express = require('express');
var bodyParser = require('body-parser');
var cors = require('cors');
var app = express();
var router = express.Router();

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cors());
app.use('/api', router);

// Define the port for the server here.
app.listen(5000, () => {
  console.log("Server is running on port 5000");
});

Then, create a file named dboperations.js and add the following code.

const sql = require('mssql/msnodesqlv8');
var dbConfig = {
    server: 'localhost',
    database: 'Reports',
    driver: "msnodesqlv8",
    options: {
        trustedConnection: true,
    }
}

Replace the server and database names in the above code with your MS SQL Server database information.

Then, in the dboperations.js file, we’ll add methods for save, save as, load, fetch, remove, and rename reports from the database.

[dboperations.js]

const sql = require('mssql/msnodesqlv8');
var dbConfig = {
    server: 'localhost',
    database: 'Reports',
    driver: "msnodesqlv8",
    options: {
        trustedConnection: true,
    }
}

async function SaveReportToDB(args) {
    try {
        let pool = await sql.connect(dbConfig);
        let isDuplicate = true;
        const request = pool.request();
        request.input('reportName', sql.VarChar, args.reportName)
        request.input('report', sql.VarChar, args.report)
        var reports = await getReports(pool);
        for (let i = 0; i < reports.length; i++) {
            if (reports[i]["ReportName"] === args.reportName) {
                isDuplicate = false;
                request.query('update ReportTable set Report=@report where ReportName=@reportName');
            }
        }
        if (isDuplicate) {
            request.query('insert into ReportTable (ReportName, Report) values (@reportName, @report)');
        }
    }
    catch (err) {
        console.log(err);
    }
}

async function RemoveReportFromDB(args) {
    try {
        let pool = await sql.connect(dbConfig);
        const request = pool.request();
        request.input('reportName', sql.VarChar, args.reportName);
        request.query('delete from ReportTable where ReportName=@reportName');
    }
    catch (err) {
        console.log(err);
    }
}

async function RenameReportInDB(args) {
    try {
        let pool = await sql.connect(dbConfig);
        const request = pool.request();
        request.input('reportName', sql.VarChar, args.reportName);
        request.input('renameReport', sql.VarChar, args.renameReport);
        if (args.isReportExists) {
            var reports = await getReports(pool);
            for (let i = 0; i < reports.length; i++) {
                if (reports[i]["ReportName"] === args.renameReport) {
                    request.query('delete from ReportTable where ReportName=@renameReport');
                }
            }
        }
        var reports = await getReports(pool);
        for (let j = 0; j < reports.length; j++) {
            if (reports[j]["ReportName"] === args.reportName) {
                isDuplicate = false;
                request.query('update ReportTable set ReportName=@renameReport where ReportName=@reportName');
            }
        }
    }
    catch (err) {
        console.log(err);
    }
}

async function FetchReportListFromDB() {
    try {
        let pool = await sql.connect(dbConfig);
        var reports = await getReports(pool);
        var reportNames = [];
        for (let j = 0; j < reports.length; j++) {
            reportNames.push(reports[j]["ReportName"]);
        }
        return reportNames;
    }
    catch (err) {
        console.log(err);
    }
}

async function LoadReportFromDB(args) {
    try {
        let pool = await sql.connect(dbConfig);
        var report = '';
        const request = pool.request();
        request.input('reportName', sql.VarChar, args.reportName)
        var reports = await getReports(pool);
        for (let i = 0; i < reports.length; i++) {
            if (reports[i]["ReportName"] === args.reportName) {
                report = reports[i]["Report"];
                break;
            }
        }
        return report;
    }
    catch (err) {
        console.log(err);
    }
}

async function getReports(pool) {
    try {
        let reports = await pool.query("select * from ReportTable");
        return reports.recordset;
    }
    catch (err) {
        console.log(err);
    }
}

module.exports = {
    SaveReportToDB: SaveReportToDB,
    RemoveReportFromDB: RemoveReportFromDB,
    RenameReportInDB: RenameReportInDB,
    FetchReportListFromDB: FetchReportListFromDB,
    LoadReportFromDB: LoadReportFromDB
}

Following that, we’ll define endpoints in our Express server to handle the report manipulation operations defined in the dboperations.js file.

[api.js]

var dboperations = require('./dboperations');
var express = require('express');
var bodyParser = require('body-parser');
var cors = require('cors');
var app = express();
var router = express.Router();

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cors());
app.use('/api', router);

router.route('/saveReport').post((request, response) => {
  let report = { ...request.body }
  console.log(report);
  dboperations.SaveReportToDB(report).then(data => {
    response.status(201).json(data);
  })
})

router.route('/removeReport').post((request, response) => {
  let report = { ...request.body }
  console.log(report);
  dboperations.RemoveReportFromDB(report).then(data => {
    response.status(201).json(data);
  })
})

router.route('/renameReport').post((request, response) => {
  let report = { ...request.body }
  console.log(report);
  dboperations.RenameReportInDB(report).then(data => {
    response.status(201).json(data);
  })
})

router.route('/fetchReport').post((request, response) => {
  dboperations.FetchReportListFromDB().then(data => {
    response.status(201).json(data);
  });
})

router.route('/loadReport').post((request, response) => {
  let report = { ...request.body }
  console.log(report);
  dboperations.LoadReportFromDB(report).then(data => {
    response.status(201).json(data);
  })
})

// Define the port for the server here.
app.listen(5000, () => {
  console.log("Server is running on port 5000");
});

Now, run the server with the following command.

npm start

Integrate with Syncfusion Vue Pivot Table

The Syncfusion Vue Pivot Table includes a built-in toolbar UI with predefined report manipulation options. These options can be used to perform dynamic report manipulation operations such as save, save as, rename, load, delete, and add reports. You can add these predefined report manipulation options to the toolbar UI, as per the documentation.

Refer to the following image.

Built-in toolbar icons in Vue Pivot Table to save, save as, rename, load, remove, and add reports

Built-in toolbar icons in Vue Pivot Table to save, save as, rename, load, remove, and add reports

With the above toolbar UI options, we can update the report to the SQL Server database via Vue Pivot Table and Node JS Express service. Refer to the following code example.

[App.vue]

<template>
  <div>
    <div class="control-section" style="overflow: auto, margin-top: 100px">
      <div class="content-wrapper">
        <ejs-pivotview id="pivotview" ref="pivotview" :dataSourceSettings="dataSourceSettings"
          :width="width" :allowExcelExport="allowExcelExport"
          :allowConditionalFormatting="allowConditionalFormatting" 
          :allowPdfExport="allowPdfExport" :showToolbar="showToolbar" :allowCalculatedField="allowCalculatedField"
          :showFieldList="showFieldList" :toolbar="toolbar" :saveReport="saveReport" :loadReport="loadReport"
          :fetchReport="fetchReport" :renameReport="renameReport" :removeReport="removeReport" :newReport="newReport"
          :toolbarRender="beforeToolbarRender" :displayOption="displayOption"
          :chartSettings="chartSettings"></ejs-pivotview>
      </div>
    </div>
  </div>
</template>

<script>
 import Vue from "vue";
 import {
   PivotViewPlugin,
   FieldList,
   CalculatedField,
   Toolbar,
   ConditionalFormatting,
   NumberFormatting
 } from "@syncfusion/ej2-vue-pivotview";
 import { enableRipple } from "@syncfusion/ej2-base";
 enableRipple(false);

 Vue.use(PivotViewPlugin);

 var data = [
  { 'Sold': 25, 'Amount': 42600, 'Country': 'France', 'Products': 'Mountain Bikes', 'Year': 'FY 2015', 'Quarter': 'Q4' },
  { 'Sold': 27, 'Amount': 46008, 'Country': 'France', 'Products': 'Mountain Bikes', 'Year': 'FY 2016', 'Quarter': 'Q1' },
  { 'Sold': 49, 'Amount': 83496, 'Country': 'France', 'Products': 'Mountain Bikes', 'Year': 'FY 2016', 'Quarter': 'Q2' },
  { 'Sold': 31, 'Amount': 52824, 'Country': 'France', 'Products': 'Mountain Bikes', 'Year': 'FY 2015', 'Quarter': 'Q1' },
  { 'Sold': 51, 'Amount': 86904, 'Country': 'France', 'Products': 'Mountain Bikes', 'Year': 'FY 2015', 'Quarter': 'Q2' },
  // ....
 ];

 export default {
  data() {
    return {
      dataSourceSettings: {
        columns: [{ name: 'Year', caption: 'Production Year' }, { name: 'Quarter' }],
        dataSource: data,
        expandAll: false,
        filters: [],
        formatSettings: [{ name: 'Amount', format: 'C0' }],
        rows: [{ name: 'Country' }, { name: 'Products' }],
        values: [{ name: 'Sold', caption: 'Units Sold' }, { name: 'Amount', caption: 'Sold Amount' }]
      },
      displayOption: { view: "Both" },
      chartSettings: {
        value: "Amount",
        enableExport: true,
        chartSeries: { type: "Column", animation: { enable: false } },
        enableMultipleAxis: false,
      },
      toolbar: [
        "New",
        "Save",
        "SaveAs",
        "Rename",
        "Remove",
        "Load",
        "Grid",
        "Chart",
        "MDX",
        "Export",
        "SubTotal",
        "GrandTotal",
        "ConditionalFormatting",
        "FieldList",
      ],
      allowExcelExport: true,
      allowConditionalFormatting: true,
      allowPdfExport: true,
      showToolbar: true,
      allowCalculatedField: true,
      showFieldList: true,
      width: "700",
    };
  },
  methods: {
    updateReport(reportList) {
      var pivotTableObj = (this.$refs.pivotview).ej2Instances;
      // Here, you can refresh the report list by feeding updated reports fetched from the database.
      var reportListObj = pivotTableObj.element.querySelector(
        "#" + pivotTableObj.element.id + "_reportlist").ej2_instances;
      if (reportListObj) {
        reportListObj[0].dataSource = reportList;
        reportListObj[0].value = pivotTableObj.toolbarModule.currentReport;
        // To remove the report.
        if (pivotTableObj.toolbarModule.currentReport === "" && (reportListObj[0].itemData === null || reportList.length < 2)) {
          pivotTableObj.toolbarModule.currentReport = reportList[reportList.length - 1];
          reportListObj[0].value = pivotTableObj.toolbarModule.currentReport;
          this.loadReport({ reportName: reportList[reportList.length - 1] })
        }
      }
    },
    saveReport(args) {
      var report = JSON.parse(args.report);
      report.dataSourceSettings.dataSource = [];
      report.pivotValues = [];
      fetch('http://localhost:5000/api/saveReport', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ reportName: args.reportName, report: JSON.stringify(report) })
      }).then(response => {
        this.fetchReport(args);
      });
    },
    fetchReport(args) {
      fetch('http://localhost:5000/api/fetchReport', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
        },
        body: ""
      }).then(res => res.json())
        .then(response => {
          this.updateReport(response.length > 0 ? response : []);
        });
    },
    loadReport(args) {
      fetch('http://localhost:5000/api/loadReport', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ reportName: args.reportName })
      }).then(res => res.json())
        .then(response => {
          if (response) {
            var report = JSON.parse(response);
            var pivotTableObj = (this.$refs.pivotview).ej2Instances;
            report.dataSourceSettings.dataSource = pivotTableObj.dataSourceSettings.dataSource;
            pivotTableObj.dataSourceSettings = report.dataSourceSettings;
          }
        });
    },
    removeReport(args) {
      fetch('http://localhost:5000/api/removeReport', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ reportName: args.reportName })
      }).then(response => {
        this.fetchReport(args);
      });
    },
    renameReport(args) {
      fetch('http://localhost:5000/api/renameReport', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ reportName: args.reportName, renameReport: args.rename, isReportExists: args.isReportExists })
      }).then(response => {
        this.fetchReport(args);
      });
    },
    newReport() {
      var pivotTableObj = (this.$refs.pivotview).ej2Instances;
      pivotTableObj.setProperties({ dataSourceSettings: { columns: [], rows: [], values: [], filters: [] } }, false);
    },
    beforeToolbarRender(args) {
      args.customToolbar.splice(6, 0, {
        type: 'Separator'
      });
      args.customToolbar.splice(9, 0, {
        type: 'Separator'
      });
    }
  },
  provide: {
    pivotview: [FieldList, CalculatedField, Toolbar, ConditionalFormatting, NumberFormatting]
  }
 };
</script>

<style>
 @import "../node_modules/@syncfusion/ej2-base/styles/material3.css";
 @import "../node_modules/@syncfusion/ej2-inputs/styles/material3.css";
 @import "../node_modules/@syncfusion/ej2-buttons/styles/material3.css";
 @import "../node_modules/@syncfusion/ej2-splitbuttons/styles/material3.css";
 @import "../node_modules/@syncfusion/ej2-dropdowns/styles/material3.css";
 @import "../node_modules/@syncfusion/ej2-lists/styles/material3.css";
 @import "../node_modules/@syncfusion/ej2-popups/styles/material3.css";
 @import "../node_modules/@syncfusion/ej2-navigations/styles/material3.css";
 @import "../node_modules/@syncfusion/ej2-grids/styles/material3.css";
 @import "../node_modules/@syncfusion/ej2-pivotview/styles/material3.css";
 @import "../node_modules/@syncfusion/ej2-vue-pivotview/styles/material3.css";
</style>

[api.js]

var dboperations = require('./dboperations');
var express = require('express');
var bodyParser = require('body-parser');
var cors = require('cors');
var app = express();
var router = express.Router();

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cors());
app.use('/api', router);

router.route('/saveReport').post((request, response) => {
  let report = { ...request.body }
  console.log(report);
  dboperations.SaveReportToDB(report).then(data => {
    response.status(201).json(data);
  })
})

router.route('/removeReport').post((request, response) => {
  let report = { ...request.body }
  console.log(report);
  dboperations.RemoveReportFromDB(report).then(data => {
    response.status(201).json(data);
  })
})

router.route('/renameReport').post((request, response) => {
  let report = { ...request.body }
  console.log(report);
  dboperations.RenameReportInDB(report).then(data => {
    response.status(201).json(data);
  })
})

router.route('/fetchReport').post((request, response) => {
  dboperations.FetchReportListFromDB().then(data => {
    response.status(201).json(data);
  });
})

router.route('/loadReport').post((request, response) => {
  let report = { ...request.body }
  console.log(report);
  dboperations.LoadReportFromDB(report).then(data => {
    response.status(201).json(data);
  })
})

// Define the port for the server here.
app.listen(5000, () => {
  console.log("Server is running on port 5000");
});

[dboperations.js]

const sql = require('mssql/msnodesqlv8');
var dbConfig = {
    server: 'localhost',
    database: 'Reports',
    driver: "msnodesqlv8",
    options: {
        trustedConnection: true,
    }
}

async function SaveReportToDB(args) {
    try {
        let pool = await sql.connect(dbConfig);
        let isDuplicate = true;
        const request = pool.request();
        request.input('reportName', sql.VarChar, args.reportName)
        request.input('report', sql.VarChar, args.report)
        var reports = await getReports(pool);
        for (let i = 0; i < reports.length; i++) {
            if (reports[i]["ReportName"] === args.reportName) {
                isDuplicate = false;
                request.query('update ReportTable set Report=@report where ReportName=@reportName');
            }
        }
        if (isDuplicate) {
            request.query('insert into ReportTable (ReportName, Report) values (@reportName, @report)');
        }
    }
    catch (err) {
        console.log(err);
    }
}

async function RemoveReportFromDB(args) {
    try {
        let pool = await sql.connect(dbConfig);
        const request = pool.request();
        request.input('reportName', sql.VarChar, args.reportName);
        request.query('delete from ReportTable where ReportName=@reportName');
    }
    catch (err) {
        console.log(err);
    }
}

async function RenameReportInDB(args) {
    try {
        let pool = await sql.connect(dbConfig);
        const request = pool.request();
        request.input('reportName', sql.VarChar, args.reportName);
        request.input('renameReport', sql.VarChar, args.renameReport);
        if (args.isReportExists) {
            var reports = await getReports(pool);
            for (let i = 0; i < reports.length; i++) {
                if (reports[i]["ReportName"] === args.renameReport) {
                    request.query('delete from ReportTable where ReportName=@renameReport');
                }
            }
        }
        var reports = await getReports(pool);
        for (let j = 0; j < reports.length; j++) {
            if (reports[j]["ReportName"] === args.reportName) {
                isDuplicate = false;
                request.query('update ReportTable set ReportName=@renameReport where ReportName=@reportName');
            }
        }
    }
    catch (err) {
        console.log(err);
    }
}

async function FetchReportListFromDB() {
    try {
        let pool = await sql.connect(dbConfig);
        var reports = await getReports(pool);
        var reportNames = [];
        for (let j = 0; j < reports.length; j++) {
            reportNames.push(reports[j]["ReportName"]);
        }
        return reportNames;
    }
    catch (err) {
        console.log(err);
    }
}

async function LoadReportFromDB(args) {
    try {
        let pool = await sql.connect(dbConfig);
        var report = '';
        const request = pool.request();
        request.input('reportName', sql.VarChar, args.reportName)
        var reports = await getReports(pool);
        for (let i = 0; i < reports.length; i++) {
            if (reports[i]["ReportName"] === args.reportName) {
                report = reports[i]["Report"];
                break;
            }
        }
        return report;
    }
    catch (err) {
        console.log(err);
    }
}

async function getReports(pool) {
    try {
        let reports = await pool.query("select * from ReportTable");
        return reports.recordset;
    }
    catch (err) {
        console.log(err);
    }
}

module.exports = {
    SaveReportToDB: SaveReportToDB,
    RemoveReportFromDB: RemoveReportFromDB,
    RenameReportInDB: RenameReportInDB,
    FetchReportListFromDB: FetchReportListFromDB,
    LoadReportFromDB: LoadReportFromDB
}

Let’s examine how to use the built-in toolbar options with the SQL Server database and the Node JS Express server, one by one, using the above code.

Saving a report

When you select the Save a Report option from the toolbar, the saveReport event will be triggered. In this event, a fetch request is sent to the Node JS Express server’s (aka api.js ) saveReport method, passing the name of the current report and its settings, which can then be checked and saved in the SQL Server database.

For example, the report shown in the above code will be passed to the saveReport method with the name Sample Report (the name Sample Report comes from the source code by default, but you can change it in the code) and saved in the SQL Server database.

Refer to the following image.

Saving the Vue Pivot Table report in the database with the name Sample Report

Saving the Vue Pivot Table report in the database with the name Sample Report

If you want to save a copy of the current report to the SQL Server database under a different name, use the Save as current report option in the toolbar UI.

When you do so, the saveReport event will be triggered with the current report and new report name Sample Report 1. As previously mentioned, they can be saved to the SQL Server database after being passed to the Node JS Express service.

Saving a copy of the current report with a different name

Saving a copy of the current report with a different name

Note: The code snippets for Save and Save as reports are all the same as mentioned above.

Loading a report

When you select a report from the toolbar’s dropdown menu, the loadReport event is triggered. In this event, a fetch request is sent to the Node JS Express server’s (aka api.js ) LoadReport method, with the selected report’s name as the parameter. This method searches the SQL Server database for the report, retrieves it, and loads it into the Pivot Table.

For example, if the report name Sample Report is chosen from a dropdown menu and passed, the LoadReport method will look up the report in the SQL Server database, retrieve it, and load it into the pivot table.

Loading a report from SQL Server to the Vue Pivot Table

Loading a report from SQL Server to the Vue Pivot Table

Renaming a report

Use the Rename a current report icon in the toolbar to rename the current report name and save it back to the SQL database.

When you select the Rename a Current Report option, the renameReport event is triggered. In this event, a fetch request is sent to the Node JS Express server’s (aka api.js ) RenameReport method, passing the current and new report names. You can use the current report name to identify the report and resave it with the new report name in the SQL Server database.

For example, suppose we rename the current report from Sample Report 1 to Sample Report 2. In that case, both names will be passed to the RenameReport method, renaming the current report to Sample Report 2 in the SQL Server database.

Renaming a report in Vue Pivot Table

Renaming a report in Vue Pivot Table

Deleting a report

When you select the Deleteacurrentreport option from the toolbar, the removeReport event is fired. In this event, a fetch request is made to the Node JS Express server’s (aka api.js) RemoveReport method, which uses the current report name to locate and delete the appropriate report from the SQL Server database.

For example, suppose we delete the current report, Sample Report 2, from the Pivot Table. In that case, the current report, Sample Report 2, is passed to the RemoveReport method, allowing you to find and remove the report from the SQL Server database.

Deleting a report from the Vue Pivot Table

Deleting a report from the Vue Pivot Table

Note:
1. If the current report n is deleted, the Vue Pivot Table will automatically load the last report in the report list.
2. If only one report is in the list and removed, the Pivot Table will display the removed report state until a new report is added.

Adding a report

When you select the Create a new report option from the toolbar, the newReport event is triggered first, followed by the saveReport event, which saves this new report to the SQL Server database using the same saveReport method outlined in the code blocks above.

Adding a new report in the Vue Pivot Table

Adding a new report in the Vue Pivot Table

Limitations for report manipulation

The following points should be considered when saving the report to the SQL Server database:

  • Both raw and aggregated data will not be saved or loaded from the database.

  • The option to add external links using Pivot Table cells will not be saved or loaded from the database.

  • The Pivot Table should always load reports from the SQL Server database using the data source that is currently assigned to it.

GitHub reference

For more details, refer to saving and loading reports in Vue Pivot Table using SQL and Node JS Express servers GitHub demo.

Conclusion

Thanks for reading! In this blog, we learned how to easily connect to an MS SQL Server database and a Node JS Express server to save and load reports into the Syncfusion Vue Pivot Table. Try this and share your thoughts in the comments section below!

For existing customers, the newest version of Essential Studio is available for download from the License and Downloads page. If you are not a Syncfusion customer, try our 30-day free trial to check out the available features.

For questions, you can contact us through our support forum, support portal, or feedback portal. We are always happy to assist you!