/* The Mia! Accounting Project * journal-entry-account-selector.js: The JavaScript for the account selector of the journal entry form */ /* Copyright (c) 2023 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: 2023/2/28 */ "use strict"; /** * The account selector. * */ class JournalEntryAccountSelector { /** * The line item editor * @type {JournalEntryLineItemEditor} */ lineItemEditor; /** * Either "debit" or "credit" * @type {string} */ #debitCredit; /** * The button to clear the account * @type {HTMLButtonElement} */ #clearButton /** * The query input * @type {HTMLInputElement} */ #query; /** * The error message when the query has no result * @type {HTMLParagraphElement} */ #queryNoResult; /** * The option list * @type {HTMLUListElement} */ #optionList; /** * The options * @type {JournalEntryAccountOption[]} */ #options; /** * The more item to show all accounts * @type {HTMLLIElement} */ #more; /** * Whether to show all accounts * @type {boolean} */ #isShowMore = false; /** * Constructs an account selector. * * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor * @param debitCredit {string} either "debit" or "credit" */ constructor(lineItemEditor, debitCredit) { this.lineItemEditor = lineItemEditor this.#debitCredit = debitCredit; const prefix = `accounting-account-selector-${debitCredit}`; this.#query = document.getElementById(`${prefix}-query`); this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`); this.#optionList = document.getElementById(`${prefix}-option-list`); this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new JournalEntryAccountOption(this, element)); this.#more = document.getElementById(`${prefix}-more`); this.#clearButton = document.getElementById(`${prefix}-btn-clear`); this.#more.onclick = () => { this.#isShowMore = true; this.#more.classList.add("d-none"); this.#filterOptions(); }; this.#query.oninput = () => this.#filterOptions(); this.#clearButton.onclick = () => this.lineItemEditor.clearAccount(); } /** * Filters the options. * */ #filterOptions() { const codesInUse = this.#getCodesUsedInForm(); let isAnyMatched = false; for (const option of this.#options) { if (option.isMatched(this.#isShowMore, codesInUse, this.#query.value)) { option.setShown(true); isAnyMatched = true; } else { option.setShown(false); } } if (!isAnyMatched && this.#isShowMore) { this.#optionList.classList.add("d-none"); this.#queryNoResult.classList.remove("d-none"); } else { this.#optionList.classList.remove("d-none"); this.#queryNoResult.classList.add("d-none"); } } /** * Returns the account codes that are used in the form. * * @return {string[]} the account codes that are used in the form */ #getCodesUsedInForm() { const inUse = this.lineItemEditor.form.getAccountCodesUsed(this.#debitCredit); if (this.lineItemEditor.account !== null) { inUse.push(this.lineItemEditor.account.code); } return inUse } /** * The callback when the account selector is shown. * */ onOpen() { this.#query.value = ""; this.#isShowMore = false; this.#more.classList.remove("d-none"); this.#filterOptions(); for (const option of this.#options) { option.setActive(this.lineItemEditor.account !== null && option.code === this.lineItemEditor.account.code); } if (this.lineItemEditor.account === null) { this.#clearButton.classList.add("btn-secondary"); this.#clearButton.classList.remove("btn-danger"); this.#clearButton.disabled = true; } else { this.#clearButton.classList.add("btn-danger"); this.#clearButton.classList.remove("btn-secondary"); this.#clearButton.disabled = false; } } /** * Returns the account selector instances. * * @param lineItemEditor {JournalEntryLineItemEditor} the line item editor * @return {{debit: JournalEntryAccountSelector, credit: JournalEntryAccountSelector}} */ static getInstances(lineItemEditor) { const selectors = {} const modals = Array.from(document.getElementsByClassName("accounting-account-selector")); for (const modal of modals) { selectors[modal.dataset.debitCredit] = new JournalEntryAccountSelector(lineItemEditor, modal.dataset.debitCredit); } return selectors; } } /** * An account option * */ class JournalEntryAccountOption { /** * The element * @type {HTMLLIElement} */ #element; /** * The account code * @type {string} */ code; /** * The account title * @type {string} */ title; /** * The account text * @type {string} */ text; /** * Whether the account is in use * @type {boolean} */ #isInUse; /** * Whether line items in the account need offset * @type {boolean} */ isNeedOffset; /** * The values to query against * @type {string[]} */ #queryValues; /** * Constructs the account in the account selector. * * @param selector {JournalEntryAccountSelector} the account selector * @param element {HTMLLIElement} the element */ constructor(selector, element) { this.#element = element; this.code = element.dataset.code; this.title = element.dataset.title; this.text = element.dataset.text; this.#isInUse = element.classList.contains("accounting-account-is-in-use"); this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset"); this.#queryValues = JSON.parse(element.dataset.queryValues); this.#element.onclick = () => selector.lineItemEditor.saveAccount(this); } /** * Returns whether the account matches the query. * * @param isShowMore {boolean} true to show all accounts, or false to show only those in use * @param codesInUse {string[]} the account codes that are used in the form * @param query {string} the query term * @return {boolean} true if the option matches, or false otherwise */ isMatched(isShowMore, codesInUse, query) { return this.#isInUseMatched(isShowMore, codesInUse) && this.#isQueryMatched(query); } /** * Returns whether the account matches the "in-use" condition. * * @param isShowMore {boolean} true to show all accounts, or false to show only those in use * @param codesInUse {string[]} the account codes that are used in the form * @return {boolean} true if the option matches, or false otherwise */ #isInUseMatched(isShowMore, codesInUse) { return isShowMore || this.#isInUse || codesInUse.includes(this.code); } /** * Returns whether the account matches the query term. * * @param query {string} the query term * @return {boolean} true if the option matches, or false otherwise */ #isQueryMatched(query) { if (query === "") { return true; } for (const queryValue of this.#queryValues) { if (queryValue.toLowerCase().includes(query.toLowerCase())) { return true; } } return false; } /** * Sets whether the option is shown. * * @param isShown {boolean} true to show, or false otherwise */ setShown(isShown) { if (isShown) { this.#element.classList.remove("d-none"); } else { this.#element.classList.add("d-none"); } } /** * Sets whether the option is active. * * @param isActive {boolean} true if active, or false otherwise */ setActive(isActive) { if (isActive) { this.#element.classList.add("active"); } else { this.#element.classList.remove("active"); } } }