diff --git a/web/dashboard.css b/web/dashboard.css
index 9984ae5..089d584 100644
--- a/web/dashboard.css
+++ b/web/dashboard.css
@@ -220,3 +220,5 @@
.mrow .m-name { display: flex; align-items: center; gap: 9px; }
.bk-row-desc.with-logo { display: flex; align-items: center; gap: 8px; }
.lx-desc .lx-logo-wrap { display: inline-flex; align-items: center; gap: 8px; }
+.mlogo-cat { display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; background: rgba(255,255,255,0.06); vertical-align: middle; flex: none; }
+.mlogo-cat img { display: block; }
diff --git a/web/dashboard.js b/web/dashboard.js
index 587442d..fab5476 100644
--- a/web/dashboard.js
+++ b/web/dashboard.js
@@ -158,7 +158,7 @@
merch.forEach(([name, d], i) => {
const cat = window.MT.spendCategory(d.txns[0].description);
const row = document.createElement('div'); row.className = 'mrow';
- const logo = window.MTlogo ? window.MTlogo.html(name, 22) : '';
+ const logo = window.MTlogo ? window.MTlogo.html(name, 22, {category: cat}) : '';
row.innerHTML = `${i + 1}${logo}${esc(name)}${esc(cat)}${d.txns.length}×${window.MT.CLPk(d.value)}`;
row.addEventListener('mousemove', e => window.MTtip.rows(e, name, col.spend(), d.txns));
row.addEventListener('mouseleave', () => window.MTtip.hide());
@@ -180,7 +180,7 @@
section.className = 'bk-section';
const rowsHtml = descs.slice(0, 30).map(([desc, d]) => {
const cpct = Math.round(d.value / catData.value * 100);
- const blogo = window.MTlogo ? window.MTlogo.html(desc, 18) : "";
+ const blogo = window.MTlogo ? window.MTlogo.html(desc, 18, {category: catName}) : "";
return `
${blogo}${esc(desc)}
${d.txns.length}×
diff --git a/web/dashboard2.js b/web/dashboard2.js
index ad44dae..7ebdb14 100644
--- a/web/dashboard2.js
+++ b/web/dashboard2.js
@@ -285,7 +285,7 @@
const shown = rows.slice(0, st.limit);
$('#lx-rows').innerHTML = shown.map(t => `
| ${t.date} |
- ${window.MTlogo ? window.MTlogo.html(t.description || t.counterparty || '', 18) : ''}${esc(t.description || t.counterparty || '—')} |
+ ${window.MTlogo ? window.MTlogo.html(t.description || t.counterparty || '', 18, {flowType: t.flow_type}) : ''}${esc(t.description || t.counterparty || '—')} |
${esc(t.bank)} ${MT.acctShort(t.doc_type)}${t.last4 ? ' ··' + t.last4 : ''} |
${FLOW_LABEL[t.flow_type] || t.flow_type} |
${t.direction === 'credit' ? '+' : '−'}${CLP(t.amount)} |
`).join('');
diff --git a/web/logos.js b/web/logos.js
index a38bda7..084164c 100644
--- a/web/logos.js
+++ b/web/logos.js
@@ -76,16 +76,46 @@
const ch=(m||desc||'?').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase();
return ch||'?';
}
- // returns an HTML string: a brand logo
![]()
with initial fallback, or just the initial chip
- function html(desc, size){
+ // ---- category fallback via Twemoji (jdecked fork) ----
+ const TW = 'https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/svg/';
+ const CATEGORY_ICON = {
+ 'Debt & loans':'1f4b3','Cash withdrawals':'1f3e7','Utilities & bills':'1f4a1',
+ 'Groceries':'1f6d2','Food & dining':'1f354','Fuel & transport':'26fd',
+ 'Health & pharmacy':'1f48a','Subscriptions & web':'1f4bb','Shopping & retail':'1f6cd',
+ 'Government & docs':'1f3db','Wallets & online':'1f4f1','Fees & interest':'1f3e6',
+ 'Other purchases':'1f9fe'
+ };
+ const FLOW_ICON = { income:'1f4b0', inter_in:'1f4b8', inter_out:'1f4b8',
+ self_transfer:'1f501', card_payment:'1f4b3', credit_line:'1f3e6', fee:'1f3e6' };
+ function emojiImg(cp, size){
+ const inner = Math.round(size*0.62);
+ return '
'+
+ '
';
+ }
+ function categoryFor(desc, opts){
+ opts = opts||{};
+ if(opts.flowType && opts.flowType!=='expense' && FLOW_ICON[opts.flowType]) return FLOW_ICON[opts.flowType];
+ let cat = opts.category;
+ if(!cat && window.MT && window.MT.spendCategory) cat = window.MT.spendCategory(desc);
+ if(cat && CATEGORY_ICON[cat]) return CATEGORY_ICON[cat];
+ return null;
+ }
+ function initChip(desc, size){
+ return '
'+esc(initialOf(desc))+'';
+ }
+ // returns an HTML string: brand logo, else category emoji, else coloured initial.
+ function html(desc, size, opts){
size = size||22;
const b = brandOf(desc);
- const key = b ? b.domain : (desc||'');
- const init = b ? b.name.charAt(0).toUpperCase() : initialOf(desc);
- const fb = '
'+esc(init)+'';
- if(!b) return fb;
- const url='https://cdn.brandfetch.io/'+b.domain+'/w/'+(size*2)+'/h/'+(size*2)+'?c='+CLIENT_ID;
- return '

';
+ if(b){
+ const url='https://cdn.brandfetch.io/'+b.domain+'/w/'+(size*2)+'/h/'+(size*2)+'?c='+CLIENT_ID;
+ const cp = categoryFor(desc, opts);
+ const fb = cp ? emojiImg(cp, size) : initChip(desc, size);
+ return '

';
+ }
+ const cp = categoryFor(desc, opts);
+ if(cp) return emojiImg(cp, size);
+ return initChip(desc, size);
}
window.MTlogo = { html, brandOf };
})();