Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance the bills extractor #118

Merged
merged 21 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2bd41a5
style(extractor/bills): Format code with prettier
fabiodrg Mar 30, 2022
0f4a4a5
refactor(extractor/bills): Use 'EventExtractor' (#116). Replace inline
fabiodrg Mar 30, 2022
93b53d3
refactor(CalendarEvent): Remove optional parameters from constructor.
fabiodrg Apr 2, 2022
135595a
feat(extractor/bills): Create events as 'All Day'. Include fees in total
fabiodrg Apr 2, 2022
1a79df7
test: Implement workaround for mocking 'document' (#90)
fabiodrg Apr 2, 2022
5669942
test: Update unit tests for bill extractor
fabiodrg Apr 2, 2022
191cbac
test: Start adding generic tests to 'Extractors' instances
fabiodrg Apr 3, 2022
1009ac7
test: Enhance mocking object for 'chrome' object
fabiodrg Apr 3, 2022
9ef2634
enhance(EventExtractor): Support nested objects for validating parameter
fabiodrg Apr 3, 2022
1b23632
fix(EventExtractor): Properly escape all newlines before using `eval` to
fabiodrg Apr 3, 2022
740be53
test: Fix unit-tests for DataTable extractor
fabiodrg Apr 3, 2022
48f66e0
docs(extractor/Bills): Add further documentation
fabiodrg Apr 3, 2022
e4edcb6
fix(extractors/Bills): Update dropdown title attribute
fabiodrg Apr 3, 2022
6fdc624
feat(extractors/BillsPaymentRefs): New extractor for pending bills with
fabiodrg Apr 3, 2022
187fcd7
test: Add unit tests for BillsPaymentRefs extractor
fabiodrg Apr 3, 2022
1a47119
fix(extractor/ExamSupervisions): Update due to changes on CalendarEvent
fabiodrg Apr 3, 2022
66554a4
feat(CalendarUrlGenerator): Support "All Day" events when generating …
fabiodrg Apr 4, 2022
38c1422
feat(utils): Add function for deep object merging
fabiodrg Apr 4, 2022
c2d59c5
enhance(exractors/EventExtractor): Use deep merging for the structure
fabiodrg Apr 4, 2022
caa4e07
feat(extractor/Bills): User-option for ignoring pending bills with
fabiodrg Apr 4, 2022
bb1f4ed
docs: Improve docs for the CalendarEvent class
fabiodrg Apr 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
Loading