Hi everyone,
I recently wrote a small inline extension for Manager that summarises all dividends by stock and thought it might be useful to share. I haven’t seen many examples or explanations of inline extensions on the forum, so hopefully this helps anyone wanting to experiment with them.
What the extension does
The extension creates a dividend summary grouped by stock (code), showing the total dividends received per stock. Each stock row also includes an accordion dropdown that expands to show the individual dividend transactions that make up the total.
So the layout looks something like:
Stock → Total Dividends
▼ Click to expand → shows individual dividend entries for that stock.
This makes it easy to quickly see:
-
Total dividend income per holding
-
A breakdown of the individual dividend payments
Screenshots
1. Where to add the inline extension code
(image showing where to insert the inline extension in Manager)
2. Dividend summary with accordion dropdown
(image showing the dividend summary table and dropdown details)
3. The extension code
<div id="app" style="font-family:Segoe UI,Arial,sans-serif;padding:12px;">Loading dividends...</div>
<script>
(async function () {
function postMessageWithResponse(message, timeoutMs = 15000) {
return new Promise((resolve, reject) => {
const requestId = crypto.randomUUID();
const timer = setTimeout(() => {
window.removeEventListener("message", onMessage);
reject(new Error(`Timeout waiting for response: ${message.type}`));
}, timeoutMs);
function onMessage(event) {
const data = event && event.data;
if (!data || data.requestId !== requestId || !String(data.type || "").endsWith("-response")) return;
clearTimeout(timer);
window.removeEventListener("message", onMessage);
resolve(data);
}
window.addEventListener("message", onMessage);
window.parent.postMessage(Object.assign({}, message, { requestId }), "*");
});
}
async function apiGet(path) {
const response = await postMessageWithResponse({ type: "api-request", method: "GET", path });
if (!response || response.status < 200 || response.status >= 300) {
throw new Error(`API GET failed (${response && response.status ? response.status : "unknown"}): ${path}`);
}
return response.body;
}
function toNumber(value) {
if (value == null) return null;
if (typeof value === "number") return Number.isFinite(value) ? value : null;
if (typeof value === "object" && value !== null && typeof value.value === "number") return Number.isFinite(value.value) ? value.value : null;
const n = Number(value);
return Number.isFinite(n) ? n : null;
}
function esc(text) {
return String(text == null ? "" : text)
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
.replace(/"/g, """).replace(/'/g, "'");
}
function fmtMoney(n) {
if (n == null || !Number.isFinite(n)) return "-";
return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function fmtQty(n) {
if (n == null || !Number.isFinite(n)) return "-";
return n.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 4 });
}
function parseTicker(accountText, descriptionText, validTickers) {
const text = `${accountText || ""} ${descriptionText || ""}`.toUpperCase();
for (const t of validTickers) {
if (
text.includes(` ${t} `) ||
text.includes(`(${t})`) ||
text.includes(`-${t} `) ||
text.includes(` ${t}-`)
) {
return t;
}
}
const tokens = text.match(/[A-Z0-9._-]+/g) || [];
for (const tok of tokens) {
if (validTickers.has(tok)) {
return tok;
}
}
return null;
}
function isDividendLike(accountText, descriptionText) {
const text = `${accountText || ""} ${descriptionText || ""}`.toUpperCase();
return text.includes("DIVIDEND");
}
async function fetchAllJournalEntryLines() {
const pageSize = 1000;
let skip = 0;
const all = [];
while (true) {
const path =
`/api2/journal-entry-lines?skip=${skip}&pageSize=${pageSize}` +
`&fields=Date&fields=Reference&fields=Narration&fields=Account&fields=LineDescription&fields=Qty&fields=Debit&fields=Credit`;
const res = await apiGet(path);
const rows = res?.journalEntryLines || [];
all.push(...rows);
if (rows.length < pageSize) break;
skip += pageSize;
}
return all;
}
const [invRes, receiptRes, journalLines] = await Promise.all([
apiGet("/api2/investments?pageSize=1000&fields=Code&fields=Name"),
apiGet("/api2/receipt-lines?pageSize=1000&fields=Date&fields=Account&fields=Description&fields=Amount"),
fetchAllJournalEntryLines()
]);
const validTickers = new Set(
(invRes?.investments || [])
.map(x => String(x?.code || "").trim().toUpperCase())
.filter(Boolean)
);
const tickerNameMap = new Map(
(invRes?.investments || []).map(x => [String(x?.code || "").trim().toUpperCase(), x?.name || ""])
);
const perTicker = new Map(); // ticker -> { total, lines[] }
function pushLine(ticker, lineObj) {
const cur = perTicker.get(ticker) || { total: 0, lines: [] };
cur.total += lineObj.amount || 0;
cur.lines.push(lineObj);
perTicker.set(ticker, cur);
}
// Cash dividends from receipt lines.
for (const line of (receiptRes?.receiptLines || [])) {
const account = line?.account || "";
const description = line?.description || "";
if (!isDividendLike(account, description)) continue;
const ticker = parseTicker(account, description, validTickers);
if (!ticker) continue;
const amount = Math.abs(toNumber(line?.amount) ?? 0);
pushLine(ticker, {
type: "Cash",
date: line?.date || "",
reference: "",
account,
description,
qty: null,
amount
});
}
// DRP dividends from journal entries: Shares debit + Dividend Income credit in same group.
const drpGroups = new Map();
for (const line of (journalLines || [])) {
const date = String(line?.date || "");
const reference = String(line?.reference || "");
const narration = String(line?.narration || "");
const key = `${date}|${reference}|${narration}`;
if (!drpGroups.has(key)) {
drpGroups.set(key, {
date,
reference,
narration,
dividendIncomeCredit: 0,
shareDebitsByTicker: new Map()
});
}
const group = drpGroups.get(key);
const account = String(line?.account || "");
const lineDescription = String(line?.lineDescription || "");
const text = `${account} ${lineDescription}`.toUpperCase();
const debit = Math.abs(toNumber(line?.debit) ?? 0);
const credit = Math.abs(toNumber(line?.credit) ?? 0);
const qty = Math.abs(toNumber(line?.qty) ?? 0);
const ticker = parseTicker(account, lineDescription, validTickers);
if (text.includes("DIVIDEND INCOME") && credit > 0) {
group.dividendIncomeCredit += credit;
}
if (text.includes("SHARES") && ticker && debit > 0) {
const cur = group.shareDebitsByTicker.get(ticker) || { debit: 0, qty: 0, account, description: lineDescription };
cur.debit += debit;
cur.qty += qty;
group.shareDebitsByTicker.set(ticker, cur);
}
}
for (const group of drpGroups.values()) {
if (group.dividendIncomeCredit <= 0 || group.shareDebitsByTicker.size === 0) continue;
let totalShareDebit = 0;
for (const item of group.shareDebitsByTicker.values()) {
totalShareDebit += item.debit;
}
if (totalShareDebit <= 0) continue;
for (const [ticker, item] of group.shareDebitsByTicker.entries()) {
const allocated = group.dividendIncomeCredit * (item.debit / totalShareDebit);
pushLine(ticker, {
type: "DRP",
date: group.date,
reference: group.reference,
account: item.account,
description: group.narration || item.description,
qty: item.qty,
amount: allocated
});
}
}
const rows = Array.from(perTicker.entries())
.map(([ticker, data]) => ({
ticker,
name: tickerNameMap.get(ticker) || ticker,
dividendIncome: data.total,
lines: data.lines.sort((a, b) => String(a.date || "").localeCompare(String(b.date || "")))
}))
.sort((a, b) => a.ticker.localeCompare(b.ticker));
const totalDiv = rows.reduce((sum, row) => sum + row.dividendIncome, 0);
const htmlRows = rows.length
? rows.map((row) => {
const innerRows = row.lines.map((line) => `
<tr>
<td>${esc(line.date)}</td>
<td>${esc(line.type)}</td>
<td>${esc(line.reference || "")}</td>
<td>${esc(line.account || "")}</td>
<td>${esc(line.description || "")}</td>
<td class="num">${fmtQty(line.qty)}</td>
<td class="num">${fmtMoney(line.amount)}</td>
</tr>
`).join("");
return `
<details class="ticker-details">
<summary>
<span><b>${esc(row.ticker)}</b> - ${esc(row.name)}</span>
<span class="num"><b>${fmtMoney(row.dividendIncome)}</b></span>
</summary>
<div class="details-wrap">
<table>
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Reference</th>
<th>Account</th>
<th>Description/Narration</th>
<th class="num">Qty</th>
<th class="num">Amount</th>
</tr>
</thead>
<tbody>${innerRows}</tbody>
</table>
</div>
</details>
`;
}).join("")
: `<div class="empty">No dividends found for investment tickers.</div>`;
document.getElementById("app").innerHTML = `
<style>
.card{border:1px solid #e5e7eb;border-radius:10px;overflow:hidden}
.head{padding:10px 12px;background:#f9fafb;border-bottom:1px solid #e5e7eb;font-weight:600}
.ticker-details{border-bottom:1px solid #f1f5f9}
.ticker-details:last-child{border-bottom:none}
.ticker-details summary{
list-style:none; cursor:pointer; display:flex; justify-content:space-between; align-items:center;
padding:10px 12px; background:#fff; font-size:13px;
}
.ticker-details summary::-webkit-details-marker{display:none}
.ticker-details summary::before{
content:"+"; margin-right:8px; color:#64748b; display:inline-block; transform:translateY(-1px);
}
.ticker-details[open] summary::before{content:"^"}
.details-wrap{padding:0 12px 12px 12px; background:#fff}
table{width:100%;border-collapse:collapse;font-size:12px}
th,td{padding:6px 8px;border-bottom:1px solid #f1f5f9;text-align:left;vertical-align:top}
th{background:#f8fafc}
.num{text-align:right;font-variant-numeric:tabular-nums}
.empty{padding:12px;color:#64748b}
</style>
<div class="card">
<div class="head">Dividends by Ticker (Total: ${fmtMoney(totalDiv)})</div>
${htmlRows}
</div>
`;
})().catch(err => {
const msg = err instanceof Error ? err.message : String(err);
const safe = String(msg).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
document.getElementById("app").innerHTML = `<div style="color:#b91c1c;">Failed: ${safe}</div>`;
});
</script>
Why I built it
Manager already stores all the information needed, but when reviewing investment income I wanted a quick way to:
-
see dividends grouped by stock
-
view totals and details in the same place
-
avoid exporting data to spreadsheets just to summarise it
How the inline extension works
The extension uses the Manager API (/api2) to retrieve dividend transactions. The data is then processed in JavaScript to group the records by stock code and calculate the total dividends per stock.
The script then dynamically inserts a small HTML table into the page. Each row contains the stock summary, and the accordion dropdown simply toggles visibility of the detailed rows containing the individual dividend transactions.
Because it runs as an inline extension, it doesn’t modify any Manager data — it only reads the data from the API and renders a custom view in the browser.
Note on inline extensions
When I started building this, I found very little documentation or examples about inline extensions, so I thought sharing a working example might help others explore what’s possible.
If anyone else has built inline extensions or has tips on best practices, I’d be very interested to see them.
Hope this is helpful to someone.

