Applied the summary helper and JavaScripts to the transaction form, so that the transaction form works in the accounting application.

This commit is contained in:
2020-08-05 07:48:50 +08:00
parent 2c6256b497
commit b2ce0eff54
10 changed files with 1539 additions and 2 deletions

View File

@ -0,0 +1,36 @@
/* The Mia Website
* summary-helper.css: The style sheet for the summary helper
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2020/4/3
*/
.summary-container {
padding: 0 0 1em 0;
}
.summary-tab-content {
padding-top: 1em;
}
.summary-categories-known {
max-height: 200px;
overflow-y: scroll;
}
.summary-categories-known .btn-summary-helper {
margin: 0.1em;
}

View File

@ -21,7 +21,7 @@
* First written: 2019/9/17
*/
.subject-line {
.account-line {
font-size: 0.833em;
}
.amount {

View File

@ -0,0 +1,63 @@
/* The Mia Website
* regular-payments.js: The JavaScript for the regular payments
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2020/4/4
*/
/**
* Returns the regular payment data.
*
* @returns {{debits: [], credits: []}}
*/
function getRegularPayments() {
const today = new Date($("#txn-date").get(0).value);
const thisMonth = today.getMonth() + 1;
const lastMonth = (thisMonth + 10) % 12 + 1;
let regular = {
debit: [],
credit: [],
};
regular.debit.push({
title: "共同生活基金",
summary: "共同生活基金" + thisMonth + "月",
account: "62651",
});
regular.debit.push({
title: "電話費",
summary: "電話費" + lastMonth + "月",
account: "62562",
});
regular.debit.push({
title: "健保",
summary: "健保" + lastMonth + "月",
account: "62621",
});
regular.debit.push({
title: "國民年金",
summary: "國民年金" + lastMonth + "月",
account: "13141",
});
regular.credit.push({
title: "薪水",
summary: lastMonth + "月份薪水",
account: "46116",
});
return regular;
}

View File

@ -0,0 +1,542 @@
/* The Mia Website
* summary-helper.js: The JavaScript for the summary helper
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2020/4/3
*/
// Initializes the summary helper JavaScript.
$(function () {
loadSummaryCategoryData();
$("#summary-helper-form")
.on("submit", function () {
return false;
});
$(".record-summary")
.attr("data-toggle", "modal")
.attr("data-target", "#summary-modal")
.on("click", function () {
startSummaryHelper(this);
});
$("#summary-summary")
.on("change", function () {
this.value = this.value.trim();
parseSummaryForHelper(this.value);
});
$(".summary-tab")
.on("click", function () {
switchSummaryTab(this);
});
// The general categories
$("#summary-general-category")
.on("change", function () {
setSummaryGeneralCategoryButtons(this.value);
setGeneralCategorySummary();
setSummaryAccount("general", this.value);
});
// The travel routes
$("#summary-travel-category")
.on("change", function () {
setSummaryTravelCategoryButtons(this.value);
setSummaryAccount("travel", this.value);
});
$(".summary-travel-part")
.on("change", function () {
this.value = this.value.trim();
setTravelSummary();
});
$(".btn-summary-travel-direction")
.on("click", function () {
$("#summary-travel-direction").get(0).value = this.innerText;
setSummaryTravelDirectionButtons(this.innerText);
setTravelSummary();
});
// The bus routes
$("#summary-bus-category")
.on("change", function () {
setSummaryBusCategoryButtons(this.value);
setSummaryAccount("bus", this.value);
});
$(".summary-bus-part")
.on("change", function () {
this.value = this.value.trim();
setBusSummary();
});
$("#summary-count")
.on("change", function () {
updateSummaryCount();
});
$("#summary-confirm")
.on("click", function () {
applySummaryToAccountingRecord();
});
});
/**
* The known categories
* @type {object}
* @private
*/
let summaryCategories = null;
/**
* The known categories and their corresponding accounts
* @type {object}
* @private
*/
let summaryAccounts = null;
/**
* The account that corresponds to this category
* @type {null|string}
* @private
*/
let summaryAccount = null;
/**
* Loads the summary category data.
*
* @private
*/
function loadSummaryCategoryData() {
const data = JSON.parse($("#summary-categories").val());
summaryCategories = {};
summaryAccounts = {};
["debit", "credit"].forEach(function (type) {
summaryCategories[type] = {};
summaryAccounts[type] = {};
["general", "travel", "bus"].forEach(function (format) {
summaryCategories[type][format] = [];
summaryAccounts[type][format] = {};
if (type + "-" + format in data) {
data[type + "-" + format]
.forEach(function (item) {
summaryCategories[type][format].push(item[0]);
summaryAccounts[type][format][item[0]] = item[1];
});
}
});
});
}
/**
* Starts the summary helper.
*
* @param {HTMLInputElement} summary the summary input element
*/
function startSummaryHelper(summary) {
const type = summary.id.substring(0, summary.id.indexOf("-"));
const no = parseInt(summary.id.substring(type.length + 1, summary.id.indexOf("-", type.length + 1)));
$("#summary-record").get(0).value = type + "-" + no;
$("#summary-summary").get(0).value = summary.value;
// Loads the know summary categories into the summary helper
loadKnownSummaryCategories(type);
// Parses the summary and sets up the summary helper
parseSummaryForHelper(summary.value);
// Focus on the summary input
setTimeout(function () {
$("#summary-summary").get(0).focus();
}, 100);
}
/**
* Loads the known summary categories into the summary helper.
*
* @param {string} type the record type, either "debit" or "credit"
* @private
*/
function loadKnownSummaryCategories(type) {
["general", "travel", "bus"].forEach(function (format) {
const knownCategories = $("#summary-" + format + "-categories-known");
knownCategories.html("");
summaryCategories[type][format].forEach(function (item) {
knownCategories.append(
$("<span/>")
.addClass("btn btn-outline-primary")
.addClass("btn-summary-helper")
.addClass("btn-summary-" + format + "-category")
.text(item));
});
});
// The regular payments
const regularPayments = getRegularPayments();
["debit", "credit"].forEach(function (type) {
summaryCategories[type].regular = [];
summaryAccounts[type].regular = {};
regularPayments[type].forEach(function (item) {
summaryCategories[type].regular.push(item);
summaryAccounts[type].regular[item.title] = item.account;
});
});
const regularPaymentButtons = $("#summary-regular-payments");
regularPaymentButtons.html("");
summaryCategories[type].regular.forEach(function (item) {
regularPaymentButtons.append(
$("<span/>")
.attr("title", item.summary)
.addClass("btn btn-outline-primary")
.addClass("btn-summary-helper")
.addClass("btn-summary-regular")
.text(item.title));
});
$(".btn-summary-general-category")
.on("click", function () {
$("#summary-general-category").get(0).value = this.innerText;
setSummaryGeneralCategoryButtons(this.innerText);
setGeneralCategorySummary();
setSummaryAccount("general", this.innerText);
});
$(".btn-summary-travel-category")
.on("click", function () {
$("#summary-travel-category").get(0).value = this.innerText;
setSummaryTravelCategoryButtons(this.innerText);
setTravelSummary();
setSummaryAccount("travel", this.innerText);
});
$(".btn-summary-bus-category")
.on("click", function () {
$("#summary-bus-category").get(0).value = this.innerText;
setSummaryBusCategoryButtons(this.innerText);
setBusSummary();
setSummaryAccount("bus", this.innerText);
});
$(".btn-summary-regular")
.on("click", function () {
$("#summary-summary").get(0).value = this.title;
setSummaryRegularPaymentButtons(this.innerText);
setSummaryAccount("regular", this.innerText);
});
}
/**
* Parses the summary and sets up the summary helper.
*
* @param {string} summary the summary
*/
function parseSummaryForHelper(summary) {
// Parses the summary and sets up the category helpers.
parseSummaryForCategoryHelpers(summary);
// The number of items
const pos = summary.lastIndexOf("×");
let count = 1;
if (pos !== -1) {
count = parseInt(summary.substr(pos + 1));
}
if (count === 0) {
count = 1;
}
$("#summary-count").get(0).value = count;
}
/**
* Parses the summary and sets up the category helpers.
*
* @param {string} summary the summary
*/
function parseSummaryForCategoryHelpers(summary) {
$(".btn-summary-helper")
.removeClass("btn-primary")
.addClass("btn-outline-primary");
$("#btn-summary-one-way")
.removeClass("btn-outline-primary")
.addClass("btn-primary");
$(".summary-helper-input").each(function () {
this.classList.remove("is-invalid");
if (this.id === "summary-travel-direction") {
this.value = $("#btn-summary-one-way").text();
} else {
this.value = "";
}
});
// A bus route
const matchBus = summary.match(/^(.+)—(.+)—(.+)→(.+?)(?:×[0-9]+)?$/);
if (matchBus !== null) {
$("#summary-bus-category").get(0).value = matchBus[1];
setSummaryBusCategoryButtons(matchBus[1]);
setSummaryAccount("bus", matchBus[1]);
$("#summary-bus-route").get(0).value = matchBus[2];
$("#summary-bus-from").get(0).value = matchBus[3];
$("#summary-bus-to").get(0).value = matchBus[4];
switchSummaryTab($("#summary-tab-bus").get(0));
return;
}
// A general travel route
const matchTravel = summary.match(/^(.+)—(.+)([→|↔])(.+?)(?:×[0-9]+)?$/);
if (matchTravel !== null) {
$("#summary-travel-category").get(0).value = matchTravel[1];
setSummaryTravelCategoryButtons(matchTravel[1]);
setSummaryAccount("travel", matchTravel[1]);
$("#summary-travel-from").get(0).value = matchTravel[2];
$("#summary-travel-direction").get(0).value = matchTravel[3];
setSummaryTravelDirectionButtons(matchTravel[3]);
$("#summary-travel-to").get(0).value = matchTravel[4];
switchSummaryTab($("#summary-tab-travel").get(0));
return;
}
// A general category
const generalCategoryTab = $("#summary-tab-category").get(0);
const matchCategory = summary.match(/^(.+)—.+(?:×[0-9]+)?$/);
if (matchCategory !== null) {
$("#summary-general-category").get(0).value = matchCategory[1];
setSummaryGeneralCategoryButtons(matchCategory[1]);
setSummaryAccount("general", matchCategory[1]);
switchSummaryTab(generalCategoryTab);
return;
}
// A general summary text
setSummaryGeneralCategoryButtons(null);
setSummaryAccount("general", null);
switchSummaryTab(generalCategoryTab);
}
/**
* Switch the summary helper to tab.
*
* @param {HTMLElement} tab the navigation tab corresponding to a type
* of helper
* @private
*/
function switchSummaryTab(tab) {
const tabName = tab.id.substr("summary-tab-".length); // "summary-tab-"
$(".summary-tab-content").each(function () {
if (this.id === "summary-tab-content-" + tabName) {
this.classList.remove("d-none");
} else {
this.classList.add("d-none");
}
});
$(".summary-tab").each(function () {
if (this.id === tab.id) {
this.classList.add("active");
} else {
this.classList.remove("active");
}
});
}
/**
* Sets the known general category buttons.
*
* @param {string} category the general category
*/
function setSummaryGeneralCategoryButtons(category) {
$(".btn-summary-general-category").each(function () {
if (this.innerText === category) {
this.classList.remove("btn-outline-primary");
this.classList.add("btn-primary");
} else {
this.classList.add("btn-outline-primary");
this.classList.remove("btn-primary");
}
});
}
/**
* Sets the summary of a general category.
*
*/
function setGeneralCategorySummary() {
const summary = $("#summary-summary").get(0);
const dashPos = summary.value.indexOf("—");
if (dashPos !== -1) {
summary.value = summary.value.substring(dashPos + 1);
}
const category = $("#summary-general-category").get(0).value;
if (category !== "") {
summary.value = category + "—" + summary.value;
}
}
/**
* Sets the known travel category buttons.
*
* @param {string} category the travel category
*/
function setSummaryTravelCategoryButtons(category) {
$(".btn-summary-travel-category").each(function () {
if (this.innerText === category) {
this.classList.remove("btn-outline-primary");
this.classList.add("btn-primary");
} else {
this.classList.add("btn-outline-primary");
this.classList.remove("btn-primary");
}
});
}
/**
* Sets the summary of a general travel.
*
*/
function setTravelSummary() {
$(".summary-travel-part").each(function () {
if (this.value === "") {
this.classList.add("is-invalid");
} else {
this.classList.remove("is-invalid");
}
});
let summary = $("#summary-travel-category").get(0).value
+ "—" + $("#summary-travel-from").get(0).value
+ $("#summary-travel-direction").get(0).value
+ $("#summary-travel-to").get(0).value;
const count = parseInt($("#summary-count").get(0).value);
if (count !== 1) {
summary = summary + "×" + count;
}
$("#summary-summary").get(0).value = summary;
}
/**
* Sets the known summary travel direction buttons.
*
* @param {string} direction the known summary travel direction
*/
function setSummaryTravelDirectionButtons(direction) {
$(".btn-summary-travel-direction").each(function () {
if (this.innerText === direction) {
this.classList.remove("btn-outline-primary");
this.classList.add("btn-primary");
} else {
this.classList.add("btn-outline-primary");
this.classList.remove("btn-primary");
}
});
}
/**
* Sets the known bus category buttons.
*
* @param {string} category the bus category
*/
function setSummaryBusCategoryButtons(category) {
$(".btn-summary-bus-category").each(function () {
if (this.innerText === category) {
this.classList.remove("btn-outline-primary");
this.classList.add("btn-primary");
} else {
this.classList.add("btn-outline-primary");
this.classList.remove("btn-primary");
}
});
}
/**
* Sets the summary of a bus travel.
*
*/
function setBusSummary() {
$(".summary-bus-part").each(function () {
if (this.value === "") {
this.classList.add("is-invalid");
} else {
this.classList.remove("is-invalid");
}
});
let summary = $("#summary-bus-category").get(0).value
+ "—" + $("#summary-bus-route").get(0).value
+ "—" + $("#summary-bus-from").get(0).value
+ "→" + $("#summary-bus-to").get(0).value;
const count = parseInt($("#summary-count").get(0).value);
if (count !== 1) {
summary = summary + "×" + count;
}
$("#summary-summary").get(0).value = summary;
}
/**
* Sets the regular payment buttons.
*
* @param {string} category the regular payment
*/
function setSummaryRegularPaymentButtons(category) {
$(".btn-summary-regular").each(function () {
if (this.innerText === category) {
this.classList.remove("btn-outline-primary");
this.classList.add("btn-primary");
} else {
this.classList.add("btn-outline-primary");
this.classList.remove("btn-primary");
}
});
}
/**
* Sets the account for this summary category.
*
* @param {string} format the category format, either "general",
* "travel", or "bus".
* @param {string} category the category
*/
function setSummaryAccount(format, category) {
const recordId = $("#summary-record").get(0).value;
const type = recordId.substring(0, recordId.indexOf("-"));
if (category in summaryAccounts[type][format]) {
summaryAccount = summaryAccounts[type][format][category];
} else {
summaryAccount = null;
}
}
/**
* Updates the count.
*
* @private
*/
function updateSummaryCount() {
const count = parseInt($("#summary-count").val());
const summary = $("#summary-summary").get(0);
const pos = summary.value.lastIndexOf("×");
if (pos === -1) {
if (count !== 1) {
summary.value = summary.value + "×" + count;
}
} else {
const content = summary.value.substring(0, pos);
if (count === 1) {
summary.value = content;
} else {
summary.value = content + "×" + count;
}
}
}
/**
* Applies the summary to the accounting record.
*
* @private
*/
function applySummaryToAccountingRecord() {
const recordId = $("#summary-record").get(0).value;
const summary = $("#" + recordId + "-summary").get(0);
summary.value = $("#summary-summary").get(0).value.trim();
const account = $("#" + recordId + "-account").get(0);
if (summaryAccount !== null && account.value === "") {
account.value = summaryAccount;
}
setTimeout(function () {
summary.blur();
}, 100);
}

View File

@ -0,0 +1,703 @@
/* The Mia Website
* transaction-form.js: The JavaScript for the transaction form
*/
/* Copyright (c) 2019-2020 imacat.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Author: imacat@mail.imacat.idv.tw (imacat)
* First written: 2019/9/19
*/
// Initializes the page JavaScript.
$(function () {
getAccountOptions();
resetRecordButtons();
$("#txn-date")
.on("blur", function () {
validateDate();
});
$(".record-account")
.on("focus", function () {
removeBlankOption(this);
})
.on("blur", function () {
validateAccount(this);
});
$(".record-summary")
.on("blur", function () {
validateSummary(this);
});
$(".record-amount")
.on("blur", function () {
validateAmount(this);
})
.on("change", function () {
updateTotalAmount(this);
validateBalance();
});
$("#txn-note")
.on("blur", function () {
validateNote();
});
$("#txn-form")
.on("submit", function () {
return validateForm();
});
$(".btn-new")
.on("click", function () {
addNewRecord(this);
});
$(".btn-del-record")
.on("click", function () {
deleteRecord(this);
});
});
/**
* The localized messages
* @type {Array.}
* @private
*/
let l10n = null;
/**
* Returns the localization of a message.
*
* @param {string} key the message key
* @returns {string} the localized message
* @private
*/
function __(key) {
if (l10n === null) {
l10n = JSON.parse($("#l10n-messages").val());
}
if (key in l10n) {
return l10n[key];
}
return key;
}
/**
* Returns whether this is a transfer transaction.
*
* @returns {boolean} true if this is a transfer transaction, or false
* otherwise
* @private
*/
function isTransfer() {
return $("#debit-records").length > 0 && $("#credit-records").length > 0;
}
/**
* The account options
* @type {Array.}
* @private
*/
let accountOptions;
/**
* Obtains the account options.
*
* @private
*/
function getAccountOptions() {
const request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
accountOptions = JSON.parse(this.responseText);
$(".record-account").each(function () {
initializeAccountOptions(this);
});
}
};
request.open("GET", $("#account-option-url").val(), true);
request.send();
}
/**
* Initialize the account options.
*
* @param {HTMLSelectElement} account the account select element
* @private
*/
function initializeAccountOptions(account) {
const jAccount = $(account);
const type = account.id.substring(0, account.id.indexOf("-"));
const selectedAccount = account.value;
let isCash = false;
if (type === "debit") {
isCash = ($(".credit-record").length === 0);
} else if (type === "credit") {
isCash = ($(".debit-record").length === 0);
}
jAccount.html("");
if (selectedAccount === "") {
jAccount.append($("<option/>"));
}
const headerInUse = $("<option/>")
.attr("disabled", "disabled")
.text(accountOptions["header_in_use"]);
jAccount.append(headerInUse);
accountOptions[type + "_in_use"].forEach(function (item) {
// Skips the cash account on cash transactions.
if (item["code"] === 1111 && isCash) {
return;
}
const option = $("<option/>")
.attr("value", item["code"])
.text(item["code"] + " " + item["title"]);
if (String(item["code"]) === selectedAccount) {
option.attr("selected", "selected");
}
jAccount.append(option);
});
const headerNotInUse = $("<option/>")
.attr("disabled", "disabled")
.text(accountOptions["header_not_in_use"]);
jAccount.append(headerNotInUse);
accountOptions[type + "_not_in_use"].forEach(function (item) {
const option = $("<option/>")
.attr("value", item["code"])
.text(item["code"] + " " + item["title"]);
if (String(item["code"]) === selectedAccount) {
option.attr("selected", "selected");
}
jAccount.append(option);
});
}
/**
* Removes the dummy blank option.
*
* @param {HTMLSelectElement} select the select element
* @private
*/
function removeBlankOption(select) {
$(select).children().each(function () {
if (this.value === "" && !this.disabled) {
$(this).remove();
}
});
}
/**
* Updates the total amount.
*
* @param {HTMLButtonElement|HTMLInputElement} element the amount
* element that
* changed, or the
* button that
* was hit to
* delete a record
* @private
*/
function updateTotalAmount(element) {
const type = element.id.substring(0, element.id.indexOf("-"));
let total = 0;
$("." + type + "-to-sum").each(function () {
if (this.value !== "") {
total += parseInt(this.value);
}
});
total = String(total);
while (total.match(/^[1-9][0-9]*[0-9]{3}/)) {
total = total.replace(/^([1-9][0-9]*)([0-9]{3})/, "$1,$2");
}
$("#" + type + "-total").text(total);
}
/**
* Adds a new accounting record.
*
* @param {HTMLButtonElement} button the button element that was hit
* to add a new record
* @private
*/
function addNewRecord(button) {
const type = button.id.substring(0, button.id.indexOf("-"));
// Finds the new number that is the maximum number plus 1.
let newNo = 0;
$("." + type + "-record").each(function () {
const no = parseInt(this.id.substring(type.length + 1));
if (newNo < no) {
newNo = no;
}
});
newNo++;
// Inserts a new table row for the new accounting record.
insertNewRecord(type, newNo);
// Resets the order of the records.
resetRecordOrders(type);
// Resets the sort and delete buttons for the records.
resetRecordButtons();
}
/**
* Inserts a new accounting record.
*
* @param {string} type the record type, either "debit" or "credit"
* @param {number} newNo the number of this new accounting record
* @private
*/
function insertNewRecord(type, newNo) {
if (isTransfer()) {
insertNewTransferRecord(type, newNo);
} else {
insertNewNonTransferRecord(type, newNo);
}
}
/**
* Inserts a new accounting record for a transfer transaction.
*
* @param {string} type the record type, either "debit" or "credit"
* @param {number} newNo the number of this new accounting record
* @private
*/
function insertNewTransferRecord(type, newNo) {
const divAccount = createAccountBlock(type, newNo)
.addClass("col-sm-12");
const divAccountRow = $("<div/>")
.addClass("row")
.append(divAccount);
const divSummary = createSummaryBlock(type, newNo)
.addClass("col-lg-8");
const divAmount = createAmountBlock(type, newNo)
.addClass("col-lg-4");
const divSummaryAmountRow = $("<div/>")
.addClass("row")
.append(divSummary, divAmount);
const divContent = $("<div/>")
.append(divAccountRow)
.append(divSummaryAmountRow);
const divBtnGroup = createActionButtonBlock(
type, newNo, type + "-" + newNo + "-delete")
.addClass("btn-group-vertical");
const divActions = $("<div/>")
.append(divBtnGroup);
$("<li/>")
.attr("id", type + "-" + newNo)
.addClass("list-group-item")
.addClass("d-flex")
.addClass(type + "-record")
.append(divContent, divActions)
.appendTo("#" + type + "-records");
}
/**
* Inserts a new accounting record for a non-transfer transaction.
*
* @param {string} type the record type, either "debit" or "credit"
* @param {number} newNo the number of this new accounting record
* @private
*/
function insertNewNonTransferRecord(type, newNo) {
const divAccount = createAccountBlock(type, newNo)
.addClass("col-lg-6");
const divSummary = createSummaryBlock(type, newNo)
.addClass("col-sm-8");
const divAmount = createAmountBlock(type, newNo)
.addClass("col-sm-4");
const divSummaryAmountRow = $("<div/>")
.addClass("row")
.append(divSummary, divAmount);
const divSummaryAmount = $("<div/>")
.addClass("col-lg-6")
.append(divSummaryAmountRow);
const divContent = $("<div/>")
.addClass("row")
.append(divAccount, divSummaryAmount);
const divBtnGroup = createActionButtonBlock(
type, newNo, type + "-" + newNo + "-delete")
.addClass("btn-group")
.addClass("d-none d-lg-flex");
const divBtnGroupVertical = createActionButtonBlock(
type, newNo, type + "-" + newNo + "-m-delete")
.addClass("btn-group-vertical")
.addClass("d-lg-none");
const divActions = $("<div/>")
.append(divBtnGroup, divBtnGroupVertical);
$("<li/>")
.attr("id", type + "-" + newNo)
.addClass("list-group-item")
.addClass("d-flex")
.addClass("justify-content-between")
.addClass(type + "-record")
.append(divContent, divActions)
.appendTo("#" + type + "-records");
}
/**
* Creates and returns a new <div></div> account block.
*
* @param {string} type the record type, either "debit" or "credit"
* @param {number} newNo the number of this new accounting record
* @returns {JQuery<HTMLElement>} the new <div></div> account block
* @private
*/
function createAccountBlock(type, newNo) {
const order = $("<input/>")
.attr("id", type + "-" + newNo + "-ord")
.attr("type", "hidden")
.attr("name", type + "-" + newNo + "-ord")
.addClass(type + "-ord");
const account = $("<select/>")
.attr("id", type + "-" + newNo + "-account")
.attr("name", type + "-" + newNo + "-account")
.addClass("form-control")
.addClass("record-account")
.addClass(type + "-account")
.on("focus", function () {
removeBlankOption(this);
})
.on("blur", function () {
validateAccount(this);
})
.each(function () {
initializeAccountOptions(this);
});
const accountError = $("<div/>")
.attr("id", type + "-" + newNo + "-account-error")
.addClass("invalid-feedback");
return $("<div/>")
.append(order, account, accountError);
}
/**
* Creates and returns a new <div></div> summary block.
*
* @param {string} type the record type, either "debit" or "credit"
* @param {number} newNo the number of this new accounting record
* @returns {JQuery<HTMLElement>} the new <div></div> summary block
* @private
*/
function createSummaryBlock(type, newNo) {
const summary = $("<input/>")
.attr("id", type + "-" + newNo + "-summary")
.attr("type", "text")
.attr("name", type + "-" + newNo + "-summary")
.addClass("form-control")
.addClass("record-summary")
.on("blur", function () {
validateSummary(this);
});
if (typeof startSummaryHelper === "function") {
summary
.attr("data-toggle", "modal")
.attr("data-target", "#summary-modal")
.on("click", function () {
startSummaryHelper(this);
});
}
const summaryError = $("<div/>")
.attr("id", type + "-" + newNo + "-summary-error")
.addClass("invalid-feedback");
return $("<div/>")
.append(summary, summaryError);
}
/**
* Creates and returns a new <div></div> amount block.
*
* @param {string} type the record type, either "debit" or "credit"
* @param {number} newNo the number of this new accounting record
* @returns {JQuery<HTMLElement>} the new <div></div> amount block
* @private
*/
function createAmountBlock(type, newNo) {
const amount = $("<input/>")
.attr("id", type + "-" + newNo + "-amount")
.attr("type", "number")
.attr("name", type + "-" + newNo + "-amount")
.attr("min", 1)
.attr("required", "required")
.addClass("form-control")
.addClass("record-amount")
.addClass(type + "-to-sum")
.on("blur", function () {
validateAmount(this);
})
.on("change", function () {
updateTotalAmount(this);
validateBalance();
});
const amountError = $("<div/>")
.attr("id", type + "-" + newNo + "-amount-error")
.addClass("invalid-feedback");
return $("<div/>")
.append(amount, amountError);
}
/**
* Creates and returns a new <div></div> action button block.
*
* @param {string} type the record type, either "debit" or "credit"
* @param {number} newNo the number of this new accounting record
* @param {string} btnDelId the ID of the delete button
* @returns {JQuery<HTMLElement>} the new <div></div> button block
* @private
*/
function createActionButtonBlock(type, newNo, btnDelId) {
const btnSort = $("<button/>")
.attr("type", "button")
.addClass("btn btn-outline-secondary")
.addClass("btn-sort-" + type)
.append($("<i/>").addClass("fas fa-sort"));
const btnDelete = $("<button/>")
.attr("id", btnDelId)
.attr("type", "button")
.addClass("btn btn-danger")
.addClass("btn-del-record")
.addClass("btn-del-" + type)
.on("click", function () {
deleteRecord(this);
})
.append($("<i/>").addClass("fas fa-trash"));
return $("<div/>")
.addClass("btn-actions-" + type)
.append(btnSort, btnDelete);
}
/**
* Deletes a record.
*
* @param {HTMLButtonElement} button the button element that was hit
* to delete this record
* @private
*/
function deleteRecord(button) {
const type = button.id.substring(0, button.id.indexOf("-"));
const no = parseInt(button.id.substring(type.length + 1, button.id.indexOf("-", type.length + 1)));
$("#" + type + "-" + no).remove();
resetRecordOrders(type);
resetRecordButtons();
updateTotalAmount(button);
}
/**
* Resets the order of the records according to their appearance.
*
* @param {string} type the record type, either "debit" or "credit".
* @private
*/
function resetRecordOrders(type) {
const sorted = $("#" + type + "-records").sortable("toArray");
for (let i = 0; i < sorted.length; i++) {
$("#" + sorted[i] + "-ord")[0].value = i + 1;
}
}
/**
* Resets the sort and delete buttons for the records.
*
* @private
*/
function resetRecordButtons() {
["debit", "credit"].forEach(function (type) {
const records = $("." + type + "-record");
if (records.length > 1) {
$("#" + type + "-records").sortable({
classes: {
"ui-sortable-helper": "list-group-item-secondary",
},
cursor: "move",
cancel: "input, select",
stop: function () {
resetRecordOrders(type);
},
}).sortable("enable");
$(".btn-actions-" + type).removeClass("invisible");
} else if (records.length === 1) {
$("#" + type + "-records").sortable().sortable("disable");
$(".btn-actions-" + type).addClass("invisible");
}
});
}
/*******************
* Form Validation *
*******************/
/**
* Validates the form.
*
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateForm() {
let isValidated = true;
isValidated = isValidated && validateDate();
$(".record-account").each(function () {
isValidated = isValidated && validateAccount(this);
});
$(".record-summary").each(function () {
isValidated = isValidated && validateSummary(this);
});
$(".record-amount").each(function () {
isValidated = isValidated && validateAmount(this);
});
if (isTransfer()) {
isValidated = isValidated && validateBalance();
}
isValidated = isValidated && validateNote();
return isValidated;
}
/**
* Validates the date column.
*
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateDate() {
const date = $("#txn-date")[0];
const errorMessage = $("#txn-date-error");
if (date.value === "") {
date.classList.add("is-invalid");
errorMessage.text(__("Please fill in the date."));
return false;
}
date.classList.remove("is-invalid");
errorMessage.text("");
return true;
}
/**
* Validates the account column.
*
* @param {HTMLSelectElement} account the account selection element
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateAccount(account) {
const errorMessage = $("#" + account.id + "-error");
if (account.value === "") {
account.classList.add("is-invalid");
errorMessage.text(__("Please select the account."));
return false;
}
account.classList.remove("is-invalid");
errorMessage.text("");
return true;
}
/**
* Validates the summary column.
*
* @param {HTMLInputElement} summary the summary input element
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateSummary(summary) {
const errorMessage = $("#" + summary.id + "-error");
summary.value = summary.value.trim();
if (summary.value.length > 128) {
summary.classList.add("is-invalid");
errorMessage.text(__("This summary is too long (max. 128 characters)."));
return false;
}
summary.classList.remove("is-invalid");
errorMessage.text("");
return true;
}
/**
* Validates the amount column.
*
* @param {HTMLInputElement} amount the amount input element
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateAmount(amount) {
const errorMessage = $("#" + amount.id + "-error");
amount.value = amount.value.trim();
if (amount.value === "") {
amount.classList.add("is-invalid");
errorMessage.text(__("Please fill in the amount."));
return false;
}
amount.classList.remove("is-invalid");
errorMessage.text("");
return true;
}
/**
* Validates the balance between debit and credit records
*
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateBalance() {
const balanceRows = $(".balance-row");
const errorMessages = $(".balance-error");
let debitTotal = 0;
$(".debit-to-sum").each(function () {
if (this.value !== "") {
debitTotal += parseInt(this.value);
}
});
let creditTotal = 0;
$(".credit-to-sum").each(function () {
if (this.value !== "") {
creditTotal += parseInt(this.value);
}
});
if (debitTotal !== creditTotal) {
balanceRows.addClass("is-invalid");
errorMessages.text(__("The sum of debit and credit are inconsistent."))
return false;
}
balanceRows.removeClass("is-invalid");
errorMessages.text("");
return true;
}
/**
* Validates the note column.
*
* @returns {boolean} true if the validation succeed, or false
* otherwise
* @private
*/
function validateNote() {
const note = $("#txn-note")[0];
const errorMessage = $("#txn-note-error");
note.value = note.value.trim();
if (note.value.length > 128) {
note.classList.add("is-invalid");
errorMessage.text(__("This note is too long (max. 128 characters)."));
return false;
}
note.classList.remove("is-invalid");
errorMessage.text("");
return true;
}