Expand create transaction form.

This commit is contained in:
James Cole
2024-01-04 14:59:37 +01:00
parent 1ba7847d84
commit 566bb2f097
26 changed files with 877 additions and 723 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{d as l,g as i,l as r}from"./load-translations-23553922.js";import{I as s}from"./vendor-e194ad60.js";let t,d=function(){return{entries:[],init(){Promise.all([i("language","en_US")]).then(e=>{t=new s;const o=e[0].replace("-","_");t.locale=o,r(t,o).then(()=>{})})}}},a={transactions:d,dates:l};function n(){Object.keys(a).forEach(e=>{console.log(`Loading page component "${e}"`);let o=a[e]();Alpine.data(e,()=>o)}),Alpine.start()}document.addEventListener("firefly-iii-bootstrapped",()=>{console.log("Loaded through event listener."),n()});window.bootstrapped&&(console.log("Loaded through window variable."),n());

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{a as r}from"./load-translations-23553922.js";function i(s,a){let t=window.__localeId__.replace("_","-");return Intl.NumberFormat(t,{style:"currency",currency:a}).format(s)}let p=class{list(a){return r.get("/api/v2/subscriptions",{params:a})}paid(a){return r.get("/api/v2/subscriptions/sum/paid",{params:a})}unpaid(a){return r.get("/api/v2/subscriptions/sum/unpaid",{params:a})}};class u{list(a){return r.get("/api/v2/piggy-banks",{params:a})}}export{p as G,u as a,i as f};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,19 @@
{
"_get-10f2a251.js": {
"file": "assets/get-10f2a251.js",
"_get-748a816c.js": {
"file": "assets/get-748a816c.js",
"imports": [
"_vendor-5ec3da0f.js"
"_load-translations-23553922.js"
],
"integrity": "sha384-MAAJQjXJHsrlavEslgFwonZh+vjugzEJlPMmXXbM2rqqqkYujuSdVcp9tE3aZ1Ro"
"integrity": "sha384-Q/jXZc5hCLcwX4RjNVIgz80TzISFu81Li1iE5ZNuARydr2CNLRxyl+RFkOj46dPc"
},
"_vendor-5ec3da0f.js": {
"_load-translations-23553922.js": {
"file": "assets/load-translations-23553922.js",
"imports": [
"_vendor-e194ad60.js"
],
"integrity": "sha384-pNfWxxe1sV2bLDoKsEg3oGRBm8+DLQ9lmeL4oFpBJBRNUWOUEfAXp0iYoZMT2hPh"
},
"_vendor-e194ad60.js": {
"assets": [
"assets/layers-1dbbe9d0.png",
"assets/layers-2x-066daca8.png",
@@ -15,8 +22,8 @@
"css": [
"assets/vendor-49001d3f.css"
],
"file": "assets/vendor-5ec3da0f.js",
"integrity": "sha384-ZVKGWd0fOujjIvJKxFA8SzgFIJQPEt1zpM+hgtL6o6S6gBRDTNEz2FlssKfAjVJ/"
"file": "assets/vendor-e194ad60.js",
"integrity": "sha384-pBeK5qr0qG0MDsIfi2/X7NE5V+YUERUHOGDEL5JCGFtw8l+EiRe7D2uuMKV4/cxm"
},
"node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf": {
"file": "assets/fa-brands-400-5656d596.ttf",
@@ -64,24 +71,36 @@
"integrity": "sha384-wg83fCOXjBtqzFAWhTL9Sd9vmLUNhfEEzfmNUX9zwv2igKlz/YQbdapF4ObdxF+R"
},
"resources/assets/v2/pages/dashboard/dashboard.js": {
"file": "assets/dashboard-f6763ad9.js",
"file": "assets/dashboard-a55f7472.js",
"imports": [
"_get-10f2a251.js",
"_vendor-5ec3da0f.js"
"_load-translations-23553922.js",
"_get-748a816c.js",
"_vendor-e194ad60.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/dashboard/dashboard.js",
"integrity": "sha384-PVEKYVS77Q0irHN7cZ9dMZ64ef7eFhIiUwoDHwwsDfNfP9bORXYxrEO2Z2ldVCQN"
"integrity": "sha384-4VlBovrF9JYeq8ywA4F+J+x+Rs9OIUKgZNtQuZz+F9XADHlvdaegrAx+MLmFc4zX"
},
"resources/assets/v2/pages/transactions/create.js": {
"file": "assets/create-5a2ad8a8.js",
"file": "assets/create-77b1cf47.js",
"imports": [
"_get-10f2a251.js",
"_vendor-5ec3da0f.js"
"_load-translations-23553922.js",
"_vendor-e194ad60.js",
"_get-748a816c.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/transactions/create.js",
"integrity": "sha384-rdu17Qy38YXrooK3NkVmEaFRuqzmsz4c2znLVKvAD0Yzpd2dXTBd4oXEYnA629b5"
"integrity": "sha384-sUxpvAj5i3XiHoRluzCByokIDbqnDMb4gjxDxArjYodV3ymR2O9atDpCZtEBJshw"
},
"resources/assets/v2/pages/transactions/edit.js": {
"file": "assets/edit-83707812.js",
"imports": [
"_load-translations-23553922.js",
"_vendor-e194ad60.js"
],
"isEntry": true,
"src": "resources/assets/v2/pages/transactions/edit.js",
"integrity": "sha384-UkvRogZBJfe4zy9IAmFghhsyJfzcml29moISocvNyF1ujn+Op54PwrMYf5plap6Y"
},
"resources/assets/v2/sass/app.scss": {
"file": "assets/app-fb7b26ec.css",

View File

@@ -20,16 +20,13 @@
import '../../boot/bootstrap.js';
import dates from '../../pages/shared/dates.js';
import {createEmptySplit} from "./shared/create-empty-split.js";
import {createEmptySplit, defaultErrorSet} from "./shared/create-empty-split.js";
import {parseFromEntries} from "./shared/parse-from-entries.js";
import formatMoney from "../../util/format-money.js";
import Autocomplete from "bootstrap5-autocomplete";
import Post from "../../api/v2/model/transaction/post.js";
import AttachmentPost from "../../api/v1/attachments/post.js";
import {getVariable} from "../../store/get-variable.js";
import {I18n} from "i18n-js";
import {loadTranslations} from "../../support/load-translations.js";
import Tags from "bootstrap5-tags";
import {loadCurrencies} from "./shared/load-currencies.js";
import {loadBudgets} from "./shared/load-budgets.js";
import {loadPiggyBanks} from "./shared/load-piggy-banks.js";
@@ -38,6 +35,17 @@ import {loadSubscriptions} from "./shared/load-subscriptions.js";
import L from "leaflet";
import 'leaflet/dist/leaflet.css';
import {addAutocomplete} from "./shared/add-autocomplete.js";
import {
changeCategory,
changeDescription,
changeDestinationAccount,
changeSourceAccount,
selectDestinationAccount,
selectSourceAccount
} from "./shared/autocomplete-functions.js";
import {processAttachments} from "./shared/process-attachments.js";
import {spliceErrorsIntoTransactions} from "./shared/splice-errors-into-transactions.js";
// TODO upload attachments to other file
// TODO fix two maps, perhaps disconnect from entries entirely.
@@ -54,121 +62,6 @@ const urls = {
tag: '/api/v2/autocomplete/tags',
};
let uploadAttachments = function (id, transactions) {
console.log('Now in uploadAttachments');
// reverse list of transactions?
transactions = transactions.reverse();
// array of all files to be uploaded:
let toBeUploaded = [];
let count = 0;
// array with all file data.
let fileData = [];
// all attachments
let attachments = document.querySelectorAll('input[name="attachments[]"]');
console.log(attachments);
// loop over all attachments, and add references to this array:
for (const key in attachments) {
if (attachments.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294) {
console.log('Now at attachment #' + key);
for (const fileKey in attachments[key].files) {
if (attachments[key].files.hasOwnProperty(fileKey) && /^0$|^[1-9]\d*$/.test(fileKey) && fileKey <= 4294967294) {
// include journal thing.
console.log('Will upload #' + fileKey + ' from attachment #' + key + ' to transaction #' + transactions[key].transaction_journal_id);
toBeUploaded.push({
journal: transactions[key].transaction_journal_id, file: attachments[key].files[fileKey]
});
count++;
}
}
}
}
console.log('Found ' + count + ' attachments.');
// loop all uploads.
for (const key in toBeUploaded) {
if (toBeUploaded.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294) {
console.log('Create file reader for file #' + key);
// create file reader thing that will read all of these uploads
(function (f, key) {
let fileReader = new FileReader();
fileReader.onloadend = function (evt) {
if (evt.target.readyState === FileReader.DONE) { // DONE == 2
console.log('Done reading file #' + key);
fileData.push({
name: toBeUploaded[key].file.name,
journal: toBeUploaded[key].journal,
content: new Blob([evt.target.result])
});
if (fileData.length === count) {
console.log('Done reading file #' + key);
uploadFiles(fileData, id);
}
}
};
fileReader.readAsArrayBuffer(f.file);
})(toBeUploaded[key], key,);
}
}
return count;
}
let uploadFiles = function (fileData, id) {
let count = fileData.length;
let uploads = 0;
console.log('Will now upload ' + count + ' file(s) to journal with id #' + id);
for (const key in fileData) {
if (fileData.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294) {
console.log('Creating attachment #' + key);
let poster = new AttachmentPost();
poster.post(fileData[key].name, 'TransactionJournal', fileData[key].journal).then(response => {
let attachmentId = parseInt(response.data.data.id);
console.log('Created attachment #' + attachmentId + ' for key #' + key);
console.log('Uploading attachment #' + key);
poster.upload(attachmentId, fileData[key].content).then(attachmentResponse => {
// console.log('Uploaded attachment #' + key);
uploads++;
if (uploads === count) {
// finally we can redirect the user onwards.
console.log('FINAL UPLOAD, redirect user to new transaction or reset form or whatever.');
const event = new CustomEvent('upload-success', {some: 'details'});
document.dispatchEvent(event);
return;
}
console.log('Upload complete!');
// return true here.
}).catch(error => {
console.error('Could not upload');
console.error(error);
// console.log('Uploaded attachment #' + key);
uploads++;
if (uploads === count) {
// finally we can redirect the user onwards.
console.log('FINAL UPLOAD, redirect user to new transaction or reset form or whatever.');
//this.redirectUser(groupId, transactionData);
}
// console.log('Upload complete!');
// return false;
// return false here
});
}).catch(error => {
console.error('Could not create upload.');
console.error(error);
uploads++;
if (uploads === count) {
// finally we can redirect the user onwards.
// console.log('FINAL UPLOAD');
console.log('FINAL UPLOAD, redirect user to new transaction or reset form or whatever.');
// this.redirectUser(groupId, transactionData);
}
// console.log('Upload complete!');
//return false;
});
}
}
}
let transactions = function () {
return {
// transactions are stored in "entries":
@@ -190,7 +83,8 @@ let transactions = function () {
// form behaviour during transaction
formBehaviour: {
formType: 'create', foreignCurrencyEnabled: true,
formType: 'create',
foreignCurrencyEnabled: true,
},
// form data (except transactions) is stored in formData
@@ -207,6 +101,8 @@ let transactions = function () {
// properties for the entire transaction group
groupProperties: {
transactionType: 'unknown',
title: null,
id: null,
totalAmount: 0,
},
@@ -223,8 +119,8 @@ let transactions = function () {
url: '',
},
wait: {
show: false,text: '',
url: '',
show: false,
text: '',
}
},
@@ -259,13 +155,12 @@ let transactions = function () {
console.log('changedDescription');
},
changedDestinationAccount(event) {
console.log('changedDestinationAccount')
this.detectTransactionType();
},
changedSourceAccount(event) {
console.log('changedSourceAccount')
this.detectTransactionType();
},
detectTransactionType() {
const sourceType = this.entries[0].source_account.type ?? 'unknown';
const destType = this.entries[0].destination_account.type ?? 'unknown';
@@ -274,6 +169,7 @@ let transactions = function () {
console.warn('Cannot infer transaction type from two unknown accounts.');
return;
}
// transfer: both are the same and in strict set of account types
if (sourceType === destType && ['Asset account', 'Loan', 'Debt', 'Mortgage'].includes(sourceType)) {
this.groupProperties.transactionType = 'transfer';
@@ -320,41 +216,34 @@ let transactions = function () {
}
console.warn('Unknown account combination between "' + sourceType + '" and "' + destType + '".');
},
selectSourceAccount(item, ac) {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
document.querySelector('#form')._x_dataStack[0].$data.entries[index].source_account = {
id: item.id,
name: item.name,
alpine_name: item.name,
type: item.type,
currency_code: item.currency_code,
};
console.log('Changed source account into a known ' + item.type.toLowerCase());
document.querySelector('#form')._x_dataStack[0].detectTransactionType();
formattedTotalAmount() {
if(this.entries.length === 0) {
return formatMoney(this.groupProperties.totalAmount, 'EUR');
}
return formatMoney(this.groupProperties.totalAmount, this.entries[0].currency_code ?? 'EUR');
},
filterForeignCurrencies(code) {
console.log('filterForeignCurrencies("' + code + '")');
let list = [];
let currency;
for (let i in this.enabledCurrencies) {
if (this.enabledCurrencies.hasOwnProperty(i)) {
let current = this.enabledCurrencies[i];
for (let i in this.formData.enabledCurrencies) {
if (this.formData.enabledCurrencies.hasOwnProperty(i)) {
let current = this.formData.enabledCurrencies[i];
if (current.code === code) {
currency = current;
}
}
}
list.push(currency);
this.foreignCurrencies = list;
this.formData.foreignCurrencies = list;
// is he source account currency anyway:
if (1 === list.length && list[0].code === this.entries[0].source_account.currency_code) {
console.log('Foreign currency is same as source currency. Disable foreign amount.');
this.foreignAmountEnabled = false;
this.formBehaviour.foreignCurrencyEnabled = false;
}
if (1 === list.length && list[0].code !== this.entries[0].source_account.currency_code) {
console.log('Foreign currency is NOT same as source currency. Enable foreign amount.');
this.foreignAmountEnabled = true;
this.formBehaviour.foreignCurrencyEnabled = true;
}
// this also forces the currency_code on ALL entries.
@@ -365,19 +254,19 @@ let transactions = function () {
}
},
filterNativeCurrencies(code) {
console.log('filterNativeCurrencies("' + code + '")');
let list = [];
let currency;
for (let i in this.enabledCurrencies) {
if (this.enabledCurrencies.hasOwnProperty(i)) {
let current = this.enabledCurrencies[i];
for (let i in this.formData.enabledCurrencies) {
if (this.formData.enabledCurrencies.hasOwnProperty(i)) {
let current = this.formData.enabledCurrencies[i];
if (current.code === code) {
currency = current;
}
}
}
list.push(currency);
this.nativeCurrencies = list;
this.formData.nativeCurrencies = list;
// this also forces the currency_code on ALL entries.
for (let i in this.entries) {
if (this.entries.hasOwnProperty(i)) {
@@ -388,164 +277,71 @@ let transactions = function () {
changedAmount(e) {
const index = parseInt(e.target.dataset.index);
this.entries[index].amount = parseFloat(e.target.value);
this.totalAmount = 0;
this.groupProperties.totalAmount = 0;
for (let i in this.entries) {
if (this.entries.hasOwnProperty(i)) {
this.totalAmount = this.totalAmount + parseFloat(this.entries[i].amount);
this.groupProperties.totalAmount = this.groupProperties.totalAmount + parseFloat(this.entries[i].amount);
}
}
console.log('Changed amount to ' + this.totalAmount);
},
selectDestAccount(item, ac) {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
document.querySelector('#form')._x_dataStack[0].$data.entries[index].destination_account = {
id: item.id,
name: item.name,
alpine_name: item.name,
type: item.type,
currency_code: item.currency_code,
};
console.log('Changed destination account into a known ' + item.type.toLowerCase());
document.querySelector('#form')._x_dataStack[0].detectTransactionType();
},
changeSourceAccount(item, ac) {
console.log('changeSourceAccount');
if (typeof item === 'undefined') {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
let source = document.querySelector('#form')._x_dataStack[0].$data.entries[index].source_account;
if (source.name === ac._searchInput.value) {
console.warn('Ignore hallucinated source account name change to "' + ac._searchInput.value + '"');
document.querySelector('#form')._x_dataStack[0].detectTransactionType();
return;
}
document.querySelector('#form')._x_dataStack[0].$data.entries[index].source_account = {
name: ac._searchInput.value, alpine_name: ac._searchInput.value,
};
console.log('Changed source account into a unknown account called "' + ac._searchInput.value + '"');
document.querySelector('#form')._x_dataStack[0].detectTransactionType();
}
},
changeDestAccount(item, ac) {
let destination = document.querySelector('#form')._x_dataStack[0].$data.entries[0].destination_account;
if (typeof item === 'undefined') {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
let destination = document.querySelector('#form')._x_dataStack[0].$data.entries[index].destination_account;
if (destination.name === ac._searchInput.value) {
console.warn('Ignore hallucinated destination account name change to "' + ac._searchInput.value + '"');
document.querySelector('#form')._x_dataStack[0].detectTransactionType();
return;
}
document.querySelector('#form')._x_dataStack[0].$data.entries[index].destination_account = {
name: ac._searchInput.value, alpine_name: ac._searchInput.value,
};
console.log('Changed destination account into a unknown account called "' + ac._searchInput.value + '"');
document.querySelector('#form')._x_dataStack[0].detectTransactionType();
}
},
changeCategory(item, ac) {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
if (typeof item !== 'undefined' && item.name) {
//this.entries[0].category_name = object.name;
document.querySelector('#form')._x_dataStack[0].$data.entries[index].category_name = item.name;
return;
}
document.querySelector('#form')._x_dataStack[0].$data.entries[index].category_name = ac._searchInput.value;
},
changeDescription(item, ac) {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
if (typeof item !== 'undefined' && item.description) {
//this.entries[0].category_name = object.name;
document.querySelector('#form')._x_dataStack[0].$data.entries[index].description = item.description;
return;
}
document.querySelector('#form')._x_dataStack[0].$data.entries[index].description = ac._searchInput.value;
},
addedSplit() {
console.log('addedSplit');
// TODO improve code location
Autocomplete.init("input.ac-source", {
server: urls.account,
serverParams: {
types: this.filters.source,
},
fetchOptions: {
headers: {
'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content
}
},
hiddenInput: true,
preventBrowserAutocomplete: true,
highlightTyped: true,
liveServer: true,
onChange: this.changeSourceAccount,
onSelectItem: this.selectSourceAccount,
onRenderItem: function (item, b, c) {
// addedSplit, is called from the HTML
// for source account
const renderAccount = function (item, b, c) {
return item.name_with_balance + '<br><small class="text-muted">' + i18n.t('firefly.account_type_' + item.type) + '</small>';
}
};
console.log(this.filters);
addAutocomplete({
selector: 'input.ac-source',
serverUrl: urls.account,
filters: this.filters.source,
onRenderItem: renderAccount,
onChange: changeSourceAccount,
onSelectItem: selectSourceAccount
});
Autocomplete.init("input.ac-category", {
server: urls.category,
fetchOptions: {
headers: {
'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content
}
},
valueField: "id",
labelField: "name",
highlightTyped: true,
onSelectItem: this.changeCategory,
onChange: this.changeCategory,
addAutocomplete({
selector: 'input.ac-dest',
serverUrl: urls.account,
filters: this.filters.destination,
onRenderItem: renderAccount,
onChange: changeDestinationAccount,
onSelectItem: selectDestinationAccount
});
Autocomplete.init("input.ac-dest", {
server: urls.account,
serverParams: {
types: this.filters.destination,
},
fetchOptions: {
headers: {
'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content
}
},
hiddenInput: true,
preventBrowserAutocomplete: true,
liveServer: true,
highlightTyped: true,
onSelectItem: this.selectDestAccount,
onChange: this.changeDestAccount,
onRenderItem: function (item, b, c) {
return item.name_with_balance + '<br><small class="text-muted">' + i18n.t('firefly.account_type_' + item.type) + '</small>';
}
addAutocomplete({
selector: 'input.ac-category',
serverUrl: urls.category,
valueField: 'id',
labelField: 'name',
onChange: changeCategory,
onSelectItem: changeCategory
});
this.filters.destination = [];
Autocomplete.init('input.ac-description', {
server: urls.description,
fetchOptions: {
headers: {
'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content
}
},
valueField: "id",
labelField: "description",
highlightTyped: true,
onSelectItem: this.changeDescription,
onChange: this.changeDescription,
addAutocomplete({
selector: 'input.ac-description',
serverUrl: urls.description,
valueField: 'id',
labelField: 'description',
onChange: changeDescription,
onSelectItem: changeDescription,
});
},
processUpload(event) {
console.log('I am ALSO event listener for upload-success!');
console.log(event);
this.showBarOrRedirect();
this.showMessageOrRedirectUser();
},
processUploadError(event) {
this.notifications.success.show = false;
this.notifications.wait.show = false;
this.notifications.error.show = true;
this.formStates.isSubmitting = false;
this.notifications.error.text = i18n.t('firefly.errors_upload');
console.error(event);
},
init() {
// get translations
// TODO loading translations could be better, but do this later.
Promise.all([getVariable('language', 'en_US')]).then((values) => {
i18n = new I18n();
const locale = values[0].replace('-', '_');
@@ -578,8 +374,12 @@ let transactions = function () {
document.addEventListener('upload-success', (event) => {
this.processUpload(event);
document.querySelectorAll("input[type=file]").value = "";
});
document.addEventListener('upload-error', (event) => {
this.processUploadError(event);
});
// source can never be expense account
this.filters.source = ['Asset account', 'Loan', 'Debt', 'Mortgage', 'Revenue account'];
@@ -587,21 +387,35 @@ let transactions = function () {
this.filters.destination = ['Expense account', 'Loan', 'Debt', 'Mortgage', 'Asset account'];
},
submitTransaction() {
// reset all views:
this.submitting = true;
this.showSuccessMessage = false;
this.showErrorMessage = false;
this.showWaitmessage = false;
// reset all messages:
this.notifications.error.show = false;
this.notifications.success.show = false;
this.notifications.wait.show = false;
// reset all errors in the entries array:
for (let i in this.entries) {
if (this.entries.hasOwnProperty(i)) {
this.entries[i].errors = defaultErrorSet();
}
}
// form is now submitting:
this.formStates.isSubmitting = true;
// final check on transaction type.
this.detectTransactionType();
// parse transaction:
let transactions = parseFromEntries(this.entries, this.groupProperties.transactionType);
let submission = {
// todo process all options
group_title: null, fire_webhooks: false, apply_rules: false, transactions: transactions
group_title: this.groupProperties.title,
fire_webhooks: this.formStates.webhooksButton,
apply_rules: this.formStates.rulesButton,
transactions: transactions
};
if (transactions.length > 1) {
// todo improve me
// catch for group title:
if (null === this.groupProperties.title && transactions.length > 1) {
submission.group_title = transactions[0].description;
}
@@ -609,20 +423,25 @@ let transactions = function () {
let poster = new Post();
console.log(submission);
poster.post(submission).then((response) => {
// submission was a success.
this.newGroupId = parseInt(response.data.data.id);
this.newGroupTitle = submission.group_title ?? submission.transactions[0].description
const attachmentCount = uploadAttachments(this.newGroupId, response.data.data.attributes.transactions);
const group = response.data.data;
// submission was a success!
this.groupProperties.id = parseInt(group.id);
this.groupProperties.title = group.attributes.group_title ?? group.attributes.transactions[0].description
// process attachments, if any:
const attachmentCount = processAttachments(this.groupProperties.id, group.attributes.transactions);
// upload transactions? then just show the wait message and do nothing else.
if (attachmentCount > 0) {
this.showWaitMessage = true;
// if count is more than zero, system is processing transactions in the background.
this.notifications.wait.show = true;
this.notifications.wait.text = i18n.t('firefly.wait_attachments');
return;
}
// if not, respond to user options:
this.showBarOrRedirect();
this.showMessageOrRedirectUser();
}).catch((error) => {
this.submitting = false;
console.log(error);
// todo put errors in form
@@ -633,137 +452,67 @@ let transactions = function () {
});
},
showBarOrRedirect() {
this.showWaitMessage = false;
this.submitting = false;
if (this.returnHereButton) {
// todo create success banner
this.showSuccessMessage = true;
this.successMessageLink = 'transactions/show/' + this.newGroupId;
this.successMessageText = i18n.t('firefly.stored_journal_js', {description: this.newGroupTitle});
// todo clear out form if necessary
if (this.resetButton) {
showMessageOrRedirectUser() {
// disable all messages:
this.notifications.error.show = false;
this.notifications.success.show = false;
this.notifications.wait.show = false;
if (this.formStates.returnHereButton) {
this.notifications.success.show = true;
this.notifications.success.url = 'transactions/show/' + this.groupProperties.id;
this.notifications.success.text = i18n.t('firefly.stored_journal_js', {description: this.groupProperties.title});
if (this.formStates.resetButton) {
this.entries = [];
this.addSplit();
this.totalAmount = 0;
this.groupProperties.totalAmount = 0;
}
return;
}
if (!this.returnHereButton) {
window.location = 'transactions/show/' + this.newGroupId + '?transaction_group_id=' + this.newGroupId + '&message=created';
}
window.location = 'transactions/show/' + this.groupProperties.id + '?transaction_group_id=' + this.groupProperties.id + '&message=created';
},
parseErrors(data) {
this.setDefaultErrors();
this.showErrorMessage = true;
this.showSuccessMessage = false;
// todo create error banner.
this.errorMessageText = i18n.t('firefly.errors_submission') + ' ' + data.message;
let transactionIndex;
let fieldName;
// disable all messages:
this.notifications.error.show = true;
this.notifications.success.show = false;
this.notifications.wait.show = false;
this.formStates.isSubmitting = false;
this.notifications.error.text = i18n.t('firefly.errors_submission', {errorMessage: data.message});
// todo add 'was-validated' to form.
console.log('Now processing errors.');
for (const key in data.errors) {
if (data.errors.hasOwnProperty(key)) {
if (key === 'group_title') {
console.log('Handling group title error.');
// todo handle group errors.
//this.group_title_errors = errors.errors[key];
if(data.hasOwnProperty('errors')) {
this.entries = spliceErrorsIntoTransactions(i18n, data.errors, this.entries);
}
if (key !== 'group_title') {
console.log('Handling errors for ' + key);
// lol, the dumbest way to explode "transactions.0.something" ever.
transactionIndex = parseInt(key.split('.')[1]);
fieldName = key.split('.')[2];
console.log('Transaction index: ' + transactionIndex);
console.log('Field name: ' + fieldName);
console.log('Errors');
console.log(data.errors[key]);
// set error in this object thing.
switch (fieldName) {
case 'currency_code':
case 'foreign_currency_code':
case 'category_name':
case 'piggy_bank_id':
case 'notes':
case 'internal_reference':
case 'external_url':
case 'latitude':
case 'longitude':
case 'zoom_level':
case 'interest_date':
case 'book_date':
case 'process_date':
case 'due_date':
case 'payment_date':
case 'invoice_date':
case 'amount':
case 'date':
case 'budget_id':
case 'bill_id':
case 'description':
case 'tags':
this.entries[transactionIndex].errors[fieldName] = data.errors[key];
break;
case 'source_name':
case 'source_id':
this.entries[transactionIndex].errors.source_account = this.entries[transactionIndex].errors.source_account.concat(data.errors[key]);
break;
case 'type':
// put the error in the description:
this.entries[transactionIndex].errors.description = this.entries[transactionIndex].errors.source_account.concat(data.errors[key]);
break;
case 'destination_name':
case 'destination_id':
this.entries[transactionIndex].errors.destination_account = this.entries[transactionIndex].errors.destination_account.concat(data.errors[key]);
break;
case 'foreign_amount':
case 'foreign_currency_id':
this.entries[transactionIndex].errors.foreign_amount = this.entries[transactionIndex].errors.foreign_amount.concat(data.errors[key]);
break;
}
}
// unique some things
if (typeof this.entries[transactionIndex] !== 'undefined') {
this.entries[transactionIndex].errors.source_account = Array.from(new Set(this.entries[transactionIndex].errors.source_account));
this.entries[transactionIndex].errors.destination_account = Array.from(new Set(this.entries[transactionIndex].errors.destination_account));
}
}
}
console.log(this.entries[0].errors);
},
setDefaultErrors() {
},
addSplit() {
this.entries.push(createEmptySplit());
setTimeout(() => {
// render tags:
Tags.init('select.ac-tags', {
allowClear: true,
server: urls.tag,
liveServer: true,
clearEnd: true,
allowNew: true,
notFoundMessage: '(nothing found)',
noCache: true,
fetchOptions: {
headers: {
'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content
}
}
});
const count = this.entries.length - 1;
let map = L.map('location_map_' + count).setView([this.latitude, this.longitude], this.zoomLevel);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap ' + count + '</a>'
}).addTo(map);
map.on('click', this.addPointToMap);
map.on('zoomend', this.saveZoomOfMap);
this.entries[count].map
// setTimeout(() => {
// // render tags:
// Tags.init('select.ac-tags', {
// allowClear: true,
// server: urls.tag,
// liveServer: true,
// clearEnd: true,
// allowNew: true,
// notFoundMessage: '(nothing found)',
// noCache: true,
// fetchOptions: {
// headers: {
// 'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content
// }
// }
// });
// const count = this.entries.length - 1;
// let map = L.map('location_map_' + count).setView([this.latitude, this.longitude], this.zoomLevel);
//
// L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
// maxZoom: 19,
// attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap ' + count + '</a>'
// }).addTo(map);
// map.on('click', this.addPointToMap);
// map.on('zoomend', this.saveZoomOfMap);
// this.entries[count].map
// const id = 'location_map_' + count;
// const map = () => {
@@ -779,7 +528,7 @@ let transactions = function () {
// }
// this.entries[count].map = map();
}, 250);
// }, 250);
},
removeSplit(index) {
@@ -788,9 +537,6 @@ let transactions = function () {
const triggerFirstTabEl = document.querySelector('#split-0-tab')
triggerFirstTabEl.click();
},
formattedTotalAmount() {
return formatMoney(this.totalAmount, 'EUR');
},
clearLocation(e) {
e.preventDefault();
const target = e.currentTarget;
@@ -844,15 +590,6 @@ function loadPage() {
Alpine.start();
}
document.addEventListener('upload-success', (event) => {
console.log('I am event listener for upload-success');
console.log(event);
//Alpine.
});
// <button x-data @click="$dispatch('custom-event', 'Hello World!')">
// wait for load until bootstrapped event is received.
document.addEventListener('firefly-iii-bootstrapped', () => {
console.log('Loaded through event listener.');

View File

@@ -0,0 +1,63 @@
/*
* add-autocomplete.js
* Copyright (c) 2024 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Autocomplete from "bootstrap5-autocomplete";
export function addAutocomplete(options) {
const params = {
server: options.serverUrl,
serverParams: {},
fetchOptions: {
headers: {
'X-CSRF-TOKEN': document.head.querySelector('meta[name="csrf-token"]').content
}
},
hiddenInput: true,
preventBrowserAutocomplete: true,
highlightTyped: true,
liveServer: true,
// onChange: this.changeSourceAccount,
// onSelectItem: this.selectSourceAccount
// onSelectItem: this.changeCategory,
// onChange: this.changeCategory,
};
if (typeof options.filters !== 'undefined' && options.filters.length > 0) {
params.serverParams.types = options.filters;
}
if (typeof options.onRenderItem !== 'undefined' && null !== options.onRenderItem) {
console.log('add on render item');
params.onRenderItem = options.onRenderItem;
}
if (options.valueField) {
params.valueField = options.valueField;
}
if (options.labelField) {
params.labelField = options.labelField;
}
if (options.onSelectItem) {
params.onSelectItem = options.onSelectItem;
}
if (options.onChange) {
params.onChange = options.onChange;
}
Autocomplete.init(options.selector, params);
}

View File

@@ -0,0 +1,85 @@
/*
* autocomplete-functions.js
* Copyright (c) 2024 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export function changeCategory(item, ac) {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
if (typeof item !== 'undefined' && item.name) {
document.querySelector('#form')._x_dataStack[0].$data.entries[index].category_name = item.name;
return;
}
document.querySelector('#form')._x_dataStack[0].$data.entries[index].category_name = ac._searchInput.value;
}
export function changeDescription(item, ac) {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
if (typeof item !== 'undefined' && item.description) {
document.querySelector('#form')._x_dataStack[0].$data.entries[index].description = item.description;
return;
}
document.querySelector('#form')._x_dataStack[0].$data.entries[index].description = ac._searchInput.value;
}
export function changeDestinationAccount(item, ac) {
if (typeof item === 'undefined') {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
let destination = document.querySelector('#form')._x_dataStack[0].$data.entries[index].destination_account;
if (destination.name === ac._searchInput.value) {
console.warn('Ignore hallucinated destination account name change to "' + ac._searchInput.value + '"');
return;
}
document.querySelector('#form')._x_dataStack[0].$data.entries[index].destination_account = {
name: ac._searchInput.value, alpine_name: ac._searchInput.value,
};
document.querySelector('#form')._x_dataStack[0].changedDestinationAccount();
}
}
export function selectDestinationAccount(item, ac) {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
document.querySelector('#form')._x_dataStack[0].$data.entries[index].destination_account = {
id: item.id, name: item.name, alpine_name: item.name, type: item.type, currency_code: item.currency_code,
};
document.querySelector('#form')._x_dataStack[0].changedDestinationAccount();
}
export function changeSourceAccount(item, ac) {
// console.log('changeSourceAccount');
if (typeof item === 'undefined') {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
let source = document.querySelector('#form')._x_dataStack[0].$data.entries[index].source_account;
if (source.name === ac._searchInput.value) {
return;
}
document.querySelector('#form')._x_dataStack[0].$data.entries[index].source_account = {
name: ac._searchInput.value, alpine_name: ac._searchInput.value,
};
document.querySelector('#form')._x_dataStack[0].changedSourceAccount();
}
}
export function selectSourceAccount(item, ac) {
const index = parseInt(ac._searchInput.attributes['data-index'].value);
document.querySelector('#form')._x_dataStack[0].$data.entries[index].source_account = {
id: item.id, name: item.name, alpine_name: item.name, type: item.type, currency_code: item.currency_code,
};
document.querySelector('#form')._x_dataStack[0].changedSourceAccount();
}

View File

@@ -29,6 +29,48 @@ function getAccount() {
};
}
export function defaultErrorSet() {
return {
description: [],
// amount information:
amount: [],
currency_code: [],
foreign_amount: [],
foreign_currency_code: [],
// source and destination
source_account: [],
destination_account: [],
// meta data information:
budget_id: [],
category_name: [],
piggy_bank_id: [],
bill_id: [],
tags: [],
notes: [],
// other meta fields:
internal_reference: [],
external_url: [],
// map
latitude: [],
longitude: [],
zoom_level: [],
// date and time
date: [],
interest_date: [],
book_date: [],
process_date: [],
due_date: [],
payment_date: [],
invoice_date: [],
};
}
export function createEmptySplit() {
let now = new Date();
let formatted = format(now, 'yyyy-MM-dd HH:mm');
@@ -75,44 +117,6 @@ export function createEmptySplit() {
payment_date: '',
invoice_date: '',
errors: {
description: [],
// amount information:
amount: [],
currency_code: [],
foreign_amount: [],
foreign_currency_code: [],
// source and destination
source_account: [],
destination_account: [],
// meta data information:
budget_id: [],
category_name: [],
piggy_bank_id: [],
bill_id: [],
tags: [],
notes: [],
// other meta fields:
internal_reference: [],
external_url: [],
// map
latitude: [],
longitude: [],
zoom_level: [],
// date and time
date: [],
interest_date: [],
book_date: [],
process_date: [],
due_date: [],
payment_date: [],
invoice_date: [],
},
errors: defaultErrorSet(),
};
}

View File

@@ -20,7 +20,6 @@
/**
*
* @param entries
*/
export function parseFromEntries(entries, transactionType) {
let returnArray = [];
@@ -71,8 +70,6 @@ export function parseFromEntries(entries, transactionType) {
current.zoom_level = entry.zoomLevel;
}
// if foreign amount currency code is set:
if (typeof entry.foreign_currency_code !== 'undefined' && '' !== entry.foreign_currency_code.toString()) {
current.foreign_currency_code = entry.foreign_currency_code;
@@ -93,7 +90,6 @@ export function parseFromEntries(entries, transactionType) {
current.destination_id = entry.destination_account.id;
}
// TODO transaction type is hard coded:
current.type = transactionType;

View File

@@ -0,0 +1,114 @@
/*
* process-attachments.js
* Copyright (c) 2024 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import AttachmentPost from "../../../api/v1/attachments/post.js";
let uploadFiles = function (fileData) {
let count = fileData.length;
let uploads = 0;
let hasError = false;
for (const key in fileData) {
if (fileData.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294 && false === hasError) {
let poster = new AttachmentPost();
poster.post(fileData[key].name, 'TransactionJournal', fileData[key].journal).then(response => {
let attachmentId = parseInt(response.data.data.id);
poster.upload(attachmentId, fileData[key].content).then(attachmentResponse => {
uploads++;
if (uploads === count) {
const event = new CustomEvent('upload-success', {some: 'details'});
document.dispatchEvent(event);
}
}).catch(error => {
console.error('Could not upload');
console.error(error);
uploads++;
// break right away
const event = new CustomEvent('upload-failed', {error: error});
document.dispatchEvent(event);
hasError = true;
});
}).catch(error => {
console.error('Could not create upload.');
console.error(error);
uploads++;
const event = new CustomEvent('upload-failed', {error: error});
document.dispatchEvent(event);
hasError = true;
});
}
}
}
export function processAttachments(groupId, transactions) {
// reverse list of transactions
transactions = transactions.reverse();
// array of all files to be uploaded:
let toBeUploaded = [];
let count = 0;
// array with all file data.
let fileData = [];
// all attachments
let attachments = document.querySelectorAll('input[name="attachments[]"]');
// loop over all attachments, and add references to this array:
for (const key in attachments) {
if (attachments.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294) {
for (const fileKey in attachments[key].files) {
if (attachments[key].files.hasOwnProperty(fileKey) && /^0$|^[1-9]\d*$/.test(fileKey) && fileKey <= 4294967294) {
// include journal thing.
toBeUploaded.push({
journal: transactions[key].transaction_journal_id,
file: attachments[key].files[fileKey]
});
count++;
}
}
}
}
// loop all uploads. This is async.
for (const key in toBeUploaded) {
if (toBeUploaded.hasOwnProperty(key) && /^0$|^[1-9]\d*$/.test(key) && key <= 4294967294) {
// create file reader thing that will read all of these uploads
(function (f, key) {
let fileReader = new FileReader();
fileReader.onloadend = function (evt) {
if (evt.target.readyState === FileReader.DONE) { // DONE == 2
fileData.push({
name: toBeUploaded[key].file.name,
journal: toBeUploaded[key].journal,
content: new Blob([evt.target.result])
});
if (fileData.length === count) {
uploadFiles(fileData);
}
}
};
fileReader.readAsArrayBuffer(f.file);
})(toBeUploaded[key], key,);
}
}
return count;
}

View File

@@ -0,0 +1,104 @@
/*
* splice-errors-into-transactions.js
* Copyright (c) 2024 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function cleanupErrors(fullName, shortName, errors) {
let newErrors = [];
let message = '';
for (let i in errors) {
if (errors.hasOwnProperty(i)) {
newErrors.push(errors[i].replace(fullName, shortName));
}
}
return newErrors;
}
export function spliceErrorsIntoTransactions(i18n, errors, transactions) {
let transactionIndex;
let fieldName;
let errorArray;
for (const key in errors) {
if (errors.hasOwnProperty(key)) {
if (key === 'group_title') {
console.error('Cannot handle error in group title.');
// todo handle group errors.
//this.group_title_errors = errors.errors[key];
continue;
}
transactionIndex = parseInt(key.split('.')[1]);
fieldName = key.split('.')[2];
errorArray = cleanupErrors(key, fieldName, errors[key]);
if (!transactions.hasOwnProperty(transactionIndex)) {
console.error('Cannot handle errors in index #' + transactionIndex);
continue;
}
switch (fieldName) {
case 'currency_code':
case 'foreign_currency_code':
case 'category_name':
case 'piggy_bank_id':
case 'notes':
case 'internal_reference':
case 'external_url':
case 'latitude':
case 'longitude':
case 'zoom_level':
case 'interest_date':
case 'book_date':
case 'process_date':
case 'due_date':
case 'payment_date':
case 'invoice_date':
case 'amount':
case 'date':
case 'budget_id':
case 'bill_id':
case 'description':
case 'tags':
transactions[transactionIndex].errors[fieldName] = errorArray;
break;
case 'source_name':
case 'source_id':
transactions[transactionIndex].errors.source_account = transactions[transactionIndex].errors.source_account.concat(errorArray);
break;
case 'type':
// add custom error to source and destination account
transactions[transactionIndex].errors.source_account = transactions[transactionIndex].errors.source_account.concat([i18n.t('validation.bad_type_source')]);
transactions[transactionIndex].errors.destination_account = transactions[transactionIndex].errors.destination_account.concat([i18n.t('validation.bad_type_destination')]);
break;
case 'destination_name':
case 'destination_id':
transactions[transactionIndex].errors.destination_account = transactions[transactionIndex].errors.destination_account.concat(errorArray);
break;
case 'foreign_amount':
case 'foreign_currency_id':
transactions[transactionIndex].errors.foreign_amount = transactions[transactionIndex].errors.foreign_amount.concat(errorArray);
break;
}
// unique some errors.
if (typeof transactions[transactionIndex] !== 'undefined') {
transactions[transactionIndex].errors.source_account = Array.from(new Set(transactions[transactionIndex].errors.source_account));
transactions[transactionIndex].errors.destination_account = Array.from(new Set(transactions[transactionIndex].errors.destination_account));
}
}
}
console.log(transactions[0].errors);
return transactions;
}

View File

@@ -1897,6 +1897,7 @@ return [
// transactions:
'wait_attachments' => 'Please wait for the attachments to upload.',
'errors_upload' => 'The upload has failed. Please check your browser console for the error.',
'amount_foreign_if' => 'Amount in foreign currency, if any',
'amount_destination_account' => 'Amount in the currency of the destination account',
'edit_transaction_title' => 'Edit transaction ":description"',
@@ -2469,7 +2470,7 @@ return [
'after_update_create_another' => 'After updating, return here to continue editing.',
'store_as_new' => 'Store as a new transaction instead of updating.',
'reset_after' => 'Reset form after submission',
'errors_submission' => 'There was something wrong with your submission. Please check out the errors.',
'errors_submission' => 'There was something wrong with your submission. Please check out the errors below: %{errorMessage}',
'transaction_expand_split' => 'Expand split',
'transaction_collapse_split' => 'Collapse split',

View File

@@ -25,6 +25,8 @@
declare(strict_types=1);
return [
'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.',
'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.',
'missing_where' => 'Array is missing "where"-clause',
'missing_update' => 'Array is missing "update"-clause',
'invalid_where_key' => 'JSON contains an invalid key for the "where"-clause',

View File

@@ -1,21 +1,32 @@
<div class="row mb-2">
<div class="col">
<template x-if="showSuccessMessage">
<template x-if="notifications.success.show">
<div class="alert alert-success alert-dismissible fade show" role="alert">
<a :href="successMessageLink" class="alert-link" x-text="successMessageText"></a>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<template x-if="notifications.success.url != ''">
<a :href="notifications.success.url" class="alert-link" x-text="notifications.success.text"></a>
</template>
<template x-if="notifications.success.url == ''">
<span x-text="notifications.success.text"></span>
</template>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('firefly.close') }}"></button>
</div>
</template>
<template x-if="showErrorMessage">
<div class="alert alert-danger alert-dismissible fade show" role="alert"
x-text="errorMessageText">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<template x-if="notifications.error.show">
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<template x-if="notifications.error.url != ''">
<a :href="notifications.error.url" class="alert-link" x-text="notifications.error.text"></a>
</template>
<template x-if="notifications.error.url == ''">
<span x-text="notifications.error.text"></span>
</template>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('firefly.close') }}"></button>
</div>
</template>
<template x-if="showWaitMessage">
<template x-if="notifications.wait.show">
<div class="alert alert-info alert-dismissible fade show" role="alert">
<em class="fa-solid fa-spinner fa-spin"></em> Please wait for the attachments to upload.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<em class="fa-solid fa-spinner fa-spin"></em>
<span x-text="notifications.wait.text"></span>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{{ __('firefly.close') }}"></button>
</div>
</template>
</div>

View File

@@ -109,7 +109,7 @@
<div class="card-footer">
<div class="row">
<div class="col text-end">
<button class="btn btn-success" :disabled="submitting" @click="submitTransaction()">Submit</button>
<button class="btn btn-success" :disabled="formStates.isSubmitting" @click="submitTransaction()">Submit</button>
</div>
</div>
</div>
@@ -117,11 +117,11 @@
</div>
<div class="col-12">
<template x-if="0 !== index">
<button :disabled="submitting" class="btn btn-danger" @click="removeSplit(index)">
<button :disabled="formStates.isSubmitting" class="btn btn-danger" @click="removeSplit(index)">
Remove this split
</button>
</template>
<button class="btn btn-info" :disabled="submitting">Add another split</button>
<button class="btn btn-info" :disabled="formStates.isSubmitting">Add another split</button>
</div>
</div>
</div>

View File

@@ -9,7 +9,7 @@
<template x-if="!formStates.loadingCurrencies">
<select class="form-control" :id="'currency_code_' + index" x-model="transaction.currency_code">
<template x-for="currency in formData.nativeCurrencies">
<option :selected="currency.id == defaultCurrency.id"
<option :selected="currency.id == formData.defaultCurrency.id"
:label="currency.name" :value="currency.code"
x-text="currency.name"></option>
</template>

View File

@@ -7,7 +7,13 @@
:id="'description_' + index"
@change="changedDescription"
x-model="transaction.description"
:class="{'is-invalid': transaction.errors.description.length > 0, 'form-control': true}"
:data-index="index"
placeholder="{{ __('firefly.description') }}">
<template x-if="transaction.errors.description.length > 0">
<div class="invalid-feedback"
x-text="transaction.errors.description[0]">
</div>
</template>
</div>
</div>

View File

@@ -5,11 +5,16 @@
</label>
<div class="col-sm-10">
<input type="text"
class="form-control ac-dest"
:class="{'is-invalid': transaction.errors.destination_account.length > 0, 'form-control': true, 'ac-dest': true}"
:id="'dest_' + index"
@changed="changedDestinationAccount"
x-model="transaction.destination_account.alpine_name"
:data-index="index"
@changed="changedDestinationAccount"
placeholder="{{ __('firefly.destination_account') }}">
<template x-if="transaction.errors.destination_account.length > 0">
<div class="invalid-feedback"
x-text="transaction.errors.destination_account[0]">
</div>
</template>
</div>
</div>

View File

@@ -5,11 +5,16 @@
</label>
<div class="col-sm-10">
<input type="text"
class="form-control ac-source"
:class="{'is-invalid': transaction.errors.source_account.length > 0, 'form-control': true, 'ac-source': true}"
:id="'source_' + index"
x-model="transaction.source_account.alpine_name"
:data-index="index"
@changed="changedSourceAccount"
placeholder="{{ __('firefly.source_account') }}">
<template x-if="transaction.errors.source_account.length > 0">
<div class="invalid-feedback"
x-text="transaction.errors.source_account[0]">
</div>
</template>
</div>
</div>

View File

@@ -15,7 +15,7 @@
</div>
<div class="row">
<div class="col text-end">
<button class="btn btn-success" :disabled="submitting" @click="submitTransaction()">Submit</button>
<button class="btn btn-success" :disabled="formStates.isSubmitting" @click="submitTransaction()">Submit</button>
</div>
</div>
</div>