---
title: >-
  Administration:  Practical Lab - High-Value Customer Listing | Shopware
  Community Hub
description: >-
  Build a custom Shopware administration page with routing, DAL-based data
  loading, and a standardized entity listing.
canonical_url: >-
  https://hub.shopware.com/learn/unit/administration-practical-lab-high-value-customer-listing
---

# Administration:  Practical Lab - High-Value Customer Listing

<LearningObjectives>

- Add a new second-level menu entry under **Customers**.
- Load customer data from the DAL using repository and `Criteria`.
- Display entity search results using Shopware's standardized administration listing.
- Customize listing behavior and column rendering with `sw-entity-listing`.
- Understand the difference between `sw-data-grid` and `sw-entity-listing`.

</LearningObjectives>

# Administration:  Practical Lab - High-Value Customer Listing

In this practical lab, you will combine everything you learned about:

- Administration **routing** and **navigation**.
- **Data handling** via repositories and `Criteria`.
- Rendering tabular data with a listing component (based on the data grid).

You will build a custom administration page that lists **high-value customers**. In this practical lab, high-value customers are defined as:

> Customers who placed an order in the last **30 days** (based on the `customer.lastOrderDate` field).

This lab is based on the reference plugin [FrontendDevIntermediateHighValueCustomerOverview](https://github.com/ShopwareAcademy/FrontendDevIntermediateHighValueCustomerOverview).

The implementation uses the `swag-academy-*` prefix because it belongs to the Academy reference plugin. For your own extensions, use your own company, plugin, or project prefix to avoid naming conflicts.

## Target Result

By the end of this practical lab, you will have implemented the following:

- A new menu entry under **Customers** called **High-Value Customers**.
- Clicking it opens a custom administration page.
- The page loads customers whose `lastOrderDate` is within the last 30 days.
- The result is displayed in a listing (using `sw-entity-listing`, which internally uses `sw-data-grid`).

## Folder Structure

At the end of the implementation, your administration folder structure will look like this:

```text
[shop_root]
 └─ custom/plugins
    └─ [your_plugin]
        ├─ src
        │  ├─ Resources
        │  │   └─ app
        │  │      └─ administration
        │  │         └─ src
        │  │            │─ module
        │  │            │   └─ swag-academy-high-value-customers
        │  │            │      ├─ index.js
        │  │            │      └─ component
        │  │            │          └─ swag-academy-high-value-customers-list
        │  │            │             ├─ index.js
        │  │            │             └─ swag-academy-high-value-customers-list.html.twig
        │  │            ├─ snippet
        │  │            │  ├─ en-GB.json
        │  │            │  └─ de-DE.json
        │  │            └─ main.js
        │  └─ [YourPluginName].php
        └─ composer.json
```

In the next steps, you will build this stricture step by step while implementing routing, data-loading, and the listing UI.

## Step 1: Register the Module, Route, and Navigation Entry

The first step is to create the second-level entry point for your administration feature. This includes:

- Registering a custom administration module.
- Defining a route and navigation entry for your module under **Customers**.

<Callout title="Reminder" type="info">

You already learned how to register a basic administration module and route in the previous course. Feel free to [recheck it out](/learn/unit/administration-basic-folder-and-module-structure#administrationfolderstructureminimalskeleton).

</Callout>

<ArticleMultipleQuestionnaire>
  <ArticleQuestionnaire>
    <ArticleQuestionnaireQuestion>Which two configuration blocks must be defined in `Shopware.Module.register()` so your module can be opened via the administration menu entry and its route?</ArticleQuestionnaireQuestion>
    <ArticleQuestionnaireAnswer correct>routes</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer>snippets</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer>mixins</ArticleQuestionnaireAnswer>
    <ArticleQuestionnaireAnswer correct>navigation</ArticleQuestionnaireAnswer>
  </ArticleQuestionnaire>
</ArticleMultipleQuestionnaire>

<Callout title="Tip" type="info">

To dock your navigation entry under **Customers**, set the navigation item's `parent` to `sw-customer`.

</Callout>

### Generate the Administration Module Skeleton

To get started, generate an administration module skeleton using the following command:

```bash
bin/console make:plugin:admin-module
```

This command creates a basic folder structure and files required for an administration extension. Your folder structure should now look like this:

```text
└── [shop_root]
     └── custom
         └── plugins
             └── [your_plugin]
                  └── src
                      └── Resources
                          └── app
                              └── administration
                                  └── src
                                      ├── module
                                      │   └── swag-example
                                      │       └── index.js
                                      ├── snippet
                                      │   │── de-DE.json
                                      │   └── en-GB.json
                                      └── main.js
```

### Register the Module, Route, and Navigation Entry

Rename the generated module older from `swag-example` to `swag-academy-high-value-customers`.

Open the file `src/Resources/app/administration/src/module/swag-academy-high-value-customers/index.js` file, replace its content with the following code:

```js
// src/Resources/app/administration/src/module/swag-academy-high-value-customers/index.js

// Register module
Shopware.Module.register('swag-academy-high-value-customers', {
  type: 'plugin',
  name: 'swag-academy-high-value-customers',
  title: 'swag-academy-high-value-customers.general.mainMenuItemGeneral',
  description: 'sw-property.general.descriptionTextModule',
  color: '#ff3d58',
  icon: 'default-shopping-paper-bag-product',

  routes: {
    list: {
      component: 'swag-academy-high-value-customers-list',
      path: 'list'
    },
  },

  navigation: [{
    label: 'swag-academy-high-value-customers.general.mainMenuItemGeneral',
    color: '#ff3d58',
    path: 'swag.academy.high.value.customers.list',
    icon: 'default-shopping-paper-bag-product',
    parent: 'sw-customer',
    position: 100
  }]
});
```

**Explanation:**

- `type: 'plugin'` registers the module as a plugin.
- `name: 'swag-academy-high-value-customers'` is the technical module name.
- `routes` defines the route and the corresponding component, which is `swag-academy-high-value-customers-list` you will create in the next step.
- `navigation` adds the menu entry defines the menu entry.
- `parent: 'sw-customer'` makes the menu entry appear under **Customers** (second-level navigation).

### Verify the Folder Structure

After these changes, your administration structure should look like this:

```text
└── [shop_root]
     └── custom
         └── plugins
             └── [your_plugin]
                  └── src
                      └── Resources
                          └── app
                              └── administration
                                  └── src
                                      ├── module
                                      │   └── swag-academy-high-value-customers
                                      │       └── index.js
                                      ├── snippet
                                      │   │── de-DE.json
                                      │   └── en-GB.json
                                      └── main.js
```

### Create Snippets for the Menu Entry

The module uses two snippets for its title and description: `swag-academy-high-value-customers.general.mainMenuItemGeneral` and `swag-academy-high-value-customers.list.textHighValueCustomersOverview`.

Replace the content of the `de-DE.json` and `en-GB.json` files with the following:

**de-DE.json**

```json
{
  "swag-academy-high-value-customers": {
    "general": {
      "mainMenuItemGeneral": "Wertstarke Kunden",
      "descriptionTextModule": "Verwalte wertstarke Kunden hier"
    }
  }
}
```

**en-GB.json**

```json
{
  "swag-academy-high-value-customers": {
    "general": {
      "mainMenuItemGeneral": "High-Value Customers",
      "descriptionTextModule": "Manage high-value customers here"
    }
  }
}
```

### Import the Module and Build the Administration

Finally, import your module in the `main.js` file:

```js
// Import admin module
import './module/swag-academy-high-value-customers';
```

And build the administration with the following command:

```bash
bin/build-administration.sh
```

Shopware CLI equivalent: `shopware-cli project admin-build`.

After refreshing the administration, you should see a new menu entry under **Customers** called **High-Value Customers**.

![Administration Custom Module Created](assets/images/administration-custom-module-created.jpg)

At this stage, the page itself is not available. The entry `component: 'swag-academy-high-value-customers-list'` is added in the module routes, but the implementation the related component is missing. In the next step, you will create the page component.

## Step 2: Create the Component

In this step, you create the component that is rendered when the route `swag.academy.high.value.customers.list` is opened.

The component consists of three parts:

- A template for the UI structure.
- The component logic (DAL access, state, criteria).
- Snippets for translation.

### Create the Component Folder Structure

Create the following folders and files inside your administration module:

```text
└── [shop_root]
     └── custom
         └── plugins
             └── [your_plugin]
                  └── src
                      └── Resources
                          └── app
                              └── administration
                                  └── src
                                      ├── module
                                      │   └── swag-academy-high-value-customers
                                      │       │── component // Create this folder
                                      │       │    └── swag-academy-high-value-customers-list // Create this folder
                                      │       │        │── index.js // Create this file
                                      │       │        └── swag-academy-high-value-customers-list.html.twig // Create this file
                                      │       └── index.js
                                      └── main.js
```

### Create the Component Template

Open the following file `[your_plugin]/src/Resources/app/administration/src/module/swag-academy-high-value-customers/component/swag-academy-high-value-customers-list/swag-academy-high-value-customer-list.html.twig` and add the following template:

```twig
{# src/Resources/app/administration/src/module/swag-academy-high-value-customers/component/swag-academy-high-value-customers-list/swag-academy-high-value-customers-list.html.twig #}
<sw-page class="swag-academy-high-value-customers-list">
    <template #smart-bar-header>
        <h2>
            {{ $t('swag-academy-high-value-customers.list.textHighValueCustomersOverview') }}
        </h2>
    </template>

    <template #content>
        <mt-loader v-if="isLoading"/>

        <mt-empty-state
            v-else-if="!highValueCustomers?.length"
            icon="solid-users"
            :headline="$t('sw-empty-state.messageNoResultTitle')"
            :description="$t('swag-academy-high-value-customers.list.messageEmpty')"
        />

        <sw-entity-listing
            v-else
            :data-source="highValueCustomers"
            :columns="highValueCustomersColumns"
            :repository="customerRepository"
        >
            <template #column-lastOrderDate="{ item }">
                <span v-if="dateFilter && item.lastOrderDate">
                    {{ dateFilter(item.lastOrderDate, {
                        year: 'numeric',
                        month: '2-digit',
                        day: '2-digit',
                        hour: '2-digit',
                        minute: '2-digit'
                    }) }}
                </span>
                <span v-else>
                   {{ item.lastOrderDate }}
               </span>
            </template>
        </sw-entity-listing>
    </template>
</sw-page>
```

**What this template does:**

- `sw-page` provides the standard administration page layout.
- `smart-bar-header` displays the page title.
- `content` is the main content area.
- `mt-loader` is shown while data is loading.
- `mt-empty-state` is shown if no customers are found (no customers match the criteria).
- `highValueCustomers` is the list of high-value customers, which is loaded asynchronously (defined in the next step)
- `highValueCustomersColumns` defines the columns to display in the listing (defined in the next step).
- `customerRepository` is the repository for accessing customers. (defined in the next step).
- `sw-entity-listing` renders the data in a table.
  We bind the `highValueCustomers`, `highValueCustomersColumns`, and `customerRepository` properties which are defined in the next step.

**Custom column rendering with slots:**

- The `column-lastOrderDate` slot customizes how the `lastOrderDate` column is rendered.
- The slot name `lastOrderDate` must match the property defined in the `highValueCustomersColumns`.
- The `item` parameter represents a single customer entity returned by the DAL.
- The `dateFilter` function is provided by the `date` filter and is used to format the date string.
  If the filter is not available, the raw date is rendered as a fallback.

You will see how the column definition and the filter are wired together in the next step.

<Callout title="Note" type="info">

Although this page uses `sw-entity-listing`, it is still based on the same concepts you learned with `sw-data-grid` component. You will learn more about it later in this learning unit.

</Callout>

### Implement the Component Logic

Now implement the logic that loads and provides the data for the template.

Open the file `[your_plugin]/src/Resources/app/administration/src/module/swag-academy-high-value-customers/component/swag-academy-high-value-customers-list/index.js` and add the following code:

```js
// src/Resources/app/administration/src/module/swag-academy-high-value-customers/component/swag-academy-high-value-customer-list/index.js
import template from './swag-academy-high-value-customers-list.html.twig';

const { Criteria } = Shopware.Data;

export default Shopware.Component.wrapComponentConfig({
  template,

  inject: [
    'repositoryFactory',
  ],

  mixins: [
    Shopware.Mixin.getByName('listing'),
  ],

  data() {
    return {
      highValueCustomers: null,
      sortBy: 'createdAt',
      sortDirection: 'DESC',
      isLoading: true,
    };
  },

  computed: {
    customerRepository() {
      return this.repositoryFactory.create('customer');
    },

    /**
     * Get the date filter function from the filter service.
     */
    dateFilter() {
      return Shopware.Filter.getByName('date');
    },

    /**
     * High-value customers are customers who placed an order in the last 30 days.
     */
    highValueCustomersCriteria() {
      const criteria = new Criteria(this.page, this.limit);

      criteria.addFilter(
        Criteria.range('lastOrderDate', {
          gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
        })
      );

      return criteria;
    },

    /**
     * Called in the template to define the columns of the high-value customers list.
     *
     * The values of each property (customerNumber, firstName, lastName, lastOrderDate) come
     * from the entity "customer", which is loaded from the repository "customerRepository".
     *
     * This means you have access to the loaded properties of the entity "customer"
     * (and to associations if they are added to the criteria) when it is loaded successfully.
     */
    highValueCustomersColumns() {
      return [
        {
          property: 'customerNumber',
          label: 'Customer Number',
          routerLink: 'sw.customer.detail',
        },
        {
          property: 'firstName',
          label: 'First Name',
        },
        {
          property: 'lastName',
          label: 'Last Name',
        },
        {
          property: 'lastOrderDate',
          label: 'Last Order Date',
        },
      ];
    },
  },

  methods: {
    /**
     * This method is called automatically by the listing mixin
     */
    async getList() {
      try {
        this.isLoading = true;
        this.highValueCustomers = await this.customerRepository.search(
          this.highValueCustomersCriteria,
          Shopware.Context.api
        );
      } catch (error) {
        console.error(error);
      } finally {
        this.isLoading = false;
      }
    },
  },
});
```

**What happens here:**

- The `customerRepository` method includes `repositoryFactory.create('customer')` to create a repository for accessing customer entity.
- The `listing` mixin provides pagination (`page`, `limit`) and lifecycle handling.
- The `highValueCustomersCriteria` defines which customers are loaded.
- The `highValueCustomersColumns` defines the columns to display in the template.
- The `getList()` is called automatically by the listing mixin. This method searches for high-value customers asynchronously and save the result in the `highValueCustomers` property.

<Callout title="Reminder: Component Registration Pattern" type="info">

This component config is wrapped with `Shopware.Component.wrapComponentConfig` (mainly for TypeScript typing) and the component is registered via lazy-loading.

If you want to revisit **why** lazy-loading is recommended and what `wrapComponentConfig` actually does, check out the dedicated [learning unit](/learn/unit/administration-basic-folder-and-module-structure#optionalrecommendedpatternlazyloadingwrapcomponentconfig).

</Callout>

### Add Snippets for the Component

The template uses two additional snippets: `swag-academy-high-value-customers.list.textHighValueCustomersOverview` and `swag-academy-high-value-customers.list.messageEmpty`.

Now extend the two snippets (`list`) in the `en-GB.json` and `de-DE.json` files:

**de-DE.json**

```json
{
  "swag-academy-high-value-customers": {
    "general": {
      "mainMenuItemGeneral": "Wertstarke Kunden",
      "descriptionTextModule": "Verwalte wertstarke Kunden hier"
    },
    "list": {
      "textHighValueCustomersOverview": "Übersicht wertstarker Kunden",
      "messageEmpty": "Noch keine wertstarken Kunden vorhanden"
    }
  }
}
```

**en-GB.json**

```json
{
  "swag-academy-high-value-customers": {
    "general": {
      "mainMenuItemGeneral": "High-Value Customers",
      "descriptionTextModule": "Manage high-value customers here"
    },
    "list": {
      "textHighValueCustomersOverview": "High-Value Customers Overview",
      "messageEmpty": "No high-value customers yet"
    }
  }
}
```

## Step 3: Register the Component

In this step, you have everything together. Now you have to register the component in the module's `index.js` file under `[your_plugin]/src/Resources/app/administration/src/module/swag-academy-high-value-customers/index.js`:

```text
└── [shop_root]
     └── custom
         └── plugins
             └── [your_plugin]
                  └── src
                      └── Resources
                          └── app
                              └── administration
                                  └── src
                                      ├── module
                                      │   └── swag-academy-high-value-customers
                                      │       │── component
                                      │       │    └── swag-academy-high-value-customers-list
                                      │       │        │── index.js
                                      │       │        └── swag-academy-high-value-customers-list.html.twig
                                      │       └── index.js // <-- Register component here
                                      └── main.js
```

Add the following code to this `index.js` file:

```js
Shopware.Component.register(
  'swag-academy-high-value-customers-list',
  () => import('./component/swag-academy-high-value-customers-list')
);
```

At this state, your whole `index.js` file should now look like this:

```js
/**
 * Register component
 */
Shopware.Component.register(
  'swag-academy-high-value-customers-list',
  () => import('./component/swag-academy-high-value-customers-list')
);

/**
 * Register module
 */
Shopware.Module.register('swag-academy-high-value-customers', {
  type: 'plugin',
  name: 'swag-academy-high-value-customers',
  title: 'swag-academy-high-value-customers.general.mainMenuItemGeneral',
  description: 'sw-property.general.descriptionTextModule',
  color: '#ff3d58',
  icon: 'default-shopping-paper-bag-product',

  routes: {
    list: {
      component: 'swag-academy-high-value-customers-list',
      path: 'list'
    },
  },

  navigation: [{
    label: 'swag-academy-high-value-customers.general.mainMenuItemGeneral',
    color: '#ff3d58',
    path: 'swag.academy.high.value.customers.list',
    icon: 'default-shopping-paper-bag-product',
    parent: 'sw-customer',
    position: 100
  }]
});
```

## Step 4: Build and Verify

At this point, you have completed the implementation:

- Created a new custom administration module.
- Created a new whole dedicated component and registered it in the module.
- Connected the component to routing and navigation of the module.
- Added translations for the menu entry and page content.
- Imported the module in the `main.js` file.

Now build the administration again:

```bash
bin/build-administration.sh
```

Shopware CLI equivalent: `shopware-cli project admin-build`.

After the build is finished:

- Refresh the administration in your browser.
- Navigate to **Customers** > **High-Value Customers**.

If you don't have any orders from the last 30 days, the page should look like this:

![Content Empty](assets/images/administration-module-content-empty.jpg)

If you do have orders from the last 30 days, the page should look like this:

![Content Not Empty](assets/images/administration-module-content-sw-entity-listing-standard.jpg)

<Callout title="Note" type="info">

If you don't have orders from the last 30 days, create new orders in the administration.

</Callout>

## Optional: Compare With the Reference Implementation

If you want to validate your solution, compare your implementation with the [reference implementation](https://github.com/ShopwareAcademy/FrontendDevIntermediateHighValueCustomerOverview).

Alternatively, you can clone the plugin in your local environment:

```bash
cd custom/plugins
git clone git@github.com:ShopwareAcademy/FrontendDevIntermediateHighValueCustomerOverview.git
```

## Data Grid and SW-Entity-Listing

The data grid component (`sw-data-grid`) is the **fundamental building block** for rendering tabular data in the Shopware administration.

In the Shopware administration core, this component is extended by the higher-level components such as `sw-entity-listing`. These extensions build on top of `sw-data-grid` and add more functionality **without changing the data grid component itself**.

### `sw-data-grid` vs. `sw-entity-listing`

The difference is not what is rendered, but how much functionality is added.

Use `sw-data-grid` when:

- You only want to render a table with data.
- You are **not** rendering data from the DAL.
- You want to manage the listing behavior yourself.
- You want to manage pagination, sorting, deletion, etc. manually.

Use `sw-entity-listing` when:

- You want to render data fetched from the DAL.
- You want to have a fully-featured listing with sorting, pagination, deletion, etc.
- You want to follow the Shopware administration core conventions.

In this practical lab, we worked with **entity search results** from the DAL. Therefore, `sw-entity-listing` is the ideal approach.

### Visual Comparison

Let's see what the difference looks like in our example:

When you use the `sw-data-grid` instead of `sw-entity-listing`, it will look like this:

![Using Data Grid](assets/images/administration-module-content-data-grid.jpg)

And when you use the `sw-entity-listing` instead of `sw-data-grid`, it will look like this:

![Using SW Entity Listing](assets/images/administration-module-content-sw-entity-listing-1.jpg)

![Using SW Entity Listing](assets/images/administration-module-content-sw-entity-listing-2.jpg)

The difference is clearly visible: With `sw-entity-listing` you have all commonly required listing features:

- Column Sorting
- Deleting single or multiple items
- Inline edit workflows
- Standard Shopware listing behavior

You can also link in the `sw-entity-listing` to add you custom logic if required.

With `sw-data-grid` you would need to implement all this functionality manually.

<Callout title="Note" type="info">

Even tough this practical lab uses `sw-entity-listing`, the same principles you learned with `sw-data-grid` still apply.

The `sw-entity-listing` build on top of `sw-data-grid`, it does not replace it.

</Callout>

If you want to explore how `sw-entity-listing` extends the data grid internally, you can find it (along with many other core components) in this [Shopware administration source code](https://github.com/shopware/shopware/blob/trunk/src/Administration/Resources/app/administration/src/app/component/index.ts).

As a Shopware developer, it is always good to get a rough overview of how the core components work internally and how you can find them in the source code.

## Summary

Great job! In this practical lab, you:

- Registered a custom administration module with routing and navigation.
- Created a dedicated administration page component (template, logic, snippets).
- Loaded customer data via a DAL repository and `Criteria` filtering.
- Implemented a standardized entity listing UI based on the `sw-entity-listing` component.
- Customized column rendering using listing slots.
- Understood when to use `sw-data-grid` vs. `sw-entity-listing`.
- Followed Shopware administration core patterns and conventions.

With this knowledge, you can now build custom administration pages that integrate cleanly with routing, data handling, and UI components – following the same patterns used in the Shopware administration core.

---

Congratulations! By completing this practical lab, you successfully finished this learning path!
