Skip to content

Commit

Permalink
Merge pull request #118 from msramalho/refactor/bills-extractor
Browse files Browse the repository at this point in the history
- Support for "All Day" events
- Migrate Bills extractor to `EventExtractor`, use modal rather than inline dropdowns, include fees in total amount
- New extractor for bills with ATM references, BillsPaymentRefs
  • Loading branch information
fabiodrg committed Apr 25, 2022
2 parents 472d332 + bb1f4ed commit 53414e6
Show file tree
Hide file tree
Showing 17 changed files with 1,698 additions and 234 deletions.
2 changes: 1 addition & 1 deletion jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"compilerOptions": {
"target": "es6"
},
"include": ["src/js/**/*"]
"include": ["src/js/**/*", "src/test/**/*"]
}
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"https://*.up.pt/*gpag_ccorrente_geral.conta_corrente_view*",
"https://*.up.pt/*GPAG_CCORRENTE_GERAL.CONTA_CORRENTE_VIEW*"
],
"js": ["js/extractors/bills.js"],
"js": ["js/extractors/bills.js", "js/extractors/bills_payment_refs.js"],
"run_at": "document_end"
},
{
Expand Down
220 changes: 158 additions & 62 deletions src/js/extractors/bills.js
Original file line number Diff line number Diff line change
@@ -1,85 +1,181 @@
class Bill extends Extractor {
class Bills extends EventExtractor {
/** @type {boolean} */
ignoreHasPaymentRefs = null;

constructor() {
super();
this.tableSelector = '#tab0 > table > tbody > tr';
this.ready();
}

structure() {
return {
extractor: "bills",
name: "Pending Bills",
description: "Extracts all pending bills from sigarra",
icon: "bill.png",
parameters: [{
name: "description",
description: "eg: Propinas - Mestrado Integrado em Engenharia Informática e Computação - Prestação 1"
}, {
name: "date",
description: "The payment deadline eg: 2019-03-31"
}, {
name: "amount",
description: "The amount to pay e.g. 99,90€"
}],
storage: {
text: [{
name: "title",
default: "${description}"
}],
textarea: [{
name: "description",
default: "Amount: ${amount}"
}]
}
}
return super.structure(
{
extractor: "bills",
name: "Pending Bills",
description: "Extracts all pending bills from sigarra",
icon: "bill.png",
parameters: [
{
name: "description",
description:
"e.g. Propinas - Mestrado Integrado em Engenharia Informática e Computação - Prestação 1",
},
{
name: "deadline",
description: "The payment deadline, e.g. 2019-03-31",
},
{
name: "amount",
description: "The amount to pay, e.g. 99,90 €",
},
],
storage: {
boolean: [
{
name: "ignoreHasPaymentRefs",
default: true,
},
],
},
},
"${description}",
"Amount: ${amount}",
"",
false,
CalendarEventStatus.FREE
);
}

attachIfPossible() {
$('<th>Sigtools</th>').appendTo(this._getBillsHeader()[0])
this._getBills().forEach((element, index) => {
let event = this._parsePendingBill(element);
let drop = getDropdown(event, this, undefined, {
target: "dropdown_" + index,
divClass: "dropdown removeFrame",
divStyle: "display:contents;",
dropdownStyle: "position: absolute;"
});
$('<td></td>').appendTo(element).append(drop[0]);
}, this);
// parse all pending bills
const events = this.getEvents();
if (events.length === 0) return;

// create button for opening the modal
const $calendarBtn = createElementFromString(
`<a class="calendarBtn"
style="display: inline-block;"
title="Save bills deadlines to your Calendar">
<img src="${chrome.extension.getURL("icons/calendar.svg")}"/>
</a>`
);
$calendarBtn.addEventListener("click", (e) => createEventsModal(events));

setDropdownListeners(this, undefined);
// insert the button before the table
this.$pendingBillsTable().insertAdjacentElement("beforebegin", $calendarBtn);
}

_getBills() {
let _billsDOM = $(this.tableSelector); // array-like object
return Array.prototype.slice.call(_billsDOM, 1); // array object, removing header row
/**
* The DOM element for the table that lists the pending bills
* @returns {HTMLElement | null}
*/
$pendingBillsTable() {
const $tables = Sig.doc.querySelectorAll("#tab0 > table");
return $tables.length > 0 ? $tables[0] : null;
}

_getBillsHeader() {
return $(this.tableSelector);
/**
* All table rows for pending bills. Each row corresponds to a bill
* @returns
*/
$pendingBillsTableRows() {
const $table = this.$pendingBillsTable();
return $table ? $table.querySelectorAll("tbody > tr") : null;
}

_parsePendingBill(billEl) {
let getDateFromBill = function (index) {
let dateFromBill = Bill._getDateOrUndefined($(billEl).children(`:nth(${index})`).text());
if (dateFromBill === undefined) dateFromBill = new Date();
return dateFromBill;
/**
* Creates calendar events for all pending bills, as long as they have
* a deadline
*
* @returns {CalendarEvent[]}
*/
getEvents() {
const eventsLst = [];

for (const bill of this.parsePendingBills()) {
// If the bill has a deadline, create an event for it, otherwise skip
//
// Also consider the 'ignore if has payment ref' user option
// The reasoning is if the ATM button does not exist for a pending
// bill has an ATM, the same bill is listed in another table with
// the ATM reference. See BillsPaymentRefs extractor. If the user
// wants, it can be skipped.

if (bill.deadline && (!this.ignoreHasPaymentRefs || bill.hasATMBtn)) {
const ev = CalendarEvent.initAllDayEvent(
this.getTitle(bill),
this.getDescription(bill),
this.isHTML,
bill.deadline
)
.setLocation(this.getLocation(bill))
.setStatus(CalendarEventStatus.FREE);
eventsLst.push(ev);
}
}
return {
description: $(billEl).children(':nth(2)').text(),
amount: $(billEl).children(':nth(7)').text(),
from: getDateFromBill(3),
to: getDateFromBill(4),
date: getDateFromBill(4),
location: "",
download: false
};

return eventsLst;
}

static _getDateOrUndefined(dateString) {
return dateString ? new Date(dateString) : undefined
/**
* Parses all pending bills found in the table
*
* @returns {{
* description: string,
* amount: string,
* deadline: string | null,
* }[]}
*/
parsePendingBills() {
/**
* @param {number} index The column index, starting at 1
* @returns {string | null}
*/
const getColumnAsText = ($tr, index) => {
const $td = $tr.querySelector(`td:nth-child(${index})`);
return $td ? $td.innerText.trim() : null;
};

/**
* @param {number} index The column index, starting at 1
* @returns {Number | null}
*/
const getColumnAsCurrency = ($tr, index) => {
const value = getColumnAsText($tr, index).replace("€", "").trim();
// note that parseFloat only supports decimal literals,
// https://262.ecma-international.org/5.1/#sec-A.2
// sigarra numbers are formatted in portuguese locale, therefore the
// , must be replaced by .
return value && Number.parseFloat(value.replace(".", "").replace(",", "."));
};

// get all <tr> for the pendings bills
const $bills = this.$pendingBillsTableRows();
if (!$bills) return [];

// iterate over the table rows, each row => a bill
// skip first row, it is a table header, inside tbody :)
const pendingBills = [];

for (let i = 1; i < $bills.length; i++) {
const $bill = $bills[i];

// parse the initial bill amount
const initialAmount = getColumnAsCurrency($bill, 8);
// parse the fees value if it exists
const fees = getColumnAsCurrency($bill, 10) || 0;
// append new bill information
pendingBills.push({
description: getColumnAsText($bill, 3),
amount: Intl.NumberFormat("pt-PT", { style: "currency", currency: "EUR" }).format(initialAmount + fees),
deadline: getColumnAsText($bill, 5) || null,
hasATMBtn: $bill.querySelector(`td:nth-child(9)`).childElementCount !== 0,
});
}

return pendingBills;
}
}

// add an instance to the EXTRACTORS variable, and also trigger attachIfPossible due to constructor
EXTRACTORS.push(new Bill());
EXTRACTORS.push(new Bills());

0 comments on commit 53414e6

Please sign in to comment.