Expense approvals, grouped by employee
Fourteen expense lines grouped by employee, status badges, three lines checked and an Approve selected button.
Overview
Fourteen expense lines grouped by employee, status badges, three lines checked and an Approve selected button. A finance manager's whole review, in one tag. The whole page is one Data Table Group By (avonni-dd-datatable-group-by) on an app page, grouping expense records by employee through its own query. This tutorial rebuilds the Expenses tab of the Finance app in your own org.
What you build

The Expenses tab of Finance opens on the September lines: four employees, four groups with a count, Amara Osei (4), Jonas Lindqvist (3), Maya Chen (4), Tomas Berg (3).
Every line carries its date, merchant, category, amount aligned right and a status badge: Submitted gray, Approved green, Rejected red.
Two of Maya Chen's lines and one of Tomas Berg's are checked, and their group checkboxes show the partial state.
Approve selected sits in the header next to Export. Status and Category filter the table, the search box finds a merchant.
Before you start
The Avonni LWC Components package is installed, and the approvers hold a license and the package permission set. See Installation & Licenses Management.
A Lightning app with an app page for the review. The examples below use the Expenses tab of the Finance app.
A custom object for the expense lines. The table needs these fields:
Expense (Expense__c)
Name
Auto Number, EXP-{0000}
The line number. Not displayed.
Employee__c
Text
The group. One group per distinct value.
Expense_Date__c
Date
The Date column and the sort inside each group
Merchant__c
Text
The Merchant column and the search field
Category__c
Picklist: Travel, Meals, Lodging, Software, Supplies
The Category column and the second filter
Amount__c
Currency, 2 decimal places
The Amount column
Status__c
Picklist: Submitted (default), Approved, Rejected
The badge text and the first filter
Status_Variant__c
Formula (Text)
Turns the status into a badge color, see step 1
The field names above are the bare API names. If your org has a namespace, every custom field is addressed with its prefix (ns__Employee__c) in the query, the columns, the filters and the search fields.
Sample data. Fourteen lines, August 24 to September 4, 2026, four employees:
Amara Osei
Sep 3, 2026
City Cabs
Travel
88.00
Submitted
Amara Osei
Sep 2, 2026
Green Leaf Cafe
Meals
54.10
Submitted
Amara Osei
Aug 31, 2026
Sketchboard
Software
149.00
Submitted
Amara Osei
Aug 28, 2026
Lakeside Hotel
Lodging
520.00
Approved
Jonas Lindqvist
Sep 4, 2026
Nordic Rail
Travel
310.60
Submitted
Jonas Lindqvist
Sep 1, 2026
Union Station Deli
Meals
42.75
Submitted
Jonas Lindqvist
Aug 26, 2026
Datastack
Software
299.00
Approved
Maya Chen
Sep 2, 2026
Northstar Air
Travel
412.80
Submitted
Maya Chen
Sep 1, 2026
Corner Bistro
Meals
38.50
Submitted
Maya Chen
Aug 27, 2026
Harbor Inn
Lodging
640.00
Approved
Maya Chen
Aug 25, 2026
Cloudnote
Software
29.00
Submitted
Tomas Berg
Sep 4, 2026
Northstar Air
Travel
1,240.00
Submitted
Tomas Berg
Sep 3, 2026
Riverside Grill
Meals
96.20
Submitted
Tomas Berg
Aug 24, 2026
Paperworks
Supplies
18.40
Rejected
Ten Submitted, three Approved, one Rejected, so every badge color is on the page and the table still reads as a queue.
Permissions. Approvers need Read on Expense and its fields, and Edit on Status__c for Approve selected to save. The Avonni query follows Salesforce sharing.
Build it
Create the object, the badge formula and the records
In Setup, open Object Manager and create the Expense object with the fields listed above.
Add the Status Variant formula field (return type Text):
Load the fourteen sample lines.
Why: the badge column colors itself from a named variant (success, error, base, and so on). A formula that returns the variant per status lets every row pick its own color, with nothing to maintain in the code when a status is added.

Define the columns
Column
fieldName
type
Settings
Date
Expense_Date__c
date
initialWidth: 130
Merchant
Merchant__c
text
Category
Category__c
text
initialWidth: 140
Amount
Amount__c
currency
initialWidth: 140, cellAttributes: { alignment: 'right' }
Status
Status__c
badge
initialWidth: 140, see step 5
(hidden)
Status_Variant__c
text
hidden: true
Why: columnType: 'objectField' binds a column to a record field, label overrides the field label in the header, and initialWidth keeps the narrow columns narrow so Merchant takes the room. Category stays a text column: a badge there would need a second variant field for five values, with no gain in reading.
Color the Status badge from the formula
In the Status column above, typeAttributes.variant is not a fixed value but { fieldName: 'Status_Variant__c' }: the badge reads its variant from the row. The hidden column carries the formula into the row so the value is there to read.
Why: a fixed variant colors every badge the same. Bound to the formula field, Approved reads green (success), Rejected red (error) and Submitted gray (base), straight from the data. Valid variants are base, brand, inverse, alt-inverse, success, warning, error, info and offline.
Group counts, header, filters and search
Why: displayGroupRowsCount puts the (4) after Amara Osei; hideUndefinedGroup drops a group for lines with no employee; groupByOrderDirection asc sorts the groups by employee name, so Amara Osei comes first and Tomas Berg last. With header-visible-actions-count at 2, both actions stay buttons. Filters render above the table (filters-attributes.type defaults to horizontal), one chip per field, labeled with the field label.
Handle the selection and Approve selected
Why: headeractionclick carries the action name only. The selection arrives through selectrows, as the key values of the checked rows (Id by default), so the handler keeps them as they change. After the update, refresh(true) runs the query again and stays on the current page, and the three badges turn green.
Expose the component and put it on the app page
In
expenseApprovals.js-meta.xml, expose the component for app pages and tabs:
Deploy, then in Lightning App Builder create an App Page with one region, drag Expense Approvals into it, Save and Activate it as the Expenses tab of the Finance app.

The settings that matter
query.groupBy and groupByAttributes.displayGroupRowsCount
Employee__c, true
One group per person, with the count in the group header.
Status column type and typeAttributes.variant
badge, { fieldName: 'Status_Variant__c' }
A color per status, driven by the record. A badge reads faster than a word.
read-only with the checkbox column shown
on, default
Select, then act. Nothing is edited in the cell.
header-visible-actions-count
2
Approve selected and Export both stay one click away.
Interactions
selectrows
selectedRowsKeyValue
Keep the ids of the checked rows. Fires on every check and uncheck.
headeractionclick
name
approve: update the kept ids, then refresh(true). export: your export code.
filter
value
Optional: the filters applied, keyed by field API name.
sort
sortedBy, sortedDirection
Optional: the column the end user sorted on.
Try it
Open the Expenses tab: four groups with their counts, Amara Osei first, fourteen lines, three Approved badges in green and one Rejected in red.
Filter Status on
Rejected: one line remains, Paperworks under Tomas Berg, and the other three groups disappear. Clear the filter and searchNorthstar: two lines, one under Maya Chen, one under Tomas Berg.Check Northstar Air ($412.80) and Corner Bistro ($38.50) under Maya Chen and Northstar Air ($1,240.00) under Tomas Berg: the two group checkboxes and the header checkbox switch to the partial state. Click Approve selected: the table refreshes and the three badges read Approved.
Take it further
Grouping exists on the Data Table of the other Avonni surfaces, configured in the builder rather than in code:
Data Table for Flow Screen Components, inside a screen flow, with its Group By section.
Data Table for Dynamic Components, on a Lightning page, with the Group By field in Data Mappings.
Variations worth trying:
Groups in a chosen order:
groupByOrderDirection: { direction: 'custom', customValue: ['Maya Chen', 'Tomas Berg', 'Amara Osei', 'Jonas Lindqvist'] }.Start folded:
collapsed: trueingroupByAttributesopens every group closed.Volume:
show-paginationwithitems-per-pagewhen a month runs past a screen.
Troubleshooting
Every badge has the same color
typeAttributes.variant is a fixed value, or the formula returns a name that is not a badge variant.
Bind variant to { fieldName: 'Status_Variant__c' }, keep the hidden column and addedFields, and make sure the formula returns success, error or base.
The table shows an error or blank cells in a namespaced org
The query and the columns name Employee__c, and the org knows the field as ns__Employee__c.
Prefix every custom field name, in the query, the columns, filters and searchFields.
The groups are not in the order you expected
groupByOrderDirection asc sorts the groups by the value of the group field, alphabetically.
Use direction: 'custom' with customValue listing the values in your order.
A group checkbox shows a dash instead of a check
Some rows of the group are checked and some are not.
Nothing: it is the partial state. Check the group box to select the whole group.
Approve selected does nothing
The handler expects the selection in the headeractionclick payload, which carries the action name only.
Handle selectrows and keep selectedRowsKeyValue; approve those ids.
The update fails for some users
The user lacks Edit on Status__c or on the object; updateRecord respects field-level security.
Grant Edit on Expense and on the Status field to the approvers.
Was this helpful?
