320 lines
9.0 KiB
JavaScript
320 lines
9.0 KiB
JavaScript
/* 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");
|
|
}
|
|
}
|
|
}
|