solid-panes
Version:
Solid-compatible Panes: applets and views for the mashlib and databrowser
477 lines (452 loc) • 20.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var UI = _interopRequireWildcard(require("solid-ui"));
var $rdf = _interopRequireWildcard(require("rdflib"));
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
/* Financial Transaction Pane
**
** This outline pane allows a user to interact with a transaction
** downloaded from a bank statement, annotting it with classes and comments,
** trips, etc
*/
const ns = UI.ns;
var _default = exports.default = {
// icon: (module.__dirname || __dirname) + '22-pixel-068010-3d-transparent-glass-icon-alphanumeric-dollar-sign.png',
icon: UI.icons.iconBase + 'noun_106746.svg',
name: 'transaction',
audience: [ns.solid('PowerUser')],
// Does the subject deserve this pane?
label: function (subject, context) {
const kb = context.session.store;
const t = kb.findTypeURIs(subject);
if (t['http://www.w3.org/2000/10/swap/pim/qif#Transaction']) return '$$';
if (kb.any(subject, UI.ns.qu('amount'))) return '$$$'; // In case schema not picked up
// if (t['http://www.w3.org/2000/10/swap/pim/qif#Period']) return "period $"
if (t['http://www.w3.org/ns/pim/trip#Trip']) return 'Trip $';
return null; // No under other circumstances (while testing at least!)
},
render: function (subject, context) {
const dom = context.dom;
const kb = context.session.store;
const fetcher = kb.fetcher;
const Q = $rdf.Namespace('http://www.w3.org/2000/10/swap/pim/qif#');
const TRIP = $rdf.Namespace('http://www.w3.org/ns/pim/trip#');
const div = dom.createElement('div');
div.setAttribute('class', 'transactionPane');
const mention = function mention(message, style) {
if (style === undefined) style = 'color: grey';
const pre = dom.createElement('pre');
pre.setAttribute('style', style);
div.appendChild(pre);
pre.appendChild(dom.createTextNode(message));
};
const complain = function complain(message) {
return mention(message, 'color: #100; background-color: #fee');
};
/*
var thisPane = this
var rerender = function (div) {
var parent = div.parentNode
var div2 = thisPane.render(subject, dom)
parent.replaceChild(div2, div)
}
*/
// //////////////////////////////////////////////////////////////////////////////
const plist = kb.statementsMatching(subject);
const qlist = kb.statementsMatching(undefined, undefined, subject);
const t = kb.findTypeURIs(subject);
// var me = authn.currentUser()
const predicateURIsDone = {};
const donePredicate = function (pred) {
predicateURIsDone[pred.uri] = true;
};
const setPaneStyle = function (account) {
let mystyle = 'padding: 0.5em 1.5em 1em 1.5em; ';
if (account) {
const backgroundColor = kb.any(account, UI.ns.ui('backgroundColor'));
if (backgroundColor) {
mystyle += 'background-color: ' + backgroundColor.value + '; ';
}
}
div.setAttribute('style', mystyle);
};
// setPaneStyle()
// Functions for displaying lists of transactions
// Click on the transaction line to expand it into a pane
// Shift-click to expand without collapsing others
const d2 = function (n) {
if (n === undefined) return '';
const s = '' + n;
if (s.indexOf('.') >= 0) {
return s.split('.')[0] + '.' + (s.split('.')[1] + '00').slice(0, 2);
}
return s + '.00';
};
const numericCell = function numericCell(amount, suppressZero) {
const td = dom.createElement('td');
if (!(0.0 + amount === 0.0 && suppressZero)) {
td.textContent = d2(amount);
}
td.setAttribute('style', 'width: 6em; text-align: right; ');
return td;
};
const headerCell = function headerCell(str) {
const td = dom.createElement('th');
td.textContent = str;
td.setAttribute('style', 'text-align: right; ');
return td;
};
const oderByDate = function (x, y) {
const dx = kb.any(x, ns.qu('date'));
const dy = kb.any(y, ns.qu('date'));
if (dx !== undefined && dy !== undefined) {
if (dx.value < dy.value) return -1;
if (dx.value > dy.value) return 1;
}
if (x.uri < y.uri) return -1; // Arbitrary but repeatable
if (x.uri > y.uri) return 1;
return 0;
};
const insertedPane = function (context, subject, paneName) {
const p = context.session.paneRegistry.byName(paneName);
const d = p.render(subject, context);
d.setAttribute('style', 'border: 0.1em solid green;');
return d;
};
const expandAfterRow = function (dom, row, subject, paneName, solo) {
const siblings = row.parentNode.children;
if (solo) {
for (let j = siblings.length - 1; j >= 0; j--) {
if (siblings[j].expanded) {
siblings[j].parentNode.removeChild(siblings[j].expanded);
siblings[j].expanded = false;
}
}
}
const tr = dom.createElement('tr');
const td = tr.appendChild(dom.createElement('td'));
td.setAttribute('style', 'width: 98%; padding: 1em; border: 0.1em solid grey;');
const cols = row.children.length;
if (row.nextSibling) {
row.parentNode.insertBefore(tr, row.nextSibling);
} else {
row.parentNode.appendChild(tr);
}
row.expanded = tr;
td.setAttribute('colspan', '' + cols);
td.appendChild(insertedPane(context, subject, paneName));
};
const expandAfterRowOrCollapse = function (dom, row, subject, paneName, solo) {
if (row.expanded) {
row.parentNode.removeChild(row.expanded);
row.expanded = false;
} else {
expandAfterRow(dom, row, subject, paneName, solo);
}
};
const transactionTable = function (dom, list, filter) {
const table = dom.createElement('table');
table.setAttribute('style', 'padding-left: 0.5em; padding-right: 0.5em; font-size: 9pt; width: 85%;');
const transactionRow = function (dom, x) {
const tr = dom.createElement('tr');
const setTRStyle = function (tr, account) {
// var mystyle = "padding: 0.5em 1.5em 1em 1.5em; "
let mystyle = '\'padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.1em;';
if (account) {
const backgroundColor = kb.any(account, UI.ns.ui('backgroundColor'));
if (backgroundColor) {
mystyle += 'background-color: ' + backgroundColor.value + '; ';
}
}
tr.setAttribute('style', mystyle);
};
const account = kb.any(x, ns.qu('toAccount'));
setTRStyle(tr, account);
const c0 = tr.appendChild(dom.createElement('td'));
const date = kb.any(x, ns.qu('date'));
c0.textContent = date ? date.value.slice(0, 10) : '???';
c0.setAttribute('style', 'width: 7em;');
const c1 = tr.appendChild(dom.createElement('td'));
c1.setAttribute('style', 'width: 36em;');
const payee = kb.any(x, ns.qu('payee'));
c1.textContent = payee ? payee.value : '???';
const a1 = c1.appendChild(dom.createElement('a'));
a1.textContent = ' ➜';
a1.setAttribute('href', x.uri);
const c3 = tr.appendChild(dom.createElement('td'));
const amount = kb.any(x, ns.qu('in_USD'));
c3.textContent = amount ? d2(amount.value) : '???';
c3.setAttribute('style', 'width: 6em; text-align: right; '); // @@ decimal alignment?
tr.addEventListener('click', function (e) {
// solo unless shift key
expandAfterRowOrCollapse(dom, tr, x, 'transaction', !e.shiftKey);
}, false);
return tr;
};
const list2 = filter ? list.filter(filter) : list.slice(); // don't sort a paramater passed in place
list2.sort(oderByDate);
for (let i = 0; i < list2.length; i++) {
table.appendChild(transactionRow(dom, list2[i]));
}
return table;
};
// Render a single transaction
// This works only if enough metadata about the properties can drive the RDFS
// (or actual type statements whichtypically are NOT there on)
if (t['http://www.w3.org/2000/10/swap/pim/qif#Transaction'] || kb.any(subject, ns.qu('toAccount'))) {
// var trip = kb.any(subject, WF('trip'))
donePredicate(ns.rdf('type'));
const account = kb.any(subject, UI.ns.qu('toAccount'));
setPaneStyle(account);
if (!account) {
complain('(Error: There is no bank account known for this transaction <' + subject.uri + '>,\n -- every transaction needs one.)');
}
let store = null;
const statement = kb.any(subject, UI.ns.qu('accordingTo'));
if (statement === undefined) {
complain('(Error: There is no back link to the original data source foir this transaction <' + subject.uri + '>,\nso I can\'t tell how to annotate it.)');
} else {
store = statement !== undefined ? kb.any(statement, UI.ns.qu('annotationStore')) : null;
if (store === undefined) {
complain('(There is no annotation document for this statement\n<' + statement.uri + '>,\nso you cannot classify this transaction.)');
}
}
const nav = dom.createElement('div');
nav.setAttribute('style', 'float:right');
div.appendChild(nav);
const navLink = function (pred, label) {
donePredicate(pred);
const obj = kb.any(subject, pred);
if (!obj) return;
const a = dom.createElement('a');
a.setAttribute('href', obj.uri);
a.setAttribute('style', 'float:right');
nav.appendChild(a).textContent = label || UI.utils.label(obj);
nav.appendChild(dom.createElement('br'));
};
navLink(UI.ns.qu('toAccount'));
navLink(UI.ns.qu('accordingTo'), 'Statement');
navLink(TRIP('trip'));
// Basic data:
const table = dom.createElement('table');
div.appendChild(table);
const preds = ['date', 'payee', 'amount', 'in_USD', 'currency'].map(Q);
const inner = preds.map(function (p) {
donePredicate(p);
const value = kb.any(subject, p);
const s = value ? UI.utils.labelForXML(value) : '';
return '<tr><td style="text-align: right; padding-right: 0.6em">' + UI.utils.labelForXML(p) + '</td><td style="font-weight: bold;">' + s + '</td></tr>';
}).join('\n');
table.innerHTML = inner;
const complainIfBad = function (ok, body) {
if (ok) {
// setModifiedDate(store, kb, store)
// rerender(div) // deletes the new trip form
} else complain('Sorry, failed to save your change:\n' + body);
};
// What trips do we know about?
// Classify:
if (store) {
kb.fetcher.nowOrWhenFetched(store.uri, subject, function (ok, body) {
if (!ok) complain('Cannot load store ' + store + ' ' + body);
const calendarYear = kb.any(store, ns.qu('calendarYear'));
const renderCatgorySelectors = function () {
div.insertBefore(UI.widgets.makeSelectForNestedCategory(dom, kb, subject, UI.ns.qu('Classified'), store, complainIfBad), table.nextSibling);
};
if (kb.any(undefined, ns.rdfs('subClassOf'), ns.qu.Classified)) {
renderCatgorySelectors();
} else if (calendarYear) {
fetcher.load(calendarYear).then(function (_xhrs) {
fetcher.load(kb.each(calendarYear, ns.rdfs('seeAlso'))).then(function () {
renderCatgorySelectors();
}).catch(function (e) {
console.log('Error loading background data: ' + e);
});
}).catch(function (e) {
console.log('Error loading calendarYear: ' + e);
});
} else {
console.log('Can\'t get categories, because no calendarYear');
}
div.appendChild(UI.widgets.makeDescription(dom, kb, subject, UI.ns.rdfs('comment'), store, complainIfBad));
let trips = kb.statementsMatching(undefined, TRIP('trip'), undefined, store).map(function (st) {
return st.object;
}); // @@ Use rdfs
const trips2 = kb.each(undefined, UI.ns.rdf('type'), TRIP('Trip'));
trips = trips.concat(trips2).sort(); // @@ Unique
const sortedBy = function (kb, list, pred, reverse) {
const l2 = list.map(function (x) {
let key = kb.any(x, pred);
key = key ? key.value : '9999-12-31';
return [key, x];
});
l2.sort();
if (reverse) l2.reverse();
return l2.map(function (pair) {
return pair[1];
});
};
trips = sortedBy(kb, trips, UI.ns.cal('dtstart'), true); // Reverse chron
if (trips.length > 1) {
div.appendChild(UI.widgets.makeSelectForOptions(dom, kb, subject, TRIP('trip'), trips, {
multiple: false,
nullLabel: '-- what trip? --',
mint: 'New Trip *',
mintClass: TRIP('Trip'),
mintStatementsFun: function (trip) {
const is = [];
is.push($rdf.st(trip, UI.ns.rdf('type'), TRIP('Trip'), trip.doc()));
return is;
}
}, store, complainIfBad));
}
div.appendChild(dom.createElement('br'));
// Add in simple comments about the transaction
const outliner = context.getOutliner(dom);
donePredicate(ns.rdfs('comment')); // Done above
UI.widgets.attachmentList(dom, subject, div);
donePredicate(ns.wf('attachment'));
div.appendChild(dom.createElement('tr')).setAttribute('style', 'height: 1em'); // spacer
// Remaining properties
outliner.appendPropertyTRs(div, plist, false, function (pred, _inverse) {
return !(pred.uri in predicateURIsDone);
});
outliner.appendPropertyTRs(div, qlist, true, function (pred, _inverse) {
return !(pred.uri in predicateURIsDone);
});
}); // fetch
}
// end of render tranasaction instance
// ////////////////////////////////////////////////////////////////////
//
// Render the transactions in a Trip
//
} else if (t['http://www.w3.org/ns/pim/trip#Trip']) {
/*
var query = new $rdf.Query(UI.utils.label(subject))
var vars = [ 'date', 'transaction', 'comment', 'type', 'in_USD']
var v = {}
vars.map(function(x){query.vars.push(v[x]=$rdf.variable(x))}) // Only used by UI
query.pat.add(v['transaction'], TRIP('trip'), subject)
var opt = kb.formula()
opt.add(v['transaction'], ns.rdf('type'), v['type']); // Issue: this will get stored supertypes too
query.pat.optional.push(opt)
query.pat.add(v['transaction'], UI.ns.qu('date'), v['date'])
var opt = kb.formula()
opt.add(v['transaction'], ns.rdfs('comment'), v['comment'])
query.pat.optional.push(opt)
//opt = kb.formula()
query.pat.add(v['transaction'], UI.ns.qu('in_USD'), v['in_USD'])
//query.pat.optional.push(opt)
var tableDiv = UI.widgets.renderTableViewPane(dom, {'query': query, 'onDone': calculations} )
div.appendChild(tableDiv)
*/
const calculations = function () {
const total = {};
const yearTotal = {};
const yearCategoryTotal = {};
const trans = kb.each(undefined, TRIP('trip'), subject);
const bankStatements = trans.map(function (t) {
return t.doc();
});
kb.fetcher.load(bankStatements).then(function () {
trans.forEach(function (t) {
const date = kb.the(t, ns.qu('date'));
const year = date ? ('' + date.value).slice(0, 4) : '????';
let ty = kb.the(t, ns.rdf('type')); // @@ find most specific type
// complain(" -- one trans: "+t.uri + ' -> '+kb.any(t, UI.ns.qu('in_USD')))
if (!ty) ty = UI.ns.qu('ErrorNoType');
if (ty && ty.uri) {
const tyuri = ty.uri;
if (!yearTotal[year]) yearTotal[year] = 0.0;
if (!yearCategoryTotal[year]) yearCategoryTotal[year] = {};
if (!total[tyuri]) total[tyuri] = 0.0;
if (!yearCategoryTotal[year][tyuri]) {
yearCategoryTotal[year][tyuri] = 0.0;
}
const lit = kb.any(t, UI.ns.qu('in_USD'));
if (!lit) {
complain('@@ No amount in USD: ' + lit + ' for ' + t);
}
if (lit) {
const amount = parseFloat(lit.value);
total[tyuri] += amount;
yearCategoryTotal[year][tyuri] += amount;
yearTotal[year] += amount;
}
}
});
const types = [];
let grandTotal = 0.0;
const years = [];
let i;
for (const y in yearCategoryTotal) {
// @@ TODO: Write away the need for exception on next line
if (Object.prototype.hasOwnProperty.call(yearCategoryTotal, y)) {
years.push(y);
}
}
years.sort();
const ny = years.length;
let cell;
const table = div.appendChild(dom.createElement('table'));
table.setAttribute('style', 'font-size: 120%; margin-left:auto; margin-right:1em; margin-top: 1em; border: 0.05em solid gray; padding: 1em;');
if (ny > 1) {
const header = table.appendChild(dom.createElement('tr'));
header.appendChild(headerCell(''));
for (i = 0; i < ny; i++) {
header.appendChild(headerCell(years[i]));
}
header.appendChild(headerCell('total'));
}
for (const uri in total) {
// @@ TODO: Write away the need for exception on next line
if (Object.prototype.hasOwnProperty.call(total, uri)) {
types.push(uri);
grandTotal += total[uri];
}
}
types.sort();
let row, label, z;
for (let j = 0; j < types.length; j++) {
const cat = kb.sym(types[j]);
row = table.appendChild(dom.createElement('tr'));
label = row.appendChild(dom.createElement('td'));
label.textContent = UI.utils.label(cat);
if (ny > 1) {
for (i = 0; i < ny; i++) {
z = yearCategoryTotal[years[i]][types[j]];
cell = row.appendChild(numericCell(z, true));
}
}
row.appendChild(numericCell(total[types[j]], true));
}
// Trailing totals
if (types.length > 1) {
row = table.appendChild(dom.createElement('tr'));
row.appendChild(headerCell('total'));
if (ny > 1) {
for (i = 0; i < ny; i++) {
z = yearTotal[years[i]];
cell = row.appendChild(numericCell(z ? d2(z) : ''));
}
}
cell = row.appendChild(numericCell(grandTotal));
cell.setAttribute('style', 'font-weight: bold; text-align: right;');
}
const tab = transactionTable(dom, trans);
tab.setAttribute('style', 'margin-left:auto; margin-right:1em; margin-top: 1em; border: padding: 1em;');
div.appendChild(tab);
UI.widgets.attachmentList(dom, subject, div);
}).catch(function (e) {
complain('Error loading transactions: ' + e);
});
};
calculations();
} // if
return div;
}
}; // ends