How to build a Bank Statement page in Tecton: a computed account summary and a q2-data-table of transactions for a statement period.
A Bank Statement page shows the activity for a statement period: a summary of the balances and totals, followed by the list of transactions. This example composes the whole view from Tecton components. The summary totals are computed from the transaction data, and the table renders from that same data. A real application would fetch the statement for the period from an API.
<!-- Account summary: opening balance, totals, and closing balance -->
<q2-card elevation="1" style="margin-block: var(--app-scale-4x);">
<q2-grid columns="1" md-columns="3" gap="comfortable" align="start">
<q2-grid-area>
<q2-detail stacked size="small" label="Account holder" description="Jane Doe"></q2-detail>
</q2-grid-area>
<q2-grid-area>
<q2-detail stacked size="small" label="Account number" description="****4821"></q2-detail>
</q2-grid-area>
<q2-grid-area>
<q2-detail stacked size="small" label="Statement period" description="March 2025"></q2-detail>
</q2-grid-area>
</q2-grid>
<q2-grid columns="1" md-columns="4" style="margin-top: var(--app-scale-8x);">
<q2-grid-area>
<q2-detail stacked size="small" label="Opening balance">
<q2-currency id="summary-opening" amount="0"></q2-currency>
</q2-detail>
</q2-grid-area>
<q2-grid-area>
<q2-detail stacked size="small" label="Deposits and credits">
<q2-currency id="summary-credits" amount="0" variant="credit"></q2-currency>
</q2-detail>
</q2-grid-area>
<q2-grid-area>
<q2-detail stacked size="small" label="Withdrawals and debits">
<q2-currency id="summary-debits" amount="0" variant="debit"></q2-currency>
</q2-detail>
</q2-grid-area>
<q2-grid-area>
<q2-detail stacked size="small" label="Closing balance">
<q2-currency id="summary-closing" amount="0"></q2-currency>
</q2-detail>
</q2-grid-area>
</q2-grid>
<q2-action-group>
<q2-btn intent="workflow-primary" id="download-btn">
<span>Download PDF</span>
<q2-icon type="download"></q2-icon>
</q2-btn>
<q2-btn intent="workflow-secondary" id="share-btn">
<span>Share</span>
<q2-icon type="upload"></q2-icon>
</q2-btn>
</q2-action-group>
</q2-card>
<!-- Transactions -->
<q2-data-table bordered id="statement-table"></q2-data-table>
<script>
// Sample statement data for the period shown. A real app would fetch this.
const statement = {
openingBalance: 6230.45,
transactions: [
{ date: '2025-03-05', description: 'Payroll Deposit', debit: null, credit: 3500.0, balance: 9730.45 },
{ date: '2025-03-08', description: 'Rent Payment', debit: 1200.0, credit: null, balance: 8530.45 },
{ date: '2025-03-10', description: 'Grocery Store', debit: 143.22, credit: null, balance: 8387.23 },
{ date: '2025-03-12', description: 'Netflix', debit: 15.99, credit: null, balance: 8371.24 },
{ date: '2025-03-15', description: 'ATM Withdrawal', debit: 200.0, credit: null, balance: 8171.24 },
{ date: '2025-03-18', description: 'Transfer to Savings', debit: 500.0, credit: null, balance: 7671.24 },
{ date: '2025-03-22', description: 'Electric Bill', debit: 98.55, credit: null, balance: 7572.69 },
{ date: '2025-03-28', description: 'Freelance Payment', debit: null, credit: 1250.0, balance: 8822.69 },
{ date: '2025-03-31', description: 'Spotify', debit: 9.99, credit: null, balance: 8812.7 },
],
};
const currencyFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
const formatCurrency = amount => currencyFormatter.format(amount);
const dateFormatter = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' });
const formatDate = iso => dateFormatter.format(new Date(iso));
// Element references.
const summaryOpening = document.querySelector('#summary-opening');
const summaryCredits = document.querySelector('#summary-credits');
const summaryDebits = document.querySelector('#summary-debits');
const summaryClosing = document.querySelector('#summary-closing');
const table = document.querySelector('#statement-table');
const tableHeaders = [
{ title: 'Date', key: 'date', width: '16%' },
{ title: 'Description', key: 'description' },
{ title: 'Type', key: 'type', width: '12%' },
{ title: 'Amount', key: 'amount', align: 'end' },
{ title: 'Balance', key: 'balance', align: 'end' },
];
// Builds a q2-data-table row from a transaction. A text Type column means debit
// versus credit never depends on color alone.
const toRow = (transaction, i) => ({
id: `statement-${i}`,
cells: {
date: formatDate(transaction.date),
description: transaction.description,
type: transaction.debit != null ? 'Debit' : 'Credit',
amount: formatCurrency(transaction.debit ?? transaction.credit),
balance: formatCurrency(transaction.balance),
},
});
// Render on load: compute the summary totals from the transactions and fill the table.
const transactions = statement.transactions;
summaryOpening.amount = statement.openingBalance;
summaryCredits.amount = transactions.reduce((sum, t) => sum + (t.credit ?? 0), 0);
summaryDebits.amount = transactions.reduce((sum, t) => sum + (t.debit ?? 0), 0);
summaryClosing.amount = transactions[transactions.length - 1].balance;
table.headers = tableHeaders;
table.rows = transactions.map(toRow);
</script>
The page renders a single statement from in-memory data. On load, the script computes the summary totals from the transactions and fills the q2-data-table. There are no selectors and no network calls; a real application would fetch the statement for the period from an API.
const statement = {
openingBalance: 6230.45,
transactions: [ /* ... */ ],
};
const transactions = statement.transactions;
summaryOpening.amount = statement.openingBalance;
summaryCredits.amount = transactions.reduce((sum, t) => sum + (t.credit ?? 0), 0);
summaryDebits.amount = transactions.reduce((sum, t) => sum + (t.debit ?? 0), 0);
summaryClosing.amount = transactions[transactions.length - 1].balance;
table.headers = tableHeaders;
table.rows = transactions.map(toRow);The summary is a q2-card of q2-detail rows. The account holder, account number, and statement period are static markup. The opening balance, totals, and closing balance each hold a q2-currency, and the values are set as properties from the statement data. The totals are computed from the transactions rather than stored.
summaryOpening.amount = statement.openingBalance;
summaryCredits.amount = transactions.reduce((sum, t) => sum + (t.credit ?? 0), 0);
summaryDebits.amount = transactions.reduce((sum, t) => sum + (t.debit ?? 0), 0);
summaryClosing.amount = transactions[transactions.length - 1].balance;The credit and debit totals use q2-currency's variant="credit" and variant="debit" for conventional coloring, but each is paired with a plain-text label ("Deposits and credits", "Withdrawals and debits"), so the meaning is carried by the label and only reinforced by color.
The transactions render through q2-data-table. Its headers and rows are set as properties, and a toRow mapper converts each raw transaction into the { id, cells } shape the table expects.
const toRow = (transaction, i) => ({
id: `statement-${i}`,
cells: {
date: formatDate(transaction.date),
description: transaction.description,
type: transaction.debit != null ? 'Debit' : 'Credit',
amount: formatCurrency(transaction.debit ?? transaction.credit),
balance: formatCurrency(transaction.balance),
},
});The explicit Type column ("Debit" or "Credit") keeps the distinction between money in and money out readable without relying on color. Amount and Balance are right-aligned (align: 'end') so the decimals line up.