Compare commits

...

3 Commits

6 changed files with 455 additions and 292 deletions

View File

@ -24,100 +24,293 @@
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
initializeBaseAccountSelector(); AccountForm.initialize();
document.getElementById("accounting-base-code")
.onchange = validateBase;
document.getElementById("accounting-title")
.onchange = validateTitle;
document.getElementById("accounting-form")
.onsubmit = validateForm;
}); });
/** /**
* Initializes the base account selector. * The account form.
* *
* @private * @private
*/ */
function initializeBaseAccountSelector() { class AccountForm {
const selector = document.getElementById("accounting-base-selector-modal");
const base = document.getElementById("accounting-base"); /**
const baseCode = document.getElementById("accounting-base-code"); * The base account selector
const baseContent = document.getElementById("accounting-base-content"); * @type {BaseAccountSelector}
const isOffsetNeededControl = document.getElementById("accounting-is-offset-needed-control"); */
const isOffsetNeeded = document.getElementById("accounting-is-offset-needed"); #baseAccountSelector;
const options = Array.from(document.getElementsByClassName("accounting-base-option"));
const btnClear = document.getElementById("accounting-btn-clear-base"); /**
base.onclick = () => { * The form element
base.classList.add("accounting-not-empty"); * @type {HTMLFormElement}
for (const option of options) { */
option.classList.remove("active"); #formElement;
}
const selected = document.getElementById("accounting-base-option-" + baseCode.value); /**
if (selected !== null) { * The control of the base account
selected.classList.add("active"); * @type {HTMLDivElement}
} */
#baseControl;
/**
* The input of the base account
* @type {HTMLInputElement}
*/
#baseCode;
/**
* The base account
* @type {HTMLDivElement}
*/
#base;
/**
* The error message for the base account
* @type {HTMLDivElement}
*/
#baseError;
/**
* The title
* @type {HTMLInputElement}
*/
#title;
/**
* The error message of the title
* @type {HTMLDivElement}
*/
#titleError;
/**
* The control of the is-offset-needed option
* @type {HTMLDivElement}
*/
#isOffsetNeededControl;
/**
* The is-offset-needed option
* @type {HTMLInputElement}
*/
#isOffsetNeeded;
/**
* Constructs the account form.
*
*/
constructor() {
this.#baseAccountSelector = new BaseAccountSelector(this);
this.#formElement = document.getElementById("accounting-form");
this.#baseControl = document.getElementById("accounting-base-control");
this.#baseCode = document.getElementById("accounting-base-code");
this.#base = document.getElementById("accounting-base");
this.#baseError = document.getElementById("accounting-base-error");
this.#title = document.getElementById("accounting-title");
this.#titleError = document.getElementById("accounting-title-error");
this.#isOffsetNeededControl = document.getElementById("accounting-is-offset-needed-control");
this.#isOffsetNeeded = document.getElementById("accounting-is-offset-needed");
this.#formElement.onsubmit = () => {
return this.#validateForm();
};
this.#baseControl.onclick = () => {
this.#baseControl.classList.add("accounting-not-empty");
this.#baseAccountSelector.onOpen(this.#baseCode.value);
}; };
selector.addEventListener("hidden.bs.modal", () => {
if (baseCode.value === "") {
base.classList.remove("accounting-not-empty");
} }
});
for (const option of options) { /**
option.onclick = () => { * The callback when the base account selector is closed.
baseCode.value = option.dataset.code; *
baseContent.innerText = option.dataset.content; */
if (["1", "2"].includes(option.dataset.content.substring(0, 1))) { onBaseAccountSelectorClosed() {
isOffsetNeededControl.classList.remove("d-none"); if (this.#baseCode.value === "") {
isOffsetNeeded.disabled = false; this.#baseControl.classList.remove("accounting-not-empty");
}
}
/**
* Sets the base account.
*
* @param code {string} the base account code
* @param text {string} the text for the base account
*/
setBaseAccount(code, text) {
this.#baseCode.value = code;
this.#base.innerText = text;
if (["1", "2"].includes(code.substring(0, 1))) {
this.#isOffsetNeededControl.classList.remove("d-none");
this.#isOffsetNeeded.disabled = false;
} else { } else {
isOffsetNeededControl.classList.add("d-none"); this.#isOffsetNeededControl.classList.add("d-none");
isOffsetNeeded.disabled = true; this.#isOffsetNeeded.disabled = true;
isOffsetNeeded.checked = false; this.#isOffsetNeeded.checked = false;
} }
btnClear.classList.add("btn-danger"); this.#validateBase();
btnClear.classList.remove("btn-secondary")
btnClear.disabled = false;
validateBase();
bootstrap.Modal.getInstance(selector).hide();
};
}
btnClear.onclick = () => {
baseCode.value = "";
baseContent.innerText = "";
btnClear.classList.add("btn-secondary")
btnClear.classList.remove("btn-danger");
btnClear.disabled = true;
validateBase();
bootstrap.Modal.getInstance(selector).hide();
}
initializeBaseAccountQuery();
} }
/** /**
* Initializes the query on the base account options. * Clears the base account.
*
*/
clearBaseAccount() {
this.#baseCode.value = "";
this.#base.innerText = "";
this.#validateBase();
}
/**
* Validates the form.
*
* @returns {boolean} true if valid, or false otherwise
*/
#validateForm() {
let isValid = true;
isValid = this.#validateBase() && isValid;
isValid = this.#validateTitle() && isValid;
return isValid;
}
/**
* Validates the base account.
*
* @returns {boolean} true if valid, or false otherwise
*/
#validateBase() {
if (this.#baseCode.value === "") {
this.#baseControl.classList.add("is-invalid");
this.#baseError.innerText = A_("Please select the base account.");
return false;
}
this.#baseControl.classList.remove("is-invalid");
this.#baseError.innerText = "";
return true;
}
/**
* Validates the title.
*
* @returns {boolean} true if valid, or false otherwise
*/
#validateTitle() {
this.#title.value = this.#title.value.trim();
if (this.#title.value === "") {
this.#title.classList.add("is-invalid");
this.#titleError.innerText = A_("Please fill in the title.");
return false;
}
this.#title.classList.remove("is-invalid");
this.#titleError.innerText = "";
return true;
}
/**
* The account form
* @type {AccountForm} the form
*/
static #form;
static initialize() {
this.#form = new AccountForm();
}
}
/**
* The base account selector.
* *
* @private * @private
*/ */
function initializeBaseAccountQuery() { class BaseAccountSelector {
const query = document.getElementById("accounting-base-selector-query");
const optionList = document.getElementById("accounting-base-option-list"); /**
const options = Array.from(document.getElementsByClassName("accounting-base-option")); * The account form
const queryNoResult = document.getElementById("accounting-base-option-no-result"); * @type {AccountForm}
query.addEventListener("input", () => { */
if (query.value === "") { #form;
for (const option of options) {
/**
* The selector modal
* @type {HTMLDivElement}
*/
#modal;
/**
* 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 {HTMLLIElement[]}
*/
#options;
/**
* The button to clear the base account value
* @type {HTMLButtonElement}
*/
#clearButton;
/**
* Constructs the base account selector.
*
* @param form {AccountForm} the form
*/
constructor(form) {
this.#form = form;
this.#modal = document.getElementById("accounting-base-selector-modal");
this.#query = document.getElementById("accounting-base-selector-query");
this.#optionList = document.getElementById("accounting-base-selector-option-list");
// noinspection JSValidateTypes
this.#options = Array.from(document.getElementsByClassName("accounting-base-selector-option"));
this.#clearButton = document.getElementById("accounting-base-selector-clear");
this.#queryNoResult = document.getElementById("accounting-base-selector-option-no-result");
this.#modal.addEventListener("hidden.bs.modal", () => {
this.#form.onBaseAccountSelectorClosed();
});
for (const option of this.#options) {
option.onclick = () => {
this.#form.setBaseAccount(option.dataset.code, option.dataset.content);
};
}
this.#clearButton.onclick = () => {
this.#form.clearBaseAccount();
};
this.#initializeBaseAccountQuery();
}
/**
* Initializes the query.
*
*/
#initializeBaseAccountQuery() {
this.#query.addEventListener("input", () => {
if (this.#query.value === "") {
for (const option of this.#options) {
option.classList.remove("d-none"); option.classList.remove("d-none");
} }
optionList.classList.remove("d-none"); this.#optionList.classList.remove("d-none");
queryNoResult.classList.add("d-none"); this.#queryNoResult.classList.add("d-none");
return return
} }
let hasAnyMatched = false; let hasAnyMatched = false;
for (const option of options) { for (const option of this.#options) {
const queryValues = JSON.parse(option.dataset.queryValues); const queryValues = JSON.parse(option.dataset.queryValues);
let isMatched = false; let isMatched = false;
for (const queryValue of queryValues) { for (const queryValue of queryValues) {
if (queryValue.includes(query.value)) { if (queryValue.includes(this.#query.value)) {
isMatched = true; isMatched = true;
break; break;
} }
@ -130,65 +323,36 @@ function initializeBaseAccountQuery() {
} }
} }
if (!hasAnyMatched) { if (!hasAnyMatched) {
optionList.classList.add("d-none"); this.#optionList.classList.add("d-none");
queryNoResult.classList.remove("d-none"); this.#queryNoResult.classList.remove("d-none");
} else { } else {
optionList.classList.remove("d-none"); this.#optionList.classList.remove("d-none");
queryNoResult.classList.add("d-none"); this.#queryNoResult.classList.add("d-none");
} }
}); });
} }
/** /**
* Validates the form. * The callback when the base account selector is shown.
* *
* @returns {boolean} true if valid, or false otherwise * @param baseCode {string} the active base code
* @private
*/ */
function validateForm() { onOpen(baseCode) {
let isValid = true; for (const option of this.#options) {
isValid = validateBase() && isValid; if (option.dataset.code === baseCode) {
isValid = validateTitle() && isValid; option.classList.add("active");
return isValid; } else {
option.classList.remove("active");
} }
/**
* Validates the base account.
*
* @returns {boolean} true if valid, or false otherwise
* @private
*/
function validateBase() {
const field = document.getElementById("accounting-base-code");
const error = document.getElementById("accounting-base-code-error");
const displayField = document.getElementById("accounting-base");
field.value = field.value.trim();
if (field.value === "") {
displayField.classList.add("is-invalid");
error.innerText = A_("Please select the base account.");
return false;
} }
displayField.classList.remove("is-invalid"); if (baseCode === "") {
error.innerText = ""; this.#clearButton.classList.add("btn-secondary")
return true; 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;
} }
/**
* Validates the title.
*
* @returns {boolean} true if valid, or false otherwise
* @private
*/
function validateTitle() {
const field = document.getElementById("accounting-title");
const error = document.getElementById("accounting-title-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");
error.innerText = A_("Please fill in the title.");
return false;
} }
field.classList.remove("is-invalid");
error.innerText = "";
return true;
} }

View File

@ -24,152 +24,151 @@
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
document.getElementById("accounting-code") CurrencyForm.initialize();
.onchange = validateCode;
document.getElementById("accounting-name")
.onchange = validateName;
document.getElementById("accounting-form")
.onsubmit = validateForm;
}); });
/** /**
* The asynchronous validation result * The currency form.
* @type {object} *
* @private * @private
*/ */
let isAsyncValid = {}; class CurrencyForm {
/**
* The form.
* @type {HTMLFormElement}
*/
#formElement;
/**
* The code
* @type {HTMLInputElement}
*/
#code;
/**
* The error message of the code
* @type {HTMLDivElement}
*/
#codeError;
/**
* The name
* @type {HTMLInputElement}
*/
#name;
/**
* The error message of the name
* @type {HTMLDivElement}
*/
#nameError;
/**
* Constructs the currency form.
*
*/
constructor() {
this.#formElement = document.getElementById("accounting-form");
this.#code = document.getElementById("accounting-code");
this.#codeError = document.getElementById("accounting-code-error");
this.#name = document.getElementById("accounting-name");
this.#nameError = document.getElementById("accounting-name-error");
this.#code.onchange = () => {
this.#validateCode().then();
};
this.#name.onchange = () => {
this.#validateName();
};
this.#formElement.onsubmit = () => {
this.#validateForm().then((isValid) => {
if (isValid) {
this.#formElement.submit();
}
});
return false;
};
}
/** /**
* Validates the form. * Validates the form.
* *
* @returns {boolean} true if valid, or false otherwise * @returns {Promise<boolean>} true if valid, or false otherwise
* @private
*/ */
function validateForm() { async #validateForm() {
isAsyncValid = {
"code": false,
"_sync": false,
};
let isValid = true; let isValid = true;
isValid = validateCode() && isValid; isValid = await this.#validateCode() && isValid;
isValid = validateName() && isValid; isValid = this.#validateName() && isValid;
isAsyncValid["_sync"] = isValid; return isValid;
submitFormIfAllAsyncValid();
return false;
}
/**
* Submits the form if the whole form passed the asynchronous
* validations.
*
* @private
*/
function submitFormIfAllAsyncValid() {
let isValid = true;
for (const key of Object.keys(isAsyncValid)) {
isValid = isAsyncValid[key] && isValid;
}
if (isValid) {
document.getElementById("accounting-form").submit()
}
} }
/** /**
* Validates the code. * Validates the code.
* *
* @param changeEvent {Event} the change event, if invoked from onchange * @param changeEvent {Event} the change event, if invoked from onchange
* @returns {boolean} true if valid, or false otherwise * @returns {Promise<boolean>} true if valid, or false otherwise
* @private
*/ */
function validateCode(changeEvent = null) { async #validateCode(changeEvent = null) {
const key = "code"; this.#code.value = this.#code.value.trim();
const isSubmission = changeEvent === null; if (this.#code.value === "") {
let hasAsyncValidation = false; this.#code.classList.add("is-invalid");
const field = document.getElementById("accounting-code"); this.#codeError.innerText = A_("Please fill in the code.");
const error = document.getElementById("accounting-code-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");
error.innerText = A_("Please fill in the code.");
return false; return false;
} }
const blocklist = JSON.parse(field.dataset.blocklist); const blocklist = JSON.parse(this.#code.dataset.blocklist);
if (blocklist.includes(field.value)) { if (blocklist.includes(this.#code.value)) {
field.classList.add("is-invalid"); this.#code.classList.add("is-invalid");
error.innerText = A_("This code is not available."); this.#codeError.innerText = A_("This code is not available.");
return false; return false;
} }
if (!field.value.match(/^[A-Z]{3}$/)) { if (!this.#code.value.match(/^[A-Z]{3}$/)) {
field.classList.add("is-invalid"); this.#code.classList.add("is-invalid");
error.innerText = A_("Code can only be composed of 3 upper-cased letters."); this.#codeError.innerText = A_("Code can only be composed of 3 upper-cased letters.");
return false; return false;
} }
const original = field.dataset.original; const original = this.#code.dataset.original;
if (original === "" || field.value !== original) { if (original === "" || this.#code.value !== original) {
hasAsyncValidation = true; const response = await fetch(this.#code.dataset.existsUrl + "?q=" + encodeURIComponent(this.#code.value));
validateAsyncCodeIsDuplicated(isSubmission, key); const data = await response.json();
if (data["exists"]) {
this.#code.classList.add("is-invalid");
this.#codeError.innerText = A_("Code conflicts with another currency.");
return false;
} }
if (!hasAsyncValidation) {
isAsyncValid[key] = true;
field.classList.remove("is-invalid");
error.innerText = "";
} }
this.#code.classList.remove("is-invalid");
this.#codeError.innerText = "";
return true; return true;
} }
/**
* Validates asynchronously whether the code is duplicated.
* The boolean validation result is stored in isAsyncValid[key].
*
* @param isSubmission {boolean} whether this is invoked from a form submission
* @param key {string} the key to store the result in isAsyncValid
* @private
*/
function validateAsyncCodeIsDuplicated(isSubmission, key) {
const field = document.getElementById("accounting-code");
const error = document.getElementById("accounting-code-error");
const url = field.dataset.existsUrl;
const onLoad = function () {
if (this.status === 200) {
const result = JSON.parse(this.responseText);
if (result["exists"]) {
field.classList.add("is-invalid");
error.innerText = A_("Code conflicts with another currency.");
if (isSubmission) {
isAsyncValid[key] = false;
}
return;
}
field.classList.remove("is-invalid");
error.innerText = "";
if (isSubmission) {
isAsyncValid[key] = true;
submitFormIfAllAsyncValid();
}
}
};
const request = new XMLHttpRequest();
request.onload = onLoad;
request.open("GET", url + "?q=" + encodeURIComponent(field.value));
request.send();
}
/** /**
* Validates the name. * Validates the name.
* *
* @returns {boolean} true if valid, or false otherwise * @returns {boolean} true if valid, or false otherwise
* @private
*/ */
function validateName() { #validateName() {
const field = document.getElementById("accounting-name"); this.#name.value = this.#name.value.trim();
const error = document.getElementById("accounting-name-error"); if (this.#name.value === "") {
field.value = field.value.trim(); this.#name.classList.add("is-invalid");
if (field.value === "") { this.#nameError.innerText = A_("Please fill in the name.");
field.classList.add("is-invalid");
error.innerText = A_("Please fill in the name.");
return false; return false;
} }
field.classList.remove("is-invalid"); this.#name.classList.remove("is-invalid");
error.innerText = ""; this.#nameError.innerText = "";
return true; return true;
} }
/**
* The form
* @type {CurrencyForm}
*/
static #form;
/**
* Initializes the currency form.
*
*/
static initialize() {
this.#form = new CurrencyForm();
}
}

View File

@ -41,9 +41,9 @@ First written: 2023/2/1
{% endif %} {% endif %}
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input id="accounting-base-code" type="hidden" name="base_code" value="{{ form.base_code.data|accounting_default }}"> <input id="accounting-base-code" type="hidden" name="base_code" value="{{ form.base_code.data|accounting_default }}">
<div id="accounting-base" class="form-control accounting-clickable accounting-material-text-field {% if form.base_code.data %} accounting-not-empty {% endif %} {% if form.base_code.errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-base-selector-modal"> <div id="accounting-base-control" class="form-control accounting-clickable accounting-material-text-field {% if form.base_code.data %} accounting-not-empty {% endif %} {% if form.base_code.errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-base-selector-modal">
<label class="form-label" for="accounting-base">{{ A_("Base account") }}</label> <label class="form-label" for="accounting-base">{{ A_("Base account") }}</label>
<div id="accounting-base-content"> <div id="accounting-base">
{% if form.base_code.data %} {% if form.base_code.data %}
{% if form.base_code.errors %} {% if form.base_code.errors %}
{{ A_("(Unknown)") }} {{ A_("(Unknown)") }}
@ -53,7 +53,7 @@ First written: 2023/2/1
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div id="accounting-base-code-error" class="invalid-feedback">{% if form.base_code.errors %}{{ form.base_code.errors[0] }}{% endif %}</div> <div id="accounting-base-error" class="invalid-feedback">{% if form.base_code.errors %}{{ form.base_code.errors[0] }}{% endif %}</div>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
@ -99,21 +99,21 @@ First written: 2023/2/1
</label> </label>
</div> </div>
<ul id="accounting-base-option-list" class="list-group accounting-selector-list"> <ul id="accounting-base-selector-option-list" class="list-group accounting-selector-list">
{% for base in form.base_options %} {% for base in form.base_options %}
<li id="accounting-base-option-{{ base.code }}" class="list-group-item accounting-base-option accounting-clickable" data-code="{{ base.code }}" data-content="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}"> <li class="list-group-item accounting-clickable accounting-base-selector-option" data-code="{{ base.code }}" data-content="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}" data-bs-dismiss="modal">
{{ base }} {{ base }}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<p id="accounting-base-option-no-result" class="d-none">{{ A_("There is no data.") }}</p> <p id="accounting-base-selector-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
{% if form.base_code.data %} {% if form.base_code.data %}
<button id="accounting-btn-clear-base" type="button" class="btn btn-danger">{{ A_("Clear") }}</button> <button id="accounting-base-selector-clear" type="button" class="btn btn-danger" data-bs-dismiss="modal">{{ A_("Clear") }}</button>
{% else %} {% else %}
<button id="accounting-btn-clear-base" type="button" class="btn btn-secondary" disabled="disabled">{{ A_("Clear") }}</button> <button id="accounting-base-selector-clear" type="button" class="btn btn-secondary" disabled="disabled" data-bs-dismiss="modal">{{ A_("Clear") }}</button>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -24,7 +24,7 @@ First written: 2023/2/25
<div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}"> <div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}">
<div class="d-flex justify-content-between mt-2 mb-3"> <div class="d-flex justify-content-between mt-2 mb-3">
<div class="form-floating accounting-currency-content"> <div class="form-floating accounting-currency-content">
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code"> <select id="accounting-currency-{{ currency_index }}-code" class="form-select {% if currency_code_errors %} is-invalid {% endif %}" name="currency-{{ currency_index }}-code">
{% for currency in accounting_currency_options() %} {% for currency in accounting_currency_options() %}
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option> <option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
{% endfor %} {% endfor %}

View File

@ -24,7 +24,7 @@ First written: 2023/2/25
<div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}"> <div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}">
<div class="d-flex justify-content-between mt-2 mb-3"> <div class="d-flex justify-content-between mt-2 mb-3">
<div class="form-floating accounting-currency-content"> <div class="form-floating accounting-currency-content">
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code"> <select id="accounting-currency-{{ currency_index }}-code" class="form-select {% if currency_code_errors %} is-invalid {% endif %}" name="currency-{{ currency_index }}-code">
{% for currency in accounting_currency_options() %} {% for currency in accounting_currency_options() %}
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option> <option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
{% endfor %} {% endfor %}

View File

@ -24,7 +24,7 @@ First written: 2023/2/25
<div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}"> <div id="accounting-currency-{{ currency_index }}-control" class="form-control accounting-currency-control {% if currency_errors %} is-invalid {% endif %}">
<div class="d-flex justify-content-between mt-2 mb-3"> <div class="d-flex justify-content-between mt-2 mb-3">
<div class="form-floating accounting-currency-content"> <div class="form-floating accounting-currency-content">
<select id="accounting-currency-{{ currency_index }}-code" class="form-select" name="currency-{{ currency_index }}-code"> <select id="accounting-currency-{{ currency_index }}-code" class="form-select {% if currency_code_errors %} is-invalid {% endif %}" name="currency-{{ currency_index }}-code">
{% for currency in accounting_currency_options() %} {% for currency in accounting_currency_options() %}
<option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option> <option value="{{ currency.code }}" {% if currency.code == currency_code_data %} selected="selected" {% endif %}>{{ currency }}</option>
{% endfor %} {% endfor %}